* [PATCH v3 1/13] drm/vino: add the DL3 wire framing
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:37 ` [PATCH v3 2/13] drm/vino: add the USB transport Mike Lothian
` (11 subsequent siblings)
12 siblings, 0 replies; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, linux-kernel
Every message a DisplayLink DL3 dock exchanges, plaintext or sealed, sits
inside the same envelope: a length, an identifier, an auxiliary word and a
payload. The identifier is not a free-standing opcode -- on several replies
it doubles as the payload's length -- so decoding it wrongly matches a
reply by luck rather than by meaning.
Add the framing on its own, with no I/O and no crypto, so the layers above
can be read against a capture without carrying the envelope's rules.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/proto.rs | 71 +++++++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
create mode 100644 drivers/gpu/drm/vino/proto.rs
diff --git a/drivers/gpu/drm/vino/proto.rs b/drivers/gpu/drm/vino/proto.rs
new file mode 100644
index 000000000000..f9a70b605135
--- /dev/null
+++ b/drivers/gpu/drm/vino/proto.rs
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The DL3 "universal" wire framing and the plaintext session-init messages (sec 3/sec 4).
+
+use super::*;
+
+/// Append a sec 3-framed message to `out` with an explicit `sub_len_dw`: a 16-byte
+/// little-endian header (`pad(2) | size(2)=total-4 | type(4) | sub_id(2) |
+/// sub_len_dw(2) | seq(4)`) followed by `body`.
+///
+/// HDCP OUT messages carry fixed `sub_len_dw` values that are *not*
+/// `body.len() / 4`, so the framer cannot derive it -- the caller passes it.
+pub(super) fn push_frame_with(
+ out: &mut KVec<u8>,
+ msg_type: u32,
+ sub_id: u16,
+ sub_len_dw: u16,
+ seq: u32,
+ body: &[u8],
+) -> Result {
+ let size = ((16 + body.len()) - 4) as u16;
+ out.extend_from_slice(&[0, 0], GFP_KERNEL)?;
+ out.extend_from_slice(&size.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&msg_type.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&sub_id.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&sub_len_dw.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&seq.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(body, GFP_KERNEL)?;
+ Ok(())
+}
+
+/// `init_25` body (sec 4).
+///
+/// The `sub_len_dw` field is the fixed value zero rather than `body.len() / 4`; the dock rejects
+/// the derived value.
+pub(super) const INIT_25: [u8; 16] = [0x05, 0, 0x08, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+/// `init_4` (Part A) body (sec 4), also framed with `sub_len_dw=0`.
+pub(super) const INIT_4: [u8; 16] = [0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+/// The HDCP-channel probe body in the second part of [`init_4_probe`].
+/// The dock only ACKs once this framed probe arrives.
+pub(super) const PROBE_BODY: [u8; 32] = {
+ let mut p = [0u8; 32];
+ p[0] = 0x14;
+ p[2] = 0x90;
+ p
+};
+
+/// `init_0`: 16-byte framing header only, empty body (sec 4).
+pub(super) fn init_0() -> Result<KVec<u8>> {
+ let mut buf = KVec::with_capacity(16, GFP_KERNEL)?;
+ push_frame_with(&mut buf, 0x01, 0x00, 0, 0, &[])?;
+ Ok(buf)
+}
+
+/// `init_25`: type=2 sub=0x25, `sub_len_dw=0`, 32 bytes total (sec 4).
+pub(super) fn init_25() -> Result<KVec<u8>> {
+ let mut buf = KVec::with_capacity(32, GFP_KERNEL)?;
+ push_frame_with(&mut buf, 0x02, 0x25, 0, 0, &INIT_25)?;
+ Ok(buf)
+}
+
+/// `init_4` + HDCP probe as one 80-byte transfer (sec 4): Part A (type=2 sub=0x04,
+/// `sub_len_dw=0`, 32 B) concatenated with Part B -- the probe framed as type=4
+/// sub=0x04 with `sub_len_dw=0x0a` over the 32-byte [`PROBE_BODY`] (48 B). This
+/// is the message the dock ACKs.
+pub(super) fn init_4_probe() -> Result<KVec<u8>> {
+ let mut buf = KVec::with_capacity(80, GFP_KERNEL)?;
+ push_frame_with(&mut buf, 0x02, 0x04, 0, 0, &INIT_4)?; // Part A
+ push_frame_with(&mut buf, 0x04, 0x04, 0x0a, 0, &PROBE_BODY)?; // Part B (framed probe)
+ Ok(buf)
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH v3 2/13] drm/vino: add the USB transport
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
2026-08-26 16:37 ` [PATCH v3 1/13] drm/vino: add the DL3 wire framing Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:58 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 3/13] drm/vino: add the crypto primitives and the HDCP 2.2 AKE Mike Lothian
` (10 subsequent siblings)
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, linux-kernel
The dock exposes control on a bulk pipe pair, video on one or two further
bulk endpoints, and an interrupt endpoint the dock pushes on. A dock
generation differs in how many of those it has and in whether video shares
the control pipe, so the transport takes the endpoint numbers as data
rather than deciding them.
Add the link: a bound-interface wrapper that opens those pipes, submits
video as a queue of pipelined URBs, and reports a stall as a stall rather
than retrying into a wedged dock.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/usb_link.rs | 250 +++++++++++++++++++++++++++++++
1 file changed, 250 insertions(+)
create mode 100644 drivers/gpu/drm/vino/usb_link.rs
diff --git a/drivers/gpu/drm/vino/usb_link.rs b/drivers/gpu/drm/vino/usb_link.rs
new file mode 100644
index 000000000000..04b9131d05ed
--- /dev/null
+++ b/drivers/gpu/drm/vino/usb_link.rs
@@ -0,0 +1,250 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The dock's USB endpoints, and the I/O handle every transfer goes through.
+//!
+//! [`Endpoints`] is resolved once against interface 0's descriptor and direction/type-checked, so
+//! the rest of the driver names an endpoint by what it is rather than by a bare address. A
+//! [`UsbLink`] can only exist while the device's [`usb::IoWindow`] is open, so a transfer cannot
+//! outlive the binding.
+
+use super::*;
+
+/// The dock's endpoints, resolved once against interface 0's descriptor and validated for
+/// direction and transfer type by [`usb::Interface::endpoint`].
+///
+/// Resolving up front means the rest of the driver names an endpoint by what it *is* rather than
+/// by a bare address, so a bulk-OUT transfer cannot be aimed at the interrupt-IN status endpoint.
+/// The whole set is [`Copy`] (each entry is an address plus a max-packet size), so it can be
+/// carried by value in a [`UsbLink`] rather than borrowed.
+#[derive(Clone, Copy)]
+pub(crate) struct Endpoints {
+ /// EP02: host->dock control-plane bulk writes.
+ pub(crate) ctrl_out: usb::Endpoint<usb::BulkOut>,
+ /// EP84: dock->host control-plane bulk replies.
+ pub(crate) ctrl_in: usb::Endpoint<usb::BulkIn>,
+ /// Per-connector video bulk-OUT endpoints, from [`DockProfile::video_endpoints`].
+ pub(crate) video: [usb::Endpoint<usb::BulkOut>; drm_sink::MAX_CONNECTORS],
+}
+
+impl Endpoints {
+ /// Resolves every endpoint the driver uses against `intf`'s active alternate setting, and
+ /// returns how many connectors the device actually backs.
+ ///
+ /// The control endpoints and the first video endpoint are required. The rest are not: a dock
+ /// in a known family with fewer outputs simply does not expose the later ones, and the count
+ /// that does resolve is the connector count. Taking it from the device is the difference
+ /// between supporting such a dock and refusing to bind to it, because a profile can only
+ /// describe the variants someone has tested. Navarro repeats addresses across connectors
+ /// (0/2 share `0x08`, 1/3 share `0x0a`), so this counts entries rather than distinct
+ /// endpoints, and the profile's own count remains the upper bound -- four resolvable entries
+ /// on a two-connector Ridge dock still means two connectors.
+ pub(crate) fn resolve<Ctx: device::DeviceContext>(
+ intf: &usb::Interface<Ctx>,
+ profile: &DockProfile,
+ ) -> Result<(Self, u8)> {
+ let mut video = [intf.endpoint::<usb::BulkOut>(profile.topology.video_endpoints[0])?;
+ drm_sink::MAX_CONNECTORS];
+ let mut connectors = 1u8;
+ for (slot, addr) in video
+ .iter_mut()
+ .zip(profile.topology.video_endpoints)
+ .skip(1)
+ {
+ let Ok(ep) = intf.endpoint::<usb::BulkOut>(addr) else {
+ break;
+ };
+ *slot = ep;
+ connectors += 1;
+ }
+
+ Ok((
+ Self {
+ ctrl_out: intf.endpoint::<usb::BulkOut>(EP_CTRL_OUT)?,
+ ctrl_in: intf.endpoint::<usb::BulkIn>(EP_CTRL_IN)?,
+ video,
+ },
+ connectors.min(profile.topology.connectors),
+ ))
+ }
+}
+
+/// A live USB transfer handle: an [`usb::Io`] token proving I/O is currently permitted, plus the
+/// resolved [`Endpoints`].
+///
+/// Obtaining one requires the device's [`usb::IoWindow`] to still be open, so a transfer cannot be
+/// issued after `disconnect()` has closed it. Carrying the endpoints in the handle also keeps raw
+/// endpoint addresses out of transfer call sites.
+pub(crate) struct UsbLink<'a> {
+ window: &'a Arc<usb::IoWindow>,
+ io: usb::Io<'a>,
+ pub(crate) endpoints: Endpoints,
+}
+
+impl<'a> UsbLink<'a> {
+ /// Opens a link on `window`, failing with `ENODEV` once the window has been closed.
+ pub(crate) fn open(window: &'a Arc<usb::IoWindow>, endpoints: Endpoints) -> Result<Self> {
+ Ok(Self {
+ io: window.enter()?,
+ window,
+ endpoints,
+ })
+ }
+
+ /// Opens the persistent EP84 control-plane reader.
+ pub(crate) fn ctrl_in_queue(&self, depth: usize, buf_len: usize) -> Result<usb::BulkInQueue> {
+ usb::BulkInQueue::new(
+ self.window,
+ &self.io,
+ &self.endpoints.ctrl_in,
+ depth,
+ buf_len,
+ )
+ }
+
+ /// Opens the pipelined EP02 control-plane writer.
+ pub(crate) fn ctrl_out_queue(&self, depth: usize, buf_len: usize) -> Result<usb::BulkOutQueue> {
+ usb::BulkOutQueue::new(
+ self.window,
+ &self.io,
+ &self.endpoints.ctrl_out,
+ depth,
+ buf_len,
+ )
+ }
+
+ /// Returns the canonical queue slot for `connector`'s physical video endpoint.
+ ///
+ /// A dock profile may repeat an endpoint address for multiple physical connectors (Navarro
+ /// uses EP08 for connectors 0/2 and EP0a for 1/3). Those connectors must share one
+ /// persistent queue: separate queues could submit interleaved URBs to the same pipe.
+ pub(crate) fn video_pipe_index(&self, connector: usize) -> Result<usize> {
+ let endpoint = self.endpoints.video.get(connector).ok_or(EINVAL)?;
+ self.endpoints
+ .video
+ .iter()
+ .position(|candidate| candidate.address() == endpoint.address())
+ .ok_or(EINVAL)
+ }
+
+ /// Opens the pipelined video writer for `connector`'s physical video endpoint.
+ pub(crate) fn video_queue(
+ &self,
+ connector: usize,
+ depth: usize,
+ buf_len: usize,
+ ) -> Result<usb::BulkOutQueue> {
+ let ep = self.endpoints.video.get(connector).ok_or(EINVAL)?;
+ usb::BulkOutQueue::new(self.window, &self.io, ep, depth, buf_len)
+ }
+
+ /// The underlying I/O token, for the paths that open a persistent queue.
+ pub(crate) fn io(&self) -> &usb::Io<'a> {
+ &self.io
+ }
+
+ /// Writes a control-plane message to EP02.
+ pub(crate) fn ctrl_send(&self, data: &[u8], timeout: Delta, gfp: Flags) -> Result<usize> {
+ self.io
+ .bulk_send(&self.endpoints.ctrl_out, data, timeout, gfp)
+ }
+
+ /// Reads a control-plane reply from EP84.
+ pub(crate) fn ctrl_recv(&self, data: &mut [u8], timeout: Delta, gfp: Flags) -> Result<usize> {
+ self.io
+ .bulk_recv(&self.endpoints.ctrl_in, data, timeout, gfp)
+ }
+
+ /// Reads the standard two-byte status word for a video endpoint.
+ ///
+ /// Bit zero is `ENDPOINT_HALT`. This is deliberately read-only: it distinguishes a dock
+ /// which has stalled the pipe from one which is merely returning NRDY without perturbing the
+ /// endpoint's host or device sequence state.
+ pub(crate) fn video_endpoint_status(&self, connector: usize) -> Result<u16> {
+ let address = self.endpoints.video.get(connector).ok_or(EINVAL)?.address();
+ let mut status = [0u8; 2];
+ self.control_recv(
+ 0x00, // USB_REQ_GET_STATUS
+ 0x82, // device-to-host, standard, endpoint
+ 0,
+ u16::from(address),
+ &mut status,
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ Ok(u16::from_le_bytes(status))
+ }
+
+ /// Sends the standard endpoint-halt clear without running Linux's host endpoint reset helper.
+ ///
+ /// Navarro uses this only during cold setup, before any video URB has touched the endpoint, so
+ /// the host sequence state is already its enumeration value. The working DLM wire transaction
+ /// is two raw `CLEAR_FEATURE(ENDPOINT_HALT)` requests 12.647 ms apart followed by its vendor
+ /// commit 143 us later. `usb_clear_halt()` adds an xHCI endpoint reset after the request; that
+ /// made each Vino operation return about 18 ms late and changed this transaction's ordering.
+ pub(crate) fn clear_video_halt_wire(&self, connector: usize) -> Result {
+ let address = self.endpoints.video.get(connector).ok_or(EINVAL)?.address();
+ self.control_send(
+ 0x01, // USB_REQ_CLEAR_FEATURE
+ 0x02, // host-to-device, standard, endpoint
+ 0, // USB_ENDPOINT_HALT
+ u16::from(address),
+ &[],
+ timeout(),
+ GFP_KERNEL,
+ )
+ }
+
+ /// Recover a video endpoint the dock has stalled.
+ ///
+ /// Unlike [`Self::clear_video_halt_wire`], this runs the full `usb_clear_halt()`, which resets
+ /// the host side's data toggle as well as sending `CLEAR_FEATURE`. That reset is exactly what
+ /// a real stall needs and what the cold-setup path deliberately avoids: after a stall the host
+ /// and device sequence state have diverged, and sending the bare request leaves every later
+ /// transfer failing.
+ ///
+ /// Nothing else clears a stalled pipe, so without this one stall is permanent -- and on a dock
+ /// whose video shares the control endpoint it takes the control plane down with it.
+ pub(crate) fn clear_video_halt(&self, connector: usize) -> Result {
+ let ep = self.endpoints.video.get(connector).ok_or(EINVAL)?;
+ self.io.clear_halt(ep)
+ }
+
+ /// Issues a control OUT transfer on EP0.
+ pub(crate) fn control_send(
+ &self,
+ request: u8,
+ request_type: u8,
+ value: u16,
+ index: u16,
+ data: &[u8],
+ timeout: Delta,
+ gfp: Flags,
+ ) -> Result {
+ self.io
+ .control_send(request, request_type, value, index, data, timeout, gfp)
+ }
+
+ /// Issues a control IN transfer on EP0.
+ pub(crate) fn control_recv(
+ &self,
+ request: u8,
+ request_type: u8,
+ value: u16,
+ index: u16,
+ data: &mut [u8],
+ timeout: Delta,
+ gfp: Flags,
+ ) -> Result {
+ self.io
+ .control_recv(request, request_type, value, index, data, timeout, gfp)
+ }
+
+ /// Selects an alternate setting on the driver's own interface.
+ pub(crate) fn set_alternate_setting(&self, alternate: u8) -> Result {
+ self.io.set_alternate_setting(alternate)
+ }
+}
+/// EP84 (dock-to-host) transfer size.
+///
+/// Replies larger than one transfer are delivered as consecutive fragments.
+pub(crate) const EP84_BUF: usize = 4096;
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 2/13] drm/vino: add the USB transport
2026-08-26 16:37 ` [PATCH v3 2/13] drm/vino: add the USB transport Mike Lothian
@ 2026-08-26 16:58 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 16:58 UTC (permalink / raw)
To: Mike Lothian; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The commit message and driver documentation claim support for an interrupt endpoint, but it is never opened or exposed, leading to a meaningful mismatch and potential device wedging.
--
commit a40630ab675c3654c37821d57cc77e281c7a9a17
Author: Mike Lothian <mike@fireburn.co.uk>
drm/vino: add the USB transport
This commit introduces the USB transport for the dock. It adds a wrapper
to manage endpoint resolution and provides functions to handle control
and video data pipelines, as well as endpoint stall recovery.
> diff --git a/drivers/gpu/drm/vino/usb_link.rs b/drivers/gpu/drm/vino/usb_link.rs
> new file mode 100644
> index 0000000000000..04b9131d05ed9
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/usb_link.rs
[ ... ]
> +#[derive(Clone, Copy)]
> +pub(crate) struct Endpoints {
> + /// EP02: host->dock control-plane bulk writes.
> + pub(crate) ctrl_out: usb::Endpoint<usb::BulkOut>,
> + /// EP84: dock->host control-plane bulk replies.
> + pub(crate) ctrl_in: usb::Endpoint<usb::BulkIn>,
> + /// Per-connector video bulk-OUT endpoints, from [`DockProfile::video_endpoints`].
> + pub(crate) video: [usb::Endpoint<usb::BulkOut>; drm_sink::MAX_CONNECTORS],
> +}
[Severity: Medium]
The commit message mentions an interrupt endpoint the dock pushes on and
states that this wrapper opens those pipes. Does the Endpoints struct omit
the interrupt endpoint entirely?
If the dock hardware requires its interrupt endpoint to be serviced to prevent
its internal FIFO from filling up, could ignoring it wedge the dock?
[ ... ]
> + pub(crate) fn resolve<Ctx: device::DeviceContext>(
> + intf: &usb::Interface<Ctx>,
> + profile: &DockProfile,
> + ) -> Result<(Self, u8)> {
[ ... ]
> + Ok((
> + Self {
> + ctrl_out: intf.endpoint::<usb::BulkOut>(EP_CTRL_OUT)?,
> + ctrl_in: intf.endpoint::<usb::BulkIn>(EP_CTRL_IN)?,
> + video,
> + },
> + connectors.min(profile.topology.connectors),
> + ))
> + }
[Severity: Medium]
In Endpoints::resolve(), the endpoint resolution logic does not seem to parse
or open the interrupt endpoint. Does this contradict the commit message claim
that the wrapper opens those pipes?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=2
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 3/13] drm/vino: add the crypto primitives and the HDCP 2.2 AKE
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
2026-08-26 16:37 ` [PATCH v3 1/13] drm/vino: add the DL3 wire framing Mike Lothian
2026-08-26 16:37 ` [PATCH v3 2/13] drm/vino: add the USB transport Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:37 ` [PATCH v3 4/13] drm/vino: add the encrypted control plane Mike Lothian
` (9 subsequent siblings)
12 siblings, 0 replies; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, linux-kernel
A DL3 dock authenticates as an HDCP 2.2 receiver before it will accept any
configuration, and every later control message is sealed with keys the
authentication establishes. The KDFs are the published HDCP 2.2 ones; what
is not published is that the dock whitens the session key with a fixed
device constant, and that the two nonces derived from the delivered random
IV differ by one bit each and are not interchangeable.
Add the AES, CMAC and RNG wrappers, the HDCP key hierarchy documented once
in one place, and the AKE itself, with the KUnit known-answer vectors that
pin them.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/ake.rs | 175 +++++++++++++++++++++++++++++++++
drivers/gpu/drm/vino/crypto.rs | 90 +++++++++++++++++
drivers/gpu/drm/vino/hdcp.rs | 146 +++++++++++++++++++++++++++
drivers/gpu/drm/vino/rng.rs | 9 ++
4 files changed, 420 insertions(+)
create mode 100644 drivers/gpu/drm/vino/ake.rs
create mode 100644 drivers/gpu/drm/vino/crypto.rs
create mode 100644 drivers/gpu/drm/vino/hdcp.rs
create mode 100644 drivers/gpu/drm/vino/rng.rs
diff --git a/drivers/gpu/drm/vino/ake.rs b/drivers/gpu/drm/vino/ake.rs
new file mode 100644
index 000000000000..54d24ca2d096
--- /dev/null
+++ b/drivers/gpu/drm/vino/ake.rs
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! HDCP 2.2 AKE wire layer: byte-exact message builders for the state machine.
+//! The dock validates fixed per-message `sub_size` and `sub_len_dw` values, so
+//! the vendor framing records those values explicitly rather than deriving them.
+//!
+//! OUT body layout (sec 5.1), after the 16-byte sec 3 transport header:
+//! ```text
+//! body[0..2] u16 sub_size (fixed per message)
+//! body[2..4] u16 = 0x0010
+//! body[4..8] u32 hdcp_seq increments 1..7 across the AKE OUT messages
+//! body[8..22] 14 zero bytes
+//! body[22..26] u32 = 0x00000030 marker
+//! body[26] u8 = 0x00 flag
+//! body[27] u8 = msg_id
+//! body[28..] HDCP payload (zero-padded to the fixed body length)
+//! ```
+
+use super::*;
+
+/// HDCP 2.2 message IDs (sec 5.3). `pub(crate)` so the AKE state machine
+/// ([`super::VinoDriver::run_ake`]) can match on the response IDs too.
+pub(crate) mod id {
+ use kernel::drm::display::hdcp::MessageId;
+
+ // Standard HDCP 2.2 message IDs: reuse the canonical values from
+ // `<drm/display/drm_hdcp.h>` rather than redefining them, so vino stays in
+ // lockstep with the kernel's HDCP definitions. Only the transport framing
+ // around these (the DisplayLink type/sub/ctr header) is vino-specific.
+ pub(crate) const AKE_INIT: u8 = MessageId::AKE_INIT.as_u8();
+ pub(crate) const AKE_SEND_CERT: u8 = MessageId::AKE_SEND_CERT.as_u8();
+ pub(crate) const AKE_NO_STORED_KM: u8 = MessageId::AKE_NO_STORED_KM.as_u8();
+ pub(crate) const AKE_SEND_H_PRIME: u8 = MessageId::AKE_SEND_H_PRIME.as_u8();
+ pub(crate) const LC_INIT: u8 = MessageId::LC_INIT.as_u8();
+ pub(crate) const LC_SEND_L_PRIME: u8 = MessageId::LC_SEND_L_PRIME.as_u8();
+ pub(crate) const SKE_SEND_EKS: u8 = MessageId::SKE_SEND_EKS.as_u8();
+ pub(crate) const REPEATERAUTH_SEND_RECEIVERID_LIST: u8 =
+ MessageId::REPEATERAUTH_SEND_RECEIVERID_LIST.as_u8();
+ pub(crate) const REPEATERAUTH_SEND_ACK: u8 = MessageId::REPEATERAUTH_SEND_ACK.as_u8();
+ pub(crate) const REPEATERAUTH_STREAM_MANAGE: u8 = MessageId::REPEATERAUTH_STREAM_MANAGE.as_u8();
+ pub(crate) const REPEATERAUTH_STREAM_READY: u8 = MessageId::REPEATERAUTH_STREAM_READY.as_u8();
+
+ // DisplayLink-specific message IDs with no `<drm/display/drm_hdcp.h>` equivalent
+ // (the AKE_Send_rrx split and the transmitter/receiver-info + auth-status messages
+ // the DL3 dock uses), kept as literals.
+ pub(crate) const AKE_SEND_RRX: u8 = 0x06;
+ pub(crate) const RECEIVER_AUTH_STATUS: u8 = 0x12;
+ pub(crate) const AKE_TRANSMITTER_INFO: u8 = 0x13;
+}
+
+/// transport `sub_id` for HDCP OUT messages (type=4 sub=0x04, sec 5.1).
+const SUB_HDCP: u16 = 0x04;
+
+/// Allocate a `body_len`-byte zeroed body with the sec 5.1 header filled in
+/// (`sub_size`, the `0x0010` marker, `hdcp_seq`, the `0x30` marker and `msg_id`).
+/// The caller writes the payload into `body[28..]`.
+fn body(body_len: usize, sub_size: u16, hdcp_seq: u32, msg_id: u8) -> Result<KVec<u8>> {
+ let mut b = KVec::from_elem(0u8, body_len, GFP_KERNEL)?;
+ b[0..2].copy_from_slice(&sub_size.to_le_bytes());
+ b[2..4].copy_from_slice(&0x0010u16.to_le_bytes());
+ b[4..8].copy_from_slice(&hdcp_seq.to_le_bytes());
+ b[22..26].copy_from_slice(&0x0000_0030u32.to_le_bytes());
+ b[27] = msg_id;
+ Ok(b)
+}
+
+/// Wrap a finished HDCP body in the vendor transport header (type=4 sub=0x04)
+/// with the message's fixed `sub_len_dw` and transport `seq`.
+fn wrap(sub_len_dw: u16, seq: u32, body: &[u8]) -> Result<KVec<u8>> {
+ let mut frame = KVec::with_capacity(16 + body.len(), GFP_KERNEL)?;
+ proto::push_frame_with(&mut frame, 0x04, SUB_HDCP, sub_len_dw, seq, body)?;
+ Ok(frame)
+}
+
+/// `session-init ACK` (the `id=0x14 sub=0x76` frame).
+///
+/// This precedes `AKE_Init`, so AKE uses `hdcp_seq` 2..8. It is a minimal
+/// `0x14 / 0x76 / hdcp_seq` header: a 32-byte body without a message payload or the
+/// `0x0010 / 0x30 / msg_id` trailer written by [`body`], wrapped with `sub_len_dw=0x0a`. The dock
+/// echoes it as a `msg_id=0` status frame that `recv_hdcp` skips.
+pub(super) fn session_init_ack(hdcp_seq: u32, seq: u32) -> Result<KVec<u8>> {
+ let mut b = KVec::from_elem(0u8, 32, GFP_KERNEL)?;
+ b[0..2].copy_from_slice(&0x0014u16.to_le_bytes());
+ b[2..4].copy_from_slice(&0x0076u16.to_le_bytes());
+ b[4..8].copy_from_slice(&hdcp_seq.to_le_bytes());
+ wrap(0x000a, seq, &b)
+}
+
+/// `AKE_Init` (msg_id 0x02): `rtx[8] || TxCaps[3]`, padded to a 48-byte body
+/// (`sub_size=0x22`, `sub_len_dw=0x0c`).
+pub(super) fn ake_init(
+ hdcp_seq: u32,
+ seq: u32,
+ rtx: &[u8; drm_hdcp::RTX_LEN],
+ tx_caps: &[u8; 3],
+) -> Result<KVec<u8>> {
+ let mut b = body(48, 0x0022, hdcp_seq, id::AKE_INIT)?;
+ b[28..36].copy_from_slice(rtx);
+ b[36..39].copy_from_slice(tx_caps);
+ wrap(0x000c, seq, &b)
+}
+
+/// `AKE_Transmitter_Info` (msg_id 0x13): byte-exact vendor framing
+/// (`sub_size=0x1f`, `sub_len_dw=0x0f`), payload `00 06 02 00 02`.
+pub(super) fn ake_transmitter_info(hdcp_seq: u32, seq: u32) -> Result<KVec<u8>> {
+ let mut b = body(48, 0x001f, hdcp_seq, id::AKE_TRANSMITTER_INFO)?;
+ b[28..33].copy_from_slice(&[0x00, 0x06, 0x02, 0x00, 0x02]);
+ wrap(0x000f, seq, &b)
+}
+
+/// `AKE_No_Stored_km` (msg_id 0x04): the 128-byte RSA-OAEP-SHA256 `Ekpub(km)`
+/// in a 160-byte body (`sub_size=0x9a`, `sub_len_dw=0x04`).
+pub(super) fn ake_no_stored_km(
+ hdcp_seq: u32,
+ seq: u32,
+ ekpub_km: &[u8; drm_hdcp::ENCRYPTED_MASTER_KEY_LEN],
+) -> Result<KVec<u8>> {
+ let mut b = body(160, 0x009a, hdcp_seq, id::AKE_NO_STORED_KM)?;
+ b[28..156].copy_from_slice(ekpub_km);
+ wrap(0x0004, seq, &b)
+}
+
+/// `LC_Init` (msg_id 0x09): `rn[8]` in a 48-byte body
+/// (`sub_size=0x22`, `sub_len_dw=0x0c`).
+pub(super) fn lc_init(hdcp_seq: u32, seq: u32, rn: &[u8; drm_hdcp::RN_LEN]) -> Result<KVec<u8>> {
+ let mut b = body(48, 0x0022, hdcp_seq, id::LC_INIT)?;
+ b[28..36].copy_from_slice(rn);
+ wrap(0x000c, seq, &b)
+}
+
+/// `SKE_Send_Eks` (msg_id 0x0b): `Edkey(ks)[16] || riv[8]` in a 64-byte body
+/// (`sub_size=0x32`, `sub_len_dw=0x0c`).
+pub(super) fn ske_send_eks(
+ hdcp_seq: u32,
+ seq: u32,
+ edkey_ks: &[u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN],
+ riv: &[u8; drm_hdcp::RIV_LEN],
+) -> Result<KVec<u8>> {
+ let mut b = body(64, 0x0032, hdcp_seq, id::SKE_SEND_EKS)?;
+ b[28..44].copy_from_slice(edkey_ks);
+ b[44..52].copy_from_slice(riv);
+ wrap(0x000c, seq, &b)
+}
+
+/// `RepeaterAuth_Send_ACK` (msg_id 0x0f): the full `V[16]` in a 48-byte body
+/// (`sub_size=0x2a`, `sub_len_dw=0x04`).
+pub(super) fn repeater_auth_send_ack(
+ hdcp_seq: u32,
+ seq: u32,
+ v: &[u8; drm_hdcp::V_PRIME_HALF_LEN],
+) -> Result<KVec<u8>> {
+ let mut b = body(48, 0x002a, hdcp_seq, id::REPEATERAUTH_SEND_ACK)?;
+ b[28..44].copy_from_slice(v);
+ wrap(0x0004, seq, &b)
+}
+
+/// `RepeaterAuth_Stream_Manage` SM2 (msg_id 0x10): `k=2`,
+/// `StreamID_Type[0]=4` and `StreamID_Type[1]=5`.
+pub(super) fn repeater_auth_stream_manage(hdcp_seq: u32, seq: u32) -> Result<KVec<u8>> {
+ let mut b = body(48, 0x002d, hdcp_seq, id::REPEATERAUTH_STREAM_MANAGE)?;
+ b[32..36].copy_from_slice(&[0x02, 0, 0, 0]); // k = 2 (LE)
+ b[36..40].copy_from_slice(&[0x04, 0, 0, 0]); // StreamID_Type[0]
+ b[43] = 0x05; // StreamID_Type[1]
+ wrap(0x0001, seq, &b)
+}
+
+/// Parse an IN HDCP message body (sec 5.2): `body[8]` marker, `body[9]` msg_id,
+/// `body[10..]` payload (for `AKE_Send_Cert`, `body[10]` is a version flag).
+/// Returns `(msg_id, payload)`.
+pub(super) fn parse_in(body: &[u8]) -> Option<(u8, &[u8])> {
+ if body.len() < 10 {
+ return None;
+ }
+ Some((body[9], &body[10..]))
+}
diff --git a/drivers/gpu/drm/vino/crypto.rs b/drivers/gpu/drm/vino/crypto.rs
new file mode 100644
index 000000000000..4fc14b8bd70c
--- /dev/null
+++ b/drivers/gpu/drm/vino/crypto.rs
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Thin adapters onto the shared [`kernel::crypto`] library-crypto bindings, so the
+//! protocol code keeps its `crypto::aes128_ecb` / `crypto::hmac_sha256` call sites.
+
+use super::*;
+
+/// An AES-128 key prepared once for single-block encryption; re-exported so the
+/// AES-CTR keystream loops in [`cp`](super::cp) can expand the key once and reuse
+/// it across every block (rather than re-expanding per block).
+pub(super) use kernel::crypto::Aes128;
+
+/// `AES_ECB(key, block)` -- one 16-byte AES-128 block. Convenience one-shot for
+/// callers that encrypt a single block (e.g. HDCP dKey derivation); the AES-CTR
+/// paths build an [`Aes128`] once and call [`Aes128::encrypt_block`] in a loop.
+pub(super) fn aes128_ecb(key: &[u8; 16], block: &[u8; 16]) -> Result<[u8; 16]> {
+ Ok(Aes128::new(key)?.encrypt_block(block))
+}
+
+/// `HMAC-SHA256(key, data)`.
+pub(super) fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
+ kernel::crypto::hmac_sha256(key, data)
+}
+
+/// `AES-CMAC-128(key, data)` (RFC 4493) via the in-tree AES-CMAC library.
+/// This is DisplayLink's "Dl3Cmac" core -- the CP per-message integrity tag is
+/// `AES_CMAC(ks, nonce8 || BE64(counter) || content)` (see `cp::dl3cmac_tag`).
+pub(super) fn aes_cmac(key: &[u8; 16], data: &[u8]) -> [u8; 16] {
+ kernel::crypto::aes_cmac(key, data)
+}
+
+/// `SHA256(data)`.
+pub(super) fn sha256(data: &[u8]) -> [u8; 32] {
+ kernel::crypto::sha256(data)
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_crypto)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn aes128_ecb_fips197_kat() -> Result {
+ // FIPS-197 / NIST SP800-38A F.1.1 AES-128 ECB known-answer vector.
+ let key = [
+ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
+ 0x4f, 0x3c,
+ ];
+ let plaintext = [
+ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
+ 0x17, 0x2a,
+ ];
+ assert_eq!(
+ aes128_ecb(&key, &plaintext)?,
+ [
+ 0x3a, 0xd7, 0x7b, 0xb4, 0x0d, 0x7a, 0x36, 0x60, 0xa8, 0x9e, 0xca, 0xf3, 0x24, 0x66,
+ 0xef, 0x97,
+ ]
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn aes_cmac_rfc4493_kat() -> Result {
+ // RFC 4493 sec 4 AES-CMAC test vectors (same key as above).
+ let key = [
+ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
+ 0x4f, 0x3c,
+ ];
+ assert_eq!(
+ aes_cmac(&key, &[]),
+ [
+ 0xbb, 0x1d, 0x69, 0x29, 0xe9, 0x59, 0x37, 0x28, 0x7f, 0xa3, 0x7d, 0x12, 0x9b, 0x75,
+ 0x67, 0x46,
+ ]
+ );
+ let msg = [
+ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
+ 0x17, 0x2a,
+ ];
+ assert_eq!(
+ aes_cmac(&key, &msg),
+ [
+ 0x07, 0x0a, 0x16, 0xb4, 0x6b, 0x4d, 0x41, 0x44, 0xf7, 0x9b, 0xdd, 0x9d, 0xd0, 0x4a,
+ 0x28, 0x7c,
+ ]
+ );
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/drm/vino/hdcp.rs b/drivers/gpu/drm/vino/hdcp.rs
new file mode 100644
index 000000000000..5118c9388cc3
--- /dev/null
+++ b/drivers/gpu/drm/vino/hdcp.rs
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! HDCP 2.2 key derivation and verifier computation, built on [`crypto`].
+//!
+//! # The key hierarchy
+//!
+//! Every key below is per-session: they are established once by the AKE and SKE exchanges at
+//! bring-up and then used for the life of the link. Nothing here is per-frame. The names are the
+//! ones the HDCP 2.2 specification uses, so that a reader can follow the spec alongside the code,
+//! and this is the one place that says how they relate.
+//!
+//! ```text
+//! km (16 bytes, host random) AKE master key. Sent to the dock RSA-OAEP encrypted
+//! | under its public key, so only the dock recovers it.
+//! |
+//! +-- dkey_0, dkey_1 (rn = 0) --> kd = dkey_0 || dkey_1 (32 bytes)
+//! | The derived key. Not a content key: it exists to prove
+//! | both ends hold km, via H', L' and V.
+//! |
+//! +-- dkey_2 (rn = SKE nonce)
+//! --> masks ks in transit:
+//! edkey = ks XOR (dkey_2 with its low 8 bytes XOR rrx)
+//!
+//! ks (16 bytes, host random) SKE content session key. Delivered under the mask above,
+//! | never in clear.
+//! |
+//! +-- XOR CP_KEY_WHITEN --> the control-plane seal key (see [`cp`](super::cp))
+//!
+//! riv (8 bytes) Content random IV, delivered beside ks. Nonces for the
+//! control plane are derived from it by fixed byte flips.
+//! ```
+//!
+//! `kd` and `ks` are easy to confuse and are not interchangeable: `kd` is 32 bytes and
+//! authenticates, `ks` is 16 bytes and encrypts. Both come from `km`, by different derivations.
+
+use super::*;
+
+/// `dkey_n = AES_ECB(km with low-8-bytes XOR rn, rtx || (rrx with byte15 XOR n))`
+/// (HDCP 2.2 IIA sec 2.7, sec 5.6). The counter `n` XORs into byte 15 (LSB of the rrx
+/// half) of the IV; `rn` XORs into the low 8 bytes (km[8..16]) of the key -- zero
+/// for the `kd` derivation, the SKE nonce for `dkey_2`.
+fn derive_dkey(
+ km: &[u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN],
+ rn: &[u8; drm_hdcp::RN_LEN],
+ rtx: &[u8; drm_hdcp::RTX_LEN],
+ rrx: &[u8; drm_hdcp::RRX_LEN],
+ n: u8,
+) -> Result<kernel::crypto::Secret<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>> {
+ let mut iv = [0u8; kernel::crypto::AES128_BLOCK_SIZE];
+ iv[..drm_hdcp::RTX_LEN].copy_from_slice(rtx);
+ iv[drm_hdcp::RTX_LEN..].copy_from_slice(rrx);
+ iv[kernel::crypto::AES128_BLOCK_SIZE - 1] ^= n;
+ let mut key = kernel::crypto::Secret::new(*km);
+ for i in 0..drm_hdcp::RN_LEN {
+ key[kernel::crypto::AES128_BLOCK_SIZE - drm_hdcp::RN_LEN + i] ^= rn[i];
+ }
+ Ok(kernel::crypto::Secret::new(crypto::aes128_ecb(&key, &iv)?))
+}
+
+/// `kd = dkey_0 || dkey_1` with `rn = 0` (sec 5.6) -- the 256-bit derived key.
+pub(super) fn derive_kd(
+ km: &[u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN],
+ rtx: &[u8; drm_hdcp::RTX_LEN],
+ rrx: &[u8; drm_hdcp::RRX_LEN],
+) -> Result<kernel::crypto::Secret<32>> {
+ let rn = [0u8; drm_hdcp::RN_LEN];
+ let dkey0 = derive_dkey(km, &rn, rtx, rrx, 0)?;
+ let dkey1 = derive_dkey(km, &rn, rtx, rrx, 1)?;
+ let mut kd = kernel::crypto::Secret::zeroed();
+ kd[..kernel::crypto::AES128_BLOCK_SIZE].copy_from_slice(&dkey0[..]);
+ kd[kernel::crypto::AES128_BLOCK_SIZE..].copy_from_slice(&dkey1[..]);
+ Ok(kd)
+}
+
+/// `H' = HMAC-SHA256(kd, rtx with byte7 ^= repeater)` (sec 5.6).
+pub(super) fn compute_h(
+ kd: &[u8; 32],
+ rtx: &[u8; drm_hdcp::RTX_LEN],
+ repeater: bool,
+) -> [u8; drm_hdcp::H_PRIME_LEN] {
+ let mut msg = *rtx;
+ msg[drm_hdcp::RTX_LEN - 1] ^= repeater as u8;
+ crypto::hmac_sha256(kd, &msg)
+}
+
+/// `L' = HMAC-SHA256(kd with low-8-bytes XOR rrx, rn)` (sec 5.6).
+///
+/// "low-8-bytes" is the *least-significant* 64 bits of the 256-bit `kd`, i.e.
+/// `kd[24..32]`.
+pub(super) fn compute_l(
+ kd: &[u8; 32],
+ rrx: &[u8; drm_hdcp::RRX_LEN],
+ rn: &[u8; drm_hdcp::RN_LEN],
+) -> [u8; drm_hdcp::L_PRIME_LEN] {
+ let mut key = kernel::crypto::Secret::new(*kd);
+ for i in 0..drm_hdcp::RRX_LEN {
+ key[32 - drm_hdcp::RRX_LEN + i] ^= rrx[i];
+ }
+ crypto::hmac_sha256(&key[..], rn)
+}
+
+/// Full `V = HMAC-SHA256(kd, list_header)` (256 bits) for RepeaterAuth (sec 2.3).
+///
+/// The MSB 128 bits (`[..16]`) are `V'` from the receiver ID list. The LSB 128 bits (`[16..]`)
+/// are sent in `RepeaterAuth_Send_Ack`; echoing `V'` there prevents the dock from completing
+/// repeater authentication.
+pub(super) fn compute_v_full(kd: &[u8; 32], list_header: &[u8]) -> [u8; 32] {
+ crypto::hmac_sha256(kd, list_header)
+}
+
+/// RSA-OAEP-SHA256 encrypt the 16-byte master key `km` under the dock's
+/// RSA-1024 public key (`modulus[128]`, `exponent`), giving the 128-byte
+/// `Ekpub(km)` for `AKE_No_Stored_km` (sec 5.4). Generates a fresh OAEP seed.
+pub(super) fn oaep_encrypt_km(
+ key: &mut kernel::crypto::akcipher::RsaPublicKey,
+ km: &[u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN],
+) -> Result<[u8; drm_hdcp::ENCRYPTED_MASTER_KEY_LEN]> {
+ let mut seed = kernel::crypto::Secret::zeroed();
+ super::rng::fill(&mut seed[..]);
+ let mut out = [0u8; drm_hdcp::ENCRYPTED_MASTER_KEY_LEN];
+ key.oaep_sha256_encrypt(km, &seed, &mut out, GFP_KERNEL)?;
+ Ok(out)
+}
+
+/// SKE: `Edkey(ks) = ks XOR (dkey_2 with low-8-bytes XOR rrx)` (sec 5.6).
+///
+/// `dkey_2` is derived with the SKE nonce `rn` mixed into the key; `rrx` then
+/// XORs into the low 8 bytes (`dkey_2[8..16]`) of the mask. The result is the
+/// 16-byte `Edkey_ks` carried by `SKE_Send_Eks` (msg_id 0x0b).
+pub(super) fn compute_eks(
+ km: &[u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN],
+ rtx: &[u8; drm_hdcp::RTX_LEN],
+ rrx: &[u8; drm_hdcp::RRX_LEN],
+ rn: &[u8; drm_hdcp::RN_LEN],
+ ks: &[u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN],
+) -> Result<[u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN]> {
+ let mut mask = derive_dkey(km, rn, rtx, rrx, 2)?;
+ for i in 0..drm_hdcp::RRX_LEN {
+ mask[kernel::crypto::AES128_BLOCK_SIZE - drm_hdcp::RRX_LEN + i] ^= rrx[i];
+ }
+ let mut edkey_ks = [0u8; drm_hdcp::ENCRYPTED_SESSION_KEY_LEN];
+ for i in 0..drm_hdcp::ENCRYPTED_SESSION_KEY_LEN {
+ edkey_ks[i] = ks[i] ^ mask[i];
+ }
+ Ok(edkey_ks)
+}
diff --git a/drivers/gpu/drm/vino/rng.rs b/drivers/gpu/drm/vino/rng.rs
new file mode 100644
index 000000000000..c34766322edd
--- /dev/null
+++ b/drivers/gpu/drm/vino/rng.rs
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Cryptographically-secure randomness for the per-session HDCP nonces/keys
+//! (`rtx`, `km`, `rn`, `ks`, `riv`, the OAEP seed).
+
+/// Fills `buf` with random bytes from the kernel CSPRNG.
+pub(super) fn fill(buf: &mut [u8]) {
+ kernel::random::fill_bytes(buf);
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH v3 4/13] drm/vino: add the encrypted control plane
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (2 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 3/13] drm/vino: add the crypto primitives and the HDCP 2.2 AKE Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:37 ` [PATCH v3 5/13] drm/vino: add the dock profiles Mike Lothian
` (8 subsequent siblings)
12 siblings, 0 replies; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, linux-kernel
Once the AKE completes, the dock takes only sealed messages: AES-CTR under
one nonce, authenticated by a CMAC under another, in host-driven lockstep
with an acknowledgment per message and an authenticated counter that a
message sent out of turn desynchronises for the rest of the session.
Add the sealed message layer and the builders that use it, grouped by
subject: the mode set and its timing derivation, the EDID probe, fetch and
downstream-sink engage, and the cursor. The tails these messages carry are
not padding -- they are HDCP payloads at fixed offsets -- and the KUnit
tests here compare each builder byte for byte against a decrypted capture,
because a malformed message is acknowledged like any other and then simply
never acted on.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
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(())
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH v3 5/13] drm/vino: add the dock profiles
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (3 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 4/13] drm/vino: add the encrypted control plane Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:37 ` [PATCH v3 6/13] drm/vino: add the video codec Mike Lothian
` (7 subsequent siblings)
12 siblings, 0 replies; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, linux-kernel
Three DisplayLink generations are driven here -- Ella (DL-3x00), Ridge
(DL-6xxx) and Navarro (DL-7400) -- and they differ in connector count,
video endpoints, codec geometry, mode ceilings, pacing, and in a dozen
smaller behaviours measured from each vendor stack. None of that belongs in
the code paths that act on it: the driver has one implementation of each
operation and no per-dock branches.
Add the profile: every such difference as data, keyed by device family so
that hardware nobody has tested is still offered to the driver rather than
being invisible to it.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/profile.rs | 1185 +++++++++++++++++++++++++++++++
1 file changed, 1185 insertions(+)
create mode 100644 drivers/gpu/drm/vino/profile.rs
diff --git a/drivers/gpu/drm/vino/profile.rs b/drivers/gpu/drm/vino/profile.rs
new file mode 100644
index 000000000000..43b5d70e02c6
--- /dev/null
+++ b/drivers/gpu/drm/vino/profile.rs
@@ -0,0 +1,1185 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! What distinguishes one DisplayLink dock from another.
+//!
+//! All of it is data -- endpoints, strip geometry, connector count, link limits, and the handful
+//! of sequencing choices that genuinely differ -- which the rest of the driver reads rather than
+//! branches on. Adding a dock is adding a profile, not adding a code path.
+//!
+//! Where two docks need different behaviour, the field says which behaviour and why, never which
+//! model. A predicate named after a product answers the wrong question at every call site: it
+//! cannot be reused by the next dock that happens to share the trait, and it tells a reader
+//! nothing about what the hardware actually wants.
+
+use super::*;
+
+/// How a dock acknowledges each control-plane operation during setup.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum ReplyDiscipline {
+ /// Read the matching authenticated counter and move on as soon as it arrives.
+ ///
+ /// The vendor's reference transaction for this dock is reply-lockstep. A generic drain adds an
+ /// empty 10 ms read after every acknowledgment, which reorders EP0 against EP02.
+ Lockstep,
+ /// Drain the interrupt endpoint until it goes quiet before continuing.
+ Drain,
+}
+
+/// Where a dock wants its video-engine transition within the setup burst.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum VideoCommitPoint {
+ /// After the reply to the status records and before the connector-selecting records.
+ ///
+ /// The working vendor transaction places it at this exact authenticated boundary.
+ BeforeConnectorRecords,
+ /// After the session is finalised.
+ AfterFinalize,
+}
+
+/// How a connector is taken down when it blanks.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum BlankBracket {
+ /// Two stream markers and then silence, holding the bracket open.
+ ///
+ /// The marker pair that opens a mode-change bracket, so the stream is held rather than torn
+ /// down and the wake owes the matching re-open. A dock that wants this re-enumerates about two
+ /// seconds after being sent [`Self::BlackThenClose`] instead, taking the desktop with it.
+ MarkersHeld,
+ /// Present black, close the stream bracket, then power the downstream sink down.
+ ///
+ /// The dock goes on scanning out whatever it last decoded, so the black frames alone leave the
+ /// panel lit on a black image; only the power-down ends the signal.
+ BlackThenClose,
+}
+
+/// How logical framebuffer updates are delivered to a dock's rotating buffers.
+///
+/// These are deliberately independent of [`DockProfile::dock_buffers`]. The ring depth describes
+/// the wire format; it does not say whether repeated presentations within one submission advance
+/// that ring, nor how many later submissions must carry a changed strip. Conflating those facts
+/// made Ella send every ordinary update three times across four logical debt frames.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) struct FrameDelivery {
+ /// Presentations made from one newly owed full-surface keyframe.
+ pub(crate) keyframe_presentations: u8,
+ /// Presentations made from one ordinary damage update.
+ pub(crate) delta_presentations: u8,
+ /// Logical frames for which a changed strip remains selected, including its first frame.
+ pub(crate) damage_frames: u8,
+}
+
+impl FrameDelivery {
+ pub(crate) const fn new(
+ keyframe_presentations: u8,
+ delta_presentations: u8,
+ damage_frames: u8,
+ ) -> Self {
+ Self {
+ keyframe_presentations,
+ delta_presentations,
+ damage_frames,
+ }
+ }
+}
+
+/// Status polls a dock's vendor spaces the stages of its setup burst with.
+///
+/// The polls are `0x14/0x0c`, the same message the keepalive sends, and they are not filler: each
+/// is a round trip, so a stage that is separated by three of them is a stage the host waited for
+/// the dock to acknowledge three times before starting the next. Sending the stages back to back
+/// puts the same records on the wire in the same order and gives the dock none of that time.
+///
+/// Stated as counts rather than delays because that is what the wire shows and what a capture can
+/// check: a delay would have to be re-derived for every dock and could not be pinned by a test.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) struct SetupPolls {
+ /// Between the per-connector engage records and the first stream open.
+ pub(crate) before_stream_opens: u8,
+ /// Between one connector's stream open and the next connector's.
+ pub(crate) between_stream_opens: u8,
+ /// Between the last stream open and the capability queries that follow it.
+ pub(crate) after_stream_opens: u8,
+}
+
+impl SetupPolls {
+ pub(crate) const fn new(before: u8, between: u8, after: u8) -> Self {
+ Self {
+ before_stream_opens: before,
+ between_stream_opens: between,
+ after_stream_opens: after,
+ }
+ }
+
+ /// Polls owed before opening connector `connector`'s stream, of `connectors` total.
+ pub(crate) fn before_open(self, connector: u8) -> u8 {
+ if connector == 0 {
+ self.before_stream_opens
+ } else {
+ self.between_stream_opens
+ }
+ }
+
+ /// A dock whose vendor does not space this burst at all.
+ pub(crate) const NONE: Self = Self::new(0, 0, 0);
+}
+
+/// How much of a dock's endpoint the driver may occupy, and how quickly.
+///
+/// Two numbers, because one does not describe the vendor's behaviour. It bursts a whole frame at
+/// tens of megabytes a second and drives frames milliseconds apart when it has them, so an
+/// instantaneous cap would be wrong; and it never sustains that, so a sustained cap alone is
+/// useless if it lets a second of unspent budget out at once. A dock that shares its endpoint
+/// between pixels and control answers a burst it cannot absorb by halting the endpoint, which
+/// takes the control plane with it.
+///
+/// Stated in bytes rather than as a frame rate because content decides what a frame costs: the
+/// same desktop is four times the size under a photographic wallpaper as under a flat one, and a
+/// frame-rate cap cannot tell those apart.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) struct StreamPacing {
+ /// Sustained bytes per second, averaged over the credit window.
+ pub(crate) bytes_per_sec: u32,
+ /// Most that may leave back to back after an idle period.
+ pub(crate) burst_bytes: u32,
+}
+
+impl StreamPacing {
+ pub(crate) const fn new(bytes_per_sec: u32, burst_bytes: u32) -> Self {
+ Self {
+ bytes_per_sec,
+ burst_bytes,
+ }
+ }
+
+ /// A dock with a video endpoint of its own, where neither limit has been measured.
+ pub(crate) const UNMETERED: Self = Self::new(u32::MAX, u32::MAX);
+
+ /// Whether this dock is metered at all.
+ pub(crate) fn is_metered(self) -> bool {
+ self.bytes_per_sec != u32::MAX
+ }
+}
+
+/// Whether a presence retry may reset one connector's stream bracket while another connector is
+/// lit.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[repr(u8)]
+pub(crate) enum ProbeBracket {
+ /// Assert the target bracket before every probe, as the dedicated-pipe docks already do.
+ Always = 0,
+ /// Defer the assertion while another connector is active on the dock.
+ DeferWithActiveSibling = 1,
+}
+
+impl ProbeBracket {
+ /// Decide without I/O so the safety boundary can be exhaustively unit-tested.
+ pub(crate) fn should_close(self, connector: u8, active_connectors: u32) -> bool {
+ match self {
+ Self::Always => true,
+ Self::DeferWithActiveSibling => {
+ active_connectors & !(1u32 << u32::from(connector)) == 0
+ }
+ }
+ }
+}
+
+/// How a dock's connectors, video pipes and USB endpoints are arranged.
+///
+/// The control plane is identical across every dock here -- bulk OUT `0x02`, bulk IN `0x84`, the
+/// same HDCP and CP sequence -- but the video endpoints are not, so they cannot be a global
+/// constant. The D6000 exposes four video bulk-OUT endpoints and drives its two connectors from
+/// `0x08` and `0x0b`; the DL7400 exposes only two, `0x08` and `0x0a`, so naming `0x0b` there fails
+/// endpoint resolution outright and the device never comes up.
+pub(crate) struct Topology {
+ /// Video bulk-OUT endpoint per physical connector. Navarro deliberately repeats its two
+ /// endpoint addresses: connectors 0/2 share 0x08 and connectors 1/3 share 0x0a.
+ pub(crate) video_endpoints: [u8; drm_sink::MAX_CONNECTORS],
+ /// Whether video records and control messages share one bulk-OUT pipe.
+ ///
+ /// DL-3x00 hardware exposes only `0x02` OUT and `0x84` IN on its display interface, so image
+ /// records and control messages form a single ordered record stream. That makes the control
+ /// plane and the scanout path two writers on one endpoint, which they are not on hardware that
+ /// separates them: a control message submitted while video is queued lands in the middle of a
+ /// record. Submission on such a dock must therefore be serialised across both paths.
+ ///
+ /// This is a submission policy, not an observation about endpoint numbering. A dock that
+ /// happened to place video on `0x02` while keeping a pipe of its own would still not want the
+ /// serialisation, and a dock that shares a pipe at some other address would.
+ pub(crate) video_on_ctrl_pipe: bool,
+ /// Number of downstream connectors the dock answers a presence probe for.
+ ///
+ /// This is the range of the selector at probe byte 22, and it is not the connector count: Ridge
+ /// has two of each, Navarro has four connectors feeding two video endpoints (`0x08` carried
+ /// connectors 0 then 2, `0x0a` carried 1 then 3, measured across cable moves). Connector index
+ /// is the physical socket number minus one.
+ pub(crate) connectors: u8,
+}
+
+/// What a dock can be asked to drive.
+pub(crate) struct Capabilities {
+ /// Highest refresh rate this dock is known to drive, or `u32::MAX` for a dock bounded by its
+ /// link rate alone.
+ ///
+ /// This is a rate limit, not a bandwidth one -- `max_connector_clock_khz` and `pixel_budget`
+ /// carry bandwidth, and on every dock here they are what actually bounds a mode. Reserve this
+ /// for a dock that refuses a refresh its clock and budget both admit.
+ ///
+ /// A bandwidth ceiling expressed as a refresh cap prunes working modes, because refresh alone
+ /// does not order two modes by how hard they are to drive: 2560x1440p144 and 3840x2160p60 sit 3
+ /// MHz of pixel clock apart and 84 Hz of refresh apart.
+ pub(crate) max_refresh_hz: u32,
+ /// Highest per-mode pixel clock in kHz this dock is known to carry.
+ ///
+ /// This is the constraint that actually bounds a mode: the DL7400 accepts 2560x1440@180 and
+ /// then fails to deliver it, and what separates that mode from the 165 Hz one it does drive is
+ /// 714.81 MHz against 699.50 MHz of link rate, not 180 against 165 of refresh.
+ ///
+ /// The set-mode message carries the clock at offsets 70..73 as a `u32` in 10 kHz units. Take
+ /// the ceiling from the mode's own clock rather than from DLM's copy of it, which is rounded
+ /// to the wire's 10 kHz unit (`0x0001113d`, 699.49 MHz, for a 699.50 MHz mode).
+ ///
+ /// Ridge's is not merely the low half of that field: driven at 2560x1440p165, 699.50 MHz, it
+ /// blanks the sink even with the other connector idle, so the limit is the dock's and not the
+ /// encoding's. It carries 2560x1440p144 at 597.29 MHz.
+ pub(crate) max_connector_clock_khz: u32,
+ /// Dock-wide pixel-rate budget in pixels per second, shared across all connectors.
+ ///
+ /// The DL7400's is the dual-connector rate DLM was measured sustaining, and it is also what
+ /// bounds the depth: three quarters of it is what a 30 bpp connector is priced against, which
+ /// puts two connectors at 2560x1440p165 outside 30 bpp. Raising it to the dock's rated
+ /// four-connector 4K60 load admits that pair and the sinks then power off with nothing logged,
+ /// so the rated figure is not a bound this budget can borrow. Ridge's is the highest combined
+ /// rate measured driving cleanly, 2560x1440p144 beside 2560x1440p120. Both are floors on the
+ /// dock's real ceiling rather than the ceiling itself.
+ ///
+ /// Do not mistake DLM's declared per-connector `pixel_per_second_limit` for the bound. It
+ /// clamps every Ridge connector to 120 Hz as policy, and the dock drives a single connector a
+ /// fifth past that figure -- a budget derived from it prunes configurations the hardware
+ /// sustains.
+ pub(crate) pixel_budget: u32,
+ /// Whether this dock has the DL-7000 "10bit profile", i.e. can be driven at 10 bits per
+ /// channel for HDR.
+ ///
+ /// DisplayLink documents HDR10 as DL-7000 only, and the D6000's own connector reports
+ /// `HDR supported = False` to Windows even with an HDR-capable monitor on it -- so this is a
+ /// silicon generation property, not a mode one. It says the dock *could* be put in that
+ /// profile; it does not say vino knows how to ask yet.
+ pub(crate) hdr_capable: bool,
+ /// Whether this dock composites a host-uploaded cursor bitmap of its own.
+ ///
+ /// A cursor image is one control message carrying the whole 64x64 premultiplied bitmap, which
+ /// is 16,448 bytes on the wire. That is fine on a dock with a video pipe of its own, and on a
+ /// dock that shares one it is both far past the largest record the platform ever carries and a
+ /// record landing in the middle of the video stream. The vendor sends no cursor message of any
+ /// kind on such a dock, and draws the pointer into the frame instead.
+ ///
+ /// A false here withdraws the cursor plane rather than merely declining to send: a plane whose
+ /// atomic commit succeeds makes the compositor stop drawing its own pointer, so starving one
+ /// loses the pointer altogether.
+ pub(crate) hw_cursor: bool,
+}
+
+/// Exceptional behaviour that has to be worked around rather than described.
+///
+/// This is not where an ordinary difference in topology, capability or protocol belongs. A field
+/// earns a place here by being a defect: behaviour that contradicts what the rest of the model
+/// would predict, and that exists only because the hardware does it.
+pub(crate) struct Quirks {
+ /// Whether one EDID handler is shared between this dock's connectors.
+ ///
+ /// On such a dock a fetch does not read the monitor named at offset 22; it reads whichever
+ /// connector the handler is currently engaged for, and engaging it for one connector disengages
+ /// it for the other. Two failures follow from treating a fetch as if it named a connector. A
+ /// fetch issued before the handler is engaged returns a block the dock synthesises for itself,
+ /// describing a 1920x1080 panel under the bridge's own vendor id, which is then published as
+ /// the sink's EDID and drives the monitor at a timing it never advertised. A fetch issued on an
+ /// empty connector returns the other connector's monitor, so one connector appears to move
+ /// between sockets.
+ ///
+ /// The presence reply says which answer is coming: offset 26 bit 7 is set once the downstream
+ /// DDC read has completed for the connector the handler is engaged for. Acceptance is gated on
+ /// it, and a connector the probe reports absent is never engaged, because engaging it would
+ /// take the handler away from a connector that is driving a panel.
+ pub(crate) shared_edid_handler: bool,
+ /// Whether this dock reports its downstream DDC read complete in the presence reply.
+ ///
+ /// Offset 26 bit 7 is that report. A block offered before it describes the dock's own bridge
+ /// rather than the monitor, and publishing it drives the panel at a timing it never
+ /// advertised, so where the bit is reported an early block is dropped and the fetch retried.
+ /// A dock that never sets it answers the fetch correctly anyway and must not be gated on it,
+ /// or discovery discards every block it is given.
+ pub(crate) edid_ready_reported: bool,
+ /// Whether a frame whose length is a whole number of maximum-size packets is split so it does
+ /// not end on a full one.
+ ///
+ /// The split cannot do what its name says. A frame of length `N` that is a multiple of the
+ /// 1024-byte maximum packet is sent as `N - 16` and then `16`, and `N - 16` is not a multiple
+ /// either, so the first transfer ends short as well: a dock that delimits frames on a short
+ /// packet sees the frame end sixteen bytes early and then a stray sixteen-byte frame behind it.
+ /// Ending a frame short without splitting it needs a zero-length packet, and there is no
+ /// binding for one yet.
+ ///
+ /// So this stays where it was measured to help and nowhere else. On a DL-6xxx it is what makes
+ /// the dock stop accepting bytes on its video endpoint about fifty milliseconds later, with the
+ /// endpoint still reporting healthy -- measured across three captures, one of which contained a
+ /// single such transfer and a single failure.
+ pub(crate) split_full_packet_frame: bool,
+}
+
+/// How a dock speaks: session bring-up, mode programming, and the video record stream.
+pub(crate) struct Protocol {
+ /// The `wValue` of the first vendor transition, before the AKE.
+ ///
+ /// Both families use the same request number and differ only in this value. Sending the wrong
+ /// one still permits AKE and an exact control-endpoint transcript, so nothing on the wire
+ /// reports it, but it leaves the dock's video-side state machine in a different state before
+ /// its later commit.
+ pub(crate) initial_vendor_state: u16,
+ /// Whether a per-connector HDCP record selects its connector as a one-hot bit at byte `22 +
+ /// connector`, rather than as a one-based connector number at byte 23.
+ pub(crate) per_connector_onehot: bool,
+ /// How long to wait for a connector's `AKE_Send_Rrx` before reading it as an empty socket.
+ ///
+ /// The push is how a connector says it has a sink at all, so this one bound decides both how
+ /// long a populated connector may take to answer and how long an empty one is waited on. It
+ /// belongs to the dock because it is a property of the receiver behind the connector and of
+ /// how busy the dock is when asked. Set below what the dock needs, a populated connector
+ /// reads as empty and loses the rest of its burst -- including the SKE that hands the dock
+ /// the key for that connector's content stream, without which the sealed decoder
+ /// configuration cannot be read and the dock renders every strip as noise. Set high, it costs
+ /// one dead wait per genuinely empty connector.
+ pub(crate) perhead_rrx_wait_ms: i64,
+ /// How this dock acknowledges each setup operation; see [`ReplyDiscipline`].
+ pub(crate) reply_discipline: ReplyDiscipline,
+ /// Where this dock wants its video-engine transition; see [`VideoCommitPoint`].
+ pub(crate) video_commit_point: VideoCommitPoint,
+ /// Whether programming any connector reconfigures the whole dock.
+ ///
+ /// On such a dock a mode set is not a per-connector operation. Reconfiguring one connector
+ /// while another is lit resets the dock, so every lit connector has to be gathered and
+ /// committed together, the cold bring-up resets all sinks before programming any, and the
+ /// control plane is held quiet across the first mode set rather than answering probe retries in
+ /// the middle of it.
+ pub(crate) dock_wide_modeset: bool,
+ /// Whether a connector's pipe is torn down before it is configured.
+ ///
+ /// The dock expects a connector cleared before a timing is programmed onto it.
+ pub(crate) clear_mode_before_set: bool,
+ /// How a connector blanks; see [`BlankBracket`].
+ pub(crate) blank_bracket: BlankBracket,
+ /// Whether a connector must keep being fed while its content is unchanged.
+ ///
+ /// A dock that tears the downstream link down over a silent video endpoint has to be re-fed
+ /// whether or not anything changed, so its settle repaint is periodic and is not charged
+ /// against the connector's keyframe budget.
+ pub(crate) video_keepalive: bool,
+ /// How the dock encodes a connector in a video record's `sub` field, as a left shift.
+ ///
+ /// Ridge uses the bare connector number (shift 0). Navarro spaces connectors eight apart --
+ /// records use `0x00`/`0x08`/`0x10`/`0x18` and stream-opens `0x07`/`0x0f`/`0x17`/`0x1f`.
+ pub(crate) connector_selector_shift: u8,
+ /// The bits a connector's content-stream id sets over its record `sub`.
+ ///
+ /// Ridge streams are `0x08 | connector`, Navarro's `(connector << 3) | 7`. See
+ /// [`video::haar::Geometry::stream_id_mask`], which this configures.
+ pub(crate) stream_id_mask: u8,
+ /// The connector-count marker byte the per-connector `strm2` record carries at offset 24.
+ pub(crate) strm2_marker: u8,
+ /// Whether an image record's `sub` carries the y-band parity; see
+ /// [`video::haar::Geometry::band_parity_bit`].
+ pub(crate) band_parity_bit: bool,
+ /// Blocks across one strip; see [`video::haar::Geometry`]. Ridge lays a strip's sixteen
+ /// blocks 8 across x 2 down (64x16 px), the DL7400 16 across x 1 down (128x8 px).
+ pub(crate) strip_blocks_x: usize,
+ /// Whether image records interlace y bands; see [`video::haar::Geometry::interlaced_bands`].
+ pub(crate) interlaced_bands: bool,
+ /// How many buffers the dock rotates through as it presents frames.
+ ///
+ /// Ridge is double buffered. The DL7400 rotates three slots -- `video::haar::ring_phase()`
+ /// steps `seq0 % 3` and its pipe descriptor names three ring addresses. Presentation and debt
+ /// counts are separate policy below: the same ring depth does not imply the same delivery
+ /// choreography.
+ pub(crate) dock_buffers: u8,
+ /// How keyframes and deltas are spread over those buffers; see [`FrameDelivery`].
+ pub(crate) frame_delivery: FrameDelivery,
+ /// Whether the dock's presence probe says anything about what is plugged into a connector.
+ ///
+ /// Where it does, an unoccupied connector stays disconnected rather than advertising a phantom
+ /// output. Where it does not, every connector the dock declares is offered: this dock answers
+ /// the probe "absent" and returns no EDID for a socket with a live monitor on it, and the
+ /// vendor configures both of its connectors before any pixels regardless.
+ pub(crate) reports_presence: bool,
+ /// Bits a steady-state image record adds to its `sub`, once a stream is past its opening.
+ ///
+ /// Read off the vendor driving this dock: after the frames that open a stream, every image
+ /// record it sends carries this over the connector and the y-band parity, and it never clears
+ /// it again. The records are otherwise byte-identical to vino's -- same size, type, aux,
+ /// sequence and payload -- so a dock that stops accepting a stream while its endpoint still
+ /// reports healthy is the failure this describes.
+ pub(crate) steady_record_sub_bit: u8,
+ /// Whether a timed presence retry may reset its bracket while another connector is active.
+ pub(crate) probe_bracket: ProbeBracket,
+ /// How the vendor spaces the stages of the setup burst; see [`SetupPolls`].
+ pub(crate) setup_polls: SetupPolls,
+ /// Outstanding EP84 reads to keep posted.
+ ///
+ /// Navarro needs exactly one, as DLM keeps: a deeper queue delays an EDID reply behind an
+ /// un-reaped slot and the dock then NAKs EP02. Ridge interleaves many more unsolicited pushes
+ /// with the replies it waits for, and loses them at a depth of one.
+ pub(crate) ep84_queue_depth: usize,
+ /// Flat carrier frames a connector presents before its first content frame.
+ ///
+ /// Every one of them walks the dock's ring another slot and steps its frame counter, so this
+ /// is a count the vendor's own stream states, not a duration to fill. `u32::MAX` leaves a
+ /// family bounded by the carrier's wall-clock window instead, which is only safe where that
+ /// window is itself the measured thing.
+ pub(crate) carrier_frames: u32,
+ /// Whether the first frame after a mode set carries the cold ARM burst.
+ ///
+ /// A dock either prefixes the ARM burst to the first frame of a stream, or opens the stream
+ /// with a short plaintext record on the connector's video `sub` followed by a sealed report on
+ /// its stream id. Sending the wrong opening leaves the stream unopened and the dock stalls the
+ /// endpoint, which surfaces as `EPROTO` on the first scanout write.
+ pub(crate) arm_burst: bool,
+ /// The `0x16/0x2e` state that takes a downstream sink down.
+ ///
+ /// 0 always brings a sink back up; what puts it down is not shared. DL-3x00 uses 3, and the
+ /// value verified against DL-6xxx is 1 -- so this is likely a bitmask rather than an
+ /// enumeration. Sending the wrong one leaves a dock accepting every byte of a frame and
+ /// displaying none of it, with nothing on the wire to say so.
+ pub(crate) sink_down_state: u8,
+ /// The two `0x2e` states this dock's post-mode-set bracket carries, in order.
+ ///
+ /// `0` is up and `3` is down. Read off each vendor driving its own dock: the DL-6xxx is sent
+ /// the down *before* the set-mode and nothing but `0` after it, so a `3` here leaves its sink
+ /// down for the rest of the bracket -- a dock that accepts every byte of a frame and displays
+ /// none of it.
+ pub(crate) post_mode_sink_states: [u8; 2],
+ /// The `0x2e` state this dock wants before a mode is programmed, if it wants one.
+ ///
+ /// The DL-6xxx is driven down and straight back up around every set-mode: its vendor sends
+ /// `0x2f` 1 and `0x2e` 3 immediately ahead of the timing and nothing but `0` behind it, which
+ /// is what retrains the downstream link onto the new timing. Without it the dock programs the
+ /// timing, accepts every byte of every frame and lights nothing. Docks whose vendor does not
+ /// bracket this way leave it `None`.
+ pub(crate) pre_mode_sink_state: Option<u8>,
+ /// The byte that names this dock in the marker opening a sealed video stream.
+ ///
+ /// The marker is otherwise the same four bytes everywhere; see `cp::stream_open`.
+ pub(crate) stream_marker_kind: u8,
+ /// The word repeated beside the surface size in a stream's mode header.
+ ///
+ /// It is not a pitch and its meaning is not established, so each generation carries the value
+ /// its own captures show rather than one computed here.
+ pub(crate) layout_word: u16,
+ /// Which form of decoder code tables this dock's stream configuration states.
+ pub(crate) code_tables: video_arm::CodeTables,
+ /// Whether the dock takes the three dock-wide records that precede the per-connector blocks.
+ ///
+ /// `0x14/0x30`, `0x15/0x0b` and one `0x16/0x2a` per connector. A dock that does not expect
+ /// them is left every later inner counter and AES block out of step by sending them.
+ pub(crate) dock_wide_init: bool,
+ /// Shortest interval between two frames on one connector, in milliseconds.
+ ///
+ /// A dock with a video pipe of its own can be fed as fast as the encoder and USB allow, and
+ /// this only has to stop a busy compositor from queueing work faster than it drains. A dock
+ /// that carries video on the control pipe needs it to do more: the gaps between frames are the
+ /// only time its control plane gets the endpoint, so an interval that is too short does not
+ /// merely drop frames, it silences the dock.
+ pub(crate) frame_period_ms: i64,
+ /// Interval between the session keepalive's status queries, in milliseconds.
+ ///
+ /// A dock with a video pipe of its own can be asked as often as is convenient: the query and
+ /// the pixels do not contend. Where they share an endpoint the query is bytes queued ahead of
+ /// a frame and a reply the dock has to produce mid-scanout, so this follows what the vendor
+ /// does on that dock rather than what is convenient here.
+ pub(crate) status_period_ms: i64,
+ /// How much this dock will take, and how fast; see [`StreamPacing`].
+ pub(crate) stream_pacing: StreamPacing,
+ /// How this dock states its framebuffer allocation in a set-mode; see [`Allocation`].
+ pub(crate) allocation: Allocation,
+}
+
+/// Everything that distinguishes one dock from another.
+pub(crate) struct DockProfile {
+ /// Human name, logged at probe so an unfamiliar unit identifies itself in dmesg.
+ pub(crate) name: &'static str,
+ /// How this dock's connectors, pipes and endpoints are arranged.
+ pub(crate) topology: Topology,
+ /// What this dock can be asked to drive.
+ pub(crate) capabilities: Capabilities,
+ /// How this dock speaks.
+ pub(crate) protocol: Protocol,
+ /// Where this dock departs from what the model above would predict.
+ pub(crate) quirks: Quirks,
+}
+
+/// The offset-46 stride and offset-48 row count of a set-mode.
+///
+/// The pair describes the dock's own framebuffer allocation rather than the timing, and getting it
+/// wrong is invisible on the wire: the dock accepts the set-mode, accepts pixels, and then has
+/// nowhere to put the frame after the first.
+pub(crate) enum Allocation {
+ /// A device-level override the dock carries on every mode, whatever the width.
+ ///
+ /// The whole decrypted Ridge corpus carries one pair at 1280x720, 1920x1080 and 2560x1440
+ /// alike, where a derived stride would differ per width.
+ Fixed { stride: u16, rows: u16 },
+ /// Stride quantised up from the width, row count derived from a fixed framebuffer size.
+ ///
+ /// The vendor's serializer computes the row count as its framebuffer allocation divided by one
+ /// row of the render stride, so a dock that partitions a fixed number of bytes per connector
+ /// states a row count that falls out of the width and the sample depth alone. `bytes` is that
+ /// partition.
+ Derived { bytes: u32 },
+ /// Stride quantised up from the width, row count as measured for the resolution.
+ ///
+ /// For a dock whose allocator hands out a different size per mode, nothing derives the row
+ /// count and a resolution no capture covers has no answer but a family default.
+ Measured {
+ rows: &'static [(u16, u16, u16)],
+ default_rows: u16,
+ },
+}
+
+impl Allocation {
+ /// The `(stride, rows)` pair for one mode, and whether the rows are known rather than guessed.
+ ///
+ /// A row of the dock's framebuffer is `stride * bytes_per_pixel` wide, so the depth the
+ /// connector will actually send belongs in the division: a 30 bpp connector is told three
+ /// quarters of the rows a 24 bpp one is.
+ pub(crate) fn words(&self, hactive: u16, vactive: u16, ten_bit: bool) -> (u16, u16, bool) {
+ // The dock's DMA formats index a bytes-per-pixel table; the two this driver sends are
+ // 24 bpp packed and 30 bpp in a 32-bit container.
+ let bytes_per_pixel = if ten_bit { 4 } else { 3 };
+ match self {
+ Allocation::Fixed { stride, rows } => (*stride, *rows, true),
+ Allocation::Derived { bytes } => {
+ let stride = cp::render_stride(hactive);
+ let row = u32::from(stride) * bytes_per_pixel;
+ // A stride is never zero for a width a sink can advertise, but say so rather than
+ // carrying a division that has to trap.
+ let rows = bytes.checked_div(row).unwrap_or(0).min(u32::from(u16::MAX)) as u16;
+ (stride, rows, true)
+ }
+ Allocation::Measured { rows, default_rows } => {
+ let stride = cp::render_stride(hactive);
+ let (rows, known) =
+ match rows.iter().find(|(w, h, _)| *w == hactive && *h == vactive) {
+ Some((_, _, r)) => (*r, true),
+ None => (*default_rows, false),
+ };
+ // The table is measured at 24 bpp and the partition behind it does not grow with
+ // the depth, so the row count gives way by the ratio the row width gained.
+ let rows = (u32::from(rows) * 3 / bytes_per_pixel) as u16;
+ (stride, rows, known)
+ }
+ }
+ }
+}
+
+impl DockProfile {
+ /// This dock's codec geometry, for the codec calls made before a DRM device exists.
+ ///
+ /// The steady-state path reads `VinoDrmData::geometry()` instead; both describe the same
+ /// dock, and this exists because CP setup names stream ids before the sink is published.
+ pub(crate) fn geometry(&self) -> video::haar::Geometry {
+ video::haar::Geometry::new(
+ self.protocol.strip_blocks_x,
+ self.protocol.interlaced_bands,
+ self.protocol.band_parity_bit,
+ self.protocol.connector_selector_shift,
+ self.protocol.stream_id_mask,
+ self.protocol.dock_buffers,
+ )
+ .with_coding(self.protocol.code_tables)
+ .with_steady_sub_bit(self.protocol.steady_record_sub_bit)
+ }
+}
+
+/// DL-3x00 docks (Ella), such as the HP 3005pr port replicator.
+///
+/// This dock has no video endpoint. Its display interface exposes only the control pair, `0x02`
+/// OUT and `0x84` IN, and it carries pixels down `0x02` alongside the control messages -- measured
+/// at 289 MB of video against 68 kB of replies in one session. The two planes are told apart by a
+/// record's `sub`: a connector number for video, and the sealed control subs otherwise. Naming
+/// `0x02` here is what lets the ordinary video path drive it unchanged; the cost is that the
+/// control and video writers now share an endpoint and must not interleave mid-record.
+///
+/// Everything else is Ridge: 64x16 strips, the bare connector number in a record's `sub`, the
+/// `0x08 | connector` stream ids, the same record framing and the same set-mode layout. The one
+/// departure is band handling, which follows Navarro -- bands are interlaced and a record carries
+/// no parity bit, so a record may span a band boundary and fill to the stride cap.
+pub(crate) static PROFILE_ELLA: DockProfile = DockProfile {
+ name: "DL-3x00 dock (Ella, DL-3900)",
+ topology: Topology {
+ video_endpoints: [0x02, 0x02, 0x02, 0x02],
+ video_on_ctrl_pipe: true,
+ connectors: 2,
+ // Three, measured: the per-frame opener's slot field cycles 0, 1, 2 across 7115 records.
+ },
+ capabilities: Capabilities {
+ max_refresh_hz: 75,
+ max_connector_clock_khz: 148_500,
+ pixel_budget: 248_832_000,
+ hdr_capable: false,
+ // No cursor message of any kind appears in 326 s of the vendor driving this dock, and its
+ // largest control record is 192 bytes against the 16,448 a cursor upload would need.
+ hw_cursor: false,
+ },
+ protocol: Protocol {
+ initial_vendor_state: 3,
+ per_connector_onehot: false,
+ // A cold DL-3x00 takes 221 ms to answer the first per-connector AKE_No_Stored_km,
+ // against under two once it is warm, so the bound is sized above the cold figure with
+ // margin.
+ perhead_rrx_wait_ms: 500,
+ reply_discipline: ReplyDiscipline::Drain,
+ video_commit_point: VideoCommitPoint::AfterFinalize,
+ dock_wide_modeset: false,
+ clear_mode_before_set: false,
+ // Measured against this dock's own vendor: the sink goes down on `2f=1`, `2e=3` alone,
+ // with no video in the window. Painting black first is not merely redundant here, it is
+ // harmful -- video shares the control pipe on this platform, a black desktop quadruples
+ // what the vendor puts on that pipe, and the endpoint halts under exactly that load,
+ // which abandons the session and leaves the dock scanning out its last frame.
+ blank_bracket: BlankBracket::MarkersHeld,
+ video_keepalive: false,
+ connector_selector_shift: 0,
+ stream_id_mask: 0x08,
+ // Not a connector count: this platform sends 0x10 with two connectors where Ridge sends
+ // 0x06 with two and Navarro 0x0c with four.
+ strm2_marker: 0x10,
+ band_parity_bit: false,
+ strip_blocks_x: 8,
+ interlaced_bands: true,
+ dock_buffers: 3,
+ // DLM presents every content frame once. Keep a changed strip selected across the ring
+ // instead of multiplying every logical frame into three back-to-back copies.
+ frame_delivery: FrameDelivery::new(3, 1, 3),
+ // DLM offers 75 Hz at 1024x768, 1280x1024, 800x600 and 640x480, and stops at 1920x1080@60
+ // -- so refresh is not what bounds this dock, the pixel clock is. 1920x1080@60 is 148.5 MHz
+ // and is the largest mode offered; the budget is that mode on both connectors.
+ reports_presence: false,
+ // Kept: this is the family the split was measured on.
+ steady_record_sub_bit: 0,
+ // Ella shares control and pixels on EP02. Its vendor stream never resets an idle
+ // connector's bracket periodically, and doing so while the sibling is lit drops that
+ // sibling's sink.
+ probe_bracket: ProbeBracket::DeferWithActiveSibling,
+ // Counted off the vendor's own burst: two polls between the engage records and the first
+ // stream open, three between the two opens, three before the capability queries.
+ setup_polls: SetupPolls::new(2, 3, 3),
+ // One is the whole opening: the vendor's first connector goes straight from a single flat
+ // frame into a full content frame. Its second connector sends several because that is what
+ // its compositor had, not because the dock wants them.
+ ep84_queue_depth: 1,
+ // DLM opens an Ella stream the way it opens a Navarro one: a 48-byte plaintext record on
+ // the connector's video sub, then a 336-byte sealed record on stream id 0x08|connector,
+ // then strips.
+ carrier_frames: 1,
+ // One, as Navarro keeps and as this dock's vendor keeps: 313 submissions against 312
+ // completions in a 326 s session, never two outstanding. A deeper queue leaves a reply
+ // behind an un-reaped slot, and this dock answers that by halting EP02 -- which on a dock
+ // that carries its pixels there takes the video with it.
+ arm_burst: false,
+ sink_down_state: 3,
+ post_mode_sink_states: [3, 0],
+ pre_mode_sink_state: None,
+ stream_marker_kind: 0x01,
+ layout_word: 0x1800,
+ code_tables: video_arm::CodeTables::Narrow,
+ dock_wide_init: true,
+ // Measured from DLM, per connector, between the records that close consecutive frames:
+ // median 16.6 ms with a tenth percentile of 15.8, which is a 60 Hz producer and not a dock
+ // limit. The floor is far lower -- 1.3 ms -- and the peak rate 82.9 MB/s.
+ frame_period_ms: 16,
+ // DLM sends 179 sealed control records in a 326 s session -- one status query per 2.5 s --
+ // and nothing at all during its long silences. Its pixels and its control plane share this
+ // endpoint, and it spends the endpoint on pixels.
+ status_period_ms: 2500,
+ // The vendor's own envelope over a 326 s session, measured as peak bytes in a sliding
+ // window: 34.4 MB in any second, 36.2 in any two, 38.5 in any three, 42.8 in any five. It
+ // bursts a frame at 158 MB/s and then goes quiet, so the pair has to be read as a curve --
+ // a single sustained figure large enough for the bursts permits many times the five-second
+ // total.
+ //
+ // A bucket of 24 MB refilling at 8 MB/s reproduces that curve: 32 MB in a second against
+ // the vendor's 34.4, 40 in two against 36.2, 64 in five against 42.8. Deliberately at the
+ // vendor's shoulder rather than under it, because what halts the endpoint is a frame
+ // ending on a full packet, not volume, and a ceiling below the vendor's own demand is
+ // a ceiling the desktop feels.
+ stream_pacing: StreamPacing::new(8_000_000, 24_000_000),
+ allocation: Allocation::Derived {
+ bytes: 48 * 1024 * 1024,
+ },
+ },
+ quirks: Quirks {
+ shared_edid_handler: false,
+ edid_ready_reported: false,
+ split_full_packet_frame: true,
+ },
+};
+/// Dell D6000 and other Ridge-platform docks.
+pub(crate) static PROFILE_RIDGE: DockProfile = DockProfile {
+ name: "Dell D6000 (Ridge, DL-6xxx)",
+ topology: Topology {
+ video_endpoints: [0x08, 0x0b, 0x08, 0x0b],
+ video_on_ctrl_pipe: false,
+ connectors: 2,
+ },
+ capabilities: Capabilities {
+ max_refresh_hz: u32::MAX,
+ max_connector_clock_khz: 655_350,
+ pixel_budget: 973_209_600,
+ hdr_capable: false,
+ hw_cursor: true,
+ },
+ protocol: Protocol {
+ initial_vendor_state: 3,
+ per_connector_onehot: false,
+ perhead_rrx_wait_ms: 30,
+ reply_discipline: ReplyDiscipline::Drain,
+ video_commit_point: VideoCommitPoint::AfterFinalize,
+ dock_wide_modeset: false,
+ clear_mode_before_set: false,
+ blank_bracket: BlankBracket::BlackThenClose,
+ video_keepalive: false,
+ connector_selector_shift: 0,
+ stream_id_mask: 0x08,
+ strm2_marker: 0x06,
+ band_parity_bit: true,
+ strip_blocks_x: 8,
+ interlaced_bands: false,
+ dock_buffers: 2,
+ frame_delivery: FrameDelivery::new(2, 1, 3),
+ reports_presence: true,
+ // One handler, shared: the vendor reads this dock twice, discards the first block and takes
+ // the second once the readiness bit is set.
+ steady_record_sub_bit: 0x20,
+ // A stray sixteen-byte transfer is what this dock stops accepting bytes over.
+ probe_bracket: ProbeBracket::Always,
+ // Ridge opens its streams from the scanout path, on a video pipe of its own.
+ setup_polls: SetupPolls::NONE,
+ ep84_queue_depth: 4,
+ // Unmeasured here: the cold timeline this family was tuned on bounds the carrier by its
+ // wall-clock window, and that window is the measured thing.
+ carrier_frames: u32::MAX,
+ arm_burst: true,
+ sink_down_state: 1,
+ // Read off DLM driving this dock: it sends the down before the set-mode, and every `0x2e`
+ // after it carries 0.
+ post_mode_sink_states: [0, 0],
+ pre_mode_sink_state: Some(3),
+ stream_marker_kind: 0x03,
+ layout_word: 0x4000,
+ code_tables: video_arm::CodeTables::Wide,
+ dock_wide_init: false,
+ // The vendor's own floor on this endpoint: it never puts two frames closer together than
+ // 8.2 ms. Matching it rather than undercutting it by three times costs nothing a desktop
+ // can see.
+ frame_period_ms: 8,
+ // Video has an endpoint of its own here, so a status query costs a frame nothing.
+ status_period_ms: 250,
+ // A video endpoint of its own, and no limit of either kind has been measured on it.
+ stream_pacing: StreamPacing::UNMETERED,
+ allocation: Allocation::Fixed {
+ stride: 0x4000,
+ rows: 0x6000,
+ },
+ },
+ quirks: Quirks {
+ shared_edid_handler: true,
+ edid_ready_reported: true,
+ split_full_packet_frame: false,
+ },
+};
+/// DL-7400 quad-display docks (Navarro).
+///
+/// Four independent physical connectors multiplexed over two video endpoints. This is not tiling:
+/// the Windows capture has a distinct stream-open and record `sub` for each socket.
+pub(crate) static PROFILE_NAVARRO: DockProfile = DockProfile {
+ name: "DL-7400 quad dock (Navarro, DL-7000)",
+ topology: Topology {
+ video_endpoints: [0x08, 0x0a, 0x08, 0x0a],
+ video_on_ctrl_pipe: false,
+ connectors: 4,
+ },
+ capabilities: Capabilities {
+ max_refresh_hz: u32::MAX,
+ max_connector_clock_khz: 699_500,
+ pixel_budget: 1_216_512_000,
+ hdr_capable: true,
+ hw_cursor: true,
+ },
+ protocol: Protocol {
+ initial_vendor_state: 0,
+ per_connector_onehot: true,
+ perhead_rrx_wait_ms: 30,
+ reply_discipline: ReplyDiscipline::Lockstep,
+ video_commit_point: VideoCommitPoint::BeforeConnectorRecords,
+ dock_wide_modeset: true,
+ clear_mode_before_set: true,
+ blank_bracket: BlankBracket::MarkersHeld,
+ video_keepalive: true,
+ connector_selector_shift: 3,
+ stream_id_mask: 0x07,
+ strm2_marker: 0x0c,
+ band_parity_bit: false,
+ strip_blocks_x: 16,
+ interlaced_bands: true,
+ dock_buffers: 3,
+ frame_delivery: FrameDelivery::new(3, 1, 4),
+ reports_presence: true,
+ steady_record_sub_bit: 0,
+ // Left as it has been since the split was introduced. It is very likely wrong here for the
+ // same reason it is wrong on a DL-6xxx, but this dock was not on the bus to test it and
+ // changing behaviour that is currently working on hardware nobody can check is not a
+ // trade worth making. Re-measure when the dock is back: look for 16-byte transfers on
+ // its video endpoints.
+ probe_bracket: ProbeBracket::Always,
+ // As Ridge: the DL7400 has video endpoints of its own and opens a stream ahead of the
+ // frame.
+ setup_polls: SetupPolls::NONE,
+ ep84_queue_depth: 1,
+ // DLM opens a stream here with five quiescent frames of about fifty image records each
+ // before the first detailed one. A window instead of a count made this whatever the
+ // endpoint would take in 400 ms -- four frames when the dock was draining slowly and 852
+ // when it was not.
+ carrier_frames: 5,
+ arm_burst: false,
+ sink_down_state: 3,
+ // Unchanged: this dock drives a panel with both downs in place.
+ post_mode_sink_states: [3, 3],
+ pre_mode_sink_state: Some(3),
+ stream_marker_kind: 0x05,
+ layout_word: 0x2100,
+ code_tables: video_arm::CodeTables::Wide,
+ dock_wide_init: true,
+ frame_period_ms: 5,
+ // Video has an endpoint of its own here, so a status query costs a frame nothing.
+ status_period_ms: 250,
+ // A video endpoint of its own, and no limit of either kind has been measured on it.
+ stream_pacing: StreamPacing::UNMETERED,
+ allocation: Allocation::Measured {
+ rows: &[(2560, 1440, 0x66db), (640, 480, 0x6800)],
+ default_rows: 0x6000,
+ },
+ },
+ quirks: Quirks {
+ shared_edid_handler: false,
+ edid_ready_reported: true,
+ split_full_packet_frame: true,
+ },
+};
+
+/// The profile for a dock family, or `None` for a family this driver cannot drive yet.
+///
+/// The family comes from the device's own identity descriptor, so this is what a dock *is*, not
+/// what product ID it happens to ship under. A product-ID table can only ever describe the
+/// hardware someone tested; see Documentation/gpu/vino.rst.
+///
+/// Declining a family is deliberate: an unrecognised device that says what it is produces a usable
+/// report, whereas a guessed profile produces a dock reset.
+///
+/// Firefly has never been seen here at all.
+pub(crate) fn for_family(family: firmware::Family) -> Option<&'static DockProfile> {
+ match family {
+ firmware::Family::Ella => Some(&PROFILE_ELLA),
+ firmware::Family::Ridge => Some(&PROFILE_RIDGE),
+ firmware::Family::Navarro => Some(&PROFILE_NAVARRO),
+ firmware::Family::Firefly => None,
+ }
+}
+
+/// The profile for a device whose identity descriptor could not be read.
+///
+/// This is the quirk table, and the only thing product IDs are still good for: a dock that will
+/// not answer `GET_DESCRIPTOR` for its identity is one this driver has no other way to place. It
+/// is not the gate -- a device missing from it is still driven if it names its family.
+pub(crate) fn for_product(product: u16) -> Option<&'static DockProfile> {
+ match product {
+ PID_D6000 => Some(&PROFILE_RIDGE),
+ PID_DL7400 => Some(&PROFILE_NAVARRO),
+ _ => None,
+ }
+}
+
+/// Control and per-connector bulk endpoints.
+pub(crate) const EP_CTRL_OUT: u8 = 0x02;
+pub(crate) const EP_CTRL_IN: u8 = 0x84;
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_profile)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn an_endpoint_list_logs_on_one_line() -> Result {
+ // A log line has to be one line. `{:#04x?}` on an array compiles and reads fine in source
+ // but asks the derived `Debug` to pretty-print, putting each endpoint on its own line in
+ // dmesg; nothing but rendering it catches that.
+ let rendered = kernel::str::CString::try_from_fmt(kernel::prelude::fmt!(
+ "{}",
+ crate::HexList(&PROFILE_NAVARRO.topology.video_endpoints)
+ ))?;
+ assert_eq!(rendered.to_bytes(), b"08 0a 08 0a");
+ assert!(!rendered.to_bytes().contains(&b'\n'));
+ Ok(())
+ }
+
+ #[test]
+ fn stream_ids_follow_the_dock_profile() {
+ // Each dock's ids come from its own geometry value, so two bound docks cannot interfere.
+ let ridge = PROFILE_RIDGE.geometry();
+ assert_eq!(ridge.stream_id(0), 0x0008);
+ assert_eq!(ridge.stream_id(1), 0x0009);
+
+ let navarro = PROFILE_NAVARRO.geometry();
+ assert_eq!(navarro.stream_id(0), 0x0007);
+ assert_eq!(navarro.stream_id(1), 0x000f);
+ assert_eq!(navarro.stream_id(2), 0x0017);
+ assert_eq!(navarro.stream_id(3), 0x001f);
+
+ // And the Ridge values are unchanged by having read the Navarro ones.
+ assert_eq!(ridge.stream_id(0), 0x0008);
+ }
+
+ /// cannot be asked.
+ ///
+ /// The failure this pins is silent and total: swap two arms and every dock of both families
+ /// gets the other's codec geometry, which the hardware answers with a reset. The unsupported
+ /// families must stay unsupported for the same reason -- a guessed profile is worse than a
+ /// declined bind, because a declined bind produces a report.
+ #[test]
+ fn a_dock_is_placed_by_family_and_product_ids_are_only_quirks() {
+ use crate::firmware::Family;
+
+ assert!(core::ptr::eq(
+ for_family(Family::Navarro).unwrap(),
+ &PROFILE_NAVARRO
+ ));
+ assert!(core::ptr::eq(
+ for_family(Family::Ridge).unwrap(),
+ &PROFILE_RIDGE
+ ));
+ assert!(core::ptr::eq(
+ for_family(Family::Ella).unwrap(),
+ &PROFILE_ELLA
+ ));
+ assert!(for_family(Family::Firefly).is_none());
+
+ // DL-3x00 is the only shape that shares the control pipe, and it is the only one allowed
+ // to: `send_cp_reply` excludes scanout for the duration of a control message on such a
+ // dock. A profile that starts sharing the pipe without that exclusion splits a record.
+ // Every family except Ella, which is the exception being pinned.
+ for family in [Family::Ridge, Family::Navarro, Family::Firefly] {
+ if let Some(profile) = for_family(family) {
+ assert!(!profile.topology.video_on_ctrl_pipe);
+ }
+ }
+ assert!(PROFILE_ELLA.topology.video_on_ctrl_pipe);
+
+ // Measured from a DLM capture of an Ella dock driving two 1920x1080 connectors: 992,496
+ // strips at 30 distinct x positions 64 apart and 68 distinct y positions 16 apart, image
+ // records whose `sub` is a bare connector number with no band-parity bit, and `0x08 |
+ // connector` stream ids. Getting any of these wrong produces a dock that accepts every byte
+ // and shows nothing, so pin them.
+ let ella = PROFILE_ELLA.geometry();
+ assert_eq!(ella.strip_w(), 64);
+ assert_eq!(ella.strip_h(), 16);
+ assert_eq!(PROFILE_ELLA.protocol.connector_selector_shift, 0);
+ assert_eq!(PROFILE_ELLA.protocol.stream_id_mask, 0x08);
+ assert!(!PROFILE_ELLA.protocol.band_parity_bit);
+ assert!(PROFILE_ELLA.protocol.interlaced_bands);
+
+ // A dock that shares the control pipe lives in the gaps between its own frames, so the
+ // interval is a functional requirement rather than a tuning preference: driving it at the
+ // shared 5 ms default put 191 MB/s on the pipe and the dock stopped answering EP84
+ // entirely. DLM's own median gap between the records closing consecutive frames is
+ // 16.6 ms, with a tenth percentile of 15.8.
+ assert_eq!(PROFILE_ELLA.protocol.frame_period_ms, 16);
+ // Pinned per family rather than as "everything except Ella". The vendor never puts two
+ // frames on a DL-6xxx's video endpoint closer together than 8.2 ms, so that dock's floor
+ // is its own measurement and not the shared default.
+ assert_eq!(PROFILE_RIDGE.protocol.frame_period_ms, 8);
+ assert_eq!(PROFILE_NAVARRO.protocol.frame_period_ms, 5);
+
+ // The quirk table agrees with the families it stands in for, and knows nothing else. A
+ // product missing here is still driven if its identity descriptor can be read.
+ assert!(core::ptr::eq(for_product(0x6006).unwrap(), &PROFILE_RIDGE));
+ assert!(core::ptr::eq(
+ for_product(0x7000).unwrap(),
+ &PROFILE_NAVARRO
+ ));
+ assert!(for_product(0x6015).is_none());
+ }
+
+ #[test]
+ fn edid_is_gated_on_the_readiness_report_where_the_dock_makes_one() {
+ // Gating a dock that never reports the read complete discards every block it offers, and
+ // not gating one that does publishes the dock's own bridge descriptor as the sink's EDID.
+ // The two properties are independent of whether the connectors share a handler.
+ assert!(!PROFILE_ELLA.quirks.edid_ready_reported);
+ assert!(PROFILE_RIDGE.quirks.edid_ready_reported);
+ assert!(PROFILE_NAVARRO.quirks.edid_ready_reported);
+ assert!(!PROFILE_NAVARRO.quirks.shared_edid_handler);
+ }
+
+ /// The rrx bound has to cover a cold receiver, and so is pinned per dock.
+ ///
+ /// A connector that does not answer inside it is taken for an empty socket and loses the rest
+ /// of its authentication, which costs that connector its content-stream key. A cold DL-3x00
+ /// takes 221 ms to answer.
+ #[test]
+ fn the_rrx_bound_covers_a_cold_receiver_on_the_dock_that_needs_it() {
+ assert!(PROFILE_ELLA.protocol.perhead_rrx_wait_ms >= 221);
+ assert_eq!(PROFILE_ELLA.protocol.perhead_rrx_wait_ms, 500);
+ assert_eq!(PROFILE_RIDGE.protocol.perhead_rrx_wait_ms, 30);
+ assert_eq!(PROFILE_NAVARRO.protocol.perhead_rrx_wait_ms, 30);
+ }
+
+ /// Status queries follow the dock, because on a shared pipe they cost a frame.
+ #[test]
+ fn status_period_follows_the_vendor_on_the_shared_pipe_dock() {
+ // A dedicated video endpoint means a query contends with nothing.
+ assert_eq!(PROFILE_RIDGE.protocol.status_period_ms, 250);
+ assert_eq!(PROFILE_NAVARRO.protocol.status_period_ms, 250);
+ // DLM sends 179 sealed control records in 326 s on the DL-3x00 -- one per 2.5 s.
+ assert_eq!(PROFILE_ELLA.protocol.status_period_ms, 2500);
+ assert!(PROFILE_ELLA.topology.video_on_ctrl_pipe);
+ }
+
+ /// Blanking follows the vendor's own disable, and the two shapes are not interchangeable.
+ ///
+ /// These two behaviours were once selected by one predicate that appeared twice, and splitting
+ /// it into a two-valued field inverted them: every dock got the other one's disable, which
+ /// leaves a panel lit on black rather than taking its signal away. Pin each dock to the
+ /// sequence measured from its own vendor.
+ #[test]
+ fn blanking_follows_the_vendor_disable_for_each_dock() {
+ // The DL-7400's disable is `2f=1`, `2e=3` and then silence, holding the bracket open.
+ // Sending it the close bracket re-enumerates the dock about two seconds later.
+ assert!(matches!(
+ PROFILE_NAVARRO.protocol.blank_bracket,
+ BlankBracket::MarkersHeld
+ ));
+ assert_eq!(PROFILE_NAVARRO.protocol.sink_down_state, 3);
+
+ // The DL-3x00 takes the same pair, and must not paint on the way: video shares the
+ // control pipe there, a black desktop quadruples what the vendor puts on that pipe, and
+ // the endpoint halts under that load -- which abandons the session and leaves the dock
+ // scanning out its last frame, the opposite of blanking.
+ assert!(matches!(
+ PROFILE_ELLA.protocol.blank_bracket,
+ BlankBracket::MarkersHeld
+ ));
+ assert!(PROFILE_ELLA.topology.video_on_ctrl_pipe);
+
+ // A dock with a video endpoint of its own presents black, closes the bracket and then
+ // powers the sink down. Black frames alone leave the panel lit, because the dock goes on
+ // scanning out what it last decoded.
+ assert!(matches!(
+ PROFILE_RIDGE.protocol.blank_bracket,
+ BlankBracket::BlackThenClose
+ ));
+ assert!(!PROFILE_RIDGE.topology.video_on_ctrl_pipe);
+
+ // Pinned per dock, not as a group: the state is the vendor's own, a zero would skip the
+ // power-down entirely, and the moment one dock is measured separately a shared assertion
+ // stops describing any of them.
+ assert_eq!(PROFILE_ELLA.protocol.sink_down_state, 3);
+ assert_eq!(PROFILE_RIDGE.protocol.sink_down_state, 1);
+ }
+
+ /// The setup burst is spaced the way the vendor spaces it, per dock.
+ #[test]
+ fn setup_polls_space_the_burst_the_way_the_vendor_does() {
+ // Counted off the vendor's own stream-open block: two polls, open, three, open, three.
+ let ella = PROFILE_ELLA.protocol.setup_polls;
+ assert_eq!(ella.before_stream_opens, 2);
+ assert_eq!(ella.between_stream_opens, 3);
+ assert_eq!(ella.after_stream_opens, 3);
+ assert_eq!(ella.before_open(0), 2);
+ assert_eq!(ella.before_open(1), 3);
+ assert_eq!(ella.before_open(3), 3);
+
+ // The docks that open a stream from the scanout path space nothing here, and must keep
+ // sending the burst they already send.
+ for polls in [
+ PROFILE_RIDGE.protocol.setup_polls,
+ PROFILE_NAVARRO.protocol.setup_polls,
+ ] {
+ assert_eq!(polls, SetupPolls::NONE);
+ assert_eq!(polls.before_open(0), 0);
+ assert_eq!(polls.before_open(1), 0);
+ assert_eq!(polls.after_stream_opens, 0);
+ }
+ }
+
+ #[test]
+ fn probe_bracket_close_is_suppressed_only_beside_a_lit_shared_pipe() {
+ use ProbeBracket::{Always, DeferWithActiveSibling};
+
+ // Ridge and Navarro keep their existing unconditional recovery sequence.
+ assert!(Always.should_close(0, 0));
+ assert!(Always.should_close(0, 1 << 1));
+
+ // Ella can establish an unknown bracket when nothing else is at risk, and can retry the
+ // target itself. Only a different live connector blocks the shared-pipe reset.
+ assert!(DeferWithActiveSibling.should_close(0, 0));
+ assert!(DeferWithActiveSibling.should_close(0, 1 << 0));
+ assert!(!DeferWithActiveSibling.should_close(0, 1 << 1));
+ assert!(!DeferWithActiveSibling.should_close(1, (1 << 0) | (1 << 1)));
+
+ assert_eq!(PROFILE_RIDGE.protocol.probe_bracket, Always);
+ assert_eq!(PROFILE_NAVARRO.protocol.probe_bracket, Always);
+ assert_eq!(PROFILE_ELLA.protocol.probe_bracket, DeferWithActiveSibling);
+ }
+
+ /// Every offset-48 row count DLM states on a DL-3x00, across two sinks and ten modes.
+ ///
+ /// The vendor's serializer divides a fixed framebuffer size by one row of the render stride,
+ /// so the row count depends on the width alone. These are the six distinct widths of the mode
+ /// sweep; a table keyed on the full resolution would have answered only for the one that was
+ /// captured and sent a default -- a whole framebuffer too large -- for the other nine.
+ #[test]
+ fn ella_row_count_follows_the_width_alone() -> Result {
+ // (hactive, vactive, offset-48 row count DLM sent)
+ let measured: [(u16, u16, u16); 8] = [
+ (2560, 1440, 6241),
+ (1920, 1080, 8192),
+ (1280, 1024, 11915),
+ // Same width, three heights and two refreshes: DLM sent one value for all of them.
+ (1280, 960, 11915),
+ (1280, 720, 11915),
+ (1024, 768, 14563),
+ // The only width that is not a multiple of 128, so the only one that shows the stride
+ // quantising: it allocates as 896 + 128 wide.
+ (800, 600, 16384),
+ (640, 480, 21845),
+ ];
+ for (hactive, vactive, rows) in measured {
+ let (_, got, known) = PROFILE_ELLA
+ .protocol
+ .allocation
+ .words(hactive, vactive, false);
+ assert_eq!(got, rows);
+ assert!(known);
+ }
+
+ // A row is `stride * bytes_per_pixel`, so 30 bpp in a 32-bit container fits three quarters
+ // of the rows 24 bpp does.
+ let (_, ten_bit_rows, _) = PROFILE_ELLA.protocol.allocation.words(1920, 1080, true);
+ assert_eq!(ten_bit_rows, 6144);
+
+ // A measured table owes the same three quarters: a 24 bpp row count alongside a 30 bpp
+ // format hands the dock a partition it cannot hold.
+ let (_, eight, known) = PROFILE_NAVARRO.protocol.allocation.words(2560, 1440, false);
+ assert_eq!(eight, 0x66db);
+ assert!(known);
+ let (_, ten, _) = PROFILE_NAVARRO.protocol.allocation.words(2560, 1440, true);
+ assert_eq!(ten, 19748); // 0x66db * 3 / 4
+ Ok(())
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH v3 6/13] drm/vino: add the video codec
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (4 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 5/13] drm/vino: add the dock profiles Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:56 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 7/13] drm/vino: add control-session bring-up Mike Lothian
` (6 subsequent siblings)
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Nathan Chancellor, Nick Desaulniers,
Bill Wendling, Justin Stitt, linux-kernel, llvm
The dock decodes a proprietary multilevel Haar codec: 128x8 or 64x16
strips, an 8x8 transform, per-plane significance trees and a unary VLC
whose payload bits are interleaved into the runs rather than following
them. A per-strip parameter map tells the dock how to size its own
allocations, and on one generation it has to land among the image records
rather than after them.
Add the encoder, split into the transform, the strip grammar and the record
framing; the decoder-configuration burst that arms a stream; and the
software colour pipeline that applies CTM and gamma while the driver still
has the pixels, since a dock has no colour hardware to program.
Record framing is the part with no margin: records are 4096 bytes, 16-byte
aligned, and carry their own pad count, and a dock given a malformed one
accepts it, paints nothing, and reports nothing. The KUnit tests compare
whole frames against a captured stream for that reason.
It is one codec parameterised by sample depth, not two: an HDR frame
differs from an SDR one only in how deep its samples are. A coefficient
is four times the sample, so every escape ceiling the entropy coder
holds gains two categories at ten bits, and the decoder configuration
handed to the dock states the same rise. The ceilings are part of the
wire format: at the maximum category an escape omits its unary
terminator, so a DC ceiling that disagrees desynchronises the dock's
decoder outright, while an AC one that is left behind stays in step and
reconstructs every sharp edge from a truncated magnitude.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/color.rs | 420 ++++++++
drivers/gpu/drm/vino/cp.rs | 8 +-
drivers/gpu/drm/vino/video.rs | 436 ++++++++
drivers/gpu/drm/vino/video/haar/records.rs | 985 +++++++++++++++++++
drivers/gpu/drm/vino/video/haar/strip.rs | 507 ++++++++++
drivers/gpu/drm/vino/video/haar/transform.rs | 802 +++++++++++++++
drivers/gpu/drm/vino/video_arm.rs | 243 +++++
7 files changed, 3397 insertions(+), 4 deletions(-)
create mode 100644 drivers/gpu/drm/vino/color.rs
create mode 100644 drivers/gpu/drm/vino/video.rs
create mode 100644 drivers/gpu/drm/vino/video/haar/records.rs
create mode 100644 drivers/gpu/drm/vino/video/haar/strip.rs
create mode 100644 drivers/gpu/drm/vino/video/haar/transform.rs
create mode 100644 drivers/gpu/drm/vino/video_arm.rs
diff --git a/drivers/gpu/drm/vino/color.rs b/drivers/gpu/drm/vino/color.rs
new file mode 100644
index 000000000000..050ddb12f617
--- /dev/null
+++ b/drivers/gpu/drm/vino/color.rs
@@ -0,0 +1,420 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Software colour management for the CRTC's `CTM` and `GAMMA_LUT` properties.
+//!
+//! There is no colour hardware to program: the dock is handed pixels that are already encoded.
+//! A compositor that colour-corrects through the KMS properties, as GNOME's Night Light and KDE's
+//! Night Colour both do rather than rewriting the framebuffer, therefore has nowhere to put the
+//! correction on such an output unless the driver applies it while it still has the pixels. This
+//! module is that application.
+//!
+//! Pipeline order follows DRM's: degamma, then CTM, then gamma. No degamma LUT is advertised, so
+//! what runs here is CTM then gamma.
+//!
+//! The transform is applied to the framebuffer's encoded (typically sRGB) values, not to linear
+//! light, because there is no degamma stage to linearise them first. That is the same
+//! simplification every software implementation of this makes, and it is what compositors expect
+//! when they compute a correction for a CRTC that advertises no degamma LUT.
+//!
+//! # Why the representation is an enum
+//!
+//! This runs on every pixel of every changed region, and `PixelSource::px` is the third-hottest
+//! symbol in the kernel under fullscreen video (13.8% of the machine on a 4K clip), so the
+//! per-pixel cost matters more than generality. A CTM that only scales each channel, which is the shape *every*
+//! colour-temperature corrector produces, collapses into per-channel lookup tables exactly as a
+//! gamma ramp does; [`ColorPipeline::Fused`] keeps that case at one table lookup per channel, the
+//! same cost as before CTM existed. Only a matrix that genuinely mixes channels pays for
+//! arithmetic.
+
+use kernel::drm::kms::crtc::{ColorCtm, ColorLut};
+
+/// Fixed-point scale for pixel values and matrix coefficients: 1.0 is `1 << 16`.
+///
+/// Q16 rather than the UAPI's S31.32 so that a coefficient-by-channel multiply is `i32 * i32` into
+/// an `i64`. S31.32 would need 128-bit intermediates, which are not available on every
+/// architecture the kernel builds for.
+const Q: i32 = 1 << 16;
+
+/// Largest Q16 pixel value, i.e. 1.0 in the pixel range.
+const MAX: i32 = 0xffff;
+
+/// Added before a `>> 16` so a fixed-point product rounds to nearest instead of truncating.
+///
+/// Truncation biases every multiply downwards by up to half a level, and the bias is systematic
+/// across the whole image rather than random -- a half-gain of 255 would come out at 127 instead
+/// of 128, and a corrected desktop would sit measurably darker than the correction asked for.
+const HALF_Q: i64 = 1 << 15;
+
+/// Entries in each channel's lookup table. Matches the `GAMMA_LUT_SIZE` advertised to userspace.
+pub(crate) const LUT_LEN: usize = 256;
+
+/// Expand an 8-bit channel to Q16 so that 0 -> 0 and 255 -> 65535 exactly.
+#[inline]
+fn expand(v: u8) -> i32 {
+ v as i32 * 257
+}
+
+/// Round a Q16 channel back to 8 bits.
+///
+/// The divisor is 257, not 256, because that is what [`expand`] multiplied by. Rounding by 256
+/// instead makes `narrow(expand(v))` drift above `v` -- 200 comes back as 201 -- so even an
+/// identity transform would shift the whole image.
+#[inline]
+fn narrow(v: i32) -> u8 {
+ ((v.clamp(0, MAX) + 128) / 257).min(255) as u8
+}
+
+/// Sample a 256-entry Q16 table at a Q16 input, interpolating between neighbours.
+///
+/// Straight indexing by the top 8 bits would quantise the CTM's output back to 8 bits before the
+/// gamma ramp ever saw it, which shows up as banding on the smooth gradients this is most often
+/// used on.
+#[inline]
+fn sample(table: &[u16], v: i32) -> i32 {
+ let v = v.clamp(0, MAX);
+ // Entry `i` of the table describes input `i * 257`, so the step is 257 -- the same divisor
+ // [`narrow`] uses, and for the same reason.
+ let idx = (v / 257) as usize;
+ let frac = v - idx as i32 * 257;
+ let a = table[idx.min(LUT_LEN - 1)] as i32;
+ let b = table[(idx + 1).min(LUT_LEN - 1)] as i32;
+ (a * (257 - frac) + b * frac + 128) / 257
+}
+
+/// A CRTC's colour transform, precomputed into whichever form is cheapest to apply per pixel.
+#[derive(Clone, Copy)]
+pub(crate) enum ColorPipeline {
+ /// Per-channel 8-bit tables, red then green then blue. Covers a gamma ramp alone and a gamma
+ /// ramp after a channel-independent CTM.
+ Fused([u8; 3 * LUT_LEN]),
+ /// A CTM that mixes channels, so it cannot collapse into per-channel tables. `ctm` is Q16,
+ /// row-major, and `gamma` is applied after it.
+ Mixed {
+ ctm: [i32; 9],
+ gamma: Option<[u16; 3 * LUT_LEN]>,
+ },
+}
+
+/// Read a DRM gamma blob into three Q16 channel tables, extending an under-length LUT with
+/// identity rather than with zeroes (which would render the output black).
+fn read_lut(lut: &[ColorLut]) -> [u16; 3 * LUT_LEN] {
+ let mut t = [0u16; 3 * LUT_LEN];
+ for i in 0..LUT_LEN {
+ let identity = (i * 257) as u16;
+ match lut.get(i) {
+ Some(e) => {
+ t[i] = e.red();
+ t[LUT_LEN + i] = e.green();
+ t[2 * LUT_LEN + i] = e.blue();
+ }
+ None => {
+ t[i] = identity;
+ t[LUT_LEN + i] = identity;
+ t[2 * LUT_LEN + i] = identity;
+ }
+ }
+ }
+ t
+}
+
+/// Convert a decoded S31.32 coefficient to Q16, saturating rather than wrapping.
+fn to_q16(v: i64) -> i32 {
+ (v >> 16).clamp(i32::MIN as i64, i32::MAX as i64) as i32
+}
+
+impl ColorPipeline {
+ /// Build the pipeline for a CRTC state's `CTM` and `GAMMA_LUT`, or [`None`] when neither is
+ /// programmed or both are the identity.
+ ///
+ /// [`None`] is not merely an optimisation: it is what keeps the direct-scanout path
+ /// available, so an uncorrected desktop pays nothing at all for this feature existing.
+ pub(crate) fn build(lut: Option<&[ColorLut]>, ctm: Option<&ColorCtm>) -> Option<Self> {
+ let gamma = lut.map(read_lut);
+ let matrix = ctm.map(|c| {
+ let raw = c.coefficients();
+ let mut m = [0i32; 9];
+ for (o, r) in m.iter_mut().zip(raw.iter()) {
+ *o = to_q16(*r);
+ }
+ m
+ });
+
+ // An identity matrix is what a compositor programs when it turns a corrector *off*, and it
+ // arrives as a real blob rather than as a removal. Treating it as "no CTM" is what lets
+ // the fast path come back afterwards.
+ let mixes = |m: &[i32; 9]| {
+ m[1] != 0 || m[2] != 0 || m[3] != 0 || m[5] != 0 || m[6] != 0 || m[7] != 0
+ };
+ let matrix = match matrix {
+ Some(m) if mixes(&m) => return Some(Self::mixed(m, gamma)),
+ Some(m) if m[0] == Q && m[4] == Q && m[8] == Q => None,
+ other => other,
+ };
+
+ if matrix.is_none() && gamma.is_none() {
+ return None;
+ }
+ Some(Self::fuse(matrix, gamma))
+ }
+
+ fn mixed(ctm: [i32; 9], gamma: Option<[u16; 3 * LUT_LEN]>) -> Self {
+ Self::Mixed { ctm, gamma }
+ }
+
+ /// Collapse a channel-independent CTM and a gamma ramp into one 8-bit table per channel.
+ ///
+ /// Done once per property change over 768 entries, so the per-pixel path never sees the
+ /// arithmetic.
+ fn fuse(diag: Option<[i32; 9]>, gamma: Option<[u16; 3 * LUT_LEN]>) -> Self {
+ let mut fused = [0u8; 3 * LUT_LEN];
+ // Diagonal entries of a row-major 3x3.
+ let gains = diag.map(|m| [m[0], m[4], m[8]]);
+ for c in 0..3 {
+ for i in 0..LUT_LEN {
+ let mut v = expand(i as u8);
+ if let Some(g) = gains {
+ v = ((g[c] as i64 * v as i64 + HALF_Q) >> 16).clamp(0, MAX as i64) as i32;
+ }
+ if let Some(t) = &gamma {
+ v = sample(&t[c * LUT_LEN..(c + 1) * LUT_LEN], v);
+ }
+ fused[c * LUT_LEN + i] = narrow(v.clamp(0, MAX));
+ }
+ }
+ Self::Fused(fused)
+ }
+
+ /// Apply the transform to one pixel.
+ #[inline]
+ pub(crate) fn apply(&self, r: u8, g: u8, b: u8) -> (u8, u8, u8) {
+ match self {
+ Self::Fused(t) => (
+ t[r as usize],
+ t[LUT_LEN + g as usize],
+ t[2 * LUT_LEN + b as usize],
+ ),
+ Self::Mixed { ctm, gamma } => {
+ let (r, g, b) = (expand(r) as i64, expand(g) as i64, expand(b) as i64);
+ let mul = |row: usize| {
+ let acc = ctm[row * 3] as i64 * r
+ + ctm[row * 3 + 1] as i64 * g
+ + ctm[row * 3 + 2] as i64 * b;
+ // Clamping here, before the gamma ramp, is what keeps an out-of-gamut
+ // intermediate from wrapping into the opposite corner of the colour cube.
+ (((acc + HALF_Q) >> 16).clamp(0, MAX as i64)) as i32
+ };
+ let mut out = [mul(0), mul(1), mul(2)];
+ if let Some(t) = gamma {
+ for (c, o) in out.iter_mut().enumerate() {
+ *o = sample(&t[c * LUT_LEN..(c + 1) * LUT_LEN], *o);
+ }
+ }
+ (
+ narrow(out[0].clamp(0, MAX)),
+ narrow(out[1].clamp(0, MAX)),
+ narrow(out[2].clamp(0, MAX)),
+ )
+ }
+ }
+ }
+
+ /// A value that changes whenever the transform does.
+ ///
+ /// The encoded-strip cache keys on the pixels a strip contained, so a transform change that
+ /// leaves the source pixels alone would otherwise serve a stale body.
+ pub(crate) fn tag(&self) -> u64 {
+ const SEED: u64 = 0x9e37_79b1_85eb_ca87;
+ match self {
+ Self::Fused(t) => kernel::xxhash::xxh64(&t[..], SEED),
+ Self::Mixed { ctm, gamma } => {
+ let mut bytes = [0u8; 9 * 4];
+ for (i, c) in ctm.iter().enumerate() {
+ bytes[i * 4..i * 4 + 4].copy_from_slice(&c.to_le_bytes());
+ }
+ let h = kernel::xxhash::xxh64(&bytes, SEED);
+ match gamma {
+ Some(t) => {
+ let mut g = [0u8; 2 * 3 * LUT_LEN];
+ for (i, v) in t.iter().enumerate() {
+ g[i * 2..i * 2 + 2].copy_from_slice(&v.to_le_bytes());
+ }
+ kernel::xxhash::xxh64(&g, h)
+ }
+ None => h,
+ }
+ }
+ }
+ }
+}
+
+impl PartialEq for ColorPipeline {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (Self::Fused(a), Self::Fused(b)) => a[..] == b[..],
+ (Self::Mixed { ctm: a, gamma: ga }, Self::Mixed { ctm: b, gamma: gb }) => {
+ a == b
+ && match (ga, gb) {
+ (Some(x), Some(y)) => x[..] == y[..],
+ (None, None) => true,
+ _ => false,
+ }
+ }
+ _ => false,
+ }
+ }
+}
+
+// This module is shared verbatim, so the tests are gated on either driver's KUnit symbol and
+// name nothing outside it.
+#[cfg(any(CONFIG_DRM_VINO_KUNIT_TEST, CONFIG_DRM_EVDI_KUNIT_TEST))]
+use kernel::prelude::kunit_tests;
+
+#[cfg(any(CONFIG_DRM_VINO_KUNIT_TEST, CONFIG_DRM_EVDI_KUNIT_TEST))]
+#[kunit_tests(drm_color_pipeline)]
+mod tests {
+ use super::*;
+ use kernel::error::code::EINVAL;
+ use kernel::prelude::*;
+
+ /// S31.32 sign-magnitude constants: 1.0, +0.5, and -0.5 as sign bit + magnitude.
+ const CTM_ONE: u64 = 1 << 32;
+ const CTM_HALF: u64 = 1 << 31;
+ const CTM_NEG_HALF: u64 = (1u64 << 63) | (1u64 << 31);
+
+ fn ctm_diag(r: u64, g: u64, b: u64) -> ColorCtm {
+ ColorCtm::from_raw([r, 0, 0, 0, g, 0, 0, 0, b])
+ }
+
+ /// A ramp that halves every channel, at the LUT's full 16-bit precision. The `+ 1` rounds:
+ /// entry 255 is 65535/2 = 32767.5, and truncating it would make the fixture itself ask for
+ /// 127 rather than 128.
+ fn half_lut() -> KVec<ColorLut> {
+ let mut v = KVec::new();
+ for i in 0..LUT_LEN {
+ let h = ((i * 257 + 1) / 2) as u16;
+ let _ = v.push(ColorLut::new(h, h, h), GFP_KERNEL);
+ }
+ v
+ }
+
+ #[test]
+ fn ctm_decodes_sign_magnitude_not_twos_complement() -> Result {
+ // The UAPI encodes CTM entries in sign-magnitude. Reading the u64 as an i64 would make
+ // -0.5 come back as a huge positive number and saturate instead of darkening.
+ let m = ctm_diag(CTM_ONE, CTM_NEG_HALF, CTM_ONE);
+ assert_eq!(m.coefficient(0), Some(1i64 << 32));
+ assert_eq!(m.coefficient(4), Some(-(1i64 << 31)));
+ assert_eq!(m.coefficient(9), None);
+ Ok(())
+ }
+
+ #[test]
+ fn identity_transform_builds_nothing() -> Result {
+ // Turning a corrector off programs an identity matrix rather than removing the blob. If
+ // that did not collapse to None the encoder would never regain its direct-scanout path.
+ assert!(ColorPipeline::build(None, None).is_none());
+ let ident = ctm_diag(CTM_ONE, CTM_ONE, CTM_ONE);
+ assert!(ColorPipeline::build(None, Some(&ident)).is_none());
+ Ok(())
+ }
+
+ #[test]
+ fn identity_gamma_ramp_is_a_no_op() -> Result {
+ // The reason `narrow` divides by 257 and not 256. With the wrong divisor every value above
+ // about 128 came back one level high, so merely *enabling* colour management shifted the
+ // whole image even when the ramp asked for nothing.
+ let mut lut = KVec::new();
+ for i in 0..LUT_LEN {
+ let v = (i * 257) as u16;
+ let _ = lut.push(ColorLut::new(v, v, v), GFP_KERNEL);
+ }
+ let p = ColorPipeline::build(Some(&lut), None).ok_or(EINVAL)?;
+ for v in 0..=255u8 {
+ assert_eq!(p.apply(v, v, v), (v, v, v));
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn gamma_only_applies_the_ramp() -> Result {
+ let lut = half_lut();
+ let p = ColorPipeline::build(Some(&lut), None).ok_or(EINVAL)?;
+ assert_eq!(p.apply(0, 0, 0), (0, 0, 0));
+ assert_eq!(p.apply(255, 255, 255), (128, 128, 128));
+ Ok(())
+ }
+
+ #[test]
+ fn diagonal_ctm_matches_the_general_matrix() -> Result {
+ // The diagonal fast path exists for speed; if it ever disagreed with the general path the
+ // colour would silently change with the optimisation rather than with the CTM.
+ let fast = ColorPipeline::build(None, Some(&ctm_diag(CTM_ONE, CTM_HALF, CTM_ONE)))
+ .ok_or(EINVAL)?;
+ // The same transform with a real off-diagonal zero-effect term, so it must take the
+ // mixing path. A sub-Q16 term would be truncated to zero and stay on the fast path.
+ let mixed =
+ ColorCtm::from_raw([CTM_ONE, 0, CTM_ONE / 65536, 0, CTM_HALF, 0, 0, 0, CTM_ONE]);
+ let slow = ColorPipeline::build(None, Some(&mixed)).ok_or(EINVAL)?;
+ for v in [0u8, 1, 63, 127, 128, 200, 254, 255] {
+ assert_eq!(fast.apply(v, v, v), slow.apply(v, v, v));
+ }
+ assert_eq!(fast.apply(255, 255, 255), (255, 128, 255));
+ Ok(())
+ }
+
+ #[test]
+ fn mixing_ctm_moves_channels() -> Result {
+ // Swap red and blue: proves the matrix is row-major and applied the way the UAPI documents.
+ let swap = ColorCtm::from_raw([0, 0, CTM_ONE, 0, CTM_ONE, 0, CTM_ONE, 0, 0]);
+ let p = ColorPipeline::build(None, Some(&swap)).ok_or(EINVAL)?;
+ assert_eq!(p.apply(200, 100, 50), (50, 100, 200));
+ Ok(())
+ }
+
+ #[test]
+ fn negative_coefficient_clamps_to_black() -> Result {
+ let p = ColorPipeline::build(None, Some(&ctm_diag(CTM_ONE, CTM_NEG_HALF, CTM_ONE)))
+ .ok_or(EINVAL)?;
+ assert_eq!(p.apply(255, 255, 255), (255, 0, 255));
+ Ok(())
+ }
+
+ #[test]
+ fn out_of_gamut_saturates_instead_of_wrapping() -> Result {
+ // An intermediate above 1.0 must clamp. Wrapping would put the brightest pixels at the
+ // opposite corner of the colour cube -- the failure looks like inverted highlights.
+ let gain4 = ctm_diag(4 * CTM_ONE, 4 * CTM_ONE, 4 * CTM_ONE);
+ let p = ColorPipeline::build(None, Some(&gain4)).ok_or(EINVAL)?;
+ assert_eq!(p.apply(200, 100, 255), (255, 255, 255));
+ assert_eq!(p.apply(0, 0, 0), (0, 0, 0));
+ Ok(())
+ }
+
+ #[test]
+ fn short_lut_extends_with_identity_not_black() -> Result {
+ // A LUT blob shorter than the advertised size must not leave the tail at zero, which would
+ // render everything above the truncation point black.
+ let mut lut = KVec::new();
+ for i in 0..4usize {
+ let v = (i * 257) as u16;
+ let _ = lut.push(ColorLut::new(v, v, v), GFP_KERNEL);
+ }
+ let p = ColorPipeline::build(Some(&lut), None).ok_or(EINVAL)?;
+ assert_eq!(p.apply(255, 255, 255), (255, 255, 255));
+ Ok(())
+ }
+
+ #[test]
+ fn transform_change_changes_the_strip_cache_tag() -> Result {
+ // The encoded-strip cache keys on source pixels, so a transform change that leaves the
+ // pixels alone must still invalidate it or the whole screen keeps its old colours.
+ let a = ColorPipeline::build(None, Some(&ctm_diag(CTM_ONE, CTM_HALF, CTM_ONE)))
+ .ok_or(EINVAL)?;
+ let b = ColorPipeline::build(None, Some(&ctm_diag(CTM_HALF, CTM_ONE, CTM_ONE)))
+ .ok_or(EINVAL)?;
+ assert_ne!(a.tag(), b.tag());
+ // `assert!` rather than `assert_ne!`: the latter needs `Debug`, and deriving it on a type
+ // holding 768-entry tables is code the driver would carry purely for one test message.
+ assert!(a != b);
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/drm/vino/cp.rs b/drivers/gpu/drm/vino/cp.rs
index 0afbf64974db..24a739654db6 100644
--- a/drivers/gpu/drm/vino/cp.rs
+++ b/drivers/gpu/drm/vino/cp.rs
@@ -1492,8 +1492,8 @@ fn navarro_pipe_descriptor_matches_authenticated_capture() -> Result {
// 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)?;
+ let header = video_arm::mode_header(2560, 1440, 0x2100, false);
+ let config = video_arm::build_config(video_arm::CodeTables::Wide, &header, &tail, false)?;
assert_eq!(config.len(), 1104);
assert_eq!(
&config[..26],
@@ -1545,8 +1545,8 @@ fn ella_stream_records_match_the_captured_bytes() -> Result {
// 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, &[])?;
+ let header = video_arm::mode_header(1920, 1088, 0x1800, false);
+ let config = video_arm::build_config(video_arm::CodeTables::Narrow, &header, &[], false)?;
assert_eq!(config.len(), 304);
assert_eq!(
&config[..26],
diff --git a/drivers/gpu/drm/vino/video.rs b/drivers/gpu/drm/vino/video.rs
new file mode 100644
index 000000000000..5ee4eef2f83a
--- /dev/null
+++ b/drivers/gpu/drm/vino/video.rs
@@ -0,0 +1,436 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! DisplayLink full-colour video encoder and framing.
+
+use super::*;
+
+/// Pack 8-bit RGB into RGB565.
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+pub(crate) fn rgb565(r: u8, g: u8, b: u8) -> u16 {
+ ((r as u16 >> 3) << 11) | ((g as u16 >> 2) << 5) | (b as u16 >> 3)
+}
+
+/// DisplayLink's 8x8 multilevel Haar codec and 64x16 strip grammar.
+pub(crate) mod haar {
+ mod records;
+ mod strip;
+ mod transform;
+
+ pub(crate) use records::*;
+ pub(crate) use strip::*;
+ pub(crate) use transform::*;
+
+ use super::*;
+
+ /// Per-coefficient `(shift, bias)` quantization parameters.
+ ///
+ /// All steps are powers of two. Arithmetic right shift therefore implements the required floor
+ /// division for both positive and negative coefficients without an integer division.
+ const fn step_bias(i: usize) -> (u32, i32) {
+ match i {
+ 0 => (4, 8),
+ 1 | 2 => (4, 8),
+ 3 => (5, 16),
+ 4..=11 => (2, 2),
+ 12..=15 => (3, 4),
+ 16..=47 => (1, 0),
+ _ => (2, 2), // 48..=63
+ }
+ }
+
+ /// [`step_bias`] and the chroma-AC shift, resolved for every coefficient at compile time.
+ ///
+ /// Both are pure functions of the coefficient index over only 64 inputs, but they were being
+ /// evaluated per coefficient -- 64 times per plane, three planes per block, sixteen blocks per
+ /// strip. Profiling `colour_block` found the resulting range-test chains dominating it: 190
+ /// compare and branch instructions against 61 arithmetic ones. A table turns each into a load.
+ static STEP_BIAS: [(u32, i32); COEFFS] = {
+ let mut t = [(0u32, 0i32); COEFFS];
+ let mut i = 0;
+ while i < COEFFS {
+ t[i] = step_bias(i);
+ i += 1;
+ }
+ t
+ };
+
+ static CHROMA_AC_SHIFT: [u32; COEFFS] = {
+ let mut t = [0u32; COEFFS];
+ let mut i = 0;
+ while i < COEFFS {
+ t[i] = if matches!(i, 1 | 2 | 4..=11) {
+ 4
+ } else if i >= 48 {
+ 6
+ } else {
+ 5
+ };
+ i += 1;
+ }
+ t
+ };
+
+ /// Quantize coefficient `coeff` at position `i`: `sign(coeff) * floor((|coeff| + bias) / step)`
+ /// and clamp it to the 12-bit signed long-token range.
+ pub(crate) fn quantize(coeff: i32, i: usize) -> i32 {
+ let (shift, bias) = STEP_BIAS[i];
+ // Coarse bands round half-up on the signed value; the finest bands truncate towards zero.
+ let q = if bias == 0 {
+ let q = coeff.abs() >> shift;
+ if coeff < 0 {
+ -q
+ } else {
+ q
+ }
+ } else {
+ (coeff + bias) >> shift
+ };
+ q.clamp(-2048, 2047)
+ }
+
+ // The AC ceilings deliberately do not vary with depth. The DC one has to, because a DC is a
+ // direct multiple of the sample and 10-bit content immediately overflows category 10. No
+ // capture shows an AC ceiling above 8-bit's: the largest luma AC coefficient measured on
+ // 10-bit content is |273|, comfortably inside category 9, because the host tone-maps to the
+ // sink's peak luminance before the codec sees it. Guessing upward is the unsafe direction --
+ // `esc` saturates a magnitude whose category exceeds the ceiling, so an under-sized ceiling
+ // clips extreme AC detail while an over-sized one desynchronises the dock. Raise these only
+ // against a capture that needs them.
+
+ // Colour strip codec (Cb/Cr planes).
+ //
+ // Per block the 3 planes are (Cr=64*(B-G), Cb=64*(R-G), Y=64*G + 64*((Cb+Cr)>>2)).
+ // * SYNC unit = [Cr field][Cb field][Y field]; chroma fields present only when last>0
+ // (the per-block plane mask), Y field always present (luma `sync_unit`).
+ // * DC plane = 16-block DPCM (Cr,Cb,Y), 3 tokens/block, chroma step 64 / luma step 16,
+ // round-half-up on the signed value.
+ // * AC rows (row0 blocks 0..8, row1 8..16): per block (Cr,Cb,Y) present planes, chroma
+ // quant flat step 16 (truncate toward zero), positions 1..last, run-bit `0` for zeros.
+ // * Strip length = w1c + round_even(row1) (the 2-byte tail overlaps row1's tail).
+
+ /// Encode a full `width`x`height` RGB frame into Haar colour records. `px(x, y)` yields the
+ /// source pixel's `(R, G, B)`; the caller applies rotation, gamma and format conversion. The
+ /// surface is tiled into 64x16 strips in raster order, each built from [`colour_block`] +
+ /// [`colour_strip`], and the strip stream is framed for the wire by [`frame_records`] using
+ /// the EP08 TLV layout. The frame counter belongs to the records that name a ring slot, not
+ /// here.
+ ///
+ /// `width`/`height` must be multiples of 64 and 16 (`EINVAL` otherwise). Live scanout pads a
+ /// non-aligned mode with black to this strip grid while preserving the real mode dimensions.
+ /// This function remains out of line to bound kernel stack use.
+ #[inline(never)]
+ pub(crate) fn colour_frame_ep08(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ connector: u8,
+ mut px: impl FnMut(usize, usize) -> (u16, u16, u16),
+ ) -> Result<KVec<KVec<u8>>> {
+ colour_frame_ep08_variant(geometry, width, height, connector, None, &mut px)
+ }
+
+ /// Encode a full live Navarro frame using the ordinary-frame producer permutation measured
+ /// from DLM. The first ordinary band is y=8, not y=0, and the captured worker boundaries are
+ /// part of the record grammar for a 2560x1440 surface.
+ pub(crate) fn colour_frame_ep08_navarro_ordinary(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ connector: u8,
+ mut px: impl FnMut(usize, usize) -> (u16, u16, u16),
+ ) -> Result<KVec<KVec<u8>>> {
+ colour_frame_ep08_variant(geometry, width, height, connector, Some(true), &mut px)
+ }
+
+ fn colour_frame_ep08_variant(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ connector: u8,
+ navarro_ordinary: Option<bool>,
+ px: &mut impl FnMut(usize, usize) -> (u16, u16, u16),
+ ) -> Result<KVec<KVec<u8>>> {
+ if width & (geometry.strip_w() - 1) != 0 || height & (geometry.strip_h() - 1) != 0 {
+ return Err(kernel::error::code::EINVAL);
+ }
+ // Build every strip body (raster order; each strip's natural row1 tail, no echo).
+ let mut strips: KVec<KVec<u8>> = KVec::new();
+ let mut sy = 0usize;
+ while sy < height {
+ let mut sx = 0usize;
+ while sx < width {
+ let blocks = colour_strip_blocks(geometry, sx, sy, px)?;
+ strips.push(
+ colour_strip(geometry, &blocks, sx as u16, sy as u16)?,
+ GFP_KERNEL,
+ )?;
+ sx += geometry.strip_w();
+ }
+ sy += geometry.strip_h();
+ }
+ frame_records_with_boundary(
+ geometry,
+ &strips,
+ connector,
+ navarro_ordinary.filter(|_| strips.len() == 3600),
+ )
+ }
+
+ /// Build a valid all-black Haar frame without sampling or transforming a framebuffer.
+ ///
+ /// This is the post-mode-set training carrier. Its zero-coefficient blocks are built once and
+ /// reused, so construction is proportional to the strip grid rather than the pixel count. A
+ /// real framebuffer keyframe follows.
+ pub(crate) fn black_frame_ep08(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ connector: u8,
+ ) -> Result<KVec<KVec<u8>>> {
+ black_frame_ep08_variant(geometry, width, height, connector, Some(false))
+ }
+
+ /// Build the ordinary Navarro black carrier which follows the prologue frame.
+ ///
+ /// DLM's ordinary 2560x1440 carriers contain the same 201600 bytes of strip payload as the
+ /// prologue, but split them across 53 image records rather than 52. Its additional boundary is
+ /// after strip 2804, making the complete frame 208624 bytes. Navarro accepts vino's 208608-byte
+ /// second frame and then NAKs the first transfer of frame three, so this distinction is part of
+ /// the producer grammar rather than harmless USB chunking.
+ pub(crate) fn black_frame_ep08_ordinary(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ connector: u8,
+ ) -> Result<KVec<KVec<u8>>> {
+ black_frame_ep08_variant(geometry, width, height, connector, Some(true))
+ }
+
+ fn black_frame_ep08_variant(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ connector: u8,
+ navarro_ordinary: Option<bool>,
+ ) -> Result<KVec<KVec<u8>>> {
+ if width & (geometry.strip_w() - 1) != 0 || height & (geometry.strip_h() - 1) != 0 {
+ return Err(kernel::error::code::EINVAL);
+ }
+ let mut blocks: KVec<ColourBlock> = KVec::with_capacity(STRIP_BLOCKS, GFP_KERNEL)?;
+ for _ in 0..STRIP_BLOCKS {
+ blocks.push(
+ ColourBlock {
+ qcr: [0; COEFFS],
+ qcb: [0; COEFFS],
+ qy: [0; COEFFS],
+ lcr: 0,
+ lcb: 0,
+ ly: 0,
+ },
+ GFP_KERNEL,
+ )?;
+ }
+ let mut strips: KVec<KVec<u8>> = KVec::new();
+ let mut sy = 0usize;
+ while sy < height {
+ let mut sx = 0usize;
+ while sx < width {
+ strips.push(
+ colour_strip(geometry, &blocks, sx as u16, sy as u16)?,
+ GFP_KERNEL,
+ )?;
+ sx += geometry.strip_w();
+ }
+ sy += geometry.strip_h();
+ }
+ frame_records_with_boundary(geometry, &strips, connector, navarro_ordinary)
+ }
+
+ /// The raster-ordered top-left coordinate of every strip a damage set selects.
+ ///
+ /// Split out of [`colour_frame_ep08_damage`] so the serial and parallel encoders select
+ /// exactly the same strips in exactly the same order. The order is load-bearing:
+ /// [`frame_records`] groups strips into one record per single-Y band and requires them
+ /// x-ordered within each band, so reordering here changes the wire format.
+ ///
+ /// A strip is selected when a clip overlaps its 256x64 macro-tile. Every strip in a touched
+ /// tile is resent.
+ pub(crate) fn damage_strip_coords(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ clips: &[(usize, usize, usize, usize)],
+ ) -> Result<KVec<(usize, usize)>> {
+ let mut coords: KVec<(usize, usize)> = KVec::new();
+ let (mw, mh) = (geometry.macro_w(), geometry.macro_h());
+ let mut sy = 0usize;
+ while sy < height {
+ let mut sx = 0usize;
+ while sx < width {
+ let mx = sx & !(mw - 1);
+ let my = sy & !(mh - 1);
+ let hit = clips
+ .iter()
+ .any(|&(x0, y0, x1, y1)| mx < x1 && x0 < mx + mw && my < y1 && y0 < my + mh);
+ if hit {
+ coords.push((sx, sy), GFP_KERNEL)?;
+ }
+ sx += geometry.strip_w();
+ }
+ sy += geometry.strip_h();
+ }
+ Ok(coords)
+ }
+
+ /// Every strip of a full frame, in the same raster order as [`damage_strip_coords`].
+ pub(crate) fn all_strip_coords(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ ) -> Result<KVec<(usize, usize)>> {
+ let mut coords: KVec<(usize, usize)> = KVec::new();
+ let mut sy = 0usize;
+ while sy < height {
+ let mut sx = 0usize;
+ while sx < width {
+ coords.push((sx, sy), GFP_KERNEL)?;
+ sx += geometry.strip_w();
+ }
+ sy += geometry.strip_h();
+ }
+ Ok(coords)
+ }
+
+ /// Damage-aware variant of [`colour_frame_ep08`]. It encodes only the macro-tiles selected by
+ /// the client damage rectangles.
+ ///
+ /// `clips` are `(x0, y0, x1, y1)` half-open rectangles in output/source pixels (identity
+ /// rotation only -- the caller sends a full [`colour_frame_ep08`] otherwise). A strip at
+ /// `(sx, sy)` is included iff some clip overlaps
+ /// `[sx, sx+strip_w()) x [sy, sy+strip_h())`. Raster iteration keeps strips
+ /// x-ordered within each y-band, so [`frame_records`] groups them as the
+ /// full-frame path does. Returns an empty frame list when no strip is
+ /// touched -- the caller must skip the USB write in that case (no-op
+ /// flip). The first frame after a mode-set must still be a full
+ /// keyframe (the dock's framebuffer is undefined until then).
+ ///
+ /// This function remains out of line for the same stack bound as [`colour_frame_ep08`].
+ #[inline(never)]
+ pub(crate) fn colour_frame_ep08_damage(
+ geometry: Geometry,
+ width: usize,
+ height: usize,
+ connector: u8,
+ clips: &[(usize, usize, usize, usize)],
+ mut px: impl FnMut(usize, usize) -> (u16, u16, u16),
+ ) -> Result<KVec<KVec<u8>>> {
+ if width & (geometry.strip_w() - 1) != 0 || height & (geometry.strip_h() - 1) != 0 {
+ return Err(kernel::error::code::EINVAL);
+ }
+ let coords = damage_strip_coords(geometry, width, height, clips)?;
+ let mut strips: KVec<KVec<u8>> = KVec::with_capacity(coords.len(), GFP_KERNEL)?;
+ for &(sx, sy) in coords.iter() {
+ let blocks = colour_strip_blocks(geometry, sx, sy, &mut px)?;
+ strips.push(
+ colour_strip(geometry, &blocks, sx as u16, sy as u16)?,
+ GFP_KERNEL,
+ )?;
+ }
+ frame_records(geometry, &strips, connector)
+ }
+
+ // Exact producer completion order from DLM's authenticated 2560x1440 cold capture. Navarro
+ // stops draining immediately after vino's first ordering mismatch, at strip 300. Rows alone
+ // encode almost the whole permutation; the handful of split rows below are worker boundaries.
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_video)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn rgb565_packing() {
+ assert_eq!(rgb565(0xff, 0x00, 0x00), 0xf800);
+ assert_eq!(rgb565(0x00, 0xff, 0x00), 0x07e0);
+ assert_eq!(rgb565(0x00, 0x00, 0xff), 0x001f);
+ }
+
+ #[test]
+ fn colour_frame_ep08_damage_selects_changed_strips() -> Result {
+ // Deterministic gradient source (a plain fn item so it's Copy/reusable across calls).
+ fn g(x: usize, y: usize) -> (u16, u16, u16) {
+ (
+ ((x * 7) & 0xff) as u16,
+ ((y * 5) & 0xff) as u16,
+ (((x + y) * 3) & 0xff) as u16,
+ )
+ }
+ let total = |fs: &KVec<KVec<u8>>| fs.iter().map(|f| f.len()).sum::<usize>();
+ let flat = |fs: &KVec<KVec<u8>>| -> Result<KVec<u8>> {
+ let mut v = KVec::new();
+ for f in fs.iter() {
+ v.extend_from_slice(f, GFP_KERNEL)?;
+ }
+ Ok(v)
+ };
+ // Damage granularity is the 256x64 macro-tile (`MACRO_W`/`MACRO_H`), not the 64x16 strip:
+ // every strip of a touched macro-tile is resent. Use several macro-tiles so the partial
+ // update assertions below can distinguish one selected tile from the full frame.
+ //
+ // 512x128 = 8 strips wide (512/64) x 8 bands (128/16) = 64 strips
+ // = 2 x 2 macro-tiles, each 4 strips wide x 4 bands = 16 strips.
+ let (w, h) = (512usize, 128usize);
+ const STRIPS_PER_MACRO: usize = 16;
+ let geometry = profile::PROFILE_RIDGE.geometry();
+ let full = haar::colour_frame_ep08(geometry, w, h, 0, g)?;
+
+ // A damage clip covering the WHOLE surface selects every strip in the same raster order as
+ // the full-frame path, so the wire bytes are identical.
+ let dfull = haar::colour_frame_ep08_damage(geometry, w, h, 0, &[(0, 0, w, h)], g)?;
+ assert_eq!(flat(&full)?.as_slice(), flat(&dfull)?.as_slice());
+
+ // No damage -> no strips -> empty frame list (caller must skip the USB write).
+ let empty = haar::colour_frame_ep08_damage(geometry, w, h, 0, &[], g)?;
+ assert!(empty.is_empty());
+
+ // Selection is exact and macro-tile-quantised. Assert the strip COUNT directly (the shared
+ // selector both encoders use) as well as the byte totals -- a count is a far sharper
+ // statement than "smaller than full", and it is what actually pins the tiling behaviour.
+ let coords = |clips: &[(usize, usize, usize, usize)]| -> Result<usize> {
+ Ok(haar::damage_strip_coords(geometry, w, h, clips)?.len())
+ };
+ assert_eq!(coords(&[])?, 0);
+ assert_eq!(coords(&[(0, 0, w, h)])?, 4 * STRIPS_PER_MACRO); // all four macro-tiles
+
+ // A 1-pixel clip lands in ONE macro-tile and selects all 16 of its strips -- not 1.
+ assert_eq!(coords(&[(1, 1, 2, 2)])?, STRIPS_PER_MACRO);
+ let d1 = haar::colour_frame_ep08_damage(geometry, w, h, 0, &[(1, 1, 2, 2)], g)?;
+ assert!(!d1.is_empty());
+ assert!(total(&d1) < total(&full));
+
+ // A 1-pixel-wide clip down the whole left edge spans the left macro-tile COLUMN: 2 tiles.
+ assert_eq!(coords(&[(0, 0, 1, h)])?, 2 * STRIPS_PER_MACRO);
+ let d2 = haar::colour_frame_ep08_damage(geometry, w, h, 0, &[(0, 0, 1, h)], g)?;
+ assert!(total(&d1) < total(&d2) && total(&d2) < total(&full));
+
+ // Non-aligned geometry is rejected (same contract as colour_frame_ep08).
+ assert!(haar::colour_frame_ep08_damage(geometry, 100, 32, 0, &[(0, 0, 1, 1)], g).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn black_training_frame_matches_captured_1440p_size() -> Result {
+ // Captured first writes are 205,696 bytes:
+ // 2,560-byte arm prefix + 203,040-byte black image + 96-byte frame trailer.
+ let geometry = profile::PROFILE_RIDGE.geometry();
+ let frame = haar::black_frame_ep08(geometry, 2560, 1440, 0)?;
+ let image_len = frame.iter().map(|part| part.len()).sum::<usize>();
+ assert_eq!(image_len, 203_040);
+ assert_eq!(
+ 2_560 + image_len + haar::frame_trailer(geometry, 0, 0).len(),
+ 205_696
+ );
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/drm/vino/video/haar/records.rs b/drivers/gpu/drm/vino/video/haar/records.rs
new file mode 100644
index 000000000000..422ae7d83ae2
--- /dev/null
+++ b/drivers/gpu/drm/vino/video/haar/records.rs
@@ -0,0 +1,985 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Framing strips into the records a frame is made of.
+//!
+//! Record stride, padding, sequence and sub-band coordinates are all checked by the dock, and
+//! a frame whose records are mis-framed is accepted byte for byte and displayed as nothing.
+
+use super::*;
+
+/// Write the sixteen-byte header every record on every one of these docks begins with.
+///
+/// ```text
+/// off 0..2 zero
+/// off 2..4 size : u16 the stride less four, so a record ends at `size + 4`
+/// off 4..8 type : u32 2 for a plaintext marker, 4 for everything else
+/// off 8..10 sub : u16 the plane: a connector for video, a control sub otherwise
+/// off 10..12 aux : u16 the trailing pad count on an image record, a subtype on others
+/// off 12..16 seq : u32 the sealed stream's AES-CTR block counter, else zero
+/// ```
+///
+/// Verified unchanged across all three generations against 92,072 captured records, which is
+/// why it is written once. `out` must be the whole record: its length fixes `size`.
+pub(crate) fn record_header(out: &mut [u8], kind: u32, sub: u16, aux: u16, seq: u32) {
+ let size = (out.len() - 4) as u16;
+ out[0..2].fill(0);
+ out[2..4].copy_from_slice(&size.to_le_bytes());
+ out[4..8].copy_from_slice(&kind.to_le_bytes());
+ out[8..10].copy_from_slice(&sub.to_le_bytes());
+ out[10..12].copy_from_slice(&aux.to_le_bytes());
+ out[12..16].copy_from_slice(&seq.to_le_bytes());
+}
+/// The shape of an encoded frame, read back off the records themselves.
+///
+/// Returns `(records, largest record stride, largest strip)`. The wire invariants are that a
+/// stride never passes the 4080-byte cap and that a strip fits inside a record; the sizes DLM
+/// reaches for comparison are 1758 bytes per strip on DL-6xxx, 1780 on the DL-7400 and 2036 on
+/// DL-3x00. Reported when a frame fails to reach the dock, because a dock refuses a malformed
+/// record by halting the endpoint several transfers later, where it is indistinguishable from
+/// any other transport fault.
+pub(crate) fn record_stats(chunks: &[KVec<u8>]) -> (usize, usize, usize) {
+ let (mut records, mut max_stride, mut max_strip) = (0usize, 0usize, 0usize);
+ for chunk in chunks {
+ let mut off = 0usize;
+ while off + 16 <= chunk.len() {
+ let size = u16::from_le_bytes([chunk[off + 2], chunk[off + 3]]) as usize;
+ let stride = size + 4;
+ if stride < 16 || off + stride > chunk.len() {
+ break;
+ }
+ let aux = u16::from_le_bytes([chunk[off + 10], chunk[off + 11]]) as usize;
+ let body = &chunk[off + 16..off + stride];
+ let payload = body.len().saturating_sub(aux);
+ let mut at = 0usize;
+ while at + 2 <= payload {
+ let len = u16::from_le_bytes([body[at], body[at + 1]]) as usize;
+ if len == 0 || at + 2 + len > payload {
+ break;
+ }
+ max_strip = max_strip.max(len);
+ at += 2 + len;
+ }
+ max_stride = max_stride.max(stride);
+ records += 1;
+ off += stride;
+ }
+ }
+ (records, max_stride, max_strip)
+}
+/// Frame a raster-ordered list of strip bodies into EP08 records:
+///
+/// ```text
+/// record (one per single-Y band of strips):
+/// u16 pad = 0
+/// u16 size = total record length (TLV..trailer, excludes the inter-record gap)
+/// u32 type = 4
+/// u16 sub = connector | (((y / 16) & 1) << 4)
+/// u16 aux = zero-padding byte count
+/// u32 fseq = 0
+/// per strip: u16 strip_id (== strip length) ++ strip bytes
+/// u8[aux] zero padding so the complete `size + 4` stride is 16-byte aligned
+/// ```
+///
+/// The record stream is chunked internally into small record-aligned buffers. The sink streams
+/// those fragments directly into persistent 65536-byte URBs, so the internal boundaries are
+/// not USB framing. Complete record strides are limited to 4080 bytes.
+pub(crate) fn frame_records(
+ geometry: Geometry,
+ strips: &[KVec<u8>],
+ connector: u8,
+) -> Result<KVec<KVec<u8>>> {
+ frame_records_with_boundary(geometry, strips, connector, None)
+}
+/// Frame a full live Navarro surface with the ordinary DLM producer order. Other modes and
+/// damage subsets deliberately fall back to the generic interlaced order: the measured
+/// permutation and its split-worker boundaries describe exactly 3600 128x8 strips.
+pub(crate) fn frame_records_navarro_ordinary(
+ geometry: Geometry,
+ strips: &[KVec<u8>],
+ connector: u8,
+) -> Result<KVec<KVec<u8>>> {
+ frame_records_with_boundary(
+ geometry,
+ strips,
+ connector,
+ (strips.len() == 3600).then_some(true),
+ )
+}
+pub(crate) const NAVARRO_PROLOGUE_ROWS: &[u8] = &[
+ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47,
+ 49, 51, 53, 54, 57, 59, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94,
+ 96, 98, 100, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 30, 32, 34, 36, 38, 40, 42, 44,
+ 46, 48, 50, 52, 55, 56, 58, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93,
+ 95, 97, 99, 101, 102, 103, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134,
+ 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172,
+ 174, 176, 178, 100, 104, 105, 106, 107, 108, 109, 111, 113, 115, 117, 119, 121, 123, 125, 127,
+ 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165,
+ 167, 169, 171, 173, 175, 177, 179,
+];
+
+// Producer band order for a 1920x1080 DL-3x00 surface: 30 strips across x 68 bands of 64x16.
+// Taken from DLM's own stream. Like the DL7400, this dock stops draining at the first band it
+// did not expect, so the generic even-then-odd interlace is not close enough -- it diverges at
+// the second band, sending 2 where the dock wants 3. No band appears twice, so unlike the
+// DL7400 there are no split-row producer boundaries to reproduce.
+/// The DL-3x00 producer band order, exposed so a selftest can pin it.
+pub(crate) fn ella_rows_1080p() -> &'static [u8] {
+ ELLA_ROWS_1080P
+}
+pub(crate) const ELLA_ROWS_1080P: &[u8] = &[
+ 0, 3, 5, 7, 9, 11, 13, 16, 18, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 44, 46, 48, 50,
+ 51, 53, 55, 56, 58, 60, 61, 63, 65, 66, 1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 20, 22, 24, 26,
+ 28, 30, 32, 34, 36, 38, 40, 42, 45, 47, 49, 52, 54, 57, 59, 62, 64, 67,
+];
+
+pub(crate) const NAVARRO_ORDINARY_ROWS: &[u8] = &[
+ 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49,
+ 51, 53, 55, 57, 59, 61, 63, 64, 66, 68, 70, 72, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95,
+ 97, 99, 0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 28, 30, 32, 34, 36, 38, 40, 42, 44,
+ 46, 48, 50, 52, 54, 56, 58, 60, 62, 65, 67, 69, 71, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94,
+ 96, 98, 100, 102, 99, 101, 102, 103, 105, 107, 109, 111, 113, 115, 117, 118, 120, 122, 125,
+ 127, 129, 131, 133, 135, 137, 139, 141, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164,
+ 166, 168, 170, 172, 174, 176, 178, 101, 104, 106, 108, 110, 112, 114, 116, 119, 121, 123, 124,
+ 126, 128, 130, 132, 134, 136, 138, 140, 142, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161,
+ 163, 165, 167, 169, 171, 173, 175, 177, 179,
+];
+
+pub(crate) fn frame_records_with_boundary(
+ geometry: Geometry,
+ strips: &[KVec<u8>],
+ connector: u8,
+ navarro_ordinary: Option<bool>,
+) -> Result<KVec<KVec<u8>>> {
+ let Geometry {
+ band_parity_bit,
+ interlaced_bands,
+ ..
+ } = geometry;
+ // Both platforms count an image record's 0..15 padding bytes in `aux`. Navarro also uses
+ // `aux` as a subtype on non-image records; its fixed black carriers masked the image rule
+ // because their 4048-byte strides require no padding.
+ let aux_is_pad_count = true;
+ const PREFIX: usize = 8;
+ const STRIDE_CAP: usize = 0x0ff0;
+ // Allocation boundary only, not wire framing.
+ const CHUNK: usize = 0x4000;
+ let mut frames: KVec<KVec<u8>> = KVec::new();
+ let mut chunk: KVec<u8> = KVec::new();
+ // Interlaced ordering sends even bands before odd bands while preserving x order.
+ let mut order: KVec<usize> = KVec::with_capacity(strips.len(), GFP_KERNEL)?;
+ // The rows below are a producer permutation of a 2560x1440 Navarro surface: 20 strips
+ // across x 180 bands, its strips being 128x8. A strip count alone cannot select it --
+ // Ridge at the same resolution is also exactly 3600 strips, 40 across x 90 bands -- and
+ // both black carriers reach here on every dock, so match the layout itself.
+ let navarro_layout = geometry.interlaced_bands
+ && geometry.strip_w() == STRIP_BLOCKS * DIM
+ && strips.len() == 3600;
+ // 64-wide strips with interlaced bands is DL-3x00 and nothing else: Ridge is 64 wide but
+ // raster, the DL7400 is 128 wide. Deliberately not conditioned on the strip count -- a
+ // partial frame has to reach the dock in the same order a full one would.
+ let ella_layout = geometry.interlaced_bands && geometry.strip_w() == DIM * 8;
+ let navarro_rows = match navarro_ordinary {
+ Some(false) if navarro_layout => Some(NAVARRO_PROLOGUE_ROWS),
+ Some(true) if navarro_layout => Some(NAVARRO_ORDINARY_ROWS),
+ _ => None,
+ };
+ if let Some(rows) = navarro_rows {
+ // 2560 px / 128 px per strip on the DL7400, 1920 / 64 on the DL-3x00. Guaranteed by
+ // the layout checks above.
+ let strips_across = if ella_layout { 30 } else { 20 };
+ let ordinary = navarro_ordinary == Some(true);
+ for (run, &y) in rows.iter().enumerate() {
+ // The DL-3x00 order carries whole bands; only the DL7400 splits rows at its
+ // producer boundaries.
+ let (x0, x1) = if ella_layout {
+ (0, strips_across)
+ } else if ordinary {
+ match run {
+ 50 => (0, 8),
+ 101 => (0, 8),
+ 102 => (8, 20),
+ 103 => (0, 4),
+ 104 => (8, 20),
+ 143 => (4, 20),
+ _ => (0, 20),
+ }
+ } else {
+ match run {
+ 51 => (0, 4),
+ 139 => (4, 20),
+ _ => (0, 20),
+ }
+ };
+ for x in x0..x1 {
+ order.push(y as usize * strips_across + x, GFP_KERNEL)?;
+ }
+ }
+ } else if ella_layout {
+ // Order by where each strip's band sits in the producer table, read from the strip
+ // itself rather than from an assumed full-surface layout. That is what lets a frame
+ // carry a *subset* of the surface and still arrive in the order the dock expects --
+ // which it must, because this dock will not take a whole surface in one frame.
+ let rank = |s: &KVec<u8>| -> usize {
+ let band = (strip_y(s) >> geometry.strip_h_shift()) as u8;
+ ELLA_ROWS_1080P
+ .iter()
+ .position(|&b| b == band)
+ .unwrap_or(usize::MAX)
+ };
+ let mut idx: KVec<usize> = KVec::with_capacity(strips.len(), GFP_KERNEL)?;
+ for n in 0..strips.len() {
+ idx.push(n, GFP_KERNEL)?;
+ }
+ // Insertion sort: the comparison is a table lookup and the driver has no sort in
+ // scope; a frame's strip count here is bounded by the dock's frame ceiling.
+ for a in 1..idx.len() {
+ let mut b = a;
+ while b > 0 {
+ let (l, r) = (idx[b - 1], idx[b]);
+ let key = |n: usize| (rank(&strips[n]), strip_x(&strips[n]));
+ if key(l) <= key(r) {
+ break;
+ }
+ idx.swap(b - 1, b);
+ b -= 1;
+ }
+ }
+ for n in idx {
+ order.push(n, GFP_KERNEL)?;
+ }
+ } else if interlaced_bands {
+ for pass in 0..2u16 {
+ for (n, s) in strips.iter().enumerate() {
+ if (strip_y(s) >> geometry.strip_h_shift()) & 1 == pass {
+ order.push(n, GFP_KERNEL)?;
+ }
+ }
+ }
+ } else {
+ for n in 0..strips.len() {
+ order.push(n, GFP_KERNEL)?;
+ }
+ }
+ let mut i = 0usize;
+ while i < order.len() {
+ let y0 = strip_y(&strips[order[i]]);
+ let mut record: KVec<u8> = KVec::new();
+ // Sub bit 4 carries the y-band parity when the selected framing
+ // profile requires it.
+ record.extend_from_slice(&[0u8; 8 + PREFIX], GFP_KERNEL)?;
+ let parity = u16::from(band_parity_bit) & ((y0 >> geometry.strip_h_shift()) & 1);
+ // Once a stream is running the vendor marks every image record here. Its frames and
+ // vino's are otherwise byte-identical -- same size, type, aux, sequence and payload --
+ // so this one bit is the whole difference between a stream the dock keeps taking and
+ // one it stops accepting with the endpoint still reporting healthy.
+ let sub = u16::from(geometry.connector_selector(connector))
+ | (parity << 4)
+ | u16::from(geometry.steady_sub_bit);
+ let mut n = 0usize;
+ // A record ends at a y-band boundary only where the band is part of its identity.
+ // Ridge carries the band parity in `sub`, so a record cannot span two bands; Navarro
+ // does not, and fills each record to the stride cap instead.
+ while i < order.len() && (!band_parity_bit || strip_y(&strips[order[i]]) == y0) {
+ // Preserve DLM's captured producer flushes exactly. They are not a uniform
+ // 1024-strip rule: applying that to the final 1552-strip segment added a record
+ // at 3072 and made the prologue 210064 bytes instead of 210048. The ordinary
+ // carrier also schedules the two 1024-strip producers differently, so it has its
+ // own three boundaries and one extra image record (208624 bytes total).
+ let producer_boundary = match navarro_ordinary {
+ Some(false) => matches!(i, 1024 | 2048 | 2764),
+ Some(true) => matches!(i, 2032 | 2048 | 2804),
+ None => false,
+ };
+ if interlaced_bands && n > 0 && producer_boundary {
+ break;
+ }
+ let s = &strips[order[i]];
+ let projected = record.len() + 2 + s.len();
+ let projected_aligned = (projected + 15) & !15;
+ if projected_aligned > STRIDE_CAP {
+ if n > 0 {
+ break;
+ }
+ // A strip too big for a record of its own cannot be framed at all. Emitting it
+ // anyway produces a record whose stride is over the cap, which a dock answers
+ // by halting the endpoint -- a failure indistinguishable from any other
+ // transport fault, surfacing several transfers later and blamed on the
+ // transport. Refuse the frame instead, so the encoder is what gets looked at.
+ pr_err!(
+ "vino: strip at {}x{} encoded to {} B, over the {} a record can carry\n",
+ strip_x(s),
+ strip_y(s),
+ s.len(),
+ STRIDE_CAP - 18
+ );
+ return Err(kernel::error::code::EOVERFLOW);
+ }
+ record.extend_from_slice(&(s.len() as u16).to_le_bytes(), GFP_KERNEL)?;
+ record.extend_from_slice(s, GFP_KERNEL)?;
+ n += 1;
+ i += 1;
+ }
+ // The wire format pads each complete record stride to 16 bytes. There is no
+ // additional trailer or inter-record gap: `size` counts from after the four-byte
+ // pad+size prefix, so `stride = size + 4` lands on the next record.
+ //
+ // Ridge carries the pad count in `aux`. Navarro does not: there `aux` names a record
+ // type, and every one of its image records carries zero, so a pad count written there
+ // would be read as some other kind of record entirely.
+ let pad = (16 - (record.len() & 15)) & 15;
+ record.extend_from_slice(&[0u8; 15][..pad], GFP_KERNEL)?;
+ // Written once the strips and the padding are in, because the stride the header
+ // states is the length of the finished record.
+ let aux = if aux_is_pad_count { pad as u16 } else { 0 };
+ record_header(&mut record, 4, sub, aux, 0);
+
+ if !chunk.is_empty() && chunk.len() + record.len() > CHUNK {
+ frames.push(chunk, GFP_KERNEL)?;
+ chunk = KVec::new();
+ }
+ chunk.extend_from_slice(&record, GFP_KERNEL)?;
+ }
+ if !chunk.is_empty() {
+ frames.push(chunk, GFP_KERNEL)?;
+ }
+ Ok(frames)
+}
+/// Bands described by one `kind=0x200f` sub-record, and the row stride of a band's values.
+///
+/// Measured: every full sub-record carries eight bands and 256 payload bytes, so a band is a
+/// fixed 32 bytes regardless of how many strips actually occupy it -- at 2560 wide only the
+/// first 20 are ever non-zero and bytes 20..32 are zero in all 5760 bytes of every map.
+///
+/// 32 covers any width up to 4096 px, but nothing wider has been captured, so a mode past
+/// that must not assume the stride still holds.
+pub(crate) const PARAM_BANDS_PER_TLV: usize = 8;
+pub(crate) const PARAM_BAND_STRIDE: usize = 32;
+/// Sub-records in the first of the pair; the rest go in the second. DLM splits 180 bands as
+/// 120 + 60, which is this many full sub-records and then the remainder.
+pub(crate) const PARAM_TLVS_PER_RECORD: usize = 15;
+/// A strip's size class, as carried in the `kind=0x200f` map.
+///
+/// The dock needs each strip's length before it parses the strip, because a strip is a
+/// self-delimiting bitstream whose end it cannot otherwise find. The class is simply the
+/// length in 512-byte units.
+///
+/// Measured over 68,347 `(strip, map value)` pairs in a DLM capture with zero disagreements:
+/// value 0 covers 54..510 bytes, 1 covers 512..1022, 2 covers 1024..1498 and 3 covers
+/// 1594..1670. Every boundary falls on a multiple of 512.
+#[inline]
+pub(crate) fn strip_size_class(len: usize) -> u8 {
+ // The field does not saturate: the corpus only reaches class 3 because its longest strip
+ // is 1670 B, and clamping a live desktop's larger strips to 3 corrupts them.
+ (len >> 9) as u8
+}
+/// Collect `(x, y, byte length)` for every strip in an already-framed set of image records.
+///
+/// The map is derived from the exact bytes about to go on the wire rather than from the
+/// encoder's intermediate state, so it cannot drift from what the dock actually receives.
+/// Each record body holds strips as `[u16 len][body]`, and every strip body opens with the
+/// codec's `01 28` magic -- which is what terminates the walk when a record's trailing
+/// padding is reached, since `aux` is a producer lane on this dock and not a pad count.
+pub(crate) fn framed_strip_extents(
+ frames: &[KVec<u8>],
+) -> impl Iterator<Item = (usize, usize, usize)> + '_ {
+ frames.iter().flat_map(|chunk| {
+ // Each element of `frames` is an allocation chunk holding several complete records,
+ // not one record, so walk records by their own stride: a record is `[u16 pad][u16
+ // size][12 B header][ (u16 len ++ strip)* ][ 0..15 pad ]` and the next begins
+ // `size + 4` bytes on. Walking strips to the end of a chunk instead stops at the first
+ // inner record boundary, because the next record's zero `pad` reads as a zero length.
+ let mut next_record = 0usize;
+ let mut p = 0usize;
+ let mut record_end = 0usize;
+ core::iter::from_fn(move || loop {
+ if p + 2 > record_end {
+ // This record is exhausted (or none has started yet): step to the next.
+ let hdr = chunk.get(next_record..next_record + 16)?;
+ let stride = usize::from(u16::from_le_bytes([hdr[2], hdr[3]])) + 4;
+ if stride < 16 || next_record + stride > chunk.len() {
+ return None;
+ }
+ p = next_record + 16;
+ record_end = next_record + stride;
+ next_record = record_end;
+ continue;
+ }
+ let sl = usize::from(u16::from_le_bytes([chunk[p], chunk[p + 1]]));
+ // A length that cannot be a strip is this record's trailing zero padding.
+ if sl < 16 || p + 2 + sl > record_end {
+ p = record_end;
+ continue;
+ }
+ let s = &chunk[p + 2..p + 2 + sl];
+ p += 2 + sl;
+ if s[0] != 0x01 || s[1] != 0x28 {
+ p = record_end;
+ continue;
+ }
+ let x = usize::from(u16::from_le_bytes([s[2], s[3]]));
+ let y = usize::from(u16::from_le_bytes([s[4], s[5]]));
+ return Some((x, y, sl));
+ })
+ })
+}
+/// Build the DL7400's per-strip parameter map for one frame, as the pair of `kind=0x200f`
+/// records DLM sends.
+///
+/// The map covers the whole frame: one byte per strip, `height / strip_h` bands of
+/// `width / strip_w` strips, each band padded to [`PARAM_BAND_STRIDE`]. At 2560x1440 that is
+/// 180 bands of 20 strips, which is what every captured DLM map contains, split 120 + 60
+/// across the two records.
+///
+/// Values come from [`strip_size_class`] applied to the strips in `frames`. A position this
+/// frame does not carry stays zero, which is what DLM sends for it.
+///
+/// An all-zero map is not a harmless approximation: it announces every strip as "under 512
+/// bytes", so the dock mis-parses exactly the detailed strips and renders them as coloured
+/// noise while flat fills stay perfect.
+pub(crate) fn navarro_strip_params(
+ geometry: Geometry,
+ connector: u8,
+ width: usize,
+ height: usize,
+ frames: &[KVec<u8>],
+ remembered: &mut KVec<u8>,
+) -> Result<KVec<u8>> {
+ let bands = height.div_ceil(geometry.strip_h());
+ let across = width.div_ceil(geometry.strip_w()).min(PARAM_BAND_STRIDE);
+ let sub = u16::from(geometry.connector_selector(connector));
+ let mut out = KVec::new();
+
+ // One byte per map slot, laid out exactly as the sub-records carry it.
+ //
+ // The map covers the whole surface while a delta frame carries only its damaged strips,
+ // so carry the previous frame's classes forward and overwrite only what this frame sends;
+ // rebuilding from zero would re-declare every untouched strip as class 0. `remembered` is
+ // the caller's per-connector buffer, zeroed by the resize a mode change triggers, which is
+ // correct because the dock's framebuffer is undefined until the keyframe that follows.
+ if remembered.len() != bands * PARAM_BAND_STRIDE {
+ remembered.clear();
+ remembered.resize(bands * PARAM_BAND_STRIDE, 0, GFP_KERNEL)?;
+ }
+ let mut values: KVec<u8> = KVec::new();
+ values.resize(bands * PARAM_BAND_STRIDE, 0, GFP_KERNEL)?;
+ values.copy_from_slice(remembered);
+ let mut described = 0usize;
+ let mut classes = [0usize; 8];
+ let mut longest = 0usize;
+ for (x, y, len) in framed_strip_extents(frames) {
+ let (bx, by) = (x / geometry.strip_w(), y / geometry.strip_h());
+ if bx >= across || by >= bands {
+ continue;
+ }
+ let class = strip_size_class(len);
+ values[by * PARAM_BAND_STRIDE + bx] = class;
+ if let Some(slot) = classes.get_mut(usize::from(class)) {
+ *slot += 1;
+ }
+ longest = longest.max(len);
+ described += 1;
+ }
+ // A position the walk misses is announced as class 0, so `described` must equal the strip
+ // count of the records handed in, and the histogram distinguishes a map that covers every
+ // strip from one that calls them all class 0.
+ vino_debug!(
+ "vino: connector={connector} strip map {described} strip(s) over {} chunk(s), classes {:?}, longest {longest} B\n",
+ frames.len(),
+ &classes[..4]
+ );
+ remembered.copy_from_slice(&values);
+
+ let mut band = 0usize;
+ let mut record = 0usize;
+ while band < bands {
+ // The first record takes up to PARAM_TLVS_PER_RECORD sub-records, the second the rest.
+ let take_tlvs = if record == 0 {
+ PARAM_TLVS_PER_RECORD
+ } else {
+ bands.div_ceil(PARAM_BANDS_PER_TLV)
+ };
+ let mut body = KVec::new();
+ for _ in 0..take_tlvs {
+ if band >= bands {
+ break;
+ }
+ let count = PARAM_BANDS_PER_TLV.min(bands - band);
+ let payload = count * PARAM_BAND_STRIDE;
+ body.extend_from_slice(&((6 + payload) as u16).to_le_bytes(), GFP_KERNEL)?;
+ body.extend_from_slice(&0x200fu16.to_le_bytes(), GFP_KERNEL)?;
+ body.extend_from_slice(&(band as u16).to_le_bytes(), GFP_KERNEL)?;
+ body.extend_from_slice(&(count as u16).to_le_bytes(), GFP_KERNEL)?;
+ let from = band * PARAM_BAND_STRIDE;
+ body.extend_from_slice(&values[from..from + payload], GFP_KERNEL)?;
+ band += count;
+ }
+ // DLM pads the first record's body to 3968 bytes and leaves the second exact.
+ if record == 0 && body.len() < 3968 {
+ body.resize(3968, 0, GFP_KERNEL)?;
+ }
+ let size = (body.len() + 12) as u16;
+ let aux: u16 = if record == 0 { 0x0008 } else { 0x0000 };
+ out.extend_from_slice(&0u16.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&size.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&4u32.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&sub.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&aux.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&0u32.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&body, GFP_KERNEL)?;
+ record += 1;
+ }
+ Ok(out)
+}
+/// A frame's closing records.
+///
+/// The two platforms delimit a frame with a different number of records, so this carries its
+/// own length and derefs to exactly the bytes that go on the wire.
+pub(crate) struct FrameTrailer {
+ bytes: [u8; 96],
+ len: usize,
+}
+impl FrameTrailer {
+ /// A frame that ends at its last strip record, with nothing closing it.
+ ///
+ /// A trailer borrowed from another generation is an unrecognised record in the middle of
+ /// the stream, so a dock whose format is not known closes nothing.
+ pub(crate) fn none() -> Self {
+ Self {
+ bytes: [0u8; 96],
+ len: 0,
+ }
+ }
+
+ /// Carry a single closing record, for a generation that delimits a frame with one.
+ pub(crate) fn one(record: &[u8]) -> Self {
+ let mut bytes = [0u8; 96];
+ let len = record.len().min(bytes.len());
+ bytes[..len].copy_from_slice(&record[..len]);
+ Self { bytes, len }
+ }
+}
+impl core::ops::Deref for FrameTrailer {
+ type Target = [u8];
+
+ fn deref(&self) -> &[u8] {
+ &self.bytes[..self.len]
+ }
+}
+/// The ring slot a frame writes, and the one it writes next.
+///
+/// Both platforms cycle a connector's frames through three buffers. Ridge names them by a phase
+/// of `0`, `2` or `4`; Navarro names the dock-side slot id and address of each.
+pub(crate) fn ring_phase(seq0: u32) -> (u8, u8) {
+ let phase = ((seq0 % 3) as u8) * 2;
+ (phase, (phase + 2) % 6)
+}
+/// Build the DL7400 record that opens a non-prologue frame.
+///
+/// Both working transports put a USB-transfer boundary between the `aux=0x0006` close and this
+/// `aux=0x0004` next-slot record, so the opener belongs to the frame it describes rather than
+/// to the preceding trailer. This is protocol framing, not cosmetic grouping.
+pub(crate) fn navarro_frame_opener(geometry: Geometry, connector: u8, seq0: u32) -> [u8; 32] {
+ let (phase, _) = ring_phase(seq0);
+ let prev_phase = (phase + 4) % 6;
+ let slot = super::super::cp::navarro_pipe_slot(connector, u16::from(phase));
+ let ring = super::super::cp::navarro_pipe_ring(connector, u16::from(phase)) as u16;
+ let prev_ring = super::super::cp::navarro_pipe_ring(connector, u16::from(prev_phase)) as u16;
+ let sub = u16::from(geometry.connector_selector(connector));
+
+ let mut out = [0u8; 32];
+ record_header(&mut out, 4, sub, 0x0004, 0);
+ out[16..19].copy_from_slice(&[0x0a, 0x00, 0x04]);
+ out[19] = slot as u8;
+ out[22..24].copy_from_slice(&ring.to_le_bytes());
+ out[26..28].copy_from_slice(&prev_ring.to_le_bytes());
+ out
+}
+/// Build the DL-3x00 record that opens a connector's video stream, sent once before any frame.
+///
+/// Two ring descriptors naming slot 0, distinguished from the per-frame close by `aux`: this
+/// dock uses that field as a record subtype on non-image records, `0x0008` here and `0x000a`
+/// for [`ella_frame_close`]. Sending the wrong opening leaves the stream unconfigured and the
+/// dock stalls the endpoint on the first image write.
+pub(crate) fn ella_stream_open(geometry: Geometry, connector: u8) -> [u8; 48] {
+ let mut out = [0u8; 48];
+ ella_record_header(&mut out, geometry, connector, 0x0008);
+ // Ring descriptor: slot 0 next, with the last slot named as the one it follows.
+ out[16..18].copy_from_slice(&10u16.to_le_bytes());
+ out[18] = 0x04;
+ out[27] = (ELLA_RING_SLOTS - 1) as u8;
+ out[28..30].copy_from_slice(&10u16.to_le_bytes());
+ out[30] = 0x04;
+ out
+}
+/// Build the DL-3x00 record that closes a frame.
+///
+/// Names the ring slot this frame filled and the slot the next one will fill. The dock rotates
+/// [`ELLA_RING_SLOTS`] buffers, so a wrong modulus hands it a slot it is still scanning out.
+///
+/// It is the last record of the frame's final USB transfer, which is what tells the dock the
+/// frame is complete: every short-terminated transfer carrying pixels ends on one of these.
+pub(crate) fn ella_frame_close(geometry: Geometry, connector: u8, seq0: u32) -> [u8; 48] {
+ let cur = (seq0 % ELLA_RING_SLOTS) as u8;
+ let next = ((seq0 + 1) % ELLA_RING_SLOTS) as u8;
+ // The frame counter is one-based and occupies a single byte on the wire.
+ let seq = (seq0 % 256) as u8;
+
+ let mut out = [0u8; 48];
+ ella_record_header(&mut out, geometry, connector, 0x000a);
+ out[16..18].copy_from_slice(&8u16.to_le_bytes());
+ out[18] = 0x05;
+ out[19] = cur;
+ out[23] = cur;
+ out[24] = 0x01;
+ out[25] = seq.wrapping_add(1);
+ out[26..28].copy_from_slice(&10u16.to_le_bytes());
+ out[28] = 0x04;
+ out[29] = next;
+ out[33] = next;
+ // The slot this frame filled, repeated after the pair naming the next one.
+ out[37] = cur;
+ out
+}
+/// Buffers the DL-3x00 dock rotates through; see `DockProfile::dock_buffers`.
+pub(crate) const ELLA_RING_SLOTS: u32 = 3;
+/// Fill the 16-byte record header shared by this dock's non-image records.
+pub(crate) fn ella_record_header(out: &mut [u8; 48], geometry: Geometry, connector: u8, aux: u16) {
+ record_header(
+ out,
+ 4,
+ u16::from(geometry.connector_selector(connector)),
+ aux,
+ 0,
+ );
+}
+/// Build the DL7400's closing record for the ring slot this frame filled.
+///
+/// The next slot is announced by [`navarro_frame_opener`] only after this frame's final USB
+/// transfer has terminated.
+pub(crate) fn navarro_frame_trailer(geometry: Geometry, connector: u8, seq0: u32) -> FrameTrailer {
+ let (phase, _) = ring_phase(seq0);
+ let slot = super::super::cp::navarro_pipe_slot(connector, u16::from(phase));
+ let ring = super::super::cp::navarro_pipe_ring(connector, u16::from(phase)) as u16;
+ let sub = u16::from(geometry.connector_selector(connector));
+
+ let mut out = [0u8; 96];
+ record_header(&mut out[..32], 4, sub, 0x0006, 0);
+
+ // Slot complete: its id, its ring address, and this frame's number.
+ out[16..19].copy_from_slice(&[0x08, 0x00, 0x05]);
+ out[19] = slot as u8;
+ out[22..24].copy_from_slice(&ring.to_le_bytes());
+ out[25] = (seq0 as u8).wrapping_add(1);
+
+ FrameTrailer {
+ bytes: out,
+ len: 32,
+ }
+}
+/// They delimit every logical frame, including the ARM-prefixed first frame. The first record
+/// carries a wrapping one-based frame counter; all three carry a three-slot phase (`0,2,4`) and
+/// the selected connector.
+pub(crate) fn frame_trailer(geometry: Geometry, connector: u8, seq0: u32) -> FrameTrailer {
+ let (phase, next_phase) = ring_phase(seq0);
+ let phase_off = phase * 4;
+ let next_off = next_phase * 4;
+ let frame_no = (seq0 as u8).wrapping_add(1);
+ let mut out = [0u8; 96];
+
+ let h = geometry.connector_selector(connector);
+ for (i, connector_byte) in [h, h, h | 0x10].into_iter().enumerate() {
+ let o = i * 32;
+ let aux = if i == 0 { 0x0006 } else { 0x0004 };
+ record_header(&mut out[o..o + 32], 4, u16::from(connector_byte), aux, 0);
+ }
+
+ // Record A: frame-present marker + current ring phase + one-based u8 frame number.
+ out[16] = 0x08;
+ out[18] = 0x05;
+ out[19] = phase;
+ out[23] = phase_off;
+ out[25] = frame_no;
+
+ // Records B/C are identical apart from C's connector|0x10 header selector.
+ for o in [32usize, 64] {
+ out[o + 16] = 0x0a;
+ out[o + 18] = 0x04;
+ out[o + 19] = next_phase;
+ out[o + 23] = next_off;
+ out[o + 27] = phase_off;
+ }
+ FrameTrailer {
+ bytes: out,
+ len: 96,
+ }
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_haar_records)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn ella_stream_records_match_the_dlm_capture() -> Result {
+ // Byte-for-byte against a DLM capture of an Ella dock driving 1920x1080 on two connectors.
+ // The dock accepts a malformed record and then simply never paints, so nothing on the wire
+ // and nothing in dmesg reports a mistake here -- only this comparison does. Field meanings,
+ // in the order they appear: `aux` is a record subtype on this dock (0x0008 opens a stream,
+ // 0x000a closes a frame); the closing record names the slot the frame filled, the slot the
+ // next frame will fill, and a one-based frame counter.
+ let geometry = profile::PROFILE_ELLA.geometry();
+
+ // Stream open, connector 0. Two ring descriptors naming slot 0.
+ let open = ella_stream_open(geometry, 0);
+ assert_eq!(
+ &open[..40],
+ &[
+ 0x00, 0x00, 0x2c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+ 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ ][..]
+ );
+
+ // Three consecutive frame closes on connector 0, walking the ring 0 -> 1 -> 2.
+ let expect: [[u8; 40]; 3] = [
+ [
+ 0x00, 0x00, 0x2c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x0a, 0x00,
+ 0x04, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ ],
+ [
+ 0x00, 0x00, 0x2c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x08, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x0a, 0x00,
+ 0x04, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ ],
+ [
+ 0x00, 0x00, 0x2c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x08, 0x00, 0x05, 0x02, 0x00, 0x00, 0x00, 0x02, 0x01, 0x03, 0x0a, 0x00,
+ 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
+ ],
+ ];
+ for (seq0, want) in expect.iter().enumerate() {
+ let got = ella_frame_close(geometry, 0, seq0 as u32);
+ assert_eq!(&got[..40], &want[..]);
+ }
+
+ // Head 1 differs only in the record `sub`: this dock uses the bare connector number.
+ let head1 = ella_frame_close(geometry, 1, 0);
+ assert_eq!(head1[8], 0x01);
+ assert_eq!(&head1[16..38], &expect[0][16..38]);
+ Ok(())
+ }
+
+ #[test]
+ fn an_encoded_strip_never_exceeds_the_decoder_input_bound() -> Result {
+ // A strip has to fit inside one record. The record builder starts a fresh record when the
+ // next strip would pass the stride cap, but the *first* strip of a record is taken at
+ // whatever size it has -- it has to be, or a frame carrying such a strip could not be
+ // built at all. So an over-long strip does not produce a short record, it produces a
+ // record whose stride is over the cap: a wire-format violation the dock can only report by
+ // halting the endpoint, which is indistinguishable from any other transport fault.
+ //
+ // The bound is the cap less the record header and the strip's own length prefix. For
+ // reference, DLM never exceeds 1758 bytes on DL-6xxx, 1780 on the DL-7400 or 2036 on
+ // DL-3x00 across roughly half a million strips, so conforming content has a wide margin;
+ // pseudo-random pixels are the worst case for an entropy coder and are what this uses.
+ const DECODER_STRIP_BOUND: usize = 0x0ff0 - 16 - 2;
+ let mut state: u32 = 0x1234_5678;
+ let mut next = || {
+ state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
+ (state >> 16) as u16
+ };
+ let mut worst = 0usize;
+ let mut worst_eight = 0usize;
+ for geometry in [
+ video::haar::RIDGE_GEOMETRY,
+ video::haar::Geometry::new(8, true, false, 0, 0x08, 3),
+ video::haar::Geometry::new(16, true, false, 3, 0x07, 3),
+ ] {
+ // Eight-bit samples are what every mode on these docks carries; the ten-bit range is
+ // the DL-7000 HDR profile and is measured separately so an out-of-range sample cannot
+ // be mistaken for an encoder that overshoots.
+ for (mask, ten_bit) in [(0xffu16, false), (0x3ff, true)] {
+ let geometry = if ten_bit {
+ geometry.with_depth(video::haar::Depth::Ten)
+ } else {
+ geometry
+ };
+ for _ in 0..16 {
+ let mut px =
+ |_x: usize, _y: usize| (next() & mask, next() & mask, next() & mask);
+ let strip = video::haar::colour_strip_at(geometry, 0, 0, &mut px)?;
+ worst = worst.max(strip.len());
+ if !ten_bit {
+ worst_eight = worst_eight.max(strip.len());
+ }
+ }
+ }
+ }
+ pr_info!("vino-selftest: worst 8-bit encoded strip {worst_eight} B\n");
+
+ // A flat strip is the one picture whose encoding can be compared to the vendor's exactly:
+ // every strip of a black frame encodes identically, so a whole captured frame collapses to
+ // a single payload and any difference is unambiguous. These are the bytes DLM puts on the
+ // wire for the strip at 0,0 of a black 1920x1088 DL-3x00 frame, and all 1024 strips of that
+ // frame carry them. Matching the length is not enough: the same 54 bytes can hold a
+ // different code, and a dock told one code and sent another decodes every strip to noise.
+ const DLM_FLAT_STRIP: [u8; 54] = [
+ 0x01, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x36, 0x00,
+ 0x00, 0x00, 0x54, 0x15, 0xaa, 0x0a, 0x55, 0x85, 0xaa, 0x42, 0x55, 0xa1, 0xaa, 0x50,
+ 0x55, 0xa8, 0x2a, 0x54, 0x15, 0xaa, 0x0a, 0x55, 0x85, 0xaa, 0x42, 0x55, 0xa1, 0xaa,
+ 0x50, 0x55, 0xa8, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ ];
+ let ella = video::haar::Geometry::new(8, true, false, 0, 0x08, 3)
+ .with_coding(video_arm::CodeTables::Narrow);
+ let mut black = |_x: usize, _y: usize| (0u16, 0u16, 0u16);
+ let flat = video::haar::colour_strip_at(ella, 0, 0, &mut black)?;
+ // A smooth gradient stands in for an ordinary desktop, where DLM averages about 175 bytes.
+ let mut ramp = |x: usize, y: usize| {
+ let v = ((x + y) & 0xff) as u16;
+ (v, v, v)
+ };
+ let gradient = video::haar::colour_strip_at(ella, 0, 0, &mut ramp)?;
+
+ // A flat strip cannot tell the payload orders apart: every field of it carries an all-zero
+ // payload, so the bytes above are identical whichever end goes out first. Pin a strip that
+ // does carry payload bits. The order itself was settled against 8000 captured DL-3x00
+ // strips three ways -- reading the interleaved payload least significant bit first takes
+ // whole-strip decoding from 77% to 100%, brings the recovered luma DC into its exact
+ // 0..1020 range instead of an impossible -3205..996, and turns the reconstructed frame
+ // from streaks into a legible desktop.
+ const NARROW_GRADIENT_HEAD: [u8; 64] = [
+ 0x01, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0xf8, 0x00,
+ 0x00, 0x00, 0x5c, 0xe1, 0x0a, 0x57, 0xb8, 0xc2, 0x15, 0xae, 0x70, 0x85, 0x2b, 0x5c,
+ 0xe1, 0x0a, 0x57, 0xb8, 0xc2, 0x15, 0xae, 0x70, 0x85, 0x2b, 0x5c, 0x8f, 0xab, 0xc2,
+ 0x55, 0xe1, 0xaa, 0x70, 0x55, 0xb8, 0x2a, 0x5c, 0x15, 0xae, 0x0a, 0x55, 0xd5, 0xb8,
+ 0x2a, 0x5c, 0x15, 0xae, 0x0a, 0x57, 0x85, 0xab,
+ ];
+ assert_eq!(
+ &gradient[..NARROW_GRADIENT_HEAD.len()],
+ &NARROW_GRADIENT_HEAD[..]
+ );
+ pr_info!(
+ "vino-selftest: flat strip {} B (DLM sends 54), gradient strip {} B\n",
+ flat.len(),
+ gradient.len()
+ );
+ assert_eq!(flat.len(), DLM_FLAT_STRIP.len());
+ if let Some(at) = (0..flat.len()).find(|&i| flat[i] != DLM_FLAT_STRIP[i]) {
+ pr_err!(
+ "vino-selftest: flat strip differs from DLM at byte {}: {:#04x} vs {:#04x}\n",
+ at,
+ flat[at],
+ DLM_FLAT_STRIP[at]
+ );
+ }
+ assert_eq!(&flat[..], &DLM_FLAT_STRIP[..]);
+ pr_info!("vino-selftest: worst encoded strip {worst} B (bound {DECODER_STRIP_BOUND})\n");
+ // Eight bits per channel is every mode these docks are driven at today, and it stays
+ // inside the bound with room to spare.
+ assert!(worst_eight <= DECODER_STRIP_BOUND);
+ // The ten-bit profile does not, so the framing has to refuse it rather than put an
+ // over-cap record on the wire. Build a frame from one such strip and check that it does.
+ if worst > DECODER_STRIP_BOUND {
+ let geometry = video::haar::RIDGE_GEOMETRY.with_depth(video::haar::Depth::Ten);
+ let mut oversized: KVec<KVec<u8>> = KVec::new();
+ loop {
+ let mut px =
+ |_x: usize, _y: usize| (next() & 0x3ff, next() & 0x3ff, next() & 0x3ff);
+ let strip = video::haar::colour_strip_at(geometry, 0, 0, &mut px)?;
+ if strip.len() > DECODER_STRIP_BOUND {
+ oversized.push(strip, GFP_KERNEL)?;
+ break;
+ }
+ }
+ assert!(frame_records(geometry, &oversized, 0).is_err());
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn record_stats_reads_a_frame_back_off_its_own_records() -> Result {
+ // The diagnostic that runs when a frame fails to reach a dock, so it has to be right when
+ // nothing else is: a wrong reading here sends the next investigation at the transport.
+ let geometry = video::haar::Geometry::new(8, true, false, 0, 0x08, 3);
+ let mut strips: KVec<KVec<u8>> = KVec::new();
+ let mut px = |_x: usize, _y: usize| (0u16, 0u16, 0u16);
+ for _ in 0..40 {
+ strips.push(
+ video::haar::colour_strip_at(geometry, 0, 0, &mut px)?,
+ GFP_KERNEL,
+ )?;
+ }
+ let flat = strips[0].len();
+ let records = frame_records(geometry, &strips, 0)?;
+ let (count, max_stride, max_strip) = record_stats(&records);
+ assert!(count > 0);
+ assert_eq!(max_strip, flat);
+ assert!(max_stride <= 0x0ff0);
+ assert_eq!(max_stride % 16, 0);
+ Ok(())
+ }
+
+ #[test]
+ fn ella_band_order_is_the_producer_order_not_a_plain_interlace() -> Result {
+ // This dock stops draining at the first band it did not expect, so the order is part of
+ // the format rather than a preference. The generic even-then-odd interlace diverges at the
+ // second band -- it sends 2 where the dock wants 3 -- which is inside the first sixty
+ // strips of every frame.
+ let rows = ella_rows_1080p();
+ assert_eq!(rows.len(), 68);
+ assert_eq!(&rows[..8], &[0, 3, 5, 7, 9, 11, 13, 16]);
+ assert_eq!(&rows[33..37], &[65, 66, 1, 2]);
+ assert_eq!(rows[67], 67);
+ // Every band exactly once: a repeat would mean a split-row producer boundary, which this
+ // dock does not have and which the ordering code would silently mis-handle.
+ let mut seen = [false; 68];
+ for &y in rows {
+ assert!(!seen[y as usize]);
+ seen[y as usize] = true;
+ }
+ assert!(seen.iter().all(|s| *s));
+ Ok(())
+ }
+
+ #[test]
+ fn video_frame_trailer_matches_dlm_cycle_and_head() {
+ let geometry = profile::PROFILE_RIDGE.geometry();
+ let t0 = frame_trailer(geometry, 0, 0);
+ assert_eq!(
+ &t0[..32],
+ &[
+ 0, 0, 0x1c, 0, 4, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 8, 0, 5, 0, 0, 0, 0, 0, 0, 1, 0,
+ 0, 0, 0, 0, 0,
+ ]
+ );
+ assert_eq!(
+ &t0[32..64],
+ &[
+ 0, 0, 0x1c, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0x0a, 0, 4, 2, 0, 0, 0, 8, 0, 0,
+ 0, 0, 0, 0, 0, 0,
+ ]
+ );
+ // Record C ORs the connector selector with 0x10. `sub` is the little-endian u16 at bytes
+ // 8..10, so the selector belongs in byte 8; placing it in byte 9 would encode 0x1100
+ // instead of 0x0011 and prevent connector 1 from presenting the frame.
+ assert_eq!(
+ &t0[64..],
+ &[
+ 0, 0, 0x1c, 0, 4, 0, 0, 0, 0x10, 0, 4, 0, 0, 0, 0, 0, 0x0a, 0, 4, 2, 0, 0, 0, 8, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ]
+ );
+
+ let t1 = frame_trailer(geometry, 1, 1);
+ assert_eq!(u16::from_le_bytes([t1[8], t1[9]]), 0x0001);
+ assert_eq!(u16::from_le_bytes([t1[32 + 8], t1[32 + 9]]), 0x0001);
+ assert_eq!(u16::from_le_bytes([t1[64 + 8], t1[64 + 9]]), 0x0011);
+ assert_eq!(t1[19], 2);
+ assert_eq!(t1[23], 8);
+ assert_eq!(t1[25], 2);
+ assert_eq!(t1[32 + 19], 4);
+ assert_eq!(t1[32 + 23], 16);
+ assert_eq!(t1[32 + 27], 8);
+ }
+}
diff --git a/drivers/gpu/drm/vino/video/haar/strip.rs b/drivers/gpu/drm/vino/video/haar/strip.rs
new file mode 100644
index 000000000000..9930063b6226
--- /dev/null
+++ b/drivers/gpu/drm/vino/video/haar/strip.rs
@@ -0,0 +1,507 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Strip geometry and the encoding of one strip.
+//!
+//! A strip is the unit the dock accepts: sixteen blocks, laid out per family, carrying Y, Cb
+//! and Cr for a rectangle of the surface. Geometry says how a surface is cut into them.
+
+use super::*;
+
+pub(crate) const STRIP_ROW_BLOCKS: usize = 8; // blocks in one coded half
+pub(crate) const STRIP_BLOCKS: usize = 16;
+
+/// Everything about a dock's video encoding that differs between platforms.
+///
+/// Passed by value rather than held in shared state: two docks of different generations may
+/// encode concurrently, and each frame must carry its own dock's layout.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) struct Geometry {
+ /// `log2` of the strip width in pixels. A strip is always [`STRIP_BLOCKS`] blocks of
+ /// `DIM` x `DIM` split into two halves of [`STRIP_ROW_BLOCKS`]; the docks differ only in
+ /// how those blocks are laid over pixels. Ridge puts them 8 across x 2 down (64x16), the
+ /// DL7400 16 across x 1 down (128x8). Same block count, same coded strip.
+ ///
+ /// Held as shifts because a strip is a power of two in each direction, so every geometry
+ /// query is a shift or a mask: dividing by a value the compiler cannot bound would put a
+ /// panic path into the codec's hot functions.
+ strip_w_shift: u32,
+ /// `log2` of the strip height in pixels; see [`Geometry::strip_w_shift`].
+ strip_h_shift: u32,
+ /// Whether image records are emitted with even y-bands before odd ones.
+ ///
+ /// Ridge sends them in raster order; the DL7400 interlaces, which its own records show as
+ /// a y sequence of 0, 16, 32 ... over 8-row strips before it returns for 8, 24, 40 ...
+ pub(crate) interlaced_bands: bool,
+ /// Whether an image record's `sub` carries the y-band parity in bit 4.
+ ///
+ /// Ridge does, which also means one of its records can never span two bands. Navarro's
+ /// image records carry only the connector, and fill to the stride cap across band
+ /// boundaries.
+ pub(crate) band_parity_bit: bool,
+ /// How the dock encodes a connector in a video record's `sub` field, as a left shift.
+ ///
+ /// Ridge puts the bare connector number there (0, 1). Navarro shifts it by three: its
+ /// records use `0x00`/`0x08`/`0x10`/`0x18` and its stream-open ids are
+ /// `0x07`/`0x0f`/`0x17`/`0x1f` -- the same eight-apart spacing.
+ pub(crate) connector_selector_shift: u8,
+ /// The bits a connector's stream id sets over its record `sub`.
+ ///
+ /// A dock names each video stream by its connector's record `sub` with a fixed low pattern
+ /// set: Ridge uses `0x08 | connector`, Navarro `(connector << 3) | 7`. The same id is the
+ /// wire `sub` of the stream's control records, the value its `RepeaterAuth_Stream_Manage`
+ /// restatement declares, and the byte-7 tweak deriving the stream's AES-CTR nonce from its
+ /// SKE RIV.
+ pub(crate) stream_id_mask: u8,
+ /// How many buffers the dock rotates through as it presents frames; see
+ /// `DockProfile::dock_buffers`.
+ pub(crate) dock_buffers: u8,
+ /// Bits a steady-state image record adds to its `sub`; see
+ /// `DockProfile::steady_record_sub_bit`.
+ ///
+ /// Cleared for the frames that open a stream, which the vendor sends without it.
+ pub(crate) steady_sub_bit: u8,
+ /// Bits per channel of the surface being encoded; see [`Depth`].
+ ///
+ /// Geometry is otherwise fixed per dock, and this is not -- a connector moves between
+ /// depths at runtime when a compositor turns HDR on. It lives here because it is the one
+ /// remaining thing the codec needs to know that is neither a block coordinate nor a
+ /// coefficient, and `Geometry` is already threaded through every path that would have to
+ /// carry it separately. [`Geometry::new`] gives [`Depth::Eight`]; a 10-bit connector asks
+ /// for [`Geometry::with_depth`].
+ depth: Depth,
+ /// Which dialect of the shared unary code this dock's decoder reads.
+ ///
+ /// The same profile field states the dock's code tables in its stream configuration, so
+ /// the code vino emits and the code it declares cannot drift apart.
+ coding: super::super::video_arm::CodeTables,
+}
+impl Geometry {
+ /// Build a dock's geometry from its profile.
+ ///
+ /// `strip_blocks_x` must divide [`STRIP_BLOCKS`]; anything else falls back to the Ridge
+ /// layout rather than producing a strip that is not a whole number of blocks.
+ pub(crate) fn new(
+ strip_blocks_x: usize,
+ interlaced_bands: bool,
+ band_parity_bit: bool,
+ connector_selector_shift: u8,
+ stream_id_mask: u8,
+ dock_buffers: u8,
+ ) -> Self {
+ let (strip_w_shift, strip_h_shift) = match strip_blocks_x {
+ 16 => (7, 3), // 128 x 8
+ _ => (6, 4), // 64 x 16
+ };
+ Self {
+ strip_w_shift,
+ strip_h_shift,
+ interlaced_bands,
+ band_parity_bit,
+ connector_selector_shift,
+ stream_id_mask,
+ dock_buffers: dock_buffers.max(1),
+ steady_sub_bit: 0,
+ depth: Depth::Eight,
+ coding: super::super::video_arm::CodeTables::Wide,
+ }
+ }
+
+ /// The same dock geometry at a different sample depth; see [`Geometry::depth`].
+ #[inline]
+ pub(crate) fn with_depth(self, depth: Depth) -> Self {
+ Self { depth, ..self }
+ }
+
+ /// Select the bitstream dialect; see [`Geometry::coding`].
+ pub(crate) fn with_coding(self, coding: super::super::video_arm::CodeTables) -> Self {
+ Self { coding, ..self }
+ }
+
+ /// The same geometry with the steady-state record bit set; see
+ /// [`Geometry::steady_sub_bit`].
+ pub(crate) fn with_steady_sub_bit(self, bit: u8) -> Self {
+ Self {
+ steady_sub_bit: bit,
+ ..self
+ }
+ }
+
+ /// The same geometry for a frame that opens a stream, which carries no steady-state bit.
+ pub(crate) fn opening(self) -> Self {
+ Self {
+ steady_sub_bit: 0,
+ ..self
+ }
+ }
+
+ /// Which dialect of the shared unary code this dock's decoder reads.
+ #[inline]
+ pub(crate) fn coding(&self) -> super::super::video_arm::CodeTables {
+ self.coding
+ }
+
+ /// Bits per channel this frame is being encoded at.
+ #[inline]
+ pub(crate) fn depth(&self) -> Depth {
+ self.depth
+ }
+
+ /// `log2` of the strip width; see [`Geometry::strip_w_shift`].
+ #[inline]
+ pub(crate) fn strip_w_shift(&self) -> u32 {
+ self.strip_w_shift
+ }
+
+ /// `log2` of the strip height; see [`Geometry::strip_w_shift`].
+ #[inline]
+ pub(crate) fn strip_h_shift(&self) -> u32 {
+ self.strip_h_shift
+ }
+
+ /// Strip width in pixels: [`Geometry::strip_w_shift`] blocks across, each `DIM` square.
+ #[inline]
+ pub(crate) fn strip_w(&self) -> usize {
+ 1usize << self.strip_w_shift
+ }
+
+ /// Strip height in pixels; see [`Geometry::strip_w`].
+ #[inline]
+ pub(crate) fn strip_h(&self) -> usize {
+ 1usize << self.strip_h_shift
+ }
+
+ /// Damage macro-tile: 4x4 strips. A touched macro-tile must be resent in full because the
+ /// dock rotates its backing buffers at this granularity.
+ #[inline]
+ pub(crate) fn macro_w(&self) -> usize {
+ 4 * self.strip_w()
+ }
+
+ /// Damage macro-tile height; see [`Geometry::macro_w`].
+ #[inline]
+ pub(crate) fn macro_h(&self) -> usize {
+ 4 * self.strip_h()
+ }
+
+ /// Encode `connector` the way this dock expects it in a record `sub` field.
+ #[inline]
+ pub(crate) fn connector_selector(&self, connector: u8) -> u8 {
+ connector << self.connector_selector_shift
+ }
+
+ /// The content-stream id of `connector` on this dock; see [`Geometry::stream_id_mask`].
+ #[inline]
+ pub(crate) fn stream_id(&self, connector: u8) -> u16 {
+ u16::from(self.connector_selector(connector) | self.stream_id_mask)
+ }
+}
+/// The Ridge layout, and the value every geometry-free code path starts from.
+pub(crate) const RIDGE_GEOMETRY: Geometry = Geometry {
+ strip_w_shift: 6,
+ strip_h_shift: 4,
+ interlaced_bands: false,
+ band_parity_bit: true,
+ connector_selector_shift: 0,
+ stream_id_mask: 0x08,
+ dock_buffers: 2,
+ // The geometry-free starting point describes a stream that has not opened yet.
+ steady_sub_bit: 0,
+ depth: Depth::Eight,
+ coding: super::super::video_arm::CodeTables::Wide,
+};
+
+/// `log2(DIM)`, so a block index splits into (x, y) by shift and mask rather than division.
+pub(crate) const DIM_SHIFT: u32 = 3;
+
+/// Round a byte count up to an even number (every coder sub-region is even-aligned).
+pub(crate) fn round_even(n: usize) -> usize {
+ n + (n & 1)
+}
+/// One quantized colour block: the three planes' 64 coefficients and exact last-significant
+/// AC positions. Built by [`colour_block`] from a block's per-plane samples.
+pub(crate) struct ColourBlock {
+ pub(crate) qcr: [i32; COEFFS],
+ pub(crate) qcb: [i32; COEFFS],
+ pub(crate) qy: [i32; COEFFS],
+ pub(crate) lcr: usize,
+ pub(crate) lcb: usize,
+ pub(crate) ly: usize,
+}
+/// Return the exact chroma AC extent.
+/// Only the KUnit tests call this now: `colour_block` folds the same search into the pass that
+/// writes the coefficients, so the production path never scans them a second time.
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+pub(crate) fn chroma_last(q: &[i32; COEFFS]) -> usize {
+ (1..COEFFS).rev().find(|&i| q[i] != 0).unwrap_or(0)
+}
+/// Transform + quantize one block's three planes (each 64 samples in the codec's x64 fixed
+/// point: `cr[i] = 64*(B-G)`, `cb[i] = 64*(R-G)`, `y[i] = 64*G + 64*((Cb+Cr)>>2)`). Luma uses
+/// the per-position `quantize`; chroma AC uses `quantize_chroma_ac`; all DCs use
+/// `quantize_dc_round`.
+///
+/// `#[inline(never)]`: see `haar2d`'s doc comment -- part of the kernel-stack-overflow fix.
+#[inline(never)]
+pub(crate) fn colour_block(
+ cr: &[i32; PIXELS],
+ cb: &[i32; PIXELS],
+ y: &[i32; PIXELS],
+) -> ColourBlock {
+ // Do not reach for SIMD here. An AVX2 in-block Haar is byte-exact but parity-to-slower
+ // once `kernel_fpu_begin`/`end` are paid per block, costing about 18% more CPU on a live
+ // encode. A strip is ~72% entropy coder, which is bit-serial, so even a free transform
+ // caps the whole win near 23%.
+ let (tcr, tcb, ty) = (transform(cr), transform(cb), transform(y));
+ // Quantise all three planes and find each one's last significant coefficient in a single
+ // pass. Folding the search into the write avoids three further 63-element reverse scans
+ // ([`chroma_last`]) over arrays just written, and keeps the three planes in cache together.
+ //
+ // An explicit ascending loop rather than `core::array::from_fn`: the fold depends on the
+ // index order, and `from_fn` does not document one. Every element is written, so the zero
+ // initialiser costs nothing.
+ let mut qcr = [0i32; COEFFS];
+ let mut qcb = [0i32; COEFFS];
+ let mut qy = [0i32; COEFFS];
+ let (mut lcr, mut lcb, mut ly) = (0usize, 0usize, 0usize);
+ qcr[0] = quantize_dc_round(2, tcr[0]);
+ qcb[0] = quantize_dc_round(1, tcb[0]);
+ qy[0] = quantize_dc_round(0, ty[0]);
+ // Coefficient 0 is deliberately excluded: the last-significant index is over 1..COEFFS.
+ for i in 1..COEFFS {
+ let (vcr, vcb, vy) = (
+ quantize_chroma_ac(tcr[i], i),
+ quantize_chroma_ac(tcb[i], i),
+ quantize(ty[i], i),
+ );
+ if vcr != 0 {
+ lcr = i;
+ }
+ if vcb != 0 {
+ lcb = i;
+ }
+ if vy != 0 {
+ ly = i;
+ }
+ qcr[i] = vcr;
+ qcb[i] = vcb;
+ qy[i] = vy;
+ }
+ ColourBlock {
+ qcr,
+ qcb,
+ qy,
+ lcr,
+ lcb,
+ ly,
+ }
+}
+/// Encode one 64x16 COLOUR strip at pixel `(x, y)` from its 16 quantized colour blocks
+/// (raster: 0..8 top 8-px half, 8..16 bottom).
+///
+/// `#[inline(never)]`: see `haar2d`'s doc comment -- part of the kernel-stack-overflow fix.
+#[inline(never)]
+pub(crate) fn colour_strip(
+ geometry: Geometry,
+ blocks: &[ColourBlock],
+ x: u16,
+ y: u16,
+) -> Result<KVec<u8>> {
+ let dc_cmax = geometry.depth().dc_cmax();
+ let coding = geometry.coding();
+ let mut main = Bits::new(coding);
+ for b in blocks {
+ main.colour_sync_unit(b.lcr, b.lcb, b.ly)?;
+ }
+ let (mut pcr, mut pcb, mut py) = (0i32, 0i32, 0i32);
+ for b in blocks {
+ let (cr, cb, yv) = (b.qcr[0], b.qcb[0], b.qy[0]);
+ main.esc(cr - pcr, dc_cmax)?;
+ main.esc(cb - pcb, dc_cmax)?;
+ main.esc(yv - py, dc_cmax)?;
+ (pcr, pcb, py) = (cr, cb, yv);
+ }
+ let mut row0 = Bits::new(coding);
+ for b in &blocks[..STRIP_ROW_BLOCKS] {
+ row0.colour_block_ac(&b.qcr, &b.qcb, &b.qy, b.lcr, b.lcb, b.ly, geometry.depth())?;
+ }
+ let mut row1 = Bits::new(coding);
+ for b in &blocks[STRIP_ROW_BLOCKS..] {
+ row1.colour_block_ac(&b.qcr, &b.qcb, &b.qy, b.lcr, b.lcb, b.ly, geometry.depth())?;
+ }
+
+ // `Bits` buffers into a word, so `out` is only complete once finished -- take all three
+ // before any length is read.
+ let (main, row0, row1) = (main.finish()?, row0.finish()?, row1.finish()?);
+ let r0 = round_even(row0.len());
+ let r1 = round_even(row1.len());
+ let main_b = round_even(main.len()) + 2;
+ let w18 = 16 + main_b;
+ let w1c = w18 + r0;
+ // The 2-byte tail overlaps the end of the row1 region (len = w1c + round_even(row1)).
+ let len = w1c + r1;
+
+ let mut out = KVec::new();
+ out.resize(len, 0, GFP_KERNEL)?;
+ out[0] = 0x01;
+ out[1] = 0x28;
+ out[2..4].copy_from_slice(&x.to_le_bytes());
+ // Strip y is the band's top edge.
+ out[4..6].copy_from_slice(&y.to_le_bytes());
+ out[10..12].copy_from_slice(&(w18 as u16).to_le_bytes());
+ out[12..14].copy_from_slice(&(w1c as u16).to_le_bytes());
+ out[16..16 + main.len()].copy_from_slice(&main);
+ out[w18..w18 + row0.len()].copy_from_slice(&row0);
+ out[w1c..w1c + row1.len()].copy_from_slice(&row1);
+ // No forward-hint tail: on the EP08 wire the strip's last 2 bytes are the natural row1
+ // bit-packing. The record framing carries the length as `strip_id == len`, so the in-strip
+ // echo the sink hook showed is not transmitted on the wire. See `frame_records`.
+ Ok(out)
+}
+/// Gather one 64x16 strip's 16 colour blocks from a pixel source. `px(x, y)` returns the
+/// 8-bit `(R, G, B)` at absolute frame coordinate `(x, y)`; `(ox, oy)` is the strip's
+/// top-left pixel. Each block's three planes are built in the codec's x64 fixed point via
+/// [`colour`] (per-pixel `(Y, Cb, Cr)`, stored `(Cr, Cb, Y)` for [`colour_block`]). Blocks are
+/// raster order within the strip (0..8 top 8-px half, 8..16 bottom), matching [`colour_strip`].
+///
+/// `#[inline(never)]`: see `haar2d`'s doc comment -- part of the kernel-stack-overflow fix.
+/// The block array is heap allocated because it is about 6.5 KiB and nested copies can exhaust
+/// the kernel stack.
+#[inline(never)]
+pub(crate) fn colour_strip_blocks(
+ geometry: Geometry,
+ ox: usize,
+ oy: usize,
+ px: &mut impl FnMut(usize, usize) -> (u16, u16, u16),
+) -> Result<KVec<ColourBlock>> {
+ let mut blocks = KVec::with_capacity(STRIP_BLOCKS, GFP_KERNEL)?;
+ for k in 0..STRIP_BLOCKS {
+ let across_shift = geometry.strip_w_shift() - DIM_SHIFT;
+ let (bx, by) = (k & ((1usize << across_shift) - 1), k >> across_shift);
+ // One `px` call per pixel, in the same row-major order as before -- it is the
+ // expensive accessor, so the converted values are gathered once and then split into
+ // the three planes. `from_fn` avoids the zeroing that filling `[0i32; PIXELS]`
+ // afterwards would leave behind.
+ // Left as a zero-initialised fill on purpose. Gathering into an interleaved
+ // array and splitting it (as `colour_block`/`transform` now do) removes the `memset`
+ // but costs ~736 bytes of stack here, and this path already runs ~8 KB deep inside a
+ // 16 KB kernel stack -- the same budget the `#[inline(never)]` markers protect.
+ let (mut cr, mut cb, mut y) = ([0i32; PIXELS], [0i32; PIXELS], [0i32; PIXELS]);
+ // Monotonic indexing lets LLVM prove that every element is initialized before use.
+ let mut i = 0usize;
+ for r in 0..DIM {
+ for c in 0..DIM {
+ let (rr, gg, bb) = px(ox + bx * DIM + c, oy + by * DIM + r);
+ let (yv, cbv, crv) = colour(rr as i32, gg as i32, bb as i32);
+ (cr[i], cb[i], y[i]) = (crv, cbv, yv);
+ i += 1;
+ }
+ }
+ blocks.push(colour_block(&cr, &cb, &y), GFP_KERNEL)?;
+ }
+ Ok(blocks)
+}
+/// Encode ONE 64x16 strip whose top-left output pixel is `(sx, sy)`.
+///
+/// This is the unit of work both frame encoders are built from, and it is the reason a
+/// parallel encode is possible at all: a strip reads only its own 64x16 region through `px`
+/// and produces its own independent byte vector, sharing no state with any other strip. The
+/// scanout encoder in `drm_sink.rs` fans batches of these across CPUs; see `EncodeChunk`.
+pub(crate) fn colour_strip_at(
+ geometry: Geometry,
+ sx: usize,
+ sy: usize,
+ px: &mut impl FnMut(usize, usize) -> (u16, u16, u16),
+) -> Result<KVec<u8>> {
+ let blocks = colour_strip_blocks(geometry, sx, sy, px)?;
+ colour_strip(geometry, &blocks, sx as u16, sy as u16)
+}
+/// A strip's `y` (the EP08 record bands group strips by row). Reads the `y` field the strip
+/// builders write at byte offset 4 ([`colour_strip`] / [`solid_strip`]).
+pub(crate) fn strip_y(s: &[u8]) -> u16 {
+ u16::from_le_bytes([s[4], s[5]])
+}
+/// A strip's `x`, written by the strip builders at byte offset 2.
+pub(crate) fn strip_x(s: &[u8]) -> u16 {
+ u16::from_le_bytes([s[2], s[3]])
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_haar_strip)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn haar_colour_and_quantize() {
+ use video::haar;
+ // Colour transform against captured transform-DC values: white maps to Y=16320,
+ // achromatic pixels have zero chroma, and red's floored luma is 4032.
+ assert_eq!(haar::colour(255, 255, 255), (16320, 0, 0));
+ assert_eq!(haar::colour(128, 128, 128), (128 * 64, 0, 0));
+ assert_eq!(haar::colour(255, 0, 0), (4032, 64 * 255, 0));
+ // Green has two negative signed-chroma components.
+ assert_eq!(haar::colour(0, 255, 0), (8128, -64 * 255, -64 * 255));
+ assert_eq!(haar::colour(0, 0, 255), (4032, 0, 64 * 255));
+ // White Y_DC=16320 quantizes to 1020 at DC position zero.
+ assert_eq!(haar::quantize(16320, 0), 1020);
+ // AC clamps to the 12-bit signed long-token range.
+ assert_eq!(haar::quantize(1_000_000, 16), 2047);
+ assert_eq!(haar::quantize(-1_000_000, 16), -2048);
+ }
+
+ /// The colour transform is depth-agnostic: it carries code words, applies no transfer function
+ /// and no matrix, and the host has already encoded whatever curve the sink wants.
+ #[test]
+ fn haar_colour_carries_ten_bit_unchanged() {
+ use video::haar;
+ assert_eq!(haar::colour(1023, 1023, 1023), (64 * 1023, 0, 0));
+ assert_eq!(haar::colour(512, 512, 512), (512 * 64, 0, 0));
+ assert_eq!(haar::colour(1023, 0, 0), (64 * (1023 >> 2), 64 * 1023, 0));
+ // 10-bit white quantises to 4092 at DC, which is why the ceiling has to move.
+ //
+ // Through `quantize_dc_round`, which is what the encoder applies to coefficient zero. The
+ // AC quantiser is the wrong function to assert here: its clamp to the 12-bit signed token
+ // range caps the value at 2047, and the DC never reaches it. That clamp is deliberate --
+ // an over-range AC magnitude saturates in `esc` and only loses detail, whereas an
+ // over-sized ceiling desynchronises the dock's decoder outright.
+ let (y, _, _) = haar::colour(1023, 1023, 1023);
+ assert_eq!(
+ haar::quantize_dc_round(0, haar::transform(&[y; haar::BLOCK])[0]),
+ 4092
+ );
+ }
+
+ /// The same quantised blocks encode to different bytes at the two depths.
+ ///
+ /// This is the whole of the HDR difference on this wire, so it is worth a test that would fail
+ /// if the depth ever stopped reaching the entropy coder: a DC of 4092 saturates to category
+ /// 10's largest value at 8 bits and survives intact at 10.
+ #[test]
+ fn haar_strip_encoding_follows_the_depth() -> Result {
+ use video::haar::{self, Depth};
+ let white = haar::colour(1023, 1023, 1023).0;
+ let plane = [white; haar::PIXELS];
+ let zero = [0i32; haar::PIXELS];
+ let mut blocks = KVec::new();
+ for _ in 0..16 {
+ blocks.push(haar::colour_block(&zero, &zero, &plane), GFP_KERNEL)?;
+ }
+ let geometry = haar::RIDGE_GEOMETRY;
+ let eight = haar::colour_strip(geometry, &blocks, 0, 0)?;
+ let ten = haar::colour_strip(geometry.with_depth(Depth::Ten), &blocks, 0, 0)?;
+ assert_ne!(eight[..], ten[..]);
+ // Saturating at the lower ceiling costs bits, so the 8-bit encoding is the shorter one.
+ assert!(eight.len() <= ten.len());
+ Ok(())
+ }
+
+ #[test]
+ fn haar_chroma_last_is_exact() {
+ use video::haar::{chroma_last, COEFFS};
+ let mut q = [0i32; COEFFS];
+ assert_eq!(chroma_last(&q), 0);
+ for exact in [1usize, 2, 3, 4, 7, 8, 11, 15, 16, 27, 31, 32, 48, 62, 63] {
+ q.fill(0);
+ q[exact] = 1;
+ assert_eq!(chroma_last(&q), exact);
+ }
+ }
+}
diff --git a/drivers/gpu/drm/vino/video/haar/transform.rs b/drivers/gpu/drm/vino/video/haar/transform.rs
new file mode 100644
index 000000000000..b009061ce13b
--- /dev/null
+++ b/drivers/gpu/drm/vino/video/haar/transform.rs
@@ -0,0 +1,802 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The Haar transform, the quantiser and the entropy coder.
+//!
+//! Three levels of separable 2-D Haar in a Mallat decomposition, a power-of-two quantiser
+//! whose ceilings differ per plane, and DisplayLink's unary VLC. These are the numbers the
+//! dock's decoder expects exactly; every constant here was read off the wire.
+
+use super::*;
+
+/// One separable 2-D Haar step over the top-left `n`x`n` of `src` (row-major, `n` columns,
+/// `n` in {8,4,2}). The 1-D Haar butterfly is `lo = a + b`, `hi = a - b`; applied to rows then
+/// columns it splits the `n`x`n` block into four `(n/2)`x`(n/2)` subbands written to
+/// `ll`/`hl`/`lh`/`hh` (row-major, stride `n/2`). Unnormalized -- `transform()` floor-divides
+/// the final coefficients by 64.
+///
+/// Fixed array sizes let LLVM remove bounds checks and allocate only the scratch space needed
+/// by each transform level.
+macro_rules! haar2d_level {
+ ($name:ident, $n:literal, $h:literal) => {
+ /// One separable 2-D Haar step for a single level; see the macro's documentation.
+ #[inline(always)]
+ fn $name(
+ src: &[i32; $n * $n],
+ ll: &mut [i32; $h * $h],
+ hl: &mut [i32; $h * $h],
+ lh: &mut [i32; $h * $h],
+ hh: &mut [i32; $h * $h],
+ ) {
+ // Row pass: L = row-lo, H = row-hi (each n rows x h cols).
+ let mut l = [0i32; $n * $h];
+ let mut hb = [0i32; $n * $h];
+ for r in 0..$n {
+ for i in 0..$h {
+ let (a, b) = (src[r * $n + 2 * i], src[r * $n + 2 * i + 1]);
+ l[r * $h + i] = a + b;
+ hb[r * $h + i] = a - b;
+ }
+ }
+ // Column pass: LL/LH = col-lo/hi of L, HL/HH = col-lo/hi of H (each h x h).
+ for c in 0..$h {
+ for i in 0..$h {
+ let (a, b) = (l[2 * i * $h + c], l[(2 * i + 1) * $h + c]);
+ ll[i * $h + c] = a + b;
+ lh[i * $h + c] = a - b;
+ let (a2, b2) = (hb[2 * i * $h + c], hb[(2 * i + 1) * $h + c]);
+ hl[i * $h + c] = a2 + b2;
+ hh[i * $h + c] = a2 - b2;
+ }
+ }
+ }
+ };
+}
+
+haar2d_level!(haar2d_8, 8, 4);
+haar2d_level!(haar2d_4, 4, 2);
+haar2d_level!(haar2d_2, 2, 1);
+
+/// Transform block geometry. Each 8x8 input block produces 64 coefficients.
+pub(crate) const DIM: usize = 8;
+pub(crate) const PIXELS: usize = DIM * DIM;
+pub(crate) const COEFFS: usize = 64;
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+pub(crate) const BLOCK: usize = PIXELS;
+/// Vino colour transform, in the codec's 64x fixed point: `Cb = 64(R-G)`,
+/// `Cr = 64(B-G)` (achromatic R=G=B -> Cb=Cr=0), and the reversible luma
+///
+/// ```text
+/// Y = 64*G + 64*((Cb_raw + Cr_raw) >> 2) where Cb_raw=R-G, Cr_raw=B-G
+/// ```
+///
+/// The arithmetic shift rounds negative chroma contributions towards negative infinity.
+///
+/// Channels are `i32` rather than `u8` because the same transform carries a 10-bit surface
+/// unchanged -- see [`Depth`]. Values are the framebuffer's own code words at whatever depth
+/// the plane is in; this applies no transfer function and no matrix of its own.
+pub(crate) fn colour(r: i32, g: i32, b: i32) -> (i32, i32, i32) {
+ let (cb, cr) = (r - g, b - g);
+ (64 * g + 64 * ((cb + cr) >> 2), 64 * cb, 64 * cr)
+}
+/// Apply the codec's 8x8 2-D Haar (Mallat) transform, floor-divided by 64. `block` is
+/// 8x8 luma (`Y` in the
+/// codec's x64 fixed point); the output is 64 coefficients in the wire's Mallat layout:
+/// `c[0]` = LL; `c[1..4]` = level-3 HL/LH/HH; `c[4..8]/[8..12]/[12..16]` = level-2 HL/LH/HH
+/// (2x2 row-major each); `c[16..32]`, `c[32..48]`, and `c[48..64]` are the level-1 HL, LH,
+/// and HH 4x4 bands. Each level-1 band uses the same 2x2 Morton scan. A uniform block yields
+/// `DC = mean`, all AC = 0.
+///
+/// This function must remain out of line. Inlining it into the frame encoder combines the
+/// transform scratch arrays with its callers and can exhaust a 16-KiB kernel stack.
+#[inline(never)]
+pub(crate) fn transform(block: &[i32; PIXELS]) -> [i32; COEFFS] {
+ let sh = |x: i32| x >> 6; // arithmetic shift: wire fixed-point floor division by 64
+ // Level 1: 8x8 -> three 4x4 detail bands.
+ let (mut ll1, mut hl1, mut lh1, mut hh1) = ([0i32; 16], [0i32; 16], [0i32; 16], [0i32; 16]);
+ haar2d_8(block, &mut ll1, &mut hl1, &mut lh1, &mut hh1);
+ // Level 2: LL1 (4x4) -> 2x2 subbands.
+ let (mut ll2, mut hl2, mut lh2, mut hh2) = ([0i32; 4], [0i32; 4], [0i32; 4], [0i32; 4]);
+ haar2d_4(&ll1, &mut ll2, &mut hl2, &mut lh2, &mut hh2);
+ // Level 3: LL2 (2x2) -> the DC and coarse coefficients.
+ let (mut ll3, mut hl3, mut lh3, mut hh3) = ([0i32; 1], [0i32; 1], [0i32; 1], [0i32; 1]);
+ haar2d_2(&ll2, &mut ll3, &mut hl3, &mut lh3, &mut hh3);
+ // Every 4x4 level-one band uses 2x2 Morton scan order.
+ const SCAN4_MORTON: [usize; 16] = [0, 2, 8, 10, 1, 3, 9, 11, 4, 6, 12, 14, 5, 7, 13, 15];
+ // Assemble band by band, not coefficient by coefficient. Selecting the source with a
+ // `from_fn(|i| match i { .. })` reads well but compiles to a range test per coefficient
+ // in a
+ // rolled 64-iteration loop: profiling this function showed the `cmp`/`and`/`jne` dispatch
+ // dominating it, against only ~66 add/sub for the transform itself. These fixed-length
+ // loops unroll into straight-line stores, and every element is written so the initialiser
+ // costs nothing.
+ let mut out = [0i32; COEFFS];
+ out[0] = sh(ll3[0]);
+ out[1] = sh(hl3[0]);
+ out[2] = sh(lh3[0]);
+ out[3] = sh(hh3[0]);
+ for i in 0..4 {
+ out[4 + i] = sh(hl2[i]);
+ out[8 + i] = sh(lh2[i]);
+ out[12 + i] = sh(hh2[i]);
+ }
+ for i in 0..16 {
+ let m = SCAN4_MORTON[i];
+ out[16 + i] = sh(hl1[m]);
+ out[32 + i] = sh(lh1[m]);
+ out[48 + i] = sh(hh1[m]);
+ }
+ out
+}
+/// Vino entropy VLC, indexed by symbol as `(code, nbits)` and emitted least-significant bit
+/// first.
+/// Symbol 0 = the 1-bit code `0` (zero / most common); symbol 31 = the all-ones escape prefix.
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+pub(crate) const CODEBOOK: [(u32, u8); 32] = [
+ (0, 1),
+ (1, 3),
+ (5, 3),
+ (3, 5),
+ (19, 5),
+ (11, 5),
+ (27, 5),
+ (7, 7),
+ (71, 7),
+ (39, 7),
+ (103, 7),
+ (23, 7),
+ (87, 7),
+ (55, 7),
+ (119, 7),
+ (15, 8),
+ (143, 8),
+ (79, 8),
+ (207, 8),
+ (47, 8),
+ (175, 8),
+ (111, 8),
+ (239, 8),
+ (31, 8),
+ (159, 8),
+ (95, 8),
+ (223, 8),
+ (63, 8),
+ (191, 8),
+ (127, 8),
+ (255, 9),
+ (511, 9),
+];
+
+/// LSB-first VLC bit packer matching the dock (final byte padded with 1-bits -- a
+/// truncated all-ones code required by the wire format).
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+pub(crate) struct Vlc {
+ out: KVec<u8>,
+ acc: u32,
+ nbits: u32,
+}
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+impl Vlc {
+ pub(crate) fn new() -> Self {
+ Self {
+ out: KVec::new(),
+ acc: 0,
+ nbits: 0,
+ }
+ }
+
+ /// Append one bit (LSB-first within each byte).
+ fn bit(&mut self, b: u32) -> Result {
+ self.acc |= (b & 1) << self.nbits;
+ self.nbits += 1;
+ if self.nbits == 8 {
+ self.out.push((self.acc & 0xff) as u8, GFP_KERNEL)?;
+ self.acc = 0;
+ self.nbits = 0;
+ }
+ Ok(())
+ }
+
+ /// Emit codebook `sym`'s code, least-significant bit first.
+ pub(crate) fn symbol(&mut self, sym: usize) -> Result {
+ let (code, n) = CODEBOOK[sym];
+ for k in 0..n as u32 {
+ self.bit(code >> k)?;
+ }
+ Ok(())
+ }
+
+ /// Emit one quantized coefficient as a JPEG-SSSS-style magnitude code. A zero coefficient
+ /// is the one-bit symbol 0. A nonzero
+ /// `q` emits the unary category `c = bit_length(|q|)` (c ones + a 0 terminator), then the
+ /// `(c-1)`-bit magnitude offset `|q| - 2^(c-1)` (MSB-first within the field), then a sign
+ /// bit (`0` = negative). This helper is used for the luma codebook; the full-colour path
+ /// uses [`Bits::esc`].
+ /// This helper rejects categories >= 9 instead of silently mixing the two grammars.
+ pub(crate) fn coeff(&mut self, q: i32) -> Result {
+ if q == 0 {
+ return self.symbol(0);
+ }
+ let c = mag_category(q); // bit_length(|q|)
+ if c >= 9 {
+ return Err(kernel::error::code::EOVERFLOW);
+ }
+ for _ in 0..c {
+ self.bit(1)?; // unary category
+ }
+ self.bit(0)?; // terminator
+ let offset = q.unsigned_abs() - (1 << (c - 1));
+ for i in (0..c - 1).rev() {
+ self.bit(offset >> i)?; // (c-1)-bit magnitude offset, MSB-first
+ }
+ self.bit(if q < 0 { 0 } else { 1 }) // sign bit (0 = negative)
+ }
+
+ /// Flush, padding the final byte with 1-bits (matches the dock's truncated all-ones code).
+ pub(crate) fn finish(mut self) -> Result<KVec<u8>> {
+ if self.nbits > 0 {
+ while self.nbits < 8 {
+ self.acc |= 1 << self.nbits;
+ self.nbits += 1;
+ }
+ self.out.push((self.acc & 0xff) as u8, GFP_KERNEL)?;
+ }
+ Ok(self.out)
+ }
+}
+/// Magnitude category of a quantized coefficient: `bit_length(|coeff|)`, or zero for zero.
+pub(crate) fn mag_category(coeff: i32) -> u32 {
+ coeff.unsigned_abs().checked_ilog2().map_or(0, |l| l + 1)
+}
+/// Maximum DC escape category at 8 bits per channel; the maximum category omits the unary
+/// 0-terminator (a complete prefix code on categories `1..=SOLID_DC_CMAX`).
+/// `|qY| <= 1020 => c <= 10`, `|qCb| <= 255`. See [`Depth::dc_cmax`] for the 10-bit ceiling.
+pub(crate) const SOLID_DC_CMAX: u32 = 10;
+/// Max LUMA AC magnitude category (the maximum category omits the unary 0-terminator).
+pub(crate) const AC_CMAX: u32 = 9;
+/// Maximum chroma AC magnitude category. It is higher than luma's, so a category-9 chroma
+/// coefficient still carries the unary 0-terminator that luma's omits.
+pub(crate) const CHROMA_AC_CMAX: u32 = 10;
+/// Bits per colour channel in the surface being encoded.
+///
+/// This is the only thing that differs between an SDR and an HDR frame on this wire. Measured
+/// against a DL7400 driven by Windows with HDR content: record framing, strip header,
+/// significance tree, transform and quantiser are byte-identical across an HDR toggle, and the
+/// colour maths is entirely host-side -- what arrives is an ordinary PQ-encoded 10-bit RGB
+/// surface. So this is one codec parameterised by depth, not two codecs.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum Depth {
+ /// 8 bits per channel: every mode on a Ridge dock, and a Navarro one outside HDR.
+ Eight,
+ /// 10 bits per channel, the DL-7000 "10bit profile".
+ Ten,
+}
+impl Depth {
+ /// Maximum escape category for a DC coefficient at this depth.
+ ///
+ /// A luma DC is four times the sample, so the largest magnitude is `4 * ((1 << bits) - 1)`
+ /// -- 1020 at 8 bits (category 10) and 4092 at 10 bits (category 12). The ceiling is part
+ /// of the wire format, not an implementation limit: at the maximum category `esc` omits
+ /// the unary 0-terminator, so a decoder reading with the wrong ceiling silently
+ /// desynchronises. Both values are measured, each uniquely, by decoding a captured PQ ramp
+ /// under every candidate and keeping the one that stays monotonic.
+ #[inline]
+ pub(crate) fn dc_cmax(self) -> u32 {
+ match self {
+ Depth::Eight => SOLID_DC_CMAX,
+ Depth::Ten => SOLID_DC_CMAX + 2,
+ }
+ }
+
+ /// Maximum escape category for a luma AC coefficient at this depth.
+ ///
+ /// A coefficient is four times the sample, so every depth-sensitive ceiling gains two
+ /// categories at 10 bits, not just the DC one, and the code table the dock is handed states
+ /// the same rise. Coding to the eight-bit ceiling on a ten-bit link fails quietly, because an
+ /// AC escape saturates rather than desynchronising: the category still fixes the field width,
+ /// so the stream stays in step and only the largest coefficients come out wrong. Smooth
+ /// gradients are perfect and every sharp edge is reconstructed from a truncated magnitude.
+ #[inline]
+ pub(crate) fn ac_cmax(self) -> u32 {
+ match self {
+ Depth::Eight => AC_CMAX,
+ Depth::Ten => AC_CMAX + 2,
+ }
+ }
+
+ /// Maximum escape category for a chroma AC coefficient at this depth.
+ ///
+ /// One category above luma's at either depth, so a coefficient at luma's ceiling still carries
+ /// the unary 0-terminator on the chroma planes. See [`Self::ac_cmax`].
+ #[inline]
+ pub(crate) fn chroma_ac_cmax(self) -> u32 {
+ match self {
+ Depth::Eight => CHROMA_AC_CMAX,
+ Depth::Ten => CHROMA_AC_CMAX + 2,
+ }
+ }
+
+ /// The depth of a DRM pixel format, or `None` for one this codec cannot encode.
+ ///
+ /// Both layouts are four bytes per pixel, so they share the snapshot path and differ only
+ /// in how `PixelSource` splits a word.
+ #[inline]
+ pub(crate) fn from_fourcc(fourcc: u32) -> Option<Self> {
+ match fourcc {
+ kernel::drm::fourcc::XRGB8888 => Some(Depth::Eight),
+ kernel::drm::fourcc::XRGB2101010 => Some(Depth::Ten),
+ _ => None,
+ }
+ }
+}
+/// LSB-first bit accumulator for the production AC-strip coder.
+///
+/// Bits are buffered in a 64-bit word and copied to `out` a byte at a time. `out` is incomplete
+/// until [`Bits::finish`] flushes the final zero-padded partial byte.
+pub(crate) struct Bits {
+ out: KVec<u8>,
+ /// Pending bits, LSB-first, valid in the low `nacc`.
+ acc: u64,
+ nacc: u32,
+ /// Which dialect of the shared unary code to emit; see [`Bits::unary`].
+ coding: super::super::video_arm::CodeTables,
+}
+impl Bits {
+ pub(crate) fn new(coding: super::super::video_arm::CodeTables) -> Self {
+ Self {
+ out: KVec::new(),
+ acc: 0,
+ nacc: 0,
+ coding,
+ }
+ }
+
+ /// Append one bit.
+ ///
+ /// `#[inline(always)]` because this is the codec's innermost operation -- `esc` calls it
+ /// once per unary, offset and sign bit. Out of line it costs a call, a return and a
+ /// reload/store of `acc`/`nacc` *per bit*; inlined, the accumulator stays in registers
+ /// across a run of bits. The eight-byte spill is deliberately left out of line so that
+ /// inlining stays cheap.
+ #[inline(always)]
+ fn bit(&mut self, b: u32) -> Result {
+ self.acc |= ((b & 1) as u64) << self.nacc;
+ self.nacc += 1;
+ if self.nacc == 64 {
+ self.spill()?;
+ }
+ Ok(())
+ }
+
+ /// Write the full accumulator out and reset it. Cold: once per 64 bits.
+ #[inline(never)]
+ fn spill(&mut self) -> Result {
+ for k in 0..8 {
+ self.out.push((self.acc >> (8 * k)) as u8, GFP_KERNEL)?;
+ }
+ self.acc = 0;
+ self.nacc = 0;
+ Ok(())
+ }
+
+ /// Flush the accumulator and yield the packed bytes, zero-padding the final byte.
+ pub(crate) fn finish(mut self) -> Result<KVec<u8>> {
+ let nbytes = self.nacc.div_ceil(8) as usize;
+ for k in 0..nbytes {
+ self.out.push((self.acc >> (8 * k)) as u8, GFP_KERNEL)?;
+ }
+ Ok(self.out)
+ }
+
+ /// The one code every field in a strip is built from: a unary category of `count` ones, a
+ /// `0` terminator unless the category is the codebook maximum, and exactly `count` payload
+ /// bits.
+ ///
+ /// The dock generations differ in where the payload sits and in which end of it comes
+ /// first. Ridge and the DL7400 group it after the terminator, most significant bit first.
+ /// A DL-3x00 decoder expects one payload bit immediately after each unary one, terminator
+ /// last, and reads that interleaved payload least significant bit first. Every
+ /// spelling is the same length, so emitting the wrong one produces records of exactly the
+ /// right size that decode to noise -- and a flat strip cannot tell them apart, because its
+ /// payload is all zeroes.
+ fn unary(&mut self, count: u32, terminate: bool, payload: u32) -> Result {
+ match self.coding {
+ super::super::video_arm::CodeTables::Wide => {
+ for _ in 0..count {
+ self.bit(1)?;
+ }
+ if terminate {
+ self.bit(0)?;
+ }
+ for i in (0..count).rev() {
+ self.bit(payload >> i)?;
+ }
+ }
+ super::super::video_arm::CodeTables::Narrow => {
+ for i in 0..count {
+ self.bit(1)?;
+ self.bit(payload >> i)?;
+ }
+ if terminate {
+ self.bit(0)?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ /// The shared escape value code: a 0 is one `0` bit; else a category of `c` carrying
+ /// `offset(c-1) ++ sign(1=positive)` as its payload. `c = bit_length(|v|)`.
+ pub(crate) fn esc(&mut self, v: i32, cmax: u32) -> Result {
+ if v == 0 {
+ return self.bit(0);
+ }
+ // Saturate a magnitude whose category exceeds the codebook maximum (`cmax`) to the
+ // largest value that category `cmax` encodes. This keeps the unary prefix at most
+ // `cmax` ones: a decoder that stops after `cmax` ones (the max-category escape, whose
+ // 0-terminator is omitted) would otherwise read the (cmax+1)th one as an offset bit
+ // and desync the rest of the strip. In-range coefficients (`c <= cmax`) are
+ // unaffected; this only bounds the out-of-range case, which the recovered grammar does
+ // not otherwise exercise.
+ let c = mag_category(v).min(cmax);
+ let off = v.unsigned_abs().min((1 << c) - 1) - (1 << (c - 1));
+ // The payload is the offset with the sign bit below it, which is `c` bits: an offset
+ // spans `c - 1` bits at category `c`.
+ self.unary(c, c < cmax, (off << 1) | u32::from(v > 0))
+ }
+
+ /// Per-block luma significance, after the two zero root branches a present chroma plane
+ /// replaces. For `k=floor(log2(64-last))` the category is `k` and the payload the `k`-bit
+ /// value `(64-2^k)-last`; a flat block is the maximum category with an all-zero payload,
+ /// which is why it reads as one fixed-width code rather than a position.
+ fn sync_unit_after(&mut self, last: usize, skip: usize) -> Result {
+ for _ in skip..2 {
+ self.bit(0)?;
+ }
+ if last == 0 {
+ self.unary(6, true, 0)
+ } else {
+ debug_assert!(last < COEFFS);
+ let k = usize::BITS - 1 - (COEFFS - last).leading_zeros();
+ let end = COEFFS - (1usize << k);
+ self.unary(k, true, (end - last) as u32)
+ }
+ }
+
+ fn sync_unit(&mut self, last: usize) -> Result {
+ self.sync_unit_after(last, 0)
+ }
+}
+/// Chroma AC quantizer. Coarse bands 1/2 and 4..11 use step 16; coefficient 3 and positions
+/// 12..47 use step 32; the final HH band uses step 64. All use signed half-up rounding.
+///
+/// Shifts rather than divisions, for the reason given on [`step_bias`]; steps 16/32/64 are
+/// shifts 4/5/6 and `step / 2` is `1 << (shift - 1)`.
+pub(crate) fn quantize_chroma_ac(coeff: i32, i: usize) -> i32 {
+ let shift = CHROMA_AC_SHIFT[i];
+ (coeff + (1 << (shift - 1))) >> shift
+}
+/// Per-plane DC quantizer, round-half-up on the SIGNED value (toward +inf): luma (plane 0)
+/// step 16, chroma step 64. `+224/64 = 3.5 -> 4`; `-8416/64 = -131.5 -> -131`.
+pub(crate) fn quantize_dc_round(plane: usize, v: i32) -> i32 {
+ let shift: u32 = if plane == 0 { 4 } else { 6 };
+ (v + (1 << (shift - 1))) >> shift
+}
+impl Bits {
+ /// Exact chroma last-position tree node. For `c=floor(log2(last+1))` the category is `c`
+ /// and the payload the `c`-bit offset `last-(2^c-1)`.
+ fn chroma_base(&mut self, last: usize) -> Result {
+ debug_assert!(last > 0 && last < COEFFS);
+ let c = usize::BITS - 1 - (last + 1).leading_zeros();
+ self.unary(c, true, (last - ((1usize << c) - 1)) as u32)
+ }
+
+ /// One block's three-plane significance tree. The luma code begins with two
+ /// zero root branches. A present Cr replaces the first with its chroma node; a present Cb
+ /// replaces the second.
+ pub(crate) fn colour_sync_unit(&mut self, lcr: usize, lcb: usize, ly: usize) -> Result {
+ match (lcr != 0, lcb != 0) {
+ (false, false) => self.sync_unit(ly),
+ (true, false) => {
+ self.chroma_base(lcr)?;
+ self.sync_unit_after(ly, 1)
+ }
+ (false, true) => {
+ self.bit(0)?;
+ self.chroma_base(lcb)?;
+ self.sync_unit_after(ly, 2)
+ }
+ (true, true) => {
+ self.chroma_base(lcr)?;
+ self.chroma_base(lcb)?;
+ self.sync_unit_after(ly, 2)
+ }
+ }
+ }
+
+ /// One block's colour AC: present planes in (Cr, Cb, Y) order, positions `1..=last`,
+ /// run-bit `0` for an insignificant coefficient else the magnitude escape. Chroma and luma
+ /// use DIFFERENT codebook maxima (`CHROMA_AC_CMAX` / `AC_CMAX`) -- see `CHROMA_AC_CMAX`.
+ pub(crate) fn colour_block_ac(
+ &mut self,
+ qcr: &[i32; COEFFS],
+ qcb: &[i32; COEFFS],
+ qy: &[i32; COEFFS],
+ lcr: usize,
+ lcb: usize,
+ ly: usize,
+ depth: Depth,
+ ) -> Result {
+ for &(q, last, cmax) in &[
+ (qcr, lcr, depth.chroma_ac_cmax()),
+ (qcb, lcb, depth.chroma_ac_cmax()),
+ (qy, ly, depth.ac_cmax()),
+ ] {
+ for i in 1..=last {
+ if q[i] == 0 {
+ self.bit(0)?;
+ } else {
+ self.esc(q[i], cmax)?;
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_haar_transform)]
+mod tests {
+ use super::*;
+
+ /// The AC ceilings scale with the sample depth, exactly as the DC ceiling does.
+ ///
+ /// A ten-bit sample makes every coefficient four times larger, which is two categories. Held
+ /// at their eight-bit values the escape saturates the bulk of the AC energy rather than just
+ /// the extremes, and the picture breaks up into blocks. The vendor's own stress content states
+ /// the same scaling: a two-pixel grating that is category 9 in SDR is category 11 in HDR.
+ #[test]
+ fn haar_depth_selects_the_ac_codebooks() {
+ use Depth;
+ assert_eq!(Depth::Eight.ac_cmax(), 9);
+ assert_eq!(Depth::Ten.ac_cmax(), 11);
+ assert_eq!(Depth::Eight.chroma_ac_cmax(), 10);
+ assert_eq!(Depth::Ten.chroma_ac_cmax(), 12);
+ // An AC ceiling left behind stays in step, because the category fixes the field width,
+ // and reconstructs every sharp edge from a truncated magnitude instead.
+ assert_eq!(Depth::Ten.ac_cmax(), Depth::Eight.ac_cmax() + 2);
+ assert_eq!(
+ Depth::Ten.chroma_ac_cmax(),
+ Depth::Eight.chroma_ac_cmax() + 2
+ );
+ assert_eq!(Depth::Ten.dc_cmax(), Depth::Eight.dc_cmax() + 2);
+ // Chroma stays one category above luma, so a coefficient at luma's ceiling still carries
+ // the unary terminator on the chroma planes.
+ assert_eq!(Depth::Eight.chroma_ac_cmax(), Depth::Eight.ac_cmax() + 1);
+ assert_eq!(Depth::Ten.chroma_ac_cmax(), Depth::Ten.ac_cmax() + 1);
+ }
+
+ /// The escape codebook's ceiling is part of the wire format and follows the sample depth.
+ ///
+ /// Measured against a DL7400 driven by Windows in its 10-bit profile: a captured PQ ramp
+ /// decodes monotonically at ceiling 12 and at no other value, and the SDR half of the same
+ /// capture only at 10. Getting this wrong does not degrade the picture, it desynchronises the
+ /// dock's decoder -- at the maximum category `esc` omits the unary 0-terminator, so the next
+ /// value's first bit is read as an offset bit.
+ #[test]
+ fn haar_depth_selects_the_dc_codebook() {
+ use Depth;
+ assert_eq!(Depth::Eight.dc_cmax(), 10);
+ assert_eq!(Depth::Ten.dc_cmax(), 12);
+ // A luma DC is four times the sample, so each ceiling is exactly the category that holds
+ // its depth's largest value: 4 x 255 = 1020 (c=10) and 4 x 1023 = 4092 (c=12).
+ assert_eq!(mag_category(4 * 255), Depth::Eight.dc_cmax());
+ assert_eq!(mag_category(4 * 1023), Depth::Ten.dc_cmax());
+ }
+
+ /// The depth comes from the committed framebuffer's fourcc, never from state of our own.
+ #[test]
+ fn haar_depth_from_fourcc() {
+ use Depth;
+ assert!(matches!(
+ Depth::from_fourcc(kernel::drm::fourcc::XRGB8888),
+ Some(Depth::Eight)
+ ));
+ assert!(matches!(
+ Depth::from_fourcc(kernel::drm::fourcc::XRGB2101010),
+ Some(Depth::Ten)
+ ));
+ assert!(Depth::from_fourcc(kernel::drm::fourcc::ARGB8888).is_none());
+ }
+
+ #[test]
+ fn haar_transform_uniform() {
+ use video::haar;
+ // A uniform block has the per-pixel value at DC and zero AC terms.
+ let block = [16320i32; haar::BLOCK];
+ let c = haar::transform(&block);
+ assert_eq!(c[0], 16320);
+ assert!(c[1..].iter().all(|&x| x == 0));
+ // White pixel -> Y plane -> Haar DC -> quantized value.
+ let (y, _, _) = haar::colour(255, 255, 255);
+ assert_eq!(
+ haar::quantize(haar::transform(&[y; haar::BLOCK])[0], 0),
+ 1020
+ );
+ }
+
+ /// The quantisers divide by powers of two with arithmetic shifts, avoiding a runtime division
+ /// for every coefficient.
+ ///
+ /// The rewrite is only valid because floor division by `2^k` IS an arithmetic right shift, for
+ /// negative operands as well as positive. That identity is easy to state and easy to get wrong
+ /// (a *truncating* `/` is not the same thing on negatives), and a coefficient off by one is a
+ /// wire-visible codec change. So assert it directly, over the full coefficient range the
+ /// transform can produce, against the equivalent division.
+ #[test]
+ fn quantiser_shifts_match_division() {
+ use video::haar::{quantize, COEFFS};
+ // 8-bit input in the codec's x64 fixed point, summed over an 8x8 block and floor-divided by
+ // 64 by `transform`, bounds |coeff| well inside this; step past it on both signs anyway.
+ const LIMIT: i32 = 200_000;
+ // The luma table, restated here as DIVISORS so the test is not written against the same
+ // shift constants it is checking.
+ let step_bias = |i: usize| -> (i32, i32) {
+ match i {
+ 0 | 1 | 2 => (16, 8),
+ 3 => (32, 16),
+ 4..=11 => (4, 2),
+ 12..=15 => (8, 4),
+ 16..=47 => (2, 0),
+ _ => (4, 2),
+ }
+ };
+ for i in 0..COEFFS {
+ let (step, bias) = step_bias(i);
+ for coeff in (-LIMIT..=LIMIT).step_by(37) {
+ let want = if bias == 0 {
+ let q = coeff.abs() / step;
+ if coeff < 0 {
+ -q
+ } else {
+ q
+ }
+ } else {
+ (coeff + bias).div_euclid(step)
+ }
+ .clamp(-2048, 2047);
+ assert_eq!(quantize(coeff, i), want);
+ }
+ }
+ // Boundary cases the stride above can step over: every exact multiple and half-step of the
+ // coarsest divisor, on both signs, is where floor-vs-truncate actually differs.
+ for i in 0..COEFFS {
+ let (step, bias) = step_bias(i);
+ for m in -4i32..=4 {
+ for d in [-1, 0, 1, step / 2, -step / 2] {
+ let coeff = m * step + d;
+ let want = if bias == 0 {
+ let q = coeff.abs() / step;
+ if coeff < 0 {
+ -q
+ } else {
+ q
+ }
+ } else {
+ (coeff + bias).div_euclid(step)
+ }
+ .clamp(-2048, 2047);
+ assert_eq!(quantize(coeff, i), want);
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn haar_transform_haar_vectors() {
+ // Independent golden vectors cover the source gradient blocks. Input luma is
+ // `Y = 64 * gray`.
+ use video::haar::{transform, DIM, PIXELS};
+ // Build an 8x8 Y block by evaluating a gray-per-(row,col) selector.
+ fn build(gray: impl Fn(usize, usize) -> i32) -> [i32; PIXELS] {
+ let mut b = [0i32; PIXELS];
+ for r in 0..DIM {
+ for c in 0..DIM {
+ b[r * DIM + c] = 64 * gray(r, c);
+ }
+ }
+ b
+ }
+ // vstripe2 (period-2 vertical, full contrast 0/255) -> level-2 HL band c[4..8] = -2040.
+ let c = transform(&build(|_, c| if (c / 2) & 1 != 0 { 255 } else { 0 }));
+ assert_eq!(&c[4..8], &[-2040, -2040, -2040, -2040]);
+ assert!(c[1..4].iter().all(|&x| x == 0) && c[8..].iter().all(|&x| x == 0));
+ // Period-four vertical stripe: coarse HL c[1] = -8160.
+ let c = transform(&build(|_, c| if (c / 4) & 1 != 0 { 255 } else { 0 }));
+ assert_eq!(c[1], -8160);
+ // Period-two horizontal stripe: level-two LH band is -2040.
+ let c = transform(&build(|r, _| if (r / 2) & 1 != 0 { 255 } else { 0 }));
+ assert_eq!(&c[8..12], &[-2040, -2040, -2040, -2040]);
+ // A per-column gradient exercises DC, coarse-HL, and the finest band.
+ let c = transform(&build(|_, col| 36 * col as i32));
+ assert_eq!(c[0], 8064); // DC = mean(36*0..36*7)*64/64 = 8064
+ assert_eq!(&c[4..8], &[-576, -576, -576, -576]);
+ // The level-1 tail contains three 4x4 Morton-scanned bands:
+ // c[16..32] = HL1, c[32..48] = LH1, and c[48..64] = HH1.
+ //
+ // A per-column ramp has no vertical detail: HL1 is uniformly -72 and LH1/HH1 are zero.
+ assert!(c[16..32].iter().all(|&x| x == -72)); // finest HL: horizontal detail only
+ assert!(c[32..].iter().all(|&x| x == 0)); // LH1 + HH1: no vertical detail
+ }
+
+ #[test]
+ fn haar_vlc_codebook_byte_exact() -> Result {
+ // The LSB-first entropy VLC is checked against independent golden output. Symbol 7 is the
+ // AC code
+ // 0b1110000 (LSB-first); four of them pack to the wire's per-block AC unit bytes, and the
+ // final byte is padded with 1-bits (a truncated all-ones code), exactly as the dock emits.
+ use Vlc;
+ let mut w = Vlc::new();
+ for _ in 0..4 {
+ w.symbol(7)?;
+ }
+ assert_eq!(&w.finish()?[..], &[0x87, 0xc3, 0xe1, 0xf0]);
+ // The full per-block AC unit `0 0 0 7 7 7 7` (idx1-3 zero, idx4-7 AC) -- matches the live
+ // wire bytes `38 1c 0e ...` captured for vstripe2.
+ let mut w = Vlc::new();
+ for s in [0usize, 0, 0, 7, 7, 7, 7] {
+ w.symbol(s)?;
+ }
+ assert_eq!(&w.finish()?[..4], &[0x38, 0x1c, 0x0e, 0x87]);
+ // Symbol 0 (the 1-bit `0` code) alone -> one byte padded with seven 1-bits.
+ let mut w = Vlc::new();
+ w.symbol(0)?;
+ assert_eq!(&w.finish()?[..], &[0xfe]);
+ Ok(())
+ }
+
+ #[test]
+ fn haar_coeff_magnitude_code() -> Result {
+ // The AC magnitude-code emitter is checked against per-coefficient golden wire bits for
+ // q-4, q-8, and q-16.
+ use Vlc;
+ // Four q-4 coefficients (category 3, zero offset) == four sym7 -- the per-block AC unit.
+ let mut w = Vlc::new();
+ for _ in 0..4 {
+ w.coeff(-4)?;
+ }
+ assert_eq!(&w.finish()?[..], &[0x87, 0xc3, 0xe1, 0xf0]);
+ // A zero coefficient is the 1-bit symbol 0 -> one byte padded with seven 1-bits.
+ let mut w = Vlc::new();
+ w.coeff(0)?;
+ assert_eq!(&w.finish()?[..], &[0xfe]);
+ // Within-category offset (q-6 = category 3, offset 2) and sign polarity (negative vs +).
+ let mut w = Vlc::new();
+ w.coeff(-6)?;
+ assert_eq!(&w.finish()?[..], &[0x97]);
+ let mut w = Vlc::new();
+ w.coeff(6)?;
+ assert_eq!(&w.finish()?[..], &[0xd7]); // same magnitude, sign bit flipped
+ // Category 5 with offset (q-16) spans two bytes.
+ let mut w = Vlc::new();
+ w.coeff(-16)?;
+ assert_eq!(&w.finish()?[..], &[0x1f, 0xf8]);
+ // The unsupported long-form escape is rejected.
+ let mut w = Vlc::new();
+ assert!(w.coeff(-256).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn haar_magnitude_category() {
+ // Magnitude category is `bit_length(abs(coeff))`.
+ use mag_category;
+ assert_eq!(mag_category(0), 0);
+ assert_eq!(mag_category(1), 1);
+ assert_eq!(mag_category(-4), 3);
+ assert_eq!(mag_category(7), 3);
+ assert_eq!(mag_category(-8), 4);
+ assert_eq!(mag_category(16), 5);
+ assert_eq!(mag_category(-128), 8);
+ assert_eq!(mag_category(255), 8);
+ }
+}
diff --git a/drivers/gpu/drm/vino/video_arm.rs b/drivers/gpu/drm/vino/video_arm.rs
new file mode 100644
index 000000000000..85a2bdef8d09
--- /dev/null
+++ b/drivers/gpu/drm/vino/video_arm.rs
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Video decoder configuration carried in cold pipe-arm records.
+
+use kernel::{alloc::flags::GFP_KERNEL, prelude::*};
+
+const WIDE_TABLE_RECORD_LEN: u16 = 194;
+const QUANT_TABLE_LEN: u16 = 82;
+
+/// Record kind of a code table, which also fixes how its values are laid out.
+const WIDE_TABLE_KIND: u16 = 0x000d;
+const NARROW_TABLE_KIND: u16 = 0x0009;
+
+/// Record kind of the quantiser table, the same on every generation.
+const QUANT_TABLE_KIND: u16 = 0x000a;
+
+// Five decoder code tables follow the mode header. Each record contains a table index, a version
+// word, and 47 little-endian values.
+const CODE_TABLES: [[u32; 47]; 5] = [
+ [
+ 0, 6, 0, 28, 0, 120, 0, 496, 0, 2016, 0, 8128, 0, 32640, 0, 130816, 262144, 0, 0, 0, 0, 0,
+ 0, 0, 0, 3, 0, 21, 0, 105, 0, 465, 0, 1953, 0, 8001, 0, 32385, 0, 130305, 261121, 0, 0, 0,
+ 0, 0, 0,
+ ],
+ [
+ 0, 6, 0, 28, 0, 120, 0, 496, 0, 2016, 0, 8128, 0, 32640, 0, 130816, 0, 523776, 1048576, 0,
+ 0, 0, 0, 0, 0, 3, 0, 21, 0, 105, 0, 465, 0, 1953, 0, 8001, 0, 32385, 0, 130305, 0, 522753,
+ 1046529, 0, 0, 0, 0,
+ ],
+ [
+ 0, 6, 0, 28, 0, 120, 0, 496, 0, 2016, 0, 8128, 0, 32640, 0, 130816, 0, 523776, 1048576, 0,
+ 0, 0, 0, 0, 0, 3, 0, 21, 0, 105, 0, 465, 0, 1953, 0, 8001, 0, 32385, 0, 130305, 0, 522753,
+ 1046529, 0, 0, 0, 0,
+ ],
+ [
+ 0, 6, 0, 28, 0, 120, 255, 512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 21,
+ 0, 105, 225, 480, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 6, 0, 28, 0, 120, 0, 496, 0, 2016, 0, 8128, 16383, 32768, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 3, 0, 21, 0, 105, 0, 465, 0, 1953, 0, 8001, 16129, 32512, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ],
+];
+
+// The same five tables for a DL-3x00 decoder, which states them as a counted list of 16-bit
+// values rather than a version word and 47 32-bit ones. The code they describe is a different one:
+// where the wider form carries a run of unary prefixes, this carries a plain power-of-two ladder.
+const NARROW_CODE_TABLES: [&[u16]; 5] = [
+ &[
+ 1, 0, 2, 0, 4, 0, 8, 0, 16, 0, 32, 0, 64, 0, 128, 0, 256, 512,
+ ],
+ &[
+ 1, 0, 2, 0, 4, 0, 8, 0, 16, 0, 32, 0, 64, 0, 128, 0, 256, 0, 512, 1024,
+ ],
+ &[
+ 1, 0, 2, 0, 4, 0, 8, 0, 16, 0, 32, 0, 64, 0, 128, 0, 256, 0, 512, 1024,
+ ],
+ &[1, 0, 2, 0, 4, 0, 8, 15, 2],
+ &[1, 0, 2, 0, 4, 0, 8, 0, 16, 0, 32, 0, 64, 127, 2],
+];
+
+/// The decoder code tables one dock generation states when it opens a stream.
+///
+/// The configuration record is otherwise identical everywhere -- the same mode header opens it and
+/// the same quantiser table closes it -- so only the code tables select a variant.
+///
+/// This also selects the dialect the encoder emits, because the two must agree: a dock told one
+/// code and sent another decodes every strip to noise while each record stays exactly the right
+/// length. Keeping both behind one field is what makes disagreeing impossible.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum CodeTables {
+ /// Five 47-entry 32-bit tables, each with a version word, under record kind 0x0d.
+ Wide,
+ /// Five counted 16-bit tables under record kind 0x09.
+ Narrow,
+}
+
+// Decoder quantization parameters. These match the Haar configuration used by the video encoder.
+const QUANT_TABLE: [u16; 41] = [
+ 10, 1, 1, 0, 64, 64, 16, 16, 16, 16, 16, 16, 16, 32, 32, 32, 1, 1, 1, 16, 16, 4, 16, 16, 4, 32,
+ 32, 8, 1, 1, 1, 32, 32, 2, 32, 32, 2, 64, 64, 4, 0,
+];
+
+fn push_u16(out: &mut KVec<u8>, value: u16) -> Result {
+ out.extend_from_slice(&value.to_le_bytes(), GFP_KERNEL)?;
+ Ok(())
+}
+
+fn push_u32(out: &mut KVec<u8>, value: u32) -> Result {
+ out.extend_from_slice(&value.to_le_bytes(), GFP_KERNEL)?;
+ Ok(())
+}
+
+/// The 26-byte `[len=0x0018][kind=0x030b]` header that states a stream's mode.
+///
+/// It opens the decoder configuration and is repeated verbatim by the mode-restating form of the
+/// per-frame stream report, so both build it here. The mode appears twice, each time as
+/// `[format][width][height][layout word]`.
+///
+/// The format word repeats the DMA format the set-mode states at offset 23: a connector whose
+/// timing is programmed for 30 bpp has to open its decoder for 30 bpp too.
+pub(super) fn mode_header(width: u16, height: u16, layout_word: u16, ten_bit: bool) -> [u8; 26] {
+ let format = if ten_bit { 0x0003u16 } else { 0x0002 };
+ let mut out = [0u8; 26];
+ for (i, value) in [
+ 0x0018u16,
+ 0x030b,
+ 0x0204,
+ 0x0002,
+ format,
+ width,
+ height,
+ layout_word,
+ format,
+ width,
+ height,
+ layout_word,
+ 0,
+ ]
+ .into_iter()
+ .enumerate()
+ {
+ out[i * 2..i * 2 + 2].copy_from_slice(&value.to_le_bytes());
+ }
+ out
+}
+
+/// Build the plaintext decoder configuration a connector's stream opens with.
+///
+/// The configuration opens with the stream's [`mode_header`] and closes with the quantiser table;
+/// `tail` is whatever the generation appends after that, which for some is a host-random nonce and
+/// for others nothing at all.
+/// One plane's escape codebook, stating a ceiling of `naturals + 1`.
+///
+/// The entries are `2^n * (2^(n+1) - 1)` up to a terminator of `2^(2N+2)`, and the second half of
+/// the record repeats each less `2^(n+1) - 1`.
+fn code_table(naturals: usize) -> [u32; 47] {
+ let mut t = [0u32; 47];
+ let mut n = 1;
+ while n <= naturals {
+ t[2 * n - 1] = ((1u64 << n) * ((1u64 << (n + 1)) - 1)) as u32;
+ t[24 + 2 * n - 1] = t[2 * n - 1] - ((1u32 << (n + 1)) - 1);
+ n += 1;
+ }
+ t[2 * naturals] = 1u32 << (2 * naturals + 2);
+ t[24 + 2 * naturals] = t[2 * naturals] - ((1u32 << (naturals + 2)) - 1);
+ t
+}
+
+/// Naturals each table carries for a 30 bpp connector, or zero for one the depth does not move.
+///
+/// Indexed as the dock is given them: luma AC, chroma AC, DC, then two the sample depth leaves
+/// alone. The chroma AC and DC tables are identical at 24 bpp because they share a ceiling there.
+const DEEP_NATURALS: [usize; 5] = [10, 11, 11, 0, 0];
+
+pub(super) fn build_config(
+ tables: CodeTables,
+ mode_header: &[u8; 26],
+ tail: &[u8],
+ ten_bit: bool,
+) -> Result<KVec<u8>> {
+ let mut out = KVec::new();
+
+ out.extend_from_slice(mode_header, GFP_KERNEL)?;
+
+ match tables {
+ CodeTables::Wide => {
+ // The dock decodes each plane with the ceiling its table states, so a table left at
+ // 24 bpp while the encoder codes 30 bpp coefficients loses the stream or the detail.
+ let mut deep = [0u32; 47];
+ for (index, table) in CODE_TABLES.iter().enumerate() {
+ let raise = ten_bit && DEEP_NATURALS[index] != 0;
+ if raise {
+ deep = code_table(DEEP_NATURALS[index]);
+ }
+ let table: &[u32; 47] = if raise { &deep } else { table };
+ push_u16(&mut out, WIDE_TABLE_RECORD_LEN)?;
+ push_u16(&mut out, ((index as u16) << 8) | WIDE_TABLE_KIND)?;
+ push_u32(&mut out, 1)?;
+ for &value in table {
+ push_u32(&mut out, value)?;
+ }
+ }
+ }
+ CodeTables::Narrow => {
+ for (index, table) in NARROW_CODE_TABLES.iter().enumerate() {
+ // The record length counts everything after itself: the kind word, the count
+ // word, and the values.
+ push_u16(&mut out, 4 + 2 * table.len() as u16)?;
+ push_u16(&mut out, ((index as u16) << 8) | NARROW_TABLE_KIND)?;
+ push_u16(&mut out, table.len() as u16)?;
+ for &value in table.iter() {
+ push_u16(&mut out, value)?;
+ }
+ }
+ }
+ }
+
+ push_u16(&mut out, QUANT_TABLE_LEN)?;
+ debug_assert_eq!(QUANT_TABLE[0], QUANT_TABLE_KIND);
+ for value in QUANT_TABLE {
+ push_u16(&mut out, value)?;
+ }
+ out.extend_from_slice(tail, GFP_KERNEL)?;
+ Ok(out)
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_video_arm)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn video_arm_configuration_uses_mode_and_nonce() -> Result {
+ let nonce = [0x5a; 14];
+ let header = mode_header(1920, 1080, 0x4000, false);
+ let config = build_config(CodeTables::Wide, &header, &nonce, false)?;
+
+ assert_eq!(config.len(), 1104);
+ assert_eq!(&config[10..14], &[0x80, 0x07, 0x38, 0x04]);
+ assert_eq!(&config[18..22], &[0x80, 0x07, 0x38, 0x04]);
+ assert_eq!(&config[1090..], &nonce);
+
+ // A 30 bpp connector differs from a 24 bpp one in the two format words and nothing else.
+ assert_eq!(&header[8..10], &[0x02, 0x00]);
+ assert_eq!(&header[16..18], &[0x02, 0x00]);
+ let deep = mode_header(1920, 1080, 0x4000, true);
+ assert_eq!(&deep[8..10], &[0x03, 0x00]);
+ assert_eq!(&deep[16..18], &[0x03, 0x00]);
+ assert_eq!(&deep[10..16], &header[10..16]);
+
+ // The generator has to reproduce the captured tables before it can be trusted to raise
+ // one, and the record holds exactly the eleven naturals a ceiling of twelve needs.
+ assert_eq!(code_table(8), CODE_TABLES[0]);
+ assert_eq!(code_table(9), CODE_TABLES[1]);
+ assert_eq!(code_table(9), CODE_TABLES[2]);
+ let deep_dc = code_table(DEEP_NATURALS[2]);
+ assert_eq!(deep_dc[21], 8_386_560);
+ assert_eq!(deep_dc[22], 16_777_216);
+ assert_eq!(deep_dc[46], 16_769_025);
+ Ok(())
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 6/13] drm/vino: add the video codec
2026-08-26 16:37 ` [PATCH v3 6/13] drm/vino: add the video codec Mike Lothian
@ 2026-08-26 16:56 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 16:56 UTC (permalink / raw)
To: Mike Lothian; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The Navarro parameter map generation exceeds the maximum record stride cap (4080 bytes) for high-resolution displays like 4K.
- [High] The production entropy VLC encoder `Bits::finish` pads the final byte with zeroes instead of the wire-format-required one-bits.
--
commit 74ff1e1c314d743efc9972c4a877920b6216a374
Author: Mike Lothian <mike@fireburn.co.uk>
Date: Wed Aug 26 16:37:31 2026 +0000
drm/vino: add the video codec
This commit adds a proprietary multilevel Haar codec to encode video frames
for DisplayLink docks. It includes the encoder, transform, strip grammar,
record framing, and a software color pipeline.
> diff --git a/drivers/gpu/drm/vino/video/haar/records.rs b/drivers/gpu/drm/vino/video/haar/records.rs
> new file mode 100644
> index 0000000000000..422ae7d83ae27
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/video/haar/records.rs
[ ... ]
> + let mut band = 0usize;
> + let mut record = 0usize;
> + while band < bands {
> + // The first record takes up to PARAM_TLVS_PER_RECORD sub-records, the second the rest.
> + let take_tlvs = if record == 0 {
> + PARAM_TLVS_PER_RECORD
> + } else {
> + bands.div_ceil(PARAM_BANDS_PER_TLV)
> + };
[Severity: High]
Will this logic exceed the 4080-byte stride cap on high-resolution displays?
When configuring a supported high-resolution mode like 4K (3840x2160), the
display is split into 270 bands (bands = 270). The first parameter map record
consumes 15 TLVs (120 bands). For the second record, navarro_strip_params()
evaluates take_tlvs as 270.div_ceil(8) = 34.
The loop then writes the remaining 19 TLVs into the second record. Since 19
TLVs * 262 bytes equals 4978 bytes, this exceeds the STRIDE_CAP of 4080
bytes, which can cause the dock's USB endpoint to halt and the display to
fail.
[ ... ]
> diff --git a/drivers/gpu/drm/vino/video/haar/transform.rs b/drivers/gpu/drm/vino/video/haar/transform.rs
> new file mode 100644
> index 0000000000000..b009061ce13bc
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/video/haar/transform.rs
[ ... ]
> + /// Flush the accumulator and yield the packed bytes, zero-padding the final byte.
> + pub(crate) fn finish(mut self) -> Result<KVec<u8>> {
> + let nbytes = self.nacc.div_ceil(8) as usize;
> + for k in 0..nbytes {
> + self.out.push((self.acc >> (8 * k)) as u8, GFP_KERNEL)?;
> + }
> + Ok(self.out)
> + }
[Severity: High]
Does padding the final byte with zeros instead of ones violate the expected
wire format and risk image corruption?
In Bits::finish(), the unused high bits of self.acc contain zeros, meaning
the partial byte at the end of an unaligned strip is padded with zeros.
Because zero is a valid symbol in the entropy code, the dock may decode
unintended extra coefficients and desynchronize.
If the byte is padded with 1-bits instead, it forms a truncated all-ones
escape sequence that the decoder safely ignores.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=6
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 7/13] drm/vino: add control-session bring-up
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (5 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 6/13] drm/vino: add the video codec Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:57 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 8/13] drm/vino: add the KMS device and the atomic path Mike Lothian
` (5 subsequent siblings)
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, linux-kernel
Bringing a dock up runs once per bind, in order: a plaintext initialisation
preamble, the link AKE, then the sealed setup burst that authenticates each
downstream connector, transitions the video engine and reads back what the
dock will answer. The burst's ordering and its spacing both come from the
vendor, and a dock driven faster than it stays dark.
Add the three stages as three modules, and the interrupt-endpoint reader
that decodes what the dock pushes rather than counting it and discarding
the bytes.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/session.rs | 625 ++++++++++
drivers/gpu/drm/vino/session/replies.rs | 337 ++++++
drivers/gpu/drm/vino/session/setup.rs | 1390 +++++++++++++++++++++++
3 files changed, 2352 insertions(+)
create mode 100644 drivers/gpu/drm/vino/session.rs
create mode 100644 drivers/gpu/drm/vino/session/replies.rs
create mode 100644 drivers/gpu/drm/vino/session/setup.rs
diff --git a/drivers/gpu/drm/vino/session.rs b/drivers/gpu/drm/vino/session.rs
new file mode 100644
index 000000000000..f14a0f47311e
--- /dev/null
+++ b/drivers/gpu/drm/vino/session.rs
@@ -0,0 +1,625 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Bringing a dock's encrypted control session up.
+//!
+//! In order: the plaintext initialisation preamble, the HDCP 2.2 link AKE, then the sealed control
+//! setup that authenticates each downstream connector and reads its EDID. Everything here runs
+//! once per bind, on the bring-up worker; the steady-state keepalive and the KMS transactions live
+//! in [`super::drm_sink`].
+
+use super::*;
+
+mod replies;
+mod setup;
+
+impl VinoDriver {
+ /// Initialize the plaintext control transport.
+ pub(super) fn bring_up(link: &UsbLink<'_>, profile: &DockProfile) -> Result {
+ // Control-request preamble: dock identity, interface selection, then the
+ // vendor-OUT 0x24 / vendor-IN 0x22 pair that starts the HDCP path.
+ const VENDOR_OUT: u8 = 0x40; // host->link, vendor, device
+ const VENDOR_IN_IFACE: u8 = 0xc1; // device-to-host, vendor, interface recipient
+
+ // Individual vendor requests may stall. Only bulk initialization and
+ // its acknowledgment are required.
+ let mut identity_bytes = [0u8; 16];
+ match link.control_recv(
+ 0xfe,
+ VENDOR_IN_IFACE,
+ 0,
+ 1,
+ &mut identity_bytes,
+ timeout(),
+ GFP_KERNEL,
+ ) {
+ // The raw blob is printed only when it cannot be read as an identity, which is the
+ // case where someone has to decode it by hand. Info, not debug: on unfamiliar
+ // hardware this is what places the device, and needing a debug build to see it costs
+ // a whole test round trip.
+ Ok(()) => match firmware::Identity::parse(&identity_bytes) {
+ // The DFU interface names the dock at info level, so one that parses is already
+ // reported once.
+ Some(id) => vino_debug!("{id} running firmware {}\n", id.version),
+ None => pr_info!("unrecognised device identity = {identity_bytes:02x?}\n"),
+ },
+ Err(e) => pr_info!("device identity unavailable ({e:?})\n"),
+ }
+ // A composite driver may only change its own interface.
+ match link.set_alternate_setting(0) {
+ Ok(()) => {}
+ Err(e) => vino_debug!("vino: alternate setting unchanged ({e:?})\n"),
+ }
+ // The first vendor transition is platform-specific even though both platforms use the
+ // same request number. Ridge uses wValue=3; both occurrences in the authenticated
+ // Navarro/DLM USB transcript use wValue=0. Sending Ridge's value here still permits AKE
+ // and an exact EP02 transcript, but leaves Navarro's video-side state machine different
+ // before its later value-0 commit.
+ let vendor_state = profile.protocol.initial_vendor_state;
+ match link.control_send(
+ 0x24,
+ VENDOR_OUT,
+ vendor_state,
+ 0,
+ &[],
+ timeout(),
+ GFP_KERNEL,
+ ) {
+ Ok(()) => {}
+ Err(e) => vino_debug!("vino: vendor preamble request stalled ({e:?})\n"),
+ }
+ // Request interface 0 state using the vendor interface recipient.
+ let mut state = [0u8; 28];
+ match link.control_recv(
+ 0x22,
+ VENDOR_IN_IFACE,
+ 1,
+ 0,
+ &mut state,
+ timeout(),
+ GFP_KERNEL,
+ ) {
+ Ok(()) => vino_debug!("vino: interface state = {state:02x?}\n"),
+ Err(e) => vino_debug!("vino: interface state unavailable ({e:?})\n"),
+ }
+
+ // The dock requires this exact plaintext initialization order. It acknowledges the
+ // sequence only after `init_4` and the following probe. The interleaved descriptor reads
+ // are best-effort because a short reply still completes the required control transfer.
+ const STD_IN: u8 = 0x80; // link->host, standard, device
+ let mut config_descriptor = KVec::from_elem(0u8, 618, GFP_KERNEL)?;
+ let _ = link.control_recv(
+ 0x06,
+ STD_IN,
+ 0x0200,
+ 0,
+ &mut config_descriptor[..40],
+ timeout(),
+ GFP_KERNEL,
+ ); // CONFIG, 40
+ let _ = link.control_recv(
+ 0x06,
+ STD_IN,
+ 0x0200,
+ 0,
+ &mut config_descriptor,
+ timeout(),
+ GFP_KERNEL,
+ );
+
+ // Report EP02's maximum packet size because exact-multiple messages require an explicit
+ // terminating short packet.
+ {
+ let total = ((config_descriptor[2] as usize) | ((config_descriptor[3] as usize) << 8))
+ .min(config_descriptor.len());
+ let mut i = 0usize;
+ while i + 2 <= total {
+ let blen = config_descriptor[i] as usize;
+ if blen == 0 {
+ break;
+ }
+ if config_descriptor[i + 1] == 0x05
+ && i + 7 <= total
+ && config_descriptor[i + 2] == EP_CTRL_OUT
+ {
+ let wmax = (config_descriptor[i + 4] as u16)
+ | ((config_descriptor[i + 5] as u16) << 8);
+ vino_debug!("vino: EP02 max packet size {wmax}\n");
+ }
+ i += blen;
+ }
+ }
+
+ let send_required = |label: &str, msg: &[u8]| -> Result {
+ match link.ctrl_send(msg, timeout(), GFP_KERNEL) {
+ Ok(_) => Ok(vino_debug!("vino: sent {label} ({} bytes)\n", msg.len())),
+ Err(e) => {
+ pr_err!("vino: {label} failed ({e:?})\n");
+ Err(e)
+ }
+ }
+ };
+ send_required("init_0", &proto::init_0()?)?;
+ send_required("init_25", &proto::init_25()?)?;
+ // Two required string reads between `init_25` and `init_4`.
+ let _ = link.control_recv(
+ 0x06,
+ STD_IN,
+ 0x0300,
+ 0x0000,
+ &mut config_descriptor[..255],
+ timeout(),
+ GFP_KERNEL,
+ ); // STRING #0
+ let _ = link.control_recv(
+ 0x06,
+ STD_IN,
+ 0x0303,
+ 0x0409,
+ &mut config_descriptor[..255],
+ timeout(),
+ GFP_KERNEL,
+ ); // STRING #3 en-US
+ send_required("init_4+probe", &proto::init_4_probe()?)?;
+
+ // Read the single ACK that follows init_4+probe.
+ let mut ack = KVec::from_elem(0u8, 1024, GFP_KERNEL)?;
+ match link.ctrl_recv(&mut ack, timeout(), GFP_KERNEL) {
+ Ok(n) => vino_debug!(
+ "vino: session-init ACK = {n} bytes: {:02x?}\n",
+ &ack[..n.min(40)]
+ ),
+ Err(e) => {
+ pr_err!("vino: session acknowledgment failed ({e:?})\n");
+ return Err(e);
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Number of display connectors the post-msg0 CP setup burst re-states the AKE for. Tied to the
+ /// single connector-count knob `drm_sink::MAX_CONNECTORS` so bumping the connector count is a
+ /// one-line change (was a duplicated literal `2` that had to be kept in sync by hand).
+ pub(super) const CP_SETUP_CONNECTORS: usize = drm_sink::MAX_CONNECTORS;
+
+ /// Reads the next HDCP response (type=4 sub=0x25, sec 5.2) from EP `0x84`,
+ /// skipping any non-HDCP frames (e.g. plain ACKs) in between, and returns the
+ /// parsed `(msg_id, payload)`. Bounded retry so a chatty dock can't wedge us.
+ fn recv_hdcp(link: &UsbLink<'_>) -> Result<(u8, KVec<u8>)> {
+ const SUB_HDCP_RESP: u16 = 0x25;
+ // The dock interleaves capability blocks up to ~5.8 KiB into the AKE reply
+ // stream; size the buffer like the rest of the EP84 reads ([`EP84_BUF`]) so a
+ // large frame is read whole rather than truncated/`-EOVERFLOW`'d.
+ let mut buf = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL)?;
+ for _ in 0..24 {
+ // The dock interleaves status and capability pushes with the HDCP replies.
+ let n = link.ctrl_recv(&mut buf, timeout(), GFP_KERNEL)?;
+ if n < 16 {
+ continue;
+ }
+ // Include interleaved capability and status frames in dynamic-debug output.
+ {
+ let wsub = u16::from_le_bytes([buf[8], buf[9]]);
+ let iid = if n >= 18 {
+ u16::from_le_bytes([buf[16], buf[17]])
+ } else {
+ 0
+ };
+ let isub = if n >= 20 {
+ u16::from_le_bytes([buf[18], buf[19]])
+ } else {
+ 0
+ };
+ vino_debug!(
+ "vino: AKE-EP84 {n}B wsub={wsub:#x} inner_id={iid:#x} inner_sub={isub:#x}\n"
+ );
+ }
+ if u16::from_le_bytes([buf[8], buf[9]]) != SUB_HDCP_RESP {
+ continue; // non-HDCP frame -- skip
+ }
+ if let Some((id, payload)) = ake::parse_in(&buf[16..n]) {
+ // Inner msg_id 0 is a status/ACK frame (the dock emits one as a
+ // sub=0x25 frame after each OUT message, e.g. the `14 00 76 00...`
+ // frame after AKE_Init) -- skip it and keep reading for the real
+ // HDCP response, mirroring the oracle's recv_hdcp_msg.
+ if id == 0 {
+ continue;
+ }
+ let mut pl = KVec::with_capacity(payload.len(), GFP_KERNEL)?;
+ pl.extend_from_slice(payload, GFP_KERNEL)?;
+ return Ok((id, pl));
+ }
+ }
+ Err(EINVAL)
+ }
+
+ /// Drain one repeater-authentication acknowledgment before the next request.
+ ///
+ /// The dock enforces request/reply lockstep during this phase. The drain is bounded and
+ /// best-effort so an idle dock cannot stall teardown.
+ fn pace_cap_ack(link: &UsbLink<'_>, want_ctr: u16, saw_cap_complete: &mut bool) {
+ // EP84 frames here can carry an interleaved capability block up to ~5.8 KiB;
+ // size to [`EP84_BUF`] so a large frame isn't truncated mid-pacing.
+ let Ok(mut buf) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) else {
+ return;
+ };
+ for _ in 0..8 {
+ match link.ctrl_recv(&mut buf, Delta::from_millis(30), GFP_KERNEL) {
+ Ok(len) if len >= 22 => {
+ let wsub = u16::from_le_bytes([buf[8], buf[9]]);
+ let iid = u16::from_le_bytes([buf[16], buf[17]]);
+ let isub = u16::from_le_bytes([buf[18], buf[19]]);
+ let ictr = u16::from_le_bytes([buf[20], buf[21]]);
+ if iid == 0x0b && isub == 0x84 {
+ *saw_cap_complete = true;
+ }
+ // The per-frame cap-ack: wsub=0x25, inner id=0x14 sub=0x10 ctr=want.
+ // An interleaved cap push (sub=0x84) or earlier ack -- keep reading.
+ if wsub == 0x25 && iid == 0x14 && ictr == want_ctr {
+ return;
+ }
+ }
+ // A short frame (header-only ack/keepalive): not our cap-ack, but the
+ // dock is still talking -- keep pacing rather than bailing out.
+ Ok(_) => continue,
+ // Nothing queued within the short window -- the dock is idle, don't block.
+ Err(_) => return,
+ }
+ }
+ }
+
+ /// Drain the terminal capability burst before arming the encrypted control plane.
+ ///
+ /// The terminal markers are capability-complete (`id=0x0b sub=0x84`) and
+ /// `RepeaterAuth_Stream_Ready`. If the latter is absent, a bounded quiet interval after the
+ /// capability marker is accepted for firmware compatibility.
+ fn wait_cap_complete(link: &UsbLink<'_>, kd: &[u8; 32], mut saw_0b: bool) {
+ let Ok(mut buf) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) else {
+ return;
+ };
+ // M' verification is diagnostic until every vendor stream-management field is decoded.
+ let sha_kd = crypto::sha256(kd);
+ let mut saw_ready = false;
+ let mut quiet = 0usize;
+ const QUIET_GAP: usize = 3; // ~3 consecutive empty short reads => dock done pushing
+ const MAX_ROUNDS: usize = 48;
+ for _ in 0..MAX_ROUNDS {
+ match link.ctrl_recv(&mut buf, Delta::from_millis(5), GFP_KERNEL) {
+ Ok(len) if len >= 20 => {
+ quiet = 0;
+ let iid = u16::from_le_bytes([buf[16], buf[17]]);
+ let isub = u16::from_le_bytes([buf[18], buf[19]]);
+ let mid = if len >= 26 { buf[25] } else { 0 }; // HDCP msg_id (body[9])
+ if isub == 0x84 && iid == 0x0b {
+ saw_0b = true;
+ }
+ if mid == ake::id::REPEATERAUTH_STREAM_READY && len >= 58 {
+ saw_ready = true;
+ let mprime = &buf[26..58];
+ vino_debug!("vino: AKE: Stream_Ready (0x11) M'={mprime:02x?}\n");
+ // The content-stream-management input contains two
+ // seven-byte stream entries and a three-byte sequence.
+ let m_data: [u8; 17] = [
+ 0, 0, 0, 0x04, 0, 0, 0, // stream 0: StreamID_Type[0]
+ 0, 0, 0, 0x05, 0, 0, 0, // stream 1: StreamID_Type[1]
+ 0, 0, 0, // seq_num_M = 0 (first Stream_Manage, big-endian)
+ ];
+ let m = crypto::hmac_sha256(&sha_kd, &m_data);
+ let eq = if &m[..] == mprime { "==" } else { "!=" };
+ vino_debug!("vino: AKE: M {} M' (CSM stream-entry layout)\n", eq);
+ } else if mid == ake::id::RECEIVER_AUTH_STATUS && len >= 27 {
+ vino_debug!("vino: AKE: RECEIVER_AUTH_STATUS=0x{:02x}\n", buf[26]);
+ }
+ // Both terminal markers complete the burst; do not add a quiet delay here.
+ if saw_0b && saw_ready {
+ vino_debug!("vino: repeater authentication complete\n");
+ return;
+ }
+ }
+ // Empty/short read = a quiet window. Fallback when Stream_Ready (0x11) never
+ // arrives:
+ // once id=0x0b has arrived AND the dock has been quiet for QUIET_GAP rounds, the
+ // terminal burst is drained -- arm now.
+ _ => {
+ if saw_0b {
+ quiet += 1;
+ if quiet >= QUIET_GAP {
+ vino_debug!("vino: repeater reply drained (ready={saw_ready})\n");
+ return;
+ }
+ }
+ }
+ }
+ }
+ vino_debug!("vino: repeater drain ended (complete={saw_0b}, ready={saw_ready})\n");
+ }
+
+ /// Run HDCP 2.2 AKE, locality check, session-key exchange and repeater authentication.
+ ///
+ /// `H'`, `L'` and `V'` are verified locally. Outbound messages use `type=4 sub=0x04` and the
+ /// inner sequence is:
+ ///
+ /// * ctr=1 session-init ACK (id=0x14/0x76), ctr=2 AKE_Init, ctr=3 AKE_Transmitter_Info
+ /// * ctr=4 AKE_No_Stored_km, ctr=5 LC_Init, ctr=6 SKE_Send_Eks
+ /// * ctr=7 RepeaterAuth_Send_Ack, ctr=8 RepeaterAuth_Stream_Manage (then msg0 at ctr=9)
+ pub(super) fn run_ake(link: &UsbLink<'_>) -> Result<Session> {
+ use ake::id;
+
+ let mut saw_cap_complete = false;
+
+ // A warm rebind can leave replies from the previous session queued on EP84.
+ let flush_probe = Delta::from_millis(3);
+ if let Ok(mut flush) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) {
+ let mut flushed = 0usize;
+ for _ in 0..32 {
+ match link.ctrl_recv(&mut flush, flush_probe, GFP_KERNEL) {
+ Ok(n) if n > 0 => flushed += 1,
+ _ => break,
+ }
+ }
+ if flushed > 0 {
+ vino_debug!("vino: flushed {flushed} stale EP84 frame(s) before AKE\n");
+ }
+ }
+
+ // The setup phase continues this counter through `Session::next_ctr`.
+ let mut hseq: u32 = 1;
+
+ // (1) session-init ACK (ctr=1, id=0x14/0x76).
+ link.ctrl_send(&ake::session_init_ack(hseq, 0)?, timeout(), GFP_KERNEL)?;
+ // The dock requires the counter-1 echo to be drained before AKE_Init.
+ Self::pace_cap_ack(link, hseq as u16, &mut saw_cap_complete);
+ hseq += 1;
+
+ // (2) AKE_Init -- use a fresh rtx and the transmitter capability profile.
+ let mut rtx = [0u8; drm_hdcp::RTX_LEN];
+ rng::fill(&mut rtx);
+ link.ctrl_send(
+ &ake::ake_init(hseq, 0, &rtx, &[0; 3])?,
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ hseq += 1;
+
+ // (2) AKE_Send_Cert: payload = REPEATER(1) || cert_rx(522). Extract the
+ // RSA-1024 public key (modulus[5..133], exponent[133..136]).
+ let (cid, cert_msg) = Self::recv_hdcp(link)?;
+ const CERT_KEY_END: usize = 5 + drm_hdcp::RSA_MODULUS_LEN + drm_hdcp::RSA_EXPONENT_LEN;
+ if cid != id::AKE_SEND_CERT || cert_msg.len() < 1 + CERT_KEY_END {
+ pr_err!(
+ "vino: AKE: bad AKE_Send_Cert (id={cid:#x}, {} B)\n",
+ cert_msg.len()
+ );
+ return Err(EINVAL);
+ }
+ let repeater = cert_msg[0] != 0;
+ let cert = &cert_msg[1..];
+ let mut modulus = [0u8; drm_hdcp::RSA_MODULUS_LEN];
+ modulus.copy_from_slice(&cert[5..5 + drm_hdcp::RSA_MODULUS_LEN]);
+ let mut exponent = [0u8; drm_hdcp::RSA_EXPONENT_LEN];
+ exponent.copy_from_slice(&cert[5 + drm_hdcp::RSA_MODULUS_LEN..CERT_KEY_END]);
+
+ // (3) AKE_Transmitter_Info (ctr=3), then read AKE_Receiver_Info (RxCaps unused).
+ link.ctrl_send(&ake::ake_transmitter_info(hseq, 0)?, timeout(), GFP_KERNEL)?;
+ hseq += 1;
+ let _ = Self::recv_hdcp(link)?;
+
+ // (5) AKE_No_Stored_km -- fresh km, RSA-OAEP-SHA256 to Ekpub(km).
+ let mut km = kernel::crypto::Secret::<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>::zeroed();
+ rng::fill(&mut km[..]);
+ let mut rsa = kernel::crypto::akcipher::RsaPublicKey::new(&modulus, &exponent, GFP_KERNEL)?;
+ let ekpub = hdcp::oaep_encrypt_km(&mut rsa, &km)?;
+ // (4) AKE_No_Stored_km (ctr=4). The dock authenticates its downstream link before it
+ // answers, so the following receive naturally covers that interval.
+ link.ctrl_send(
+ &ake::ake_no_stored_km(hseq, 0, &ekpub)?,
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ hseq += 1;
+
+ // (6) AKE_Send_Rrx.
+ let (rid, rrx_pl) = Self::recv_hdcp(link)?;
+ if rid != id::AKE_SEND_RRX || rrx_pl.len() < drm_hdcp::RRX_LEN {
+ pr_err!("vino: AKE: bad AKE_Send_Rrx (id={rid:#x})\n");
+ return Err(EINVAL);
+ }
+ let mut rrx = [0u8; drm_hdcp::RRX_LEN];
+ rrx.copy_from_slice(&rrx_pl[..drm_hdcp::RRX_LEN]);
+
+ // (7)/(8) AKE_Send_H_prime -- verify H' = HMAC(kd, rtx^REPEATER).
+ let (hid, hp) = Self::recv_hdcp(link)?;
+ if hid != id::AKE_SEND_H_PRIME || hp.len() < drm_hdcp::H_PRIME_LEN {
+ pr_err!("vino: AKE: bad H' (id={hid:#x})\n");
+ return Err(EINVAL);
+ }
+ let kd = hdcp::derive_kd(&km, &rtx, &rrx)?;
+ if hdcp::compute_h(&kd, &rtx, repeater)[..] != hp[..drm_hdcp::H_PRIME_LEN] {
+ pr_err!("vino: AKE: H' mismatch -- authentication failed\n");
+ return Err(EINVAL);
+ }
+ vino_debug!("vino: AKE: H' verified\n");
+
+ // (9) AKE_Send_Pairing_Info (Ekh_km) -- read and discard (no-stored path).
+ let _ = Self::recv_hdcp(link)?;
+
+ // (10) Locality Check -- LC_Init(rn) then verify L'.
+ let mut rn = [0u8; drm_hdcp::RN_LEN];
+ rng::fill(&mut rn);
+ // (5) LC_Init (ctr=5).
+ link.ctrl_send(&ake::lc_init(hseq, 0, &rn)?, timeout(), GFP_KERNEL)?;
+ hseq += 1;
+ let (lid, lp) = Self::recv_hdcp(link)?;
+ if lid != id::LC_SEND_L_PRIME || lp.len() < drm_hdcp::L_PRIME_LEN {
+ pr_err!("vino: AKE: bad L' (id={lid:#x})\n");
+ return Err(EINVAL);
+ }
+ if hdcp::compute_l(&kd, &rrx, &rn)[..] != lp[..drm_hdcp::L_PRIME_LEN] {
+ pr_err!("vino: AKE: L' mismatch -- locality check failed\n");
+ return Err(EINVAL);
+ }
+ vino_debug!("vino: AKE: L' verified\n");
+
+ // (11) Session Key Exchange -- send Edkey(ske_ks) and the fresh RIV. The wrapped value is
+ // the raw SKE key; both peers apply the control-plane whitening constant afterwards.
+ let mut ske_ks =
+ kernel::crypto::Secret::<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>::zeroed();
+ let mut riv = [0u8; drm_hdcp::RIV_LEN];
+ rng::fill(&mut ske_ks[..]);
+ rng::fill(&mut riv);
+ let edkey = hdcp::compute_eks(&km, &rtx, &rrx, &rn, &ske_ks)?;
+ let ks = cp::cp_session_key(&ske_ks);
+ // SKE carries the full RIV. Control AES-CTR toggles byte 7 bit 2;
+ // Dl3Cmac separately transforms byte 0 bit 7.
+ let riv_ske = riv; // deliver the full random RIV before the control transform
+ riv[7] ^= 0x04; // OUT CP AES-CTR nonce = delivered ^0x04@byte7 (byte0 UNCHANGED)
+ // (6) SKE_Send_Eks (ctr=6).
+ link.ctrl_send(
+ &ake::ske_send_eks(hseq, 0, &edkey, &riv_ske)?,
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ hseq += 1;
+ // (12) RepeaterAuth -- verify V' over the ReceiverID_List, ACK, then SM2. Retained (empty
+ // on the non-repeater path) so `send_cp_setup`'s per-connector restatement can recompute a
+ // fresh per-connector `V = HMAC(kd_h, rxid_list)` over the same list the dock sent.
+ let mut rxid_list: KVec<u8> = KVec::new();
+ if repeater {
+ let (vid, list) = Self::recv_hdcp(link)?;
+ if vid != id::REPEATERAUTH_SEND_RECEIVERID_LIST || list.len() < 16 {
+ pr_err!("vino: AKE: bad ReceiverID_List (id={vid:#x})\n");
+ return Err(EINVAL);
+ }
+ let split = list.len() - 16;
+ // V' is the first 128 bits; RepeaterAuth_Send_Ack carries the second 128 bits.
+ let v_full = hdcp::compute_v_full(&kd, &list[..split]);
+ let mut v_ack = [0u8; 16];
+ v_ack.copy_from_slice(&v_full[16..]);
+ if v_full[..16] != list[split..] {
+ pr_err!("vino: AKE: V' mismatch -- repeater verification failed\n");
+ return Err(EINVAL);
+ }
+ vino_debug!("vino: AKE: V' verified\n");
+ rxid_list.extend_from_slice(&list[..split], GFP_KERNEL)?;
+ // (7) RepeaterAuth_Send_Ack (ctr=7).
+ link.ctrl_send(
+ &ake::repeater_auth_send_ack(hseq, 0, &v_ack)?,
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ // Preserve repeater-authentication request/reply lockstep.
+ Self::pace_cap_ack(link, hseq as u16, &mut saw_cap_complete);
+ hseq += 1;
+ // (8) RepeaterAuth_Stream_Manage (ctr=8).
+ link.ctrl_send(
+ &ake::repeater_auth_stream_manage(hseq, 0)?,
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ Self::pace_cap_ack(link, hseq as u16, &mut saw_cap_complete);
+ hseq += 1;
+ // Drain capability-complete and Stream_Ready before arming the control plane.
+ Self::wait_cap_complete(link, &kd, saw_cap_complete);
+ }
+
+ // `hseq` points past the last capability/AKE frame; `send_cp_setup` continues the inner
+ // counter from here for msg0.
+ if trace_crypto_enabled() {
+ pr_info!(
+ "vino-crypto: control key={:02x?} riv_out={riv:02x?} next_ctr={}\n",
+ &ks[..],
+ hseq
+ );
+ }
+ Ok(Session {
+ ks,
+ riv,
+ next_ctr: hseq as u16,
+ rsa,
+ rxid_list,
+ })
+ }
+
+ /// Submit one encrypted control-plane frame without changing protocol counters on failure. Name
+ /// a connector's content stream, on a dock where that is a control-plane record.
+ ///
+ /// A dock whose video shares the control pipe has no video endpoint to open a stream on later,
+ /// so every connector it may ever drive has to be named during setup -- including one whose
+ /// downstream authentication did not run, because a monitor plugged in afterwards comes up on
+ /// exactly that connector and would then be driven on a stream the dock was never told about.
+ /// The vendor names both connectors unconditionally. Docks with a video pipe of their own carry
+ /// this ahead of the first frame instead, so they get nothing here.
+ fn announce_stream(
+ link: &UsbLink<'_>,
+ out_q: &mut Option<usb::BulkOutQueue>,
+ profile: &DockProfile,
+ connector: usize,
+ ) -> Result {
+ if !profile.topology.video_on_ctrl_pipe {
+ return Ok(());
+ }
+ let stream_id = profile.geometry().stream_id(connector as u8);
+ Self::submit_cp_frame(
+ link,
+ out_q,
+ &cp::stream_announce(stream_id, cp::STREAM_ANNOUNCE_MARKER),
+ )
+ }
+
+ fn submit_cp_frame(
+ link: &UsbLink<'_>,
+ out_q: &mut Option<usb::BulkOutQueue>,
+ frame: &[u8],
+ ) -> Result {
+ match out_q {
+ // The queued path, which is the one both docks actually use. An error here is what
+ // surfaces as `control session failed after N attempts (ETIMEDOUT)`; the 40-retry NAK
+ // loop further down is the *unqueued* fallback and does not run, which is why
+ // instrumenting it said nothing.
+ Some(queue) => queue.send(link.io(), frame, timeout()).inspect_err(|e| {
+ pr_err!(
+ "vino: EP02 queued submit of {} B failed ({e:?})\n",
+ frame.len()
+ );
+ }),
+ None => link.ctrl_send(frame, timeout(), GFP_KERNEL).map(|_| ()),
+ }
+ }
+
+ /// Seal and send one live interactive control message.
+ ///
+ /// The OUT is one long-lived URB, then EP84 is drained once after its completion. The returned
+ /// tally distinguishes verified acknowledgments from rejected ciphertext.
+ ///
+ /// Do not implement a short-timeout retry here. A USB bulk timeout does not prove that no
+ /// bytes reached the device: Navarro accepted several complete 64-byte frames just before
+ /// Vino cancelled their 5 ms URBs. Re-submitting the same sealed frame consequently put
+ /// duplicate inner counters on the wire (measured at counters 72, 75 and 95), while DLM sends
+ /// each transaction once. Leaving one URB outstanding lets xHCI retry NRDY packets without
+ /// replaying an already accepted application message.
+ fn send_live_cp(
+ link: &UsbLink<'_>,
+ session: &Session,
+ mut q: Option<&mut usb::BulkInQueue>,
+ resp: &mut [u8],
+ edid_out: &mut Option<KVec<u8>>,
+ id: u16,
+ wire_seq: u32,
+ content: &[u8],
+ ) -> Result<Ep84Drain> {
+ let frame = cp::seal_interactive(&session.ks, &session.riv, id, wire_seq, content)?;
+
+ let mut tally = Ep84Drain::default();
+ link.ctrl_send(&frame, timeout(), GFP_KERNEL)?;
+ // Collect the dock's reply, including a possible get-EDID id=0x194 frame.
+ tally.add(Self::drain_ep84(
+ link,
+ q.as_deref_mut(),
+ resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ ));
+ Ok(tally)
+ }
+}
diff --git a/drivers/gpu/drm/vino/session/replies.rs b/drivers/gpu/drm/vino/session/replies.rs
new file mode 100644
index 000000000000..b86380d7327c
--- /dev/null
+++ b/drivers/gpu/drm/vino/session/replies.rs
@@ -0,0 +1,337 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Reading the dock's interrupt endpoint.
+//!
+//! The dock answers on EP84 and also pushes unsolicited messages there. A reply left undrained
+//! desynchronises the control plane, so the read paths here are what keep the session in lockstep
+//! rather than merely convenient.
+
+use super::*;
+
+impl VinoDriver {
+ /// Log one EP84 wire header and its decoded inner header when available.
+ fn log_ep84(session: &Session, frame: &[u8]) {
+ let len = frame.len();
+ let wtype = if len >= 8 {
+ u32::from_le_bytes([frame[4], frame[5], frame[6], frame[7]])
+ } else {
+ 0
+ };
+ let wsub = if len >= 10 {
+ u16::from_le_bytes([frame[8], frame[9]])
+ } else {
+ 0
+ };
+ let aux = if len >= 12 {
+ u16::from_le_bytes([frame[10], frame[11]])
+ } else {
+ 0
+ };
+ let wseq = if len >= 16 {
+ u32::from_le_bytes([frame[12], frame[13], frame[14], frame[15]])
+ } else {
+ 0
+ };
+ {
+ // Bound dynamic-debug output and split large frames below printk's line limit.
+ let cap = len.min(768);
+ if cap <= 64 {
+ let raw = &frame[..cap];
+ vino_debug!("vino: dock EP84 RAW {len}B {raw:02x?}\n");
+ } else {
+ vino_debug!("vino: dock EP84 RAW {len}B (first {cap} B in 128-B chunks):\n");
+ let mut o = 0usize;
+ while o < cap {
+ let e = (o + 128).min(cap);
+ let chunk = &frame[o..e];
+ vino_debug!("vino: ep84[{o:#06x}] {chunk:02x?}\n");
+ o = e;
+ }
+ }
+ }
+ match cp::decode_any(&session.ks, &session.riv, frame) {
+ Some((rivtag, rid, rsub, rictr, _)) => {
+ vino_debug!("vino: EP84 {rivtag} id={rid:#x} sub={rsub:#x} ctr={rictr:#x}\n");
+ }
+ None => vino_debug!(
+ "vino: EP84 type={wtype} sub={wsub:#x} aux={aux:#x} seq={wseq:#x} len={len}\n"
+ ),
+ }
+ }
+ /// Read one EP84 frame from the persistent queue or the synchronous fallback.
+ pub(super) fn read_ep84(
+ link: &UsbLink<'_>,
+ q: Option<&mut usb::BulkInQueue>,
+ buf: &mut [u8],
+ to: Delta,
+ ) -> Result<usize> {
+ match q {
+ Some(queue) => match queue.recv(link.io(), buf, to) {
+ Ok(Some(n)) => Ok(n),
+ Ok(None) => Err(ETIMEDOUT),
+ Err(e) => Err(e),
+ },
+ None => link.ctrl_recv(buf, to, GFP_KERNEL),
+ }
+ }
+ pub(super) fn drain_ep84(
+ link: &UsbLink<'_>,
+ mut q: Option<&mut usb::BulkInQueue>,
+ buf: &mut [u8],
+ session: &Session,
+ edid_out: &mut Option<KVec<u8>>,
+ // The first read may cover an HDCP computation interval; subsequent reads only drain a
+ // contiguous reply burst.
+ first_wait: Delta,
+ ) -> Ep84Drain {
+ const MAX_READS: usize = 16;
+ let mut out = Ep84Drain::default();
+ // Read EP84 before doing any unrelated work: the control plane is lockstep.
+ for i in 0..MAX_READS {
+ let wait = if i == 0 {
+ first_wait
+ } else {
+ Delta::from_millis(10)
+ };
+ match Self::read_ep84(link, q.as_deref_mut(), buf, wait) {
+ Ok(len) if len > 0 => {
+ out.reads += 1;
+ Self::log_ep84(session, &buf[..len]);
+ // Decode every downstream-HDCP milestone, not just Rrx. A generic encrypted
+ // envelope acknowledgment does not mean the downstream authentication has
+ // advanced; L', V' and M' are the state-machine gates.
+ if let Some(push) =
+ cp::per_connector_hdcp_push(&session.ks, &session.riv, &buf[..len])
+ {
+ out.observe_perhead(push);
+ }
+ if len >= 10 && u16::from_le_bytes([buf[8], buf[9]]) == 0x45 {
+ // The 0x45 wire tag is shared by status traffic. Only a valid decrypted
+ // inner header proves that this session's cipher is engaged.
+ match cp::verify_in_ack(&session.ks, &session.riv, &buf[..len]) {
+ Some((id, sub, ctr)) => {
+ out.acks += 1;
+ vino_debug!(
+ "vino: CP acknowledgment id={id:#x} sub={sub:#x} ctr={ctr}\n"
+ );
+ // A display-capability reply identifies a
+ // present monitor and echoes the request counter.
+ if cp::is_display_cap_reply(id, sub) {
+ out.display_cap_ctr = Some(ctr);
+ }
+ // Capture the first `id=0x194 sub=0x21` EDID
+ // reply for the standard DRM mode helpers.
+ if edid_out.is_none() {
+ if let Ok(Some(e)) = cp::parse_edid_from_reply(
+ &session.ks,
+ &session.riv,
+ &buf[..len],
+ ) {
+ vino_debug!(
+ "vino: EDID read from dock ({} bytes)\n",
+ e.len()
+ );
+ *edid_out = Some(e);
+ }
+ }
+ // Track the downstream-DDC readiness bit so
+ // the EDID loop can distinguish pending work.
+ if let Some(true) =
+ cp::edid_poll_ready(&session.ks, &session.riv, &buf[..len])
+ {
+ out.edid_ready = true;
+ }
+ }
+ None => {
+ match cp::decode_in_lenient(&session.ks, &session.riv, &buf[..len])
+ {
+ // A structurally valid header with an uncatalogued sub-id still
+ // proves possession of the session key.
+ Some((id, sub, ctr)) => {
+ out.acks += 1;
+ vino_debug!(
+ "vino: CP reply id={id:#x} sub={sub:#x} ctr={ctr}\n"
+ );
+ if sub == 0x000c {
+ if let Some(inner) = cp::inner_plaintext(
+ &session.ks,
+ &session.riv,
+ &buf[..len],
+ ) {
+ if let Some(line) = cp::dock_trace_line(&inner) {
+ vino_debug!(
+ "vino: dock: {}\n",
+ core::str::from_utf8(&line).unwrap_or("?")
+ );
+ }
+ }
+ }
+ }
+ // Not a sealed reply. Navarro pushes plaintext-framed
+ // messages the dock originates rather than answers, so a
+ // frame vino cannot open is only a rejection once that
+ // framing has been ruled out too.
+ None => match cp::inner_plaintext(
+ &session.ks,
+ &session.riv,
+ &buf[..len],
+ ) {
+ Some(inner) => {
+ out.acks += 1;
+ if let Some(line) = cp::dock_trace_line(&inner) {
+ vino_debug!(
+ "vino: dock: {}\n",
+ core::str::from_utf8(&line).unwrap_or("?")
+ );
+ }
+ }
+ None => {
+ out.rejects += 1;
+ pr_warn!("vino: invalid encrypted CP reply\n");
+ }
+ },
+ }
+ }
+ }
+ }
+ }
+ _ => break,
+ }
+ }
+ out
+ }
+ /// Read through the downstream-HDCP reply stream until one specific protocol milestone.
+ ///
+ /// Navarro acknowledges every encrypted envelope before it emits the corresponding HDCP
+ /// result. Advancing on the generic ACK alone allowed Vino to send SKE/V-ACK/Stream-Manage
+ /// without ever receiving L', V' or M'. The dock accepted the envelopes but never enabled
+ /// the video consumer. This routine makes the HDCP result, rather than the wrapper ACK, the
+ /// sequencing condition.
+ pub(super) fn wait_per_connector_push(
+ link: &UsbLink<'_>,
+ mut q: Option<&mut usb::BulkInQueue>,
+ buf: &mut [u8],
+ session: &Session,
+ want_msg_id: u8,
+ wait: Delta,
+ ) -> Ep84Drain {
+ const MAX_READS: usize = 16;
+ let mut out = Ep84Drain::default();
+ for _ in 0..MAX_READS {
+ match Self::read_ep84(link, q.as_deref_mut(), buf, wait) {
+ Ok(len) if len > 16 => {
+ out.reads += 1;
+ Self::log_ep84(session, &buf[..len]);
+ if let Some(push) =
+ cp::per_connector_hdcp_push(&session.ks, &session.riv, &buf[..len])
+ {
+ out.observe_perhead(push);
+ if out.saw_perhead(want_msg_id) {
+ return out;
+ }
+ continue;
+ }
+ if u16::from_le_bytes([buf[8], buf[9]]) != 0x45 {
+ continue;
+ }
+ match cp::verify_in_ack(&session.ks, &session.riv, &buf[..len]) {
+ Some((id, sub, ctr)) => {
+ out.acks += 1;
+ vino_debug!(
+ "vino: CP acknowledgment while waiting for HDCP {want_msg_id:#x}: id={id:#x} sub={sub:#x} ctr={ctr}\n"
+ );
+ }
+ None if cp::decode_in_lenient(&session.ks, &session.riv, &buf[..len])
+ .is_some() =>
+ {
+ out.acks += 1;
+ }
+ None => {
+ out.rejects += 1;
+ pr_warn!("vino: invalid encrypted CP reply\n");
+ }
+ }
+ }
+ Ok(_) => continue,
+ Err(_) => break,
+ }
+ }
+ out
+ }
+ /// Drain replies until a verified inner counter echoes the submitted request.
+ ///
+ /// Asynchronous pushes are processed while waiting, and the operation remains bounded.
+ pub(super) fn lockstep_reply(
+ link: &UsbLink<'_>,
+ mut q: Option<&mut usb::BulkInQueue>,
+ buf: &mut [u8],
+ session: &Session,
+ ictr: u16,
+ edid_out: &mut Option<KVec<u8>>,
+ ) -> Ep84Drain {
+ const MAX_READS: usize = 8;
+ let mut out = Ep84Drain::default();
+ for _ in 0..MAX_READS {
+ match Self::read_ep84(link, q.as_deref_mut(), buf, Delta::from_millis(30)) {
+ Ok(len) if len > 16 => {
+ out.reads += 1;
+ Self::log_ep84(session, &buf[..len]);
+ if u16::from_le_bytes([buf[8], buf[9]]) != 0x45 {
+ continue;
+ }
+ if let Some(push) =
+ cp::per_connector_hdcp_push(&session.ks, &session.riv, &buf[..len])
+ {
+ out.observe_perhead(push);
+ }
+ match cp::verify_in_ack(&session.ks, &session.riv, &buf[..len]) {
+ Some((id, sub, ctr)) => {
+ out.acks += 1;
+ let echo = if ctr == ictr {
+ " (echoes our ictr)"
+ } else {
+ ""
+ };
+ vino_debug!("vino: CP reply id={id:#x} sub={sub:#x} ctr={ctr}{echo}\n");
+ // Opportunistically extract an EDID from an id=0x194 reply.
+ if edid_out.is_none() {
+ if let Ok(Some(e)) = cp::parse_edid_from_reply(
+ &session.ks,
+ &session.riv,
+ &buf[..len],
+ ) {
+ vino_debug!("vino: EDID read from dock ({} bytes)\n", e.len());
+ *edid_out = Some(e);
+ }
+ }
+ // Stop early once the dock acknowledges the counter we sent.
+ if ctr == ictr {
+ break;
+ }
+ }
+ None => match cp::decode_in_lenient(&session.ks, &session.riv, &buf[..len])
+ {
+ // Decrypts to a plausible CP header, just an unlisted `sub` -- a valid
+ // ack (cipher engaged), not a rejection. See the drain_ep84 branch.
+ Some((id, sub, ctr)) => {
+ out.acks += 1;
+ vino_debug!("vino: CP reply id={id:#x} sub={sub:#x} ctr={ctr}\n");
+ }
+ None => {
+ out.rejects += 1;
+ pr_warn!("vino: invalid encrypted CP reply\n");
+ }
+ },
+ }
+ }
+ // A short, header-only frame (bare ack/keepalive, len <= 16): not a CP
+ // reply, but the dock is still talking -- keep reading for the 0x45 rather
+ // than dropping the rest of the lockstep window.
+ Ok(_) => continue,
+ // Read error / nothing queued within the window: the dock is idle, stop.
+ Err(_) => break,
+ }
+ }
+ out
+ }
+}
diff --git a/drivers/gpu/drm/vino/session/setup.rs b/drivers/gpu/drm/vino/session/setup.rs
new file mode 100644
index 000000000000..b278ebcf4b4e
--- /dev/null
+++ b/drivers/gpu/drm/vino/session/setup.rs
@@ -0,0 +1,1390 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The post-authentication setup burst.
+//!
+//! Once the AKE completes the dock still knows nothing about how it is to be driven. This is the
+//! sequence that tells it: the dock-wide records, the per-connector HDCP restatement, the video
+//! engine transition and the connector-selecting records. Its ordering is measured from the
+//! vendor, and a message sent out of turn desynchronises the authenticated counter for the rest of
+//! the session.
+
+use super::*;
+
+impl VinoDriver {
+ /// Configure the encrypted control plane after SKE.
+ ///
+ /// The sequence contains the plaintext arm marker, the first encrypted message, initialization,
+ /// per-connector authentication and stream finalization. The returned counters continue the
+ /// live session, and `video_keys` receives the key and nonce established for each connector.
+ pub(crate) fn send_cp_setup(
+ link: &UsbLink<'_>,
+ profile: &DockProfile,
+ session: &mut Session,
+ // Scratch slot filled by reply drains and moved into the selected connector's EDID cache.
+ edid_out: &mut Option<KVec<u8>>,
+ edid_connectors: &mut [Option<KVec<u8>>; Self::CP_SETUP_CONNECTORS],
+ video_keys: &mut [kernel::crypto::Secret<32>; Self::CP_SETUP_CONNECTORS],
+ connectors_present: &mut [bool; Self::CP_SETUP_CONNECTORS],
+ discovery_deferred: &mut [bool; Self::CP_SETUP_CONNECTORS],
+ // Which connectors had their video stream opened in this burst, as a connector bitmask. A
+ // connector with no sink yet is skipped here and owes its open to whatever drives it later.
+ stream_opened: &mut u32,
+ ) -> Result<(usize, u32, u16)> {
+ let connector_count =
+ usize::from(profile.topology.connectors).min(Self::CP_SETUP_CONNECTORS);
+ // 16 KiB so the dock's ~5787 B capability block is read whole (see [`EP84_BUF`]).
+ let mut resp = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL)?;
+ let mut drained = 0usize;
+ let mut acks = 0usize;
+ let mut rejects = 0usize;
+ let mut sent = 0usize;
+ // Match each display-capability response to the stream-open counter of its connector.
+ let mut stream_open_ctr: [Option<u16>; Self::CP_SETUP_CONNECTORS] =
+ [None; Self::CP_SETUP_CONNECTORS];
+
+ // Plaintext `type=2 sub=0x24`+`0x45` stream-open arm marker -- the mandatory gate
+ // before the first encrypted frame.
+ const STREAM_OPEN: [u8; 64] = [
+ 0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x00, 0x00, // pad, size, type
+ 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // wsub=0x24, aux=0, seq=0
+ 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // payload
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
+ 0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x00, 0x00, // pad, size, type
+ 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // wsub=0x45, aux=0, seq=0
+ 0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, // payload
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
+ ];
+
+ // Post the persistent EP84 reader before arming so asynchronous replies cannot fill the
+ // dock's IN FIFO while the host submits control traffic.
+ let ep84_depth = profile.protocol.ep84_queue_depth;
+ let mut ep84_q = match link.ctrl_in_queue(ep84_depth, EP84_BUF) {
+ Ok(q) => {
+ vino_debug!("vino: EP84 async IN queue opened (depth={ep84_depth})\n");
+ Some(q)
+ }
+ Err(e) => {
+ vino_debug!("vino: EP84 queue unavailable ({e:?}); using synchronous reads\n");
+ None
+ }
+ };
+
+ let mut out_q = match link.ctrl_out_queue(4, 1024) {
+ Ok(q) => {
+ vino_debug!("vino: EP02 async OUT queue opened (depth=4)\n");
+ Some(q)
+ }
+ Err(e) => {
+ vino_debug!("vino: EP02 async OUT queue open failed ({e:?})\n");
+ None
+ }
+ };
+
+ // Submit and flush the arm before sealing the first encrypted message.
+ let arm_res = match out_q.as_mut() {
+ Some(q) => q
+ .send(link.io(), &STREAM_OPEN, timeout())
+ .and_then(|()| q.flush(link.io(), timeout())),
+ None => link
+ .ctrl_send(&STREAM_OPEN, timeout(), GFP_KERNEL)
+ .map(|_| ()),
+ };
+ arm_res?;
+ // The first live message continues the AKE inner counter and starts the encrypted wire
+ // block counter at zero. Every following message advances both counters from its true size.
+ let mut cp_ctr: u16 = session.next_ctr;
+ let mut wseq: u32 = 0;
+
+ let content = cp::session_hello(cp_ctr);
+ let body_len = content.len() + 16; // AES-CTR ciphertext + 16-byte Dl3Cmac
+ let size = ((16 + body_len) - 4) as u16;
+ let aux = cp::aux_for_id(0x14, body_len);
+ let mut hdr = [0u8; 16];
+ hdr[2..4].copy_from_slice(&size.to_le_bytes());
+ hdr[4..8].copy_from_slice(&4u32.to_le_bytes()); // type=4
+ hdr[8..10].copy_from_slice(&0x24u16.to_le_bytes()); // sub=0x24 (interactive CP)
+ hdr[10..12].copy_from_slice(&aux.to_le_bytes());
+ // Running AES-CTR block index, initially zero.
+ hdr[12..16].copy_from_slice(&wseq.to_le_bytes());
+ let frame = cp::seal_livemac(&session.ks, &session.riv, &hdr, &content)?;
+
+ match out_q.as_mut() {
+ Some(q) => {
+ q.send(link.io(), &frame, timeout())?;
+ q.flush(link.io(), timeout())?;
+ if profile.protocol.reply_discipline == profile::ReplyDiscipline::Lockstep {
+ // DLM advances as soon as the dock authenticates msg0. The old eight-drain
+ // loop waited for seven empty 10-ms windows after that reply and moved every
+ // following setup transition roughly 90 ms later on the wire.
+ let d = Self::lockstep_reply(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ cp_ctr,
+ edid_out,
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ } else {
+ for _ in 0..8 {
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ }
+ }
+ }
+ None => {
+ // A NAK transfers no bytes, so cancel and retry are safe.
+ // Between attempts drain EP84 so the dock can push/drain its IN queue. Bounded.
+ const TRIES: usize = 40;
+ let mut last_err = ETIMEDOUT;
+ let mut accepted = false;
+ // Name what the dock is refusing. This path returns `control session failed ...
+ // (ETIMEDOUT)`, which is otherwise indistinguishable from a reply that never
+ // arrived; the distinction matters because this is the send side, where the dock
+ // has stopped taking EP02 writes at all.
+ let mut nak_reported = false;
+ for _ in 0..TRIES {
+ match link.ctrl_send(&frame, Delta::from_millis(5), GFP_KERNEL) {
+ Ok(_) => {
+ accepted = true;
+ break;
+ }
+ // OUT NAK'd (nothing transferred) -- let the dock push on EP84, then retry.
+ Err(e) => {
+ last_err = e;
+ if !nak_reported {
+ nak_reported = true;
+ vino_debug!(
+ "vino: EP02 NAKed a {} B control frame (cp_ctr={cp_ctr}, wseq={wseq}); retrying up to {TRIES}x\n",
+ frame.len()
+ );
+ }
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ }
+ }
+ }
+ if !accepted {
+ pr_err!(
+ "vino: EP02 refused {TRIES} submissions of a {} B frame (cp_ctr={cp_ctr}, wseq={wseq}) -- giving up\n",
+ frame.len()
+ );
+ return Err(last_err);
+ }
+ }
+ }
+ sent += 1;
+ cp_ctr += 1; // past msg0
+ wseq += 2; // msg0 content is 32 B = 2 AES blocks
+
+ // Initialization continues with two dock-wide records and one `0x16/0x2a` record for
+ // every physical connector. The authenticated Navarro transcript carries selectors
+ // 0,1,2,3 here; emitting Ridge's historical pair left all later counters four AES blocks
+ // behind DLM and never initialized Navarro's last two connector slots.
+ macro_rules! send_init {
+ ($id:expr, $sub:expr, $fixed_prefix:expr) => {{
+ let id: u16 = $id;
+ let sub: u16 = $sub;
+ let fixed_prefix: &[u8] = $fixed_prefix;
+ let mut c = [0u8; 32];
+ c[0..2].copy_from_slice(&id.to_le_bytes());
+ c[2..4].copy_from_slice(&sub.to_le_bytes());
+ c[4..6].copy_from_slice(&cp_ctr.to_le_bytes());
+ rng::fill(&mut c[22..32]);
+ c[22..22 + fixed_prefix.len()].copy_from_slice(fixed_prefix);
+ let frame = cp::seal_interactive(&session.ks, &session.riv, id, wseq, &c)?;
+ Self::submit_cp_frame(link, &mut out_q, &frame)?;
+ sent += 1;
+ // Navarro's reference transaction is reply-lockstep: the next operation follows
+ // the matching authenticated counter immediately. A generic burst drain adds an
+ // empty 10-ms read after every acknowledgment and changes the EP0/EP02 ordering.
+ let d = if profile.protocol.reply_discipline == profile::ReplyDiscipline::Lockstep {
+ Self::lockstep_reply(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ cp_ctr,
+ edid_out,
+ )
+ } else {
+ Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ )
+ };
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ cp_ctr += 1;
+ wseq += 2; // every initialization message is 32 B = 2 AES blocks
+ }};
+ }
+ // See `DockProfile::dock_wide_init`: a dock that does not expect these is left every
+ // later inner counter and AES block out of step by them.
+ if profile.protocol.dock_wide_init {
+ send_init!(0x0014, 0x0030, &[]);
+ send_init!(0x0015, 0x000b, &[0x01]);
+ }
+
+ if profile.protocol.video_commit_point == profile::VideoCommitPoint::BeforeConnectorRecords
+ {
+ // The working DLM transaction places the video-engine transition at this exact
+ // authenticated boundary: after the reply to 0x15/0x0b (counter 11), before the four
+ // connector-selecting 0x16/0x2a records (counters 12..15). Measured submit times are
+ // EP08 clear, +12.647 ms EP0a clear, +143 us vendor commit, +2.941 ms first 0x16/0x2a.
+ // Performing the same requests after finalization moved them 53 messages later.
+ link.clear_video_halt_wire(0)?;
+ fsleep(Delta::from_millis(13));
+ link.clear_video_halt_wire(1)?;
+ link.control_send(
+ 0x24,
+ 0x40, /* VENDOR_OUT */
+ 0,
+ 0,
+ &[],
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ let mut state2 = [0u8; 28];
+ link.control_recv(0x22, 0xc1, 1, 0, &mut state2, timeout(), GFP_KERNEL)?;
+ fsleep(Delta::from_millis(3));
+ }
+ if profile.protocol.dock_wide_init {
+ for connector in 0..connector_count {
+ let prefix = [connector as u8, 0x01];
+ send_init!(0x0016, 0x002a, &prefix);
+ }
+ }
+
+ // Drain pending replies before starting the per-connector authentication blocks. Each block
+ // mirrors the HDCP AKE layout and ends by opening that connector's stream.
+ {
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ if let Some(c) = d.display_cap_ctr {
+ for h in 0..Self::CP_SETUP_CONNECTORS {
+ if stream_open_ctr[h] == Some(c) {
+ connectors_present[h] = true;
+ }
+ }
+ }
+ }
+ // Which connectors completed their downstream authentication. A connector with nothing
+ // plugged into it never runs one, so this is not expected to be all of them.
+ let mut connector_ok = [false; Self::CP_SETUP_CONNECTORS];
+ let mut heads_authenticated = 0usize;
+ 'per_head: for connector in 0..connector_count {
+ // The socket number printed on the dock's case; the wire counts connectors from zero.
+ let socket = connector + 1;
+ // Derive an independent HDCP 2.2 authentication chain for this downstream connector.
+ let mut rtx_h = [0u8; drm_hdcp::RTX_LEN];
+ rng::fill(&mut rtx_h);
+ let mut km_h =
+ kernel::crypto::Secret::<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>::zeroed();
+ rng::fill(&mut km_h[..]);
+ let mut rn_h = [0u8; drm_hdcp::RN_LEN];
+ rng::fill(&mut rn_h);
+ let mut ske_ks_h =
+ kernel::crypto::Secret::<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>::zeroed();
+ rng::fill(&mut ske_ks_h[..]);
+ let mut riv_h = [0u8; drm_hdcp::RIV_LEN];
+ rng::fill(&mut riv_h);
+ let ekpub_h = hdcp::oaep_encrypt_km(&mut session.rsa, &km_h)?;
+ let mut kd_h: Option<kernel::crypto::Secret<32>> = None;
+ let mut edkey_h = None;
+ let mut v_h = None;
+ let mut fresh_rrx: Option<[u8; drm_hdcp::RRX_LEN]> = None;
+ let mut per_connector_repeater: Option<bool> = None;
+ let mut rrx_applied = false;
+ // SKE_Send_Eks establishes this connector's video key. Store the whitened key and the
+ // video nonce derived from the delivered RIV for the scanout arm burst. Layout: key(16)
+ // || nonce(8) || pad(8).
+ let stream_id = profile.geometry().stream_id(connector as u8);
+ video_keys[connector] = kernel::crypto::Secret::zeroed();
+ // Whether the dock applies the control-plane whitening constant to a per-connector SKE
+ // key is proven only for the link stream; the per-connector rule is carried over from
+ // Ridge, and both docks accept the sealed records it produces.
+ let video_key = cp::cp_session_key(&ske_ks_h);
+ video_keys[connector][..16].copy_from_slice(&video_key[..]);
+ let vnonce = cp::stream_content_nonce(&riv_h, stream_id);
+ video_keys[connector][16..24].copy_from_slice(&vnonce);
+ for (i, (id, sub, content_len)) in cp::CP_SETUP_PER_HEAD.iter().copied().enumerate() {
+ // The per-connector `rrx` arrives with the response to AKE_No_Stored_km. It is
+ // mandatory for deriving this connector's kd and Edkey before the consuming
+ // messages. V is not computed until the connector's own ReceiverID_List/V' has been
+ // received and verified.
+ if i >= 3 && !rrx_applied {
+ let Some(rrx_h) = fresh_rrx else {
+ // No `rrx` means this connector never began a downstream authentication,
+ // which is what an empty DisplayPort connector looks like -- DLM does not
+ // run a per-connector burst for a connector with no sink either, as a
+ // capture of it driving a monitorless dock shows: one AKE for the dock,
+ // none per connector.
+ //
+ // Skip the connector rather than failing the device. Aborting here took the
+ // whole dock down whenever a single connector was empty, so a two-connector
+ // dock with one monitor never came up at all, and a dock with none was
+ // unreachable even for EDID and hotplug.
+ vino_debug!(
+ "vino: socket {socket} has no downstream sink (no AKE_Send_Rrx); skipping its authentication\n"
+ );
+ Self::announce_stream(link, &mut out_q, profile, connector)?;
+ continue 'per_head;
+ };
+ let kd = hdcp::derive_kd(&km_h, &rtx_h, &rrx_h)?;
+ edkey_h = Some(hdcp::compute_eks(&km_h, &rtx_h, &rrx_h, &rn_h, &ske_ks_h)?);
+ // Ridge retains the older reply-drain path and has only the dock-wide list
+ // available here. Navarro replaces this after SKE with the verified list from
+ // this exact connector.
+ if !profile.protocol.per_connector_onehot {
+ let vf = hdcp::compute_v_full(&kd, &session.rxid_list);
+ let mut ack = [0u8; drm_hdcp::V_PRIME_HALF_LEN];
+ ack.copy_from_slice(&vf[drm_hdcp::V_PRIME_HALF_LEN..]);
+ v_h = Some(ack);
+ }
+ kd_h = Some(kd);
+ rrx_applied = true;
+ }
+ // id=0x26 (Stream_Manage restatement) is fully decoded -- deterministic content,
+ // not the generic path below. See `cp::stream_manage_restatement`'s doc comment.
+ if id == 0x0026 {
+ let content = cp::stream_manage_restatement(
+ cp_ctr,
+ connector as u8,
+ stream_id,
+ profile.protocol.per_connector_onehot,
+ )?;
+ let frame =
+ cp::seal_interactive(&session.ks, &session.riv, id, wseq, &content)?;
+ Self::submit_cp_frame(link, &mut out_q, &frame)?;
+ sent += 1;
+ let d = if profile.protocol.per_connector_onehot {
+ Self::wait_per_connector_push(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ ake::id::REPEATERAUTH_STREAM_READY,
+ Delta::from_millis(30),
+ )
+ } else {
+ Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ )
+ };
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ fresh_rrx = fresh_rrx.or(d.per_connector_rrx);
+ if profile.protocol.per_connector_onehot && d.per_connector_mprime.is_none() {
+ pr_err!(
+ "vino: socket {socket} downstream HDCP never returned M'/Stream_Ready\n"
+ );
+ return Err(EPROTO);
+ }
+ cp_ctr += 1;
+ wseq += ((content_len + 15) / 16) as u32;
+ // The vendor names the connector's content stream here, between the HDCP
+ // restatement and the stream-open control record. The position is part of the
+ // sequence: carried ahead of the first frame instead, this dock stops
+ // answering altogether.
+ Self::announce_stream(link, &mut out_q, profile, connector)?;
+ continue;
+ }
+ let mut c = KVec::from_elem(0u8, content_len, GFP_KERNEL)?;
+ // Shared header (id / sub=0x10 / inner counter), identical to the plaintext AKE
+ // body layout (`ake::body`). The buffer is already zeroed by `from_elem`.
+ c[0..2].copy_from_slice(&id.to_le_bytes());
+ c[2..4].copy_from_slice(&sub.to_le_bytes());
+ c[4..6].copy_from_slice(&cp_ctr.to_le_bytes());
+ // Per-connector AKE messages carry the platform-specific connector marker, HDCP
+ // message id at offset 27 and the standard HDCP payload at offset 28.
+ match i {
+ // AKE restatements: connector marker @23, HDCP msg-id tag @27, HDCP field @28..
+ 0 | 1 | 2 | 3 | 4 | 5 => {
+ cp::connector_marker(
+ &mut c,
+ connector as u8,
+ profile.protocol.per_connector_onehot,
+ );
+ c[27] = match i {
+ 0 => 0x02, // AKE_Init (rtx)
+ 1 => 0x13, // AKE_Transmitter_Info
+ 2 => 0x04, // AKE_No_Stored_km (Ekpub)
+ 3 => 0x09, // LC_Init (rn)
+ 4 => 0x0b, // SKE_Send_Eks (edkey+riv)
+ _ => 0x0f, // 5: RepeaterAuth_Send_Ack (V)
+ };
+ match i {
+ 0 => {
+ // AKE_Init carries Rtx and a fresh proprietary suffix.
+ c[28..36].copy_from_slice(&rtx_h);
+ rng::fill(&mut c[36..48]);
+ }
+ 1 => {
+ c[28..33].copy_from_slice(&[0x00, 0x06, 0x02, 0x00, 0x02]);
+ rng::fill(&mut c[33..48]);
+ }
+ 2 => {
+ c[28..156].copy_from_slice(&ekpub_h);
+ rng::fill(&mut c[156..160]);
+ }
+ 3 => {
+ c[28..36].copy_from_slice(&rn_h); // LC_Init: rn
+ rng::fill(&mut c[36..48]);
+ }
+ 4 => {
+ let Some(ed) = edkey_h.as_ref() else {
+ return Err(EPROTO);
+ };
+ c[28..44].copy_from_slice(ed);
+ c[44..52].copy_from_slice(&riv_h);
+ rng::fill(&mut c[52..64]);
+ }
+ _ => {
+ let Some(v) = v_h else {
+ return Err(EPROTO);
+ };
+ c[28..44].copy_from_slice(&v); // RepeaterAuth_Send_Ack: V
+ rng::fill(&mut c[44..48]);
+ }
+ }
+ }
+ // Stream-open control: header + zero[8..22] + 10 host-random bytes[22..32]; no
+ // connector marker, no tag (confirmed genuinely fully random across both
+ // connectors). Record this connector's request counter. The display-capability
+ // reply echoes it only when this connector has a monitor.
+ 7 => {
+ if connector < stream_open_ctr.len() {
+ stream_open_ctr[connector] = Some(cp_ctr);
+ }
+ rng::fill(&mut c[22..]);
+ }
+ // strm2: connector index @22, then the `<marker> [connector*4] 04` triple
+ // @24..27, then a fresh 5-byte host-random tail. The marker is a per-platform
+ // constant, not a connector count: Ridge sends 0x06 and Navarro 0x0c for two
+ // and four connectors, but DL-3x00 sends 0x10 with two.
+ 8 => {
+ c[22] = connector as u8;
+ c[24] = profile.protocol.strm2_marker;
+ c[25] = (connector as u8) * 4;
+ c[26] = 0x04;
+ rng::fill(&mut c[27..]);
+ }
+ _ => {}
+ }
+ let send_at = Instant::<Monotonic>::now();
+ let frame = cp::seal_interactive(&session.ks, &session.riv, id, wseq, &c)?;
+ Self::submit_cp_frame(link, &mut out_q, &frame)?;
+ sent += 1;
+ let mut d = if profile.protocol.per_connector_onehot && i <= 5 {
+ let want = match i {
+ 0 => ake::id::AKE_SEND_CERT,
+ 1 => 0x14, // DisplayLink AKE_Receiver_Info
+ 2 => ake::id::AKE_SEND_RRX,
+ 3 => ake::id::LC_SEND_L_PRIME,
+ 4 => ake::id::REPEATERAUTH_SEND_RECEIVERID_LIST,
+ _ => ake::id::RECEIVER_AUTH_STATUS,
+ };
+ Self::wait_per_connector_push(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ want,
+ Delta::from_millis(30),
+ )
+ } else if profile.protocol.per_connector_onehot {
+ Self::lockstep_reply(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ cp_ctr,
+ edid_out,
+ )
+ } else {
+ Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ )
+ };
+ per_connector_repeater = per_connector_repeater.or(d.per_connector_repeater);
+ fresh_rrx = fresh_rrx.or(d.per_connector_rrx);
+
+ // AKE_No_Stored_km starts the receiver's H' calculation. Rrx is the immediate
+ // result; H' and pairing info are later milestones, and DLM sends no LC_Init
+ // (i == 3) until both have crossed EP84. Both platforms need the wait and the
+ // drain, or the connector authenticates on material that never arrived.
+ if i == 2 && !profile.protocol.per_connector_onehot {
+ hold_until(send_at, HDCP_HPRIME_WAIT_US);
+ // Wait for the rrx rather than sampling for it. A fixed drain window returns
+ // whatever has already arrived, so a connector whose receiver answers a little
+ // late reads as a connector with no sink and loses the whole rest of its burst
+ // -- including the key exchange that gives the dock a key for its content
+ // stream. How long a receiver may take to answer is a property of the dock,
+ // so the bound comes from its profile.
+ let dh = Self::wait_per_connector_push(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ ake::id::AKE_SEND_RRX,
+ Delta::from_millis(profile.protocol.perhead_rrx_wait_ms),
+ );
+ fresh_rrx = fresh_rrx.or(dh.per_connector_rrx);
+ per_connector_repeater = per_connector_repeater.or(dh.per_connector_repeater);
+ d.add(dh);
+ }
+ if i == 2 && profile.protocol.per_connector_onehot {
+ let Some(rrx_h) = fresh_rrx else {
+ cp_ctr += 1;
+ wseq += ((content_len + 15) / 16) as u32;
+ vino_debug!(
+ "vino: socket {socket} has no downstream sink (no AKE_Send_Rrx); skipping its authentication\n"
+ );
+ Self::announce_stream(link, &mut out_q, profile, connector)?;
+ continue 'per_head;
+ };
+ hold_until(send_at, HDCP_HPRIME_WAIT_US);
+ let dh = Self::wait_per_connector_push(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ ake::id::AKE_SEND_H_PRIME,
+ Delta::from_millis(50),
+ );
+ d.add(dh);
+ let dp = Self::wait_per_connector_push(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ 0x08, // AKE_Send_Pairing_Info
+ Delta::from_millis(30),
+ );
+ d.add(dp);
+ let Some(hprime) = d.per_connector_hprime else {
+ pr_err!(
+ "vino: socket {socket} downstream HDCP returned no H'\n",
+ socket = connector + 1
+ );
+ return Err(EPROTO);
+ };
+ let kd = hdcp::derive_kd(&km_h, &rtx_h, &rrx_h)?;
+ let want_h =
+ hdcp::compute_h(&kd, &rtx_h, per_connector_repeater.unwrap_or(true));
+ if want_h != hprime {
+ pr_err!(
+ "vino: socket {socket} downstream H' mismatch\n",
+ socket = connector + 1
+ );
+ return Err(EPROTO);
+ }
+ edkey_h = Some(hdcp::compute_eks(&km_h, &rtx_h, &rrx_h, &rn_h, &ske_ks_h)?);
+ kd_h = Some(kd);
+ rrx_applied = true;
+ vino_debug!(
+ "vino: socket {socket} downstream H' verified\n",
+ socket = connector + 1
+ );
+ }
+
+ if i == 3 && profile.protocol.per_connector_onehot {
+ let Some(lprime) = d.per_connector_lprime else {
+ pr_err!(
+ "vino: socket {socket} downstream HDCP returned no L'\n",
+ socket = connector + 1
+ );
+ return Err(EPROTO);
+ };
+ let (Some(kd), Some(rrx_h)) = (kd_h.as_ref(), fresh_rrx.as_ref()) else {
+ return Err(EPROTO);
+ };
+ if hdcp::compute_l(kd, rrx_h, &rn_h) != lprime {
+ pr_err!(
+ "vino: socket {socket} downstream L' mismatch\n",
+ socket = connector + 1
+ );
+ return Err(EPROTO);
+ }
+ vino_debug!(
+ "vino: socket {socket} downstream L' verified\n",
+ socket = connector + 1
+ );
+ }
+
+ if i == 4 && profile.protocol.per_connector_onehot {
+ let Some((list_header, vprime)) = d.per_connector_v else {
+ pr_err!(
+ "vino: socket {socket} downstream HDCP returned no ReceiverID_List/V'\n"
+ );
+ return Err(EPROTO);
+ };
+ let Some(kd) = kd_h.as_ref() else {
+ return Err(EPROTO);
+ };
+ let vf = hdcp::compute_v_full(kd, &list_header);
+ if vf[..drm_hdcp::V_PRIME_HALF_LEN] != vprime {
+ pr_err!(
+ "vino: socket {socket} downstream V' mismatch\n",
+ socket = connector + 1
+ );
+ return Err(EPROTO);
+ }
+ let mut ack = [0u8; drm_hdcp::V_PRIME_HALF_LEN];
+ ack.copy_from_slice(&vf[drm_hdcp::V_PRIME_HALF_LEN..]);
+ v_h = Some(ack);
+ if session.rxid_list.as_slice() != list_header {
+ vino_debug!(
+ "vino: socket {socket} ReceiverID list differs from dock-wide list\n",
+ socket = connector + 1
+ );
+ }
+ vino_debug!(
+ "vino: socket {socket} downstream V' verified\n",
+ socket = connector + 1
+ );
+ }
+
+ if i == 5 && profile.protocol.per_connector_onehot {
+ let Some(status) = d.per_connector_auth_status else {
+ pr_err!(
+ "vino: socket {socket} downstream HDCP returned no receiver-auth status\n"
+ );
+ return Err(EPROTO);
+ };
+ if status != 0x04 {
+ pr_err!(
+ "vino: socket {socket} downstream receiver-auth status {status:#x}, expected 0x04\n"
+ );
+ return Err(EPROTO);
+ }
+ }
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ // Attribute a display-capability reply by its echoed stream-open counter.
+ if let Some(c) = d.display_cap_ctr {
+ for h in 0..Self::CP_SETUP_CONNECTORS {
+ if stream_open_ctr[h] == Some(c) {
+ connectors_present[h] = true;
+ }
+ }
+ }
+ cp_ctr += 1;
+ wseq += ((content_len + 15) / 16) as u32;
+ }
+ // Collect replies before moving to the next connector without adding another phase
+ // delay.
+ {
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ if let Some(c) = d.display_cap_ctr {
+ for h in 0..Self::CP_SETUP_CONNECTORS {
+ if stream_open_ctr[h] == Some(c) {
+ connectors_present[h] = true;
+ }
+ }
+ }
+ }
+
+ if trace_crypto_enabled() {
+ pr_info!(
+ "vino-crypto: video connector={connector} raw_key={:02x?} delivered_riv={riv_h:02x?} key={:02x?} nonce={:02x?}\n",
+ &ske_ks_h[..],
+ &video_keys[connector][..16],
+ &video_keys[connector][16..24]
+ );
+ }
+ connector_ok[connector] = true;
+ heads_authenticated += 1;
+ }
+ vino_debug!(
+ "vino: {heads_authenticated}/{} connector(s) authenticated\n",
+ connector_count
+ );
+
+ // Navarro performs one dock-wide state transition after the last per-connector AKE and
+ // before any connector finalizer. This message was absent from vino even though its
+ // authenticated DLM reply reports state 2. Keep it Navarro-only until a Ridge transcript
+ // establishes that platform's behavior.
+ if profile.protocol.per_connector_onehot {
+ let state = cp::post_auth_state_req(cp_ctr)?;
+ let frame = cp::seal_interactive(&session.ks, &session.riv, 0x15, wseq, &state)?;
+ Self::submit_cp_frame(link, &mut out_q, &frame)?;
+ sent += 1;
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ cp_ctr += 1;
+ wseq += ((state.len() + 15) / 16) as u32;
+ }
+
+ // Finalize the streams of the connectors that authenticated, before entering the
+ // steady-state heartbeat.
+ //
+ // Only those connectors: finalizing a stream whose downstream authentication never ran
+ // makes the dock hard-reset a few seconds later and re-enumerate, which reads as a
+ // spontaneous dock reset rather than as a message it refused. The sequence is per
+ // connector, so it follows the dock's connector count rather than a fixed pair: a
+ // four-connector dock finalizes four, and DLM does the same. A dock that shares its control
+ // pipe is never finalized: DLM sends neither `0x16/0x4c` nor `0x15/0x4a` to it in a whole
+ // session, and every message it has not been asked to handle is one more it must answer on
+ // the pipe it is about to take pixels on.
+ let finalize = (0..connector_count)
+ .filter(|_| !profile.topology.video_on_ctrl_pipe)
+ .flat_map(|c| {
+ cp::CP_SETUP_FINALIZE_STEPS
+ .iter()
+ .map(move |&(id, sub)| (id, sub, c as u8))
+ });
+ for (id, sub, off22) in finalize {
+ if (off22 as usize) < Self::CP_SETUP_CONNECTORS && !connector_ok[off22 as usize] {
+ continue;
+ }
+ // Offset 22 selects the connector or step; sub 0x4c also carries 1 at offset 23.
+ let mut c = [0u8; 32];
+ c[0..2].copy_from_slice(&id.to_le_bytes());
+ c[2..4].copy_from_slice(&sub.to_le_bytes());
+ c[4..6].copy_from_slice(&cp_ctr.to_le_bytes());
+ c[22] = off22;
+ if sub == 0x004c {
+ c[23] = 0x01;
+ rng::fill(&mut c[24..]);
+ } else {
+ rng::fill(&mut c[23..]);
+ }
+ let frame = cp::seal_interactive(&session.ks, &session.riv, id, wseq, &c)?;
+ Self::submit_cp_frame(link, &mut out_q, &frame)?;
+ sent += 1;
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ cp_ctr += 1;
+ wseq += 2; // 32 B content = 2 AES blocks
+ }
+ if let Some(queue) = out_q.as_mut() {
+ // A flush timeout is not fatal: `reap()` leaves a slow slot posted for a later call,
+ // and the session is otherwise complete. A dock with an empty connector never drains
+ // that connector's last EP02 write, which would otherwise fail the whole setup.
+ if let Err(e) = queue.flush(link.io(), timeout()) {
+ vino_debug!(
+ "vino: EP02 queue flush timed out ({e:?}); session already acknowledged, continuing\n"
+ );
+ }
+ }
+
+ // Ridge commits after finalization. Navarro has already committed at its measured
+ // post-0x15/pre-0x16 boundary above.
+ if profile.protocol.video_commit_point == profile::VideoCommitPoint::AfterFinalize {
+ link.control_send(
+ 0x24,
+ 0x40, /* VENDOR_OUT */
+ 0,
+ 0,
+ &[],
+ timeout(),
+ GFP_KERNEL,
+ )?;
+ // Refresh the interface state after the render/commit request.
+ let mut state2 = [0u8; 28];
+ link.control_recv(0x22, 0xc1, 1, 0, &mut state2, timeout(), GFP_KERNEL)?;
+ }
+
+ // Read the dock's reply: a VERIFIED `wsub=0x45` ack means the cipher engaged on our frame.
+ let ls = Self::lockstep_reply(link, ep84_q.as_mut(), &mut resp, session, 0x08, edid_out);
+ drained += ls.reads;
+ acks += ls.acks;
+ rejects += ls.rejects;
+
+ const MAX_ROUNDS: usize = 16;
+ for _ in 0..MAX_ROUNDS {
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ if d.reads == 0 {
+ break;
+ }
+ }
+
+ if acks == 0 {
+ pr_err!(
+ "vino: encrypted session not acknowledged (reads={drained}, rejects={rejects})\n"
+ );
+ return Err(EPROTO);
+ }
+
+ // Complete downstream discovery on the authenticated counter stream.
+ if profile.protocol.per_connector_onehot {
+ // Navarro's authenticated DLM transcript is a compact, ordered transaction. Keep it
+ // separate from Ridge's older retry-heavy discovery below: hundreds of extra status
+ // polls delayed Navarro's first mode by ~23 seconds and changed every live counter.
+ macro_rules! navarro_send {
+ ($id:expr, $body:expr) => {{
+ let id: u16 = $id;
+ let body = $body;
+ let e = Self::send_live_cp(
+ link,
+ session,
+ ep84_q.as_mut(),
+ &mut resp,
+ edid_out,
+ id,
+ wseq,
+ &body,
+ )?;
+ drained += e.reads;
+ acks += e.acks;
+ rejects += e.rejects;
+ sent += 1;
+ wseq = wseq.wrapping_add(((body.len() + 15) / 16) as u32);
+ cp_ctr += 1;
+ }};
+ }
+
+ // DLM first seeks every physical connector, including empty sockets.
+ for connector in 0..connector_count {
+ let probe = cp::get_edid_req_sub(cp_ctr, 0x20, connector as u8)?;
+ navarro_send!(0x15, probe);
+ }
+ let devq = cp::device_query_req(cp_ctr, 0x0000)?;
+ navarro_send!(0x14, devq);
+
+ let mut active = [false; Self::CP_SETUP_CONNECTORS];
+ for connector in 0..connector_count {
+ // Navarro exposes four connector numbers on two physical video pipes, and the
+ // reference DLM session opened only the first connector on each pipe -- but that
+ // session had its two panels in sockets 1 and 2. Sockets 3 and 4 are not aliases
+ // of them: they are separate physical connectors that happen to share a bulk
+ // endpoint, with their own selector, EDID and stream. Skipping them by endpoint
+ // made a monitor in socket 3 or 4 invisible, with the dock never even probed for
+ // it. Read every socket; engagement below is what stays selective.
+ active[connector] = true;
+ *edid_out = None;
+ let hu8 = connector as u8;
+ let kick = cp::edid_readiness_kick(cp_ctr, hu8)?;
+ navarro_send!(0x16, kick);
+ let probe = cp::get_edid_req_sub(cp_ctr, 0x20, hu8)?;
+ navarro_send!(0x15, probe);
+ let fetch = cp::get_edid_req(cp_ctr, hu8)?;
+ navarro_send!(0x15, fetch);
+ // The fetch drain carries the asynchronous EDID in the working DLM cadence.
+ edid_connectors[connector] = edid_out.take();
+ discovery_deferred[connector] = edid_connectors[connector].is_none();
+ }
+ // Engage each connector that answered with an EDID, once, after all kick/probe/fetch
+ // triplets. Gated on a recovered EDID because that is the presence signal on this
+ // platform, and because driving setup at a socket with nothing in it is exactly what
+ // makes this dock hard-reset a few seconds later.
+ for connector in 0..connector_count {
+ if active[connector] && edid_connectors[connector].is_some() {
+ let engage = cp::edid_engage_req(cp_ctr, connector as u8)?;
+ navarro_send!(0x16, engage);
+ }
+ }
+
+ // Two status samples bracket the one-shot RTC synchronization, followed by thirteen
+ // more samples in the same-day working capture.
+ for _ in 0..2 {
+ let status = cp::device_query_req(cp_ctr, 0x000c)?;
+ navarro_send!(0x14, status);
+ }
+ let now = kernel::time::ktime_get_real_seconds();
+ let rtc = cp::rtc_sync_req(
+ cp_ctr,
+ now,
+ *crate::module_parameters::rtc_utc_offset_minutes.value(),
+ )?;
+ navarro_send!(0x1e, rtc);
+ for _ in 0..13 {
+ let status = cp::device_query_req(cp_ctr, 0x000c)?;
+ navarro_send!(0x14, status);
+ }
+ // The last discovery records immediately preceding DLM's clear-mode pair re-seek
+ // each active connector once.
+ for connector in 0..connector_count {
+ if active[connector] {
+ let probe = cp::get_edid_req_sub(cp_ctr, 0x20, connector as u8)?;
+ navarro_send!(0x15, probe);
+ }
+ }
+ } else if profile.topology.video_on_ctrl_pipe {
+ // Discovery on a dock that shares its control pipe walks all its connectors through
+ // each phase before starting the next, rather than taking one connector end to end.
+ // Running a connector to completion first leaves the connector that went first without
+ // an EDID while the second streams, which is what a single-monitor dock looks like from
+ // userspace.
+ //
+ // It is also short: no reader kick, one engage rather than two, and no readiness
+ // polling. Every extra transaction here is one the dock has to answer on the pipe it
+ // will shortly be asked to take pixels on.
+ macro_rules! walk_send {
+ ($id:expr, $body:expr) => {{
+ let id: u16 = $id;
+ let body = $body;
+ let e = Self::send_live_cp(
+ link,
+ session,
+ ep84_q.as_mut(),
+ &mut resp,
+ edid_out,
+ id,
+ wseq,
+ &body,
+ )?;
+ drained += e.reads;
+ acks += e.acks;
+ rejects += e.rejects;
+ sent += 1;
+ wseq = wseq.wrapping_add(((body.len() + 15) / 16) as u32);
+ cp_ctr += 1;
+ }};
+ }
+
+ for connector in 0..connector_count {
+ let hu8 = connector as u8;
+ *edid_out = None;
+ let probe = cp::get_edid_req_sub(cp_ctr, 0x20, hu8)?;
+ walk_send!(0x15, probe);
+ let fetch = cp::get_edid_req(cp_ctr, hu8)?;
+ walk_send!(0x15, fetch);
+ // The EDID arrives asynchronously, a few messages behind the fetch it answers, and
+ // the push names no connector -- so a connector's answer has to be collected before
+ // the next connector is asked, even though DLM issues both fetches back to back.
+ let waited = Instant::<Monotonic>::now();
+ while edid_out.is_none() && Instant::<Monotonic>::now() - waited < EDID_REPLY_WAIT {
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(20),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ }
+ edid_connectors[connector] = edid_out.take();
+ vino_debug!(
+ "vino: socket {socket} EDID fetch {}\n",
+ if edid_connectors[connector].is_some() {
+ "succeeded"
+ } else {
+ "returned no EDID"
+ },
+ socket = connector + 1
+ );
+ }
+ // Both probes are answered before either sink is engaged, and the last message before
+ // the engages restates the session hello.
+ let hello = cp::session_hello(cp_ctr);
+ walk_send!(0x14, hello);
+ // Every connector, not only the ones that answered with an EDID. This dock reports no
+ // EDID for a sink it is nonetheless driving, so an EDID is evidence of presence and
+ // its absence is evidence of nothing; the vendor engages both connectors before any
+ // pixels and leaves an unoccupied one engaged for the life of the session.
+ for connector in 0..connector_count {
+ let engage = cp::edid_engage_req(cp_ctr, connector as u8)?;
+ walk_send!(0x16, engage);
+ }
+
+ // Open every connector's sealed video stream, and announce its video plane.
+ //
+ // This is where DLM does it: after the sinks are engaged, before the capability
+ // queries, and long before any mode is set. The sealed record takes block zero of the
+ // stream so the decoder configuration that follows it at prologue time takes block
+ // one; `set_video_keys` is told as much.
+ //
+ // Every connector, not only the ones that answered with an EDID: a monitor that arrives
+ // after setup would otherwise be driven on a stream the dock was never told about, and
+ // this dock has no video pipe to open one on later. The vendor opens both.
+ //
+ // A dock with a video pipe of its own is opened by the scanout path instead,
+ // immediately ahead of the frame that needs it -- it has a pipe to do that on.
+ // The vendor spaces the stages of this burst with status polls; see `SetupPolls`.
+ // They are round trips, so a stage that follows three of them is a stage the dock was
+ // given three acknowledged messages' worth of time to reach. Opening both streams back
+ // to back puts the same records on the wire and gives it none of that.
+ macro_rules! settle_polls {
+ ($n:expr) => {{
+ for _ in 0..$n {
+ let status = cp::device_query_req(cp_ctr, 0x000c)?;
+ walk_send!(0x14, status);
+ }
+ }};
+ }
+
+ for connector in 0..connector_count {
+ settle_polls!(profile.protocol.setup_polls.before_open(connector as u8));
+ let stream_id = profile.geometry().stream_id(connector as u8);
+ let mut vkey = kernel::crypto::Secret::<16>::zeroed();
+ vkey.copy_from_slice(&video_keys[connector][..16]);
+ let mut vnonce = [0u8; 8];
+ vnonce.copy_from_slice(&video_keys[connector][16..24]);
+ let content = cp::stream_open(profile.protocol.stream_marker_kind);
+ let open = cp::seal_video_arm(&vkey, &vnonce, stream_id, 0x000a, 0, &content)?;
+ link.ctrl_send(&open, timeout(), GFP_KERNEL)?;
+ let announce = cp::stream_announce(
+ u16::from(profile.geometry().connector_selector(connector as u8)),
+ 0,
+ );
+ link.ctrl_send(&announce, timeout(), GFP_KERNEL)?;
+ sent += 2;
+ *stream_opened |= 1u32 << connector;
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ }
+
+ settle_polls!(profile.protocol.setup_polls.after_stream_opens);
+
+ // Every connector again, for the same reason the engages are.
+ for connector in 0..connector_count {
+ let query = cp::post_edid_query(cp_ctr, connector as u8)?;
+ walk_send!(0x15, query);
+ }
+ } else {
+ // Open discovery with a heartbeat and the one-shot device-capability query.
+ let hb = cp::heartbeat(cp_ctr)?;
+ let e = Self::send_live_cp(
+ link,
+ session,
+ ep84_q.as_mut(),
+ &mut resp,
+ edid_out,
+ 0x16,
+ wseq,
+ &hb,
+ )?;
+ drained += e.reads;
+ acks += e.acks;
+ rejects += e.rejects;
+ wseq = wseq.wrapping_add(((hb.len() + 15) / 16) as u32);
+ cp_ctr += 1;
+
+ let devq = cp::device_query_req(cp_ctr, 0x0000)?;
+ let e = Self::send_live_cp(
+ link,
+ session,
+ ep84_q.as_mut(),
+ &mut resp,
+ edid_out,
+ 0x14,
+ wseq,
+ &devq,
+ )?;
+ drained += e.reads;
+ acks += e.acks;
+ rejects += e.rejects;
+ wseq = wseq.wrapping_add(((devq.len() + 15) / 16) as u32);
+ cp_ctr += 1;
+
+ // EDID discovery is a probe/kick/fetch sequence followed by two engage messages. A
+ // cold receiver then needs bounded status polling until the readiness bit is set.
+ const EDID_STEP_DELAY: Delta = Delta::from_millis(100);
+ const EDID_EARLY_ROUNDS: usize = 1;
+ // Bound both the poll count and wall-clock duration.
+ const EDID_POLL_ITERS: usize = 250;
+ const EDID_POLL_DELAY: Delta = Delta::from_millis(20);
+ const EDID_POLL_PROBE_EVERY: usize = 8;
+ // Offset 22 selects the downstream connector. A connector the display-capability
+ // transaction reported empty need not be probed, but that inference only holds once
+ // some connector has reported one: a dock that pushed no capability at all has said
+ // nothing about any socket, and treating silence as "empty" leaves every connector past
+ // the first undiscovered.
+ let capabilities_reported = connectors_present.iter().any(|&present| present);
+ for connector in 0..connector_count {
+ if connector != 0 && capabilities_reported && !connectors_present[connector] {
+ continue;
+ }
+ let hu8 = connector as u8;
+ let socket = connector + 1;
+ *edid_out = None;
+ let mut edid_ready = false;
+ let mut transport_error = None;
+ 'discovery: {
+ macro_rules! edid_send {
+ ($ep:expr, $body:expr, $tag:expr) => {{
+ match Self::send_live_cp(
+ link,
+ session,
+ ep84_q.as_mut(),
+ &mut resp,
+ edid_out,
+ $ep,
+ wseq,
+ &$body,
+ ) {
+ Ok(e) => {
+ drained += e.reads;
+ acks += e.acks;
+ rejects += e.rejects;
+ wseq = wseq.wrapping_add((($body.len() + 15) / 16) as u32);
+ cp_ctr += 1;
+ edid_ready |= e.edid_ready;
+ vino_debug!(
+ "vino: live connector {} {} sent\n",
+ connector,
+ $tag
+ );
+ }
+ Err(e) => {
+ transport_error = Some(e);
+ break 'discovery;
+ }
+ }
+ }};
+ }
+ // A block that arrives before the dock reports its downstream read complete
+ // describes the dock's own bridge rather than the monitor, and publishing it
+ // drives the panel at a timing it never advertised. Offset 26 bit 7 of the
+ // presence reply is that report; drop anything offered ahead of it and let the
+ // engage below produce the real one.
+ let gate_on_ready = profile.quirks.edid_ready_reported;
+ macro_rules! edid_settled {
+ () => {{
+ if gate_on_ready && !edid_ready && edid_out.is_some() {
+ vino_debug!(
+ "vino: socket {socket} discarding an EDID offered before the \
+ downstream read completed\n"
+ );
+ *edid_out = None;
+ }
+ edid_out.is_some()
+ }};
+ }
+ 'early: for cycle in 0..EDID_EARLY_ROUNDS {
+ if edid_settled!() {
+ break;
+ }
+ vino_debug!(
+ "vino: live get-EDID socket {socket} early round {cycle}\n",
+ socket = connector + 1
+ );
+ for _ in 0..2 {
+ let probe = cp::get_edid_req_sub(cp_ctr, 0x20, hu8)?;
+ edid_send!(0x15, probe, "get-EDID probe (id=0x15 sub=0x20)");
+ fsleep(EDID_STEP_DELAY);
+ }
+ // Start or continue the selected connector's downstream DDC read.
+ let kick = cp::edid_readiness_kick(cp_ctr, hu8)?;
+ edid_send!(0x16, kick, "get-EDID kick (id=0x16 sub=0x4b)");
+ fsleep(EDID_STEP_DELAY);
+ let req = cp::get_edid_req(cp_ctr, hu8)?;
+ edid_send!(0x15, req, "get-EDID fetch (id=0x15 sub=0x21)");
+ if edid_settled!() {
+ break 'early;
+ }
+ fsleep(EDID_STEP_DELAY);
+ // EDID arrives asynchronously after the fetch acknowledgment.
+ let reply_wait = Instant::<Monotonic>::now();
+ while Instant::<Monotonic>::now() - reply_wait < Delta::from_secs(2) {
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(20),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ edid_ready |= d.edid_ready;
+ if edid_settled!() {
+ break 'early;
+ }
+ }
+ }
+ // Engage is required twice even if the EDID push arrived early.
+ for _ in 0..2 {
+ let engage = cp::edid_engage_req(cp_ctr, hu8)?;
+ edid_send!(0x16, engage, "get-EDID engage (id=0x16 sub=0x0023)");
+ fsleep(EDID_STEP_DELAY);
+ }
+ if edid_out.is_none() {
+ // Bound wall-clock time independently of the iteration
+ // count because each failed send has its own USB timeout.
+ const EDID_POLL_MAX: Delta = Delta::from_secs(6);
+ let poll_start = Instant::<Monotonic>::now();
+ 'poll: for i in 0..EDID_POLL_ITERS {
+ if edid_out.is_some() || edid_ready {
+ break 'poll;
+ }
+ if Instant::<Monotonic>::now() - poll_start > EDID_POLL_MAX {
+ vino_debug!(
+ "vino: get-EDID socket {socket} readiness poll hit wall-clock cap\n"
+ );
+ break 'poll;
+ }
+ let status = cp::device_query_req(cp_ctr, 0x000c)?;
+ edid_send!(0x14, status, "device-status poll (id=0x14 sub=0x000c)");
+ if i % EDID_POLL_PROBE_EVERY == EDID_POLL_PROBE_EVERY - 1 {
+ let probe = cp::get_edid_req_sub(cp_ctr, 0x20, hu8)?;
+ edid_send!(
+ 0x15,
+ probe,
+ "get-EDID readiness probe (id=0x15 sub=0x20)"
+ );
+ }
+ if edid_out.is_some() || edid_ready {
+ break 'poll;
+ }
+ fsleep(EDID_POLL_DELAY);
+ }
+ vino_debug!(
+ "vino: get-EDID socket {socket} readiness poll finished (ready={edid_ready})\n"
+ );
+ // The asynchronous `id=0x194` EDID can follow the fetch
+ // acknowledgment by several messages.
+ for _ in 0..24 {
+ if edid_settled!() {
+ break;
+ }
+ let req = cp::get_edid_req(cp_ctr, hu8)?;
+ edid_send!(0x15, req, "get-EDID retry (id=0x15 sub=0x21)");
+ let d = Self::drain_ep84(
+ link,
+ ep84_q.as_mut(),
+ &mut resp,
+ session,
+ edid_out,
+ Delta::from_millis(10),
+ );
+ drained += d.reads;
+ acks += d.acks;
+ rejects += d.rejects;
+ fsleep(EDID_POLL_DELAY);
+ }
+ }
+ // Complete this connector with its post-EDID capability query.
+ let query = cp::post_edid_query(cp_ctr, hu8)?;
+ edid_send!(0x15, query, "post-EDID capability query (id=0x15 sub=0x53)");
+ // `edid_send!` folds the drain's readiness bit into `edid_ready`. This is the
+ // last statement of the per-connector iteration and the next connector
+ // re-derives it, so that update is deliberately not read again here.
+ let _ = edid_ready;
+ }
+
+ discovery_deferred[connector] = transport_error.is_some();
+ if let Some(e) = transport_error {
+ // The encrypted session is already authenticated. Keep it running and recover
+ // this connector independently after a connector-local discovery timeout.
+ *edid_out = None;
+ pr_warn!(
+ "vino: socket {socket} discovery timed out ({e:?}); deferring to runtime \
+ recovery\n",
+ socket = connector + 1
+ );
+ }
+ edid_connectors[connector] = edid_out.take();
+ vino_debug!(
+ "vino: socket {socket} EDID fetch {}\n",
+ if edid_connectors[connector].is_some() {
+ "succeeded"
+ } else {
+ "returned no EDID"
+ },
+ socket = connector + 1
+ );
+ }
+
+ // KMS is the sole owner of mode selection; setup only discovers connector state.
+ }
+
+ if rejects > 0 {
+ pr_warn!("vino: dock returned {rejects} undecryptable control frame(s)\n");
+ }
+ vino_debug!("vino: control setup tx={sent} rx={drained} ack={acks} reject={rejects}\n");
+ // Hand the caller the running counters: the next free AES-CTR block (`wseq`) and inner
+ // message counter (`cp_ctr`), so runtime KMS sends (mode-set/cursor) continue the sequence.
+ Ok((sent, wseq, cp_ctr))
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 7/13] drm/vino: add control-session bring-up
2026-08-26 16:37 ` [PATCH v3 7/13] drm/vino: add control-session bring-up Mike Lothian
@ 2026-08-26 16:57 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 16:57 UTC (permalink / raw)
To: Mike Lothian; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The driver encrypts `stream_open` messages for unoccupied connectors using a randomly generated video key that is never transmitted to the dock, permanently breaking hotplug for those connectors.
- [High] EDID crosstalk in Navarro discovery due to missing wait loop for asynchronous replies, causing one connector to claim another's EDID.
- [High] AES-CTR keystream reuse and control protocol desynchronization when an EDID transmission times out.
--
commit fa0367d27511540d9b322089cdf1062994527dcc
Author: Mike Lothian <mike@fireburn.co.uk>
drm/vino: add control-session bring-up
Bringing a dock up runs once per bind, in order: a plaintext initialisation
preamble, the link AKE, then the sealed setup burst that authenticates each
downstream connector, transitions the video engine and reads back what the
dock will answer.
> diff --git a/drivers/gpu/drm/vino/session/setup.rs b/drivers/gpu/drm/vino/session/setup.rs
> new file mode 100644
> index 0000000000000..b278ebcf4b4e5
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/session/setup.rs
[ ... ]
> + if i >= 3 && !rrx_applied {
> + let Some(rrx_h) = fresh_rrx else {
> + // No `rrx` means this connector never began a downstream authentication,
[ ... ]
> + vino_debug!(
> + "vino: socket {socket} has no downstream sink (no AKE_Send_Rrx); skipping its authentication\n"
> + );
> + Self::announce_stream(link, &mut out_q, profile, connector)?;
> + continue 'per_head;
> + };
[ ... ]
> + for connector in 0..connector_count {
> + settle_polls!(profile.protocol.setup_polls.before_open(connector as u8));
> + let stream_id = profile.geometry().stream_id(connector as u8);
> + let mut vkey = kernel::crypto::Secret::<16>::zeroed();
> + vkey.copy_from_slice(&video_keys[connector][..16]);
> + let mut vnonce = [0u8; 8];
> + vnonce.copy_from_slice(&video_keys[connector][16..24]);
> + let content = cp::stream_open(profile.protocol.stream_marker_kind);
> + let open = cp::seal_video_arm(&vkey, &vnonce, stream_id, 0x000a, 0, &content)?;
> + link.ctrl_send(&open, timeout(), GFP_KERNEL)?;
[Severity: High]
Will this break hotplug functionality for unoccupied connectors on DL-3x00
docks?
When initializing a dock with an unplugged connector (where video_on_ctrl_pipe
is true), the per_head loop aborts early via continue 'per_head. This means
SKE_Send_Eks is skipped and the generated video key is never sent to the dock.
Later in send_cp_setup(), the driver attempts to open the stream using this
same unsent, randomly generated key to encrypt the stream_open message. The
dock won't be able to decrypt this message, causing the stream to fail to
open, which leads to permanent black screens if a monitor is subsequently
plugged in.
[ ... ]
> + for connector in 0..connector_count {
[ ... ]
> + let fetch = cp::get_edid_req(cp_ctr, hu8)?;
> + navarro_send!(0x15, fetch);
> + // The fetch drain carries the asynchronous EDID in the working DLM cadence.
> + edid_connectors[connector] = edid_out.take();
> + discovery_deferred[connector] = edid_connectors[connector].is_none();
> + }
[Severity: High]
Is there a risk of EDID crosstalk here if the downstream panel's DDC read
takes longer than the 10ms timeout used in the navarro_send! macro?
During Navarro EDID discovery, if the fetch times out (yielding no EDID), the
loop advances to the next connector and issues a readiness kick. While
draining responses for the new connector's commands, the delayed EDID reply
from the previous connector could arrive.
Because drain_ep84() unconditionally assigns any parsed EDID to edid_out
without checking the connector ID, the current connector might mistakenly
claim the delayed EDID from the previous one, causing monitors to be
identified incorrectly.
[ ... ]
> + for connector in 0..connector_count {
[ ... ]
> + 'discovery: {
> + macro_rules! edid_send {
> + ($ep:expr, $body:expr, $tag:expr) => {{
> + match Self::send_live_cp(
> + link,
> + session,
> + ep84_q.as_mut(),
> + &mut resp,
> + edid_out,
> + $ep,
> + wseq,
> + &$body,
> + ) {
> + Ok(e) => {
[ ... ]
> + }
> + Err(e) => {
> + transport_error = Some(e);
> + break 'discovery;
> + }
> + }
> + }};
> + }
[Severity: High]
Does this error path lead to AES-CTR keystream reuse and control protocol
desynchronization?
If send_live_cp() fails (e.g., due to a USB timeout) during Ridge EDID
discovery, the edid_send! macro sets transport_error and breaks out of the
'discovery block. This skips the increments for both the AES-CTR block
counter (wseq) and the message counter (cp_ctr).
When the outer loop advances to the next connector, it encrypts the new
request using the exact same wseq and cp_ctr.
This reuse breaks cryptographic confidentiality. Additionally, if the dock
successfully received the initial message but the host timed out waiting for
the ACK, the dock will reject the next message due to the reused sequence
numbers, permanently breaking the session.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=7
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 8/13] drm/vino: add the KMS device and the atomic path
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (6 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 7/13] drm/vino: add control-session bring-up Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:59 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 9/13] drm/vino: add the dock activation and scanout path Mike Lothian
` (4 subsequent siblings)
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Benno Lossin, Gary Guo, linux-kernel,
rust-for-linux
Add the KMS half of the sink: the DRM driver and its GEM and file
types, the CRTCs, planes and connectors the compositor drives over a
software vblank clock, the mode admission checks that keep a connector
from being handed a mode past the dock's budget, the settings a matched
profile installs, and the workers the atomic callbacks publish to.
An atomic callback may not sleep or touch USB, so it records what the dock
should be doing and wakes a worker; each operation class owns one slot,
which makes publication infallible and lets a stale cursor position be
overwritten rather than queued behind the state that replaced it.
Sample depth is decided here too, and from `max bpc` rather than from
the committed framebuffer's format: a compositor drives a ten-bit link
from an eight-bit surface, which is what the property means everywhere
else. The dock's pixel budget is shared and was measured with three
bytes stored per pixel, so a ten-bit connector costs a third more and
the whole dock is priced at its deepest one. Where a pair does not fit,
the depth gives way rather than the mode: a compositor handed EINVAL
disables the output instead of asking for a shallower link. The decision
is taken in the enable, beside the mode set that carries it to the dock,
because a check also runs for page flips and for TEST_ONLY commits that
are never applied.
The planes publish the single modifier scanout accepts, so userspace reads
IN_FORMATS rather than inferring what will be taken from the format list.
The dock-facing half of the sink -- activation, presence, the streams and
the encoder feed -- follows in the next commit.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/drm_sink.rs | 1868 +++++++++++++++++
drivers/gpu/drm/vino/drm_sink/dispatch.rs | 446 ++++
drivers/gpu/drm/vino/drm_sink/driver.rs | 211 ++
drivers/gpu/drm/vino/drm_sink/limits.rs | 489 +++++
drivers/gpu/drm/vino/drm_sink/mode_objects.rs | 978 +++++++++
drivers/gpu/drm/vino/drm_sink/settings.rs | 574 +++++
drivers/gpu/drm/vino/drm_sink/worker.rs | 571 +++++
7 files changed, 5137 insertions(+)
create mode 100644 drivers/gpu/drm/vino/drm_sink.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/dispatch.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/driver.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/limits.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/mode_objects.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/settings.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/worker.rs
diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
new file mode 100644
index 000000000000..86708db89699
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -0,0 +1,1868 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! DRM/KMS integration for Vino.
+//!
+//! Each dock connector has a primary plane, cursor plane, CRTC, encoder and connector. Framebuffers
+//! are copied into driver-owned snapshots before atomic completion, then compressed and sent by
+//! per-connector workers. Connector modes come from downstream EDID tunneled over the dock's
+//! control protocol.
+
+use core::sync::atomic::{AtomicBool, AtomicI64, Ordering};
+use kernel::{
+ drm,
+ drm::kms::{
+ self,
+ connector::{self, Connector, ConnectorGuard, ConnectorModeValidation, ModeStatus, Status},
+ crtc::{self, CrtcAtomicCheck, CrtcAtomicCommit, RawCrtc as _, RawCrtcState as _},
+ encoder,
+ modes::DisplayMode,
+ plane::{self, PlaneAtomicCheck, PlaneAtomicCommit, RawPlaneState as _},
+ vblank::{
+ OwnedVblankRef, RawVblankCrtcState as _, VblankGuard, VblankSupport, VblankTimestamp,
+ },
+ KmsDriver, ModeConfigGuard, ModeConfigInfo, ModeObject as _, UnregisteredKmsDevice,
+ },
+ error::code::{EINVAL, ENODEV, ENOMEM, ENOTSUPP},
+ impl_has_hr_timer,
+ interrupt::LocalInterruptDisabled,
+ io::Io,
+ prelude::*,
+ sync::{
+ aref::ARef, new_mutex, new_spinlock, new_spinlock_irq, Arc, ArcBorrow, Completion, Mutex,
+ SpinLock, SpinLockIrq,
+ },
+ time::{
+ delay::{fsleep, udelay},
+ hrtimer::{
+ ArcHrTimerHandle, HrTimer, HrTimerCallback, HrTimerCallbackContext, HrTimerPointer,
+ HrTimerRestart, RelativeHardMode,
+ },
+ Delta, Instant, Monotonic,
+ },
+ workqueue::{
+ self, impl_has_delayed_work, impl_has_work, new_delayed_work, new_work, DelayedWork, Work,
+ WorkItem,
+ },
+ xxhash,
+};
+
+mod dispatch;
+mod driver;
+mod limits;
+mod mode_objects;
+mod settings;
+mod worker;
+
+pub(crate) use driver::VinoObject;
+use limits::{active_pixel_rate, timing_key, DEFAULT_MAX_HEAD_CLOCK_KHZ, DEFAULT_MAX_REFRESH_HZ};
+pub(super) use mode_objects::{
+ PlaneArgs, VblankTimer, VinoConnector, VinoCrtc, VinoEncoder, VinoPlane,
+};
+
+/// Connector mode used until a downstream EDID is available.
+const FALLBACK_W: i32 = 2560;
+const FALLBACK_H: i32 = 1440;
+
+/// Primary-plane format list (opaque 32bpp scanout).
+static PRIMARY_FORMATS: [u32; 1] = [drm::fourcc::XRGB8888];
+
+/// Primary-plane format list for a dock whose pipeline carries 10 bits per channel.
+///
+/// `XRGB8888` stays first: it is what every ordinary desktop commits, and a compositor choosing
+/// between them should not be pushed towards the deeper one by list order alone.
+static PRIMARY_FORMATS_HDR: [u32; 2] = [drm::fourcc::XRGB8888, drm::fourcc::XRGB2101010];
+
+/// The only framebuffer layout any of these planes accepts.
+///
+/// Publishing it gives userspace an `IN_FORMATS` property; without it the plane advertises formats
+/// with no modifier information at all.
+static LINEAR_MODIFIER: [u64; 1] = [drm::fourcc::FORMAT_MOD_LINEAR];
+
+/// Cursor-plane format list.
+static CURSOR_FORMATS: [u32; 1] = [drm::fourcc::ARGB8888];
+
+/// Stream-marker state which powers down a connector's downstream sink.
+///
+/// The resulting probe silence must be paired with [`VinoDrmData::self_blanked`] so it is not
+/// mistaken for a physical disconnect.
+///
+/// The vendor drives a DL-3x00 sink down with `0x2f` state 1 followed by `0x2e` state 3, and back
+/// up with `0x2e` state 0 then `0x2f` state 0. State 1 is what has been verified against a
+/// DL-6xxx dock, so the field is likely a bitmask rather than an enumeration; a platform that
+/// needs the vendor's exact value will have to carry it per profile.
+const BLANK_MARKER_STATE: u8 = 1;
+
+/// Delay before retrying a transient asynchronous control operation.
+const KMS_RETRY_MS: u32 = 50;
+
+/// Consecutive deferrals of a KMS command batch before it is dropped.
+///
+/// A command that fails because the dock has stopped answering will fail again for the same
+/// reason, and retrying it forever reprograms a dead link twenty times a second: it buries every
+/// other message in the log and keeps writing to a dock that has already abandoned its session.
+/// Anything genuinely transient clears well inside this, and a later commit or hotplug queues
+/// fresh work regardless. The bound is a little over the dock's own no-answer watchdog.
+const KMS_RETRY_LIMIT: u32 = 128;
+
+/// Maximum number of physical downstream connectors Vino exposes.
+///
+/// Ridge docks use the first two. Navarro has four physical DP sockets; connectors 0/2 share
+/// bulk endpoint 0x08 and connectors 1/3 share 0x0a. `DockProfile::connectors` selects the
+/// active prefix at runtime, while this constant keeps the DRM object layout fixed at registration.
+pub(crate) const MAX_CONNECTORS: usize = 4;
+
+/// Bulk transfer size on the video endpoints: a multiple of the 1024-byte maximum packet size, so
+/// only a frame's final transfer terminates short.
+const VIDEO_XFER: usize = 65536;
+
+/// Stream reports a connector sends after its stream is opened, on a dock that carries video on the
+/// control pipe.
+///
+/// The report restates the mode on a stream the dock has just been handed. DLM sends fourteen of
+/// them across 7115 frames and never two together, so one per stream is the whole of it; sending
+/// one per frame spends both the dock's bandwidth budget and its sealed block counter on a record
+/// it is not waiting for.
+const STREAM_REPORT_BURST: u32 = 1;
+
+/// Frame of a fresh stream that carries the report, counting the prologue as zero.
+///
+/// DLM restates the mode with the third frame, not the first: the two frames before it carry
+/// nothing but pixels and their closing record.
+const STREAM_REPORT_FRAME: u32 = 2;
+
+/// Whether a presentation named a ring slot, and so consumed a frame counter.
+///
+/// The counter belongs to whichever record names the slot: the frame opener on a dock that
+/// carries one, the frame trailer on a dock that carries the transition there. A presentation with
+/// neither says nothing about the ring, and counting it anyway puts every later record one slot
+/// ahead of the buffer the host actually wrote, so the dock scans out a buffer nothing has filled.
+pub(crate) fn names_ring_slot(opener: &[u8], trailer: &[u8]) -> bool {
+ !opener.is_empty() || !trailer.is_empty()
+}
+
+/// Maximum number of individual frame-damage rectangles re-converted per flip before they are
+/// collapsed into a single bounding box. Bounds the stack array used on the atomic-commit path
+/// (no per-flip allocation); a compositor that reports more clips than this just gets a coarser
+/// (still correct) repaint.
+const MAX_DAMAGE_CLIPS: usize = 16;
+/// Minimum interval between normal frames for one connector.
+const FRAME_PERIOD_MS: i64 = 5;
+/// The coalescing window in microseconds. Whole-millisecond arithmetic truncated the elapsed time
+/// and forced a 1 ms minimum sleep, so a frame could wait materially longer than the window.
+const FRAME_PERIOD_US: i64 = FRAME_PERIOD_MS * 1000;
+/// Interval between keepalive status queries on a dock whose profile does not state one.
+const STATUS_PERIOD_MS: i64 = 250;
+
+/// Activation timing relative to the mode-set submission.
+const PROMPT_VIDEO_MS: i64 = 110;
+const PROMPT_CLOSE_2F_MS: i64 = 123;
+const PROMPT_CLOSE_2E_MS: i64 = 125;
+/// Upper bound used to quiesce an already-running keepalive iteration.
+const PROMPT_KEEPALIVE_QUIESCE_MS: i64 = 40;
+/// Minimum interval between streaming status polls (`id=0x14 sub=0x0c`).
+///
+/// Issued per presentation this would be 200-600 CP round-trips a second across two connectors at
+/// ~100 fps, every one of them serialising on the single control link, and whichever connector
+/// looped fastest would re-acquire it immediately and starve the other. DLM issues ~3.8 per second,
+/// so a quarter-second floor matches the reference and leaves the link free for the other
+/// connector.
+const STATUS_POLL_MIN_MS: i64 = 250;
+const PROMPT_TRAINING_OPEN_MS: i64 = 0;
+const PROMPT_TRAINING_TAIL_MS: i64 = 400;
+/// How long [`VinoDrmData::blank_connector`] keeps presenting black when a CRTC is disabled.
+///
+/// It only has to outlast the dock's buffer rotation, which is at most three presentations on the
+/// current profiles; a black frame is ~200 KB and presents in a couple of milliseconds, so this is
+/// generous by an order of magnitude and still finishes well inside a DPMS transition. It is not a
+/// training window -- nothing downstream needs settling -- so it does not reuse
+/// [`PROMPT_TRAINING_TAIL_MS`].
+const BLANK_PRESENT_MS: i64 = 120;
+
+/// `edid_target` sentinel: nobody is waiting for an EDID.
+const NO_EDID_TARGET: u32 = u32::MAX;
+
+/// How much of a DL7400 frame's image data precedes its per-strip parameter map.
+///
+/// The map tells the dock how to read the records around it, and where it sits in the frame is
+/// load-bearing rather than cosmetic: the vendor's stream carries it this far into the image
+/// records of every frame, and a frame that carries the same valid records after all of its pixels
+/// instead is accepted twice and then leaves the endpoint permanently un-drained.
+pub(crate) const NAVARRO_PARAM_IMAGE_OFFSET: usize = 115168;
+
+/// How many leading record chunks precede the parameter map in a frame.
+///
+/// The map has to land on a record boundary, and an encoded frame's chunks are the boundaries this
+/// side knows: each holds whole records, while [`NAVARRO_PARAM_IMAGE_OFFSET`] itself falls wherever
+/// a frame's record lengths put it. So round the vendor's offset back to a chunk, and keep at least
+/// one chunk in front of the map -- what the dock will not take is the map arriving after every
+/// record it describes, not the exact byte it arrives at.
+pub(crate) fn param_map_chunk_split(frames: &[KVec<u8>]) -> usize {
+ let mut consumed = 0usize;
+ let mut chunks = 0usize;
+ for f in frames {
+ if consumed + f.len() > NAVARRO_PARAM_IMAGE_OFFSET {
+ break;
+ }
+ consumed += f.len();
+ chunks += 1;
+ }
+ chunks.max(1).min(frames.len())
+}
+type DamageRect = (usize, usize, usize, usize);
+type BoundInterface<'a> = super::UsbLink<'a>;
+
+/// A dock's unspent sustained-throughput credit.
+///
+/// Credit accrues at the profile's rate and is capped at one second of it, so a dock idle for a
+/// minute does not bank a minute of bytes and then hand them to the endpoint at once. Spending is
+/// allowed to overdraw: a frame's size is known only once it is encoded, and refusing to send a
+/// frame already committed to the wire would strand it. The debt is repaid before the next frame
+/// is selected, which is what turns the ledger into a rate.
+pub(crate) struct StreamCredit {
+ bytes: i64,
+ topped_up: Option<Instant<Monotonic>>,
+}
+
+impl StreamCredit {
+ fn new() -> Self {
+ Self {
+ bytes: 0,
+ topped_up: None,
+ }
+ }
+}
+
+/// Credit accrued over `elapsed_us` at `bps`, saturating rather than wrapping on a long idle.
+pub(crate) fn stream_credit_accrued(bps: u32, elapsed_us: i64) -> i64 {
+ let bps = i64::from(bps);
+ elapsed_us
+ .max(0)
+ .saturating_mul(bps)
+ .checked_div(1_000_000)
+ .unwrap_or(0)
+}
+
+/// Microseconds until an overdrawn ledger is back in credit.
+pub(crate) fn stream_credit_wait_us(bps: u32, bytes: i64) -> Option<i64> {
+ if bytes >= 0 {
+ return None;
+ }
+ let bps = i64::from(bps).max(1);
+ Some(
+ bytes
+ .saturating_neg()
+ .saturating_mul(1_000_000)
+ .checked_div(bps)
+ .unwrap_or(0)
+ .saturating_add(1),
+ )
+}
+
+/// Presentations made by one logical scanout submission.
+///
+/// Cold training remains a transport exception for docks with a dedicated video endpoint. Normal
+/// keyframe and delta counts are profile data because ring depth alone does not describe how a
+/// platform expects the host to populate that ring.
+pub(crate) fn frame_presentation_count(
+ policy: super::profile::FrameDelivery,
+ full: bool,
+ cold_training: bool,
+ video_on_ctrl_pipe: bool,
+) -> u32 {
+ if full && cold_training && !video_on_ctrl_pipe {
+ COLD_TRAINING_PRESENTATIONS
+ } else if full {
+ u32::from(policy.keyframe_presentations.max(1))
+ } else {
+ u32::from(policy.delta_presentations.max(1))
+ }
+}
+
+/// Account one accepted logical submission against per-strip delivery debt.
+pub(crate) fn pay_damage_debt(debt: &mut [u8], full: bool) {
+ if full {
+ debt.fill(0);
+ } else {
+ for remaining in debt {
+ *remaining = remaining.saturating_sub(1);
+ }
+ }
+}
+
+/// Content of the last frame successfully submitted for one connector, represented in the dock's
+/// native 64x16 strip grid. KWin frequently omits `FB_DAMAGE_CLIPS` when switching framebuffer
+/// objects; a raw-content shadow is therefore the authoritative way to distinguish an unchanged
+/// flip from a real repaint. `KVVec` permits vmalloc fallback for the roughly 29-KiB 1440p hash
+/// table.
+struct StripHashState {
+ padded_width: usize,
+ padded_height: usize,
+ hashes: KVVec<u64>,
+ /// Encoded strip bodies, parallel to `hashes`. Retransmit debt can reuse a body while its
+ /// pixels and encoding tag remain unchanged.
+ bodies: KVec<KVec<u8>>,
+ /// Everything other than the strip's own pixels that its encoded bytes depend on.
+ ///
+ /// The hash covers the raw framebuffer, so a change that alters the ENCODED output without
+ /// altering the source pixels would otherwise serve a stale body. Gamma is exactly that: a new
+ /// LUT re-maps every pixel on the way into the codec while leaving the framebuffer identical,
+ /// and although a gamma change owes a keyframe, a keyframe re-selects every strip rather than
+ /// invalidating anything, so the reuse test would still hit. Rotation is the same hazard, and
+ /// is handled by only ever caching under identity rotation.
+ tag: u64,
+}
+
+/// The DRM driver marker type.
+pub(super) struct VinoDrmDriver;
+
+/// Convenience alias for our concrete `drm::Device`.
+pub(super) type VinoDrmDevice = drm::Device<VinoDrmDriver>;
+
+/// Active control-protocol session.
+///
+/// `wire_seq` counts AES-CTR content blocks; the authentication tag does not consume keystream.
+/// `counter` is the inner protocol counter. The enclosing mutex advances both atomically with a
+/// complete request/reply transaction.
+pub(super) struct CpLink {
+ ks: kernel::crypto::Secret<16>,
+ riv: [u8; 8],
+ wire_seq: u32,
+ counter: u16,
+ ep84_q: Option<super::usb::BulkInQueue>,
+}
+
+/// How long the dock may answer nothing at all before the session is abandoned.
+///
+/// Measured in silence rather than in unanswered messages, because those are not the same thing. A
+/// connector whose sink is down leaves its own re-engage unanswered every few seconds for the life
+/// of the session while its sibling drives a lit panel and the status dialogue replies throughout;
+/// counting messages tears that session down. A dock with a video pipe of its own answers a lit,
+/// idle link continuously, so real silence there is unambiguous.
+const CP_SILENCE_LIMIT_MS: i64 = 5000;
+
+/// The same limit for a dock that shares its control pipe with video.
+///
+/// Silence is not evidence of anything on such a dock: the vendor's own capture has it say nothing
+/// for 79 s while a panel is lit, and the vendor sends nothing either. Applying the short limit
+/// there abandons a working session, and the reset that follows is what turns a stalled dock into
+/// one that has to be unplugged.
+const CP_SILENCE_LIMIT_SHARED_MS: i64 = 90_000;
+
+/// How often the watchdog checks the silence deadline.
+const CP_WATCHDOG_PERIOD_MS: u32 = 1000;
+
+/// A control-protocol operation deferred from the non-blocking atomic callbacks.
+enum KmsCmd {
+ ModeSet {
+ connector: u8,
+ timing: super::cp::Timing,
+ },
+ CursorCreate {
+ connector: u8,
+ w: u16,
+ h: u16,
+ },
+ CursorImage {
+ connector: u8,
+ w: u16,
+ h: u16,
+ bgra: KVec<u8>,
+ },
+ CursorMove {
+ connector: u8,
+ x: u16,
+ y: u16,
+ /// The dock's own visible flag. Hiding by parking the cursor at `u16::MAX` instead left a
+ /// ghost pointer at the top-left of both panels: the dock wraps an out-of-range origin
+ /// rather than clipping the cursor away.
+ visible: bool,
+ },
+ /// Drive the stream black and close its control-protocol bracket.
+ Blank {
+ connector: u8,
+ },
+}
+
+impl KmsCmd {
+ fn connector(&self) -> usize {
+ match self {
+ Self::ModeSet { connector, .. }
+ | Self::CursorCreate { connector, .. }
+ | Self::CursorImage { connector, .. }
+ | Self::CursorMove { connector, .. }
+ | Self::Blank { connector } => *connector as usize,
+ }
+ }
+}
+
+/// The cursor state one connector's dock connector is holding, as last acknowledged on the wire.
+///
+/// A mode set leaves the dock's cursor undefined -- `owe_keyframe` says so by bumping
+/// `cursor_epoch` -- but vino could only act on that when the compositor next committed the cursor
+/// plane. A pointer that is not moving produces no commit, so the cursor simply vanished until it
+/// was moved, and every mode set is now dock-wide, so it vanished on *both* panels for a change
+/// made to one. Keeping the last accepted bitmap and position lets the mode-set path put it back
+/// itself.
+struct CursorShot {
+ w: u16,
+ h: u16,
+ bgra: KVec<u8>,
+ x: u16,
+ y: u16,
+ visible: bool,
+}
+
+struct PendingKmsHead {
+ stream: Option<KmsCmd>,
+ cursor_create: Option<KmsCmd>,
+ cursor_image: Option<KmsCmd>,
+ cursor_move: Option<KmsCmd>,
+}
+
+impl PendingKmsHead {
+ const fn new() -> Self {
+ Self {
+ stream: None,
+ cursor_create: None,
+ cursor_image: None,
+ cursor_move: None,
+ }
+ }
+
+ fn slot(&mut self, cmd: &KmsCmd) -> &mut Option<KmsCmd> {
+ match cmd {
+ KmsCmd::ModeSet { .. } | KmsCmd::Blank { .. } => &mut self.stream,
+ KmsCmd::CursorCreate { .. } => &mut self.cursor_create,
+ KmsCmd::CursorImage { .. } => &mut self.cursor_image,
+ KmsCmd::CursorMove { .. } => &mut self.cursor_move,
+ }
+ }
+
+ fn update(&mut self, cmd: KmsCmd) {
+ let slot = self.slot(&cmd);
+ *slot = Some(cmd);
+ }
+
+ /// Restore a failed operation unless a newer desired operation already occupies its slot.
+ fn retry(&mut self, cmd: KmsCmd) {
+ let slot = self.slot(&cmd);
+ if slot.is_none() {
+ *slot = Some(cmd);
+ }
+ }
+
+ fn has_stream(&self) -> bool {
+ self.stream.is_some()
+ }
+}
+
+struct PendingKms {
+ connectors: [PendingKmsHead; MAX_CONNECTORS],
+}
+
+impl PendingKms {
+ const fn new() -> Self {
+ Self {
+ connectors: [const { PendingKmsHead::new() }; MAX_CONNECTORS],
+ }
+ }
+
+ fn is_empty(&self) -> bool {
+ self.connectors.iter().all(|connector| {
+ connector.stream.is_none()
+ && connector.cursor_create.is_none()
+ && connector.cursor_image.is_none()
+ && connector.cursor_move.is_none()
+ })
+ }
+
+ /// Discard every queued command. Used when the link they address is gone; see
+ /// `KMS_RETRY_LIMIT`.
+ fn clear(&mut self) {
+ self.connectors = [const { PendingKmsHead::new() }; MAX_CONNECTORS];
+ }
+
+ fn has_stream(&self) -> bool {
+ self.connectors.iter().any(PendingKmsHead::has_stream)
+ }
+
+ fn update(&mut self, cmd: KmsCmd) {
+ if let Some(pending) = self.connectors.get_mut(cmd.connector()) {
+ pending.update(cmd);
+ }
+ }
+
+ fn retry(&mut self, cmd: KmsCmd) {
+ if let Some(pending) = self.connectors.get_mut(cmd.connector()) {
+ pending.retry(cmd);
+ }
+ }
+
+ /// Restore a drained batch without replacing newer state published while it was executing.
+ fn retry_batch(&mut self, batch: Self) {
+ for connector in batch.connectors {
+ let PendingKmsHead {
+ stream,
+ cursor_create,
+ cursor_image,
+ cursor_move,
+ } = connector;
+ for cmd in [stream, cursor_create, cursor_image, cursor_move]
+ .into_iter()
+ .flatten()
+ {
+ self.retry(cmd);
+ }
+ }
+ }
+}
+
+fn kms_error_retryable(error: Error) -> bool {
+ error != EINVAL && error != ENOTSUPP
+}
+
+/// Latest primary-plane flip awaiting compression on the deferred worker. The framebuffer is
+/// refcounted, so it remains valid after the atomic commit callback returns. There is one slot per
+/// connector: a newer flip replaces an older unsent flip instead of building an unbounded queue
+/// behind a slow encoder. When replacement could lose accumulated damage, the newer flip is
+/// promoted to a full-output damage rectangle.
+struct PendingScanout {
+ connector: u8,
+ rotation: plane::Rotation,
+ clips: [DamageRect; MAX_DAMAGE_CLIPS],
+ nclips: usize,
+ w: usize,
+ h: usize,
+ /// Which private surface contains this commit's pixels.
+ shadow_idx: usize,
+ /// Generation of `shadow_idx`, used to reject a slot replaced before the worker claims it.
+ shadow_generation: u64,
+}
+
+impl Clone for PendingScanout {
+ fn clone(&self) -> Self {
+ Self {
+ connector: self.connector,
+ rotation: self.rotation,
+ clips: self.clips,
+ nclips: self.nclips,
+ w: self.w,
+ h: self.h,
+ shadow_idx: self.shadow_idx,
+ shadow_generation: self.shadow_generation,
+ }
+ }
+}
+
+/// Private snapshots let atomic commit copy into one while the worker encodes another. Copying
+/// before flip completion ensures that the compositor cannot reuse storage while Vino reads it.
+///
+/// Three, not two: a slot can be reserved by the encoder (`inflight`) *and* by a snapshot that has
+/// dropped the pool lock to copy (`writing`) at the same time, which with two slots left none free
+/// and silently dropped the commit. Three keeps one available in that state.
+const SHADOW_SLOTS: usize = 3;
+
+/// Maximum number of prepared compositor buffers retained per connector.
+///
+/// Compositors normally rotate through a small swapchain. Keeping four validated mappings moves
+/// vmap preparation out of repeated flips while bounding pinned memory when a client reallocates.
+const SOURCE_BINDINGS: usize = 4;
+
+struct SourceBinding {
+ framebuffer: ARef<kms::framebuffer::Framebuffer<VinoDrmDriver>>,
+ mapping: kms::framebuffer::FramebufferVMapOwned<VinoObject>,
+}
+
+struct SourceBindingCache {
+ entries: [Option<Arc<SourceBinding>>; SOURCE_BINDINGS],
+ next: usize,
+}
+
+impl SourceBindingCache {
+ const fn new() -> Self {
+ Self {
+ entries: [const { None }; SOURCE_BINDINGS],
+ next: 0,
+ }
+ }
+
+ fn get(
+ &mut self,
+ fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
+ ) -> Result<Arc<SourceBinding>> {
+ if let Some(binding) = self
+ .entries
+ .iter()
+ .flatten()
+ .find(|binding| &*binding.framebuffer == fb)
+ {
+ return Ok(binding.clone());
+ }
+
+ let binding = Arc::new(
+ SourceBinding {
+ framebuffer: ARef::from(fb),
+ mapping: fb.owned_vmap::<VinoObject>()?,
+ },
+ GFP_KERNEL,
+ )?;
+ self.entries[self.next] = Some(binding.clone());
+ self.next = (self.next + 1) % SOURCE_BINDINGS;
+ Ok(binding)
+ }
+
+ fn discard(&mut self) {
+ self.entries = [const { None }; SOURCE_BINDINGS];
+ self.next = 0;
+ }
+}
+
+struct ShadowSurface {
+ w: usize,
+ h: usize,
+ pixels: KVVec<u8>,
+ /// Per-strip content hashes, computed while copying into this immutable snapshot.
+ hashes: KVVec<u64>,
+ /// Scratch holding one `STRIP_H`-row band of the source, in this surface's packed stride.
+ ///
+ /// The snapshot reads the source one full row at a time into this buffer and then hashes and
+ /// copies the band's strips out of it, so the source is read once per frame, sequentially, and
+ /// every strip's second pass hits a buffer small enough to stay in cache. At 2560 wide it is
+ /// 160 KiB against the 14.7 MB of `pixels`.
+ band: KVVec<u8>,
+}
+
+struct ShadowSlot {
+ generation: u64,
+ surface: Option<ShadowSurface>,
+}
+
+impl ShadowSlot {
+ const fn new() -> Self {
+ Self {
+ generation: 0,
+ surface: None,
+ }
+ }
+}
+
+/// One connector's shadow surfaces. Locked per connector: the snapshot copies ~14.7 MB while
+/// holding this, and it runs on the compositor's non-blocking commit tail, so a device-wide lock
+/// made one connector's commit stall the other's -- measured at up to 4.2 ms, half a 120 Hz frame
+/// budget.
+struct ShadowPool {
+ slots: [ShadowSlot; SHADOW_SLOTS],
+ inflight: Option<usize>,
+ /// Slot currently being written by a snapshot that has released the pool lock.
+ ///
+ /// The copy is far too long to hold the lock across, so the commit takes the slot's surface
+ /// out, drops the lock and copies into it unlocked. This marks the slot reserved for that
+ /// window, exactly as `inflight` does for the encoder's side.
+ writing: Option<usize>,
+ source_bindings: SourceBindingCache,
+}
+
+impl ShadowPool {
+ const fn new() -> Self {
+ Self {
+ slots: [const { ShadowSlot::new() }; SHADOW_SLOTS],
+ inflight: None,
+ writing: None,
+ source_bindings: SourceBindingCache::new(),
+ }
+ }
+
+ fn discard(&mut self) {
+ for slot in &mut self.slots {
+ slot.generation = slot.generation.wrapping_add(1);
+ slot.surface = None;
+ }
+ self.source_bindings.discard();
+ }
+}
+
+/// How long a cold downstream link is fed keyframes at frame cadence; see `sustain_window`.
+const SUSTAIN_MS: i64 = 3000;
+
+/// Delay before the one-shot post-keyframe repaint.
+const SETTLE_REPAINT_MS: i64 = 1200;
+
+/// Number of post-keyframe repaints. Cold-link training uses its separate bounded deadline.
+const SETTLE_REPAINTS: u32 = 1;
+
+/// Longest the DL7400 tolerates a silent video endpoint before it tears the link down.
+///
+/// Measured twice, with very different transfer shapes: a full 204 KB frame and a single 4 KB
+/// image record both ended with every outstanding URB completing `-ESHUTDOWN` 1.06 s and 1.10 s
+/// after the last video byte, the dock going deaf on the control plane at the same instant. DLM
+/// never gets near it -- it pairs a sealed report with every frame, a median 9-19 ms apart and at
+/// most 1.0 s apart even when the desktop is still.
+const NAVARRO_VIDEO_QUIET_MS: i64 = 1000;
+
+/// Period at which an idle DL7400 connector is re-fed, comfortably inside
+/// [`NAVARRO_VIDEO_QUIET_MS`].
+const NAVARRO_KEEPALIVE_MS: i64 = 250;
+
+/// Keep a missed repaint from being enough to trip the dock's teardown.
+const _: () = assert!(NAVARRO_KEEPALIVE_MS * 3 <= NAVARRO_VIDEO_QUIET_MS);
+
+/// DRM device-private data: the bound USB interface, engaged CP session, connector state, deferred
+/// scanout slots and per-connector transport state.
+#[pin_data]
+pub(super) struct VinoDrmData {
+ /// The USB I/O-permitted window for this device's interface, shared with the persistent
+ /// queues. `disconnect()` closes it, after which every transfer path here fails cleanly
+ /// instead of touching an unbound interface.
+ pub(super) io: Arc<super::usb::IoWindow>,
+ /// The dock's endpoints, resolved and direction/type-checked once during probe.
+ pub(super) endpoints: super::Endpoints,
+ /// Stops every producer before unplug drains the embedded work item. This is checked while
+ /// holding the producer's queue lock so a late atomic callback cannot enqueue a self-owning
+ /// `ARef<VinoDrmDevice>` after `cancel_sync()` has already returned.
+ shutting_down: AtomicBool,
+ #[pin]
+ cp_link: Mutex<Option<CpLink>>,
+ /// When the dock last answered anything, and whether a session exists at all.
+ ///
+ /// Deliberately outside `cp_link`. A dock that has stopped answering must be stopped talking
+ /// to, but the thread that discovers this is the one already stuck: `usb_bulk_msg` honours its
+ /// own timeout and then kills the URB, and *that* wait is unbounded, so a controller which
+ /// will not retire the transfer leaves the caller blocked uninterruptibly with `cp_link` held.
+ /// Everything that has to take the mutex to learn the link is stuck therefore blocks behind
+ /// the very transfer it is trying to diagnose -- including the keepalive's own liveness check.
+ /// A spinlock and an atomic are always available.
+ #[pin]
+ cp_last_reply: SpinLock<Instant<Monotonic>>,
+ cp_session_live: AtomicBool,
+ /// Set once this device has been asked to reset itself out of a wedged session.
+ ///
+ /// One attempt only. A reset that works re-probes into a fresh device with this cleared; a
+ /// reset that does not must not become a loop.
+ cp_reset_queued: AtomicBool,
+ /// Watchdog that enforces the silence deadline from off the control path.
+ ///
+ /// The keepalive cannot do this itself: it *is* the thread that wedges, so its own check at
+ /// the top of the loop is never reached again. Scheduled on the system queue rather than
+ /// vino's, which the stuck transaction owns.
+ #[pin]
+ cp_watchdog: DelayedWork<VinoDrmDevice, 5>,
+ /// Latest desired control/KMS state per connector.
+ #[pin]
+ pending_kms: Mutex<PendingKms>,
+ /// Coalescing per-connector scanout slots consumed by `cmd_work`.
+ ///
+ /// Compression and USB submission may sleep and therefore cannot run in
+ /// `atomic_update`.
+ #[pin]
+ pending_scanout: Mutex<[Option<PendingScanout>; MAX_CONNECTORS]>,
+ /// A one-shot repaint of the connector's newest known framebuffer. Cleared as soon as it is
+ /// taken, or whenever a real flip arrives (that flip already carries newer content, so the
+ /// redundant repaint is pointless). See [`SETTLE_REPAINT_MS`] for the hardware observation
+ /// behind it.
+ ///
+ /// The `bool` is "promote to a full keyframe". It is true for the post-keyframe settle repaint,
+ /// whose job is to replace a stale surface. It is false for a *debt* repaint, which carries
+ /// outstanding `dirty_ttl` retransmissions to the dock's second buffer without promoting them
+ /// to a keyframe.
+ #[pin]
+ settle_repaint: Mutex<[Option<(Instant<Monotonic>, PendingScanout, bool)>; MAX_CONNECTORS]>,
+ /// Private committed surfaces and their worker ownership state.
+ #[pin]
+ shadow: [Mutex<ShadowPool>; MAX_CONNECTORS],
+ /// Active software-vblank timers. The device owns their cancellation handles so shutdown does
+ /// not depend on atomic-disable callbacks running. A spinlock is required because
+ /// `enable_vblank` runs with local interrupts disabled.
+ #[pin]
+ vblank: SpinLock<[Option<(Arc<VblankTimer>, ArcHrTimerHandle<VblankTimer>)>; MAX_CONNECTORS]>,
+ /// Work item that drains control/KMS commands.
+ #[pin]
+ cmd_work: DelayedWork<VinoDrmDevice>,
+ /// Independent per-connector scanout workers. Their work IDs are const generics, so each
+ /// connector has an explicit field; transport state is taken from per-connector slots while a
+ /// frame is submitted.
+ #[pin]
+ scanout_work_h0: Work<VinoDrmDevice, 1>,
+ #[pin]
+ scanout_work_h1: Work<VinoDrmDevice, 2>,
+ #[pin]
+ scanout_work_h2: Work<VinoDrmDevice, 3>,
+ #[pin]
+ scanout_work_h3: Work<VinoDrmDevice, 4>,
+ /// Dedicated queue for initial authentication and the steady-state control session.
+ session_queue: workqueue::OwnedQueue,
+ /// Ordered queue for runtime KMS and cursor control transactions.
+ kms_queue: workqueue::OwnedQueue,
+ /// Per-device unbound queue for the two scanout workers.
+ scanout_queue: workqueue::OwnedQueue,
+ /// Downstream EDID per connector. Connector callbacks use their connector index to read this
+ /// owned state; publishing EDID therefore requires no raw pointer back into a DRM mode object.
+ #[pin]
+ cached_edids: Mutex<[Option<KVec<u8>>; MAX_CONNECTORS]>,
+ /// Bit N is set once CP confirms that a real downstream monitor is present on connector N.
+ connectors_present: core::sync::atomic::AtomicU32,
+ /// Each connector's gamma ramp cached from its CRTC atomic hook as three 256-entry 8-bit LUTs
+ /// (`[r; 256] ++ [g; 256] ++ [b; 256]`), or `None` for identity. Cached here (not read from the
+ /// CRTC state) because scanout runs in the plane path; each entry is `Copy`, so the scanout
+ /// snapshots its connector's entry under the lock and applies it without holding the lock in
+ /// the pixel loop. Per connector so a second display's gamma cannot clobber the first's.
+ #[pin]
+ color: Mutex<[Option<super::color::ColorPipeline>; MAX_CONNECTORS]>,
+ /// Per-connector strip hashes for the last frame accepted by the USB submission path. Updated
+ /// only after the complete frame has been queued, so a failed transfer can never advance the
+ /// shadow beyond what the dock may actually display.
+ #[pin]
+ strip_hashes: Mutex<[Option<StripHashState>; MAX_CONNECTORS]>,
+ /// The DL7400 per-strip size-class map most recently sent for each connector.
+ ///
+ /// The map describes the whole surface while a delta frame carries only its damaged strips, so
+ /// rebuilding it from zero each frame re-declares every untouched position as class 0. See
+ /// `video::haar::navarro_strip_params`.
+ #[pin]
+ strip_classes: Mutex<[KVec<u8>; MAX_CONNECTORS]>,
+ /// Per-strip retransmit debt. Spreading repeated updates across frames reaches both of the
+ /// dock's scanout buffers; consecutive presentations can target the same buffer.
+ #[pin]
+ dirty_ttl: Mutex<[Option<KVVec<u8>>; MAX_CONNECTORS]>,
+ /// Set once the dock engages the CP cipher (`wsub=0x45` acks > 0); EP08 scanout is gated on it.
+ /// Per device, so a second connected dock does not share one dock's engagement state.
+ cp_engaged: core::sync::atomic::AtomicBool,
+ /// Set once encrypted setup, initial sink discovery, and the platform's pre-mode-set readiness
+ /// interval have all completed. KMS producers may coalesce state before this, but no activation
+ /// may touch the dock until the bring-up worker publishes this one-way gate.
+ kms_activation_ready: core::sync::atomic::AtomicBool,
+ /// This device's codec geometry, packed; see [`VinoDrmData::geometry`] and
+ /// [`super::video::haar::Geometry`].
+ codec_geometry: core::sync::atomic::AtomicU32,
+ /// Keyframe, delta and damage-debt presentation counts, packed one byte each; see
+ /// [`super::profile::FrameDelivery`].
+ frame_delivery: core::sync::atomic::AtomicU32,
+ /// Whether a presence retry may reset a bracket beside a live connector; see
+ /// [`super::profile::ProbeBracket`].
+ probe_bracket: core::sync::atomic::AtomicU8,
+ /// Bit `h` set when connector `h`'s committed framebuffer is 10 bits per channel.
+ ///
+ /// Separate from `codec_geometry` because it is the one part of the codec's configuration that
+ /// is neither device-wide nor fixed: the DL7400 negotiates depth per connector, measured on
+ /// Windows holding one connector at 8 bits while the other ran at 10.
+ connector_ten_bit: core::sync::atomic::AtomicU32,
+ /// Bits per channel userspace asked the link to carry, one byte per connector, from `max bpc`.
+ ///
+ /// Deliberately separate from `connector_ten_bit`: that one says what the framebuffer holds and
+ /// decides how a pixel is decoded, this one says what the dock is told to carry.
+ connector_max_bpc: core::sync::atomic::AtomicU32,
+ /// Connectors whose requested ten-bit link does not fit the dock's shared bandwidth.
+ ///
+ /// Ten bits costs a third more per pixel, so a pair of modes that fits at eight may not fit at
+ /// ten. Refusing the mode is the wrong answer -- a compositor answers `EINVAL` by disabling the
+ /// output rather than choosing a shallower link -- so the depth gives way instead and both
+ /// connectors light at eight bits.
+ connector_deny_ten_bit: core::sync::atomic::AtomicU32,
+ /// Bit `h` set when connector `h`'s connector is being driven with the SMPTE ST 2084 (PQ)
+ /// transfer function, taken from the `HDR_OUTPUT_METADATA` blob userspace attached to it.
+ ///
+ /// Deliberately not folded into `connector_ten_bit`: depth and transfer function are two fields
+ /// of the dock's set-mode message and two independent decisions by the compositor.
+ head_st2084: core::sync::atomic::AtomicU32,
+ /// Which protocol generation this dock speaks; see `DockProfile::generation`. The two
+ /// platforms differ in their initialisation, per-connector HDCP framing, stream open and mode
+ /// description, so one flag drives all of them rather than three that can disagree.
+ dock_wide_modeset: core::sync::atomic::AtomicBool,
+ clear_mode_before_set: core::sync::atomic::AtomicBool,
+ blank_markers_held: core::sync::atomic::AtomicBool,
+ video_keepalive: core::sync::atomic::AtomicBool,
+ /// Whether the first frame after a mode set carries the cold ARM burst; see
+ /// `DockProfile::arm_burst`.
+ arm_burst: core::sync::atomic::AtomicBool,
+ /// How this dock states its framebuffer allocation; see [`profile::Allocation`].
+ allocation: kernel::sync::SetOnce<&'static super::profile::Allocation>,
+ /// Whether video records travel on the control bulk-OUT pipe; see
+ /// `DockProfile::video_on_ctrl_pipe`.
+ video_on_ctrl_pipe: core::sync::atomic::AtomicBool,
+ /// The `0x16/0x2e` state that takes a sink down; see `DockProfile::sink_down_state`.
+ sink_down_state: core::sync::atomic::AtomicU8,
+ post_mode_sink_states: core::sync::atomic::AtomicU16,
+ /// `DockProfile::pre_mode_sink_state`, with `u16::MAX` standing for `None`.
+ pre_mode_sink_state: core::sync::atomic::AtomicU16,
+ /// Heads whose sealed video stream has been opened, as a bitmask; see `set_video_keys`.
+ stream_opened: core::sync::atomic::AtomicU32,
+ /// Stream reports a connector still owes after its stream was opened; see
+ /// `arm_stream_prologue`.
+ stream_reports_owed: [core::sync::atomic::AtomicU32; MAX_CONNECTORS],
+ /// Consecutive deferrals of the asynchronous KMS batch; see `KMS_RETRY_LIMIT`.
+ kms_retries: core::sync::atomic::AtomicU32,
+ /// How this dock's video stream describes itself, packed: the layout word in the low sixteen
+ /// bits, the stream-marker kind above it, and the code-table form in bit 24. See
+ /// `set_video_stream_desc`.
+ video_stream_desc: core::sync::atomic::AtomicU32,
+ /// Shortest interval between frames on one connector; see `DockProfile::frame_period_ms`.
+ frame_period_us: core::sync::atomic::AtomicI64,
+ /// Interval between keepalive status queries; see `DockProfile::status_period_ms`.
+ status_period_ms: core::sync::atomic::AtomicI64,
+ /// Flat carrier frames a connector opens its stream with; see `DockProfile::carrier_frames`.
+ carrier_frames: core::sync::atomic::AtomicU32,
+ /// Sustained bytes per second this dock accepts; see `DockProfile::stream_pacing`.
+ stream_budget_bps: core::sync::atomic::AtomicU32,
+ /// Most that may leave back to back after an idle period; the credit ceiling.
+ stream_burst_bytes: core::sync::atomic::AtomicU32,
+ /// Unspent bytes of that budget, and when they were last topped up.
+ ///
+ /// One ledger for the whole dock rather than one per connector: what the budget describes is a
+ /// decoder behind a single endpoint, and two connectors sharing it spend from the same pool.
+ #[pin]
+ stream_credit: SpinLock<StreamCredit>,
+ /// How many downstream connectors this dock answers a presence probe for; see
+ /// `DockProfile::connectors`. Ridge: 2; Navarro: all four physical sockets.
+ connectors: core::sync::atomic::AtomicU8,
+ /// Excludes the independent keepalive loop while the mode worker emits the mode-relative
+ /// activation timeline. Without this, a keepalive poll can win `cp_link` between
+ /// two explicitly paced markers and stretch/reorder the sequence.
+ cp_timeline_exclusive: core::sync::atomic::AtomicBool,
+ /// Navarro's authenticated setup transcript continues directly into the first KMS
+ /// transaction: its first runtime message is a pipe clear, not a background status poll.
+ /// Hold the keepalive after publishing the session until that transaction has claimed the
+ /// control timeline.
+ initial_modeset_quiet: core::sync::atomic::AtomicBool,
+ /// Mode generation successfully programmed on each dock connector. Scanout must match it
+ /// because atomic plane updates can precede the deferred mode-set transaction.
+ modeset_active: [core::sync::atomic::AtomicU64; MAX_CONNECTORS],
+ /// Exact timing bytes most recently programmed on each connector.
+ ///
+ /// Kept apart from `modeset_active`: that atomic is the producer's request token, while
+ /// `dual_nivo` can be filled only after another connector on the endpoint publishes its own
+ /// request. No-op detection compares this exact dock-side state instead of pretending the
+ /// request token also describes a send-time topology correction.
+ #[pin]
+ programmed_timing: SpinLock<[Option<super::cp::Timing>; MAX_CONNECTORS]>,
+ /// Latest mode userspace currently requests per connector, encoded like `modeset_active`; zero
+ /// means the CRTC is disabled. The deferred worker uses this generation key to discard stale
+ /// mode-set commands and framebuffers left by a rapid disable/re-enable sequence.
+ modeset_requested: [core::sync::atomic::AtomicU64; MAX_CONNECTORS],
+ /// Whether a frame ending on a full packet is split; see
+ /// `DockProfile::split_full_packet_frame`.
+ split_full_packet_frame: AtomicBool,
+ /// Per-connector timestamp of the last accepted frame, used to bound scanout cadence.
+ #[pin]
+ last_frame: SpinLock<[Option<Instant<Monotonic>>; MAX_CONNECTORS]>,
+ /// When `queue_scanout` last ran for each connector, i.e. when KWin's commit tail last handed
+ /// us a framebuffer. Distinguishes "the compositor stopped committing" from "we dropped the
+ /// frame".
+ #[pin]
+ /// When the streaming status poll last went out, device-wide. The poll keeps the control
+ /// dialogue alive; it does not need to be per presentation.
+ #[pin]
+ last_status_poll: SpinLock<Option<Instant<Monotonic>>>,
+ /// When each connector's scanout work item last began executing.
+ #[pin]
+ /// Rate limiter for the stall diagnostic below.
+ #[pin]
+ /// Deadline for the sustained full-frame stream required to train a cold downstream link.
+ #[pin]
+ sustain_until: SpinLock<[Option<Instant<Monotonic>>; MAX_CONNECTORS]>,
+ /// Logical Haar frame sequence per connector.
+ #[pin]
+ scanout_seq: Mutex<[u32; MAX_CONNECTORS]>,
+ /// Persistent pipelined bulk-OUT queue per physical video endpoint. It remains live between
+ /// frames.
+ ///
+ /// The slot is the first connector whose endpoint address matches the caller's (see
+ /// [`UsbLink::video_pipe_index`](super::UsbLink::video_pipe_index)); duplicate slots remain
+ /// empty. Holding an individual slot mutex over a whole frame serializes connectors that share
+ /// a pipe without needlessly serializing independent endpoints.
+ #[pin]
+ video_q: [Mutex<Option<super::usb::BulkOutQueue>>; MAX_CONNECTORS],
+ /// Held by whoever is writing to a pipe that carries both planes; see [`Self::own_pipe`].
+ #[pin]
+ pipe_writer: Mutex<u8>,
+ /// One reusable 64-KiB coalescing window per connector. `frame_records` deliberately stores a
+ /// frame as small allocations so encoding never asks kmalloc for multi-megabyte physically
+ /// contiguous memory; scanout joins those fragments into this bounded window before
+ /// `BulkOutQueue::send` copies it into the persistent DMA ring. Internal record boundaries
+ /// remain invisible on USB.
+ #[pin]
+ video_staging: Mutex<[Option<KVec<u8>>; MAX_CONNECTORS]>,
+ /// Last requested timing, retained so scanout can retry a failed mode-set.
+ #[pin]
+ last_timing: SpinLock<[Option<super::cp::Timing>; MAX_CONNECTORS]>,
+ /// Heads whose next video stream must be prefixed with the pipe-arm records.
+ arm_prefix_pending: core::sync::atomic::AtomicU32,
+ /// Heads for which the read-only endpoint status at the first video stall was logged.
+ endpoint_status_logged: core::sync::atomic::AtomicU32,
+ /// Connectors still owed the short sealed open that names a stream vino does not drive.
+ ///
+ /// Held apart from `arm_prefix_pending` because it is the complement of it: a connector vino
+ /// is about to send pixels to opens its stream with the pipe descriptor instead, and both DLM
+ /// captures send this record only on the stream ids of the connectors left idle. The opens go
+ /// out before any connector's first frame, as DLM's do.
+ stream_open_pending: core::sync::atomic::AtomicU32,
+ /// Per-connector "owes a full keyframe" bitmask (bit `h` = connector `h`). Set (all connectors)
+ /// after a `KmsCmd::ModeSet` send: a new mode leaves the dock's framebuffer undefined, so the
+ /// first scanout after it must be a FULL frame ([`super::video::haar::colour_frame_ep08`]), not
+ /// a damage delta -- otherwise the un-redrawn strips stay garbage. Cleared for a connector once
+ /// its keyframe is sent; subsequent flips send only changed strips through
+ /// [`super::video::haar::colour_frame_ep08_damage`].
+ keyframe_pending: core::sync::atomic::AtomicU32,
+ /// Per-connector generation of the dock's cursor bitmap, bumped by [`Self::owe_keyframe`].
+ ///
+ /// The cursor plane re-uploads only when its bitmap differs from the last one sent, so it needs
+ /// to know when the dock stopped holding that bitmap. A mode-set discards it.
+ cursor_epoch: [core::sync::atomic::AtomicU32; MAX_CONNECTORS],
+ /// Rotates the shadow slot each commit so successive snapshots do not land in the same one.
+ shadow_rr: [core::sync::atomic::AtomicU32; MAX_CONNECTORS],
+ /// Geometry last announced with `cursor_create`, per connector. Whether the dock keeps one
+ /// shared cursor bitmap or one per connector is not established, so each connector announces
+ /// and uploads its own -- correct either way, at the cost of one extra upload per shape change.
+ #[pin]
+ cursor_geometry: Mutex<[Option<(u16, u16)>; MAX_CONNECTORS]>,
+ /// Heads whose next activation is a *repair* of a sink the dock dropped underneath us,
+ /// rather than a cold bring-up. A repair must not run the cold training window: the link
+ /// is already trained, and that window presents full keyframes at [`FRAME_PERIOD_MS`]
+ /// for three seconds -- measured at 1.07 GB over 12 seconds across two connectors, which is
+ /// the documented way to destabilise this dock.
+ repair_connectors: core::sync::atomic::AtomicU32,
+ /// The cursor each connector's connector is holding; see [`CursorShot`].
+ #[pin]
+ cursor_shot: Mutex<[Option<CursorShot>; MAX_CONNECTORS]>,
+ /// Dock-wide pixel-rate budget in pixels per second; zero means unknown.
+ dock_pixel_budget: core::sync::atomic::AtomicU32,
+ /// Highest refresh rate this dock is known to drive; see `DockProfile::max_refresh_hz`.
+ max_refresh_hz: core::sync::atomic::AtomicU32,
+ /// Highest per-mode pixel clock in kHz; see `DockProfile::max_connector_clock_khz`.
+ max_connector_clock_khz: core::sync::atomic::AtomicU32,
+ /// Excludes scanout while a mode-set batch can submit on a video endpoint. Paired with
+ /// `video_inflight` using sequentially consistent store-then-check handshakes.
+ cmd_busy: core::sync::atomic::AtomicBool,
+ /// Set around `run_pending_scanout`, allowing `cmd_work` to wait for a
+ /// frame already in flight when it set [`Self::cmd_busy`].
+ video_inflight: [core::sync::atomic::AtomicBool; MAX_CONNECTORS],
+ /// Consecutive failed live-scanout frames per connector, for log rate-limiting.
+ scanout_fails: [core::sync::atomic::AtomicU64; MAX_CONNECTORS],
+ /// Upcoming page flips to skip for per-connector transport backoff.
+ scanout_skip: [core::sync::atomic::AtomicU64; MAX_CONNECTORS],
+ /// Settle repaints this connector may still arm. See [`SETTLE_REPAINTS`].
+ settle_budget: [core::sync::atomic::AtomicU32; MAX_CONNECTORS],
+ /// Last inner status returned for each connector's presence probe.
+ presence_reply: [core::sync::atomic::AtomicU32; MAX_CONNECTORS],
+ /// Pending downstream-topology notification for this device's keepalive worker.
+ downstream_event: AtomicBool,
+ /// Head currently expecting an EDID from a re-engage, or [`NO_EDID_TARGET`].
+ ///
+ /// The EDID arrives as an `id=0x194` push, and during a re-engage it lands in `send_cp`'s
+ /// own lockstep drain rather than in `drain_cp_pushes`. This says "somebody is waiting for
+ /// one", so that drain can stash it instead of discarding it.
+ edid_target: core::sync::atomic::AtomicU32,
+ /// The blob that drain caught, handed back to [`VinoDrmData::reengage_connector`].
+ #[pin]
+ edid_caught: Mutex<Option<KVec<u8>>>,
+ /// Heads intentionally blanked by Vino. Their expected probe silence is not a hot-unplug.
+ self_blanked: core::sync::atomic::AtomicU32,
+ /// Heads whose blank bracket is still open on the dock, one bit each.
+ ///
+ /// Distinct from [`Self::self_blanked`], which `atomic_enable` clears on the commit thread
+ /// before the command worker runs; by then the wake choreography would no longer know a blank
+ /// was owed a close. A bracket left open keeps the sink dark through the next mode set.
+ blank_bracket_open: core::sync::atomic::AtomicU32,
+ /// Whether this dock's video pipeline can carry ten bits per channel, from its profile.
+ hdr_capable: AtomicBool,
+ /// Whether this dock composites a cursor bitmap of its own; see [`DockProfile::hw_cursor`].
+ hw_cursor: AtomicBool,
+ /// Whether the dock's presence probe describes a connector; see
+ /// [`DockProfile::reports_presence`].
+ reports_presence: AtomicBool,
+ /// Whether the connectors share one EDID handler; see [`DockProfile::shared_edid_handler`].
+ shared_edid_handler: AtomicBool,
+ /// Per-connector key and nonce used to seal pipe-arm records.
+ #[pin]
+ video_keys: Mutex<[kernel::crypto::Secret<32>; MAX_CONNECTORS]>,
+ /// When each connector last put a byte on its video endpoint.
+ ///
+ /// Drives the DL7400 keep-alive: see [`NAVARRO_VIDEO_QUIET_MS`] for why a connector that has
+ /// nothing to draw still has to say something.
+ #[pin]
+ last_video_at: SpinLock<[Option<Instant<Monotonic>>; MAX_CONNECTORS]>,
+ /// Per-connector AES-CTR block counter for the sealed records on that connector's video stream.
+ ///
+ /// Every sealed video record carries this counter in its wire `seq`, and `seal_livemac` uses
+ /// it both as the CTR block index and as the Dl3Cmac counter. It is stream state, not record
+ /// state: DLM advances it by `ceil(plaintext / 16)` for every sealed record it sends on a
+ /// stream and never rewinds it, so a re-arm continues the count rather than restarting. It is
+ /// reset only when new video keys arrive, because a fresh key is a fresh keystream.
+ video_seal_seq: [core::sync::atomic::AtomicU32; MAX_CONNECTORS],
+}
+
+impl VinoDrmData {
+ /// `hdr_capable`, `hw_cursor` and `connectors` come from the dock's profile and must be
+ /// supplied here rather than stored afterwards: `create_objects` runs inside
+ /// `drm::Registration::new_static`, which is *before* probe reaches the block that publishes
+ /// the rest of the profile. Set late, they were always false while the connectors and planes
+ /// were being built, so the ten-bit format and the three HDR properties were silently never
+ /// attached, and no dock could ever withhold its cursor plane.
+ ///
+ /// `connectors` decides how many connectors exist at all. A dock that advertises more
+ /// connectors than it has sockets offers userspace outputs that can never carry a monitor, and
+ /// a compositor that enables one makes the driver encode and transmit full frames to nothing.
+ pub(super) fn new(
+ io: Arc<super::usb::IoWindow>,
+ endpoints: super::Endpoints,
+ hdr_capable: bool,
+ hw_cursor: bool,
+ connectors: u8,
+ ) -> impl PinInit<Self, Error> {
+ try_pin_init!(Self {
+ io,
+ endpoints,
+ shutting_down: AtomicBool::new(false),
+ cp_link <- new_mutex!(Option::<CpLink>::None),
+ cp_last_reply <- new_spinlock!(Instant::<Monotonic>::now()),
+ cp_session_live: AtomicBool::new(false),
+ cp_reset_queued: AtomicBool::new(false),
+ cp_watchdog <- new_delayed_work!("vino::cp_watchdog"),
+ pending_kms <- new_mutex!(PendingKms::new()),
+ pending_scanout <- new_mutex!([const { None }; MAX_CONNECTORS]),
+ settle_repaint <- new_mutex!([const { None }; MAX_CONNECTORS]),
+ shadow <- pin_init::pin_init_array_from_fn(|_| new_mutex!(ShadowPool::new())),
+ vblank <- new_spinlock!([const { None }; MAX_CONNECTORS]),
+ cmd_work <- new_delayed_work!("vino::kms_cmd"),
+ scanout_work_h0 <- new_work!("vino::scanout_h0"),
+ scanout_work_h1 <- new_work!("vino::scanout_h1"),
+ scanout_work_h2 <- new_work!("vino::scanout_h2"),
+ scanout_work_h3 <- new_work!("vino::scanout_h3"),
+ session_queue: workqueue::Queue::new_ordered().build(kernel::c_str!("vino_session"))?,
+ // High priority: this queue carries cursor movement, and its work items are a few
+ // small control messages. Left at default priority they queue behind whatever else
+ // the machine is doing, and the pointer visibly stutters under load.
+ kms_queue: workqueue::Queue::new_ordered()
+ .highpri()
+ .build(kernel::c_str!("vino_kms"))?,
+ scanout_queue: workqueue::Queue::new_unbound()
+ .max_active(MAX_CONNECTORS as u32)
+ .build(kernel::c_str!("vino_scanout"))?,
+ cached_edids <- new_mutex!([const { None }; MAX_CONNECTORS]),
+ connectors_present: core::sync::atomic::AtomicU32::new(0),
+ color <- new_mutex!([None; MAX_CONNECTORS]),
+ strip_hashes <- new_mutex!([const { None }; MAX_CONNECTORS]),
+ strip_classes <- new_mutex!(core::array::from_fn(|_| KVec::new())),
+ dirty_ttl <- new_mutex!([const { None }; MAX_CONNECTORS]),
+ cp_engaged: core::sync::atomic::AtomicBool::new(false),
+ kms_activation_ready: core::sync::atomic::AtomicBool::new(false),
+ cp_timeline_exclusive: core::sync::atomic::AtomicBool::new(false),
+ initial_modeset_quiet: core::sync::atomic::AtomicBool::new(false),
+ modeset_active: core::array::from_fn(|_| core::sync::atomic::AtomicU64::new(0)),
+ programmed_timing <- new_spinlock!([None; MAX_CONNECTORS]),
+ modeset_requested: core::array::from_fn(|_| core::sync::atomic::AtomicU64::new(0)),
+ split_full_packet_frame: AtomicBool::new(false),
+ last_frame <- new_spinlock!([const { None }; MAX_CONNECTORS]),
+ last_status_poll <- new_spinlock!(None),
+ sustain_until <- new_spinlock!([const { None }; MAX_CONNECTORS]),
+ scanout_seq <- new_mutex!([0; MAX_CONNECTORS]),
+ video_q <- pin_init::pin_init_array_from_fn(|_| new_mutex!(None)),
+ pipe_writer <- new_mutex!(0u8),
+ video_staging <- new_mutex!([const { None }; MAX_CONNECTORS]),
+ last_timing <- new_spinlock!([None; MAX_CONNECTORS]),
+ arm_prefix_pending: core::sync::atomic::AtomicU32::new(0),
+ endpoint_status_logged: core::sync::atomic::AtomicU32::new(0),
+ stream_open_pending: core::sync::atomic::AtomicU32::new(0),
+ keyframe_pending: core::sync::atomic::AtomicU32::new(0),
+ cursor_epoch: core::array::from_fn(|_| core::sync::atomic::AtomicU32::new(0)),
+ shadow_rr: core::array::from_fn(|_| core::sync::atomic::AtomicU32::new(0)),
+ cursor_geometry <- new_mutex!([None; MAX_CONNECTORS]),
+ repair_connectors: core::sync::atomic::AtomicU32::new(0),
+ cursor_shot <- new_mutex!([const { None }; MAX_CONNECTORS]),
+ // D6000 default: 442,368,000 px/s (one 1440p@120) x2 compression headroom = dual
+ // 1440p@120. Replace it if a dock capability supplies a limit.
+ dock_pixel_budget: core::sync::atomic::AtomicU32::new(884_736_000),
+ max_refresh_hz: core::sync::atomic::AtomicU32::new(DEFAULT_MAX_REFRESH_HZ),
+ max_connector_clock_khz: core::sync::atomic::AtomicU32::new(DEFAULT_MAX_HEAD_CLOCK_KHZ),
+ cmd_busy: core::sync::atomic::AtomicBool::new(false),
+ video_inflight: core::array::from_fn(|_| core::sync::atomic::AtomicBool::new(false)),
+ scanout_fails: core::array::from_fn(|_| core::sync::atomic::AtomicU64::new(0)),
+ scanout_skip: core::array::from_fn(|_| core::sync::atomic::AtomicU64::new(0)),
+ settle_budget: core::array::from_fn(|_| core::sync::atomic::AtomicU32::new(0)),
+ presence_reply: core::array::from_fn(|_| core::sync::atomic::AtomicU32::new(0)),
+ downstream_event: AtomicBool::new(false),
+ edid_target: core::sync::atomic::AtomicU32::new(NO_EDID_TARGET),
+ edid_caught <- new_mutex!(None),
+ self_blanked: core::sync::atomic::AtomicU32::new(0),
+ blank_bracket_open: core::sync::atomic::AtomicU32::new(0),
+ hdr_capable: AtomicBool::new(hdr_capable),
+ hw_cursor: AtomicBool::new(hw_cursor),
+ reports_presence: AtomicBool::new(true),
+ shared_edid_handler: AtomicBool::new(false),
+ codec_geometry: core::sync::atomic::AtomicU32::new(0),
+ // Ridge-compatible defaults until probe publishes the matched profile.
+ frame_delivery: core::sync::atomic::AtomicU32::new(2 | (1 << 8) | (3 << 16)),
+ probe_bracket: core::sync::atomic::AtomicU8::new(
+ super::profile::ProbeBracket::Always as u8
+ ),
+ connector_ten_bit: core::sync::atomic::AtomicU32::new(0),
+ connector_max_bpc: core::sync::atomic::AtomicU32::new(0),
+ connector_deny_ten_bit: core::sync::atomic::AtomicU32::new(0),
+ head_st2084: core::sync::atomic::AtomicU32::new(0),
+ // A dock that names no connector count still has to expose something, so fall back to
+ // the maximum rather than building a card with no connectors at all.
+ connectors: core::sync::atomic::AtomicU8::new(if connectors == 0 {
+ MAX_CONNECTORS as u8
+ } else {
+ connectors.min(MAX_CONNECTORS as u8)
+ }),
+ dock_wide_modeset: core::sync::atomic::AtomicBool::new(false),
+ clear_mode_before_set: core::sync::atomic::AtomicBool::new(false),
+ blank_markers_held: core::sync::atomic::AtomicBool::new(false),
+ video_keepalive: core::sync::atomic::AtomicBool::new(false),
+ arm_burst: core::sync::atomic::AtomicBool::new(true),
+ allocation: kernel::sync::SetOnce::new(),
+ video_on_ctrl_pipe: core::sync::atomic::AtomicBool::new(false),
+ sink_down_state: core::sync::atomic::AtomicU8::new(BLANK_MARKER_STATE),
+ post_mode_sink_states: core::sync::atomic::AtomicU16::new(0x0303),
+ pre_mode_sink_state: core::sync::atomic::AtomicU16::new(u16::MAX),
+ stream_opened: core::sync::atomic::AtomicU32::new(0),
+ stream_reports_owed: core::array::from_fn(|_| core::sync::atomic::AtomicU32::new(0)),
+ kms_retries: core::sync::atomic::AtomicU32::new(0),
+ video_stream_desc: core::sync::atomic::AtomicU32::new(0),
+ frame_period_us: core::sync::atomic::AtomicI64::new(FRAME_PERIOD_US),
+ status_period_ms: core::sync::atomic::AtomicI64::new(STATUS_PERIOD_MS),
+ carrier_frames: core::sync::atomic::AtomicU32::new(u32::MAX),
+ stream_budget_bps: core::sync::atomic::AtomicU32::new(u32::MAX),
+ stream_burst_bytes: core::sync::atomic::AtomicU32::new(u32::MAX),
+ stream_credit <- new_spinlock!(StreamCredit::new()),
+ video_keys <- new_mutex!(core::array::from_fn(
+ |_| kernel::crypto::Secret::zeroed()
+ )),
+ last_video_at <- new_spinlock!([None; MAX_CONNECTORS]),
+ video_seal_seq: core::array::from_fn(
+ |_| core::sync::atomic::AtomicU32::new(0)
+ ),
+ })
+ }
+
+ /// Publish the producers' stop flag and nothing else.
+ ///
+ /// `disconnect()` calls this *before* `IoWindow::close()`. The scanout and command workers each
+ /// hold an `Io` token for as long as they loop and re-read `shutting_down` every iteration, so
+ /// setting it early is what keeps them from holding `close()`'s wait open. Everything in
+ /// [`shutdown`](Self::shutdown) proper must wait until USB I/O is quiesced; this must not.
+ pub(super) fn begin_shutdown(&self) {
+ self.shutting_down.store(true, Ordering::Release);
+ self.kms_activation_ready.store(false, Ordering::Release);
+ self.cp_timeline_exclusive.store(false, Ordering::Release);
+ self.initial_modeset_quiet.store(false, Ordering::Release);
+ }
+
+ /// Stand every producer down because the device is about to be reset.
+ ///
+ /// A reset takes the whole session with it: the dock forgets its content-protection keys, its
+ /// open streams and the sinks it was driving, and nothing this driver holds describes the
+ /// device on the other side of one. So the link is marked gone before the reset rather than
+ /// after, which is what stops a worker submitting a transfer across it.
+ pub(super) fn stop_for_reset(&self) {
+ self.cp_session_live.store(false, Ordering::Release);
+ self.begin_shutdown();
+ }
+
+ /// Whether the parent interface is being removed.
+ pub(super) fn is_shutting_down(&self) -> bool {
+ self.shutting_down.load(Ordering::Acquire)
+ }
+
+ /// Queue used by the session bring-up and keepalive work item.
+ pub(super) fn session_queue(&self) -> &workqueue::Queue {
+ &self.session_queue
+ }
+
+ /// Take this device's pending downstream-topology notification.
+ pub(super) fn take_downstream_event(&self) -> bool {
+ self.downstream_event.swap(false, Ordering::Acquire)
+ }
+
+ /// Stop deferred DRM work while the parent USB interface is still bound. `cmd_work` is
+ /// embedded in this DRM device and each successful enqueue temporarily owns an
+ /// `ARef<VinoDrmDevice>`; pending scanouts also retain compositor framebuffers. Quiesce both
+ /// producers, reclaim any queued work pointer, and drop those framebuffers before the final
+ /// device references disappear during devres teardown.
+ pub(super) fn shutdown(&self) {
+ // Idempotent, and `disconnect()` has normally already done this: see `begin_shutdown`.
+ self.begin_shutdown();
+ for mode in &self.modeset_requested {
+ mode.store(0, Ordering::Release);
+ }
+
+ // Stop the software vblank clocks before releasing their CRTC references. Take the
+ // registry out from under the spinlock before dropping the handles:
+ // Dropping `ArcHrTimerHandle` waits for a running callback and must
+ // not happen in atomic context.
+ let timers = {
+ let mut slots = self.vblank.lock();
+ core::mem::replace(&mut *slots, [const { None }; MAX_CONNECTORS])
+ };
+ // Clear `enabled` before cancelling so a callback racing the cancel returns `NoRestart`
+ // instead of re-arming behind it.
+ for (timer, _) in timers.iter().flatten() {
+ timer.enabled.store(false, Ordering::Relaxed);
+ }
+ // Split the registry: drop every `ArcHrTimerHandle` (each drop == `hrtimer_cancel`, which
+ // waits for a running callback), but keep the `Arc<VblankTimer>`s alive so the published
+ // CRTC handles can be released below. From here on no vblank callback can run or be
+ // re-armed, because `VinoCrtc::vblank` is only reachable through a CRTC of this device and
+ // every producer is already refusing work.
+ let timers = timers.map(|slot| {
+ slot.map(|(timer, handle)| {
+ drop(handle);
+ timer
+ })
+ });
+
+ // Break the two device-to-itself reference cycles. Both run through a `crtc::CrtcRef`,
+ // which owns an `ARef<VinoDrmDevice>`:
+ //
+ // 1. `VblankTimer::crtc`, published by the first `enable_vblank` and never released. The
+ // timer is owned by `VinoCrtc`, which lives inside the DRM device allocation.
+ // 2. `VinoCrtc::vblank_pinned`, the driver-held vblank reference. Teardown cannot rely on
+ // `atomic_disable` running before unplug.
+ //
+ // Safe to do here even though these were the last self-references: `shutdown()`'s only
+ // caller is `VinoDriver::disconnect`, which reaches it through the `drm::Registration`
+ // still held in the bound data -- and that owns an `ARef<VinoDrmDevice>` of its own -- so
+ // `&self` outlives this function regardless of what is dropped below. The taken values
+ // are dropped outside both locks: `drm_dev_put()` can end in `drm_dev_release()` and
+ // `drm_crtc_vblank_put()` takes the DRM vblank locks, neither of which may run under our
+ // spinlock.
+ for timer in timers.iter().flatten() {
+ let published = timer.crtc.lock().take();
+ if let Some(crtc_ref) = published {
+ // The software vblank clock has just stopped, and a page flip armed by
+ // `atomic_flush` is waiting on a tick that will never come. `drm_crtc_vblank_off()`
+ // both refuses further vblank references -- so `drm_atomic_helper_wait_for_vblanks`
+ // skips this CRTC instead of warning -- and sends every event still queued on the
+ // device's vblank list, which is exactly where `PendingVblankEvent::arm` put ours.
+ //
+ // Without it an unplug left the compositor's `commit_tail` blocked until DRM's own
+ // deadlines expired: this boot logged 73 `vblank wait timed out` warnings and 110
+ // pairs of `flip_done timed out` / `commit wait timed out`, ten seconds each, on
+ // top of every dock reset. That is most of the delay between a dock coming back and
+ // pixels reappearing.
+ crtc_ref.crtc().vblank_off();
+ let crtc: &VinoCrtc = crtc_ref.crtc();
+ drop(crtc.vblank_pinned.lock().take());
+ drop(crtc_ref);
+ }
+ }
+ drop(timers);
+
+ *self.pending_kms.lock() = PendingKms::new();
+ *self.pending_scanout.lock() = [const { None }; MAX_CONNECTORS];
+ *self.settle_repaint.lock() = [const { None }; MAX_CONNECTORS];
+ for h in 0..MAX_CONNECTORS {
+ self.shadow[h].lock().discard();
+ }
+ *self.strip_hashes.lock() = [const { None }; MAX_CONNECTORS];
+ *self.dirty_ttl.lock() = [const { None }; MAX_CONNECTORS];
+ // Cancel the queued drain and reclaim the `ARef<VinoDrmDevice>` the enqueue handed to
+ // the workqueue, if it was still pending. Dropping it here releases the self-reference
+ // that would otherwise keep this device alive until the work ran.
+ //
+ // Cancel `cmd_work` first because it can enqueue both scanout workers. `shutting_down` is
+ // already visible to all workers, so cancellation only waits for work already in flight.
+ drop(self.cmd_work.cancel_sync());
+ drop(self.cp_watchdog.cancel_sync());
+ drop(self.scanout_work_h0.cancel_sync());
+ drop(self.scanout_work_h1.cancel_sync());
+ drop(self.scanout_work_h2.cancel_sync());
+ drop(self.scanout_work_h3.cancel_sync());
+
+ // A running callback may have taken a batch just before shutdown was published. It has
+ // finished now; clear anything it left behind and tear the USB queues down while their
+ // parent interface is still in Bound context.
+ *self.pending_kms.lock() = PendingKms::new();
+ *self.pending_scanout.lock() = [const { None }; MAX_CONNECTORS];
+ *self.settle_repaint.lock() = [const { None }; MAX_CONNECTORS];
+ for h in 0..MAX_CONNECTORS {
+ self.shadow[h].lock().discard();
+ }
+ *self.strip_hashes.lock() = [const { None }; MAX_CONNECTORS];
+ *self.dirty_ttl.lock() = [const { None }; MAX_CONNECTORS];
+ for queue in &self.video_q {
+ *queue.lock() = None;
+ }
+ *self.video_staging.lock() = [const { None }; MAX_CONNECTORS];
+ self.cp_session_live.store(false, Ordering::Release);
+ *self.cp_link.lock() = None;
+ vino_debug!("vino: deferred KMS/video work drained for unplug\n");
+ }
+
+ /// Cache `connector`'s CRTC colour transform (from `RawCrtcState::gamma_lut` and
+ /// `RawCrtcState::ctm`) for the scanout to apply, or clear it to identity with two `None`s.
+ pub(super) fn update_color(
+ &self,
+ connector: usize,
+ lut: Option<&[crtc::ColorLut]>,
+ ctm: Option<&crtc::ColorCtm>,
+ ) {
+ let socket = connector + 1;
+ let cached = super::color::ColorPipeline::build(lut, ctm);
+ let changed = if let Some(slot) = self.color.lock().get_mut(connector) {
+ if *slot == cached {
+ false
+ } else {
+ *slot = cached;
+ true
+ }
+ } else {
+ false
+ };
+ if changed {
+ if cached.is_some() {
+ vino_debug!("vino: socket {socket} colour transform updated\n");
+ } else {
+ vino_debug!("vino: socket {socket} colour transform cleared\n");
+ }
+ // The encoded-strip cache keys on a strip's source pixels, so a transform change that
+ // leaves those pixels untouched would otherwise re-send stale bodies for the whole
+ // screen. Drop the cache and owe a keyframe.
+ self.strip_hashes.lock()[connector] = None;
+ self.dirty_ttl.lock()[connector] = None;
+ self.owe_keyframe(connector);
+ }
+ }
+
+ /// Snapshot `connector`'s cached colour transform for a scanout pass (`Copy`, so no lock is
+ /// held afterwards).
+ pub(super) fn color_snapshot(&self, connector: usize) -> Option<super::color::ColorPipeline> {
+ self.color.lock().get(connector).copied().flatten()
+ }
+
+ /// Number of physical connectors selected by the matched dock profile.
+ pub(super) fn connector_count(&self) -> usize {
+ usize::from(self.connectors.load(Ordering::Acquire)).min(MAX_CONNECTORS)
+ }
+
+ /// Whether `connector` represents a distinct runtime stream on this dock.
+ ///
+ /// Every physical connector does, including both halves of a shared endpoint: the DL7400 maps
+ /// its four connectors in pairs onto two video bulk endpoints, and treating the second of each
+ /// pair as an alias would make a monitor in socket 3 or 4 invisible. Sharing an endpoint is a
+ /// transport detail, handled where it belongs by [`UsbLink::video_pipe_index`], which gives
+ /// both connectors of a pair the same persistent queue.
+ ///
+ /// Empty sockets cost nothing here: the presence probe answers negative for them, and the
+ /// keepalive's re-engage retry stands down permanently once it has.
+ pub(super) fn runtime_connector(&self, connector: usize) -> bool {
+ connector < self.connector_count()
+ }
+
+ /// Whether bring-up has reached the point where KMS may touch the dock.
+ pub(super) fn kms_activation_ready(&self) -> bool {
+ self.kms_activation_ready.load(Ordering::Acquire)
+ }
+
+ /// Take every connector down once the control session has been abandoned.
+ ///
+ /// Userspace can move its windows off a connector that has disappeared, but not off one that
+ /// is merely frozen. Recovery is a replug, which rebinds and starts a fresh session.
+ pub(super) fn drop_connectors_with_session(&self, drm_dev: &VinoDrmDevice) {
+ let mut dropped = false;
+ for connector in 0..self.connector_count() {
+ if !self.runtime_connector(connector) || !self.connector_present(connector) {
+ continue;
+ }
+ self.set_disconnected(connector);
+ dropped = true;
+ pr_warn!(
+ "vino: socket {socket} dropped with the control session\n",
+ socket = connector + 1
+ );
+ }
+ if dropped {
+ drm_dev.hotplug_event();
+ }
+ }
+
+ /// Take the shared pipe for one indivisible sequence of writes.
+ ///
+ /// A record is never split: the vendor's control records sit between records, never inside
+ /// one. On a dock with a video pipe of its own that is free -- the two planes cannot collide.
+ /// Here they share an endpoint, and a control write submitted between two of a frame's URBs
+ /// lands in the middle of an image record, where it desynchronises the dock's parser for the
+ /// rest of the frame. The dock accepts every byte and shows nothing, which is indistinguishable
+ /// from a dead sink.
+ ///
+ /// Returns `None` on a dock whose planes have separate endpoints, where the exclusion would
+ /// only cost the control plane latency.
+ ///
+ /// Lock order is `cp_link` then this then `video_q`. Nothing may send a control message while
+ /// holding it.
+ pub(super) fn own_pipe(
+ &self,
+ ) -> Option<kernel::sync::lock::Guard<'_, u8, kernel::sync::lock::mutex::MutexBackend>> {
+ self.video_on_ctrl_pipe().then(|| self.pipe_writer.lock())
+ }
+
+ /// Retire every outstanding URB after a physical video queue reports an error.
+ ///
+ /// The caller must still own this endpoint's `own_pipe()` guard (when present) and its
+ /// canonical `video_q` slot. Taking and explicitly dropping the complete queue synchronously
+ /// kills every submitted URB before a stalled endpoint is cleared. In particular, no queued
+ /// frame tail may resume after `usb_clear_halt()` without the prefix that made it parseable.
+ /// Keeping the canonical slot empty also makes the next writer create a fresh queue on a
+ /// dedicated video endpoint.
+ ///
+ /// A shared control/video pipe cannot resume locally. Submission advances the host's frame
+ /// and ring counters before asynchronous URBs complete, so cancellation can leave the dock
+ /// expecting an earlier frame than Vino. There is no independent endpoint to re-arm and no
+ /// safe counter-only rewind; abandon the complete session and let USB reset establish a new
+ /// one instead. Dedicated video endpoints retain their local drain/clear recovery.
+ pub(super) fn retire_failed_video_queue(
+ &self,
+ dev: &BoundInterface<'_>,
+ connector: usize,
+ queue_slot: &mut Option<super::usb::BulkOutQueue>,
+ cause: Error,
+ clear_halt: bool,
+ ) -> Result {
+ let doomed = queue_slot.take();
+ drop(doomed);
+
+ vino_debug!(
+ "vino: connector={} retired failed physical video queue ({:?})\n",
+ connector,
+ cause
+ );
+ if self.video_on_ctrl_pipe()
+ && (cause == kernel::error::code::EPIPE
+ || cause == kernel::error::code::EPROTO
+ || cause == kernel::error::code::ETIMEDOUT)
+ {
+ // Publish the terminal state before invalidating connectors or requesting reset. A CP
+ // writer already queued behind `own_pipe()` rechecks this flag after it acquires the
+ // pipe, and a scanout writer may not recreate the canonical queue once it is false.
+ if self
+ .cp_session_live
+ .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok()
+ {
+ pr_warn!(
+ "vino: shared video/control pipe failed ({:?}); abandoning the session\n",
+ cause
+ );
+ }
+ // This device instance is terminal even though USB reset is asynchronous. Close the
+ // producer gate as well as the transport gate so KMS callbacks may coalesce their
+ // latest state, but cannot enqueue another activation in the reset window. The fresh
+ // probe owns a new VinoDrmData and publishes its own readiness after setup completes.
+ self.kms_activation_ready.store(false, Ordering::Release);
+ let mut programmed = self.programmed_timing.lock();
+ for h in 0..self.connector_count() {
+ self.modeset_active[h].store(0, Ordering::Release);
+ programmed[h] = None;
+ }
+ drop(programmed);
+ self.reset_after_wedge();
+ return Ok(());
+ }
+ if clear_halt
+ && (cause == kernel::error::code::EPIPE || cause == kernel::error::code::EPROTO)
+ {
+ dev.clear_video_halt(connector)?;
+ pr_info!(
+ "vino: connector {} video queue drained and endpoint halt cleared\n",
+ connector
+ );
+ }
+ Ok(())
+ }
+
+ /// Keep Navarro's setup-to-first-mode-set control stream free of background traffic.
+ pub(super) fn hold_cp_for_initial_modeset(&self) {
+ self.initial_modeset_quiet.store(true, Ordering::Release);
+ }
+
+ /// Whether the initial Navarro mode set still owns the next control message.
+ pub(super) fn initial_modeset_quiet(&self) -> bool {
+ self.initial_modeset_quiet.load(Ordering::Acquire)
+ }
+
+ /// Release the initial hold if userspace never submits a mode set.
+ pub(super) fn release_initial_modeset_quiet(&self) {
+ self.initial_modeset_quiet.store(false, Ordering::Release);
+ }
+
+ /// Store the per-connector video keys produced by the `id=0x32` exchange.
+ ///
+ /// Called with [`publish_session`](Self::publish_session) when CP engages. `opened` is the
+ /// connector bitmask whose streams the setup burst opened; each of those consumed block zero of
+ /// its stream, so its chain continues at block one. A connector not in the mask had no sink at
+ /// setup time and still owes its open, so its chain must start at zero -- sealing at one leaves
+ /// a gap the dock accounts for as a keystream it never received, and it discards the record
+ /// with nothing on the wire to say so.
+ pub(super) fn set_video_keys(
+ &self,
+ keys: [kernel::crypto::Secret<32>; MAX_CONNECTORS],
+ opened: u32,
+ ) {
+ *self.video_keys.lock() = keys;
+ self.stream_opened.store(opened, Ordering::Release);
+ // A new key is a new keystream, so the block counters start over with it.
+ for (connector, seq) in self.video_seal_seq.iter().enumerate() {
+ let used = u32::from(opened & (1u32 << connector) != 0);
+ seq.store(used, Ordering::Release);
+ }
+ }
+
+ /// Make a connector owe the records that open its stream, ahead of its next frame.
+ ///
+ /// On a dock that shares its control pipe the prologue restarts the stream, and the dock's
+ /// frame counter with it: DLM's next opener names ring slot 0 and frame 1 whatever the
+ /// connector had reached before. Carrying the old count over hands the dock a slot it is still
+ /// scanning out. A dock with a video pipe of its own has no such restart, and keeps counting.
+ fn arm_stream_prologue(&self, connector: usize) {
+ self.arm_prefix_pending
+ .fetch_or(1u32 << connector, Ordering::Release);
+ if self.video_on_ctrl_pipe() && connector < MAX_CONNECTORS {
+ self.scanout_seq.lock()[connector] = 0;
+ self.stream_reports_owed[connector].store(STREAM_REPORT_BURST, Ordering::Release);
+ }
+ }
+
+ /// Reserve `blocks` AES-CTR blocks on a connector's video stream and return the counter to seal
+ /// at.
+ ///
+ /// Sealed video records must tile the stream's keystream without gaps or overlaps: the dock
+ /// tracks the same counter, and a record that repeats a block a previous record already used is
+ /// a replay of that keystream. Reserving before sealing keeps that true no matter how the
+ /// records are grouped into transfers.
+ fn take_seal_seq(&self, connector: usize, blocks: u32) -> u32 {
+ self.video_seal_seq[connector].fetch_add(blocks, Ordering::AcqRel)
+ }
+
+ /// Status polls issued immediately before the first video presentation of a mode generation.
+ ///
+ /// Captured sequences interleave two status messages here and begin video while the stream
+ /// bracket is still active. The longer downstream training interval follows the bracket.
+ const PREWRITE_POLLS: u32 = 2;
+ const PREWRITE_POLL_MS: u64 = 1;
+ /// Send one `id=0x14 sub=0x000c` device-status poll.
+ fn poll_status(&self, dev: &BoundInterface<'_>) -> Result {
+ self.send_cp(dev, 0x14, 0, |ctr| super::cp::device_query_req(ctr, 0x000c))
+ }
+
+ /// One `id=0x16 sub=0x2e|0x2f` stream/display marker. State lives in byte 23, not byte 22
+ /// (byte 22 is constantly `1` -- reading it makes every marker look like state=1).
+ fn stream_marker(&self, dev: &BoundInterface<'_>, connector: u8, sub: u16, st: u8) -> Result {
+ self.send_cp(dev, 0x16, 0, |ctr| {
+ super::cp::stream_marker(ctr, connector, sub, st)
+ })
+ }
+
+ /// Send one captured Navarro sink-reset operation.
+ fn navarro_cold_op(&self, dev: &BoundInterface<'_>, op: NavarroColdOp) -> Result {
+ match op {
+ NavarroColdOp::Poll => self.poll_status(dev),
+ NavarroColdOp::EdidState(connector, state) => self.send_cp(dev, 0x16, 0, |ctr| {
+ super::cp::edid_readiness_state(ctr, connector, state)
+ }),
+ NavarroColdOp::Probe(connector) => self.send_cp(dev, 0x15, 0, |ctr| {
+ super::cp::get_edid_req_sub(ctr, 0x20, connector)
+ }),
+ NavarroColdOp::Fetch(connector) => {
+ self.send_cp(dev, 0x15, 0, |ctr| super::cp::get_edid_req(ctr, connector))
+ }
+ NavarroColdOp::SinkTeardown(connector) => self.send_cp(dev, 0x16, 0, |ctr| {
+ super::cp::edid_sink_state(ctr, connector, 0xff)
+ }),
+ NavarroColdOp::Engage(connector) => self.send_cp(dev, 0x16, 0, |ctr| {
+ super::cp::edid_engage_req(ctr, connector)
+ }),
+ NavarroColdOp::PostEdid(connector) => self.send_cp(dev, 0x15, 0, |ctr| {
+ super::cp::post_edid_query(ctr, connector)
+ }),
+ NavarroColdOp::Clear(connector) => {
+ self.send_cp(dev, 0x48, 0, |ctr| super::cp::clear_mode(ctr, connector))
+ }
+ }
+ }
+
+ /// Whether another connector on `connector`'s video endpoint is also being driven.
+ ///
+ /// `0x08` owns connectors {0, 2} and `0x0a` owns {1, 3}, so a connector's partner is the one
+ /// two away. Such a pair must declare `Dual NIVO` in both mode sets or the dock drives only one
+ /// of the two streams it is sent, however correctly they are tagged.
+ pub(super) fn endpoint_is_shared(&self, connector: usize) -> bool {
+ self.endpoint_is_shared_in_mask(connector, self.requested_connector_mask())
+ }
+
+ /// Requested connectors as one topology snapshot.
+ fn requested_connector_mask(&self) -> u32 {
+ self.modeset_requested
+ .iter()
+ .enumerate()
+ .fold(0u32, |mask, (connector, requested)| {
+ mask | (u32::from(requested.load(Ordering::Acquire) != 0) << connector)
+ })
+ }
+
+ /// Whether `connector` shares its endpoint with another requested connector in `mask`.
+ fn endpoint_is_shared_in_mask(&self, connector: usize, mask: u32) -> bool {
+ if connector >= MAX_CONNECTORS || self.connector_count() <= 2 {
+ return false;
+ }
+ let partner = connector ^ 2;
+ partner < MAX_CONNECTORS && mask & (1u32 << partner) != 0
+ }
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_sink)]
+mod tests {
+ use super::*;
+ use crate::*;
+
+ #[test]
+ fn only_a_presentation_that_names_a_ring_slot_advances_the_frame_counter() -> Result {
+ // The frame counter belongs to the ring, and every generation names the ring in the
+ // record that closes a frame. A presentation carrying neither an opener nor a trailer says
+ // nothing about the ring and must not consume a slot, or every later record names a buffer
+ // one ahead of the one the host filled.
+ assert!(!names_ring_slot(&[], &video::haar::FrameTrailer::none()));
+ let ella = profile::PROFILE_ELLA.geometry();
+ assert!(names_ring_slot(
+ &[],
+ &video::haar::FrameTrailer::one(&video::haar::ella_frame_close(ella, 0, 0))
+ ));
+
+ // Both other generations close every frame, so every presentation advances the counter and
+ // this rule leaves them exactly as they were.
+ let ridge = profile::PROFILE_RIDGE.geometry();
+ assert!(names_ring_slot(
+ &[],
+ &video::haar::frame_trailer(ridge, 0, 0)
+ ));
+ let navarro = profile::PROFILE_NAVARRO.geometry();
+ assert!(names_ring_slot(
+ &[],
+ &video::haar::navarro_frame_trailer(navarro, 0, 0)
+ ));
+ Ok(())
+ }
+
+ /// The DL7400 parameter map goes among a frame's records, not after all of them.
+ ///
+ /// The dock reads the records around the map with what the map says, and takes a frame that
+ /// carries it after every record it describes twice before it stops draining the endpoint
+ /// altogether. The split lands on a chunk because that is a record boundary; the vendor's byte
+ /// offset on its own is wherever a frame's record lengths put it.
+ #[test]
+ fn param_map_lands_among_a_frame_s_records() -> Result {
+ let chunk = |len: usize| -> Result<KVec<u8>> {
+ let mut c = KVec::new();
+ c.resize(len, 0, GFP_KERNEL)?;
+ Ok(c)
+ };
+
+ // A frame of even chunks: the split is the last chunk that fits under the vendor's offset,
+ // and leaves the rest of the frame behind the map.
+ let mut even: KVec<KVec<u8>> = KVec::new();
+ for _ in 0..27 {
+ even.push(chunk(16_000)?, GFP_KERNEL)?;
+ }
+ let split = param_map_chunk_split(&even);
+ assert_eq!(split, 7);
+ assert!(split * 16_000 <= NAVARRO_PARAM_IMAGE_OFFSET);
+ assert!((split + 1) * 16_000 > NAVARRO_PARAM_IMAGE_OFFSET);
+
+ // A frame smaller than the offset still puts records in front of the map, and never names
+ // a chunk it does not have.
+ let mut small: KVec<KVec<u8>> = KVec::new();
+ small.push(chunk(4_000)?, GFP_KERNEL)?;
+ small.push(chunk(4_000)?, GFP_KERNEL)?;
+ assert_eq!(param_map_chunk_split(&small), 2);
+
+ // A single chunk larger than the offset cannot be split, and the map goes behind it rather
+ // than in front of every record in the frame.
+ let mut one: KVec<KVec<u8>> = KVec::new();
+ one.push(chunk(NAVARRO_PARAM_IMAGE_OFFSET * 2)?, GFP_KERNEL)?;
+ assert_eq!(param_map_chunk_split(&one), 1);
+ Ok(())
+ }
+
+ #[test]
+ fn frame_delivery_is_profile_data_not_ring_geometry() {
+ let ridge = profile::PROFILE_RIDGE.protocol.frame_delivery;
+ let navarro = profile::PROFILE_NAVARRO.protocol.frame_delivery;
+ let ella = profile::PROFILE_ELLA.protocol.frame_delivery;
+
+ // Preserve both established dedicated-pipe families exactly.
+ assert_eq!(ridge.keyframe_presentations, 2);
+ assert_eq!(ridge.delta_presentations, 1);
+ assert_eq!(ridge.damage_frames, 3);
+ assert_eq!(navarro.keyframe_presentations, 3);
+ assert_eq!(navarro.delta_presentations, 1);
+ assert_eq!(navarro.damage_frames, 4);
+ for (policy, keys, deltas) in [(ridge, 2, 1), (navarro, 3, 1)] {
+ assert_eq!(frame_presentation_count(policy, true, false, false), keys);
+ assert_eq!(
+ frame_presentation_count(policy, false, false, false),
+ deltas
+ );
+ }
+
+ // Ella still initialises all three buffers, but DLM carries one ordinary presentation per
+ // logical frame. Later debt frames walk the ring without multiplying each update in place.
+ assert_eq!(ella.keyframe_presentations, 3);
+ assert_eq!(ella.delta_presentations, 1);
+ assert_eq!(ella.damage_frames, 3);
+ assert_eq!(frame_presentation_count(ella, true, false, true), 3);
+ assert_eq!(frame_presentation_count(ella, false, false, true), 1);
+
+ // Dedicated endpoints retain their bounded cold-training burst. A shared control pipe
+ // uses its profile keyframe count instead, so it never receives eight multi-megabyte
+ // copies back to back.
+ assert_eq!(
+ frame_presentation_count(ridge, true, true, false),
+ drm_sink::COLD_TRAINING_PRESENTATIONS
+ );
+ assert_eq!(frame_presentation_count(ridge, false, true, false), 1);
+ assert_eq!(frame_presentation_count(ella, true, true, true), 3);
+
+ // `damage_frames` includes the first accepted submission. Ella therefore leaves exactly
+ // two scheduled debt submissions after the changed frame, covering slots 0, 1 and 2 once.
+ let mut debt = [ella.damage_frames, 1, 0];
+ pay_damage_debt(&mut debt, false);
+ assert_eq!(debt, [2, 0, 0]);
+ pay_damage_debt(&mut debt, false);
+ assert_eq!(debt, [1, 0, 0]);
+ pay_damage_debt(&mut debt, false);
+ assert_eq!(debt, [0, 0, 0]);
+ let mut full = [3, 2, 1];
+ pay_damage_debt(&mut full, true);
+ assert_eq!(full, [0, 0, 0]);
+ }
+
+ /// The pacing envelope is the vendor's, and only a shared-pipe dock declares one.
+ #[test]
+ fn stream_pacing_is_the_vendors_envelope_on_the_shared_pipe_dock_only() {
+ assert!(!profile::PROFILE_RIDGE.protocol.stream_pacing.is_metered());
+ assert!(!profile::PROFILE_NAVARRO.protocol.stream_pacing.is_metered());
+ let pacing = profile::PROFILE_ELLA.protocol.stream_pacing;
+ assert!(pacing.is_metered());
+ assert_eq!(pacing.bytes_per_sec, 8_000_000);
+ assert_eq!(pacing.burst_bytes, 24_000_000);
+ // Room for the dock-wide activation keyframe, which is 9.56 MB inside half a second and
+ // is accepted every time.
+ assert!(pacing.burst_bytes > 9_560_000);
+ // And within reach of the vendor's own worst second, rather than a fraction of it.
+ assert!(i64::from(pacing.burst_bytes) + i64::from(pacing.bytes_per_sec) >= 30_000_000);
+
+ let bps = pacing.bytes_per_sec;
+ // A second of idle accrues exactly a second of budget, before the burst cap applies.
+ assert_eq!(stream_credit_accrued(bps, 1_000_000), 8_000_000);
+ assert_eq!(stream_credit_accrued(bps, 1_000), 8_000);
+ // A long idle must not wrap into a negative windfall.
+ assert!(stream_credit_accrued(bps, i64::MAX) > 0);
+ assert_eq!(stream_credit_accrued(bps, -5), 0);
+
+ // In credit, a frame goes now. Overdrawn, it waits for exactly the debt.
+ assert_eq!(stream_credit_wait_us(bps, 1), None);
+ assert_eq!(stream_credit_wait_us(bps, 0), None);
+ // Overdrawn by a second's refill, a frame waits exactly a second.
+ assert_eq!(stream_credit_wait_us(bps, -8_000_000), Some(1_000_001));
+ assert!(stream_credit_wait_us(bps, i64::MIN).is_some());
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/dispatch.rs b/drivers/gpu/drm/vino/drm_sink/dispatch.rs
new file mode 100644
index 000000000000..bd759ad4bc99
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/dispatch.rs
@@ -0,0 +1,446 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Publishing desired state to the asynchronous workers.
+//!
+//! Atomic callbacks run in contexts that may not sleep or touch USB, so they record what the
+//! dock should be doing and wake a worker. Each operation class owns one slot, which makes an
+//! update infallible and lets a stale cursor position or stream state be overwritten rather
+//! than queued behind the state that replaced it.
+
+use super::*;
+
+impl VinoDrmData {
+ /// Publish the latest desired operation for a connector and wake the async worker.
+ ///
+ /// Each operation class has one fixed slot, so updates cannot fail allocation and obsolete
+ /// cursor positions or stream states do not build a backlog.
+ pub(super) fn queue_cmd(&self, dev: &VinoDrmDevice, cmd: KmsCmd) {
+ let mut pending = self.pending_kms.lock();
+ if self.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ pending.update(cmd);
+ // Registration precedes the blocking encrypted setup and platform readiness interval.
+ // Retain and coalesce commands that arrive there, but do not let them touch the dock.
+ if !self.kms_activation_ready() {
+ return;
+ }
+ // Enqueue while the queue lock still serializes us with `shutdown()`. Otherwise shutdown
+ // could cancel an idle work item between this unlock and enqueue, leaving a late work-owned
+ // device reference behind after teardown.
+ //
+ // `::<_, 0>` names `cmd_work`. The ID is only inferrable while a single `WorkItem` impl
+ // exists; adding the per-connector scanout items made every bare `enqueue` ambiguous, which
+ // is exactly the failure mode you want here -- an unannotated enqueue would otherwise be
+ // free to pick the wrong worker.
+ let _ = self.kms_queue.enqueue::<_, 0>(ARef::from(dev));
+ drop(pending);
+ }
+
+ /// Publish the end of bring-up and wake transport state retained while it ran.
+ pub(crate) fn publish_kms_activation_ready(&self, dev: &VinoDrmDevice) {
+ let pending = self.pending_kms.lock();
+ if self.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ // Share `pending_kms` with `queue_cmd` as the readiness/wakeup handshake: either this sees
+ // a retained command, or a later producer sees readiness and enqueues the work itself.
+ self.kms_activation_ready.store(true, Ordering::Release);
+ if !pending.is_empty() {
+ let _ = self.kms_queue.enqueue::<_, 0>(ARef::from(dev));
+ }
+ drop(pending);
+ // Plane state may also have arrived after CP engagement but before activation readiness.
+ // Its worker gates on the same flag and leaves the coalesced frame in place until this
+ // wake.
+ self.enqueue_scanout_all(dev);
+ }
+
+ /// Publish the latest framebuffer for one connector and wake the same deferred worker used by
+ /// the blocking runtime CP commands. Replacing an unsent flip is deliberate backpressure: the
+ /// dock needs the newest desktop, not every historical compositor buffer. If damaged flips are
+ /// coalesced, carry the unsent damage into the newest framebuffer so no intermediate update is
+ /// lost without needlessly promoting every busy compositor interval to a full-screen refresh.
+ pub(super) fn queue_scanout(
+ &self,
+ dev: &VinoDrmDevice,
+ fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
+ mut frame: PendingScanout,
+ ) {
+ let connector = frame.connector as usize;
+ let socket = connector + 1;
+ if connector >= MAX_CONNECTORS || self.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ // Do not snapshot faster than the encoder consumes. An unclaimed frame in the coalescing
+ // slot means the worker has not caught up, so this snapshot would be overwritten before it
+ // was ever read -- and it is not free: it runs on the compositor's atomic-commit thread and
+ // reads the whole source to hash it. On a busy machine the encoder falls behind by many
+ // commits, and paying that read for every one of them stalls the compositor itself, on
+ // every output it drives rather than only this one.
+ //
+ // Nothing is lost by dropping this flip. Damage is decided by comparing strip hashes
+ // against the last *encoded* baseline, never by the compositor's damage clips, so whatever
+ // this commit changed is still described by the next snapshot that gets through. The frame
+ // the worker eventually takes is at most one encode period old, which is the pacing the
+ // hardware imposes anyway.
+ //
+ // Geometry changes and owed keyframes are never dropped: the first makes the pending
+ // frame's damage coordinates meaningless, and the second is a mode set waiting on current
+ // content rather than an ordinary repaint.
+ let coalesce = {
+ let pending = self.pending_scanout.lock();
+ pending[connector].as_ref().is_some_and(|queued| {
+ queued.w == frame.w
+ && queued.h == frame.h
+ && queued.rotation == frame.rotation
+ && self.keyframe_pending.load(Ordering::Acquire) & (1u32 << connector) == 0
+ })
+ };
+ if coalesce {
+ vino_debug!("vino: socket {socket} flip coalesced before snapshot\n");
+ return;
+ }
+ // The snapshot below is format-agnostic -- both layouts are four bytes per pixel -- so the
+ // depth only has to be recorded, not acted on, before the copy.
+ if let Some(depth) = crate::video::haar::Depth::from_fourcc(fb.format()) {
+ self.set_connector_depth(frame.connector, depth);
+ }
+ let (source_w, source_h) = src_dims(frame.rotation, frame.w, frame.h);
+ // Reserve a slot and lend its surface out, so the ~14.7 MB copy below runs with the pool
+ // lock dropped. Holding it across the copy put this connector's scanout worker into
+ // `mutex_spin_on_owner` for the whole snapshot -- 5.4% of the machine, burnt spinning.
+ let (mut surface, binding, idx) = {
+ let mut pool = self.shadow[connector].lock();
+ // Rotate, rather than always taking the first free slot. `find` returned slot 0 on
+ // every commit whenever nothing was inflight, so consecutive snapshots overwrote the
+ // same slot and bumped its generation -- invalidating any frame the worker had already
+ // selected from it, which showed up as a third of all frames being dropped at the
+ // generation check. Alternating means a fresh snapshot lands clear of the frame the
+ // worker is about to pick up.
+ let start = self.shadow_rr[connector].fetch_add(1, Ordering::Relaxed) as usize;
+ let Some(idx) = (0..SHADOW_SLOTS)
+ .map(|i| (start + i) % SHADOW_SLOTS)
+ .find(|&idx| pool.inflight != Some(idx) && pool.writing != Some(idx))
+ else {
+ return;
+ };
+ let binding = match pool.source_bindings.get(fb) {
+ Ok(binding) => binding,
+ Err(e) => {
+ pr_warn!("vino: socket {socket} framebuffer binding failed ({e:?})\n");
+ return;
+ }
+ };
+ pool.writing = Some(idx);
+ (pool.slots[idx].surface.take(), binding, idx)
+ };
+
+ let r = snapshot_to_shadow(
+ self.geometry(),
+ &mut surface,
+ &binding.mapping,
+ source_w,
+ source_h,
+ );
+
+ let snapshot = {
+ let mut pool = self.shadow[connector].lock();
+ pool.writing = None;
+ let slot = &mut pool.slots[idx];
+ slot.surface = surface;
+ // Bump unconditionally: the slot's contents have been rewritten either way, so any
+ // frame still pointing at the old generation must not be encoded from it.
+ slot.generation = slot.generation.wrapping_add(1);
+ r.map(|()| (idx, slot.generation))
+ };
+ let (idx, generation) = match snapshot {
+ Ok(snapshot) => snapshot,
+ Err(e) => {
+ pr_warn!("vino: socket {socket} framebuffer snapshot failed ({e:?})\n");
+ return;
+ }
+ };
+ frame.shadow_idx = idx;
+ frame.shadow_generation = generation;
+
+ // A real flip carries newer content than an armed repaint.
+ self.settle_repaint.lock()[connector] = None;
+
+ let mut pending = self.pending_scanout.lock();
+ if self.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ if let Some(old) = pending[connector].take() {
+ if old.w != frame.w || old.h != frame.h || old.rotation != frame.rotation {
+ // Damage coordinates are not comparable across a geometry transform. A mode-set
+ // already owes a keyframe, but keep this conservative for a rotation-only commit.
+ frame.clips[0] = (0, 0, frame.w, frame.h);
+ frame.nclips = 1;
+ } else if old.nclips + frame.nclips <= MAX_DAMAGE_CLIPS {
+ // `frame` names the newest complete framebuffer. Repainting the union of its own
+ // damage and every unsent older clip reproduces all intermediate changes directly
+ // from that newest image.
+ for &clip in &old.clips[..old.nclips] {
+ frame.clips[frame.nclips] = clip;
+ frame.nclips += 1;
+ }
+ } else {
+ // Too many rectangles for the bounded atomic-state payload: collapse their union
+ // to one bounding box. This may repaint extra strips, but unlike the previous
+ // full-output fallback it remains small for typical pointer/window motion.
+ let mut bb = (frame.w, frame.h, 0usize, 0usize);
+ for &r in &frame.clips[..frame.nclips] {
+ bb = (bb.0.min(r.0), bb.1.min(r.1), bb.2.max(r.2), bb.3.max(r.3));
+ }
+ for &r in &old.clips[..old.nclips] {
+ bb = (bb.0.min(r.0), bb.1.min(r.1), bb.2.max(r.2), bb.3.max(r.3));
+ }
+ if bb.0 < bb.2 && bb.1 < bb.3 {
+ frame.clips[0] = bb;
+ frame.nclips = 1;
+ } else {
+ frame.nclips = 0;
+ }
+ }
+ }
+ pending[connector] = Some(frame);
+ self.enqueue_scanout(dev, connector);
+ drop(pending);
+ }
+
+ /// Wake `connector`'s scanout worker. The work ID is a const generic, so the runtime connector
+ /// index has to be matched into it here. Enqueueing an already-pending item is a no-op, and
+ /// enqueueing one that is currently running re-arms it, preserving a flip that arrives during
+ /// encoding for the worker's next pass.
+ pub(super) fn enqueue_scanout(&self, dev: &VinoDrmDevice, connector: usize) {
+ match connector {
+ 0 => {
+ let _ = self.scanout_queue.enqueue::<_, 1>(ARef::from(dev));
+ }
+ 1 => {
+ let _ = self.scanout_queue.enqueue::<_, 2>(ARef::from(dev));
+ }
+ 2 => {
+ let _ = self.scanout_queue.enqueue::<_, 3>(ARef::from(dev));
+ }
+ 3 => {
+ let _ = self.scanout_queue.enqueue::<_, 4>(ARef::from(dev));
+ }
+ _ => {}
+ }
+ }
+
+ /// Wait for any frame already in flight on a scanout worker to finish, after [`Self::cmd_busy`]
+ /// has been published. A worker that has not yet started re-checks `cmd_busy` and backs off on
+ /// its own; this only covers one that got past that check before the flag was set.
+ ///
+ /// Bounded, and it proceeds anyway on timeout: a mode-set that never reaches the dock is worse
+ /// than one that races a frame, and this is the path cold activation depends on. The bound is
+ /// generous against a worst-case frame (a ~3.19 MB keyframe: ~21 ms to encode plus its wire
+ /// time), so exceeding it means something is genuinely wedged and the log line is the point.
+ pub(super) fn wait_for_video_idle(&self) {
+ use core::sync::atomic::Ordering::SeqCst;
+ for _ in 0..500 {
+ if !self.video_inflight.iter().any(|f| f.load(SeqCst)) {
+ return;
+ }
+ fsleep(Delta::from_millis(1));
+ }
+ pr_warn!("vino: timed out waiting for in-flight scanout before a mode-set; proceeding\n");
+ }
+
+ /// Wake every connector's scanout worker. Used by `cmd_work` once its batch is done, since a
+ /// command batch is exactly what makes the scanout workers bail (see [`run_scanout_worker`]).
+ pub(crate) fn enqueue_scanout_all(&self, dev: &VinoDrmDevice) {
+ for connector in 0..MAX_CONNECTORS {
+ self.enqueue_scanout(dev, connector);
+ }
+ }
+
+ /// Record that `connector` owes a full keyframe, and refill its settle-repaint budget.
+ ///
+ /// Mode sets, output enables, and gamma changes use this path. Training
+ /// and settle repaints may re-raise the keyframe bit without refilling
+ /// the budget, which bounds idle keyframe generation.
+ pub(super) fn owe_keyframe(&self, connector: usize) {
+ self.keyframe_pending
+ .fetch_or(1u32 << connector, Ordering::Release);
+ self.settle_budget[connector].store(SETTLE_REPAINTS, Ordering::Relaxed);
+ // Whatever left the dock's framebuffer undefined left its cursor bitmap undefined too, so
+ // the two invalidations are raised together. Keeping them in one place is deliberate:
+ // their being separate is exactly how the cursor came to be dropped on a mode-set.
+ self.cursor_epoch[connector].fetch_add(1, Ordering::Release);
+ self.cursor_geometry.lock()[connector] = None;
+ }
+
+ /// Note a cursor command the dock has just accepted, so [`Self::rearm_cursor`] can replay it.
+ pub(super) fn record_cursor(&self, cmd: &KmsCmd) {
+ let connector = cmd.connector();
+ if connector >= MAX_CONNECTORS {
+ return;
+ }
+ let mut slots = self.cursor_shot.lock();
+ match cmd {
+ KmsCmd::CursorImage { w, h, bgra, .. } => {
+ let mut copy = KVec::new();
+ if copy.extend_from_slice(bgra, GFP_KERNEL).is_err() {
+ // A cursor that cannot be cached is still on the dock; it just will not be
+ // restored across the next mode set. Nothing else depends on this.
+ return;
+ }
+ match &mut slots[connector] {
+ Some(shot) => {
+ shot.w = *w;
+ shot.h = *h;
+ shot.bgra = copy;
+ }
+ slot @ None => {
+ *slot = Some(CursorShot {
+ w: *w,
+ h: *h,
+ bgra: copy,
+ x: 0,
+ y: 0,
+ visible: false,
+ })
+ }
+ }
+ }
+ KmsCmd::CursorMove { x, y, visible, .. } => {
+ if let Some(shot) = &mut slots[connector] {
+ shot.x = *x;
+ shot.y = *y;
+ shot.visible = *visible;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ /// Re-upload the cursor on every connector in `connectors` after a mode set discarded it.
+ ///
+ /// `owe_keyframe` marks the dock's cursor stale, but only a compositor commit on the cursor
+ /// plane acted on that mark -- and a pointer that is not moving never produces one, so the
+ /// cursor stayed missing until it was moved. Replaying the cached shot closes that window
+ /// without waiting for userspace.
+ pub(super) fn rearm_cursor(&self, dev: &VinoDrmDevice, connectors: u32) {
+ for connector in 0..MAX_CONNECTORS {
+ if connectors & (1u32 << connector) == 0 {
+ continue;
+ }
+ let (w, h, bgra, x, y, visible) = {
+ let slots = self.cursor_shot.lock();
+ let Some(shot) = &slots[connector] else {
+ continue;
+ };
+ let mut copy = KVec::new();
+ if copy.extend_from_slice(&shot.bgra, GFP_KERNEL).is_err() {
+ continue;
+ }
+ (shot.w, shot.h, copy, shot.x, shot.y, shot.visible)
+ };
+ let connector = connector as u8;
+ // The same order the plane callback uses, and the same order `cmd_work` drains them
+ // in: geometry, then bitmap, then position.
+ self.queue_cmd(dev, KmsCmd::CursorCreate { connector, w, h });
+ self.queue_cmd(
+ dev,
+ KmsCmd::CursorImage {
+ connector,
+ w,
+ h,
+ bgra,
+ },
+ );
+ self.queue_cmd(
+ dev,
+ KmsCmd::CursorMove {
+ connector,
+ x,
+ y,
+ visible,
+ },
+ );
+ }
+ }
+
+ /// Choose the next frame or delay for `connector`.
+ ///
+ /// Neither means this connector is idle and its worker can exit.
+ pub(super) fn select_scanout(&self, connector: usize) -> (Option<PendingScanout>, Option<i64>) {
+ let socket = connector + 1;
+ // Keep a frame that arrived before the cadence deadline in the
+ // coalescing slot. Userspace may stop committing after that flip, so
+ // discarding it could leave the newest image unsent.
+ // A dock with a sustained budget is held to it ahead of everything else, including an owed
+ // keyframe: the frame that overruns it costs the session, not a repaint. The coalescing
+ // slot keeps the newest image meanwhile, so waiting here drops intermediate frames rather
+ // than delaying the desktop.
+ if let Some(us) = self.stream_budget_wait_us() {
+ return (None, Some(us));
+ }
+ let mut pending = self.pending_scanout.lock();
+ let mut selected = None;
+ let mut wait_us: Option<i64> = None;
+ if self.modeset_requested[connector].load(Ordering::Acquire) != 0
+ && pending[connector].is_some()
+ {
+ let owes_keyframe =
+ self.keyframe_pending.load(Ordering::Acquire) & (1u32 << connector) != 0;
+ let elapsed_us = self.last_frame.lock()[connector]
+ .map_or(self.frame_period_us(), |t| t.elapsed().as_micros_ceil());
+ // An owed keyframe normally jumps the cadence queue, because it is the frame that makes
+ // the output correct and a compositor may not send another. A dock sharing the control
+ // pipe cannot grant that: a keyframe is its largest frame, and anything that re-raises
+ // the keyframe bit would let it bypass the interval repeatedly and hold the endpoint
+ // for as long as it keeps being raised. There the interval binds every frame.
+ let urgent = owes_keyframe && !self.video_on_ctrl_pipe();
+ if urgent || elapsed_us >= self.frame_period_us() {
+ selected = pending[connector].take();
+ // A busy compositor continuously replaces `settle_repaint`.
+ // Force cadence-selected frames to be keyframes while training;
+ // the elapsed check above still applies the cadence limit.
+ let sustaining = self.sustain_until.lock()[connector]
+ .is_some_and(|until| (until - Instant::<Monotonic>::now()).as_millis() > 0);
+ if sustaining {
+ self.keyframe_pending
+ .fetch_or(1u32 << connector, Ordering::Release);
+ }
+ } else {
+ wait_us = Some(self.frame_period_us() - elapsed_us);
+ }
+ }
+ // Nothing flipped in. Fall back to the one-shot settle repaint if one is due, so a
+ // compositor that went idle straight after enabling the output still ends up with its real
+ // desktop on the panel rather than the buffer that happened to be current when the
+ // mode-set's keyframe went out.
+ if selected.is_none() {
+ let mut settle = self.settle_repaint.lock();
+ if self.modeset_requested[connector].load(Ordering::Acquire) == 0 {
+ settle[connector] = None;
+ } else if let Some((due, _, _)) = settle[connector].as_ref() {
+ let remaining = *due - Instant::<Monotonic>::now();
+ if remaining.as_millis() <= 0 {
+ let taken = settle[connector].take();
+ let as_keyframe = taken.as_ref().is_some_and(|(_, _, kf)| *kf);
+ selected = taken.map(|(_, f, _)| f);
+ if as_keyframe {
+ self.keyframe_pending
+ .fetch_or(1u32 << connector, Ordering::Release);
+ }
+ let kind = if as_keyframe {
+ "settle repaint (compositor idle after mode-set)"
+ } else {
+ "debt repaint (retransmissions owed, compositor idle)"
+ };
+ vino_debug!("vino: socket {socket} {kind}\n");
+ } else {
+ let remaining = remaining.as_micros_ceil().max(1);
+ wait_us = Some(wait_us.map_or(remaining, |old| old.min(remaining)));
+ }
+ }
+ }
+ (selected, wait_us)
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/driver.rs b/drivers/gpu/drm/vino/drm_sink/driver.rs
new file mode 100644
index 000000000000..aed8339f6336
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/driver.rs
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Registration: the DRM driver description, its GEM object and file types, and the KMS
+//! entry points the core calls into.
+
+use super::*;
+
+/// GEM object inner data. Empty: the shmem-backed `drm::gem::shmem::Object` (which
+/// wires `drm_gem_shmem_dumb_create`, so userspace `DRM_IOCTL_MODE_CREATE_DUMB`
+/// works) is enough until the EP08 scanout path consumes the framebuffers.
+#[pin_data]
+pub(crate) struct VinoObject {}
+
+impl drm::gem::DriverObject for VinoObject {
+ type Driver = VinoDrmDriver;
+ type Args = ();
+
+ fn new(
+ _dev: &drm::Device<VinoDrmDriver>,
+ _size: usize,
+ _args: (),
+ ) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoObject {})
+ }
+}
+
+/// Per-open DRM client state. The generic DRM fops pin the owning module for the file lifetime.
+#[pin_data]
+pub(crate) struct VinoDrmFile {}
+
+impl drm::file::DriverFile for VinoDrmFile {
+ type Driver = VinoDrmDriver;
+
+ fn open(_dev: &drm::Device<Self::Driver>) -> Result<Pin<KBox<Self>>> {
+ KBox::try_pin_init(try_pin_init!(Self {}), GFP_KERNEL)
+ }
+}
+
+pub(super) const INFO: drm::DriverInfo = drm::DriverInfo {
+ major: 0,
+ minor: 1,
+ patchlevel: 0,
+ name: c"vino",
+ desc: c"DisplayLink DL3 (Dell D6000) DRM driver",
+};
+
+#[vtable]
+impl drm::Driver for VinoDrmDriver {
+ type Data = VinoDrmData;
+ type File = VinoDrmFile;
+ type Object = drm::gem::shmem::Object<VinoObject>;
+ type ParentDevice<Ctx: kernel::device::DeviceContext> = crate::usb::Interface<Ctx>;
+ type RegistrationData<'a> = ();
+ type Kms = Self;
+
+ const INFO: drm::DriverInfo = INFO;
+
+ // No driver-private ioctls (GEM/dumb + KMS handled by the DRM core).
+ kernel::declare_drm_ioctls! {}
+}
+
+#[vtable]
+impl KmsDriver for VinoDrmDriver {
+ type Connector = VinoConnector;
+ type Plane = VinoPlane;
+ type Crtc = VinoCrtc;
+ type Encoder = VinoEncoder;
+
+ fn mode_config_info(
+ _dev: &kernel::device::Device,
+ _drm_data: &Self::Data,
+ ) -> Result<ModeConfigInfo> {
+ Ok(ModeConfigInfo {
+ min_resolution: (0, 0),
+ max_resolution: (4096, 4096),
+ max_cursor: (64, 64),
+ preferred_depth: 32,
+ preferred_fourcc: Some(drm::fourcc::XRGB8888),
+ })
+ }
+
+ fn create_objects(dev: &UnregisteredKmsDevice<'_, Self>) -> Result {
+ let data: &VinoDrmData = dev;
+ // Build one independent connector (CRTC + primary/cursor plane + encoder + connector) per
+ // wired display, each pinned to its own video endpoint via its connector index.
+ //
+ // Only as many as the dock has sockets. `MAX_CONNECTORS` is the largest any supported dock
+ // has, so it sizes the per-connector arrays, but building that many objects on a
+ // two-connector dock publishes outputs with nothing behind them: they never gain an EDID, a
+ // compositor is free to enable one anyway, and the driver then encodes and transmits whole
+ // frames to a socket that cannot display them -- onto the same endpoint the real connector
+ // is using.
+ for connector in 0..data.connector_count() {
+ // `possible_crtcs` for the plane/encoder is a bitmask of CRTC *indices*, which only
+ // exist once `UnregisteredCrtc::new` runs -- but planes must exist before the CRTC that
+ // references them. CRTCs are created here one per connector in order, so this
+ // connector's CRTC index is `connector` and its mask is `1 << connector`.
+ let crtc_mask = 1u32 << connector;
+ let primary = plane::UnregisteredPlane::<VinoPlane>::new(
+ dev,
+ crtc_mask,
+ if data.hdr_capable() {
+ &PRIMARY_FORMATS_HDR[..]
+ } else {
+ &PRIMARY_FORMATS[..]
+ },
+ // Scanout is linear and nothing else is accepted: `Framebuffer` rejects any other
+ // modifier outright. Saying so publishes IN_FORMATS, so a compositor picks a format
+ // knowing what the plane takes rather than inferring it from the bare format list.
+ Some(&LINEAR_MODIFIER[..]),
+ plane::Type::Primary,
+ None,
+ PlaneArgs {
+ connector: connector as u8,
+ is_cursor: false,
+ },
+ )?;
+ // Tell compositors that this primary plane accepts the standard FB_DAMAGE_CLIPS
+ // property. The scanout path already consumes those clips and emits only intersecting
+ // 64x16 Haar strips, but without attaching the property KWin cannot provide them:
+ // unchanged commits arrive with an empty clip list while real updates fall back to
+ // ambiguous framebuffer swaps. That left the first keyframe frozen when empty damage
+ // was correctly treated as a no-op, or forced multi-megabyte full frames when it was
+ // treated as a repaint. EVDI exposes the same property before plane registration.
+ primary.enable_fb_damage_clips();
+ // Advertise every rotation vino's re-encode can produce by remapping source pixels
+ // (`rot_src`): the four 90-degree rotations plus the two reflections.
+ primary.create_rotation_property(
+ plane::Rotation::ROTATE_0,
+ plane::Rotation::ROTATE_0
+ | plane::Rotation::ROTATE_90
+ | plane::Rotation::ROTATE_180
+ | plane::Rotation::ROTATE_270
+ | plane::Rotation::REFLECT_X
+ | plane::Rotation::REFLECT_Y,
+ )?;
+ // A dock that composites no cursor of its own gets no cursor plane, rather than a
+ // plane whose messages are then withheld: a cursor plane whose atomic commit succeeds
+ // makes the compositor hand the pointer over and stop drawing its own, so starving one
+ // loses the pointer entirely instead of falling back to software. A CRTC with no
+ // cursor plane is how a driver says "draw it yourself".
+ let cursor = if data.hw_cursor() {
+ let cursor = plane::UnregisteredPlane::<VinoPlane>::new(
+ dev,
+ crtc_mask,
+ &CURSOR_FORMATS,
+ Some(&LINEAR_MODIFIER[..]),
+ plane::Type::Cursor,
+ None,
+ PlaneArgs {
+ connector: connector as u8,
+ is_cursor: true,
+ },
+ )?;
+ // An alpha framebuffer requires a blend-mode property. The dock composites the
+ // cursor from a premultiplied bitmap, so premultiplied is the only supported mode.
+ cursor.create_blend_mode_property(plane::BlendModes::PREMULTIPLIED)?;
+ Some(cursor)
+ } else {
+ None
+ };
+ let crtc_obj = crtc::UnregisteredCrtc::<VinoCrtc>::new(
+ dev,
+ primary,
+ cursor,
+ None,
+ connector as u8,
+ )?;
+ // Advertise CTM and a 256-entry GAMMA_LUT; the scanout applies both (cached via the
+ // CRTC hooks). The dock has no colour hardware, so software application here is the
+ // only place a compositor's correction can land -- KDE's Night Colour and GNOME's
+ // Night Light drive these properties rather than rewriting the framebuffer.
+ crtc_obj.enable_color_mgmt(0, true, crate::color::LUT_LEN as u32);
+ let enc = encoder::UnregisteredEncoder::<VinoEncoder>::new(
+ dev,
+ encoder::Type::Virtual,
+ crtc_obj.mask(),
+ 0,
+ None,
+ (),
+ )?;
+ let conn = connector::UnregisteredConnector::<VinoConnector>::new(
+ dev,
+ // DisplayPort connectors receive DRM's standard EDID property. A virtual connector
+ // would not, and therefore could not publish the downstream monitor's modes.
+ connector::Type::DisplayPort,
+ connector as u8,
+ )?;
+ conn.attach_encoder(&*enc)?;
+ // HDR is a property of the dock's pipeline, not of the monitor: a sink that declares
+ // ST 2084 is useless if the dock cannot be told to carry ten bits. `hdr_capable`
+ // keeps a Ridge connector from advertising an output it has no set-mode encoding for.
+ //
+ // The whole path exists now: the transfer function is offset-42 bit 6
+ // (`ST2084 colorspace used (HDR)`, read out of DLM's own `setupVideo` decode),
+ // `atomic_enable` takes it from this connector's HDR_OUTPUT_METADATA EOTF, and the
+ // depth (offset 69) and DMA format (offset 23, `NM30`) go with it.
+ //
+ // Attaching these is what makes a compositor re-encode the desktop in PQ/BT.2020. If
+ // the sink does not follow, the failure is a washed-out grey desktop that is invisible
+ // on the wire: a capture shows correct PQ code words in both the working and the
+ // broken case.
+ if data.hdr_capable() {
+ conn.attach_max_bpc_property(8, 10)?;
+ conn.attach_colorspace_property()?;
+ conn.attach_hdr_output_metadata_property();
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/limits.rs b/drivers/gpu/drm/vino/drm_sink/limits.rs
new file mode 100644
index 000000000000..96d2d8dcf8c2
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/limits.rs
@@ -0,0 +1,489 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! What a dock will accept: pixel-clock ceilings, refresh ceilings and the shared bandwidth
+//! budget a multi-connector commit has to fit inside.
+//!
+//! A dock silently refuses to light a mode past its budget rather than reporting anything, so
+//! these are the checks that keep a connector from being handed one.
+
+use super::*;
+
+/// Per-mode pixel-clock ceiling in kHz for a dock whose profile has not been applied yet.
+///
+/// Ridge's DLM never programs above 497.75 MHz, so no Ridge capture fills the high half of the
+/// offset-70 `u32`; this keeps the value that half can express on its own.
+pub(super) const DEFAULT_MAX_HEAD_CLOCK_KHZ: u32 = 655_350;
+
+/// Refresh ceiling for a dock whose profile has not been applied yet.
+///
+/// This is Ridge's limit, which is also DLM's: asked for 2560x1440@180 it puts 119.998 Hz on the
+/// wire, and asked for @85 it programs the 59.95 Hz CVT-RB timing.
+pub(super) const DEFAULT_MAX_REFRESH_HZ: u32 = 120;
+
+/// Return the active pixel rate, saturating on invalidly large modes.
+pub(crate) fn active_pixel_rate(hdisplay: u16, vdisplay: u16, vrefresh: i32) -> u32 {
+ u32::from(hdisplay)
+ .saturating_mul(u32::from(vdisplay))
+ .saturating_mul(vrefresh.max(0) as u32)
+}
+
+/// Nonzero generation key for every deterministic field of a set-mode timing.
+///
+/// Zero means "disabled" in the atomics that carry this key. This is a fingerprint rather than
+/// a packed subset: porches, sync widths, allocation, VIC, depth and dual-pipe state all change
+/// bytes the dock consumes and therefore all have to invalidate a previously active mode.
+pub(crate) fn timing_key(t: &crate::cp::Timing) -> u64 {
+ let mut hash = 0x7669_6e6f_6d6f_6465u64;
+ for field in [
+ u64::from(t.hactive),
+ u64::from(t.hblank),
+ u64::from(t.hsync_front),
+ u64::from(t.hsync_width),
+ u64::from(t.vactive),
+ u64::from(t.vblank),
+ u64::from(t.vsync_front),
+ u64::from(t.vsync_width),
+ u64::from(t.refresh_hz),
+ u64::from(t.pixel_clock_10khz),
+ u64::from(t.sync_flags),
+ u64::from(t.stride),
+ u64::from(t.total_rows),
+ u64::from(t.vic_word),
+ u64::from(t.ten_bit),
+ u64::from(t.st2084),
+ u64::from(t.dual_nivo),
+ ] {
+ hash = xxhash::xxh64(&field.to_le_bytes(), hash);
+ }
+ if hash == 0 {
+ 1
+ } else {
+ hash
+ }
+}
+
+/// Whether a live connector already holds the exact effective Timing a command would send.
+pub(crate) fn programmed_mode_matches(
+ active_generation: u64,
+ programmed: Option<crate::cp::Timing>,
+ effective: crate::cp::Timing,
+) -> bool {
+ active_generation != 0 && programmed == Some(effective)
+}
+
+impl VinoDrmData {
+ /// Exact Timing that would be put on the wire for the current requested topology.
+ pub(super) fn effective_timing(
+ &self,
+ connector: usize,
+ timing: &crate::cp::Timing,
+ ) -> crate::cp::Timing {
+ self.effective_timing_in_mask(connector, timing, self.requested_connector_mask())
+ }
+
+ /// Exact Timing for a stable requested-connector snapshot shared by a multi-connector
+ /// transaction.
+ pub(super) fn effective_timing_in_mask(
+ &self,
+ connector: usize,
+ timing: &crate::cp::Timing,
+ requested_heads: u32,
+ ) -> crate::cp::Timing {
+ crate::cp::Timing {
+ dual_nivo: self.endpoint_is_shared_in_mask(connector, requested_heads),
+ ..*timing
+ }
+ }
+
+ /// Adopt an already-programmed exact mode under the caller's current request generation.
+ ///
+ /// A dynamic `dual_nivo` correction can make two request tokens differ while the exact timing
+ /// the dock holds is unchanged. Conversely, equal tokens are not enough if endpoint topology
+ /// changed. Compare the separately recorded wire state and change only `modeset_active`, whose
+ /// job is to gate scanout against the current producer request. An explicit repair clears
+ /// `modeset_active`, so it can never be optimized away here.
+ pub(super) fn adopt_programmed_mode(
+ &self,
+ connector: usize,
+ timing: &crate::cp::Timing,
+ want: u64,
+ ) -> bool {
+ if connector >= MAX_CONNECTORS
+ || self.modeset_requested[connector].load(Ordering::Acquire) != want
+ {
+ return false;
+ }
+ let active = self.modeset_active[connector].load(Ordering::Acquire);
+ if !programmed_mode_matches(
+ active,
+ self.programmed_timing.lock()[connector],
+ self.effective_timing(connector, timing),
+ ) {
+ return false;
+ }
+ // A disable that races this compare either clears `active` first (CAS fails) or clears it
+ // after (the disable wins). A newer nonzero request may leave the old active token in
+ // place, but scanout remains gated until its own command adopts or programs that request.
+ self.modeset_active[connector]
+ .compare_exchange(active, want, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok()
+ }
+
+ /// The dock's total pixel-rate budget shared across all connectors, unpriced by depth.
+ ///
+ /// Zero means unknown and disables limiting.
+ pub(super) fn dock_budget(&self) -> u32 {
+ self.dock_pixel_budget
+ .load(core::sync::atomic::Ordering::Relaxed)
+ }
+
+ /// Whether a commit totalling `combined` pixels per second can be driven at ten bits.
+ ///
+ /// The answer moves the depth rather than refusing the commit: a compositor handed `EINVAL`
+ /// disables the output instead of asking for a shallower link.
+ pub(super) fn ten_bit_fits(&self, combined: u32) -> bool {
+ let raw = self.dock_budget();
+ raw == 0 || combined <= self.budget_at_depth(raw, true)
+ }
+
+ /// A budget priced for a connector driven at ten bits per channel.
+ ///
+ /// The budget was measured with the dock storing three bytes per pixel; ten bits stores four,
+ /// so the same pixel costs a third more. The whole dock is priced at its deepest connector,
+ /// because the bandwidth is shared.
+ ///
+ /// `budget` must be the unpriced [`Self::dock_budget`]. Pricing an already-priced budget leaves
+ /// nine sixteenths of the dock, which no pair of ten-bit connectors fits inside.
+ pub(super) fn budget_at_depth(&self, budget: u32, ten_bit: bool) -> u32 {
+ if budget != 0 && ten_bit {
+ budget / 4 * 3
+ } else {
+ budget
+ }
+ }
+
+ /// Record this dock's pixel-rate budget, refresh ceiling and pixel-clock ceiling.
+ pub(crate) fn set_mode_limits(
+ &self,
+ pixel_budget: u32,
+ max_refresh_hz: u32,
+ max_connector_clock_khz: u32,
+ ) {
+ self.dock_pixel_budget
+ .store(pixel_budget, core::sync::atomic::Ordering::Relaxed);
+ self.max_refresh_hz.store(
+ if max_refresh_hz == 0 {
+ DEFAULT_MAX_REFRESH_HZ
+ } else {
+ max_refresh_hz
+ },
+ core::sync::atomic::Ordering::Relaxed,
+ );
+ self.max_connector_clock_khz.store(
+ if max_connector_clock_khz == 0 {
+ DEFAULT_MAX_HEAD_CLOCK_KHZ
+ } else {
+ max_connector_clock_khz
+ },
+ core::sync::atomic::Ordering::Relaxed,
+ );
+ }
+
+ /// Highest per-mode pixel clock in kHz this dock is known to accept.
+ pub(crate) fn max_connector_clock_khz(&self) -> u32 {
+ self.max_connector_clock_khz
+ .load(core::sync::atomic::Ordering::Relaxed)
+ }
+
+ /// Highest refresh rate this dock is known to drive.
+ pub(crate) fn max_refresh_hz(&self) -> u32 {
+ self.max_refresh_hz
+ .load(core::sync::atomic::Ordering::Relaxed)
+ }
+
+ /// Whether DRM's rounded refresh rate is within this dock's limit.
+ pub(super) fn refresh_within_limit(&self, vrefresh: i32) -> bool {
+ vrefresh <= 0 || (vrefresh as u32) <= self.max_refresh_hz()
+ }
+
+ /// Combined pixel rate of every connector *except* `connector` that currently has a mode driven
+ /// onto it.
+ ///
+ /// A connector the commit carries is taken from the commit; every other connector is taken from
+ /// what is programmed. Both halves are needed: a commit reconfiguring several connectors at
+ /// once must be weighed at the rates it is asking for, and a connector standing outside it
+ /// still spends what it was last given.
+ ///
+ /// Only active connectors consume the shared limit. `last_timing` survives `atomic_disable`, so
+ /// activity is read from `modeset_requested`, which is cleared on disable.
+ ///
+ /// A connector whose monitor has gone is not spending anything either, whatever mode it was
+ /// last asked for: its downstream sink is already torn down. That has to be read from presence
+ /// rather than from the mode state, because a removal and the arrival that replaces it reach
+ /// userspace as two events -- moving a monitor from one socket to another is checked at its new
+ /// socket while the disable of the old one has not been committed yet, and charging the dock
+ /// for the dark connector is what refuses the new one its mode.
+ pub(super) fn other_connectors_rate(
+ &self,
+ state: &kernel::drm::kms::atomic::AtomicStateMutator<VinoDrmDriver>,
+ connector: usize,
+ ) -> u32 {
+ // A connector this commit describes is charged what the commit gives it. Reading its
+ // programmed rate instead would price every connector in a multi-connector commit at what
+ // it is leaving, so a pair that rises together would be admitted at the sum of the rates it
+ // is abandoning.
+ let mut proposed: [Option<u32>; MAX_CONNECTORS] = [None; MAX_CONNECTORS];
+ state.for_each_new_crtc_state(|crtc, crtc_state| {
+ let Some(slot) = proposed.get_mut(crtc.connector as usize) else {
+ return;
+ };
+ *slot = Some(if crtc_state.active() {
+ let m = crtc_state.mode();
+ active_pixel_rate(m.hdisplay(), m.vdisplay(), m.vrefresh())
+ } else {
+ 0
+ });
+ });
+
+ let timings = *self.last_timing.lock();
+ let mut total: u32 = 0;
+ for (i, t) in timings.iter().enumerate() {
+ if i == connector {
+ continue;
+ }
+ if let Some(rate) = proposed[i] {
+ total = total.saturating_add(rate);
+ continue;
+ }
+ if self.modeset_requested[i].load(Ordering::Acquire) == 0 || !self.connector_present(i)
+ {
+ continue;
+ }
+ if let Some(t) = t {
+ total = total.saturating_add(
+ u32::from(t.hactive)
+ .saturating_mul(u32::from(t.vactive))
+ .saturating_mul(u32::from(t.refresh_hz)),
+ );
+ }
+ }
+ total
+ }
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_mode_limits)]
+mod tests {
+ use super::*;
+ use crate::*;
+
+ #[test]
+ fn timing_key_covers_every_set_mode_field() {
+ let base = cp::Timing {
+ hactive: 1920,
+ hblank: 280,
+ hsync_front: 88,
+ hsync_width: 44,
+ vactive: 1080,
+ vblank: 45,
+ vsync_front: 4,
+ vsync_width: 5,
+ refresh_hz: 60,
+ pixel_clock_10khz: 14_850,
+ sync_flags: 0x0400,
+ stride: 0x0800,
+ total_rows: 0x2000,
+ vic_word: 0x2810,
+ ten_bit: false,
+ st2084: false,
+ dual_nivo: false,
+ };
+ let timings = [
+ base,
+ cp::Timing {
+ hactive: 1921,
+ ..base
+ },
+ cp::Timing {
+ hblank: 281,
+ ..base
+ },
+ cp::Timing {
+ hsync_front: 89,
+ ..base
+ },
+ cp::Timing {
+ hsync_width: 45,
+ ..base
+ },
+ cp::Timing {
+ vactive: 1081,
+ ..base
+ },
+ cp::Timing { vblank: 46, ..base },
+ cp::Timing {
+ vsync_front: 5,
+ ..base
+ },
+ cp::Timing {
+ vsync_width: 6,
+ ..base
+ },
+ // Exercise the high byte that the former packed key discarded.
+ cp::Timing {
+ refresh_hz: 0x013c,
+ ..base
+ },
+ // Exercise bits above the former 22-bit pixel-clock mask.
+ cp::Timing {
+ pixel_clock_10khz: 0x0140_3a02,
+ ..base
+ },
+ cp::Timing {
+ sync_flags: 0x0401,
+ ..base
+ },
+ cp::Timing {
+ stride: 0x0880,
+ ..base
+ },
+ cp::Timing {
+ total_rows: 0x2001,
+ ..base
+ },
+ cp::Timing {
+ vic_word: 0x281f,
+ ..base
+ },
+ cp::Timing {
+ ten_bit: true,
+ ..base
+ },
+ cp::Timing {
+ st2084: true,
+ ..base
+ },
+ cp::Timing {
+ dual_nivo: true,
+ ..base
+ },
+ ];
+ let mut keys = [0u64; 18];
+ for (i, timing) in timings.iter().enumerate() {
+ keys[i] = timing_key(timing);
+ assert_ne!(keys[i], 0);
+ for previous in &keys[..i] {
+ assert_ne!(keys[i], *previous);
+ }
+ }
+ }
+
+ #[test]
+ fn no_op_mode_set_compares_the_exact_programmed_timing() {
+ let raw = cp::Timing {
+ hactive: 2560,
+ hblank: 160,
+ hsync_front: 48,
+ hsync_width: 32,
+ vactive: 1440,
+ vblank: 41,
+ vsync_front: 3,
+ vsync_width: 5,
+ refresh_hz: 60,
+ pixel_clock_10khz: 24_150,
+ sync_flags: 0x0600,
+ stride: 0x0a80,
+ total_rows: 0x66db,
+ vic_word: 0x0800,
+ ten_bit: false,
+ st2084: false,
+ dual_nivo: false,
+ };
+ let effective = cp::Timing {
+ dual_nivo: true,
+ ..raw
+ };
+
+ // The request can have been queued before an endpoint partner appeared, so its raw token
+ // and the exact Timing corrected at send time legitimately differ.
+ assert_ne!(timing_key(&raw), timing_key(&effective));
+ assert!(programmed_mode_matches(
+ timing_key(&raw),
+ Some(effective),
+ effective
+ ));
+ assert!(!programmed_mode_matches(
+ timing_key(&raw),
+ Some(raw),
+ effective
+ ));
+ // Clearing the active generation is an explicit request to touch hardware, even if an old
+ // programmed-state snapshot remains available for diagnostics.
+ assert!(!programmed_mode_matches(0, Some(effective), effective));
+ }
+
+ /// The boundary cases matter most: each must pass by equality. A `<` would prune a dock's
+ /// working configuration and dark its panels.
+ #[test]
+ fn mode_ceilings_bound_bandwidth_not_refresh() {
+ let refresh_ok =
+ |p: &DockProfile, hz: i32| hz <= 0 || (hz as u32) <= p.capabilities.max_refresh_hz;
+ let clock_ok = |p: &DockProfile, khz: u32| khz <= p.capabilities.max_connector_clock_khz;
+ let rate = active_pixel_rate;
+
+ // Ridge carries 2560x1440p144, so no refresh cap may hide it: 597.29 MHz of clock and
+ // 530,841,600 pixels per second both sit inside its ceilings. The 180 Hz request DLM
+ // answers with 119.998 Hz is 746.64 MHz, which the clock ceiling already refuses -- that
+ // is the whole of what a refresh cap here would have bought.
+ assert!(refresh_ok(&profile::PROFILE_RIDGE, 144));
+ assert!(clock_ok(&profile::PROFILE_RIDGE, 597_290));
+ assert!(rate(2560, 1440, 144) <= profile::PROFILE_RIDGE.capabilities.pixel_budget);
+ assert!(!clock_ok(&profile::PROFILE_RIDGE, 746_640));
+
+ // The DL7400 is bounded by link rate alone too -- DLM drives it at 2560x1440@164.96.
+ assert!(
+ refresh_ok(&profile::PROFILE_NAVARRO, 180)
+ && refresh_ok(&profile::PROFILE_NAVARRO, 240)
+ );
+
+ // 2560x1440: p165 is 699.50 MHz and carried; p180 is 714.81 MHz and is the mode the dock
+ // accepts and then fails to deliver.
+ assert!(clock_ok(&profile::PROFILE_NAVARRO, 699_500));
+ assert!(!clock_ok(&profile::PROFILE_NAVARRO, 714_810));
+ // Ridge carries 2560x1440p144 at 597.29 MHz and blanks the sink at p165's 699.50 MHz.
+ assert!(
+ clock_ok(&profile::PROFILE_RIDGE, 597_290)
+ && !clock_ok(&profile::PROFILE_RIDGE, 699_500)
+ );
+
+ // A degenerate mode reports 0 Hz and carries no rate information; a signed refresh must
+ // never be read as a huge unsigned one.
+ assert!(
+ refresh_ok(&profile::PROFILE_NAVARRO, 0) && refresh_ok(&profile::PROFILE_NAVARRO, -1)
+ );
+
+ // Each budget admits its own dual-connector configuration and nothing beyond it.
+ assert_eq!(rate(2560, 1440, 120), 442_368_000);
+ // Ridge sustains 2560x1440p144 beside 2560x1440p120, so its budget must admit that pair.
+ assert_eq!(
+ profile::PROFILE_RIDGE.capabilities.pixel_budget,
+ rate(2560, 1440, 144) + rate(2560, 1440, 120)
+ );
+ assert_eq!(
+ profile::PROFILE_NAVARRO.capabilities.pixel_budget,
+ 2 * rate(2560, 1440, 165)
+ );
+ // That pair is admitted at 24 bpp and refused at 30, which is what the hardware does: a
+ // budget large enough to admit it deep leaves both sinks powered off with nothing logged.
+ let price = |budget: u32| budget / 4 * 3;
+ let deep = price(profile::PROFILE_NAVARRO.capabilities.pixel_budget);
+ assert!(2 * rate(2560, 1440, 165) > deep);
+ assert!(2 * rate(2560, 1440, 120) <= deep);
+ // Two 1440p120 connectors fit deep, and stop fitting the moment the price is charged
+ // twice, so a budget priced at the depth being decided withdraws the ten bits that same
+ // pair was just admitted at.
+ assert!(2 * rate(2560, 1440, 120) > price(deep));
+ assert_eq!(rate(65535, 65535, 65535), u32::MAX); // saturates, never wraps small
+ assert_eq!(rate(2560, 1440, -1), 0);
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/mode_objects.rs b/drivers/gpu/drm/vino/drm_sink/mode_objects.rs
new file mode 100644
index 000000000000..d3ec818ec1da
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/mode_objects.rs
@@ -0,0 +1,978 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The KMS objects the compositor drives: CRTC, primary and cursor planes, encoder and connector,
+//! plus the software vblank timer that paces them.
+//!
+//! These callbacks run under the DRM atomic lock and must not block, so anything that talks to the
+//! dock is queued for [`super::VinoDrmData`]'s workers rather than done here.
+
+use super::*;
+// `hdr_output_eotf` reads the connector's HDR metadata from the CRTC enable path; the trait is
+// implemented for every connector state but has to be in scope to be called on an opaque one.
+use kernel::drm::kms::connector::RawConnectorState;
+
+/// A software vblank source: an hrtimer that fires once per frame and drives
+/// `drm_crtc_handle_vblank()`. It stops when vblank is disabled and is also cancelled
+/// unconditionally by [`VinoDrmData::shutdown`].
+#[pin_data]
+pub(crate) struct VblankTimer {
+ #[pin]
+ timer: HrTimer<Self>,
+ /// Owned CRTC reference used by the hard-timer callback.
+ ///
+ /// This reference forms a cycle through the DRM device, so shutdown clears it after cancelling
+ /// the timer. The IRQ-aware lock permits access from both process and hard-timer context.
+ #[pin]
+ pub(super) crtc: SpinLockIrq<Option<crtc::CrtcRef<VinoCrtc>>>,
+ /// One scanout frame in nanoseconds (from the mode's `framedur_ns`).
+ interval_ns: AtomicI64,
+ /// Whether vblanks should currently be delivered (toggled by enable/disable_vblank).
+ pub(super) enabled: AtomicBool,
+}
+
+impl VblankTimer {
+ fn new() -> impl PinInit<Self> {
+ pin_init!(VblankTimer {
+ timer <- HrTimer::new(),
+ crtc <- new_spinlock_irq!(None, "vino::vblank_crtc"),
+ interval_ns: AtomicI64::new(16_666_666), // ~60 Hz until a mode sets it
+ enabled: AtomicBool::new(false),
+ })
+ }
+}
+
+impl HrTimerCallback for VblankTimer {
+ type Pointer<'a> = Arc<Self>;
+
+ fn run(this: ArcBorrow<'_, Self>, mut ctx: HrTimerCallbackContext<'_, Self>) -> HrTimerRestart {
+ // Vblank is off: let the timer die instead of ticking uselessly; `enable_vblank` re-arms
+ // it. A concurrent re-arm racing this return is safe -- hrtimer keeps a timer that was
+ // re-queued during its callback enqueued even on NORESTART.
+ if !this.enabled.load(Ordering::Relaxed) {
+ return HrTimerRestart::NoRestart;
+ }
+ // Take an owned copy of the published handle and release the lock *before* delivering the
+ // vblank. `drm_crtc_handle_vblank()` takes `dev->vblank_time_lock`, and `enable_vblank`
+ // runs the other way round -- it is called with the DRM vblank locks already held and
+ // acquires this one -- so holding this lock across the delivery would be a lock inversion.
+ // Cloning is just a `drm_dev_get()`, and the clone cannot drop the last reference: the
+ // handle we cloned from stays published for the whole callback, because the only code that
+ // clears it (`VinoDrmData::shutdown`) does so after `hrtimer_cancel` has waited for this
+ // callback to return.
+ let crtc = this.crtc.lock_with(ctx.local_interrupt_disabled()).clone();
+ if let Some(crtc) = crtc {
+ crtc.crtc().handle_vblank();
+ }
+ let interval = this.interval_ns.load(Ordering::Relaxed).max(1_000_000);
+ ctx.forward_now(Delta::from_nanos(interval));
+ HrTimerRestart::Restart
+ }
+}
+
+impl_has_hr_timer! {
+ impl HasHrTimer<Self> for VblankTimer {
+ mode: RelativeHardMode<Monotonic>, field: self.timer
+ }
+}
+
+#[pin_data]
+pub(crate) struct VinoCrtc {
+ /// Which display connector (0-based) this CRTC drives. Names the connector in diagnostics, and
+ /// maps a CRTC in an atomic commit onto the connector whose rate it spends from the dock-wide
+ /// budget.
+ pub(super) connector: u8,
+ /// The software vblank source for this CRTC.
+ vblank: Arc<VblankTimer>,
+ /// One driver-owned DRM vblank reference held for the whole active interval. A USB display has
+ /// no hardware interrupt to bootstrap the compositor's first post-modeset presentation; if no
+ /// initial page-flip event is attached, the DRM core never calls `enable_vblank`, the software
+ /// timer never starts, and KWin leaves the first framebuffer frozen forever. Pinning one ref
+ /// while active starts the clock deterministically; `atomic_disable` balances it before off.
+ /// The vblank reference held for the whole time this CRTC is active. Taken in
+ /// `atomic_enable` and released in `atomic_disable`, which is longer than a borrowed
+ /// `VblankRef` can live, so an owned one is stored here.
+ #[pin]
+ pub(super) vblank_pinned: Mutex<Option<OwnedVblankRef<VinoCrtc>>>,
+}
+
+#[derive(Clone, Default)]
+pub(crate) struct VinoCrtcState;
+
+impl crtc::DriverCrtcState for VinoCrtcState {
+ type Crtc = VinoCrtc;
+}
+
+/// Whether a connector state asks for a ten-bit link.
+///
+/// A compositor sets `max bpc` to ten on every connector it can, so the request alone is not the
+/// condition: the vendor moves a link to ten bits when the connector is driven in PQ. A commit
+/// carrying no connector state, which is every page flip, asks for nothing.
+fn asks_for_ten_bits(
+ conn: Option<&kernel::drm::kms::connector::OpaqueConnectorState<VinoDrmDriver>>,
+) -> bool {
+ conn.is_some_and(|conn| {
+ conn.max_requested_bpc() >= 10
+ && conn.hdr_output_eotf() == Some(connector::Eotf::SmpteSt2084)
+ })
+}
+
+#[vtable]
+impl crtc::DriverCrtc for VinoCrtc {
+ type Args = u8;
+ type Driver = VinoDrmDriver;
+ type State = VinoCrtcState;
+ type VblankImpl = Self;
+
+ fn new(_device: &drm::Device<Self::Driver>, connector: &u8) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoCrtc {
+ connector: *connector,
+ vblank: Arc::pin_init(VblankTimer::new(), GFP_KERNEL)?,
+ vblank_pinned <- new_mutex!(None),
+ })
+ }
+
+ /// The display is turning on (scanout begins). Enables vblank pacing, pushes a live mode-set CP
+ /// message for the negotiated mode. The command is queued and is a no-op until CP engages.
+ const HAS_ATOMIC_CHECK: bool = true;
+
+ /// Reject a commit that exceeds the dock's combined active-connector budget.
+ ///
+ /// `mode_valid` checks each connector against the complete budget because the
+ /// advertised modes must not depend on another connector's current state.
+ /// A commit that does not increase this connector's rate is always allowed (it can only hold or
+ /// reduce the combined total); no limiting when the budget is 0 (unknown).
+ fn atomic_check(check: CrtcAtomicCheck<'_, Self>) -> Result {
+ let crtc = check.crtc();
+ let connector = crtc.connector as usize;
+ let data: &VinoDrmData = crtc.drm_dev();
+ let budget = data.dock_budget();
+ let (state, old, mut new) = check.take_all();
+ let wants_deep =
+ data.hdr_capable() && asks_for_ten_bits(state.new_connector_state_for_crtc(crtc));
+ // The colour description reaches the dock in the mode-set message, which is only sent from
+ // `atomic_enable`. Toggling HDR on a live output changes nothing the core considers a mode
+ // change, so ask for one: otherwise the compositor starts encoding PQ into a sink that was
+ // never told, and the picture washes out.
+ let colour_changed = {
+ let eotf = |c: Option<&_>| -> (u32, Option<connector::Eotf>) {
+ c.map_or(
+ (0, None),
+ |c: &kernel::drm::kms::connector::OpaqueConnectorState<VinoDrmDriver>| {
+ (c.colorspace(), c.hdr_output_eotf())
+ },
+ )
+ };
+ eotf(state.old_connector_state_for_crtc(crtc))
+ != eotf(state.new_connector_state_for_crtc(crtc))
+ };
+ if colour_changed {
+ new.set_mode_changed(true);
+ }
+ let old_rate = if old.active() {
+ let m = old.mode();
+ active_pixel_rate(m.hdisplay(), m.vdisplay(), m.vrefresh())
+ } else {
+ 0
+ };
+ let new_rate = if new.active() {
+ let m = new.mode();
+ if (!old.active() || new.mode_changed()) && !crate::cp::mode_supported(m) {
+ pr_warn!(
+ "vino: socket {socket} mode {}x{}@{} has no dock profile\n",
+ m.hdisplay(),
+ m.vdisplay(),
+ m.vrefresh(),
+ socket = connector + 1
+ );
+ return Err(EINVAL);
+ }
+ active_pixel_rate(m.hdisplay(), m.vdisplay(), m.vrefresh())
+ } else {
+ 0
+ };
+ // Refresh ceiling, enforced here as well as in `mode_valid`, because pruning the mode list
+ // is not a limit: a client can commit a user-defined mode that was never advertised
+ // (`xrandr --newmode`, a modeline in a compositor config, `drm_mode_setcrtc` with its own
+ // timing). This is the check that actually stops the dock being driven at a rate it goes
+ // dark on, and it costs one comparison on the commit path.
+ //
+ // Only a commit that raises the refresh is rejected, exactly as the budget check below only
+ // examines a commit that raises the rate. Every page flip carries the CRTC state through
+ // here, so revalidating an unchanged rate would only add work to the hot path.
+ let old_refresh = if old.active() {
+ old.mode().vrefresh()
+ } else {
+ 0
+ };
+ let new_refresh = if new.active() {
+ new.mode().vrefresh()
+ } else {
+ 0
+ };
+ if new_refresh > old_refresh && !data.refresh_within_limit(new_refresh) {
+ let limit = data.max_refresh_hz();
+ pr_warn!(
+ "vino: socket {socket} refresh {new_refresh} exceeds {limit} Hz\n",
+ socket = connector + 1
+ );
+ return Err(EINVAL);
+ }
+ if budget == 0 || new_rate <= old_rate {
+ return Ok(());
+ }
+ let others = data.other_connectors_rate(&state, connector);
+ let combined = new_rate.saturating_add(others);
+ // Price the dock at the depth this commit drives, and at what its other connectors already
+ // hold, because the bandwidth is shared. Nothing is recorded here: a check also runs for
+ // page flips and for `TEST_ONLY` commits that are never applied, so a depth recorded from
+ // one would move the codec with no mode set to re-state it to the dock.
+ let deep = wants_deep && data.ten_bit_fits(combined);
+ let budget = data.budget_at_depth(
+ budget,
+ deep || data.other_connector_programmed_ten_bit(crtc.connector),
+ );
+ if combined > budget {
+ pr_warn!(
+ "vino: socket {socket} combined rate {combined} exceeds {budget}\n",
+ socket = connector + 1
+ );
+ return Err(EINVAL);
+ }
+ Ok(())
+ }
+
+ fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
+ let crtc = commit.crtc();
+ crtc.vblank_on();
+ // Keep the software presentation clock running for the complete active interval. The
+ // reference is stored as an owned one because it must outlive this callback and is only
+ // released in `atomic_disable`. Page-flip events take their own additional refs.
+ let mut pinned = crtc.vblank_pinned.lock();
+ if pinned.is_none() {
+ match crtc.vblank_get() {
+ Ok(vblank_ref) => *pinned = Some(vblank_ref.into_owned()),
+ Err(e) => pr_warn!(
+ "vino: failed to start connector {} software vblank clock ({e:?})\n",
+ crtc.connector
+ ),
+ }
+ }
+ drop(pinned);
+ let connector = crtc.connector;
+ let dev: &VinoDrmDevice = crtc.drm_dev();
+ let data: &VinoDrmData = dev;
+ let (state, new) = commit.take_state_new_state();
+ // The transfer function is a connector property, but the mode set that carries it to the
+ // dock is built here, so read it across from the connector routed to this CRTC. Only PQ
+ // is distinguished: the dock's flags word has exactly one HDR bit, and every other EOTF
+ // (including HLG, which it cannot express) is carried as SDR rather than mislabelled.
+ let st2084 = state
+ .new_connector_state_for_crtc(crtc)
+ .and_then(|conn| conn.hdr_output_eotf())
+ == Some(connector::Eotf::SmpteSt2084);
+ data.set_connector_st2084(connector, st2084);
+ // The link depth userspace asked for, which is a separate question from the framebuffer's
+ // format: an eight-bit surface over a ten-bit link is the ordinary case, and reading the
+ // format alone silently ignores the request.
+ let requested_bpc = state
+ .new_connector_state_for_crtc(crtc)
+ .map_or(0, |conn| conn.max_requested_bpc());
+ data.set_connector_max_bpc(connector, requested_bpc);
+ // Decide the depth rather than refuse the mode: a pair that fits at eight bits may not fit
+ // at ten, and a compositor answers a rejected mode by disabling the output. Recorded here
+ // and not in the check, so that the decision and the set-mode carrying it are one commit.
+ let combined = if new.active() {
+ let m = new.mode();
+ active_pixel_rate(m.hdisplay(), m.vdisplay(), m.vrefresh())
+ } else {
+ 0
+ }
+ .saturating_add(data.other_connectors_rate(&state, connector as usize));
+ let wants_deep =
+ data.hdr_capable() && asks_for_ten_bits(state.new_connector_state_for_crtc(crtc));
+ data.set_connector_ten_bit_denied(connector, wants_deep && !data.ten_bit_fits(combined));
+ // Cache this connector's colour transform for the scanout to apply.
+ data.update_color(connector as usize, new.gamma_lut(), new.ctm());
+ // Whatever this connector's sink state was, the enable path re-runs the full bracket and
+ // mode-set, so any silence from here is the dock's news, not vino's.
+ data.set_self_blanked(connector as usize, false);
+ // The depth the dock is told must match the depth the plane will actually send.
+ // `connector_ten_bit` is set from the committed framebuffer's fourcc, so a connector that
+ // never gets a 10-bit buffer is never announced as 30 bpp. It goes in rather than being
+ // patched on afterwards because the framebuffer allocation the set-mode states is derived
+ // from it.
+ let ten_bit = data.connector_is_ten_bit(connector as usize);
+ let timing = match crate::cp::timing_from_drm_mode(new.mode(), data.allocation(), ten_bit) {
+ Ok(mut timing) => {
+ // Read back rather than reusing the local, so both halves of the colour
+ // description come from the same per-connector state every other path consults.
+ timing.st2084 = data.connector_is_st2084(connector as usize);
+ // Declare the shared video endpoint. Four connectors are multiplexed onto two bulk
+ // endpoints, and DLM names bit 2 of the flags word `Dual NIVO`; a connector whose
+ // partner connector is also live has to say so, or the dock drives only one of the
+ // two streams it is being sent.
+ timing.dual_nivo = data.endpoint_is_shared(connector as usize);
+ timing
+ }
+ Err(e) => {
+ pr_err!(
+ "vino: connector {} reached atomic enable with an unsupported mode ({e:?})\n",
+ connector
+ );
+ return;
+ }
+ };
+ vino_debug!(
+ "vino: KMS CRTC enable -- connector {} display ON, mode {}x{}@{} {} bpc{} (scanout begins)\n",
+ connector,
+ timing.hactive,
+ timing.vactive,
+ timing.refresh_hz,
+ if timing.ten_bit { 10 } else { 8 },
+ if timing.st2084 { " PQ" } else { "" }
+ );
+ // Publish the desired timing; atomic callbacks must not block on USB.
+ let mode_key = timing_key(&timing);
+ data.last_timing.lock()[connector as usize] = Some(timing);
+ data.modeset_requested[connector as usize].store(mode_key, Ordering::Release);
+ data.queue_cmd(dev, KmsCmd::ModeSet { connector, timing });
+ }
+
+ /// The display is turning off (DPMS-off/blank/suspend all land here in atomic KMS).
+ /// Resets the scanout state so a later re-enable sends a full keyframe rather than diffing
+ /// against a shadow the dock may have dropped. Do not send the monitor's DDC/CI VCP 0xd6
+ /// here: hard standby is separate from stopping the DisplayLink stream and can leave a panel
+ /// asleep across a dock power cycle.
+ fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
+ let crtc = commit.crtc();
+ // Dropping the stored reference releases the vblank reference `atomic_enable` took.
+ drop(crtc.vblank_pinned.lock().take());
+ crtc.vblank_off();
+ let connector = crtc.connector;
+ let dev: &VinoDrmDevice = crtc.drm_dev();
+ let data: &VinoDrmData = dev;
+ data.update_color(connector as usize, None, None);
+ // The stream is torn down; a later re-enable must re-send the mode-set before any video
+ // write (the dock EPIPEs a write onto an unconfigured stream). Forget the active mode so
+ // the scanout gate defers until the re-enable's mode-set lands.
+ data.modeset_requested[connector as usize].store(0, Ordering::Release);
+ data.modeset_active[connector as usize].store(0, core::sync::atomic::Ordering::Release);
+ data.programmed_timing.lock()[connector as usize] = None;
+ // Drop a framebuffer queued while this CRTC was active. Otherwise the deferred worker can
+ // retry its old mode and paint after DPMS-off.
+ data.pending_scanout.lock()[connector as usize] = None;
+ data.settle_repaint.lock()[connector as usize] = None;
+ // Spend nothing on a connector that is off; the re-enable's mode-set refills it.
+ data.settle_budget[connector as usize].store(0, Ordering::Relaxed);
+ // Release the ~14.7 MB private copy; a re-enable owes a keyframe and re-snapshots.
+ data.shadow[connector as usize].lock().discard();
+ data.sustain_until.lock()[connector as usize] = None;
+ data.strip_hashes.lock()[connector as usize] = None;
+ data.dirty_ttl.lock()[connector as usize] = None;
+ vino_debug!(
+ "vino: KMS CRTC disable -- socket {socket} display OFF (scanout stopped)\n",
+ socket = connector + 1
+ );
+ // Stopping locally is not enough: the dock goes on scanning out whatever it last received,
+ // so a DPMS-off left the panel lit on a frozen desktop. Queue the dock-side take-down for
+ // the command worker -- this callback must not block on USB (see `KmsCmd`). It is queued
+ // last, after the mode generation has been zeroed, because `blank_connector` keys its write
+ // on exactly that zero.
+ data.queue_cmd(dev, KmsCmd::Blank { connector });
+ }
+
+ /// Arm the page-flip completion event to be sent by the next vblank tick, so userspace is paced
+ /// to the refresh rate rather than signalled immediately.
+ fn atomic_flush(commit: CrtcAtomicCommit<'_, Self>) {
+ let crtc = commit.crtc();
+ let data: &VinoDrmData = crtc.drm_dev();
+ let mut new = commit.take_new_state();
+ // Re-cache the colour transform on every commit that touches this CRTC, so a dynamic
+ // GAMMA_LUT or CTM change on an already-enabled connector (which does not re-run
+ // atomic_enable) is picked up rather than deferred to the next full modeset. A night-light
+ // corrector ramps its CTM continuously, so this is the path that carries it, not
+ // atomic_enable.
+ data.update_color(crtc.connector as usize, new.gamma_lut(), new.ctm());
+ if let Some(pending) = new.get_pending_vblank_event() {
+ match crtc.vblank_get() {
+ Ok(vbl_ref) => pending.arm(vbl_ref),
+ // Vblank couldn't be enabled (e.g. mid-teardown): fall back to sending now.
+ Err(_) => pending.send(),
+ }
+ }
+ }
+}
+
+impl VblankSupport for VinoCrtc {
+ type Crtc = VinoCrtc;
+
+ fn enable_vblank(
+ crtc: &crtc::Crtc<Self::Crtc>,
+ vblank_guard: &VblankGuard<'_, Self::Crtc>,
+ irq: &LocalInterruptDisabled,
+ ) -> Result {
+ let data: &VinoCrtc = crtc;
+ // Track the mode's real frame duration so the tick matches the negotiated refresh rate.
+ let fd = vblank_guard.frame_duration();
+ if fd > 0 {
+ data.vblank.interval_ns.store(fd as i64, Ordering::Relaxed);
+ }
+ // Publish the CRTC for the timer callback. Only the first enable stores it; the CRTC a
+ // given timer serves never changes, and re-taking the reference on every enable would just
+ // leak one `drm_dev_get()` per DPMS cycle. `lock_with` because the DRM core already called
+ // us with local interrupts disabled -- proven by the `irq` token.
+ {
+ let mut published = data.vblank.crtc.lock_with(irq);
+ if published.is_none() {
+ *published = Some(crtc.to_owned_ref());
+ }
+ }
+ data.vblank.enabled.store(true, Ordering::Relaxed);
+ let interval = data.vblank.interval_ns.load(Ordering::Relaxed);
+ // The started timer is registered on the DEVICE, so teardown can cancel it without
+ // depending on the DRM core calling `disable_vblank` -- see `VinoDrmData::vblank`.
+ let drm_data: &VinoDrmData = crtc.drm_dev();
+ let connector = usize::from(data.connector);
+ if connector >= MAX_CONNECTORS {
+ return Ok(());
+ }
+ let mut slots = drm_data.vblank.lock();
+ match &slots[connector] {
+ None => {
+ // First enable: start the timer and keep the handle as its sole owner.
+ slots[connector] = Some((
+ data.vblank.clone(),
+ data.vblank.clone().start(Delta::from_nanos(interval)),
+ ));
+ }
+ Some((_, h)) => {
+ // Re-enable after `disable_vblank` let the timer die (NoRestart): re-queue it in
+ // place. `restart` removes and re-inserts a still-pending timer, so this is
+ // correct whether the final disabled tick has already fired or not, and it never
+ // blocks on the callback -- which matters because we are called under the vblank
+ // locks with interrupts disabled.
+ h.restart(Delta::from_nanos(interval));
+ }
+ }
+ Ok(())
+ }
+
+ fn disable_vblank(
+ crtc: &crtc::Crtc<Self::Crtc>,
+ _vblank_guard: &VblankGuard<'_, Self::Crtc>,
+ _irq: &LocalInterruptDisabled,
+ ) {
+ let data: &VinoCrtc = crtc;
+ data.vblank.enabled.store(false, Ordering::Relaxed);
+ }
+
+ fn get_vblank_timestamp(
+ _crtc: &crtc::Crtc<Self::Crtc>,
+ _in_vblank_irq: bool,
+ ) -> Option<VblankTimestamp> {
+ // Let DRM estimate the timestamp from the mode timings.
+ None
+ }
+}
+
+// ---- Planes: primary (scanout) + cursor -------------------------------------
+//
+// The safe KMS layer allows one `DriverPlane` type per driver, so `VinoPlane` serves both the
+// primary and cursor planes, told apart by `is_cursor` (from the plane's `Args`).
+
+/// Constructor arguments for a [`VinoPlane`]: which connector it belongs to and whether it is that
+/// connector's cursor plane (vs. its primary scanout plane).
+#[derive(Clone, Copy)]
+pub(crate) struct PlaneArgs {
+ pub(super) connector: u8,
+ pub(super) is_cursor: bool,
+}
+
+#[pin_data]
+pub(crate) struct VinoPlane {
+ /// Which display connector (0-based) this plane belongs to. Selects the scanout video endpoint
+ /// (see `DockProfile::video_endpoints`) and the cursor CP `connector` field.
+ connector: u8,
+ /// Whether this is the cursor plane (vs. the primary scanout plane).
+ is_cursor: bool,
+ /// The framebuffer region last uploaded as the cursor bitmap.
+ #[pin]
+ cursor_last: Mutex<Option<CursorUpload>>,
+}
+
+struct CursorUpload {
+ framebuffer: ARef<kms::framebuffer::Framebuffer<VinoDrmDriver>>,
+ /// Value of this connector's `cursor_epoch` when the bitmap was sent. A newer epoch means the
+ /// dock has since been reconfigured and is no longer holding it.
+ epoch: u32,
+}
+
+#[derive(Clone, Default)]
+pub(crate) struct VinoPlaneState;
+
+impl plane::DriverPlaneState for VinoPlaneState {
+ type Plane = VinoPlane;
+}
+
+#[vtable]
+impl plane::DriverPlane for VinoPlane {
+ type Args = PlaneArgs;
+ type Driver = VinoDrmDriver;
+ type State = VinoPlaneState;
+
+ fn new(_device: &drm::Device<Self::Driver>, args: PlaneArgs) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoPlane {
+ connector: args.connector,
+ is_cursor: args.is_cursor,
+ cursor_last <- new_mutex!(None),
+ })
+ }
+
+ /// Validate plane geometry and populate `drm_plane_state.visible`, which the damage iterator
+ /// requires before it can report changed rectangles.
+ fn atomic_check(check: PlaneAtomicCheck<'_, Self>) -> Result {
+ let plane = check.plane();
+ let (state, _old, mut new) = check.take_all();
+ let Some(crtc) = new.crtc::<VinoDrmDriver>() else {
+ // A disabled plane is not visible and needs no geometry validation.
+ return Ok(());
+ };
+ let crtc_state = match state.get_new_crtc_state(crtc) {
+ Some(s) => s,
+ None => state.add_crtc_state(crtc)?,
+ };
+ // Vino supports 1:1 scanout only. Primary planes must cover the CRTC; cursor planes may be
+ // positioned and clipped by the helper. Updates on a disabled CRTC remain disallowed.
+ new.atomic_helper_check::<_, VinoDrmDriver>(&crtc_state, plane.is_cursor, false)?;
+
+ // The transfer paths currently consume a full framebuffer, not an arbitrary source crop.
+ // Require exactly that at the UAPI boundary. Cursor clipping performed by the helper above
+ // is represented separately in its derived source rectangle.
+ if let Some(fb) = new.framebuffer::<VinoDrmDriver>() {
+ let full_width = fb.width().checked_shl(16).ok_or(EINVAL)?;
+ let full_height = fb.height().checked_shl(16).ok_or(EINVAL)?;
+ if new.source_x_16_16() != 0
+ || new.source_y_16_16() != 0
+ || new.source_width_16_16() != full_width
+ || new.source_height_16_16() != full_height
+ {
+ return Err(EINVAL);
+ }
+ }
+
+ Ok(())
+ }
+
+ /// A new framebuffer was flipped in. Maps it, converts XRGB8888 -> RGB565 (or feeds the
+ /// Haar colour codec directly for an aligned mode), and bulk-writes the resulting EP08
+ /// frame(s).
+ ///
+ /// EP08 writes happen only after CP engagement and a matching mode-set has landed.
+ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
+ let plane = commit.plane();
+ let connector = plane.connector;
+ let dev: &VinoDrmDevice = plane.drm_dev();
+ let data: &VinoDrmData = dev;
+ if !data.cp_engaged.load(core::sync::atomic::Ordering::SeqCst) {
+ return;
+ }
+
+ // Cursor plane: publish bitmap and position commands for the asynchronous control worker.
+ // The protocol uses id=0x1b for create, id=0x1c with the inner bitmap flag set for image,
+ // and id=0x1a for movement.
+ if plane.is_cursor {
+ let new = commit.take_new_state();
+ match new.framebuffer::<VinoDrmDriver>() {
+ Some(fb) => {
+ let Some(source) = new.visible_source().ok().flatten() else {
+ *plane.cursor_last.lock() = None;
+ data.queue_cmd(
+ dev,
+ KmsCmd::CursorMove {
+ connector,
+ x: 0,
+ y: 0,
+ visible: false,
+ },
+ );
+ return;
+ };
+ let Some(destination) = new.visible_destination() else {
+ return;
+ };
+ // The complete framebuffer, not the helper's clipped rectangle: the dock
+ // expects a fixed-size cursor and clips at the panel edge itself.
+ let Ok(w) = u16::try_from(fb.width()) else {
+ return;
+ };
+ let Ok(h) = u16::try_from(fb.height()) else {
+ return;
+ };
+ let epoch = data.cursor_epoch[usize::from(connector)].load(Ordering::Acquire);
+ let mut last = plane.cursor_last.lock();
+ // Re-send when the bitmap changed, or when a reconfigure means the dock is no
+ // longer holding it: `CursorMove` succeeds against a cursor that no longer
+ // exists, so a stale match is silent on the wire.
+ let unchanged = last.as_ref().is_some_and(|last| {
+ core::ptr::eq(&*last.framebuffer, fb) && last.epoch == epoch
+ });
+ if !unchanged {
+ if let Ok(bgra) = read_cursor_bgra(fb, usize::from(w), usize::from(h)) {
+ // One shared bitmap per device: announce geometry only when it
+ // changes, not on every shape change.
+ let hi = usize::from(connector);
+ if data.cursor_geometry.lock()[hi].replace((w, h)) != Some((w, h)) {
+ data.queue_cmd(dev, KmsCmd::CursorCreate { connector, w, h });
+ }
+ data.queue_cmd(
+ dev,
+ KmsCmd::CursorImage {
+ connector,
+ w,
+ h,
+ bgra,
+ },
+ );
+ *last = Some(CursorUpload {
+ framebuffer: ARef::from(fb),
+ epoch,
+ });
+ }
+ }
+ // The dock positions the whole bitmap by its top-left, so this is the
+ // unclipped origin: scanout is 1:1, so how far into the source the helper
+ // started is how far off-screen the origin is. Clamped because the wire
+ // coordinates are unsigned -- the pointer stops at the edge.
+ let Ok(x) = u16::try_from((destination.x1 - source.x1).max(0)) else {
+ return;
+ };
+ let Ok(y) = u16::try_from((destination.y1 - source.y1).max(0)) else {
+ return;
+ };
+ data.queue_cmd(
+ dev,
+ KmsCmd::CursorMove {
+ connector,
+ x,
+ y,
+ visible: true,
+ },
+ );
+ }
+ // Cursor disabled: clear the dock's visible flag and forget the bitmap so a later
+ // enable uploads it again.
+ None => {
+ *plane.cursor_last.lock() = None;
+ data.queue_cmd(
+ dev,
+ KmsCmd::CursorMove {
+ connector,
+ x: 0,
+ y: 0,
+ visible: false,
+ },
+ );
+ }
+ }
+ return;
+ }
+
+ // Primary plane: take both old and new state so the frame-damage clips can be merged.
+ let (old, new) = commit.take_old_new_state();
+ let Some(fb) = new.framebuffer::<VinoDrmDriver>() else {
+ return;
+ };
+ // Plane rotation/reflection (identity unless the compositor set the rotation property).
+ let rotation = new.rotation();
+ // atomic_check has already rejected scaling, positioning, and partial source rectangles,
+ // so these destination dimensions describe the complete output and cannot overrun the
+ // framebuffer under any advertised rotation.
+ let (w, h) = (new.crtc_w() as usize, new.crtc_h() as usize);
+ // Collect the client's individual frame-damage clips (the rectangles that
+ // `damage_merged()` would collapse into one bounding box), each clamped to the output, so
+ // only the genuinely changed rectangles are re-converted from the source rather than their
+ // whole enclosing box. Only for identity rotation (the clips are in un-rotated source
+ // space; mapping them through 90/270 is not worth it for the throttled fallback path), and
+ // never on the Haar keyframe path -- see `encode_and_send`. A fixed stack array keeps the
+ // atomic-commit path allocation-free; on overflow the clips collapse into one bounding box.
+ // An empty list means the client reported no changed pixels. Rotation/reflection still
+ // promotes it to a full frame in `encode_and_send_haar`, because source-space clips cannot
+ // yet be transformed safely for those cases.
+ let mut clips = [(0usize, 0usize, 0usize, 0usize); MAX_DAMAGE_CLIPS];
+ let mut nclips = 0usize;
+ if rotation.angle() == plane::Rotation::ROTATE_0
+ && !rotation.contains(plane::Rotation::REFLECT_X | plane::Rotation::REFLECT_Y)
+ {
+ new.for_each_damage_clip(old, |r| {
+ let c = (
+ (r.x1.max(0) as usize).min(w),
+ (r.y1.max(0) as usize).min(h),
+ (r.x2.max(0) as usize).min(w),
+ (r.y2.max(0) as usize).min(h),
+ );
+ if nclips < MAX_DAMAGE_CLIPS {
+ clips[nclips] = c;
+ nclips += 1;
+ } else {
+ // Overflow: collapse ALL accumulated clips plus `c` into a single bounding box
+ // in clips[0]. Folding only clips[0] with `c` here would silently drop the
+ // damage in clips[1..], leaving those regions stale on screen; union every
+ // pending clip so the whole changed area is still repainted.
+ let mut bb = c;
+ for &r in &clips[..nclips] {
+ bb = (bb.0.min(r.0), bb.1.min(r.1), bb.2.max(r.2), bb.3.max(r.3));
+ }
+ clips[0] = bb;
+ nclips = 1;
+ }
+ });
+ }
+
+ use core::sync::atomic::Ordering::Relaxed;
+ // Throttle: while scanout is failing (dock NAKing because CP isn't engaged), skip the
+ // upcoming pageflips set by the backoff below instead of converting+encoding+sending a
+ // frame the dock will just drop.
+ let skip = data.scanout_skip[connector as usize].load(Relaxed);
+ if skip > 0 {
+ data.scanout_skip[connector as usize].store(skip - 1, Relaxed);
+ return;
+ }
+ data.queue_scanout(
+ dev,
+ fb,
+ PendingScanout {
+ connector,
+ rotation,
+ clips,
+ nclips,
+ w,
+ h,
+ shadow_idx: 0,
+ shadow_generation: 0,
+ },
+ );
+ }
+}
+
+/// The connector's fixed encoder. The dock has no encoder configuration of its own.
+#[pin_data]
+pub(crate) struct VinoEncoder;
+
+#[vtable]
+impl encoder::DriverEncoder for VinoEncoder {
+ type Driver = VinoDrmDriver;
+ type Args = ();
+
+ fn new(_device: &drm::Device<Self::Driver>, _args: ()) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoEncoder {})
+ }
+}
+
+// ---- Connector --------------------------------------------------------------
+
+#[pin_data]
+pub(crate) struct VinoConnector {
+ /// Index into the owning device's per-connector EDID/presence arrays.
+ connector: u8,
+}
+
+#[derive(Clone, Default)]
+pub(crate) struct VinoConnectorState;
+
+impl connector::DriverConnectorState for VinoConnectorState {
+ type Connector = VinoConnector;
+}
+
+#[vtable]
+impl connector::DriverConnector for VinoConnector {
+ type Args = u8;
+ type Driver = VinoDrmDriver;
+ type State = VinoConnectorState;
+
+ fn new(_device: &drm::Device<Self::Driver>, connector: u8) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoConnector { connector })
+ }
+
+ /// Install the dock's real EDID (read during probe) when available; otherwise fall back
+ /// to a single 1920x1080@60 CVT mode. Reading the real EDID gives the true monitor
+ /// name/size and its native mode list; the fallback keeps the connector usable when
+ /// nothing is plugged into the dock or the CP channel has not yet delivered the EDID.
+ fn get_modes<'a>(
+ connector: ConnectorGuard<'a, Self>,
+ guard: &ModeConfigGuard<'a, Self::Driver>,
+ ) -> i32 {
+ let data: &VinoDrmData = connector.drm_dev();
+ let edids = data.cached_edids.lock();
+ if let Some(blob) = edids
+ .get(connector.connector as usize)
+ .and_then(Option::as_ref)
+ {
+ // A failed EDID update adds no modes; fall through to the built-in list rather than
+ // reporting a count the core did not actually get.
+ match connector.add_edid_modes(blob) {
+ Ok(n) if n > 0 => return n,
+ other => {
+ // A cached EDID that yields no modes is indistinguishable downstream from a
+ // socket with nothing in it: both land on the synthesised list. Say which.
+ vino_debug!(
+ "vino: connector {} EDID ({} B) produced no modes ({:?}); using the built-in list\n",
+ connector.connector,
+ blob.len(),
+ other.map(|n| n)
+ );
+ }
+ }
+ }
+ drop(edids);
+ let _ = guard;
+ // A connector handed to DRM's EDID override must report NO modes here: the core applies the
+ // override (`drm_kms_helper.edid_firmware=`, or a debugfs `edid_override` write) purely as
+ // a fallback for a connector that is connected and produced none. Synthesising the
+ // built-in list below would satisfy the probe helper and the override would never be
+ // consulted.
+ if data.edid_from_userspace(connector.connector as usize) {
+ return 0;
+ }
+ // No downstream EDID yet: advertise the standard mode list up to the fallback resolution
+ // and prefer it, keeping the connector usable until the dock delivers a real EDID.
+ let n = connector.add_modes_noedid((FALLBACK_W as u32, FALLBACK_H as u32));
+ connector.set_preferred_mode((FALLBACK_W as u32, FALLBACK_H as u32));
+ n
+ }
+
+ /// Report the connector connected once the dock has delivered this connector's downstream EDID
+ /// (a real monitor is attached and described) OR the bring-up work item confirmed CP engagement
+ /// + this connector's DISPLAY-CAP push (`connectors_present`: on 3.4.26 the raw-EDID path can
+ /// fail, so cached EDID alone would leave every connector permanently disconnected despite a
+ /// fully-engaged dock). A connector with neither stays disconnected rather than advertising a
+ /// phantom output.
+ fn detect(connector: &Connector<Self>, _force: bool) -> Status {
+ let data: &VinoDrmData = connector.drm_dev();
+ let connector = connector.connector as usize;
+ if connector >= data.connector_count() {
+ return Status::Disconnected;
+ }
+ // On a dock that reports no downstream presence, both signals are absent for a connector
+ // that has a monitor on it, and the whole measured choreography -- the framebuffer
+ // allocation the set-mode states, the mode-set bracket, the sink states -- is the vendor
+ // driving every connector it has. Offering a subset of that leaves the dock configured for
+ // a display arrangement nobody described; `get_modes` supplies the fallback list until an
+ // EDID arrives.
+ if !data.reports_presence() {
+ // The dock recovers an EDID for the socket a monitor is in and nothing for the one
+ // that is empty, so the EDID is the whole presence signal here. The socket with
+ // nothing in it is still configured -- it joins the dock-wide transaction at its
+ // sibling's mode -- so it no longer has to be advertised to hold its place.
+ //
+ // Bring-up owns the endpoint until encrypted setup publishes the runtime CP link, so
+ // a connector must not be offered before then whatever its EDID says.
+ return if data.connector_has_edid(connector) && data.kms_activation_ready() {
+ Status::Connected
+ } else {
+ Status::Disconnected
+ };
+ }
+ let has_edid = data
+ .cached_edids
+ .lock()
+ .get(connector)
+ .is_some_and(Option::is_some);
+ let present = data
+ .connectors_present
+ .load(core::sync::atomic::Ordering::Acquire)
+ & (1 << connector)
+ != 0;
+ if has_edid || present {
+ Status::Connected
+ } else {
+ Status::Disconnected
+ }
+ }
+
+ /// Prune modes whose pixel clock exceeds a single connector's bandwidth ceiling
+ /// ([`MAX_HEAD_CLOCK_KHZ`], ~4K@60), whose refresh rate exceeds what the dock has ever been
+ /// shown to display ([`DOCK_MAX_REFRESH_HZ`]), or whose pixel rate exceeds the dock's budget.
+ fn mode_valid(connector: ConnectorModeValidation<'_, Self>, mode: &DisplayMode) -> ModeStatus {
+ let data: &VinoDrmData = connector.drm_dev();
+ if mode.clock() < 0 || mode.clock() as u32 > data.max_connector_clock_khz() {
+ return ModeStatus::ClockHigh;
+ }
+ if !data.refresh_within_limit(mode.vrefresh()) {
+ return ModeStatus::ClockHigh;
+ }
+ if !crate::cp::mode_supported(mode) {
+ return ModeStatus::Bad;
+ }
+ // Reject a mode only when that connector exceeds the dock's whole pixel budget. The atomic
+ // CRTC check enforces the combined rate of simultaneously active connectors.
+ let budget = data.dock_budget();
+ let connector_rate = active_pixel_rate(mode.hdisplay(), mode.vdisplay(), mode.vrefresh());
+ if budget != 0 && connector_rate > budget {
+ return ModeStatus::Bad;
+ }
+ ModeStatus::Ok
+ }
+}
+
+/// Map an output pixel `(dx, dy)` back to its source-framebuffer pixel `(sx, sy)` under a DRM
+/// plane `rotation` bitmask (`DRM_MODE_ROTATE_*` | `DRM_MODE_REFLECT_*`, the values the
+/// standard `drm_plane_create_rotation_property` exposes). `sw`/`sh` are the source
+/// (framebuffer) dimensions. Rotation is clockwise; reflection is applied in source space
+/// after rotation. Pure and total (saturating), so it is unit-tested directly. Applied per source
+/// pixel in [`encode_and_send`]/[`encode_and_send_haar`] for the plane's rotation property.
+#[inline]
+pub(crate) fn rot_src(
+ rotation: plane::Rotation,
+ dx: usize,
+ dy: usize,
+ sw: usize,
+ sh: usize,
+) -> (usize, usize) {
+ let xmax = sw.saturating_sub(1);
+ let ymax = sh.saturating_sub(1);
+ let rot = rotation.angle();
+ let (mut sx, mut sy) = if rot == plane::Rotation::ROTATE_90 {
+ (dy, ymax.saturating_sub(dx))
+ } else if rot == plane::Rotation::ROTATE_180 {
+ (xmax.saturating_sub(dx), ymax.saturating_sub(dy))
+ } else if rot == plane::Rotation::ROTATE_270 {
+ (xmax.saturating_sub(dy), dx)
+ } else {
+ (dx, dy) // ROTATE_0 / unset
+ };
+ if rotation.contains(plane::Rotation::REFLECT_X) {
+ sx = xmax.saturating_sub(sx);
+ }
+ if rotation.contains(plane::Rotation::REFLECT_Y) {
+ sy = ymax.saturating_sub(sy);
+ }
+ (sx, sy)
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_mode_objects)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn rotation_pixel_mapping() {
+ use drm::kms::plane::Rotation;
+
+ // Source 2x3 (sw=2, sh=3). 0deg is identity; 180deg mirrors both axes.
+ assert_eq!(rot_src(Rotation::ROTATE_0, 0, 0, 2, 3), (0, 0));
+ assert_eq!(rot_src(Rotation::ROTATE_0, 1, 2, 2, 3), (1, 2));
+ assert_eq!(rot_src(Rotation::ROTATE_180, 0, 0, 2, 3), (1, 2));
+ assert_eq!(rot_src(Rotation::ROTATE_180, 1, 2, 2, 3), (0, 0));
+ // 90deg: output dims are (sh,sw)=(3,2); (dx,dy) -> (dy, sh-1-dx).
+ assert_eq!(rot_src(Rotation::ROTATE_90, 0, 0, 2, 3), (0, 2));
+ assert_eq!(rot_src(Rotation::ROTATE_90, 2, 1, 2, 3), (1, 0));
+ // 270deg: (dx,dy) -> (sw-1-dy, dx).
+ assert_eq!(rot_src(Rotation::ROTATE_270, 0, 0, 2, 3), (1, 0));
+ assert_eq!(rot_src(Rotation::ROTATE_270, 2, 1, 2, 3), (0, 2));
+ // Reflect-X composes on top of the rotation (here identity): sx -> sw-1-sx.
+ assert_eq!(
+ rot_src(Rotation::ROTATE_0 | Rotation::REFLECT_X, 0, 0, 2, 3),
+ (1, 0)
+ );
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/settings.rs b/drivers/gpu/drm/vino/drm_sink/settings.rs
new file mode 100644
index 000000000000..2142b5f20230
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/settings.rs
@@ -0,0 +1,574 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The runtime knobs a matched dock profile installs on the KMS device.
+//!
+//! The KMS side has one code path per operation and no per-dock branches; every difference
+//! between generations arrives here as data, is stored in an atomic, and is read back by the
+//! workers. Nothing in this module decides anything -- [`crate::profile`] does.
+
+use super::*;
+
+impl VinoDrmData {
+ /// Record this device's codec geometry; see [`Self::geometry`].
+ pub(crate) fn set_codec_geometry(
+ &self,
+ strip_blocks_x: usize,
+ interlaced: bool,
+ band_parity: bool,
+ connector_selector_shift: u8,
+ stream_id_mask: u8,
+ dock_buffers: u8,
+ coding: crate::video_arm::CodeTables,
+ steady_sub_bit: u8,
+ ) {
+ let narrow = coding == crate::video_arm::CodeTables::Narrow;
+ let packed = (strip_blocks_x as u32 & 0xff)
+ | ((interlaced as u32) << 8)
+ | ((band_parity as u32) << 9)
+ | ((narrow as u32) << 10)
+ | (((steady_sub_bit != 0) as u32) << 11)
+ | ((connector_selector_shift as u32) << 16)
+ | ((stream_id_mask as u32) << 20)
+ | ((dock_buffers as u32) << 28);
+ self.codec_geometry
+ .store(packed | 0x8000, core::sync::atomic::Ordering::Release);
+ }
+
+ /// This device's codec geometry, to be passed into every codec call made on its behalf.
+ ///
+ /// Stored packed because the DRM device allocation is pin-initialised before `probe` knows
+ /// which dock it matched. A device with no profile applied reads the Ridge layout.
+ pub(crate) fn geometry(&self) -> crate::video::haar::Geometry {
+ let p = self
+ .codec_geometry
+ .load(core::sync::atomic::Ordering::Acquire);
+ if p & 0x8000 == 0 {
+ return crate::video::haar::RIDGE_GEOMETRY;
+ }
+ crate::video::haar::Geometry::new(
+ ((p & 0xff) as usize).max(1),
+ p & (1 << 8) != 0,
+ p & (1 << 9) != 0,
+ ((p >> 16) & 0xf) as u8,
+ ((p >> 20) & 0xff) as u8,
+ ((p >> 28) & 0xf) as u8,
+ )
+ .with_coding(if p & (1 << 10) != 0 {
+ crate::video_arm::CodeTables::Narrow
+ } else {
+ crate::video_arm::CodeTables::Wide
+ })
+ .with_steady_sub_bit(if p & (1 << 11) != 0 { 0x20 } else { 0 })
+ }
+
+ /// Record how this dock delivers logical framebuffer updates to its ring buffers.
+ pub(crate) fn set_frame_delivery(&self, policy: crate::profile::FrameDelivery) {
+ let packed = u32::from(policy.keyframe_presentations.max(1))
+ | (u32::from(policy.delta_presentations.max(1)) << 8)
+ | (u32::from(policy.damage_frames.max(1)) << 16);
+ self.frame_delivery.store(packed, Ordering::Release);
+ }
+
+ /// Snapshot this dock's frame-delivery policy.
+ pub(crate) fn frame_delivery(&self) -> crate::profile::FrameDelivery {
+ let packed = self.frame_delivery.load(Ordering::Acquire);
+ crate::profile::FrameDelivery::new(
+ ((packed & 0xff) as u8).max(1),
+ (((packed >> 8) & 0xff) as u8).max(1),
+ (((packed >> 16) & 0xff) as u8).max(1),
+ )
+ }
+
+ /// Record whether timed presence probes may reset a bracket beside a live connector.
+ pub(crate) fn set_probe_bracket(&self, policy: crate::profile::ProbeBracket) {
+ self.probe_bracket.store(policy as u8, Ordering::Release);
+ }
+
+ /// This dock's bracket-reset policy.
+ pub(crate) fn probe_bracket(&self) -> crate::profile::ProbeBracket {
+ match self.probe_bracket.load(Ordering::Acquire) {
+ x if x == crate::profile::ProbeBracket::DeferWithActiveSibling as u8 => {
+ crate::profile::ProbeBracket::DeferWithActiveSibling
+ }
+ _ => crate::profile::ProbeBracket::Always,
+ }
+ }
+
+ /// Whether this dock can be driven at ten bits per channel; see [`DockProfile::hdr_capable`].
+ pub(crate) fn hdr_capable(&self) -> bool {
+ self.hdr_capable.load(Ordering::Acquire)
+ }
+
+ /// Whether this dock composites a cursor bitmap of its own; see [`DockProfile::hw_cursor`].
+ pub(crate) fn hw_cursor(&self) -> bool {
+ self.hw_cursor.load(Ordering::Acquire)
+ }
+
+ /// Record whether the dock's presence probe describes a connector; see
+ /// [`DockProfile::reports_presence`].
+ pub(crate) fn set_reports_presence(&self, on: bool) {
+ self.reports_presence.store(on, Ordering::Release);
+ }
+
+ /// Whether the dock's presence probe describes a connector; see
+ /// [`DockProfile::reports_presence`].
+ pub(crate) fn reports_presence(&self) -> bool {
+ self.reports_presence.load(Ordering::Acquire)
+ }
+
+ /// Record whether the connectors share one EDID handler; see
+ /// [`DockProfile::shared_edid_handler`].
+ pub(crate) fn set_shared_edid_handler(&self, on: bool) {
+ self.shared_edid_handler.store(on, Ordering::Release);
+ }
+
+ /// Whether the connectors share one EDID handler; see [`DockProfile::shared_edid_handler`].
+ pub(crate) fn shared_edid_handler(&self) -> bool {
+ self.shared_edid_handler.load(Ordering::Acquire)
+ }
+
+ /// Record whether a frame ending on a full packet is split; see
+ /// [`DockProfile::split_full_packet_frame`].
+ pub(crate) fn set_split_full_packet_frame(&self, on: bool) {
+ self.split_full_packet_frame.store(on, Ordering::Release);
+ }
+
+ /// Whether a frame ending on a full packet is split.
+ pub(crate) fn split_full_packet_frame(&self) -> bool {
+ self.split_full_packet_frame.load(Ordering::Acquire)
+ }
+
+ /// Whether `connector`'s committed framebuffer is 10 bits per channel.
+ ///
+ /// Read by the mode-set path, which must announce a depth to the dock that matches the one the
+ /// plane will actually send: the dock sizes its buffer from the pair and mis-sizes it if they
+ /// disagree.
+ pub(super) fn connector_is_ten_bit(&self, connector: usize) -> bool {
+ self.connector_wire_ten_bit(connector as u8)
+ }
+
+ /// Whether `connector`'s connector is being driven in PQ; see [`Self::set_connector_st2084`].
+ pub(super) fn connector_is_st2084(&self, connector: usize) -> bool {
+ self.head_st2084.load(Ordering::Acquire) & (1u32 << connector) != 0
+ }
+
+ /// Record the transfer function userspace has asked for on `connector`.
+ ///
+ /// Driven by the connector's `HDR_OUTPUT_METADATA` blob rather than by anything vino decides,
+ /// for the same reason [`Self::set_connector_depth`] follows the framebuffer's fourcc: the dock
+ /// must be told what the pixels actually are, and any state of our own could drift from them.
+ pub(super) fn set_connector_st2084(&self, connector: u8, on: bool) {
+ let bit = 1u32 << u32::from(connector);
+ if on {
+ self.head_st2084.fetch_or(bit, Ordering::Release);
+ } else {
+ self.head_st2084.fetch_and(!bit, Ordering::Release);
+ }
+ }
+
+ /// Record the link depth userspace asked this connector to carry.
+ ///
+ /// Four bits per connector is enough for every value `max bpc` takes, and keeping them in one
+ /// word means the scanout path reads the whole set with a single load.
+ pub(super) fn set_connector_max_bpc(&self, connector: u8, bpc: u32) {
+ let shift = u32::from(connector) * 4;
+ let field = bpc.min(15) << shift;
+ let mask = !(0xfu32 << shift);
+ let mut current = self.connector_max_bpc.load(Ordering::Acquire);
+ loop {
+ let next = (current & mask) | field;
+ match self.connector_max_bpc.compare_exchange_weak(
+ current,
+ next,
+ Ordering::AcqRel,
+ Ordering::Acquire,
+ ) {
+ Ok(_) => break,
+ Err(seen) => current = seen,
+ }
+ }
+ }
+
+ /// The sample depth of the framebuffer this connector is scanning out.
+ ///
+ /// This is how a pixel must be *decoded*; [`Self::geometry_for_connector`] says what the dock
+ /// is told to carry, and the two differ whenever userspace asks for a deeper link than the
+ /// surface it hands over.
+ pub(super) fn connector_buffer_depth(&self, connector: u8) -> crate::video::haar::Depth {
+ if self.connector_ten_bit.load(Ordering::Acquire) & (1u32 << u32::from(connector)) != 0 {
+ crate::video::haar::Depth::Ten
+ } else {
+ crate::video::haar::Depth::Eight
+ }
+ }
+
+ /// Whether this connector's link is driven at ten bits per channel.
+ ///
+ /// True when the framebuffer is already ten-bit, and when userspace asks for a ten-bit link
+ /// through `max bpc` on a dock that can carry one and is driving the connector in PQ. An
+ /// eight-bit surface over a ten-bit link is the ordinary case on every other driver.
+ ///
+ /// This is the decision, not the depth in force. Read it only where a mode set carries the
+ /// answer to the dock; everything else wants [`Self::connector_programmed_ten_bit`].
+ pub(super) fn connector_wire_ten_bit(&self, connector: u8) -> bool {
+ if self.connector_ten_bit.load(Ordering::Acquire) & (1u32 << u32::from(connector)) != 0 {
+ return true;
+ }
+ if self.connector_deny_ten_bit.load(Ordering::Acquire) & (1u32 << u32::from(connector)) != 0
+ {
+ return false;
+ }
+ let shift = u32::from(connector) * 4;
+ let requested = (self.connector_max_bpc.load(Ordering::Acquire) >> shift) & 0xf;
+ self.hdr_capable() && requested >= 10 && self.connector_is_st2084(connector as usize)
+ }
+
+ /// Record whether this connector's ten-bit link fits the dock's shared bandwidth.
+ pub(super) fn set_connector_ten_bit_denied(&self, connector: u8, denied: bool) {
+ let bit = 1u32 << u32::from(connector);
+ if denied {
+ self.connector_deny_ten_bit.fetch_or(bit, Ordering::Release);
+ } else {
+ self.connector_deny_ten_bit
+ .fetch_and(!bit, Ordering::Release);
+ }
+ }
+
+ /// Whether any connector other than `connector` is programmed at ten bits per channel.
+ ///
+ /// The dock's bandwidth is shared, so a commit is priced at the dock's deepest connector.
+ pub(super) fn other_connector_programmed_ten_bit(&self, connector: u8) -> bool {
+ (0..self.connector_count())
+ .any(|c| c as u8 != connector && self.connector_programmed_ten_bit(c as u8))
+ }
+
+ /// Whether the mode this connector was last programmed with drives ten bits per channel.
+ ///
+ /// The depth the dock is decoding at, so the only one the codec may encode at and the only one
+ /// the set-mode and the decoder configuration may state. Deliberately not
+ /// [`Self::connector_wire_ten_bit`], which moves between mode sets.
+ pub(super) fn connector_programmed_ten_bit(&self, connector: u8) -> bool {
+ self.last_timing
+ .lock()
+ .get(connector as usize)
+ .copied()
+ .flatten()
+ .is_some_and(|t| t.ten_bit)
+ }
+
+ /// This device's codec geometry at one connector's programmed sample depth.
+ ///
+ /// Every path that touches pixels wants this rather than [`Self::geometry`]: the depth decides
+ /// the entropy coder's escape ceiling, and getting it wrong desynchronises the dock's decoder
+ /// rather than merely degrading the picture. See [`crate::video::haar::Depth`].
+ pub(super) fn geometry_for_connector(&self, connector: u8) -> crate::video::haar::Geometry {
+ let ten = self.connector_programmed_ten_bit(connector);
+ self.geometry().with_depth(if ten {
+ crate::video::haar::Depth::Ten
+ } else {
+ crate::video::haar::Depth::Eight
+ })
+ }
+
+ /// Record the sample depth of the framebuffer a connector is scanning out.
+ ///
+ /// Driven by the committed framebuffer's fourcc rather than by any state of our own, so it
+ /// cannot drift from the pixels actually in hand. A format the codec does not know leaves the
+ /// connector where it was; `atomic_check` is what rejects those, and a plane list that only
+ /// offers `XRGB8888` means this never sees one.
+ pub(super) fn set_connector_depth(&self, connector: u8, depth: crate::video::haar::Depth) {
+ let bit = 1u32 << u32::from(connector);
+ let previous = match depth {
+ crate::video::haar::Depth::Ten => {
+ self.connector_ten_bit.fetch_or(bit, Ordering::Release)
+ }
+ crate::video::haar::Depth::Eight => {
+ self.connector_ten_bit.fetch_and(!bit, Ordering::Release)
+ }
+ };
+ // Report the change, because this is the only place the sample depth is decided and
+ // nothing else on the wire says what was chosen. A connector that advertises ten bits and
+ // is driven in ST2084 still goes out at eight if the compositor never hands over a
+ // ten-bit framebuffer, and that difference is otherwise visible only on the panel.
+ let was_ten = previous & bit != 0;
+ let now_ten = matches!(depth, crate::video::haar::Depth::Ten);
+ if was_ten != now_ten {
+ let socket = connector + 1;
+ let bits = if now_ten { 10 } else { 8 };
+ vino_debug!("vino: socket {socket} scanout depth is now {bits} bits per channel\n");
+ }
+ }
+
+ /// Record the mode-programming and blanking behaviour this dock wants.
+ pub(crate) fn set_mode_behaviour(&self, profile: &'static crate::profile::DockProfile) {
+ self.dock_wide_modeset
+ .store(profile.protocol.dock_wide_modeset, Ordering::Release);
+ self.clear_mode_before_set
+ .store(profile.protocol.clear_mode_before_set, Ordering::Release);
+ self.video_keepalive
+ .store(profile.protocol.video_keepalive, Ordering::Release);
+ self.blank_markers_held.store(
+ matches!(
+ profile.protocol.blank_bracket,
+ crate::profile::BlankBracket::MarkersHeld
+ ),
+ Ordering::Release,
+ );
+ }
+
+ /// Whether a connector must keep being fed while its content is unchanged.
+ pub(crate) fn video_keepalive(&self) -> bool {
+ self.video_keepalive.load(Ordering::Acquire)
+ }
+
+ /// Whether programming any connector reconfigures the whole dock.
+ pub(crate) fn dock_wide_modeset(&self) -> bool {
+ self.dock_wide_modeset.load(Ordering::Acquire)
+ }
+
+ /// Whether a connector's pipe is torn down before a timing is programmed onto it.
+ pub(crate) fn clear_mode_before_set(&self) -> bool {
+ self.clear_mode_before_set.load(Ordering::Acquire)
+ }
+
+ /// How a connector blanks; see [`crate::profile::BlankBracket`].
+ pub(crate) fn blank_bracket(&self) -> crate::profile::BlankBracket {
+ if self.blank_markers_held.load(Ordering::Acquire) {
+ crate::profile::BlankBracket::MarkersHeld
+ } else {
+ crate::profile::BlankBracket::BlackThenClose
+ }
+ }
+
+ /// Record how this dock states its framebuffer allocation in a set-mode.
+ pub(crate) fn set_allocation(&self, allocation: &'static crate::profile::Allocation) {
+ let _ = self.allocation.populate(allocation);
+ }
+
+ /// How this dock states its framebuffer allocation; Ridge's device override until probe has
+ /// matched a profile, as with every other value published there.
+ pub(crate) fn allocation(&self) -> &'static crate::profile::Allocation {
+ self.allocation
+ .as_ref()
+ .copied()
+ .unwrap_or(&crate::profile::PROFILE_RIDGE.protocol.allocation)
+ }
+
+ /// Record whether this dock opens a stream with the ARM burst; see `DockProfile::arm_burst`.
+ pub(crate) fn set_arm_burst(&self, on: bool) {
+ self.arm_burst.store(on, Ordering::Release);
+ }
+
+ /// Whether the first frame after a mode set carries the cold ARM burst.
+ pub(super) fn uses_arm_burst(&self) -> bool {
+ self.arm_burst.load(Ordering::Acquire)
+ }
+
+ /// Length of a continuous-presentation window, for this dock.
+ ///
+ /// The activation carrier and the blank presentation both work by presenting one encoded frame
+ /// back to back for a fixed wall-clock window, with no control transaction in between. That
+ /// trains a downstream link on a dock with a video pipe of its own. On a dock that carries
+ /// video on the control pipe it instead holds the endpoint for the whole window, and the dock
+ /// is silenced at exactly the moment the mode set needs it to answer. Such a dock gets a
+ /// single presentation instead, which is what `submit_prompt_training` does at zero.
+ pub(super) fn carrier_ms(&self, base: i64) -> i64 {
+ if self.video_on_ctrl_pipe() {
+ 0
+ } else {
+ base
+ }
+ }
+
+ /// How many carrier frames a connector presents before its first content frame; see
+ /// `DockProfile::carrier_frames`.
+ pub(super) fn carrier_presentations(&self) -> u32 {
+ self.carrier_frames
+ .load(core::sync::atomic::Ordering::Relaxed)
+ }
+
+ /// Record how this dock's video stream describes itself.
+ ///
+ /// The three values travel together because they are read together, by the one builder that
+ /// states a stream's mode and decoder tables.
+ pub(crate) fn set_video_stream_desc(
+ &self,
+ layout_word: u16,
+ marker_kind: u8,
+ tables: crate::video_arm::CodeTables,
+ ) {
+ let narrow = matches!(tables, crate::video_arm::CodeTables::Narrow);
+ let packed =
+ u32::from(layout_word) | (u32::from(marker_kind) << 16) | ((narrow as u32) << 24);
+ self.video_stream_desc.store(packed, Ordering::Release);
+ }
+
+ /// The word repeated beside the surface size in this dock's stream mode header.
+ pub(crate) fn layout_word(&self) -> u16 {
+ self.video_stream_desc.load(Ordering::Acquire) as u16
+ }
+
+ /// The byte naming this dock in a sealed stream's opening marker.
+ pub(crate) fn stream_marker_kind(&self) -> u8 {
+ (self.video_stream_desc.load(Ordering::Acquire) >> 16) as u8
+ }
+
+ /// Which form of decoder code tables this dock's stream configuration states.
+ pub(crate) fn code_tables(&self) -> crate::video_arm::CodeTables {
+ if self.video_stream_desc.load(Ordering::Acquire) & (1 << 24) != 0 {
+ crate::video_arm::CodeTables::Narrow
+ } else {
+ crate::video_arm::CodeTables::Wide
+ }
+ }
+
+ /// Record this dock's minimum frame interval; see `DockProfile::frame_period_ms`.
+ pub(crate) fn set_frame_period_ms(&self, ms: i64) {
+ let ms = if ms <= 0 { FRAME_PERIOD_MS } else { ms };
+ self.frame_period_us
+ .store(ms * 1000, core::sync::atomic::Ordering::Relaxed);
+ }
+
+ /// Record how many carrier frames open a stream; see `DockProfile::carrier_frames`.
+ pub(crate) fn set_carrier_frames(&self, frames: u32) {
+ self.carrier_frames
+ .store(frames.max(1), core::sync::atomic::Ordering::Relaxed);
+ }
+
+ /// Record this dock's keepalive status interval; see `DockProfile::status_period_ms`.
+ pub(crate) fn set_status_period_ms(&self, ms: i64) {
+ let ms = if ms <= 0 { STATUS_PERIOD_MS } else { ms };
+ self.status_period_ms
+ .store(ms, core::sync::atomic::Ordering::Relaxed);
+ }
+
+ /// This dock's interval between keepalive status queries, in milliseconds.
+ pub(crate) fn status_period_ms(&self) -> i64 {
+ self.status_period_ms
+ .load(core::sync::atomic::Ordering::Relaxed)
+ }
+
+ /// This dock's minimum interval between frames on one connector, in milliseconds.
+ pub(crate) fn frame_period_ms(&self) -> i64 {
+ self.frame_period_us() / 1000
+ }
+
+ /// This dock's minimum interval between frames on one connector, in microseconds.
+ pub(super) fn frame_period_us(&self) -> i64 {
+ self.frame_period_us
+ .load(core::sync::atomic::Ordering::Relaxed)
+ }
+
+ /// Record how much of this dock's endpoint may be occupied; see `DockProfile::stream_pacing`.
+ pub(crate) fn set_stream_pacing(&self, pacing: crate::profile::StreamPacing) {
+ self.stream_budget_bps.store(
+ pacing.bytes_per_sec.max(1),
+ core::sync::atomic::Ordering::Relaxed,
+ );
+ self.stream_burst_bytes.store(
+ pacing.burst_bytes.max(1),
+ core::sync::atomic::Ordering::Relaxed,
+ );
+ let mut credit = self.stream_credit.lock();
+ *credit = StreamCredit::new();
+ }
+
+ /// How long a frame must wait for this dock's sustained budget, or `None` to send it now.
+ ///
+ /// Tops the ledger up for the time since it was last read, so an idle dock is always in credit
+ /// and this costs a busy dock one spinlock per frame.
+ pub(super) fn stream_budget_wait_us(&self) -> Option<i64> {
+ let bps = self
+ .stream_budget_bps
+ .load(core::sync::atomic::Ordering::Relaxed);
+ if bps == u32::MAX {
+ return None;
+ }
+ let now = Instant::<Monotonic>::now();
+ let mut credit = self.stream_credit.lock();
+ let elapsed_us = credit
+ .topped_up
+ .map_or(1_000_000, |last| (now - last).as_micros_ceil());
+ credit.topped_up = Some(now);
+ // Cap the ledger at the burst allowance, not at a second of throughput. A dock idle for a
+ // minute must not bank a minute of bytes; nor may it bank a whole second's worth, which is
+ // more than this dock survives in one go.
+ let ceiling = i64::from(
+ self.stream_burst_bytes
+ .load(core::sync::atomic::Ordering::Relaxed),
+ );
+ credit.bytes = credit
+ .bytes
+ .saturating_add(stream_credit_accrued(bps, elapsed_us))
+ .min(ceiling);
+ stream_credit_wait_us(bps, credit.bytes)
+ }
+
+ /// Charge a frame that reached the dock against the sustained budget.
+ pub(super) fn charge_stream_budget(&self, bytes: usize) {
+ if self
+ .stream_budget_bps
+ .load(core::sync::atomic::Ordering::Relaxed)
+ == u32::MAX
+ {
+ return;
+ }
+ let mut credit = self.stream_credit.lock();
+ credit.bytes = credit.bytes.saturating_sub(bytes as i64);
+ }
+
+ /// Record the state that takes this dock's sinks down; see `DockProfile::sink_down_state`.
+ /// Record the `0x2e` state re-sent mid-bracket; see `DockProfile::bracket_reopen_state`.
+ pub(crate) fn set_post_mode_sink_states(&self, states: [u8; 2]) {
+ let packed = u16::from(states[0]) | (u16::from(states[1]) << 8);
+ self.post_mode_sink_states
+ .store(packed, core::sync::atomic::Ordering::Release);
+ }
+
+ /// Record the state this dock wants before a mode set; see
+ /// `DockProfile::pre_mode_sink_state`.
+ pub(crate) fn set_pre_mode_sink_state(&self, state: Option<u8>) {
+ self.pre_mode_sink_state.store(
+ state.map_or(u16::MAX, u16::from),
+ core::sync::atomic::Ordering::Release,
+ );
+ }
+
+ pub(crate) fn pre_mode_sink_state(&self) -> Option<u8> {
+ match self
+ .pre_mode_sink_state
+ .load(core::sync::atomic::Ordering::Acquire)
+ {
+ u16::MAX => None,
+ state => Some(state as u8),
+ }
+ }
+
+ pub(super) fn post_mode_sink_state(&self, index: usize) -> u8 {
+ let packed = self
+ .post_mode_sink_states
+ .load(core::sync::atomic::Ordering::Acquire);
+ (packed >> (8 * index)) as u8
+ }
+
+ pub(crate) fn set_sink_down_state(&self, state: u8) {
+ self.sink_down_state
+ .store(state, core::sync::atomic::Ordering::Release);
+ }
+
+ /// The `0x16/0x2e` state that takes a downstream sink down on this dock.
+ pub(crate) fn sink_down_state(&self) -> u8 {
+ self.sink_down_state
+ .load(core::sync::atomic::Ordering::Acquire)
+ }
+
+ /// Record whether video shares the control pipe; see `DockProfile::video_on_ctrl_pipe`.
+ pub(crate) fn set_video_on_ctrl_pipe(&self, on: bool) {
+ self.video_on_ctrl_pipe.store(on, Ordering::Release);
+ }
+
+ /// Whether video records travel on the control bulk-OUT pipe.
+ pub(crate) fn video_on_ctrl_pipe(&self) -> bool {
+ self.video_on_ctrl_pipe.load(Ordering::Acquire)
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/worker.rs b/drivers/gpu/drm/vino/drm_sink/worker.rs
new file mode 100644
index 000000000000..f06f469d1715
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/worker.rs
@@ -0,0 +1,571 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The workqueue items that carry out what the atomic callbacks asked for.
+//!
+//! One item reconciles KMS state, one per connector drives scanout, and one watches the
+//! control plane for silence. They are the only contexts in the driver allowed to block on
+//! USB.
+
+use super::*;
+
+impl_has_delayed_work! {
+ impl HasDelayedWork<VinoDrmDevice> for VinoDrmData { self.cmd_work }
+ impl HasDelayedWork<VinoDrmDevice, 5> for VinoDrmData { self.cp_watchdog }
+}
+
+impl WorkItem<5> for VinoDrmData {
+ type Pointer = ARef<VinoDrmDevice>;
+ fn run(this: ARef<VinoDrmDevice>) {
+ run_cp_watchdog(this);
+ }
+}
+
+/// Enforce the control session's silence deadline from off the control path.
+///
+/// Checking the deadline when a caller arrives to start a transaction is too late by
+/// construction: the thread that would arrive is the keepalive, and the keepalive is the thread
+/// that gets stuck -- a wedge then runs to 15 s against a 5 s limit, and ends only because the
+/// dock re-enumerates. This runs on the system queue, so it is scheduled whatever vino's own
+/// queues are doing, and it touches nothing a stuck transfer can be holding.
+pub(super) fn run_cp_watchdog(this: ARef<VinoDrmDevice>) {
+ let data: &VinoDrmData = &this;
+ if data.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ // Vino must not call a dock silent while vino has chosen not to speak to it. Navarro's
+ // setup-to-first-mode-set hold runs as long as the deadline itself, so holding the deadline
+ // off here -- rather than at each of the two places the hold ends -- is what stops a healthy
+ // cold bring-up from abandoning its own session at the boundary.
+ if data.initial_modeset_quiet() {
+ data.note_cp_reply();
+ } else if data.cp_link_alive() {
+ let silent_ms = data.cp_silent_for_ms();
+ if silent_ms >= data.cp_silence_limit_ms() {
+ data.abandon_cp_session(silent_ms);
+ data.drop_connectors_with_session(&this);
+ data.reset_after_wedge();
+ }
+ }
+ data.start_cp_watchdog(&this);
+}
+
+impl_has_work! {
+ impl HasWork<VinoDrmDevice, 1> for VinoDrmData { self.scanout_work_h0 }
+ impl HasWork<VinoDrmDevice, 2> for VinoDrmData { self.scanout_work_h1 }
+ impl HasWork<VinoDrmDevice, 3> for VinoDrmData { self.scanout_work_h2 }
+ impl HasWork<VinoDrmDevice, 4> for VinoDrmData { self.scanout_work_h3 }
+}
+
+/// One scanout work item exists per connector, and its work ID is a const generic. Keep this
+/// assertion adjacent to the explicit fields/arms below: adding another connector without all
+/// three would silently leave its frames in `pending_scanout`.
+const _: () = assert!(
+ MAX_CONNECTORS == 4,
+ "add a scanout_work_hN work item per connector (see VinoDrmData::enqueue_scanout)"
+);
+
+impl WorkItem<1> for VinoDrmData {
+ type Pointer = ARef<VinoDrmDevice>;
+ fn run(this: ARef<VinoDrmDevice>) {
+ run_scanout_worker(this, 0);
+ }
+}
+
+impl WorkItem<2> for VinoDrmData {
+ type Pointer = ARef<VinoDrmDevice>;
+ fn run(this: ARef<VinoDrmDevice>) {
+ run_scanout_worker(this, 1);
+ }
+}
+
+impl WorkItem<3> for VinoDrmData {
+ type Pointer = ARef<VinoDrmDevice>;
+ fn run(this: ARef<VinoDrmDevice>) {
+ run_scanout_worker(this, 2);
+ }
+}
+
+impl WorkItem<4> for VinoDrmData {
+ type Pointer = ARef<VinoDrmDevice>;
+ fn run(this: ARef<VinoDrmDevice>) {
+ run_scanout_worker(this, 3);
+ }
+}
+
+/// One connector's deferred scanout loop: pick this connector's due frame, encode it, transmit it,
+/// repeat until the connector has nothing left to do. Both connectors run concurrently on the
+/// per-device scanout queue.
+///
+/// A queued `ModeSet` must reach the dock before video for that connector, so this worker returns
+/// while a stream command is pending or executing. `cmd_work` re-enqueues the scanout workers when
+/// the command batch completes, and the pending framebuffer remains in its coalescing slot.
+///
+/// Two conditions, and both are needed: a stream operation in `pending_kms` (not yet drained), and
+/// [`VinoDrmData::cmd_busy`] (drained and executing -- the window in which `pending_kms` is
+/// misleadingly empty). The `video_inflight` store must be published *before* reading `cmd_busy`,
+/// and both use `SeqCst`, so this and `wait_for_video_idle` cannot both conclude the other is idle.
+pub(super) fn run_scanout_worker(this: ARef<VinoDrmDevice>, connector: usize) {
+ use core::sync::atomic::Ordering::SeqCst;
+ let data: &VinoDrmData = &this;
+ // Plane updates can be accepted once CP engages, before the later platform readiness interval
+ // finishes. Leave their coalesced frames untouched until bring-up publishes activation safety;
+ // the publisher wakes every scanout worker after opening this gate.
+ if !data.kms_activation_ready() {
+ return;
+ }
+ // As in `cmd_work`: once the I/O window refuses a token, unplug has begun and there is no USB
+ // left to do. `drm_dev_enter()` holds the parent interface Bound for the duration.
+ let Ok(link) = crate::UsbLink::open(&data.io, data.endpoints) else {
+ return;
+ };
+ let dev = &link;
+ loop {
+ if data.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ // Claim the connector's video endpoint first, then look for a reason not to use it.
+ data.video_inflight[connector].store(true, SeqCst);
+ let blocked = data.cmd_busy.load(SeqCst) || data.pending_kms.lock().has_stream();
+ if blocked {
+ data.video_inflight[connector].store(false, SeqCst);
+ return;
+ }
+ let (frame, cadence_wait_us) = data.select_scanout(connector);
+ if let Some(frame) = frame {
+ run_pending_scanout(dev, data, frame);
+ data.video_inflight[connector].store(false, SeqCst);
+ continue;
+ }
+ data.video_inflight[connector].store(false, SeqCst);
+ if let Some(us) = cadence_wait_us {
+ // Bound the sleep. The settle-repaint arm can ask for its full deadline
+ // ([`SETTLE_REPAINT_MS`], 1.2 s); sleeping that long inside the work item makes the
+ // connector unreachable, because a flip arriving meanwhile finds the item already
+ // running and its enqueue is dropped. Waking at the cadence window instead costs a few
+ // extra wakeups while idle and keeps the connector responsive to real frames.
+ let us = us.min(data.frame_period_us());
+ fsleep(Delta::from_micros(us));
+ continue;
+ }
+ // Re-check before exiting. A frame published between `select_scanout` above and this point
+ // finds the work item still running, so its `enqueue_scanout` is dropped and the frame
+ // waits for some *later* flip to enqueue successfully -- a lost wakeup that showed up as
+ // multi-second stalls on whichever connector lost the race. The condition mirrors
+ // `select_scanout`'s own guard so a connector with no mode-set cannot spin here.
+ if data.modeset_requested[connector].load(Ordering::Acquire) != 0
+ && data.pending_scanout.lock()[connector].is_some()
+ {
+ continue;
+ }
+ return;
+ }
+}
+
+impl WorkItem for VinoDrmData {
+ type Pointer = ARef<VinoDrmDevice>;
+
+ /// Reconcile the latest desired stream and cursor state from the atomic callbacks.
+ fn run(this: ARef<VinoDrmDevice>) {
+ let data: &VinoDrmData = &this;
+ // Registration deliberately precedes the blocking CP/EDID setup, so userspace can publish a
+ // complete atomic mode-set batch before the runtime session exists. Do not drain that batch
+ // yet: attempting its dock-wide activation returns ENODEV, then the ordinary command loop
+ // can split it into per-connector activations as setup becomes ready between those two
+ // attempts. A readiness deferral is not a failed transport operation, so it neither
+ // consumes `kms_retries` nor rewrites any pending slot; newer atomic state may continue to
+ // coalesce there. The common worker gate applies to every dock generation.
+ if !data.kms_activation_ready() {
+ return;
+ }
+ // `drm_dev_enter()` holds the parent USB interface in Bound typestate until this worker
+ // finishes. If unplug has begun, discard queued transport work without touching USB.
+ // The I/O window is closed by `disconnect()` before it returns, so once it refuses a token
+ // there is no USB left to do: discard the queued transport work.
+ let Ok(link) = crate::UsbLink::open(&data.io, data.endpoints) else {
+ return;
+ };
+ let dev = &link;
+ loop {
+ if data.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ // A dual-connector atomic commit runs `atomic_enable` once per connector, and each of
+ // those queues its own `ModeSet` and wakes this worker -- microseconds apart, but far
+ // less than it takes to get scheduled. Taking the first one alone turns one dock-wide
+ // wake into two single-connector activations and skips the cold choreography that arms
+ // the video endpoints, so wait, briefly and boundedly, for the siblings the compositor
+ // has already published a timing for.
+ {
+ let started = Instant::<Monotonic>::now();
+ let present = data.connectors_present.load(Ordering::Acquire);
+ loop {
+ let queued = data.pending_kms.lock().connectors.iter().enumerate().fold(
+ 0u32,
+ |acc, (h, p)| {
+ if matches!(p.stream, Some(KmsCmd::ModeSet { .. })) {
+ acc | (1u32 << h)
+ } else {
+ acc
+ }
+ },
+ );
+ // Nothing to wait for until at least one mode set has landed, and nothing
+ // left to wait for once every connector with a monitor is either already active
+ // or represented in this batch.
+ let outstanding = (0..MAX_CONNECTORS).any(|h| {
+ present & (1u32 << h) != 0
+ && queued & (1u32 << h) == 0
+ && data.modeset_active[h].load(Ordering::Acquire) == 0
+ });
+ if queued == 0
+ || !outstanding
+ || (Instant::<Monotonic>::now() - started).as_millis()
+ >= MODESET_BATCH_SETTLE_MS
+ {
+ break;
+ }
+ fsleep(Delta::from_millis(1));
+ }
+ }
+ let pending = core::mem::replace(&mut *data.pending_kms.lock(), PendingKms::new());
+ // A cold dual-connector atomic commit is one dock-wide wake: both mode-sets precede
+ // either connector's video. Detect that shape before consuming the owned state.
+ let mut dual_timings: [Option<crate::cp::Timing>; MAX_CONNECTORS] =
+ [None; MAX_CONNECTORS];
+ let mut cmd_connectors = 0u32;
+ for connector in &pending.connectors {
+ if let Some(KmsCmd::ModeSet {
+ connector: cmd_head,
+ timing,
+ }) = &connector.stream
+ {
+ let connector_index = *cmd_head as usize;
+ if connector_index < MAX_CONNECTORS {
+ cmd_connectors |= 1u32 << connector_index;
+ if data.modeset_active[connector_index].load(Ordering::Acquire) == 0
+ && data.modeset_requested[connector_index].load(Ordering::Acquire)
+ == timing_key(timing)
+ {
+ dual_timings[connector_index] = Some(*timing);
+ }
+ }
+ }
+ }
+ // A dock that comes up as one transaction over every connector it has needs a timing
+ // for each of them, and the compositor only describes the sockets it can see. A socket
+ // with nothing plugged into it still has to be configured -- the scanout path already
+ // declines to paint a connector with no EDID -- so let it join at its sibling's mode.
+ //
+ // The generation has to be published with the timing. A connector whose requested mode
+ // is zero is a connector the activation waits on and never gets, so it defers on every
+ // commit for as long as the dock is up, and that retry churn is what takes the shared
+ // pipe down. Publishing it is also what makes this happen once: the connector then has
+ // a request of its own and no longer looks unasked-for.
+ if data.video_on_ctrl_pipe() && dual_timings.iter().flatten().count() == 1 {
+ let sibling = dual_timings.iter().flatten().copied().next();
+ if let Some(timing) = sibling {
+ for connector in 0..data.connector_count().min(MAX_CONNECTORS) {
+ if dual_timings[connector].is_some()
+ || data.modeset_requested[connector].load(Ordering::Acquire) != 0
+ || data.modeset_active[connector].load(Ordering::Acquire) != 0
+ {
+ continue;
+ }
+ data.last_timing.lock()[connector] = Some(timing);
+ data.modeset_requested[connector]
+ .store(timing_key(&timing), Ordering::Release);
+ dual_timings[connector] = Some(timing);
+ cmd_connectors |= 1u32 << connector;
+ vino_debug!(
+ "vino: socket {} has no monitor and is configured at its sibling's mode\n",
+ connector + 1
+ );
+ }
+ }
+ }
+ // Exclude the scanout workers for exactly as long as this batch can touch a video
+ // endpoint. `activate_dual_wake` and the `ModeSet` arm both run
+ // `submit_prompt_training`, which writes the activation carrier to the connector's
+ // endpoint; a concurrent scanout frame there would interleave records on the wire and
+ // would have its `video_q` slot double-opened. Cursor-only batches deliberately skip
+ // this: they never touch video, and a mouse in motion produces a continuous stream of
+ // them. `Blank` writes the connector's video endpoint for the same reason `ModeSet`
+ // does, so it needs the same exclusion against the scanout workers -- otherwise a frame
+ // already in flight interleaves its records with the blanking frames on the wire.
+ let has_modeset = pending.has_stream();
+ // On the DL7400 a mode set is dock-wide, not per connector. Configuring one connector
+ // while any other is lit makes the dock re-enumerate about 100 ms after the next video
+ // write, on every shape of the change: 120 -> 165, 165 -> 120 and 120 -> 60 on a live
+ // connector, waking a second connector a second after the first, and reconfiguring a
+ // connector whose sibling has been lit and idle for minutes. The same changes with the
+ // sibling disabled are clean, and so is the simultaneous `activate_dual_wake` path. DLM
+ // behaves the same way: it logs `[Profile change] Recreating device` and re-runs a
+ // bring-up-shaped burst rather than reconfiguring one connector in place.
+ //
+ // So fold every already-active connector into this batch. Zeroing its mode generation
+ // makes `activate_dual_wake` treat it as a fresh wake, and the whole dock is then taken
+ // through the one choreography the hardware accepts. The cost is that the sibling
+ // blinks through a mode change on its neighbour; the alternative is a dock reset and
+ // tens of seconds of dark panels on both. `cmd_connectors` rather than `has_modeset`: a
+ // `Blank`-only batch also counts as a stream command, and it must not drag every lit
+ // connector through a re-activation. Folding the lit connectors in makes
+ // `activate_dual_wake` name every connector at once, which is the only shape of mode
+ // set this dock accepts while more than one connector is lit. This replays the cold
+ // choreography -- a dock-wide sink reset and pipe clears -- on a dock that is already
+ // driving its sinks. That is the cost of the only mode set this dock will take.
+ if cmd_connectors != 0 && data.dock_wide_modeset() {
+ // Gather the whole dock's desired state first, and only commit to it if at least
+ // two connectors end up in it. Below two there is nothing for the dual path to do
+ // and the per-connector schedule is the proven one, so nothing is disturbed.
+ let mut fold: [Option<crate::cp::Timing>; MAX_CONNECTORS] = [None; MAX_CONNECTORS];
+ for connector in 0..MAX_CONNECTORS {
+ // A connector this batch is already mode-setting: take the requested timing,
+ // even if the connector is currently lit. A live reconfigure is exactly the
+ // case that must not go down the per-connector path.
+ if cmd_connectors & (1u32 << connector) != 0 {
+ if let Some(timing) = data.last_timing.lock()[connector] {
+ if data.modeset_requested[connector].load(Ordering::Acquire)
+ == timing_key(&timing)
+ {
+ fold[connector] = Some(timing);
+ }
+ }
+ continue;
+ }
+ // A connector this batch does not name, but which is lit and still wants the
+ // mode it is showing. One whose request has already moved on has its own
+ // `ModeSet` queued behind this batch and is left to it.
+ let active = data.modeset_active[connector].load(Ordering::Acquire);
+ if active != 0
+ && data.modeset_requested[connector].load(Ordering::Acquire) == active
+ {
+ fold[connector] = data.last_timing.lock()[connector];
+ }
+ }
+ if fold.iter().flatten().count() >= 2 {
+ for connector in 0..MAX_CONNECTORS {
+ let socket = connector + 1;
+ let Some(timing) = fold[connector] else {
+ continue;
+ };
+ // `activate_dual_wake` only accepts a connector whose generation is zero; a
+ // dock-wide transaction re-establishes every connector from scratch, so say
+ // so.
+ data.modeset_active[connector].store(0, Ordering::Release);
+ dual_timings[connector] = Some(timing);
+ cmd_connectors |= 1u32 << connector;
+ vino_debug!(
+ "vino: socket {socket} joins a dock-wide mode set ({}x{}@{})\n",
+ timing.hactive,
+ timing.vactive,
+ timing.refresh_hz
+ );
+ }
+ }
+ }
+ if has_modeset {
+ data.cmd_busy
+ .store(true, core::sync::atomic::Ordering::SeqCst);
+ data.wait_for_video_idle();
+ }
+ // Both dock-wide schedules need two connectors coming up together; below that the
+ // per-connector path is the proven one. Which schedule applies is a property of the
+ // dock: the Ridge and DL7400 cold timeline consists of operations a dock carrying video
+ // on its control pipe does not take, and that dock has its own measured choreography in
+ // `ELLA_DOCK_WIDE`. Driving either one from the other's table fails every pass.
+ let both_connectors = dual_timings.iter().flatten().count() >= 2;
+ let dual_wake = both_connectors && !data.video_on_ctrl_pipe();
+ let dock_wide = both_connectors && data.video_on_ctrl_pipe();
+ if has_modeset {
+ vino_debug!(
+ "vino: KMS batch -- stream cmds {}, dual timings {}, dual_wake {}, requested [{} {} {} {}], active [{} {} {} {}]\n",
+ (0..MAX_CONNECTORS).filter(|&h| cmd_connectors & (1u32 << h) != 0).count(),
+ dual_timings.iter().flatten().count(),
+ dual_wake || dock_wide,
+ data.modeset_requested[0].load(Ordering::Acquire),
+ data.modeset_requested[1].load(Ordering::Acquire),
+ data.modeset_requested[2].load(Ordering::Acquire),
+ data.modeset_requested[3].load(Ordering::Acquire),
+ data.modeset_active[0].load(Ordering::Acquire),
+ data.modeset_active[1].load(Ordering::Acquire),
+ data.modeset_active[2].load(Ordering::Acquire),
+ data.modeset_active[3].load(Ordering::Acquire),
+ );
+ }
+ let multihead_attempted = dock_wide || dual_wake;
+ let dual_complete = if dock_wide {
+ match data.activate_dock_wide(dev, ELLA_DOCK_WIDE, dual_timings) {
+ Ok(done) => done,
+ Err(e) => {
+ pr_warn!("vino: dock-wide activation failed ({e:?})\n");
+ false
+ }
+ }
+ } else {
+ dual_wake
+ && match data.activate_dual_wake(dev, dual_timings) {
+ Ok(done) => done,
+ Err(e) => {
+ pr_warn!("vino: dual-connector activation failed ({e:?})\n");
+ false
+ }
+ }
+ };
+ if multihead_attempted && !dual_complete {
+ // A multihead activation is one indivisible transport transaction. In particular,
+ // never let a failed Ella cold table fall through to the ordinary per-connector
+ // loop: connector 0 can then light just as setup becomes ready and force connector
+ // 1 down the live runtime table, so the cold two-connector choreography never
+ // lands. Restore the entire owned batch; newer producer state already occupying a
+ // slot still wins.
+ if has_modeset {
+ data.cmd_busy
+ .store(false, core::sync::atomic::Ordering::SeqCst);
+ }
+ let mut retry_pending = data.pending_kms.lock();
+ retry_pending.retry_batch(pending);
+ let attempts = data.kms_retries.fetch_add(1, Ordering::Relaxed) + 1;
+ if attempts >= KMS_RETRY_LIMIT {
+ pr_warn!(
+ "vino: dropping atomic multihead KMS batch after {} deferrals; the link is not coming back on its own\n",
+ KMS_RETRY_LIMIT
+ );
+ retry_pending.clear();
+ data.kms_retries.store(0, Ordering::Relaxed);
+ return;
+ }
+ drop(retry_pending);
+ vino_debug!("vino: atomic multihead KMS batch deferred\n");
+ if !data.shutting_down.load(Ordering::Acquire) {
+ let delay = kernel::time::msecs_to_jiffies(KMS_RETRY_MS);
+ let _ = workqueue::system().enqueue_delayed::<_, 0>(ARef::from(&*this), delay);
+ }
+ return;
+ }
+ // Heads whose mode this batch actually re-programmed, and whose dock-side cursor is
+ // therefore gone. See `rearm_cursor`.
+ let mut relit = if dual_complete { cmd_connectors } else { 0 };
+ let mut cmds: [Option<KmsCmd>; MAX_CONNECTORS * 4] =
+ [const { None }; MAX_CONNECTORS * 4];
+ for (connector, pending) in pending.connectors.into_iter().enumerate() {
+ cmds[connector] = pending.stream;
+ cmds[MAX_CONNECTORS + connector] = pending.cursor_create;
+ cmds[MAX_CONNECTORS * 2 + connector] = pending.cursor_image;
+ cmds[MAX_CONNECTORS * 3 + connector] = pending.cursor_move;
+ }
+ // Control-plane ordering comes first. An enabling atomic commit queues the plane flip
+ // before its CRTC mode-set. Finish the mode transaction before
+ // selecting a pending framebuffer.
+ let mut cmds = cmds.into_iter().flatten();
+ let mut retry = false;
+ while let Some(cmd) = cmds.next() {
+ let mut mode_programmed = 0u32;
+ let res = match &cmd {
+ KmsCmd::ModeSet { connector, timing } => {
+ if dual_complete {
+ // `activate_dual_wake` consumed the current generation for both
+ // connectors. A superseding generation published while it ran remains
+ // in `pending_kms` for the next outer iteration.
+ continue;
+ }
+ let connector_index = *connector as usize;
+ let key = timing_key(timing);
+ if connector_index >= MAX_CONNECTORS
+ || data.modeset_requested[connector_index].load(Ordering::Acquire)
+ != key
+ {
+ Ok(()) // superseded or disabled while queued
+ } else {
+ data.activate_head(dev, *connector, timing, key)
+ .map(|activated| {
+ if activated {
+ mode_programmed = 1u32 << connector;
+ }
+ })
+ }
+ }
+ KmsCmd::CursorCreate { connector, w, h } => data.send_cp(dev, 0x1b, 0, |ctr| {
+ crate::cp::cursor_create(ctr, *connector, *w, *h)
+ }),
+ KmsCmd::CursorImage {
+ connector,
+ w,
+ h,
+ bgra,
+ } => data.send_cp(dev, 0x1c, 0, |ctr| {
+ crate::cp::cursor_image(ctr, *connector, *w, *h, bgra)
+ }),
+ KmsCmd::CursorMove {
+ connector,
+ x,
+ y,
+ visible,
+ } => data.send_cp(dev, 0x1a, 0, |ctr| {
+ crate::cp::cursor_move(ctr, *connector, *x, *y, *visible)
+ }),
+ KmsCmd::Blank { connector } => data.blank_connector(dev, *connector),
+ };
+ // Remember what the dock accepted, so a later mode set can put it back. Recorded
+ // here rather than where the atomic callback queues it, because only a command
+ // that actually went out describes the dock's state.
+ if res.is_ok() {
+ data.record_cursor(&cmd);
+ relit |= mode_programmed;
+ }
+ if let Err(e) = res {
+ if !kms_error_retryable(e) {
+ pr_warn!("vino: dropping invalid asynchronous KMS command ({e:?})\n");
+ continue;
+ }
+
+ // Preserve the failed command and everything ordered behind it. Concurrent
+ // atomic callbacks may already have published newer state into these slots;
+ // `retry` never replaces that newer state with this drained batch.
+ let mut pending = data.pending_kms.lock();
+ pending.retry(cmd);
+ for cmd in cmds {
+ pending.retry(cmd);
+ }
+ retry = true;
+ vino_debug!("vino: asynchronous KMS command deferred after {e:?}\n");
+ if data.kms_retries.fetch_add(1, Ordering::Relaxed) + 1 >= KMS_RETRY_LIMIT {
+ pr_warn!(
+ "vino: dropping asynchronous KMS work after {} deferrals ({e:?}); the link is not coming back on its own\n",
+ KMS_RETRY_LIMIT
+ );
+ pending.clear();
+ data.kms_retries.store(0, Ordering::Relaxed);
+ retry = false;
+ }
+ break;
+ }
+ }
+ if has_modeset {
+ data.cmd_busy
+ .store(false, core::sync::atomic::Ordering::SeqCst);
+ }
+ // Put each re-programmed connector's cursor back. Queued rather than sent inline so it
+ // drains through the ordinary path on the next turn of this loop, behind anything the
+ // compositor has published in the meantime -- a real cursor commit always wins.
+ if relit != 0 && !retry {
+ data.rearm_cursor(&this, relit);
+ }
+
+ if retry {
+ if !data.shutting_down.load(Ordering::Acquire) {
+ let delay = kernel::time::msecs_to_jiffies(KMS_RETRY_MS);
+ let _ = workqueue::system().enqueue_delayed::<_, 0>(ARef::from(&*this), delay);
+ }
+ return;
+ }
+ // A batch that got through means whatever was wrong has cleared.
+ data.kms_retries.store(0, Ordering::Relaxed);
+ if data.pending_kms.lock().is_empty() {
+ break;
+ }
+ }
+ // Wake both scanout workers after the command batch. They stop while
+ // a queued mode set must reach the dock before video and resume here.
+ data.enqueue_scanout_all(&this);
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 8/13] drm/vino: add the KMS device and the atomic path
2026-08-26 16:37 ` [PATCH v3 8/13] drm/vino: add the KMS device and the atomic path Mike Lothian
@ 2026-08-26 16:59 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 16:59 UTC (permalink / raw)
To: Mike Lothian; +Cc: dri-devel, ojeda
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Operations that sleep (Mutex locks, GFP_KERNEL allocations, and vmap) and long synchronous memory copies are performed within DRM atomic commit callbacks.
- [High] Page flips are permanently lost if an older frame is still pending, causing the screen to freeze on stale frames.
- [High] atomic_check reads shared global state without global atomic tracking, allowing concurrent commits to overcommit the dock's pixel bandwidth.
--
commit b222a22b8b0ea906ee63e26872096f97f4fb7ece
Author: Mike Lothian <mike@fireburn.co.uk>
drm/vino: add the KMS device and the atomic path
This commit adds the KMS side of the sink, including the DRM driver, GEM
and file types, CRTCs, planes, and connectors. It introduces atomic
callbacks to record desired state and workers to handle scanout, formatting,
and KMS limits checking.
> diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
> index 0000000000000..86708db89699a
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink.rs
[ ... ]
> + fn get(
> + &mut self,
> + fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
> + ) -> Result<Arc<SourceBinding>> {
[ ... ]
> + let binding = Arc::new(
> + SourceBinding {
> + framebuffer: ARef::from(fb),
> + mapping: fb.owned_vmap::<VinoObject>()?,
> + },
> + GFP_KERNEL,
> + )?;
[Severity: High]
Does this code sleep in an atomic context?
DRM CRTC and plane atomic callbacks execute in an atomic context during
non-blocking commits, where sleeping is strictly forbidden. This performs
a GFP_KERNEL allocation and maps memory with fb.owned_vmap(), both of
which can sleep.
> diff --git a/drivers/gpu/drm/vino/drm_sink/dispatch.rs b/drivers/gpu/drm/vino/drm_sink/dispatch.rs
> index 0000000000000..bd759ad4bc992
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/dispatch.rs
[ ... ]
> + pub(super) fn queue_scanout(
> + &self,
> + dev: &VinoDrmDevice,
> + fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
> + mut frame: PendingScanout,
> + ) {
[ ... ]
> + let coalesce = {
> + let pending = self.pending_scanout.lock();
> + pending[connector].as_ref().is_some_and(|queued| {
> + queued.w == frame.w
> + && queued.h == frame.h
> + && queued.rotation == frame.rotation
> + && self.keyframe_pending.load(Ordering::Acquire) & (1u32 << connector) == 0
> + })
> + };
> + if coalesce {
> + vino_debug!("vino: socket {socket} flip coalesced before snapshot\n");
> + return;
> + }
[Severity: High]
Can this early return cause the display to permanently freeze on stale frames?
If pending[connector] contains an unclaimed frame (making coalesce true),
this discards the new frame before taking a snapshot. The scanout worker
will only encode and scan out the older pending frame. If the compositor
goes idle and stops submitting frames, the worker will repeatedly process
the old frame and arm the settle_repaint timer with it, losing the most
recent updates.
[ ... ]
> + let (mut surface, binding, idx) = {
> + let mut pool = self.shadow[connector].lock();
[Severity: High]
Could this Mutex lock cause a deadlock or panic when called from the
atomic commit path?
This acquires a sleeping Mutex (shadow.lock()), which is unsafe in
atomic context.
[ ... ]
> + let r = snapshot_to_shadow(
> + self.geometry(),
> + &mut surface,
> + &binding.mapping,
> + source_w,
> + source_h,
> + );
[Severity: High]
Is it safe to perform a synchronous memory copy of up to 14.7 MB here?
Executing a long synchronous memory copy in the non-blocking atomic
commit callback can lead to system instability, as atomic contexts
should be kept as short as possible.
> diff --git a/drivers/gpu/drm/vino/drm_sink/limits.rs b/drivers/gpu/drm/vino/drm_sink/limits.rs
> index 0000000000000..96d2d8dcf8c29
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/limits.rs
[ ... ]
> + pub(super) fn other_connectors_rate(
> + &self,
> + state: &kernel::drm::kms::atomic::AtomicStateMutator<VinoDrmDriver>,
> + connector: usize,
> + ) -> u32 {
[ ... ]
> + let timings = *self.last_timing.lock();
> + let mut total: u32 = 0;
> + for (i, t) in timings.iter().enumerate() {
> + if i == connector {
> + continue;
> + }
[Severity: High]
Does reading shared global state here without global DRM atomic tracking
allow concurrent commits to overcommit the dock's pixel bandwidth?
Because this state is protected only by a local spinlock rather than being
part of the global DRM atomic state machine (e.g., via a drm_private_obj),
concurrent commits to different connectors will not serialize against each
other. Each commit will see the other connector's old bandwidth and succeed,
potentially bypassing hardware limits and causing a blank display.
> diff --git a/drivers/gpu/drm/vino/drm_sink/mode_objects.rs b/drivers/gpu/drm/vino/drm_sink/mode_objects.rs
> index 0000000000000..d3ec818ec1daa
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/mode_objects.rs
[ ... ]
> + fn atomic_check(check: CrtcAtomicCheck<'_, Self>) -> Result {
[ ... ]
> + let others = data.other_connectors_rate(&state, connector);
> + let combined = new_rate.saturating_add(others);
[Severity: High]
Could this call read a stale bandwidth value from other connectors?
As noted in limits.rs, this reads shared global state without global
atomic tracking, which might allow concurrent commits to overcommit
the display bandwidth.
[ ... ]
> + fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
[ ... ]
> + data.queue_scanout(
> + dev,
> + fb,
> + PendingScanout {
> + connector,
> + rotation,
> + clips,
> + nclips,
> + w,
> + h,
> + shadow_idx: 0,
> + shadow_generation: 0,
> + },
> + );
> + }
[Severity: High]
Does calling queue_scanout() from this atomic callback invoke operations
that are unsafe in atomic context?
VinoPlane::atomic_update() executes in an atomic context during
non-blocking commits. However, queue_scanout() acquires a sleeping
Mutex, allocates with GFP_KERNEL, maps memory, and performs large
synchronous memory copies.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=8
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 9/13] drm/vino: add the dock activation and scanout path
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (7 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 8/13] drm/vino: add the KMS device and the atomic path Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:59 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 10/13] drm/vino: read the dock's firmware version, and update it over DFU Mike Lothian
` (3 subsequent siblings)
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Benno Lossin, Gary Guo, linux-kernel,
rust-for-linux
Add the dock-facing half of the sink. A cold dock will not light from a
correct message sequence alone: it needs a measured bring-up choreography,
and one driven faster than its vendor drives it stays dark or resets. Those
timelines are data here, read off captures of each vendor stack waking the
same hardware, and they are expressed in transcript slots rather than
connector numbers because they were recorded from docks whose panels were
in the first two sockets.
With that come the pieces the choreography needs: connector presence and
its EDID, the brackets a mode program has to sit inside, stream opening,
the control-plane keepalive, and the scanout path itself -- a shadow
surface snapshotted inside the atomic commit so the encoder never races the
compositor's buffer, damage selection over the dock's own strip grid, and a
per-strip retransmit debt, because a dock holds more than one buffer and a
delta that reaches only one of them ghosts.
The encoder only ever sees the link's depth. Where userspace asked for a
deeper link than the surface it handed over, a sample is widened after
decoding by replicating its top bits into the low ones, so both
endpoints stay exact where a plain shift would leave full white three
codes short and tint every highlight. The strip cache is keyed on that
depth alongside the colour transform, since changing it re-maps every
sample on the way into the codec while leaving the framebuffer byte for
byte identical.
Waking a connector whose blank held its markers is a re-engage rather than
just a bracket close: the blank powered its downstream sink down, and the
closing markers do not bring it back. The vendor follows them with a probe,
EDID fetch and sink engage before it programs a timing, and so does this. A
dock woken without them comes back slowly, or not at all once the sink has
been down long enough for the dock to have let go of it.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/drm_sink.rs | 11 +
drivers/gpu/drm/vino/drm_sink/activation.rs | 1196 +++++++++++
drivers/gpu/drm/vino/drm_sink/bracket.rs | 369 ++++
drivers/gpu/drm/vino/drm_sink/cp_session.rs | 414 ++++
drivers/gpu/drm/vino/drm_sink/presence.rs | 435 ++++
drivers/gpu/drm/vino/drm_sink/scanout.rs | 1958 +++++++++++++++++++
drivers/gpu/drm/vino/drm_sink/stream.rs | 470 +++++
drivers/gpu/drm/vino/drm_sink/timeline.rs | 571 ++++++
8 files changed, 5424 insertions(+)
create mode 100644 drivers/gpu/drm/vino/drm_sink/activation.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/bracket.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/cp_session.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/presence.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/scanout.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/stream.rs
create mode 100644 drivers/gpu/drm/vino/drm_sink/timeline.rs
diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
index 86708db89699..535e7d18ff50 100644
--- a/drivers/gpu/drm/vino/drm_sink.rs
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -46,11 +46,18 @@
xxhash,
};
+mod activation;
+mod bracket;
+mod cp_session;
mod dispatch;
mod driver;
mod limits;
mod mode_objects;
+mod presence;
+mod scanout;
mod settings;
+mod stream;
+mod timeline;
mod worker;
pub(crate) use driver::VinoObject;
@@ -58,6 +65,10 @@
pub(super) use mode_objects::{
PlaneArgs, VblankTimer, VinoConnector, VinoCrtc, VinoEncoder, VinoPlane,
};
+use scanout::{read_cursor_bgra, run_pending_scanout, snapshot_to_shadow, src_dims};
+// Almost every item in `timeline` is read by the activation path; naming them individually
+// would list the module.
+pub(crate) use timeline::*;
/// Connector mode used until a downstream EDID is available.
const FALLBACK_W: i32 = 2560;
diff --git a/drivers/gpu/drm/vino/drm_sink/activation.rs b/drivers/gpu/drm/vino/drm_sink/activation.rs
new file mode 100644
index 000000000000..e505d80ed80d
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/activation.rs
@@ -0,0 +1,1196 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Bringing a connector's downstream sink up.
+//!
+//! A sink that is merely programmed with a timing stays dark. The dock has to be walked through a
+//! sequence of bracket states, carrier frames and mode sets, in an order and at intervals its own
+//! vendor was measured using, so the constants and the ordering here are load-bearing.
+
+use super::*;
+
+impl VinoDrmData {
+ /// Continuously present one already-encoded activation carrier for at least `duration_ms`.
+ ///
+ /// This deliberately performs no CP transaction between presentations. `BringUp` runs the
+ /// status/heartbeat dialogue concurrently on another work item; doing it here would put a
+ /// 10--15 ms control round-trip between tiny black frames and recreate the endpoint starvation
+ /// this path exists to remove. A persistent eight-URB queue bounds how far submission can run
+ /// ahead, so elapsed wall time closely follows actual endpoint progress rather than merely
+ /// copying an arbitrary number of frames into unbounded memory.
+ pub(super) fn submit_prompt_training(
+ &self,
+ dev: &BoundInterface<'_>,
+ connector: u8,
+ want: u64,
+ frames: &[KVec<u8>],
+ ordinary_frames: &[KVec<u8>],
+ duration_ms: i64,
+ with_arm: bool,
+ ) -> Result<u32> {
+ if frames.is_empty() {
+ return Err(kernel::error::code::EINVAL);
+ }
+ let geometry = self.geometry();
+ let xfer: usize = VIDEO_XFER;
+ let connector_index = connector as usize;
+ let pipe_i = dev.video_pipe_index(connector_index)?;
+ let connector_bit = 1u32 << connector;
+ // The DL7400's per-strip parameter map, ahead of the pixels it describes. This path --
+ // the startup/prompt-training submit -- is the one the dock's first frames go out on, so
+ // wiring the map only into the steady-state scanout left it absent from every frame that
+ // matters. Ridge has no equivalent record and gets an empty slice.
+ let params: KVec<u8> = if geometry.connector_selector_shift == 0 {
+ KVec::new()
+ } else {
+ let t = self
+ .last_timing
+ .lock()
+ .get(connector_index)
+ .copied()
+ .flatten();
+ match t {
+ Some(t) => {
+ let (sw, sh) = (geometry.strip_w(), geometry.strip_h());
+ let padded_width = (t.hactive as usize).div_ceil(sw) * sw;
+ let padded_height = (t.vactive as usize).div_ceil(sh) * sh;
+ crate::video::haar::navarro_strip_params(
+ geometry,
+ connector,
+ padded_width,
+ padded_height,
+ &frames,
+ &mut self.strip_classes.lock()[connector_index],
+ )?
+ }
+ None => KVec::new(),
+ }
+ };
+ vino_debug!(
+ "vino: connector={connector} prompt-training parameter map {} B\n",
+ params.len()
+ );
+ let arm = if with_arm {
+ // The bit is cleared only once this connector's carrier has gone out, so finding it
+ // clear means the stream is already open and the carrier already presented. That is
+ // this call's whole purpose, so report it done. Reporting it as a failure instead makes
+ // the caller re-arm and present a second carrier, and a retried activation then walks
+ // the dock's ring one slot further on every pass -- six flat frames where the vendor
+ // sends one, with the picture in a slot the dock is not showing.
+ if self.arm_prefix_pending.load(Ordering::Acquire) & connector_bit == 0 {
+ return Ok(0);
+ }
+ Some(self.build_stream_prefix_buf(connector_index)?)
+ } else {
+ None
+ };
+ if arm.is_some() {
+ self.send_stream_open(dev, connector_index)?;
+ // A stream that is being opened here starts its ring and its frame counter from the
+ // beginning, whatever the connector reached before. Arming the prologue already resets
+ // this, but an activation that is retried arms once and presents several times, so a
+ // connector could open a stream and immediately tell the dock it was filling a later
+ // slot with a later frame number -- and the dock scans out a slot nothing wrote.
+ self.scanout_seq.lock()[connector_index] = 0;
+ }
+ let startup = arm.is_some();
+ let seq0 = self.scanout_seq.lock()[connector_index];
+ let started = Instant::<Monotonic>::now();
+ let mut repeat = 0u32;
+ // Presentations that named a ring slot, which is what the frame counter counts. See
+ // `names_ring_slot`.
+ let mut named = 0u32;
+
+ loop {
+ if self.shutting_down.load(Ordering::Acquire)
+ || self.modeset_requested[connector_index].load(Ordering::Acquire) != want
+ || self.modeset_active[connector_index].load(Ordering::Acquire) != want
+ {
+ return Err(kernel::error::code::ENODEV);
+ }
+
+ let seq = seq0.wrapping_add(named);
+ let trailer = self.build_frame_trailer(connector, seq);
+ let arm_slice: &[u8] = if repeat == 0 {
+ arm.as_ref().map_or(&[], |a| &a[..])
+ } else {
+ &[]
+ };
+ let prologue_frame = startup && repeat == 0;
+ let opener = self.build_frame_opener(connector, seq, prologue_frame);
+ let opener_slice: &[u8] = opener.as_ref().map_or(&[], |o| &o[..]);
+ // Counted once this presentation has actually gone out; see the loop's tail. A pass
+ // that defers because the endpoint is full sends nothing, and a ring slot the dock
+ // never saw must not be spent.
+ let names_slot = names_ring_slot(opener_slice, &trailer);
+ // The report leads the frame it rides with, ahead of any pixels; see
+ // `build_stream_report_buf` for which frames carry one. The frame bearing the prologue
+ // never does: every generation goes from its decoder configuration into image records.
+ //
+ // The parameter map is not the report and has no such exception. The same-day DLM cold
+ // capture carries the map part-way through frame zero, and Windows carries it after
+ // the image records. Both put it before frame close. Omitting it made vino's first
+ // frame exactly 5,984 bytes short and left the dock briefly scanning a partially
+ // described framebuffer.
+ let report = if prologue_frame {
+ None
+ } else {
+ self.build_stream_report_buf(connector_index, seq)?
+ };
+ let report_slice: &[u8] = report.as_ref().map_or(&[], |r| &r[..]);
+ let params_slice: &[u8] = ¶ms[..];
+ // The prologue and ordinary DLM carriers contain identical strips but different
+ // producer record boundaries. Select by the presence of the one-shot arm rather than
+ // by this helper's local `repeat`: the cold timeline invokes the helper once per
+ // measured presentation, so every invocation starts with repeat zero.
+ let frame_parts = if prologue_frame {
+ frames
+ } else {
+ ordinary_frames
+ };
+ let image_len: usize = frame_parts.iter().map(|f| f.len()).sum();
+ let image_parts = frame_parts.len();
+ let wire_len = arm_slice.len()
+ + opener_slice.len()
+ + report_slice.len()
+ + params_slice.len()
+ + image_len
+ + trailer.len();
+ {
+ // One writer owns a shared pipe for the whole frame; see `own_pipe`.
+ let _pipe = self.own_pipe();
+ let mut staging_slots = self.video_staging.lock();
+ let staging_slot = &mut staging_slots[connector_index];
+ if staging_slot.is_none() {
+ let mut staging = KVec::new();
+ staging.resize(xfer, 0, GFP_KERNEL)?;
+ *staging_slot = Some(staging);
+ }
+ let staging = staging_slot.as_mut().ok_or(kernel::error::code::ENOMEM)?;
+
+ let mut queue_slot = self.video_q[pipe_i].lock();
+ if queue_slot.is_none() {
+ // Navarro's required EP08/EP0a clears are sent together at their captured
+ // pre-commit point in `send_cp_setup`. Nothing clears here: doing so after
+ // stream setup has begun changes SuperSpeed endpoint sequence state at a
+ // point DLM does not.
+ *queue_slot = Some(dev.video_queue(connector_index, 8, xfer)?);
+ vino_debug!(
+ "vino: connector={} endpoint={:#04x} persistent video queue opened by prompt training\n",
+ connector,
+ dev.endpoints.video[connector_index].address()
+ );
+ }
+ let queue = queue_slot
+ .as_mut()
+ .get_mut()
+ .as_mut()
+ .ok_or(kernel::error::code::ENODEV)?;
+
+ // A carrier is one protocol frame split over several URBs. Never block after
+ // submitting only its prefix: DLM's video producer and control workers are
+ // independent, while this cold-timeline worker also owes the marker burst that
+ // surrounds the first video bytes. On a non-draining endpoint the old path filled
+ // the eight slots with two complete frames plus one URB of frame three, then
+ // waited a second for frame-three URB two. The due +14-ms marker consequently
+ // never reached EP02 even though the dock had authenticated the preceding marker.
+ // Defer the whole presentation when it cannot fit; the next scheduled carrier can
+ // retry after the control messages have advanced the dock state.
+ let frame_urbs = wire_len.div_ceil(xfer);
+ if !queue.can_send_n(dev.io(), frame_urbs)? {
+ if self
+ .endpoint_status_logged
+ .fetch_or(connector_bit, Ordering::AcqRel)
+ & connector_bit
+ == 0
+ {
+ match dev.video_endpoint_status(connector_index) {
+ Ok(status) => vino_debug!(
+ "vino: connector={} endpoint={:#04x} stopped accepting video: GET_STATUS={:#06x} halt={}\n",
+ connector,
+ dev.endpoints.video[connector_index].address(),
+ status,
+ status & 1
+ ),
+ Err(e) => pr_warn!(
+ "vino: connector={} endpoint={:#04x} stopped accepting video: GET_STATUS failed ({e:?})\n",
+ connector,
+ dev.endpoints.video[connector_index].address()
+ ),
+ }
+ }
+ if duration_ms <= 0 {
+ return Ok(repeat);
+ }
+ if (Instant::<Monotonic>::now() - started).as_millis() >= duration_ms {
+ break;
+ }
+ fsleep(Delta::from_millis(1));
+ continue;
+ }
+
+ // Navarro's captured DLM stream flushes its two parameter records after exactly
+ // NAVARRO_PARAM_IMAGE_OFFSET bytes of image records in both the prologue and
+ // ordinary carriers. Build a borrowed scatter list in the captured order. A
+ // carrier's chunks all hold the same records, so the exact offset is usable here:
+ // the insertion may fall inside one allocation chunk, but is always on a
+ // wire-record boundary. A content frame's are not uniform and round to a chunk.
+ let mut wire_parts: KVec<&[u8]> = KVec::with_capacity(image_parts + 6, GFP_KERNEL)?;
+ if !arm_slice.is_empty() {
+ wire_parts.push(arm_slice, GFP_KERNEL)?;
+ }
+ if !opener_slice.is_empty() {
+ wire_parts.push(opener_slice, GFP_KERNEL)?;
+ }
+ if !report_slice.is_empty() {
+ wire_parts.push(report_slice, GFP_KERNEL)?;
+ }
+ let param_at = NAVARRO_PARAM_IMAGE_OFFSET.min(image_len);
+ let mut image_off = 0usize;
+ let mut param_inserted = params_slice.is_empty();
+ for f in frame_parts.iter() {
+ if image_off >= image_len {
+ break;
+ }
+ let n = f.len().min(image_len - image_off);
+ let split = param_at.saturating_sub(image_off).min(n);
+ if !param_inserted && image_off + n >= param_at {
+ if split != 0 {
+ wire_parts.push(&f[..split], GFP_KERNEL)?;
+ }
+ wire_parts.push(params_slice, GFP_KERNEL)?;
+ param_inserted = true;
+ if split != n {
+ wire_parts.push(&f[split..n], GFP_KERNEL)?;
+ }
+ } else if n != 0 {
+ wire_parts.push(&f[..n], GFP_KERNEL)?;
+ }
+ image_off += n;
+ }
+ if !param_inserted {
+ wire_parts.push(params_slice, GFP_KERNEL)?;
+ }
+ wire_parts.push(&trailer[..], GFP_KERNEL)?;
+
+ let part_count = wire_parts.len();
+ let mut part_i = 0usize;
+ let mut part_off = 0usize;
+ let mut wire_off = 0usize;
+ // DLM's authenticated first connector-0 prologue does not put all four URBs on the
+ // xHCI ring at once. It submits at +0/+806/+851/+873 us and receives completion
+ // of the first at +104 us, so the dock gets a ~700-us ready interval before the
+ // second transfer and then a three-URB pipeline. Submitting chunk two immediately
+ // leaves Navarro NRDY forever after exactly one completed URB. Preserve this
+ // producer boundary only for the one-shot, full-size prologue; ordinary frames
+ // use the normal eight-deep queue, just as DLM does.
+ const NAVARRO_PROLOGUE_SUBMIT_US: [i64; 4] = [0, 806, 851, 873];
+ let pace_prologue = !arm_slice.is_empty()
+ && xfer == VIDEO_XFER
+ && geometry.connector_selector_shift != 0;
+ let mut prologue_anchor: Option<Instant<Monotonic>> = None;
+ while wire_off < wire_len {
+ let data_len = (wire_len - wire_off).min(xfer);
+ let dst = &mut staging[..data_len];
+ let mut dst_off = 0usize;
+ while dst_off < dst.len() && part_i < part_count {
+ let part = wire_parts[part_i];
+ let n = (part.len() - part_off).min(dst.len() - dst_off);
+ dst[dst_off..dst_off + n].copy_from_slice(&part[part_off..part_off + n]);
+ dst_off += n;
+ part_off += n;
+ if part_off == part.len() {
+ part_i += 1;
+ part_off = 0;
+ }
+ }
+ if pace_prologue {
+ let chunk = wire_off / xfer;
+ if let Some(anchor) = prologue_anchor {
+ if let Some(&target_us) = NAVARRO_PROLOGUE_SUBMIT_US.get(chunk) {
+ Self::wait_video_offset(anchor, target_us);
+ }
+ }
+ }
+ // DLM's mixed transport: prologue chunk zero is reaped below, then the rest
+ // and all ordinary frames are pipelined.
+ queue.send(dev.io(), dst, crate::timeout())?;
+ if pace_prologue && wire_off == 0 {
+ let anchor = Instant::<Monotonic>::now();
+ prologue_anchor = Some(anchor);
+ // Do not expose chunk two to xHCI before chunk zero completes. The capture
+ // has the first completion at +104 us and the next submit at +806 us.
+ queue.flush(dev.io(), crate::timeout())?;
+ }
+ self.last_video_at.lock()[connector_index] = Some(Instant::<Monotonic>::now());
+ wire_off += data_len;
+ }
+ }
+
+ if repeat == 0 && startup {
+ self.arm_prefix_pending
+ .fetch_and(!connector_bit, Ordering::Release);
+ // The window runs from the first frame the dock actually receives, not from the
+ // mode set, so it is re-armed here. Whether there is a window at all is
+ // `sustain_window`'s decision and must not be second-guessed: a dock that shares
+ // its control pipe is granted none, and re-arming one unconditionally spent three
+ // seconds of full keyframes on the endpoint its control plane needs.
+ let mut sustain = self.sustain_until.lock();
+ if sustain[connector_index].is_some() {
+ sustain[connector_index] =
+ Some(Instant::<Monotonic>::now() + Delta::from_millis(SUSTAIN_MS));
+ }
+ drop(sustain);
+ vino_debug!(
+ "vino: connector {} startup frame submitted after {} ms ({} bytes)\n",
+ connector,
+ (Instant::<Monotonic>::now() - started).as_millis(),
+ wire_len
+ );
+ }
+ repeat = repeat.wrapping_add(1);
+ if names_slot {
+ named = named.wrapping_add(1);
+ }
+ self.scanout_seq.lock()[connector_index] = seq0.wrapping_add(named);
+
+ if repeat >= self.carrier_presentations() {
+ break;
+ }
+ // A zero window means this dock is bounded by the count above, not by wall clock.
+ // Testing the elapsed time against it regardless ends the carrier after one frame,
+ // whatever the count says.
+ if duration_ms > 0 && (Instant::<Monotonic>::now() - started).as_millis() >= duration_ms
+ {
+ break;
+ }
+ // Leave the endpoint between carrier frames on a dock that shares it, at the same
+ // interval its ordinary frames are paced to. A back-to-back carrier is what silenced
+ // this dock before, and it is the reason its window was reduced to a single frame.
+ if self.video_on_ctrl_pipe() {
+ fsleep(Delta::from_millis(self.frame_period_ms()));
+ }
+ }
+
+ vino_debug!(
+ "vino: connector={} training complete ({} presentations, {} ms)\n",
+ connector,
+ repeat,
+ (Instant::<Monotonic>::now() - started).as_millis()
+ );
+ Ok(repeat)
+ }
+ /// Apply one desired mode generation and its activation carrier.
+ ///
+ /// The control timeline is always released before returning. Any failed bracket, mode-set, arm,
+ /// or carrier transfer clears `modeset_active`, allowing the desired generation to be retried.
+ pub(super) fn activate_head(
+ &self,
+ dev: &BoundInterface<'_>,
+ connector: u8,
+ timing: &crate::cp::Timing,
+ want: u64,
+ ) -> Result<bool> {
+ let connector_index = connector as usize;
+ if connector_index >= MAX_CONNECTORS
+ || self.modeset_requested[connector_index].load(Ordering::Acquire) != want
+ {
+ return Ok(false);
+ }
+ // Keep no-op adoption at the activation boundary rather than in one caller. The command
+ // worker and the scanout worker can both arrive here, and either must avoid reopening an
+ // exact mode that is already usable under a different raw request token.
+ if self.adopt_programmed_mode(connector_index, timing, want) {
+ vino_debug!(
+ "vino: connector {connector_index} already active at this mode; no re-activation\n"
+ );
+ return Ok(false);
+ }
+ // Reconfiguring one connector of a dock that shares its control pipe, while another
+ // connector is lit, is a sequence of its own: see `ELLA_RUNTIME_MODE`. The schedule below
+ // is the cold one, and a dock already driving a sink stops answering partway through it,
+ // taking the lit connector down with it.
+ if self.video_on_ctrl_pipe()
+ && (0..MAX_CONNECTORS).any(|h| {
+ h != connector_index && self.modeset_active[h].load(Ordering::Acquire) != 0
+ })
+ {
+ let mut timings: [Option<crate::cp::Timing>; MAX_CONNECTORS] = [None; MAX_CONNECTORS];
+ timings[connector_index] = Some(*timing);
+ return self.activate_dock_wide(dev, ELLA_RUNTIME_MODE, timings);
+ }
+ // `wake` describes the state on entry and therefore has to be captured before invalidating
+ // it. From this point onward the transaction is going to touch the dock; do not leave the
+ // old token adoptable while a bracket, clear-mode, or set-mode is only partly complete.
+ let wake = self.modeset_active[connector_index].swap(0, Ordering::AcqRel) == 0;
+ self.programmed_timing.lock()[connector_index] = None;
+ // A superseding callback can land between the initial/adoption checks and the swap.
+ // Clearing the old active token is conservative, but sending its stale transaction is not:
+ // leave the newer request to its queued command or inline retry.
+ if self.modeset_requested[connector_index].load(Ordering::Acquire) != want {
+ return Ok(false);
+ }
+ // Same as the dual path: a connector coming back from a blank has a bracket owed before
+ // anything else is sent to it.
+ self.close_blank_bracket(dev, connector)?;
+ // The caller's timing was built when this connector was enabled, possibly before its
+ // endpoint partner existed. The request token remains the caller's generation; the exact
+ // corrected timing sent below is recorded separately in `programmed_timing`.
+ let timing = &self.effective_timing(connector_index, timing);
+
+ let geometry = self.geometry();
+ let padded_width =
+ (timing.hactive as usize + geometry.strip_w() - 1) & !(geometry.strip_w() - 1);
+ let padded_height =
+ (timing.vactive as usize + geometry.strip_h() - 1) & !(geometry.strip_h() - 1);
+ let prompt =
+ crate::video::haar::black_frame_ep08(geometry, padded_width, padded_height, connector)?;
+ let prompt_ordinary = crate::video::haar::black_frame_ep08_ordinary(
+ geometry,
+ padded_width,
+ padded_height,
+ connector,
+ )?;
+
+ self.begin_cp_timeline();
+ let transaction = (|| -> Result<bool> {
+ // The timing is what the sink has to be retrained onto, so the bracket belongs to the
+ // mode program and not to how the connector got here. A connector arriving from a blank
+ // needs it exactly as much as one being configured cold.
+ self.modeset_bracket_pre(dev, connector)?;
+ let mode_anchor = Instant::<Monotonic>::now();
+ // Tear the connector's pipe down before configuring it, as DLM does. See
+ // `cp::clear_mode`: the dock expects a connector to be torn down before it is
+ // configured.
+ if self.clear_mode_before_set() {
+ self.send_cp(dev, 0x48, 0, |ctr| crate::cp::clear_mode(ctr, connector))?;
+ }
+ self.send_cp(dev, 0x48, 0, |ctr| {
+ crate::cp::set_mode(ctr, connector, timing)
+ })?;
+ self.programmed_timing.lock()[connector_index] = Some(*timing);
+ if self.modeset_requested[connector_index].load(Ordering::Acquire) != want {
+ return Ok(false);
+ }
+
+ self.modeset_active[connector_index].store(want, Ordering::Release);
+ self.sustain_until.lock()[connector_index] = self.sustain_window(connector_index);
+ let bit = 1u32 << connector;
+ self.arm_stream_prologue(connector_index);
+ // A driven connector's stream opens with its pipe descriptor, not the idle open.
+ self.stream_open_pending.fetch_and(!bit, Ordering::Release);
+ self.owe_keyframe(connector_index);
+ self.strip_hashes.lock()[connector_index] = None;
+ self.dirty_ttl.lock()[connector_index] = None;
+
+ self.modeset_bracket_post_open(dev, connector, mode_anchor)?;
+ let opening = self.submit_prompt_training(
+ dev,
+ connector,
+ want,
+ &prompt,
+ &prompt_ordinary,
+ self.carrier_ms(PROMPT_TRAINING_OPEN_MS),
+ true,
+ );
+ let closing = self.modeset_bracket_post_close(dev, connector, mode_anchor);
+ opening?;
+ closing?;
+ Ok(true)
+ })();
+ self.end_cp_timeline();
+
+ let activated = match transaction {
+ Ok(activated) => activated,
+ Err(e) => {
+ self.modeset_active[connector_index].store(0, Ordering::Release);
+ self.programmed_timing.lock()[connector_index] = None;
+ self.unwind_bracket(dev, connector);
+ return Err(e);
+ }
+ };
+ if !activated {
+ self.programmed_timing.lock()[connector_index] = None;
+ self.unwind_bracket(dev, connector);
+ return Ok(false);
+ }
+ // The tail continues a carrier that is bounded by wall clock, so a dock bounded by a frame
+ // count has already presented all of it and this would add one more. The count is what the
+ // vendor's stream opens with, and the frames it names walk the dock's ring: an extra one
+ // puts every later frame a slot further on than the vendor puts it, and the dock presents
+ // a slot holding the flat carrier rather than the one holding the picture.
+ if self.carrier_ms(PROMPT_TRAINING_TAIL_MS) > 0 {
+ if let Err(e) = self.submit_prompt_training(
+ dev,
+ connector,
+ want,
+ &prompt,
+ &prompt_ordinary,
+ self.carrier_ms(PROMPT_TRAINING_TAIL_MS),
+ false,
+ ) {
+ self.modeset_active[connector_index].store(0, Ordering::Release);
+ self.programmed_timing.lock()[connector_index] = None;
+ self.unwind_bracket(dev, connector);
+ return Err(e);
+ }
+ }
+
+ vino_debug!(
+ "vino: applied {} stream-enable sequence for connector {}\n",
+ if wake { "wake" } else { "mode-change" },
+ connector
+ );
+ Ok(true)
+ }
+ /// Activate both downstream connectors using the dock-wide cold-link schedule.
+ ///
+ /// Both mode sets precede either connector's video. Single-connector activation and live mode
+ /// changes use the per-connector schedule.
+ ///
+ /// Every connector number in a [`ColdTimeline`] is a transcript slot, not a connector: slot 0
+ /// is the lowest-numbered activating connector and slot 1 the next, and they are resolved to
+ /// real connectors at the point of send. Taken literally they address connectors 0 and 1, whose
+ /// bits are absent from `sent` for any other pair of sockets, so no marker and no video would
+ /// go out at all.
+ pub(super) fn activate_dual_wake(
+ &self,
+ dev: &BoundInterface<'_>,
+ mut timings: [Option<crate::cp::Timing>; MAX_CONNECTORS],
+ ) -> Result<bool> {
+ let geometry = self.geometry();
+ let mut prompts: [Option<KVec<KVec<u8>>>; MAX_CONNECTORS] = core::array::from_fn(|_| None);
+ let mut ordinary_prompts: [Option<KVec<KVec<u8>>>; MAX_CONNECTORS] =
+ core::array::from_fn(|_| None);
+ let mut keys = [0u64; MAX_CONNECTORS];
+ let mut valid = 0u32;
+ // Snapshot topology once for the whole dock-wide transaction, so two partner modes cannot
+ // disagree about whether they share an endpoint if another callback lands mid-loop.
+ let requested_heads = self.requested_connector_mask();
+
+ // Pre-encode both tiny carriers before excluding the keepalive or starting either
+ // mode-set. Encoding work must not serialize the dock's back-to-back mode pair.
+ for connector in 0..MAX_CONNECTORS {
+ let Some(timing) = timings[connector] else {
+ continue;
+ };
+ let key = timing_key(&timing);
+ if self.modeset_requested[connector].load(Ordering::Acquire) != key
+ || self.modeset_active[connector].load(Ordering::Acquire) != 0
+ {
+ continue;
+ }
+ timings[connector] =
+ Some(self.effective_timing_in_mask(connector, &timing, requested_heads));
+ vino_debug!(
+ "vino: dual activation connector={} mode={}x{}@{}\n",
+ connector,
+ timing.hactive,
+ timing.vactive,
+ timing.refresh_hz
+ );
+ let padded_width =
+ (timing.hactive as usize + geometry.strip_w() - 1) & !(geometry.strip_w() - 1);
+ let padded_height =
+ (timing.vactive as usize + geometry.strip_h() - 1) & !(geometry.strip_h() - 1);
+ prompts[connector] = Some(crate::video::haar::black_frame_ep08(
+ geometry,
+ padded_width,
+ padded_height,
+ connector as u8,
+ )?);
+ ordinary_prompts[connector] = Some(crate::video::haar::black_frame_ep08_ordinary(
+ geometry,
+ padded_width,
+ padded_height,
+ connector as u8,
+ )?);
+ keys[connector] = key;
+ valid |= 1u32 << connector;
+ }
+ if valid.count_ones() < 2 {
+ return Ok(false);
+ }
+
+ // The two connectors this activation is about, in the order the timeline brings them up.
+ // Both cold timelines describe exactly two connectors, so a third is reported rather than
+ // silently left out of the choreography.
+ let mut slots = [0u8; 2];
+ let mut n = 0;
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ if n < slots.len() {
+ slots[n] = connector as u8;
+ }
+ n += 1;
+ }
+ }
+ if n > slots.len() {
+ pr_warn!(
+ "vino: {n} connectors activating but the cold timeline describes {}; choreographing {} and {}\n",
+ slots.len(),
+ slots[0],
+ slots[1]
+ );
+ }
+ // Slot -> connector. Out-of-range slots cannot occur (both timelines only name 0 and 1) but
+ // clamp rather than panic, because a timeline is data and this runs under the CP lock.
+ let connector_of = |slot: u8| usize::from(slots[usize::from(slot).min(slots.len() - 1)]);
+
+ // Keep the clear/settle phase and the real mode-set choreography in one exclusive control
+ // transaction. Their deadlines have separate anchors because the Navarro cold timeline was
+ // measured from the real connector-0 mode set, 1,156 ms after its pipe clear.
+ // A blank is closed before the choreography, not during it: the bracket must be shut
+ // before anything re-probes or re-sets the mode.
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ self.close_blank_bracket(dev, connector as u8)?;
+ }
+ }
+ self.begin_cp_timeline();
+ let activation_started = Instant::<Monotonic>::now();
+ let mut anchor = activation_started;
+ let mut sent = 0u32;
+ let mut started = 0u32;
+ let timeline = (|| -> Result<(u32, u32)> {
+ if self.dock_wide_modeset() {
+ let remap = |slot: u8| connector_of(slot) as u8;
+
+ // DLM's first clear pair begins a dock-wide sink reset. The authenticated
+ // transcript then stops/restarts each EDID reader, disengages/re-engages the
+ // downstream sinks, and clears each pipe a second time before any real mode.
+ for (slot, &connector) in slots.iter().enumerate() {
+ if slot == 1 {
+ Self::wait_mode_offset(activation_started, NAVARRO_PRIME_CLEAR_H1_MS);
+ }
+ self.send_cp(dev, 0x48, 0, |ctr| crate::cp::clear_mode(ctr, connector))?;
+ }
+ for &(at, op) in NAVARRO_COLD_PRELUDE {
+ Self::wait_mode_offset(activation_started, at);
+ self.navarro_cold_op(dev, op.remap_head(&remap))?;
+ }
+ Self::wait_mode_offset(activation_started, NAVARRO_REAL_MODE_H0_MS);
+ anchor = Instant::<Monotonic>::now();
+ }
+
+ let dock_wide_counters = if self.dock_wide_modeset() {
+ Some(self.reserve_cp_counters::<NAVARRO_COLD_COUNTERS>()?)
+ } else {
+ None
+ };
+
+ // Three cursors walk the sorted schedules; `cp_until` drains everything due at or
+ // before a given offset, preserving the ordering between markers, polls, and EDID
+ // reads.
+ let mut mi = 0usize;
+ let mut pi = 0usize;
+ let mut ei = 0usize;
+ // Replaying Ridge's choreography at Navarro leaves its video endpoint unarmed, so the
+ // timeline follows the dock, not the driver.
+ let timeline: &ColdTimeline = if self.uses_arm_burst() {
+ &COLD_RIDGE
+ } else {
+ &COLD_NAVARRO
+ };
+ let mut remoded = 0u32;
+
+ macro_rules! cp_until {
+ ($limit:expr) => {{
+ let limit: i64 = $limit;
+ loop {
+ let nm = timeline.markers.get(mi).map(|m| m.0);
+ let np = timeline.polls.get(pi).copied();
+ let ne = timeline.edid.get(ei).map(|e| e.0);
+ let next = [nm, np, ne]
+ .into_iter()
+ .flatten()
+ .filter(|&o| o <= limit)
+ .min();
+ let Some(off) = next else { break };
+ Self::wait_mode_offset(anchor, off);
+ if nm == Some(off) {
+ let (_, slot, sub, state) = timeline.markers[mi];
+ let connector = connector_of(slot) as u8;
+ if sent & (1u32 << connector) != 0 {
+ if let Some(counters) = dock_wide_counters.as_ref() {
+ let slot =
+ *NAVARRO_MARKER_COUNTER_SLOTS.get(mi).ok_or(EINVAL)?;
+ let ctr = *counters.get(slot).ok_or(EINVAL)?;
+ self.send_cp_reserved(dev, 0x16, ctr, |ctr| {
+ crate::cp::stream_marker(ctr, connector, sub, state)
+ })?;
+ } else {
+ self.stream_marker(dev, connector, sub, state)?;
+ }
+ }
+ mi += 1;
+ } else if np == Some(off) {
+ if let Some(counters) = dock_wide_counters.as_ref() {
+ let slot = *NAVARRO_POLL_COUNTER_SLOTS.get(pi).ok_or(EINVAL)?;
+ let ctr = *counters.get(slot).ok_or(EINVAL)?;
+ self.send_cp_reserved(dev, 0x14, ctr, |ctr| {
+ crate::cp::device_query_req(ctr, 0x000c)
+ })?;
+ } else {
+ self.poll_status(dev)?;
+ }
+ pi += 1;
+ } else {
+ let (_, slot, fetch) = timeline.edid[ei];
+ let connector = connector_of(slot) as u8;
+ // Re-read the sink's EDID at its required place in the transaction.
+ // This dock-side DDC operation is not a source of new modes, so discard
+ // its reply rather than publishing a hotplug during a mode set.
+ self.send_cp(dev, 0x15, 0, |ctr| {
+ if fetch {
+ crate::cp::get_edid_req(ctr, connector)
+ } else {
+ crate::cp::get_edid_req_sub(ctr, 0x0020, connector)
+ }
+ })?;
+ ei += 1;
+ }
+ }
+ }};
+ }
+
+ // Both real mode sets go out before any video, spaced according to this dock's
+ // measured cold timeline. Navarro's pipe clears were sent during the settling phase
+ // above; do not collapse them back into this loop.
+ for slot in 0..slots.len() {
+ let connector = connector_of(slot as u8);
+ let bit = 1u32 << connector;
+ if valid & bit == 0 {
+ continue;
+ }
+ let Some(timing) = timings[connector] else {
+ continue;
+ };
+ // The second connector's mode set is spaced from the first by this dock's measured
+ // interval -- 757 ms on Navarro, 29 ms on Ridge. Gate on the slot, so the spacing
+ // survives whichever sockets the monitors are in.
+ if slot == 1 {
+ cp_until!(timeline.h1_mode - 1);
+ Self::wait_mode_offset(anchor, timeline.h1_mode);
+ }
+ // Retrain the downstream link onto the new timing. The sink goes down
+ // immediately ahead of the timing and the post-mode bracket brings it back up; a
+ // dock whose vendor does not bracket this way carries no state here.
+ if let Some(state) = self.pre_mode_sink_state() {
+ self.stream_marker(dev, connector as u8, 0x2f, 1)?;
+ self.stream_marker(dev, connector as u8, 0x2e, state)?;
+ }
+ if let Some(counters) = dock_wide_counters.as_ref() {
+ // Reservation-token slots for the two mode sets, by activation order. Keyed on
+ // the connector number these collided for any pair but (0, 1), and Navarro NAKs
+ // from the first flattened counter onward.
+ let ctr_slot = if slot == 0 { 0 } else { 3 };
+ let ctr = *counters.get(ctr_slot).ok_or(EINVAL)?;
+ self.send_cp_reserved(dev, 0x48, ctr, |ctr| {
+ crate::cp::set_mode(ctr, connector as u8, &timing)
+ })?;
+ } else {
+ self.send_cp(dev, 0x48, 0, |ctr| {
+ crate::cp::set_mode(ctr, connector as u8, &timing)
+ })?;
+ }
+ self.programmed_timing.lock()[connector] = Some(timing);
+ if self.modeset_requested[connector].load(Ordering::Acquire) != keys[connector] {
+ continue;
+ }
+ self.modeset_active[connector].store(keys[connector], Ordering::Release);
+ self.sustain_until.lock()[connector] = self.sustain_window(connector);
+ self.arm_stream_prologue(connector);
+ // A driven connector's stream opens with its pipe descriptor, not the idle open.
+ self.stream_open_pending.fetch_and(!bit, Ordering::Release);
+ self.owe_keyframe(connector);
+ self.strip_hashes.lock()[connector] = None;
+ self.dirty_ttl.lock()[connector] = None;
+ sent |= bit;
+ }
+
+ // Preserve the required silent window on EP02 between the connector-1 mode set and
+ // `cold::QUIET_END`. The exclusive control timeline already excludes keepalives.
+ Self::wait_mode_offset(anchor, timeline.quiet_end);
+
+ // Bracket, status polls and the mid-bracket EDID re-read, up to the first video.
+ cp_until!(timeline.video[0].1 - 1);
+
+ // DLM opens a short sealed stream on every connector without a monitor at this point.
+ // Vino does not: this dock re-enumerates when a stream is driven at an empty connector,
+ // and an empty socket's index is not stable across bring-ups.
+
+ for &(vslot, at) in timeline.video {
+ let connector = connector_of(vslot as u8);
+ cp_until!(at - 1);
+ // Some docks set a connector's mode a second time shortly before its video.
+ for &(off, reslot) in timeline.remode {
+ let replay_connector = connector_of(reslot as u8);
+ if off >= at
+ || remoded & (1u32 << replay_connector) != 0
+ || sent & (1u32 << replay_connector) == 0
+ {
+ continue;
+ }
+ let Some(timing) = timings[replay_connector] else {
+ continue;
+ };
+ cp_until!(off - 1);
+ Self::wait_mode_offset(anchor, off);
+ self.send_cp(dev, 0x48, 0, |ctr| {
+ crate::cp::set_mode(ctr, replay_connector as u8, &timing)
+ })?;
+ self.programmed_timing.lock()[replay_connector] = Some(timing);
+ remoded |= 1u32 << replay_connector;
+ }
+ cp_until!(at - 1);
+ Self::wait_mode_offset(anchor, at);
+ let bit = 1u32 << connector;
+ if sent & bit == 0 {
+ continue;
+ }
+ // Exactly one ARM+carrier presentation keeps the closing markers from being
+ // delayed behind a blocking multi-frame submission.
+ let frames = prompts[connector].as_ref().ok_or(EINVAL)?;
+ let ordinary_frames = ordinary_prompts[connector].as_ref().ok_or(EINVAL)?;
+ let t_sub = Instant::<Monotonic>::now();
+ let first_for_head = started & bit == 0;
+ self.submit_prompt_training(
+ dev,
+ connector as u8,
+ keys[connector],
+ frames,
+ ordinary_frames,
+ self.carrier_ms(PROMPT_TRAINING_OPEN_MS),
+ first_for_head,
+ )?;
+ vino_debug!(
+ "vino: connector {} video submit took {} ms (timeline offset {} ms, {} ms since anchor)\n",
+ connector,
+ (Instant::<Monotonic>::now() - t_sub).as_millis(),
+ at,
+ (Instant::<Monotonic>::now() - anchor).as_millis()
+ );
+ started |= bit;
+ }
+
+ // Remaining polls and the closing markers.
+ cp_until!(i64::MAX);
+ Ok((sent, started))
+ })();
+ self.end_cp_timeline();
+ let (sent, started) = match timeline {
+ Ok(state) => state,
+ Err(e) => {
+ for connector in 0..MAX_CONNECTORS {
+ if sent & (1u32 << connector) != 0
+ && self.modeset_active[connector].load(Ordering::Acquire) == keys[connector]
+ {
+ self.modeset_active[connector].store(0, Ordering::Release);
+ }
+ }
+ // The choreography opens every activating connector's bracket well before it sets a
+ // mode, so unwind on `valid` rather than `sent`: a connector that failed before its
+ // mode set is still open on the dock.
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ self.unwind_bracket(dev, connector as u8);
+ }
+ }
+ return Err(e);
+ }
+ };
+ if sent.count_ones() < 2 {
+ for connector in 0..MAX_CONNECTORS {
+ if sent & (1u32 << connector) != 0
+ && self.modeset_active[connector].load(Ordering::Acquire) == keys[connector]
+ {
+ self.modeset_active[connector].store(0, Ordering::Release);
+ }
+ }
+ return Ok(false);
+ }
+
+ // Keep both endpoints busy through downstream clock training, so the carrier outlives the
+ // bracket rather than stopping with it.
+ let tail_started = Instant::<Monotonic>::now();
+ while (Instant::<Monotonic>::now() - tail_started).as_millis()
+ < self.carrier_ms(cold::CARRIER_TAIL_MS)
+ {
+ for connector in 0..MAX_CONNECTORS {
+ if started & (1u32 << connector) == 0 {
+ continue;
+ }
+ let frames = prompts[connector].as_ref().ok_or(EINVAL)?;
+ let ordinary_frames = ordinary_prompts[connector].as_ref().ok_or(EINVAL)?;
+ if let Err(e) = self.submit_prompt_training(
+ dev,
+ connector as u8,
+ keys[connector],
+ frames,
+ ordinary_frames,
+ self.carrier_ms(PROMPT_TRAINING_OPEN_MS),
+ false,
+ ) {
+ for reset in 0..MAX_CONNECTORS {
+ if sent & (1u32 << reset) != 0
+ && self.modeset_active[reset].load(Ordering::Acquire) == keys[reset]
+ {
+ self.modeset_active[reset].store(0, Ordering::Release);
+ }
+ }
+ for reset in 0..MAX_CONNECTORS {
+ if valid & (1u32 << reset) != 0 {
+ self.unwind_bracket(dev, reset as u8);
+ }
+ }
+ return Err(e);
+ }
+ }
+ }
+ vino_debug!(
+ "vino: dual-connector activation complete after {} ms (mode/started masks 0x{:x}/0x{:x})\n",
+ (Instant::<Monotonic>::now() - anchor).as_millis(),
+ sent,
+ started
+ );
+ Ok(true)
+ }
+ /// Activate both connectors of a dock that carries its video on the control pipe.
+ ///
+ /// Replays [`ELLA_DOCK_WIDE`]: one transaction configures both connectors, the first streams,
+ /// and the second's sink comes up behind it. The per-connector schedule cannot express this,
+ /// because its second pass sets a mode the dock has already been given and opens a bracket the
+ /// dock stops answering.
+ pub(super) fn activate_dock_wide(
+ &self,
+ dev: &BoundInterface<'_>,
+ steps: &[DockWideStep],
+ mut timings: [Option<crate::cp::Timing>; MAX_CONNECTORS],
+ ) -> Result<bool> {
+ // Connectors the table drives, which is what decides how many the caller has to supply.
+ let wanted = steps
+ .iter()
+ .filter_map(|step| match *step {
+ DockWideStep::SetMode(slot) => Some(u32::from(slot) + 1),
+ _ => None,
+ })
+ .max()
+ .unwrap_or(0);
+ let geometry = self.geometry();
+ let mut prompts: [Option<KVec<KVec<u8>>>; MAX_CONNECTORS] = core::array::from_fn(|_| None);
+ let mut ordinary_prompts: [Option<KVec<KVec<u8>>>; MAX_CONNECTORS] =
+ core::array::from_fn(|_| None);
+ let mut keys = [0u64; MAX_CONNECTORS];
+ let mut valid = 0u32;
+ let requested_heads = self.requested_connector_mask();
+
+ // Encode every carrier before the transaction opens. Where a table sets two modes they are
+ // adjacent on the wire and must not be separated by this dock's encoder.
+ for connector in 0..MAX_CONNECTORS {
+ let Some(timing) = timings[connector] else {
+ continue;
+ };
+ let key = timing_key(&timing);
+ // Only that the connector still wants this mode. Whether it is already lit is the
+ // caller's to decide: the cold table takes connectors whose generation is zero, and the
+ // runtime table exists precisely to reconfigure one that is streaming.
+ if self.modeset_requested[connector].load(Ordering::Acquire) != key {
+ continue;
+ }
+ timings[connector] =
+ Some(self.effective_timing_in_mask(connector, &timing, requested_heads));
+ let padded_width =
+ (timing.hactive as usize + geometry.strip_w() - 1) & !(geometry.strip_w() - 1);
+ let padded_height =
+ (timing.vactive as usize + geometry.strip_h() - 1) & !(geometry.strip_h() - 1);
+ prompts[connector] = Some(crate::video::haar::black_frame_ep08(
+ geometry,
+ padded_width,
+ padded_height,
+ connector as u8,
+ )?);
+ ordinary_prompts[connector] = Some(crate::video::haar::black_frame_ep08_ordinary(
+ geometry,
+ padded_width,
+ padded_height,
+ connector as u8,
+ )?);
+ keys[connector] = key;
+ valid |= 1u32 << connector;
+ }
+ if valid.count_ones() < wanted {
+ return Ok(false);
+ }
+
+ // Slot -> connector, in activation order. Two is this dock's whole complement of
+ // connectors.
+ let mut slots = [0u8; 2];
+ let mut n = 0;
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ if n < slots.len() {
+ slots[n] = connector as u8;
+ }
+ n += 1;
+ }
+ }
+ if n > slots.len() {
+ pr_warn!(
+ "vino: {n} connectors activating but this dock has {}; choreographing {} and {}\n",
+ slots.len(),
+ slots[0],
+ slots[1]
+ );
+ }
+ let connector_of = |slot: u8| usize::from(slots[usize::from(slot).min(slots.len() - 1)]);
+
+ // The runtime table can enter with a live target. Once any table step runs, its old stream
+ // state is no longer safe to adopt: Ella's live table changes sink markers before SetMode.
+ // Invalidate every participant before the first bracket/control write. A request that moved
+ // while carriers were encoded is then left at zero for its newer command to establish.
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ self.modeset_active[connector].store(0, Ordering::Release);
+ self.programmed_timing.lock()[connector] = None;
+ }
+ }
+ if (0..MAX_CONNECTORS).any(|connector| {
+ valid & (1u32 << connector) != 0
+ && self.modeset_requested[connector].load(Ordering::Acquire) != keys[connector]
+ }) {
+ return Ok(false);
+ }
+
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ self.close_blank_bracket(dev, connector as u8)?;
+ }
+ }
+
+ self.begin_cp_timeline();
+ let started = Instant::<Monotonic>::now();
+ let mut sent = 0u32;
+ let transaction = (|| -> Result<u32> {
+ for step in steps {
+ match *step {
+ DockWideStep::SetMode(slot) => {
+ let connector = connector_of(slot);
+ let timing = timings[connector].ok_or(EINVAL)?;
+ self.send_cp(dev, 0x48, 0, |ctr| {
+ crate::cp::set_mode(ctr, connector as u8, &timing)
+ })?;
+ self.programmed_timing.lock()[connector] = Some(timing);
+ if self.modeset_requested[connector].load(Ordering::Acquire)
+ != keys[connector]
+ {
+ return Ok(sent);
+ }
+ self.modeset_active[connector].store(keys[connector], Ordering::Release);
+ self.sustain_until.lock()[connector] = self.sustain_window(connector);
+ self.arm_stream_prologue(connector);
+ // A driven connector's stream opens with its pipe descriptor, not the idle
+ // open.
+ self.stream_open_pending
+ .fetch_and(!(1u32 << connector), Ordering::Release);
+ self.owe_keyframe(connector);
+ self.strip_hashes.lock()[connector] = None;
+ self.dirty_ttl.lock()[connector] = None;
+ sent |= 1u32 << connector;
+ }
+ DockWideStep::Marker(slot, sub, state) => {
+ self.stream_marker(dev, connector_of(slot) as u8, sub, state)?;
+ }
+ DockWideStep::Poll => self.poll_status(dev)?,
+ DockWideStep::Prologue(slot) => {
+ self.send_stream_prologue(dev, connector_of(slot) as u8)?;
+ }
+ DockWideStep::Ring(slot) => {
+ self.send_stream_ring(dev, connector_of(slot) as u8)?;
+ }
+ DockWideStep::Config(slot) => {
+ self.send_stream_config(dev, connector_of(slot) as u8)?;
+ }
+ DockWideStep::Carrier(slot) => {
+ let connector = connector_of(slot);
+ if sent & (1u32 << connector) == 0 {
+ continue;
+ }
+ self.submit_prompt_training(
+ dev,
+ connector as u8,
+ keys[connector],
+ prompts[connector].as_ref().ok_or(EINVAL)?,
+ ordinary_prompts[connector].as_ref().ok_or(EINVAL)?,
+ self.carrier_ms(PROMPT_TRAINING_OPEN_MS),
+ true,
+ )?;
+ }
+ DockWideStep::Stream(slot, frames) => {
+ // The vendor does not pause here, it keeps presenting. Its second connector
+ // is configured and its sink brought up while the first one's stream is
+ // running, so the records around this step reach a dock that is mid-frame
+ // -- which sleeping through reproduces on the wire and not at all in what
+ // the dock is doing. Present the connector's own flat surface: this
+ // transaction owns the endpoint exclusively and cannot take the
+ // compositor's live frame, but keeping the stream and its ring advancing is
+ // what the step is for.
+ let connector = connector_of(slot);
+ if sent & (1u32 << connector) == 0 {
+ continue;
+ }
+ self.submit_prompt_training(
+ dev,
+ connector as u8,
+ keys[connector],
+ prompts[connector].as_ref().ok_or(EINVAL)?,
+ ordinary_prompts[connector].as_ref().ok_or(EINVAL)?,
+ self.carrier_ms(
+ self.frame_period_ms().saturating_mul(i64::from(frames)),
+ ),
+ false,
+ )?;
+ }
+ }
+ }
+ Ok(sent)
+ })();
+ self.end_cp_timeline();
+
+ let sent = match transaction {
+ Ok(sent) => sent,
+ Err(e) => {
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ self.modeset_active[connector].store(0, Ordering::Release);
+ self.programmed_timing.lock()[connector] = None;
+ }
+ }
+ // Unwind on `valid`: the transaction opens every activating connector's bracket
+ // before it reaches that connector's mode set, so a connector that failed early is
+ // still open on the dock.
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ self.unwind_bracket(dev, connector as u8);
+ }
+ }
+ return Err(e);
+ }
+ };
+ if sent.count_ones() < wanted {
+ for connector in 0..MAX_CONNECTORS {
+ if valid & (1u32 << connector) != 0 {
+ self.modeset_active[connector].store(0, Ordering::Release);
+ self.programmed_timing.lock()[connector] = None;
+ self.unwind_bracket(dev, connector as u8);
+ }
+ }
+ return Ok(false);
+ }
+ vino_debug!(
+ "vino: dock-wide activation complete after {} ms (connectors 0x{:x})\n",
+ (Instant::<Monotonic>::now() - started).as_millis(),
+ sent
+ );
+ Ok(true)
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/bracket.rs b/drivers/gpu/drm/vino/drm_sink/bracket.rs
new file mode 100644
index 000000000000..e18a698ca16b
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/bracket.rs
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Taking a connector's sink down and back up, and the brackets around a mode set.
+//!
+//! A dock does not simply accept a new timing. Its vendor drives the sink down, programs the
+//! timing and brings it back up, and the states and the gaps between them are what retrains the
+//! downstream link. Get the sequence wrong and the dock accepts every byte of every frame and
+//! lights nothing, with nothing on the wire to say so.
+
+use super::*;
+
+impl VinoDrmData {
+ /// Drive `connector` to black on the dock, then close its stream bracket.
+ ///
+ /// Runs on the command worker for [`KmsCmd::Blank`], i.e. after `atomic_disable` has already
+ /// zeroed this connector's mode generation. That zero is what makes the write legal: every
+ /// video path gates on `modeset_requested == modeset_active == want`, and passing `want = 0`
+ /// matches exactly the disabled state and nothing else -- so a re-enable racing this blank
+ /// flips both atomics to a real key and the submit loop drops out with `ENODEV` instead of
+ /// painting black over the freshly enabled mode.
+ ///
+ /// The dock's stream itself is still configured (vino never told it otherwise), so the black
+ /// frames are an ordinary accepted write, not a write onto a torn-down pipe.
+ pub(super) fn blank_connector(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ let socket = connector + 1;
+ let connector_index = connector as usize;
+ // A dock that wants its markers held takes two of them and then silence: no video, no mode
+ // set and no close bracket for as long as the output stays down. It is the same pair
+ // `modeset_bracket_pre` opens with, so the stream is held rather than torn down, and
+ // [`Self::close_blank_bracket`] owes the matching re-open before the connector is driven
+ // again. The scanout has already stopped, because `atomic_disable` zeroed this connector's
+ // mode generation before queueing this command.
+ //
+ // Sending such a dock the close bracket below instead re-enumerates it about two seconds
+ // later, which takes the whole desktop down with it.
+ if self.blank_bracket() == crate::profile::BlankBracket::MarkersHeld {
+ self.stream_marker(dev, connector, 0x2f, 1)?;
+ self.stream_marker(dev, connector, 0x2e, self.sink_down_state())?;
+ self.set_self_blanked(connector_index, true);
+ self.blank_bracket_open
+ .fetch_or(1u32 << connector_index, Ordering::Release);
+ vino_debug!("vino: socket {socket} blanked; bracket held open, stream idle\n");
+ return Ok(());
+ }
+ let Some(timing) = self.last_timing.lock()[connector_index] else {
+ // Never modeset, so there is nothing lit to blank.
+ return Ok(());
+ };
+ let geometry = self.geometry();
+ let padded_width =
+ (timing.hactive as usize + geometry.strip_w() - 1) & !(geometry.strip_w() - 1);
+ let padded_height =
+ (timing.vactive as usize + geometry.strip_h() - 1) & !(geometry.strip_h() - 1);
+ let frames =
+ crate::video::haar::black_frame_ep08(geometry, padded_width, padded_height, connector)?;
+ let ordinary_frames = crate::video::haar::black_frame_ep08_ordinary(
+ geometry,
+ padded_width,
+ padded_height,
+ connector,
+ )?;
+ // Present for long enough to reach every dock buffer. The dock is multi-buffered and a
+ // single presentation lands in one buffer only -- the same reason damage debt exists --
+ // so a one-shot blank leaves the other buffer holding the frozen desktop and the panel
+ // alternates between black and stale content.
+ let sent = self.submit_prompt_training(
+ dev,
+ connector,
+ 0,
+ &frames,
+ &ordinary_frames,
+ BLANK_PRESENT_MS,
+ false,
+ )?;
+ self.stream_marker(dev, connector, 0x2f, 0)?;
+ self.stream_marker(dev, connector, 0x2e, 0)?;
+ // Do not take the sink down for a connector whose monitor has already gone away.
+ //
+ // `atomic_disable` fires for both a DPMS-off and a monitor removal, and they need opposite
+ // treatment. Sending the power-down marker at a sink that is already gone is pointless, and
+ // setting `self_blanked` would make the presence watcher deliberately ignore that
+ // connector's silence, preventing a later replug from being detected.
+ let candidate = if self.connector_present(connector_index) {
+ self.sink_down_state()
+ } else {
+ vino_debug!(
+ "vino: socket {socket} blank skips the sink marker -- its monitor is already gone\n"
+ );
+ 0
+ };
+ if candidate != 0 {
+ // From here the dock will stop answering this connector's presence probe, exactly as it
+ // does for a real unplug. Claim the silence before causing it.
+ self.set_self_blanked(connector_index, true);
+ if let Err(e) = self.power_down_sink(dev, connector) {
+ self.set_self_blanked(connector_index, false);
+ return Err(e);
+ }
+ }
+ vino_debug!("vino: socket {socket} blanked on the dock ({sent} black presentation(s))\n");
+ Ok(())
+ }
+ /// Take one connector's downstream sink out of power, leaving the monitor with no signal.
+ ///
+ /// The dock goes on scanning out whatever it last decoded, so a driver that simply stops
+ /// sending pixels leaves the panel lit on a frozen image indefinitely. Only this sequence ends
+ /// that.
+ fn power_down_sink(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ let socket = connector + 1;
+ self.stream_marker(dev, connector, 0x2f, 1)?;
+ self.stream_marker(dev, connector, 0x2e, self.sink_down_state())?;
+ self.poll_status(dev)?;
+ self.stream_marker(dev, connector, 0x2f, 0)?;
+ vino_debug!("vino: socket {socket} downstream sink powered down\n");
+ Ok(())
+ }
+ /// Power down every sink this driver lit, before it stops being able to.
+ ///
+ /// Unbinding does not reach the dock: `atomic_disable` queues the blank on the command worker,
+ /// and teardown discards that queue and cancels the worker before it runs. The dock therefore
+ /// keeps scanning out the last frame it decoded and the monitors stay lit on a frozen desktop
+ /// until something else drives them.
+ ///
+ /// Best effort by construction. It runs on the disconnect path, where the device may already be
+ /// physically gone -- every transfer then fails immediately, which is the right outcome -- and
+ /// it must not be the reason unbinding blocks, so it does no work at all when no connector is
+ /// lit and its waits are the bounded ones the mode-set path already uses.
+ pub(crate) fn park_sinks(&self) {
+ let lit: u32 = self
+ .modeset_active
+ .iter()
+ .enumerate()
+ .fold(0, |mask, (h, active)| {
+ mask | (u32::from(active.load(Ordering::Acquire) != 0) << h)
+ });
+ // Nothing lit, or nothing to say it to: an unplug arrives here with the session already
+ // gone, and waiting for the video path to drain against a dead dock buys nothing.
+ if lit == 0 || self.check_cp_session().is_err() {
+ return;
+ }
+ // Stand the scanout workers down and let any frame already on the wire finish, so these
+ // markers do not land in the middle of one.
+ self.cmd_busy
+ .store(true, core::sync::atomic::Ordering::SeqCst);
+ self.wait_for_video_idle();
+ let Ok(link) = crate::usb_link::UsbLink::open(&self.io, self.endpoints) else {
+ return;
+ };
+ for connector in 0..MAX_CONNECTORS {
+ if lit & (1u32 << connector) == 0 {
+ continue;
+ }
+ if let Err(e) = self.power_down_sink(&link, connector as u8) {
+ vino_debug!(
+ "vino: socket {} sink power-down on unbind failed ({e:?})\n",
+ connector + 1
+ );
+ }
+ }
+ }
+ /// Drive one connector's stream bracket to the closed state.
+ ///
+ /// The dock holds a connector opened with `2e=3` until it is told otherwise: it stops driving
+ /// the sink and disengages that connector's EDID handler, so the connector then reads exactly
+ /// like an empty socket. Nothing but this sequence puts it back, and the dock keeps the state
+ /// across a USB re-enumeration, so it must be sent rather than inferred.
+ fn send_bracket_close(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ self.stream_marker(dev, connector, 0x2f, 1)?;
+ self.stream_marker(dev, connector, 0x2e, 0)?;
+ self.stream_marker(dev, connector, 0x2f, 0)?;
+ self.stream_marker(dev, connector, 0x2e, 0)
+ }
+ /// Assert the closed bracket state on a connector that is about to be probed.
+ ///
+ /// The dock's bracket state outlives this driver's record of it, so dedicated-pipe docks assert
+ /// it before probing. Ella is different: its vendor stream never performs a periodic reset, and
+ /// the four markers share EP02 with pixels. Resetting an idle socket while its sibling is live
+ /// drops the live sink, so its profile defers the close until no sibling is active.
+ ///
+ /// Best effort otherwise: a connector that is genuinely empty is not worth failing a probe
+ /// over.
+ pub(super) fn close_bracket_before_probe(&self, dev: &BoundInterface<'_>, connector: u8) {
+ let bit = 1u32 << connector;
+ let active_connectors = self
+ .modeset_active
+ .iter()
+ .enumerate()
+ .fold(0u32, |mask, (h, active)| {
+ mask | (u32::from(active.load(Ordering::Acquire) != 0) << h)
+ });
+ if !self
+ .probe_bracket()
+ .should_close(connector, active_connectors)
+ {
+ vino_debug!(
+ "vino: socket {} probe defers bracket reset beside active sibling(s) {:#x}\n",
+ connector + 1,
+ active_connectors & !bit
+ );
+ return;
+ }
+ if self.send_bracket_close(dev, connector).is_ok() {
+ self.blank_bracket_open.fetch_and(!bit, Ordering::AcqRel);
+ }
+ }
+ /// Put a connector back into the closed bracket state after a failed mode set.
+ ///
+ /// A mode set opens the bracket before it configures anything, so an error anywhere after that
+ /// point leaves the connector open on the dock with no record of it here. Best effort: this
+ /// runs on the error path, and the transport that just failed may fail again.
+ pub(super) fn unwind_bracket(&self, dev: &BoundInterface<'_>, connector: u8) {
+ // A dead session cannot carry the close, and every queued mode set fails against it, so
+ // attempting one per failure floods the log with a consequence of the disconnect rather
+ // than a cause. The dock is being re-established anyway; a fresh session closes the
+ // bracket in `reengage_connector` before it probes.
+ if !self.cp_link_alive() {
+ return;
+ }
+ if self.send_bracket_close(dev, connector).is_err() {
+ // The dock is still holding this connector. Say so once, rather than reporting only the
+ // error that got us here, because the connector now reads as an empty socket.
+ pr_warn!(
+ "vino: socket {socket} left open after a failed mode set; it will read as empty until a re-engage closes it\n",
+ socket = connector + 1
+ );
+ }
+ }
+ /// Close a blank bracket before this connector is driven again.
+ ///
+ /// Precedes the EDID probe and the mode set that follow. A connector that was never blanked
+ /// costs nothing here.
+ pub(super) fn close_blank_bracket(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ let socket = connector + 1;
+ let bit = 1u32 << connector;
+ if self.blank_bracket_open.load(Ordering::Acquire) & bit == 0 {
+ return Ok(());
+ }
+ // Clear the record only once the dock has been told, so a send that fails partway leaves
+ // the debt standing for the next attempt. Clearing first strands the connector open
+ // forever: the driver stops believing anything is owed while the dock goes on holding it.
+ self.send_bracket_close(dev, connector)?;
+ self.blank_bracket_open.fetch_and(!bit, Ordering::AcqRel);
+ // Closing the bracket restores the stream, not the sink. The blank powered the downstream
+ // sink down and nothing else turns it back on, so the vendor follows the closing markers
+ // with a full probe, EDID fetch and sink engage before it programs a timing. A dock woken
+ // without them comes back slowly, or not at all once the sink has been down long enough
+ // for the dock to have let go of it.
+ //
+ // Best effort: a connector whose monitor genuinely went away while it was blanked belongs
+ // to the presence watcher, and the mode set below is what reports a wake that failed.
+ //
+ // Not on a dock whose video shares the control pipe, for the same reason `sustain_window`
+ // is withheld there: the seven paced messages are taken directly from the pipe the wake is
+ // also driving pixels down, and this dock answers by going silent rather than by dropping
+ // a frame.
+ if !self.video_on_ctrl_pipe() && self.reengage_connector(dev, connector).is_err() {
+ vino_debug!("vino: socket {socket} wake re-engage failed; the mode set will retry\n");
+ }
+ // A wake is still not a cold plug: the re-engage above restores the sink, so the
+ // three-second keyframe window `sustain_window` grants a cold activation buys nothing here
+ // and costs about a gigabyte per connector. Sustained bandwidth is also what destabilises
+ // this dock, so it is never spent twice.
+ self.repair_connectors.fetch_or(bit, Ordering::Release);
+ vino_debug!("vino: socket {socket} blank bracket closed; wake runs as a repair\n");
+ Ok(())
+ }
+ /// Open the per-connector stream bracket before changing an active mode.
+ pub(super) fn modeset_bracket_pre(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ self.stream_marker(dev, connector, 0x2f, 1)?;
+ // Docks that want the sink torn down before it is configured say so in their profile.
+ // Where no state is carried the vendor sets the mode first and brackets behind it, and
+ // downing a sink that is about to be programmed would leave it down for the whole bracket.
+ if let Some(state) = self.pre_mode_sink_state() {
+ self.stream_marker(dev, connector, 0x2e, state)?;
+ }
+ self.poll_status(dev)
+ }
+ /// Sleep until an absolute millisecond offset from the mode-set anchor.
+ ///
+ /// Absolute deadlines keep scheduler delay from accumulating across the activation sequence.
+ pub(super) fn wait_mode_offset(anchor: Instant<Monotonic>, target_ms: i64) {
+ let elapsed_ms = (Instant::<Monotonic>::now() - anchor).as_millis();
+ if elapsed_ms < target_ms {
+ fsleep(Delta::from_millis(target_ms - elapsed_ms));
+ }
+ }
+ /// Sleep/spin until an exact microsecond offset in a short video-transport schedule.
+ ///
+ /// `fsleep` handles the bulk of the delay without burning a CPU; the small busy-wait tail
+ /// avoids scheduling a producer boundary hundreds of microseconds late. This is used only for
+ /// the four submissions of Navarro's one-shot prologue.
+ pub(super) fn wait_video_offset(anchor: Instant<Monotonic>, target_us: i64) {
+ const SPIN_MARGIN_US: i64 = 80;
+ let elapsed = anchor.elapsed().as_micros_ceil();
+ if elapsed >= target_us {
+ return;
+ }
+ if target_us - elapsed > SPIN_MARGIN_US {
+ fsleep(Delta::from_micros(target_us - elapsed - SPIN_MARGIN_US));
+ }
+ let elapsed = anchor.elapsed().as_micros_ceil();
+ if elapsed < target_us {
+ udelay(Delta::from_micros(target_us - elapsed));
+ }
+ }
+ /// Complete the stream-open markers and status polls up to the first video deadline.
+ pub(super) fn modeset_bracket_post_open(
+ &self,
+ dev: &BoundInterface<'_>,
+ connector: u8,
+ anchor: Instant<Monotonic>,
+ ) -> Result {
+ self.poll_status(dev)?;
+ Self::wait_mode_offset(anchor, 5);
+ self.stream_marker(dev, connector, 0x2f, 1)?;
+ Self::wait_mode_offset(anchor, 9);
+ self.stream_marker(dev, connector, 0x2e, self.post_mode_sink_state(0))?;
+ Self::wait_mode_offset(anchor, 12);
+ self.stream_marker(dev, connector, 0x2f, 1)?;
+ Self::wait_mode_offset(anchor, 14);
+ // `0x2e` state 3 takes the downstream sink down and 0 brings it back up. The vendor drives
+ // it down once here and then straight back up; repeating the 3 leaves the sink down for
+ // the rest of the bracket, which on DL-3x00 is a dock that accepts every byte of a frame
+ // and displays none of it.
+ self.stream_marker(dev, connector, 0x2e, self.post_mode_sink_state(1))?;
+ // The vendor's ring descriptor and decoder configuration land here, between the fourth
+ // marker and the fifth, so the closing `2e(connector, 0)` below is the last thing the dock
+ // sees before pixels. A dock told to bring its sink up after a frame has already gone out
+ // has been handed that frame with nothing scanning it out.
+ self.send_stream_prologue(dev, connector)?;
+ Self::wait_mode_offset(anchor, 20);
+ self.stream_marker(dev, connector, 0x2f, 1)?;
+ // The status poll shares the final `2f(1)` deadline.
+ self.poll_status(dev)?;
+ Self::wait_mode_offset(anchor, 26);
+ self.stream_marker(dev, connector, 0x2e, 0)?;
+ // There is a measured 63-ms quiet interval, then three polls at +89/+95/+110 ms. The last
+ // poll and first video bytes share one deadline.
+ Self::wait_mode_offset(anchor, 89);
+ self.poll_status(dev)?;
+ Self::wait_mode_offset(anchor, 95);
+ self.poll_status(dev)?;
+ Self::wait_mode_offset(anchor, PROMPT_VIDEO_MS);
+ self.poll_status(dev)
+ }
+ /// Close the post-mode-set bracket after prompt video has started.
+ ///
+ /// The first close marker is +13 ms from video and the second is +15 ms. Background keepalive
+ /// resumes immediately after this pair and supplies the continuing status dialogue.
+ ///
+ /// A dock without a video pipe has already closed its bracket: its last marker before the
+ /// strips is the sink-up, and the vendor sends nothing at all between a frame and the next
+ /// frame's opener. Closing again there puts two records into the one gap the vendor leaves
+ /// empty, one of them a marker state it never uses on this generation.
+ pub(super) fn modeset_bracket_post_close(
+ &self,
+ dev: &BoundInterface<'_>,
+ connector: u8,
+ anchor: Instant<Monotonic>,
+ ) -> Result {
+ if self.video_on_ctrl_pipe() {
+ return Ok(());
+ }
+ Self::wait_mode_offset(anchor, PROMPT_CLOSE_2F_MS);
+ self.stream_marker(dev, connector, 0x2f, 0)?;
+ Self::wait_mode_offset(anchor, PROMPT_CLOSE_2E_MS);
+ self.stream_marker(dev, connector, 0x2e, 0)
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/cp_session.rs b/drivers/gpu/drm/vino/drm_sink/cp_session.rs
new file mode 100644
index 000000000000..36b3a46026a5
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/cp_session.rs
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The control-plane session: sending sealed messages and keeping the link alive.
+//!
+//! The control plane is host-driven lockstep. Every message carries an authenticated counter that
+//! both ends step together, so a message sent out of turn, or a reply left undrained,
+//! desynchronises the session and every later message decrypts to nothing. That is why sending is
+//! serialised here rather than at the call sites.
+
+use super::*;
+
+impl VinoDrmData {
+ pub(crate) fn set_cp_engaged(&self, engaged: bool) {
+ self.cp_engaged
+ .store(engaged, core::sync::atomic::Ordering::SeqCst);
+ }
+ /// Whether the control session is still usable.
+ ///
+ /// Reads the flag, not the mutex: the caller is usually asking precisely because the link may
+ /// be stuck, and the stuck thread is holding that mutex.
+ pub(crate) fn cp_link_alive(&self) -> bool {
+ self.cp_session_live.load(Ordering::Acquire)
+ }
+ /// Record that the dock answered. Resets the silence deadline.
+ pub(super) fn note_cp_reply(&self) {
+ *self.cp_last_reply.lock() = Instant::<Monotonic>::now();
+ }
+ /// How long the dock has answered nothing, in milliseconds.
+ pub(super) fn cp_silent_for_ms(&self) -> i64 {
+ (Instant::<Monotonic>::now() - *self.cp_last_reply.lock()).as_millis()
+ }
+ /// Give up on the control session. Idempotent, and logs once.
+ ///
+ /// Only the flag is cleared here. Callers that hold `cp_link` drop its contents themselves;
+ /// the watchdog cannot, because the mutex is exactly what a wedged transfer is holding. Every
+ /// path consults the flag before the mutex, so an orphaned [`CpLink`] is unreachable, and
+ /// [`Self::shutdown`] frees it with the rest of the session state.
+ pub(super) fn abandon_cp_session(&self, silent_ms: i64) {
+ if self
+ .cp_session_live
+ .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok()
+ {
+ pr_warn!(
+ "vino: dock has answered nothing for {silent_ms} ms; abandoning the session\n"
+ );
+ }
+ }
+ /// Gate every control transaction, without taking `cp_link`.
+ ///
+ /// Checking the deadline here rather than under the mutex is the difference between noticing
+ /// the dock has gone and queueing another thread behind the transfer that proves it.
+ pub(super) fn check_cp_session(&self) -> Result {
+ // Teardown first. `disconnect()` waits for the workers, and a worker that starts a control
+ // transfer to a device already being disconnected waits for a completion that will not
+ // come; on a dock whose video shares this endpoint the scanout path issues those transfers
+ // too, so the window is wide. The pair deadlocks `usb_hub_wq` inside `usb_disconnect()`,
+ // which stops USB hotplug machine-wide and is only recoverable by rebooting.
+ if self.shutting_down.load(Ordering::Acquire) {
+ return Err(ENODEV);
+ }
+ if !self.cp_link_alive() {
+ return Err(ENODEV);
+ }
+ let silent_ms = self.cp_silent_for_ms();
+ if silent_ms >= self.cp_silence_limit_ms() {
+ self.abandon_cp_session(silent_ms);
+ return Err(ETIMEDOUT);
+ }
+ Ok(())
+ }
+ /// How long this dock may say nothing before its session is abandoned.
+ pub(super) fn cp_silence_limit_ms(&self) -> i64 {
+ if self.video_on_ctrl_pipe() {
+ CP_SILENCE_LIMIT_SHARED_MS
+ } else {
+ CP_SILENCE_LIMIT_MS
+ }
+ }
+ /// Ask the USB core to reset the dock after the control session has been abandoned.
+ ///
+ /// Without this the outputs stay down until the user unplugs the dock, because a session can
+ /// only be established through probe. It is the one recovery a stuck transfer cannot block:
+ /// the USB core runs it from its own work item. What turns it into a fresh session is
+ /// `post_reset` asking for the interface to be rebound -- a reset on its own leaves the
+ /// driver bound with nothing to drive.
+ pub(super) fn reset_after_wedge(&self) {
+ if self
+ .cp_reset_queued
+ .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
+ .is_err()
+ {
+ return;
+ }
+ // A closed window means unplug is already under way, which needs no help from us.
+ let Ok(io) = self.io.enter() else {
+ return;
+ };
+ pr_warn!("vino: resetting the dock to recover the control session\n");
+ io.interface().queue_reset_device();
+ }
+ /// Whether the current device session has engaged content protection.
+ pub(crate) fn cp_engaged(&self) -> bool {
+ self.cp_engaged.load(core::sync::atomic::Ordering::Acquire)
+ }
+ /// Pause the background CP loop and let any iteration which passed its check finish. The mode
+ /// worker calls this before taking its timestamp anchor; the fixed delay therefore cannot move
+ /// any event relative to the mode-set itself.
+ pub(super) fn begin_cp_timeline(&self) {
+ self.cp_timeline_exclusive.store(true, Ordering::Release);
+ self.initial_modeset_quiet.store(false, Ordering::Release);
+ fsleep(Delta::from_millis(PROMPT_KEEPALIVE_QUIESCE_MS));
+ }
+ pub(super) fn end_cp_timeline(&self) {
+ self.cp_timeline_exclusive.store(false, Ordering::Release);
+ }
+ /// Used by `BringUp`'s long-lived keepalive worker. It deliberately remains cheap because that
+ /// worker checks it every millisecond while an activation sequence is in progress.
+ pub(crate) fn cp_timeline_exclusive(&self) -> bool {
+ self.cp_timeline_exclusive.load(Ordering::Acquire)
+ }
+ /// Publish the engaged CP session so the KMS callbacks can send runtime CP messages.
+ /// Called once by the bring-up work item after the dock acks (`acks > 0`). `wire_seq`/
+ /// `counter` are the next free values past the bring-up CP setup.
+ pub(crate) fn publish_session(
+ &self,
+ dev: &BoundInterface<'_>,
+ ks: &[u8; 16],
+ riv: &[u8; 8],
+ wire_seq: u32,
+ counter: u16,
+ ep84_depth: usize,
+ ) {
+ // EP84 must remain posted between runtime EP02 writes. A queue drained synchronously leaves
+ // the endpoint unposted between calls and can stall the control protocol.
+ // `ep84_depth` is the matched profile's `ep84_queue_depth`, so the runtime queue keeps the
+ // same number of reads posted that bring-up did.
+ let ep84_q = match dev.ctrl_in_queue(ep84_depth, 4096) {
+ Ok(q) => Some(q),
+ Err(e) => {
+ pr_warn!("vino: persistent EP84 queue open failed ({e:?}); using sync fallback\n");
+ None
+ }
+ };
+ *self.cp_link.lock() = Some(CpLink {
+ ks: kernel::crypto::Secret::new(*ks),
+ riv: *riv,
+ wire_seq,
+ counter,
+ ep84_q,
+ });
+ self.note_cp_reply();
+ self.cp_session_live.store(true, Ordering::Release);
+ }
+ /// Start the silence watchdog for the session just published.
+ ///
+ /// Separate from [`Self::publish_session`] because it needs an `ARef` to the device, and it
+ /// re-arms itself for the lifetime of the session.
+ pub(crate) fn start_cp_watchdog(&self, drm_dev: &VinoDrmDevice) {
+ if self.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ let delay = kernel::time::msecs_to_jiffies(CP_WATCHDOG_PERIOD_MS);
+ let _ = workqueue::system().enqueue_delayed::<_, 5>(ARef::from(drm_dev), delay);
+ }
+ /// Seal and send one interactive CP message, advance the session, and pass its paired reply
+ /// to `consume`.
+ ///
+ /// `build(counter)` produces the inner message for the dock-echoed counter. The `cp_link`
+ /// mutex serialises the complete EP02/EP84 transaction with the KMS worker and keepalive.
+ /// Callers are sleepable; atomic callbacks queue commands instead of invoking this path.
+ fn send_cp_reply<T>(
+ &self,
+ dev: &BoundInterface<'_>,
+ id: u16,
+ tag_reserved: usize,
+ reserved_counter: Option<u16>,
+ build: impl FnOnce(u16) -> Result<KVec<u8>>,
+ consume: impl FnOnce(&[u8; 16], &[u8; 8], &[u8]) -> Result<T>,
+ ) -> Result<T> {
+ self.check_cp_session()?;
+ self.send_cp_locked(dev, id, tag_reserved, reserved_counter, build, consume)
+ }
+ #[allow(clippy::too_many_arguments)]
+ fn send_cp_locked<T>(
+ &self,
+ dev: &BoundInterface<'_>,
+ id: u16,
+ tag_reserved: usize,
+ reserved_counter: Option<u16>,
+ build: impl FnOnce(u16) -> Result<KVec<u8>>,
+ consume: impl FnOnce(&[u8; 16], &[u8; 8], &[u8]) -> Result<T>,
+ ) -> Result<T> {
+ let mut guard = self.cp_link.lock();
+ let Some(link) = (&mut *guard).as_mut() else {
+ return Err(ENODEV);
+ };
+ // DLM normally uses the next wire-order counter. Its cold Navarro mode transaction is the
+ // exception: per-connector workers reserve counters before their writes interleave, so the
+ // inner counter order differs from the monotonically advancing AES block sequence.
+ let request_counter = reserved_counter.unwrap_or(link.counter);
+ let msg = build(request_counter)?;
+ let inner_sub = if msg.len() >= 4 {
+ u16::from_le_bytes([msg[2], msg[3]])
+ } else {
+ 0
+ };
+ let content = &msg[..msg.len().saturating_sub(tag_reserved)];
+ let frame = crate::cp::seal_interactive(&link.ks, &link.riv, id, link.wire_seq, content)?;
+ let pipe = self.own_pipe();
+ // A shared video failure publishes the session dead while a CP writer may already be
+ // waiting for this pipe behind the failed frame. Recheck after acquiring it so that
+ // waiter cannot issue one last control record into the terminal stream before reset.
+ if !self.cp_link_alive() {
+ return Err(ENODEV);
+ }
+ if let Err(e) = dev.ctrl_send(&frame, crate::timeout(), GFP_KERNEL) {
+ let silent_ms = self.cp_silent_for_ms();
+ if silent_ms >= self.cp_silence_limit_ms() {
+ self.abandon_cp_session(silent_ms);
+ *guard = None;
+ }
+ return Err(e);
+ }
+ // The reply arrives on the other endpoint, so the pipe is free again the moment the
+ // request is out: holding it across the wait would stall a frame for the whole timeout.
+ drop(pipe);
+ link.wire_seq = link
+ .wire_seq
+ .wrapping_add(((content.len() + 15) / 16) as u32);
+ // A normal message consumes the next counter now. A reserved message consumed its counter
+ // when its logical worker queued the transaction, before independently queued EP02 writes
+ // interleaved; consuming it twice here would skip a value after the cold transaction.
+ if reserved_counter.is_none() {
+ link.counter = link.counter.wrapping_add(1);
+ }
+ // DLM keeps reading EP84 until it sees the reply whose inner counter echoes this request.
+ // Navarro also emits unprompted `id=2/sub=0x86` status pushes on the same endpoint;
+ // treating the first such push as the paired reply advances EP02 before the dock has
+ // completed the transaction. The dock then NAKs that write until the real reply is reaped,
+ // which is the exact 100-ms staircase visible in the failed captures. Consume pushes here
+ // and stop only at the echoed counter (or a bounded timeout for request classes which do
+ // not reply).
+ //
+ //
+ // Use the validated 4096-byte request size so larger logical replies arrive intact.
+ let mut reply = KVec::from_elem(0u8, 4096, GFP_KERNEL)?;
+ let deadline = Instant::<Monotonic>::now() + Delta::from_millis(64);
+ let mut matched = 0usize;
+ let (mut reaped, mut undecodable) = (0u32, 0u32);
+ let (mut seen_id, mut seen_sub, mut seen_counter) = (0u16, 0u16, 0u16);
+ loop {
+ let got = if let Some(q) = link.ep84_q.as_mut() {
+ match q.recv(dev.io(), &mut reply, crate::cp_reply_timeout()) {
+ Ok(Some(n)) => n,
+ Ok(None) => 0,
+ Err(_) => break,
+ }
+ } else {
+ dev.ctrl_recv(&mut reply, crate::cp_reply_timeout(), GFP_KERNEL)
+ .unwrap_or(0)
+ };
+ if got > 16 {
+ // During re-engagement an EDID push can precede the paired acknowledgment. Keep
+ // it for the waiting connector while continuing to wait for the echoed counter.
+ let target = self.edid_target.load(Ordering::Relaxed);
+ if target != NO_EDID_TARGET {
+ if let Ok(Some(blob)) =
+ crate::cp::parse_edid_from_reply(&link.ks, &link.riv, &reply[..got])
+ {
+ *self.edid_caught.lock() = Some(blob);
+ }
+ }
+ if let Some((reply_id, reply_sub, reply_counter)) =
+ crate::cp::decode_in_lenient(&link.ks, &link.riv, &reply[..got])
+ {
+ if reply_counter == request_counter {
+ matched = got;
+ break;
+ }
+ seen_id = reply_id;
+ seen_sub = reply_sub;
+ seen_counter = reply_counter;
+ if reply_id == 0x44 || crate::cp::edid_reply_len(reply_id).is_some() {
+ self.downstream_event.store(true, Ordering::Release);
+ }
+ } else {
+ undecodable += 1;
+ }
+ reaped += 1;
+ }
+ if (Instant::<Monotonic>::now() - deadline).as_millis() >= 0 {
+ break;
+ }
+ }
+ // Name the message that went unanswered, and say whether the dock was silent or merely
+ // unreadable. Without this a stalled control session can only be reported as ETIMEDOUT,
+ // which cannot distinguish "the dock sent nothing" from "the dock replied and vino could
+ // not decode it" -- and on the D6000 the wire shows 50 sealed replies arriving during an
+ // attempt that ends in ETIMEDOUT.
+ if matched == 0 {
+ // Name the *inner* sub as well as the wire id. "id=0x16 went unanswered" covers the
+ // EDID engage, the readiness kick and both stream/display markers, and which one the
+ // dock ignored is the whole diagnosis.
+ vino_debug!(
+ "vino: unanswered id={id:#06x} sub={inner_sub:#06x} ctr={request_counter}: reaped {reaped} reply/replies, {undecodable} undecodable, last decoded id={seen_id:#06x} sub={seen_sub:#06x} ctr={seen_counter}\n"
+ );
+ let silent_ms = self.cp_silent_for_ms();
+ if silent_ms >= self.cp_silence_limit_ms() {
+ self.abandon_cp_session(silent_ms);
+ *guard = None;
+ return Err(ETIMEDOUT);
+ }
+ } else {
+ self.note_cp_reply();
+ }
+ consume(&link.ks, &link.riv, &reply[..matched])
+ }
+ /// Seal and send one interactive CP message on EP02, advancing the session keystream.
+ pub(crate) fn send_cp(
+ &self,
+ dev: &BoundInterface<'_>,
+ id: u16,
+ tag_reserved: usize,
+ build: impl FnOnce(u16) -> Result<KVec<u8>>,
+ ) -> Result {
+ self.send_cp_reply(dev, id, tag_reserved, None, build, |_, _, _| Ok(()))
+ }
+ /// Send one CP message using a counter token previously consumed from the live allocator.
+ pub(super) fn send_cp_reserved(
+ &self,
+ dev: &BoundInterface<'_>,
+ id: u16,
+ inner_counter: u16,
+ build: impl FnOnce(u16) -> Result<KVec<u8>>,
+ ) -> Result {
+ self.send_cp_reply(dev, id, 0, Some(inner_counter), build, |_, _, _| Ok(()))
+ }
+ /// Consume `N` consecutive counters from the live session and return them as reservation
+ /// tokens. This models DLM's independently queued per-connector workers: allocation order
+ /// remains monotonic even when their actual EP02 writes interleave in a different order.
+ pub(super) fn reserve_cp_counters<const N: usize>(&self) -> Result<[u16; N]> {
+ let mut guard = self.cp_link.lock();
+ let Some(link) = (&mut *guard).as_mut() else {
+ return Err(ENODEV);
+ };
+ let mut counters = [0u16; N];
+ for counter in &mut counters {
+ *counter = link.counter;
+ link.counter = link.counter.wrapping_add(1);
+ }
+ Ok(counters)
+ }
+ /// Consume the dock's *unprompted* EP84 pushes, i.e. reads that are not the reply to any of our
+ /// writes. Returns how many frames were drained.
+ ///
+ /// The dock also emits capability and heartbeat frames without a paired request. The bounded
+ /// zero-timeout loop consumes those pushes without delaying keepalive or allowing a chatty dock
+ /// to monopolise the worker.
+ pub(crate) fn drain_cp_pushes(&self, dev: &BoundInterface<'_>, max: usize) -> usize {
+ if !self.cp_link_alive() {
+ return 0;
+ }
+ // Best-effort really does mean best-effort: if a transaction owns the link there is
+ // nothing to reap that it is not already reaping, and blocking here would put the
+ // keepalive behind a transfer that may never return.
+ let Some(mut guard) = self.cp_link.try_lock() else {
+ return 0;
+ };
+ let Some(link) = (&mut *guard).as_mut() else {
+ return 0;
+ };
+ let Ok(mut reply) = KVec::from_elem(0u8, 4096, GFP_KERNEL) else {
+ return 0;
+ };
+ let mut n = 0;
+ while n < max {
+ let got = match link.ep84_q.as_mut() {
+ // Every queue slot is already posted. One millisecond is enough to reap a
+ // completion without turning this best-effort reader into another control
+ // deadline. A zero-jiffy completion wait does not observe an already-signalled
+ // completion reliably, so it left pushes queued until the next EP02 write.
+ Some(q) => q.recv(dev.io(), &mut reply, Delta::from_millis(1)),
+ None => dev
+ .ctrl_recv(&mut reply, Delta::from_millis(1), GFP_KERNEL)
+ .map(Some),
+ };
+ // `Ok(None)` is the queue's timeout: nothing pending, so the dock has nothing more to
+ // say right now. Any error is treated the same -- this is best-effort drainage.
+ match got {
+ Ok(Some(len)) if len > 0 => {
+ n += 1;
+ // An unprompted push is the dock answering. The silence deadline asks whether
+ // it is talking at all, not whether it is answering us in particular, and on
+ // an idle lit link the heartbeats are most of what it says.
+ self.note_cp_reply();
+ if let Some((id, _, _)) =
+ crate::cp::decode_in_lenient(&link.ks, &link.riv, &reply[..len])
+ {
+ // An EDID-handler reply arriving with no probe outstanding is the dock
+ // reporting that a downstream sink changed -- it is the *only* thing it
+ // sent between a measured monitor replug and its own give-up reset. Treat
+ // it as "re-probe now" rather than waiting out the presence period.
+ if id == 0x44 || crate::cp::edid_reply_len(id).is_some() {
+ self.downstream_event.store(true, Ordering::Release);
+ }
+ }
+ }
+ _ => break,
+ }
+ }
+ n
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/presence.rs b/drivers/gpu/drm/vino/drm_sink/presence.rs
new file mode 100644
index 000000000000..6e58f5729223
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/presence.rs
@@ -0,0 +1,435 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! What is plugged into a connector, and keeping that answer honest.
+//!
+//! A dock reports presence through its EDID probe rather than through any hotplug interrupt, so
+//! everything here is polled and everything here can be wrong for a moment. A monitor still waking
+//! up, a connector the dock has stopped answering for, and a connector this driver blanked itself
+//! all look alike on the wire; telling them apart is what the debounce and repair paths do.
+
+use super::*;
+
+impl VinoDrmData {
+ /// Cache a connector's downstream EDID (read during probe). Bring-up publishes all connectors
+ /// with one hotplug only after both presence and EDID state are complete; firing here exposed
+ /// KWin to a transient no-EDID mode list (including synthetic 1920x1440) before the real EDID
+ /// arrived. Out-of-range connectors are ignored.
+ pub(crate) fn set_edid(&self, connector: usize, blob: KVec<u8>) {
+ let mut edids = self.cached_edids.lock();
+ let Some(slot) = edids.get_mut(connector) else {
+ return;
+ };
+ *slot = Some(blob);
+ }
+ /// Whether userspace has taken responsibility for describing `connector`'s sink, because the
+ /// dock cannot read it.
+ ///
+ /// A DP-to-HDMI converter that mangles or drops DDC leaves the dock unable to read the monitor
+ /// at all: the presence probe reports the socket occupied, but no `id=0x194` ever arrives, so
+ /// the connector stays disconnected and nothing is ever driven. The monitor is real, and its
+ /// EDID is readable from a working port on another machine.
+ ///
+ /// Rather than carry a second EDID source, this hands the connector to DRM's own override,
+ /// which already accepts a blob two ways -- `drm_kms_helper.edid_firmware=<connector>:<path>`
+ /// or a write to the connector's debugfs `edid_override`. The core applies it only to a
+ /// connector that is connected and whose `get_modes` returned none, so all this flag does is
+ /// make the connector report no modes of its own while no EDID has been read.
+ ///
+ /// It deliberately does not report the connector connected. Connecting a modeless connector
+ /// hands fbdev emulation a blank cheque, and its 640x480 default mode set puts this dock into a
+ /// 25 s re-enumeration loop. Userspace supplies the description and then forces the connector
+ /// on (`echo on > .../status`), in that order, which is also what makes the sequence race-free.
+ ///
+ /// An override describes the sink, not the link. A blob claiming a mode the converter cannot
+ /// carry gives a black screen just the same: this substitutes for a broken read, it does not
+ /// negotiate anything.
+ pub(crate) fn edid_from_userspace(&self, connector: usize) -> bool {
+ usize::from(*crate::module_parameters::edid_override.value()) & (1 << connector) != 0
+ }
+ /// Mark a connector connected from CP engagement alone (no raw EDID). Bring-up fires one
+ /// hotplug after every connector's EDID has also been cached, so the compositor never probes
+ /// partial state. Called once the connector's DISPLAY-CAP push confirms monitor presence.
+ pub(crate) fn set_connected(&self, connector: usize) {
+ if connector >= MAX_CONNECTORS {
+ return;
+ }
+ self.connectors_present
+ .fetch_or(1 << connector, core::sync::atomic::Ordering::Release);
+ }
+ /// Clear a connector's presence bit and cached EDID after monitor removal.
+ ///
+ /// `detect()` reports connected when either exists, so both must be cleared together.
+ pub(crate) fn set_disconnected(&self, connector: usize) {
+ let socket = connector + 1;
+ if connector >= MAX_CONNECTORS {
+ return;
+ }
+ // Tagged with the dock's connector count, which is what tells two bound docks apart in a
+ // single log.
+ if self.connectors_present.load(Ordering::Acquire) & (1u32 << connector) != 0 {
+ vino_debug!(
+ "vino: {}-connector dock: socket {socket} presence cleared\n",
+ self.connector_count()
+ );
+ }
+ self.connectors_present
+ .fetch_and(!(1u32 << connector), core::sync::atomic::Ordering::Release);
+ if let Some(slot) = self.cached_edids.lock().get_mut(connector) {
+ *slot = None;
+ }
+ }
+ fn send_reengage_step(
+ &self,
+ io: &BoundInterface<'_>,
+ id: u16,
+ gap_ms: i64,
+ build: impl FnOnce(u16) -> Result<KVec<u8>>,
+ ) -> Result {
+ self.send_cp(io, id, 0, build)?;
+ fsleep(Delta::from_millis(gap_ms));
+ Ok(())
+ }
+ /// Re-run one connector's EDID probe, fetch, engage, and capability query on the live CP link.
+ ///
+ /// Monitor removal tears down the dock's downstream sink, so a replug requires the engage
+ /// messages before a later mode set can start its pixel clock. The cached EDID is cleared on
+ /// removal and must be repopulated here before reporting the connector as present. Returns true
+ /// only when a valid EDID was received.
+ pub(crate) fn reengage_connector(
+ &self,
+ io: &BoundInterface<'_>,
+ connector: u8,
+ ) -> Result<bool> {
+ let socket = connector + 1;
+ self.set_self_blanked(connector as usize, false);
+ // A connector that is not answering is exactly a connector that may be sitting in an open
+ // bracket, where the dock has disengaged its EDID handler and every probe below would go
+ // unanswered. The state is the dock's and it survives a re-enumeration, so a fresh session
+ // cannot know it is owed: assert the closed state rather than infer it. A connector already
+ // closed ignores this.
+ self.close_bracket_before_probe(io, connector);
+ self.edid_target.store(connector as u32, Ordering::Release);
+ *self.edid_caught.lock() = None;
+ let result = (|| -> Result {
+ self.send_reengage_step(io, 0x15, 117, |c| {
+ crate::cp::get_edid_req_sub(c, 0x0020, connector)
+ })?;
+ self.send_reengage_step(io, 0x15, 115, |c| {
+ crate::cp::get_edid_req_sub(c, 0x0020, connector)
+ })?;
+ self.send_reengage_step(io, 0x16, 107, |c| {
+ crate::cp::edid_readiness_kick(c, connector)
+ })?;
+ self.send_reengage_step(io, 0x15, 11, |c| crate::cp::get_edid_req(c, connector))?;
+ self.send_reengage_step(io, 0x16, 118, |c| crate::cp::edid_engage_req(c, connector))?;
+ self.send_reengage_step(io, 0x16, 107, |c| crate::cp::edid_engage_req(c, connector))?;
+ self.send_reengage_step(io, 0x15, 11, |c| crate::cp::post_edid_query(c, connector))
+ })();
+ let caught = self.edid_caught.lock().take();
+ self.edid_target.store(NO_EDID_TARGET, Ordering::Release);
+ result?;
+ match caught.or_else(|| self.drain_for_edid(io)) {
+ Some(blob) => {
+ let n = blob.len();
+ // Say what the EDID claims to be, not just that one arrived. On unfamiliar
+ // hardware this is what distinguishes a real monitor from a block the dock
+ // synthesised for an empty port.
+ if blob.len() >= 12 {
+ let m = u16::from_be_bytes([blob[8], blob[9]]);
+ let vendor = [
+ b'@' + ((m >> 10) & 0x1f) as u8,
+ b'@' + ((m >> 5) & 0x1f) as u8,
+ b'@' + (m & 0x1f) as u8,
+ ];
+ vino_debug!(
+ "vino: socket {socket} EDID {n} B, vendor {}{}{} product {:#06x}\n",
+ vendor[0] as char,
+ vendor[1] as char,
+ vendor[2] as char,
+ u16::from_le_bytes([blob[10], blob[11]])
+ );
+ }
+ self.set_edid(connector as usize, blob);
+ Ok(true)
+ }
+ None => {
+ // Deliberately not published here, even under `edid_override`. A connector that
+ // reports connected with no modes is immediately mode-set by fbdev emulation at
+ // its own 640x480 default, and driving that at the dock resets it -- measured, in
+ // a 25 s re-enumeration loop. The connector stays disconnected until userspace has
+ // supplied the description AND forced the connector on; see `edid_from_userspace`.
+ if self.edid_from_userspace(connector as usize) {
+ pr_warn!(
+ "vino: socket {socket} has no EDID from the dock and is waiting for one from \
+ userspace (edid_override); it stays disconnected until then\n"
+ );
+ return Ok(false);
+ }
+ vino_debug!(
+ "vino: socket {socket} re-engaged but no EDID came back -- no monitor, or it is \
+ not ready yet\n"
+ );
+ Ok(false)
+ }
+ }
+ }
+ /// Drain EP84 looking for the `id=0x194` EDID the fetch above asks for, and return it.
+ ///
+ /// The real EDID only ever arrives as that push (never inside `id=0x4c`/`0x78`), and it can
+ /// land a few messages after the fetch, so this reads a bounded run of replies rather than just
+ /// the next one. Bounded twice over -- attempt count and per-read timeout -- because it runs on
+ /// the keepalive, which must not stall.
+ fn drain_for_edid(&self, dev: &BoundInterface<'_>) -> Option<KVec<u8>> {
+ self.check_cp_session().ok()?;
+ let mut guard = self.cp_link.lock();
+ let link = (&mut *guard).as_mut()?;
+ let mut reply = KVec::from_elem(0u8, 4096, GFP_KERNEL).ok()?;
+ for _ in 0..24 {
+ let got = match link.ep84_q.as_mut() {
+ Some(q) => match q.recv(dev.io(), &mut reply, Delta::from_millis(8)) {
+ Ok(Some(n)) if n > 16 => n,
+ _ => continue,
+ },
+ None => match dev.ctrl_recv(&mut reply, Delta::from_millis(8), GFP_KERNEL) {
+ Ok(n) if n > 16 => n,
+ _ => continue,
+ },
+ };
+ if let Ok(Some(blob)) =
+ crate::cp::parse_edid_from_reply(&link.ks, &link.riv, &reply[..got])
+ {
+ return Some(blob);
+ }
+ }
+ None
+ }
+ /// Stage 2 (runtime monitor hotplug): probe one physical connector.
+ ///
+ /// Navarro multiplexes two connectors per bulk endpoint, but its EDID selector and stream
+ /// record subfield are still per socket. Never collapse sockets 0/2 or 1/3 here: doing so
+ /// turns two independently connected monitors into one KMS connector.
+ pub(crate) fn probe_connector_present(
+ &self,
+ dev: &BoundInterface<'_>,
+ connector: u8,
+ ) -> Option<bool> {
+ if usize::from(connector) >= self.connector_count() {
+ return Some(false);
+ }
+ self.send_presence_probe(dev, connector, connector)
+ }
+ /// Probe one downstream connector. `connector` selects its own presence-change cell.
+ ///
+ /// Sends the EDID probe (`id=0x15 sub=0x20`, byte22 = connector selector -- the same selector
+ /// that unblocked the whole EDID path) and decodes the dock's sealed `0x45` reply. Returns
+ /// `Some(true/false)` on a decodable reply, `None` if CP is down or nothing decoded. Reuses the
+ /// live session `ks/riv/counter` exactly like `send_cp`, so it stays in CP lockstep.
+ fn send_presence_probe(
+ &self,
+ dev: &BoundInterface<'_>,
+ sel: u8,
+ connector: u8,
+ ) -> Option<bool> {
+ let socket = connector + 1;
+ self.check_cp_session().ok()?;
+ let mut guard = self.cp_link.lock();
+ let link = (&mut *guard).as_mut()?;
+ let request_counter = link.counter;
+ let msg = crate::cp::get_edid_req_sub(request_counter, 0x0020, sel).ok()?;
+ let frame =
+ crate::cp::seal_interactive(&link.ks, &link.riv, 0x15, link.wire_seq, &msg).ok()?;
+ if dev.ctrl_send(&frame, crate::timeout(), GFP_KERNEL).is_err() {
+ let silent_ms = self.cp_silent_for_ms();
+ if silent_ms >= self.cp_silence_limit_ms() {
+ self.abandon_cp_session(silent_ms);
+ *guard = None;
+ }
+ return None;
+ }
+ link.wire_seq = link.wire_seq.wrapping_add(((msg.len() + 15) / 16) as u32);
+ link.counter = link.counter.wrapping_add(1);
+ // Take the reply that answers this probe, not simply the next frame on EP84: the connectors
+ // are probed back to back, so a late reply or an unprompted push would otherwise be
+ // attributed to the wrong connector. The inner counter echoes the request. A round that
+ // never sees its own echo returns `None`, which the caller treats as "this poll learned
+ // nothing" rather than as an unplug.
+ let mut reply = KVec::from_elem(0u8, 4096, GFP_KERNEL).ok()?;
+ let deadline = Instant::<Monotonic>::now() + Delta::from_millis(64);
+ let got = loop {
+ let n = match link.ep84_q.as_mut() {
+ Some(q) => match q.recv(dev.io(), &mut reply, crate::cp_reply_timeout()) {
+ Ok(Some(n)) => n,
+ Ok(None) => 0,
+ Err(_) => return None,
+ },
+ None => dev
+ .ctrl_recv(&mut reply, crate::cp_reply_timeout(), GFP_KERNEL)
+ .unwrap_or(0),
+ };
+ if n > 16 {
+ match crate::cp::decode_in_lenient(&link.ks, &link.riv, &reply[..n]) {
+ Some((_, _, echoed)) if echoed == request_counter => break n,
+ // Undecodable frames are the dock's asynchronous pushes; keep draining.
+ _ => {}
+ }
+ }
+ if (Instant::<Monotonic>::now() - deadline).as_millis() >= 0 {
+ return None;
+ }
+ };
+ // Decode the downstream status at inner bytes 22..26 as well as the handler ID.
+ let (id, status, ready) =
+ crate::cp::probe_reply_status(&link.ks, &link.riv, &reply[..got])?;
+ self.note_cp_reply();
+ // Presence is bit 0x10 of inner byte 23, which lands in bits 8..15 of the status word:
+ // `05 11 27 00` for an occupied connector, `05 01 <20|21|60|61> 00` for an empty one.
+ // Which handler answered says nothing about it -- both docks reply `id=0x44` either way.
+ let present = status & 0x0000_1000 != 0;
+ // One line per *changed* answer per connector, so a steady link is silent and an unplug is
+ // unmissable. Both fields are packed into the same cell: the id alone cannot distinguish a
+ // dock that keeps saying `0x44` from one whose downstream state has moved underneath it.
+ let cell = ((id as u32) << 16) | (status & 0xffff);
+ let prev = self.presence_reply[connector as usize].swap(cell, Ordering::Relaxed);
+ if prev != cell {
+ // The dock moves the *other* connector's status word too when a sink appears or
+ // disappears, and it does so sooner than it pushes anything. `prev == 0` is this
+ // connector's first ever reply, which is bring-up, not an event.
+ if prev != 0 {
+ self.downstream_event.store(true, Ordering::Release);
+ }
+ // The decoded answer itself, not just the verdict derived from it. Without this a
+ // presence flap can only be read as "monitor disconnected", which says nothing about
+ // whether the dock changed its mind or vino changed the question. It is one line per
+ // *changed* reply per connector, so a steady link prints nothing at all.
+ // Tagged with this dock's video endpoints, which name the family uniquely: 08/0b is
+ // a DL-6xxx, 08/0a a DL-7400, 02/02 a DL-3x00. A connector count does not -- two of
+ // the three have two connectors, so once both are bound the tag distinguishes
+ // nothing and the same reading reads as either dock.
+ vino_debug!(
+ "vino: [video {:02x}/{:02x}] socket {socket} presence reply id={id:#06x} \
+ status={status:#010x} -> present={present} ready={ready} \
+ (was id={:#06x} status={:#06x})\n",
+ dev.endpoints.video[0].address(),
+ dev.endpoints.video[1].address(),
+ prev >> 16,
+ prev & 0xffff
+ );
+ }
+ Some(present)
+ }
+ /// Whether vino itself took `connector`'s sink down, so the presence watcher can tell its own
+ /// blank apart from a real unplug. See [`VinoDrmData::self_blanked`].
+ pub(crate) fn is_self_blanked(&self, connector: usize) -> bool {
+ self.self_blanked.load(Ordering::Acquire) & (1u32 << connector) != 0
+ }
+ pub(crate) fn set_self_blanked(&self, connector: usize, on: bool) {
+ if on {
+ self.self_blanked
+ .fetch_or(1u32 << connector, Ordering::Release);
+ } else {
+ self.self_blanked
+ .fetch_and(!(1u32 << connector), Ordering::Release);
+ }
+ }
+ /// How long this connector should hold the post-mode-set training cadence.
+ ///
+ /// A cold activation needs it: the dock will not program its downstream pixel clock without a
+ /// sustained stream. A repair does not -- the link is already trained and the sink was dropped
+ /// underneath us -- and running it there costs the dock three seconds of full keyframes at
+ /// `FRAME_PERIOD_MS`. Consumes the repair flag, so the window returns for the next real
+ /// bring-up.
+ pub(crate) fn sustain_window(&self, connector: usize) -> Option<Instant<Monotonic>> {
+ let bit = 1u32 << connector;
+ if self.repair_connectors.fetch_and(!bit, Ordering::AcqRel) & bit != 0 {
+ return None;
+ }
+ // A dock whose video shares the control pipe cannot be given this window. It exists to
+ // train a downstream link by presenting keyframes at frame cadence, which on a pipe of its
+ // own is bandwidth well spent; here it is bandwidth taken directly from the control plane,
+ // and the dock stops answering EP84 entirely rather than merely dropping frames.
+ if self.video_on_ctrl_pipe() {
+ return None;
+ }
+ Some(Instant::<Monotonic>::now() + Delta::from_millis(SUSTAIN_MS))
+ }
+ /// Re-drive every lit connector after the dock dropped a downstream sink underneath us.
+ ///
+ /// The presence flap is the dock really taking a sink down, and the only thing that used to
+ /// repair it was letting the DRM connector disappear so the compositor would re-enable the
+ /// output. That cure was worse than the disease: it re-lays-out userspace, and the mode set it
+ /// produces names one connector while its sibling is lit, which re-enumerates the dock. So vino
+ /// repairs the connector itself and leaves the connector alone.
+ ///
+ /// Every lit connector is re-queued, not just the one that flapped, and they go into one batch:
+ /// a mode set this dock accepts is one that names every connector at once
+ /// (`activate_dual_wake`). Zeroing the mode generation is what makes that path take them.
+ ///
+ /// Returns the number of connectors queued.
+ #[expect(
+ dead_code,
+ reason = "kept for the flap-repair experiment; see its doc comment"
+ )]
+ pub(super) fn repair_flapped_connector(&self, dev: &VinoDrmDevice, flapped: usize) -> u32 {
+ let mut queued = 0u32;
+ let mut cmds: [Option<crate::cp::Timing>; MAX_CONNECTORS] = [None; MAX_CONNECTORS];
+ for connector in 0..MAX_CONNECTORS {
+ let active = self.modeset_active[connector].load(Ordering::Acquire);
+ if active == 0 || self.modeset_requested[connector].load(Ordering::Acquire) != active {
+ continue;
+ }
+ let Some(timing) = self.last_timing.lock()[connector] else {
+ continue;
+ };
+ if timing_key(&timing) != active {
+ continue;
+ }
+ cmds[connector] = Some(timing);
+ }
+ if cmds.iter().flatten().count() == 0 {
+ return 0;
+ }
+ for connector in 0..MAX_CONNECTORS {
+ let Some(timing) = cmds[connector] else {
+ continue;
+ };
+ // The dock's copy of this connector is gone, so nothing it holds can be diffed against.
+ self.repair_connectors
+ .fetch_or(1u32 << connector, Ordering::Release);
+ self.modeset_active[connector].store(0, Ordering::Release);
+ self.owe_keyframe(connector);
+ self.strip_hashes.lock()[connector] = None;
+ self.dirty_ttl.lock()[connector] = None;
+ self.queue_cmd(
+ dev,
+ KmsCmd::ModeSet {
+ connector: connector as u8,
+ timing,
+ },
+ );
+ queued += 1;
+ }
+ pr_warn!("vino: connector {flapped} sink flap -- re-driving {queued} lit connector(s) together\n");
+ queued
+ }
+ /// Whether connector `connector`'s presence bit is currently set (for the keepalive to seed its
+ /// baseline before watching for runtime connect/remove transitions). Whether a monitor has
+ /// described itself on this socket.
+ ///
+ /// The dock recovers an EDID for a socket with something plugged into it and nothing at all for
+ /// an empty one, which on a family that cannot report downstream presence is the only presence
+ /// signal there is.
+ pub(crate) fn connector_has_edid(&self, connector: usize) -> bool {
+ self.cached_edids
+ .lock()
+ .get(connector)
+ .is_some_and(Option::is_some)
+ }
+ pub(crate) fn connector_present(&self, connector: usize) -> bool {
+ connector < MAX_CONNECTORS
+ && self
+ .connectors_present
+ .load(core::sync::atomic::Ordering::Acquire)
+ & (1u32 << connector)
+ != 0
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/scanout.rs b/drivers/gpu/drm/vino/drm_sink/scanout.rs
new file mode 100644
index 000000000000..2edd29cffaf6
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/scanout.rs
@@ -0,0 +1,1958 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Turning a committed framebuffer into bytes on a video endpoint.
+//!
+//! The compositor's atomic callback does no work beyond snapshotting: everything below runs on the
+//! deferred scanout worker. In order, a frame goes through damage selection against the previous
+//! frame's per-strip hashes, encoding (fanned across CPUs as [`EncodeChunk`]s), record framing, and
+//! submission through the connector's persistent URB queue.
+
+use super::mode_objects::rot_src;
+use super::*;
+
+/// Consecutive video stalls cleared before a connector is parked for a fresh mode-set.
+const VIDEO_STALL_LIMIT: u64 = 4;
+
+/// Compress and submit one coalesced primary-plane flip on the deferred worker. Keeping all slow
+/// work here makes the DRM atomic callback bounded to state inspection plus an `ARef` increment.
+pub(super) fn run_pending_scanout(
+ dev: &BoundInterface<'_>,
+ data: &VinoDrmData,
+ frame: PendingScanout,
+) {
+ use core::sync::atomic::Ordering::Relaxed;
+
+ let connector_index = frame.connector as usize;
+ if data.modeset_requested[connector_index].load(Ordering::Acquire) == 0 {
+ scanout_gate(
+ frame.connector,
+ "worker: connector has no mode-set requested",
+ );
+ return;
+ }
+ let requested_geometry_matches = data.last_timing.lock()[connector_index]
+ .is_some_and(|t| t.hactive as usize == frame.w && t.vactive as usize == frame.h);
+ if !requested_geometry_matches {
+ // stale framebuffer from a different-size mode generation
+ scanout_gate(
+ frame.connector,
+ "worker: framebuffer size differs from the cached mode",
+ );
+ return;
+ }
+ // Was this the mode-set's owed keyframe? Read before sending, since a successful send clears
+ // the bit.
+ let was_keyframe =
+ data.keyframe_pending.load(Ordering::Acquire) & (1u32 << frame.connector) != 0;
+ let settle_copy = was_keyframe.then(|| frame.clone());
+ let slot = frame.shadow_idx;
+ let generation = frame.shadow_generation;
+ let (source_w, source_h) = src_dims(frame.rotation, frame.w, frame.h);
+ let shadow = {
+ let mut pool = data.shadow[connector_index].lock();
+ // Split the validation so the counters say *which* invariant failed.
+ let in_range = slot < SHADOW_SLOTS;
+ let not_inflight = pool.inflight.is_none();
+ let gen_ok = in_range && pool.slots[slot].generation == generation;
+ let dims_ok = in_range
+ && pool.slots[slot]
+ .surface
+ .as_ref()
+ .is_some_and(|surface| surface.w == source_w && surface.h == source_h);
+ if !not_inflight {
+ scanout_gate(frame.connector, "slot busy: another encode inflight");
+ } else if !gen_ok {
+ scanout_gate(frame.connector, "slot generation moved under us");
+ } else if !dims_ok {
+ scanout_gate(frame.connector, "slot surface missing or wrong size");
+ }
+ let valid = in_range && not_inflight && gen_ok && dims_ok;
+ if !valid {
+ None
+ } else {
+ pool.inflight = Some(slot);
+ pool.slots[slot].surface.take()
+ }
+ };
+ let Some(shadow) = shadow else {
+ scanout_gate(
+ frame.connector,
+ "worker: committed surface is no longer available",
+ );
+ return;
+ };
+
+ // `pixels` and `hashes` are lent to the encoder and moved back below; the band is scratch that
+ // the encoder has no use for, so it just waits here to be reunited with them.
+ let ShadowSurface {
+ w: source_w,
+ h: source_h,
+ pixels,
+ hashes,
+ band,
+ } = shadow;
+ let color = data.color_snapshot(connector_index);
+ let direct = direct_pixel_map(frame.rotation, &color, source_w, source_h, frame.w, frame.h);
+ let src = match Arc::new(
+ PixelSource {
+ pixels,
+ pitch: source_w * 4,
+ w: source_w,
+ h: source_h,
+ output_w: frame.w,
+ output_h: frame.h,
+ rotation: frame.rotation,
+ color,
+ direct,
+ hashes,
+ depth: data.geometry_for_connector(frame.connector).depth(),
+ source_depth: data.connector_buffer_depth(frame.connector),
+ },
+ GFP_KERNEL,
+ ) {
+ Ok(src) => src,
+ Err(_) => {
+ let mut pool = data.shadow[connector_index].lock();
+ if pool.inflight == Some(slot) {
+ pool.inflight = None;
+ }
+ scanout_gate(frame.connector, "worker: pixel source allocation failed");
+ return;
+ }
+ };
+ let result = encode_and_send(
+ dev,
+ data,
+ frame.connector,
+ &src,
+ frame.rotation,
+ &frame.clips[..frame.nclips],
+ frame.w,
+ frame.h,
+ );
+ data.last_frame.lock()[connector_index] = Some(Instant::<Monotonic>::now());
+ let returned = Arc::into_unique_or_drop(src).map(|src| {
+ let mut src = core::pin::Pin::into_inner(src);
+ ShadowSurface {
+ w: source_w,
+ h: source_h,
+ pixels: core::mem::replace(&mut src.pixels, KVVec::new()),
+ hashes: core::mem::replace(&mut src.hashes, KVVec::new()),
+ band,
+ }
+ });
+ {
+ let mut pool = data.shadow[connector_index].lock();
+ if pool.inflight == Some(slot) {
+ if pool.slots[slot].generation == generation && pool.slots[slot].surface.is_none() {
+ pool.slots[slot].surface = returned;
+ }
+ pool.inflight = None;
+ }
+ }
+ match result {
+ Ok(()) => {
+ let n = data.scanout_fails[connector_index].swap(0, Relaxed);
+ data.scanout_skip[connector_index].store(0, Relaxed);
+ if n > 0 {
+ pr_info!("vino: connector {connector_index} scanout recovered after {n} failed frame(s)\n");
+ }
+ // Arm the one-shot settle repaint. A compositor that goes idle right after enabling an
+ // output can otherwise remain on the initial keyframe indefinitely.
+ if let Some(mut copy) = settle_copy {
+ copy.clips[0] = (0, 0, copy.w, copy.h);
+ copy.nclips = 1;
+ // During the post-mode-set training window, repaint at frame cadence so the dock
+ // receives the sustained stream needed to program the downstream pixel clock.
+ // Outside that window, use the bounded settle repaint.
+ let sustaining = data.sustain_until.lock()[connector_index]
+ .is_some_and(|until| (until - Instant::<Monotonic>::now()).as_millis() > 0);
+ // Training repaints at the fast cadence and is exempt from the budget; everything
+ // else charges one settle repaint against this connector's keyframe obligation and
+ // stops when it runs out. See `SETTLE_REPAINTS` for the unbounded keyframe loop
+ // that made a static desktop stream ~2.7 MB/s per connector. A dock that tears the
+ // link down over a silent video endpoint has to be re-fed whether or not anything
+ // changed, so its repaint is periodic and unbudgeted.
+ let keepalive = data.video_keepalive();
+ let unbudgeted = sustaining || keepalive;
+ let charged = unbudgeted
+ || data.settle_budget[connector_index]
+ .fetch_update(Relaxed, Relaxed, |b| b.checked_sub(1))
+ .is_ok();
+ if charged {
+ let delay = if sustaining {
+ data.frame_period_ms()
+ } else if keepalive {
+ NAVARRO_KEEPALIVE_MS
+ } else {
+ SETTLE_REPAINT_MS
+ };
+ // Only a repaint that exists to put bytes on the wire has to be a keyframe:
+ // training programs the downstream clock with them, and a keepalive dock tears
+ // its link down over a silent endpoint, so both need a frame whether or not
+ // anything changed. The ordinary settle repaint is there to replace whatever
+ // the compositor happened to have mapped when the mode set went out, and a
+ // keyframe has since reached every dock buffer -- so what it owes is the
+ // difference, which on a desktop that did not change is nothing at all.
+ // Sending a second full surface instead costs `dock_buffers` presentations of
+ // it, and on a dock that shares its control pipe that is the transfer the dock
+ // stops accepting.
+ let as_keyframe = sustaining || keepalive;
+ data.settle_repaint.lock()[connector_index] = Some((
+ Instant::<Monotonic>::now() + Delta::from_millis(delay),
+ copy,
+ as_keyframe,
+ ));
+ }
+ } else {
+ // Repaint the same framebuffer while strips still have retransmit debt. This is a
+ // delta, not a keyframe, and terminates after at most the profile's
+ // `damage_frames` accepted submissions because each pass decrements the ledger.
+ let owes = data.dirty_ttl.lock()[connector_index]
+ .as_ref()
+ .is_some_and(|debt| debt.iter().any(|&d| d > 0));
+ if owes {
+ data.settle_repaint.lock()[connector_index] = Some((
+ Instant::<Monotonic>::now() + Delta::from_millis(data.frame_period_ms()),
+ frame.clone(),
+ false,
+ ));
+ }
+ }
+ }
+ Err(e) => {
+ // Log at exponentially sparser points and back off future worker attempts. An error is
+ // transport state, not a reason to stall the compositor's pageflip path.
+ let n = data.scanout_fails[connector_index].fetch_add(1, Relaxed) + 1;
+ if n == 1 || n.is_power_of_two() {
+ pr_err!("vino: connector {connector_index} scanout frame failed ({e:?}) [x{n}] -- throttling\n");
+ }
+ data.scanout_skip[connector_index].store(core::cmp::min(n, 120), Relaxed);
+ // The failed queue was synchronously retired at the exact `q.send()` error site while
+ // that physical pipe was still owned. Give up after a few consecutive stalls and wait
+ // for the next mode set rather than repeatedly driving a pipe the dock is refusing.
+ if e == EPIPE || e == EPROTO {
+ if n == VIDEO_STALL_LIMIT + 1 {
+ pr_err!(
+ "vino: connector {connector_index} stalled {n} times; parking it until the next mode set\n"
+ );
+ data.modeset_active[connector_index].store(0, Ordering::Release);
+ data.programmed_timing.lock()[connector_index] = None;
+ }
+ }
+ }
+ }
+}
+
+/// Copy a whole cursor framebuffer for the dock.
+///
+/// The dock takes DRM `ARGB8888` unchanged; [`crate::cp::cursor_image`] owns the wire placement.
+/// The complete bitmap is sent every time rather than the helper's clipped rectangle: the dock is
+/// configured for a fixed cursor size (`mode_config.cursor_width/height`) and clips at the panel
+/// edge itself.
+pub(super) fn read_cursor_bgra(
+ fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
+ w: usize,
+ h: usize,
+) -> Result<KVec<u8>> {
+ let vmap = fb.vmap::<VinoObject>()?;
+ let view = vmap.view();
+ let pitch = vmap.pitch();
+ let row = w.checked_mul(4).ok_or(EINVAL)?;
+ let len = row.checked_mul(h).ok_or(EINVAL)?;
+ let mut out = KVec::new();
+ out.resize(len, 0, GFP_KERNEL)?;
+ for dy in 0..h {
+ let src = dy.checked_mul(pitch).ok_or(EINVAL)?;
+ view.try_copy_to_slice(src, &mut out[dy * row..(dy + 1) * row])?;
+ }
+ Ok(out)
+}
+
+/// Source (framebuffer) dimensions for an output of `ow`x`oh` pixels under plane `rotation`.
+/// The 90/270 rotations swap width and height between the framebuffer and the displayed output;
+/// the others preserve them.
+pub(super) fn src_dims(rotation: plane::Rotation, ow: usize, oh: usize) -> (usize, usize) {
+ if matches!(
+ rotation.angle(),
+ plane::Rotation::ROTATE_90 | plane::Rotation::ROTATE_270
+ ) {
+ (oh, ow)
+ } else {
+ (ow, oh)
+ }
+}
+
+/// Copy a committed framebuffer into this connector's [`ShadowSurface`], reusing the existing
+/// allocation whenever the geometry is unchanged.
+///
+/// Runs in the atomic commit path, so everything else (damage selection, rotation, gamma, the
+/// codec) stays in the worker and reads this private surface instead of the compositor's live
+/// buffer.
+///
+/// The traversal is band-major: for each row of strips, the source's rows are pulled into
+/// [`ShadowSurface::band`] a full row per read, and only then are that band's strips hashed and --
+/// where the hash moved -- copied on into `pixels`. The obvious strip-major order costs far more
+/// for the same result, because a strip is 64 px wide but the source row is `pitch` bytes apart: it
+/// reads the source in `STRIP_W * 4`-byte fragments (57,600 of them per 1440p frame, versus 1,440
+/// full-row reads here), it walks those fragments against the row stride rather than sequentially,
+/// and it has to read a changed strip out of the source a second time to copy it, because the first
+/// read went to a fragment-sized scratch that could not be kept.
+///
+/// Strips whose hash is unchanged are still not written, so an idle desktop moves no more memory
+/// than the strip-major order would, and a busy one reads the source once.
+#[inline(never)]
+pub(super) fn snapshot_to_shadow(
+ geometry: crate::video::haar::Geometry,
+ slot: &mut Option<ShadowSurface>,
+ source: &kms::framebuffer::FramebufferVMapOwned<VinoObject>,
+ w: usize,
+ h: usize,
+) -> Result {
+ if w == 0 || h == 0 {
+ return Err(EINVAL);
+ }
+ let row = w.checked_mul(4).ok_or(EINVAL)?;
+ let need = row.checked_mul(h).ok_or(EINVAL)?;
+ // GEM dumb buffers pad the pitch, so the source stride is not necessarily `w * 4`.
+ let pitch = source.pitch();
+ let view = source.view();
+
+ let (sw, sh) = (geometry.strip_w(), geometry.strip_h());
+ let padded_width = (w + sw - 1) & !(sw - 1);
+ let padded_height = (h + sh - 1) & !(sh - 1);
+ let tiles_x = padded_width / sw;
+ let tiles_y = padded_height / sh;
+
+ // A freshly allocated surface holds zeros, not the previous frame, so nothing in it may be
+ // treated as already up to date however its stored hashes compare.
+ let band_len = sh.checked_mul(row).ok_or(EINVAL)?;
+ let mut fresh = false;
+ if !matches!(slot, Some(s) if s.w == w && s.h == h) {
+ let mut pixels: KVVec<u8> = KVVec::new();
+ pixels.resize(need, 0, GFP_KERNEL)?;
+ let mut hashes: KVVec<u64> = KVVec::new();
+ hashes.resize(tiles_x * tiles_y, 0, GFP_KERNEL)?;
+ let mut band: KVVec<u8> = KVVec::new();
+ band.resize(band_len, 0, GFP_KERNEL)?;
+ *slot = Some(ShadowSurface {
+ w,
+ h,
+ pixels,
+ hashes,
+ band,
+ });
+ fresh = true;
+ }
+ let shadow = slot.as_mut().ok_or(kernel::error::code::ENOMEM)?;
+ if shadow.hashes.len() != tiles_x * tiles_y {
+ shadow.hashes.resize(tiles_x * tiles_y, 0, GFP_KERNEL)?;
+ }
+ if shadow.band.len() != band_len {
+ shadow.band.resize(band_len, 0, GFP_KERNEL)?;
+ }
+
+ // Borrow the three buffers as disjoint fields: the band is read while `pixels` is written.
+ let ShadowSurface {
+ pixels,
+ hashes,
+ band,
+ ..
+ } = shadow;
+ for ty in 0..tiles_y {
+ let sy = ty * sh;
+ let y_end = (sy + sh).min(h);
+ // The final band is short whenever the height is not a whole number of strips.
+ let rows = y_end - sy;
+ // Pull the band out of the source through the checked I/O view, one full row per read.
+ for dy in 0..rows {
+ let dst = &mut band[dy * row..dy * row + row];
+ view.try_copy_to_slice((sy + dy) * pitch, dst)?;
+ }
+ for tx in 0..tiles_x {
+ let sx = tx * sw;
+ let x_end = (sx + sw).min(w);
+ let seed = 0x9e37_79b1_85eb_ca87u64
+ ^ (sx as u64).rotate_left(17)
+ ^ (sy as u64).rotate_left(43);
+ let mut hasher = xxhash::Xxh64::new(seed);
+ let bytes = (x_end - sx) * 4;
+ // Hash exactly the bytes, in the order, that the strip-major traversal did, so a
+ // surface's stored hashes stay comparable across this change.
+ if sx < x_end {
+ for dy in 0..rows {
+ let off = dy * row + sx * 4;
+ hasher.update(&band[off..off + bytes])?;
+ }
+ }
+ let hash = hasher.digest();
+ let idx = ty * tiles_x + tx;
+ if sx < x_end && (fresh || hashes[idx] != hash) {
+ for dy in 0..rows {
+ let src = dy * row + sx * 4;
+ let dst = (sy + dy) * row + sx * 4;
+ pixels[dst..dst + bytes].copy_from_slice(&band[src..src + bytes]);
+ }
+ }
+ hashes[idx] = hash;
+ }
+ }
+ Ok(())
+}
+
+/// Convert changed strip hashes into a compact set of damage rectangles.
+///
+/// The rectangles are built on the macro-tile grid, not the strip grid, because a touched
+/// macro-tile is resent whole either way -- see `Geometry::macro_w`. Describing damage at a
+/// granularity finer than it is transmitted at costs nothing and fragments the list by up to the
+/// sixteen strips a macro-tile holds, which is enough for a handful of scattered updates to
+/// exhaust the rectangle ceiling and take the whole surface with them.
+///
+/// Horizontal runs are joined, then equal runs on adjacent macro-rows are extended vertically. A
+/// frame still fragmented past the ceiling falls back to one full-output rectangle rather than
+/// growing an unbounded allocation or spending more time testing rectangles than encoding strips.
+#[inline(never)]
+pub(crate) fn changed_strip_rects(
+ geometry: crate::video::haar::Geometry,
+ old: &[u64],
+ new: &[u64],
+ padded_width: usize,
+ padded_height: usize,
+) -> Result<KVec<DamageRect>> {
+ const MAX_RECTS: usize = 128;
+ let tiles_x = padded_width >> geometry.strip_w_shift();
+ let tiles_y = padded_height >> geometry.strip_h_shift();
+ if old.len() != tiles_x * tiles_y || new.len() != old.len() {
+ return Err(EINVAL);
+ }
+ let (mw, mh) = (geometry.macro_w(), geometry.macro_h());
+ let per_x = mw / geometry.strip_w();
+ let per_y = mh / geometry.strip_h();
+ let macros_x = tiles_x.div_ceil(per_x);
+ let macros_y = tiles_y.div_ceil(per_y);
+ // The macro-tile grid as a changed/unchanged bitmap. One byte a tile: a 4K surface is 544 of
+ // them, so the map is cheaper than the rectangle list it replaces.
+ let mut touched: KVVec<u8> = KVVec::new();
+ touched.resize(macros_x * macros_y, 0, GFP_KERNEL)?;
+ for ty in 0..tiles_y {
+ for tx in 0..tiles_x {
+ if old[ty * tiles_x + tx] != new[ty * tiles_x + tx] {
+ touched[(ty / per_y) * macros_x + tx / per_x] = 1;
+ }
+ }
+ }
+ let mut rects: KVec<DamageRect> = KVec::new();
+ for my in 0..macros_y {
+ let mut mx = 0usize;
+ while mx < macros_x {
+ if touched[my * macros_x + mx] == 0 {
+ mx += 1;
+ continue;
+ }
+ let run_start = mx;
+ while mx < macros_x && touched[my * macros_x + mx] != 0 {
+ mx += 1;
+ }
+ let x0 = run_start * mw;
+ let x1 = (mx * mw).min(padded_width);
+ let y0 = my * mh;
+ let y1 = (y0 + mh).min(padded_height);
+ let mut merged = false;
+ for prior in rects.iter_mut().rev() {
+ if prior.0 == x0 && prior.2 == x1 && prior.3 == y0 {
+ prior.3 = y1;
+ merged = true;
+ break;
+ }
+ }
+ if !merged {
+ if rects.len() == MAX_RECTS {
+ let mut full: KVec<DamageRect> = KVec::new();
+ full.push((0, 0, padded_width, padded_height), GFP_KERNEL)?;
+ return Ok(full);
+ }
+ rects.push((x0, y0, x1, y1), GFP_KERNEL)?;
+ }
+ }
+ }
+ Ok(rects)
+}
+
+/// Maximum packet size of a SuperSpeed bulk endpoint.
+///
+/// Only its role as a divisor matters here: a transfer that is a whole number of these ends on a
+/// full packet and so terminates nothing. A device running below SuperSpeed uses a smaller value,
+/// which divides this one, so a transfer short by this measure is short by that one too.
+const BULK_MAX_PACKET: usize = 1024;
+
+/// How much of a frame's last transfer is split off behind it so the frame ends short.
+///
+/// Any value that is neither zero nor a multiple of [`BULK_MAX_PACKET`] works; a record stride is
+/// a multiple of sixteen, so this keeps the split on a stride boundary where a record allows one.
+const FRAME_TAIL_BYTES: usize = 16;
+
+/// Hard ceiling on work items per frame, purely to bound per-frame allocation.
+///
+/// Each chunk owns synchronization and a coordinate list, so the count must be
+/// bounded. At `ENCODE_MIN_STRIPS_PER_CHUNK`, a full 1440p frame needs about
+/// 112 chunks.
+const ENCODE_MAX_WORK_ITEMS: usize = 256;
+
+/// Fewest strips per chunk worth dispatching. Below this the allocation, enqueue and completion
+/// cost more than the strips themselves; a small delta stays on the serial path.
+const ENCODE_MIN_STRIPS_PER_CHUNK: usize = 32;
+
+/// Immutable driver-owned pixel source shared by parallel encode workers.
+pub(super) struct PixelSource {
+ pixels: KVVec<u8>,
+ pitch: usize,
+ /// Dimensions of the untransformed framebuffer snapshot.
+ w: usize,
+ h: usize,
+ /// Dimensions and transform of the image presented to the dock.
+ output_w: usize,
+ output_h: usize,
+ rotation: plane::Rotation,
+ color: Option<crate::color::ColorPipeline>,
+ /// True when an output pixel is the source pixel at the same coordinates: identity rotation, no
+ /// gamma table, and output dimensions equal to the snapshot's.
+ ///
+ /// Fullscreen video makes `px` the third-hottest symbol in the kernel (13.8% of the machine
+ /// on a 4K clip), because every one of the ~3.7 M pixels per frame pays a `rot_src` match and
+ /// a gamma branch that are constant for the whole frame. Deciding once per frame lets the
+ /// common case read straight out of the snapshot.
+ direct: bool,
+ /// Strip hashes computed during the snapshot -- see [`ShadowSurface::hashes`]. Carried through
+ /// so the encoder does not re-read the whole surface just to re-derive them.
+ hashes: KVVec<u64>,
+ /// Bits per channel of the snapshot's pixels, and therefore of the frame the codec produces.
+ ///
+ /// Both layouts this handles are four bytes per pixel, so the snapshot copy above is
+ /// depth-agnostic and only the unpack here differs: `XRGB8888` is three bytes in the low 24
+ /// bits, `XRGB2101010` three 10-bit fields in the low 30.
+ depth: crate::video::haar::Depth,
+ /// Bits per channel of the framebuffer itself, which decides how a pixel is decoded.
+ ///
+ /// Equal to `depth` except when userspace asked for a deeper link than the surface it handed
+ /// over, which is the ordinary way a compositor drives a ten-bit link from an eight-bit
+ /// desktop. Samples are widened after decoding so the codec always sees `depth`.
+ source_depth: crate::video::haar::Depth,
+}
+
+/// Whether the encoder can read output pixels straight out of the snapshot. See
+/// [`PixelSource::direct`].
+fn direct_pixel_map(
+ rotation: plane::Rotation,
+ color: &Option<crate::color::ColorPipeline>,
+ w: usize,
+ h: usize,
+ output_w: usize,
+ output_h: usize,
+) -> bool {
+ color.is_none() && rotation == plane::Rotation::ROTATE_0 && output_w == w && output_h == h
+}
+
+/// Widen an eight-bit sample to ten bits.
+///
+/// Replicating the top two bits into the low ones keeps both endpoints exact: black stays zero and
+/// full white stays full white, where a plain shift would land three codes short of it and tint
+/// every highlight.
+#[inline]
+fn widen_8_to_10(v: u16) -> u16 {
+ (v << 2) | (v >> 6)
+}
+
+impl PixelSource {
+ /// Split one packed 32-bit pixel into channels at this source's depth.
+ #[inline]
+ fn unpack(&self, p: u32) -> (u16, u16, u16) {
+ let (r, g, b) = self.unpack_source(p);
+ self.widen(r, g, b)
+ }
+
+ /// Widen a decoded sample from the framebuffer's depth to the link's.
+ #[inline]
+ fn widen(&self, r: u16, g: u16, b: u16) -> (u16, u16, u16) {
+ match (self.source_depth, self.depth) {
+ (crate::video::haar::Depth::Eight, crate::video::haar::Depth::Ten) => {
+ (widen_8_to_10(r), widen_8_to_10(g), widen_8_to_10(b))
+ }
+ _ => (r, g, b),
+ }
+ }
+
+ /// Split one packed 32-bit pixel into channels at the framebuffer's own depth.
+ #[inline]
+ fn unpack_source(&self, p: u32) -> (u16, u16, u16) {
+ match self.source_depth {
+ // Little-endian XRGB8888.
+ crate::video::haar::Depth::Eight => (
+ ((p >> 16) & 0xff) as u16,
+ ((p >> 8) & 0xff) as u16,
+ (p & 0xff) as u16,
+ ),
+ // XRGB2101010: two ignored bits, then R, G, B ten bits each.
+ crate::video::haar::Depth::Ten => (
+ ((p >> 20) & 0x3ff) as u16,
+ ((p >> 10) & 0x3ff) as u16,
+ (p & 0x3ff) as u16,
+ ),
+ }
+ }
+
+ /// Read one gamma-corrected pixel in untransformed framebuffer coordinates.
+ #[inline]
+ fn source_px(&self, sx: usize, sy: usize) -> (u16, u16, u16) {
+ if sx >= self.w || sy >= self.h {
+ return (0, 0, 0);
+ }
+ let off = sy * self.pitch + sx * 4;
+ // Bounds-checked once per pixel instead of the serial path's raw `read_unaligned`. The
+ // check is noise next to the 64-coefficient transform each pixel feeds.
+ let Some(chunk) = self.pixels.get(off..off + 4) else {
+ return (0, 0, 0);
+ };
+ let Ok(bytes) = <[u8; 4]>::try_from(chunk) else {
+ return (0, 0, 0);
+ };
+ let (r, g, b) = self.unpack(u32::from_le_bytes(bytes));
+ match &self.color {
+ // The colour pipeline's tables are 8-bit, so a 10-bit surface is corrected at 8-bit
+ // precision and scaled back. That loses up to two low bits of a corrected pixel -- but ignoring the
+ // correction instead would leave a compositor's night-colour shift silently unapplied
+ // on exactly the outputs most likely to be colour-managed. Revisit with a 10-bit LUT if
+ // banding is ever measured on a corrected HDR connector.
+ Some(pipeline) => match self.depth {
+ crate::video::haar::Depth::Eight => {
+ let (r, g, b) = pipeline.apply(r as u8, g as u8, b as u8);
+ (r as u16, g as u16, b as u16)
+ }
+ crate::video::haar::Depth::Ten => {
+ let (r, g, b) = pipeline.apply((r >> 2) as u8, (g >> 2) as u8, (b >> 2) as u8);
+ (
+ (r as u16) << 2 | (r as u16 >> 6),
+ (g as u16) << 2 | (g as u16 >> 6),
+ (b as u16) << 2 | (b as u16 >> 6),
+ )
+ }
+ },
+ None => (r, g, b),
+ }
+ }
+
+ /// Read one output pixel after applying the plane transform.
+ ///
+ /// Keeping the transform in the immutable shared source gives serial and parallel encoding
+ /// exactly the same sampler. Codec padding is black and never reads beyond the snapshot.
+ #[inline]
+ fn px(&self, dx: usize, dy: usize) -> (u16, u16, u16) {
+ if self.direct {
+ // The codec pads the surface up to whole strips and expects black outside the image, so
+ // the bounds check stays: without it a read past the row wraps into the next one.
+ if dx >= self.w || dy >= self.h {
+ return (0, 0, 0);
+ }
+ let off = dy * self.pitch + dx * 4;
+ let Some(chunk) = self.pixels.get(off..off + 4) else {
+ return (0, 0, 0);
+ };
+ if let crate::video::haar::Depth::Eight = self.source_depth {
+ // Little-endian XRGB8888: byte 0 is blue, 1 green, 2 red.
+ return self.widen(chunk[2] as u16, chunk[1] as u16, chunk[0] as u16);
+ }
+ let Ok(bytes) = <[u8; 4]>::try_from(chunk) else {
+ return (0, 0, 0);
+ };
+ return self.unpack(u32::from_le_bytes(bytes));
+ }
+ if dx >= self.output_w || dy >= self.output_h {
+ return (0, 0, 0);
+ }
+ let (sx, sy) = rot_src(self.rotation, dx, dy, self.w, self.h);
+ self.source_px(sx, sy)
+ }
+}
+
+/// vino's own workqueue for the parallel strip encode.
+///
+/// On the shared `system_unbound` pool the encode's CPU time is anonymous: the kernel
+/// composes worker thread names from the workqueue's, so shared-pool work appears only as
+/// `kworker/uN:M-events_unbound`, indistinguishable from every other user of that pool. On a queue
+/// of our own the same threads appear as `kworker/uN:M-vino_encode`, so `ps`/`top`/`perf`
+/// attribute the codec's cost to vino, and the fan-out does not compete with unrelated work for
+/// the shared pool's concurrency budget.
+///
+/// `WQ_UNBOUND` because strip encoding is pure compute with no CPU affinity worth preserving --
+/// that property is what let the fan-out reach ~7.4x. Allocated once on first use and never
+/// destroyed: it is driver-wide, costs one `workqueue_struct`, and outliving every `EncodeChunk` is
+/// exactly what makes the join safe.
+///
+/// `max_active` is the CPU count. An unbound queue built without it takes `WQ_DFL_ACTIVE`, which is
+/// half of `WQ_MAX_ACTIVE` and so far above any useful degree of parallelism that it does not bound
+/// the fan-out at all: a full frame is a few hundred chunks per connector, and every one of them
+/// becomes a runnable CPU-bound worker at once. On a machine that is already busy that is not
+/// parallelism, it is a thundering herd -- it evicts the caches the codec depends on and stalls
+/// unrelated work, including the compositor thread this driver is waiting on. Fine-grained chunks
+/// are still the right unit (see `encode_across_cpus`); how many run at once is a separate
+/// decision, and this is where it is made.
+///
+/// Falls back to `system_unbound` if the allocation ever fails, so a failure here costs the thread
+/// *name*, not the driver.
+fn encode_queue() -> Option<&'static workqueue::Queue> {
+ static ENCODE_WQ: kernel::sync::SetOnce<workqueue::OwnedQueue> = kernel::sync::SetOnce::new();
+ if let Some(q) = ENCODE_WQ.as_ref() {
+ return Some(q);
+ }
+ // A concurrent racer may win the `SetOnce`; its queue is dropped and the winner's is used.
+ if let Ok(q) = workqueue::Queue::new_unbound()
+ .max_active(kernel::cpu::nr_cpu_ids().max(1))
+ .build(kernel::c_str!("vino_encode"))
+ {
+ let _ = ENCODE_WQ.populate(q);
+ }
+ ENCODE_WQ.as_ref().map(|q| &**q)
+}
+
+/// Encode a batch of strips from `src`, in the order given.
+fn encode_coords(
+ geometry: crate::video::haar::Geometry,
+ src: &PixelSource,
+ coords: &[(usize, usize)],
+) -> Result<KVec<KVec<u8>>> {
+ let mut out = KVec::with_capacity(coords.len(), GFP_KERNEL)?;
+ for &(sx, sy) in coords.iter() {
+ let mut px = |dx, dy| src.px(dx, dy);
+ out.push(
+ crate::video::haar::colour_strip_at(geometry, sx, sy, &mut px)?,
+ GFP_KERNEL,
+ )?;
+ }
+ Ok(out)
+}
+
+/// One contiguous batch of strips, encoded on whichever CPU the unbound workqueue picks.
+///
+/// Chunks share nothing but the read-only [`PixelSource`]: each writes only its own `out`, so no
+/// locking is needed on the hot path and the results reassemble by chunk order.
+#[pin_data]
+struct EncodeChunk {
+ #[pin]
+ work: Work<EncodeChunk>,
+ #[pin]
+ done: Completion,
+ src: Arc<PixelSource>,
+ coords: KVec<(usize, usize)>,
+ /// The dock's strip layout, carried per chunk so two docks of different generations can
+ /// encode concurrently on the same workqueue.
+ geometry: crate::video::haar::Geometry,
+ /// Encoded strip bodies. Written once by the worker, read once by the joiner after `done`;
+ /// the lock is uncontended and taken twice per chunk per frame.
+ #[pin]
+ out: Mutex<KVec<KVec<u8>>>,
+}
+
+impl_has_work! {
+ impl HasWork<Self> for EncodeChunk { self.work }
+}
+
+impl EncodeChunk {
+ fn new(
+ geometry: crate::video::haar::Geometry,
+ src: Arc<PixelSource>,
+ coords: KVec<(usize, usize)>,
+ ) -> Result<Arc<Self>> {
+ Arc::pin_init(
+ pin_init!(EncodeChunk {
+ work <- new_work!("vino::EncodeChunk::work"),
+ done <- Completion::new(),
+ src,
+ coords,
+ geometry,
+ out <- new_mutex!(KVec::new(), "vino::EncodeChunk::out"),
+ }),
+ GFP_KERNEL,
+ )
+ }
+}
+
+impl WorkItem for EncodeChunk {
+ type Pointer = Arc<EncodeChunk>;
+
+ fn run(this: Arc<EncodeChunk>) {
+ if let Ok(strips) = encode_coords(this.geometry, &this.src, &this.coords) {
+ *this.out.lock() = strips;
+ }
+ // Complete unconditionally. On failure `out` stays short and the joiner detects that by
+ // length -- but it must never be left blocked on a completion that cannot fire.
+ this.done.complete_all();
+ }
+}
+
+/// Encode `coords` across CPUs and return the strip bodies in the same order.
+///
+/// Order is not a nicety: [`crate::video::haar::frame_records`] groups strips into one wire record
+/// per single-Y band and needs them x-ordered within a band, so the chunks are contiguous slices
+/// of the raster-ordered coordinate list and are reassembled strictly in chunk order.
+///
+/// Returns `Ok(None)` when the frame is too small to be worth splitting, so the caller falls
+/// through to the serial encoder rather than paying dispatch cost for a handful of strips.
+fn parallel_strip_encode(
+ geometry: crate::video::haar::Geometry,
+ src: &Arc<PixelSource>,
+ coords: &[(usize, usize)],
+) -> Result<Option<KVec<KVec<u8>>>> {
+ // Size chunks from the amount of work, not from the CPU count. Handing the queue more, smaller
+ // items than there are CPUs costs a little dispatch overhead but improves load balancing: with
+ // one chunk per CPU a single slow chunk holds up the whole join, whereas fine-grained items let
+ // idle workers pick up the remainder.
+ //
+ // This is only safe because `encode_queue` caps `max_active` at the CPU count. The chunk count
+ // is the unit of work; that cap is what keeps the count from also becoming the number of
+ // threads competing for the machine.
+ let nchunks = (coords.len() / ENCODE_MIN_STRIPS_PER_CHUNK).min(ENCODE_MAX_WORK_ITEMS);
+ if nchunks < 2 {
+ return Ok(None);
+ }
+ let per = coords.len().div_ceil(nchunks);
+
+ let mut chunks: KVec<Arc<EncodeChunk>> = KVec::with_capacity(nchunks, GFP_KERNEL)?;
+ let mut queued: KVec<bool> = KVec::with_capacity(nchunks, GFP_KERNEL)?;
+ let mut start = 0usize;
+ while start < coords.len() {
+ let end = (start + per).min(coords.len());
+ let mut mine: KVec<(usize, usize)> = KVec::with_capacity(end - start, GFP_KERNEL)?;
+ for &c in &coords[start..end] {
+ mine.push(c, GFP_KERNEL)?;
+ }
+ let chunk = EncodeChunk::new(geometry, src.clone(), mine)?;
+ // `enqueue` gives the item back if it is already pending -- impossible for one allocated
+ // a line ago, but if it ever happened, waiting on its completion would hang the scanout
+ // worker forever. Record it and encode that chunk inline instead.
+ let ok = encode_queue()
+ .map_or_else(
+ || workqueue::system_unbound().enqueue(chunk.clone()),
+ |q| q.enqueue(chunk.clone()),
+ )
+ .is_ok();
+ queued.push(ok, GFP_KERNEL)?;
+ chunks.push(chunk, GFP_KERNEL)?;
+ start = end;
+ }
+
+ // The scanout worker runs on the per-device scanout queue, so blocking here cannot deadlock
+ // against the separate unbound pool the chunks run on.
+ let mut strips: KVec<KVec<u8>> = KVec::with_capacity(coords.len(), GFP_KERNEL)?;
+ for (i, chunk) in chunks.iter().enumerate() {
+ let mine = if queued[i] {
+ chunk.done.wait_for_completion();
+ core::mem::take(&mut *chunk.out.lock())
+ } else {
+ encode_coords(chunk.geometry, &chunk.src, &chunk.coords)?
+ };
+ if mine.len() != chunk.coords.len() {
+ // A chunk failed to allocate. Sending a frame with strips missing would paint a
+ // partial image the dock would keep, so fail the whole encode and let the caller's
+ // retry/backoff handle it.
+ return Err(ENOMEM);
+ }
+ for s in mine {
+ strips.push(s, GFP_KERNEL)?;
+ }
+ }
+ Ok(Some(strips))
+}
+
+/// Verify that workqueue fan-out produces the same strip bytes as the serial transformed sampler.
+///
+/// This is kept behind the Vino KUnit option so the production driver carries no test allocation
+/// or dispatch path. The deliberately unaligned output also verifies that both paths produce
+/// identical black codec padding.
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+pub(crate) fn parallel_rotation_matches_serial(rotation: plane::Rotation) -> Result {
+ let (output_w, output_h) = (500usize, 123usize);
+ let (w, h) = src_dims(rotation, output_w, output_h);
+ let len = w
+ .checked_mul(h)
+ .and_then(|n| n.checked_mul(4))
+ .ok_or(EINVAL)?;
+ let mut pixels: KVVec<u8> = KVVec::new();
+ pixels.resize(len, 0, GFP_KERNEL)?;
+ for sy in 0..h {
+ for sx in 0..w {
+ let off = (sy * w + sx) * 4;
+ pixels[off] = ((sx * 3 + sy * 5) & 0xff) as u8;
+ pixels[off + 1] = ((sx * 7 + sy * 11) & 0xff) as u8;
+ pixels[off + 2] = ((sx * 13 + sy * 17) & 0xff) as u8;
+ pixels[off + 3] = 0xff;
+ }
+ }
+ let src = Arc::new(
+ PixelSource {
+ pixels,
+ pitch: w * 4,
+ w,
+ h,
+ output_w,
+ output_h,
+ rotation,
+ color: None,
+ direct: direct_pixel_map(rotation, &None, w, h, output_w, output_h),
+ hashes: KVVec::new(),
+ depth: crate::video::haar::Depth::Eight,
+ source_depth: crate::video::haar::Depth::Eight,
+ },
+ GFP_KERNEL,
+ )?;
+ let geometry = crate::video::haar::RIDGE_GEOMETRY;
+ let padded_width = output_w.next_multiple_of(geometry.strip_w());
+ let padded_height = output_h.next_multiple_of(geometry.strip_h());
+ let coords = crate::video::haar::all_strip_coords(geometry, padded_width, padded_height)?;
+
+ let mut serial: KVec<KVec<u8>> = KVec::with_capacity(coords.len(), GFP_KERNEL)?;
+ for &(strip_x, strip_y) in coords.iter() {
+ let mut px = |dx, dy| {
+ if dx >= output_w || dy >= output_h {
+ return (0, 0, 0);
+ }
+ let (sx, sy) = rot_src(rotation, dx, dy, w, h);
+ src.source_px(sx, sy)
+ };
+ serial.push(
+ crate::video::haar::colour_strip_at(geometry, strip_x, strip_y, &mut px)?,
+ GFP_KERNEL,
+ )?;
+ }
+
+ let parallel = parallel_strip_encode(geometry, &src, &coords)?.ok_or(EINVAL)?;
+ if serial.len() != parallel.len()
+ || serial
+ .iter()
+ .zip(parallel.iter())
+ .any(|(expected, actual)| expected[..] != actual[..])
+ {
+ return Err(EINVAL);
+ }
+ Ok(())
+}
+
+/// A frame that trips one of these is dropped between the compositor's commit and the wire.
+fn scanout_gate(connector: u8, reason: &str) {
+ vino_debug!("vino: scanout connector={connector} deferred: {reason}\n");
+}
+
+#[inline(never)]
+fn encode_and_send_haar(
+ dev: &BoundInterface<'_>,
+ data: &VinoDrmData,
+ connector: u8,
+ src: &Arc<PixelSource>,
+ rotation: plane::Rotation,
+ _clips: &[(usize, usize, usize, usize)],
+ w: usize,
+ h: usize,
+) -> Result {
+ let geometry = data.geometry_for_connector(connector);
+ let connector_index = connector as usize;
+ // The vendor sends the frames that open a stream without the steady-state record bit and
+ // every frame after them with it, whether they carry a whole surface or one damaged strip.
+ // The training window is that opening.
+ let opening = data.sustain_until.lock()[connector_index]
+ .is_some_and(|until| (until - Instant::<Monotonic>::now()).as_millis() > 0);
+ let geometry = if opening {
+ geometry.opening()
+ } else {
+ geometry
+ };
+ // Gate video on the matching mode-set reaching the dock. Plane updates run before the CRTC
+ // enable queues that mode-set, and the dock rejects video on an unconfigured stream. Deferring
+ // does not advance the codec sequence; the next scanout pass retries the frame.
+ let want = data.modeset_requested[connector_index].load(core::sync::atomic::Ordering::Acquire);
+ if want == 0 {
+ scanout_gate(connector, "no mode-set requested (modeset_requested == 0)");
+ return Ok(());
+ }
+ // A dock that cannot report downstream presence offers every connector it has, because its
+ // activation is one dock-wide transaction and a connector nobody described is not the same dock
+ // state as a connector that does not exist. The empty socket still recovers no EDID, and
+ // painting it spends a full surface per repaint on a sink that is not there -- on this family
+ // over the endpoint its control plane also needs. So configure the connector and leave it
+ // showing the black its carrier put there, rather than streaming to nothing.
+ if !data.reports_presence() && !data.connector_has_edid(connector_index) {
+ scanout_gate(connector, "no monitor has described this socket");
+ return Ok(());
+ }
+ let cached = data.last_timing.lock()[connector_index];
+ if !cached.is_some_and(|t| {
+ timing_key(&t) == want && t.hactive as usize == w && t.vactive as usize == h
+ }) {
+ scanout_gate(
+ connector,
+ "cached timing does not match the requested mode generation",
+ );
+ return Ok(());
+ }
+ if data.modeset_active[connector_index].load(core::sync::atomic::Ordering::Acquire) != want {
+ // A failed command-worker activation leaves the desired generation intact. This worker is
+ // sleepable, so retry the same transaction before submitting its pending framebuffer.
+ let timing = cached.ok_or(EINVAL)?;
+ data.activate_head(dev, connector, &timing, want)?;
+ // A successful inline retry has made this very commit safe to send: continue into the
+ // encoder instead of waiting for another page flip. A completely static connector may not
+ // receive another atomic update after its enabling commit.
+ if data.modeset_active[connector_index].load(core::sync::atomic::Ordering::Acquire) != want
+ {
+ scanout_gate(
+ connector,
+ "mode-set not active and the inline re-send did not land",
+ );
+ return Ok(());
+ }
+ }
+ let seq0 = data.scanout_seq.lock()[connector_index];
+ // Source dimensions (swapped from the output for 90/270 rotation).
+ let (sw, sh) = src_dims(rotation, w, h);
+ if src.w != sw
+ || src.h != sh
+ || src.output_w != w
+ || src.output_h != h
+ || src.rotation != rotation
+ {
+ return Err(EINVAL);
+ }
+ // Full keyframe vs damage delta. A mode-set requires a keyframe; rotation/reflection remains
+ // conservative because the content shadow is deliberately stored in unrotated framebuffer
+ // space. For identity rotation, compare the actual framebuffer instead of trusting optional
+ // FB_DAMAGE_CLIPS: KWin commonly changes framebuffer objects without publishing that blob.
+ let kf_bit = 1u32 << connector_index;
+ let identity = rotation.angle() == plane::Rotation::ROTATE_0
+ && !rotation.contains(plane::Rotation::REFLECT_X | plane::Rotation::REFLECT_Y);
+ let owes_keyframe = data
+ .keyframe_pending
+ .load(core::sync::atomic::Ordering::Acquire)
+ & kf_bit
+ != 0;
+ let mut full = owes_keyframe || !identity;
+ // The codec operates on complete 64x16 strips. Pad non-aligned modes to the next strip
+ // boundary; the mode-set retains the visible dimensions and the sampler supplies black for
+ // pixels outside them.
+ let padded_width = (w + geometry.strip_w() - 1) & !(geometry.strip_w() - 1);
+ let padded_height = (h + geometry.strip_h() - 1) & !(geometry.strip_h() - 1);
+ let mut content_hashes: Option<KVVec<u64>> = None;
+ let mut content_damage: KVec<DamageRect> = KVec::new();
+ // Strips whose pixels moved since the last accepted frame, before the retransmit debt and the
+ // macro-tile rounding widen that into what is actually sent. Reported next to the selected
+ // count so an oversized delta says which of the two widened it.
+ let mut moved_strips = 0usize;
+ if identity {
+ let expected = (padded_width >> geometry.strip_w_shift())
+ * (padded_height >> geometry.strip_h_shift());
+ if src.hashes.len() != expected {
+ return Err(EINVAL);
+ }
+ let mut hashes: KVVec<u64> = KVVec::new();
+ hashes.resize(expected, 0, GFP_KERNEL)?;
+ hashes.copy_from_slice(&src.hashes);
+ if !full {
+ let previous = data.strip_hashes.lock();
+ if let Some(state) = &previous[connector_index] {
+ if state.padded_width == padded_width && state.padded_height == padded_height {
+ // Charge every strip whose content moved with the profile's logical-frame
+ // debt, then select every strip that still owes a transmission -- including
+ // ones that changed on an earlier frame and have not reached every dock
+ // buffer. See `dirty_ttl` and `FrameDelivery`.
+ let mut ttl = data.dirty_ttl.lock();
+ if !ttl[connector_index]
+ .as_ref()
+ .is_some_and(|t| t.len() == hashes.len())
+ {
+ let mut fresh: KVVec<u8> = KVVec::new();
+ fresh.resize(hashes.len(), 0, GFP_KERNEL)?;
+ ttl[connector_index] = Some(fresh);
+ }
+ let debt = ttl[connector_index]
+ .as_mut()
+ .ok_or(kernel::error::code::ENOMEM)?;
+ let damage_frames = data.frame_delivery().damage_frames.max(1);
+ for i in 0..hashes.len() {
+ if state.hashes[i] != hashes[i] {
+ debt[i] = damage_frames;
+ moved_strips += 1;
+ }
+ }
+ // Reuse the hash differ: mark an owed strip by handing it a baseline value that
+ // cannot match, and an unowed one its own value.
+ let mut baseline: KVVec<u64> = KVVec::new();
+ baseline.resize(hashes.len(), 0, GFP_KERNEL)?;
+ for i in 0..hashes.len() {
+ baseline[i] = if debt[i] > 0 { !hashes[i] } else { hashes[i] };
+ }
+ content_damage = changed_strip_rects(
+ geometry,
+ &baseline,
+ &hashes,
+ padded_width,
+ padded_height,
+ )?;
+ } else {
+ full = true;
+ }
+ } else {
+ full = true;
+ }
+ }
+ content_hashes = Some(hashes);
+ }
+ if !full && content_damage.is_empty() {
+ scanout_gate(connector, "no keyframe owed and no strip content changed");
+ return Ok(());
+ }
+ // Serial fallback and parallel workers share the same transformed sampler.
+ let px = |dx: usize, dy: usize| src.px(dx, dy);
+ // Damage selection and encoded-strip reuse remain identity-only. Rotated and reflected frames
+ // are conservative full updates, but their independent strips can use the same workqueue
+ // fan-out as an identity keyframe.
+ // What the encoded bytes depend on besides the strip pixels themselves; see
+ // `StripHashState::tag`. Identity rotation is a precondition of caching at all, so it needs no
+ // representation here.
+ let encode_tag = {
+ let gamma = match &src.color {
+ Some(pipeline) => pipeline.tag(),
+ None => 0,
+ };
+ // The sample depth belongs here for the same reason the colour transform does, and is the
+ // more dangerous of the two: changing it re-maps every sample on the way into the codec
+ // and moves the escape ceiling, while leaving the framebuffer byte for byte identical. A
+ // cache keyed on the pixels alone therefore serves bodies encoded at the old depth inside
+ // a stream declared at the new one, and the dock decodes part of the frame as noise.
+ let deep = matches!(src.depth, crate::video::haar::Depth::Ten);
+ gamma ^ (u64::from(deep) * 0x9e37_79b9_7f4a_7c15)
+ };
+ // Strips carried over verbatim from the previous frame's encode, and the strips actually
+ // handed to the codec; kept for the post-send cache publish below.
+ let mut encoded: Option<(KVec<(usize, usize)>, KVec<KVec<u8>>)> = None;
+ let parallel = if !identity {
+ let coords = crate::video::haar::all_strip_coords(geometry, padded_width, padded_height)?;
+ match parallel_strip_encode(geometry, src, &coords)? {
+ Some(strips) => {
+ let records = if geometry.connector_selector_shift != 0 {
+ crate::video::haar::frame_records_navarro_ordinary(
+ geometry, &strips, connector,
+ )?
+ } else {
+ crate::video::haar::frame_records(geometry, &strips, connector)?
+ };
+ Some(records)
+ }
+ None => None,
+ }
+ } else {
+ let coords = if full {
+ crate::video::haar::all_strip_coords(geometry, padded_width, padded_height)?
+ } else {
+ crate::video::haar::damage_strip_coords(
+ geometry,
+ padded_width,
+ padded_height,
+ &content_damage,
+ )?
+ };
+ // What this frame costs the dock, and why. A delta that selects far more strips than moved
+ // was widened by the retransmit debt or by rounding out to whole macro-tiles; one that
+ // selects the surface from a handful of rectangles hit the rectangle ceiling instead.
+ vino_debug!(
+ "vino: scanout connector={} {} {}/{} strips from {} rect(s), {} moved\n",
+ connector,
+ if full { "keyframe" } else { "delta" },
+ coords.len(),
+ (padded_width >> geometry.strip_w_shift())
+ * (padded_height >> geometry.strip_h_shift()),
+ content_damage.len(),
+ moved_strips
+ );
+ // Reuse an encoded strip body when its pixels and gamma tag are unchanged. Encode only
+ // misses, then restore the required x-order within each Y band.
+ let tiles_x = padded_width >> geometry.strip_w_shift();
+ let mut reuse: KVec<Option<KVec<u8>>> = KVec::with_capacity(coords.len(), GFP_KERNEL)?;
+ let mut misses: KVec<(usize, usize)> = KVec::with_capacity(coords.len(), GFP_KERNEL)?;
+ {
+ let cache = data.strip_hashes.lock();
+ let usable = cache[connector_index].as_ref().filter(|c| {
+ c.padded_width == padded_width
+ && c.padded_height == padded_height
+ && c.tag == encode_tag
+ });
+ for &(sx, sy) in coords.iter() {
+ let idx =
+ (sy >> geometry.strip_h_shift()) * tiles_x + (sx >> geometry.strip_w_shift());
+ let hit = usable.and_then(|c| {
+ // Same pixels as when this body was produced, and a body was kept.
+ let same = c.hashes.get(idx).zip(content_hashes.as_ref()?.get(idx));
+ let body = c.bodies.get(idx)?;
+ (same.is_some_and(|(a, b)| a == b) && !body.is_empty()).then_some(body)
+ });
+ match hit {
+ Some(body) => {
+ let mut copy: KVec<u8> = KVec::with_capacity(body.len(), GFP_KERNEL)?;
+ copy.extend_from_slice(body, GFP_KERNEL)?;
+ reuse.push(Some(copy), GFP_KERNEL)?;
+ }
+ None => {
+ reuse.push(None, GFP_KERNEL)?;
+ misses.push((sx, sy), GFP_KERNEL)?;
+ }
+ }
+ }
+ }
+ let fresh = match parallel_strip_encode(geometry, src, &misses)? {
+ Some(s) => Some(s),
+ // Too few misses to be worth splitting: encode them here rather than dropping to
+ // the whole-frame serial path, which would re-encode the cache hits as well.
+ None if !misses.is_empty() => Some(encode_coords(geometry, src, &misses)?),
+ None => Some(KVec::new()),
+ };
+ match fresh {
+ Some(fresh) if fresh.len() == misses.len() => {
+ let mut strips: KVec<KVec<u8>> = KVec::with_capacity(coords.len(), GFP_KERNEL)?;
+ let mut next = fresh.into_iter();
+ for slot in reuse {
+ match slot {
+ Some(body) => strips.push(body, GFP_KERNEL)?,
+ None => strips.push(next.next().ok_or(EINVAL)?, GFP_KERNEL)?,
+ }
+ }
+ let records = if full && geometry.connector_selector_shift != 0 {
+ crate::video::haar::frame_records_navarro_ordinary(
+ geometry, &strips, connector,
+ )?
+ } else {
+ crate::video::haar::frame_records(geometry, &strips, connector)?
+ };
+ encoded = Some((coords, strips));
+ Some(records)
+ }
+ _ => None,
+ }
+ };
+ let frames = match parallel {
+ Some(r) => r,
+ None if full && geometry.connector_selector_shift != 0 => {
+ crate::video::haar::colour_frame_ep08_navarro_ordinary(
+ geometry,
+ padded_width,
+ padded_height,
+ connector,
+ px,
+ )?
+ }
+ None if full => crate::video::haar::colour_frame_ep08(
+ geometry,
+ padded_width,
+ padded_height,
+ connector,
+ px,
+ )?,
+ None => crate::video::haar::colour_frame_ep08_damage(
+ geometry,
+ padded_width,
+ padded_height,
+ connector,
+ &content_damage,
+ px,
+ )?,
+ };
+ // A damage delta that touched no aligned strip = nothing to send this flip: skip the write
+ // (no seq advance, no arm, keyframe obligation untouched). Full frames always have strips.
+ if frames.is_empty() {
+ scanout_gate(connector, "encoder produced zero records");
+ return Ok(());
+ }
+ if data.shutting_down.load(Ordering::Acquire)
+ || data.modeset_requested[connector_index].load(Ordering::Acquire) != want
+ || data.modeset_active[connector_index].load(Ordering::Acquire) != want
+ {
+ scanout_gate(
+ connector,
+ "mode generation changed between encode and submit",
+ );
+ return Ok(());
+ }
+ // A frame is one continuous bulk stream: intermediate transfers end on a full 1024-byte packet
+ // and only the final transfer is short. A short packet at a record boundary terminates the
+ // frame early and desynchronises the dock. The first frame after a mode set also prepends the
+ // connector's ten-record arm burst; clear that obligation only after a successful submission.
+ let connector_bit = 1u32 << connector;
+ // The 2560-byte arm burst appears only on frame zero after a mode set. Later frames begin
+ // directly with video records.
+ let arm = if data
+ .arm_prefix_pending
+ .load(core::sync::atomic::Ordering::Acquire)
+ & connector_bit
+ != 0
+ {
+ Some(data.build_stream_prefix_buf(connector_index)?)
+ } else {
+ None
+ };
+ let arm_len = arm.as_ref().map_or(0, |a| a.len());
+ // Revalidate at the actual wire boundary too. The encoded bytes and ARM prefix are specific to
+ // this mode generation; submitting them after a concurrent disable/re-enable poisons the next
+ // stream even though every USB URB can still complete successfully.
+ if data.shutting_down.load(Ordering::Acquire)
+ || data.modeset_requested[connector_index].load(Ordering::Acquire) != want
+ || data.modeset_active[connector_index].load(Ordering::Acquire) != want
+ {
+ vino_debug!(
+ "vino: scanout connector={} superseded before video submit; frame discarded\n",
+ connector
+ );
+ return Ok(());
+ }
+ // Preserve the last readiness-to-video adjacency from the VINO session that lit both panels.
+ // These are real CP status transactions (with EP84 replies drained by `send_cp`), paced at the
+ // required cadence, and only run for frame zero while the ARM prefix is present.
+ if arm.is_some() {
+ for _ in 0..VinoDrmData::PREWRITE_POLLS {
+ data.poll_status(dev)?;
+ fsleep(Delta::from_millis(VinoDrmData::PREWRITE_POLL_MS as i64));
+ }
+ vino_debug!(
+ "vino: inline pre-write paced poll ({}x @{}ms) before first video connector={}\n",
+ VinoDrmData::PREWRITE_POLLS,
+ VinoDrmData::PREWRITE_POLL_MS,
+ connector
+ );
+ }
+ // Frame zero starts with an arm record; later frames start with video records. Record fragments
+ // are allocation boundaries only and are joined into exact 64-KiB transfers below without a
+ // whole-frame coalescing allocation.
+ let frame_count = frames.len();
+ let image_len: usize = frames.iter().take(frame_count).map(|f| f.len()).sum();
+ // The DL7400's per-strip parameter map. DLM and Windows both include it in frame zero after
+ // at least some image records; the deterministic image-then-map ordering is used below. Ridge
+ // has no equivalent record and gets an empty slice.
+ let params: KVec<u8> = if geometry.connector_selector_shift == 0 {
+ KVec::new()
+ } else {
+ crate::video::haar::navarro_strip_params(
+ geometry,
+ connector,
+ padded_width,
+ padded_height,
+ &frames,
+ &mut data.strip_classes.lock()[connector_index],
+ )?
+ };
+ vino_debug!(
+ "vino: connector={} shift={} params={} B ({}x{} pad)\n",
+ connector,
+ geometry.connector_selector_shift,
+ params.len(),
+ padded_width,
+ padded_height
+ );
+ if arm.is_some() {
+ data.send_stream_open(dev, connector_index)?;
+ }
+ let startup = arm.is_some();
+ // A cold link requires a bounded back-to-back full-frame burst until the downstream clock is
+ // programmed. Reuse the encoded image and advance only its frame trailer and per-frame control
+ // sync. The arm prefix remains exclusive to presentation zero.
+ let training = full
+ && data.sustain_until.lock()[connector_index]
+ .is_some_and(|until| (until - Instant::<Monotonic>::now()).as_millis() > 0);
+ // A dock that shares its control pipe cannot be given the training window's presentation
+ // count: a whole-surface keyframe is 1.88 MB there, and eight back-to-back copies of it are
+ // 15 MB in one scanout against a dock DLM feeds at 0.86 MB/s. Measured, the dock accepts
+ // three such frames and then stops answering the control plane entirely. Every buffer still
+ // gets the keyframe; it just does not get it eight times.
+ // A keyframe has to initialise every buffer before its content shadow can become authoritative.
+ // Ordinary updates are different: DLM sends one Ella presentation per logical frame and lets
+ // successive frames walk the ring. The profile keeps those two requirements separate and the
+ // damage ledger schedules later logical frames if the compositor becomes idle.
+ let repeat_count = frame_presentation_count(
+ data.frame_delivery(),
+ full,
+ training,
+ data.video_on_ctrl_pipe(),
+ );
+ let first_opener_len = data
+ .build_frame_opener(connector, seq0, startup)
+ .as_ref()
+ .map_or(0, |o| o.len());
+ // Every ordinary Navarro presentation pairs its frame-sub records with one authenticated
+ // report on the stream sub. Build presentation zero once here so the diagnostic length and
+ // the bytes submitted below refer to the same live counter reservation. The prologue itself
+ // has no report; presentation one will allocate its own inside the loop.
+ let mut first_report = if startup {
+ None
+ } else {
+ data.build_stream_report_buf(connector_index, seq0)?
+ };
+ let first_report_len = first_report.as_ref().map_or(0, |r| r.len());
+ let first_wire_len = arm_len
+ + first_opener_len
+ + first_report_len
+ + params.len()
+ + image_len
+ + data.build_frame_trailer(connector, seq0).len();
+ let (rec_count, max_stride, max_strip) =
+ crate::video::haar::record_stats(&frames[..frame_count]);
+ vino_debug!(
+ "vino: connector={} chunks={} arm={} first={} presentations={} records={} max_stride={} max_strip={}\n",
+ connector,
+ frame_count,
+ arm_len,
+ first_wire_len,
+ repeat_count,
+ rec_count,
+ max_stride,
+ max_strip
+ );
+ // Split at 65536-byte boundaries, a multiple of the endpoint's 1024-byte maximum packet size,
+ // so only the final transfer terminates short. Submit through a persistent eight-deep queue to
+ // keep the frame continuous across transfer boundaries. Do not flush between frames: slot reuse
+ // reaps completions without introducing a pipeline gap.
+ const XFER: usize = VIDEO_XFER;
+ let pipe_i = dev.video_pipe_index(connector_index)?;
+ let mut last_wire_len = 0usize;
+ // Presentations that named a ring slot, which is what the frame counter counts. See
+ // `names_ring_slot`.
+ let mut named = 0u32;
+ for repeat in 0..repeat_count {
+ // Pace the copies apart on a dock whose control plane shares this endpoint. Back-to-back
+ // presentations hold it for as long as it takes to push several megabytes, which is
+ // exactly when the dock has to be able to answer, and it stops answering at all.
+ if repeat > 0 && data.video_on_ctrl_pipe() {
+ fsleep(Delta::from_millis(data.frame_period_ms()));
+ }
+ // A compositor mode change can arrive while the presentation is in flight. Never let the
+ // old frame cross the new mode generation.
+ if data.shutting_down.load(Ordering::Acquire)
+ || data.modeset_requested[connector_index].load(Ordering::Acquire) != want
+ || data.modeset_active[connector_index].load(Ordering::Acquire) != want
+ {
+ vino_debug!(
+ "vino: scanout connector={} superseded during presentation; stopped at {}/{}\n",
+ connector,
+ repeat,
+ repeat_count
+ );
+ return Ok(());
+ }
+
+ let repeat_seq = seq0.wrapping_add(named);
+ let frame_trailer = data.build_frame_trailer(connector, repeat_seq);
+ // Prefix ARM only to presentation zero. Every later presentation starts directly at the
+ // image records and carries a freshly advanced three-record frame trailer.
+ let arm_slice: &[u8] = if repeat == 0 {
+ arm.as_ref().map_or(&[], |a| &a[..])
+ } else {
+ &[]
+ };
+ let prologue_frame = startup && repeat == 0;
+ let frame_opener = data.build_frame_opener(connector, repeat_seq, prologue_frame);
+ let opener_slice: &[u8] = frame_opener.as_ref().map_or(&[], |o| &o[..]);
+ if super::names_ring_slot(opener_slice, &frame_trailer) {
+ named = named.wrapping_add(1);
+ }
+ let report = if prologue_frame {
+ None
+ } else if repeat == 0 {
+ first_report.take()
+ } else {
+ data.build_stream_report_buf(connector_index, repeat_seq)?
+ };
+ let report_slice: &[u8] = report.as_ref().map_or(&[], |r| &r[..]);
+ let wire_len = arm_slice.len()
+ + opener_slice.len()
+ + report_slice.len()
+ + params.len()
+ + image_len
+ + frame_trailer.len();
+ last_wire_len = wire_len;
+ {
+ // Take this connector's staging buffer while submitting, then restore it. The queue
+ // mutex stays locked for the complete frame: two connectors that address the same
+ // physical endpoint must never submit their record streams concurrently.
+ let mut staging = match data.video_staging.lock()[connector_index].take() {
+ Some(s) => s,
+ None => {
+ let mut s = KVec::new();
+ s.resize(XFER, 0, GFP_KERNEL)?;
+ s
+ }
+ };
+ let submitted = {
+ // One writer owns a shared pipe for the whole frame; see `own_pipe`.
+ let _pipe = data.own_pipe();
+ // A shared-pipe queue failure is terminal for its complete control/video session.
+ // It may have happened while this worker waited behind its sibling, so recheck
+ // under pipe ownership before recreating or touching the canonical queue.
+ if data.video_on_ctrl_pipe() && !data.cp_link_alive() {
+ data.video_staging.lock()[connector_index] = Some(staging);
+ return Err(ENODEV);
+ }
+ let mut queue_slot = data.video_q[pipe_i].lock();
+ if queue_slot.is_none() {
+ match dev.video_queue(connector_index, 8, XFER) {
+ Ok(q) => {
+ *queue_slot = Some(q);
+ vino_debug!(
+ "vino: connector={} endpoint={:#04x} persistent video queue opened (depth=8, {} B URBs)\n",
+ connector,
+ dev.endpoints.video[connector_index].address(),
+ XFER
+ );
+ }
+ // Nothing was taken from the shared pipe slot, but return this connector's
+ // staging allocation before propagating the open error.
+ Err(e) => {
+ data.video_staging.lock()[connector_index] = Some(staging);
+ return Err(e);
+ }
+ }
+ }
+ // Keep both borrows inside the mutex scope. Dropping the queue or unlocking it
+ // mid-frame would permit a second connector to interleave URBs on this endpoint.
+ let submit = |staging: &mut KVec<u8>, q: &mut crate::usb::BulkOutQueue| -> Result {
+ let staging = &mut staging[..];
+ let q = &mut *q;
+ // Scatter/gather cursor over
+ // [optional ARM][optional Navarro opener][authenticated stream report]
+ // [record chunks, with the parameter map among them][trailer]. Join only one
+ // transfer at a time in the reusable bounded staging allocation, avoiding a
+ // contiguous allocation spanning the complete frame.
+ let arm_parts = usize::from(!arm_slice.is_empty());
+ let opener_parts = usize::from(!opener_slice.is_empty());
+ let report_parts = usize::from(!report_slice.is_empty());
+ let param_parts = usize::from(!params.is_empty());
+ let trailer_parts = 1usize;
+ let opener_end = arm_parts + opener_parts;
+ let lead = opener_end + report_parts;
+ // The map describes the records around it and goes where the vendor puts it,
+ // part-way through them; see `param_map_chunk_split`. A dock with no map has
+ // no split to make.
+ let param_after = if param_parts == 0 {
+ frame_count
+ } else {
+ super::param_map_chunk_split(&frames[..frame_count])
+ };
+ let param_end = lead + param_after + param_parts;
+ let part_count = lead + frame_count + param_parts + trailer_parts;
+ let mut part_i = 0usize;
+ let split_tail = data.split_full_packet_frame();
+ let mut part_off = 0usize;
+ let mut wire_off = 0usize;
+ while wire_off < wire_len {
+ let mut data_len = (wire_len - wire_off).min(XFER);
+ // The dock delimits a frame by the short packet its last transfer ends on.
+ // A frame whose length is a whole number of maximum-size packets ends on a
+ // full one, delimits nothing, and is read as running into the next frame.
+ // Split one short tail off it so there is always a boundary; a record may
+ // span transfers, so where the split falls does not matter.
+ if split_tail
+ && data_len == wire_len - wire_off
+ && data_len % BULK_MAX_PACKET == 0
+ {
+ data_len -= FRAME_TAIL_BYTES;
+ }
+ let dst = &mut staging[..data_len];
+ let mut dst_off = 0usize;
+ while dst_off < dst.len() && part_i < part_count {
+ let part: &[u8] = if part_i < arm_parts {
+ arm_slice
+ } else if part_i < opener_end {
+ opener_slice
+ } else if part_i < lead {
+ report_slice
+ } else if part_i < lead + param_after {
+ &frames[part_i - lead][..]
+ } else if part_i < param_end {
+ ¶ms[..]
+ } else if part_i < part_count - trailer_parts {
+ &frames[part_i - lead - param_parts][..]
+ } else {
+ &frame_trailer[..]
+ };
+ let n = (part.len() - part_off).min(dst.len() - dst_off);
+ dst[dst_off..dst_off + n]
+ .copy_from_slice(&part[part_off..part_off + n]);
+ dst_off += n;
+ part_off += n;
+ if part_off == part.len() {
+ part_i += 1;
+ part_off = 0;
+ }
+ }
+ if let Err(e) = q.send(dev.io(), dst, crate::timeout()) {
+ // The queue is eight URBs deep and `send` reaps a completion when its
+ // slot is reused, so this offset is where the error surfaced, not
+ // where the dock objected -- that was about eight transfers earlier.
+ // Report the frame's shape instead: a dock refuses a malformed record
+ // by halting the endpoint, which arrives here as a transport error and
+ // gets blamed on the transport.
+ let (records, max_stride, max_strip) =
+ crate::video::haar::record_stats(&frames[..frame_count]);
+ pr_warn!(
+ "vino: scanout connector={} pipeline submit at off={}/{} failed; frame has {} records, largest stride {}, largest strip {}\n",
+ connector,
+ wire_off,
+ wire_len,
+ records,
+ max_stride,
+ max_strip
+ );
+ return Err(e);
+ }
+ wire_off += data_len;
+ }
+ Ok(())
+ };
+ let submitted = {
+ let mut queue = queue_slot.as_mut().get_mut().as_mut().ok_or(ENODEV)?;
+ submit(&mut staging, &mut queue)
+ };
+ match submitted {
+ Ok(()) => Ok(()),
+ Err(e) => {
+ // Stay inside the existing physical-pipe and canonical-queue guards. The
+ // queue drop kills and drains every later URB before halt-clear, so neither
+ // this connector's unframed tail nor a sibling's new frame can race
+ // recovery.
+ let clear_halt = (e == EPIPE || e == EPROTO)
+ && data.scanout_fails[connector_index].load(Ordering::Relaxed)
+ < VIDEO_STALL_LIMIT;
+ if let Err(recovery) = data.retire_failed_video_queue(
+ dev,
+ connector_index,
+ &mut *queue_slot,
+ e,
+ clear_halt,
+ ) {
+ pr_err!(
+ "vino: connector {connector_index} video queue recovery failed ({recovery:?})\n"
+ );
+ }
+ Err(e)
+ }
+ }
+ };
+ // Restore the per-connector staging allocation after the endpoint transaction.
+ data.video_staging.lock()[connector_index] = Some(staging);
+ submitted?;
+ }
+
+ // The ARM burst was delivered with presentation zero. Clear it immediately rather than
+ // after the whole replay: if a later copy fails, retrying ARM would corrupt a pipe that is
+ // already armed.
+ if repeat == 0 && startup {
+ data.arm_prefix_pending
+ .fetch_and(!connector_bit, core::sync::atomic::Ordering::Release);
+ // The cold-link requirement is measured from the start of continuous VIDEO, not from
+ // the earlier mode-set. Refresh the complete training window here so modeset bracket
+ // latency, cross-connector serialization, and encoder time cannot make it
+ // intermittently too short. Subsequent cadence-selected compositor flips and idle
+ // settle repaints are both promoted to full keyframes while this deadline is live.
+ // Re-armed from the first frame the dock actually received, but only where
+ // `sustain_window` granted one: a dock that shares its control pipe is granted none.
+ {
+ let mut sustain = data.sustain_until.lock();
+ if sustain[connector_index].is_some() {
+ sustain[connector_index] =
+ Some(Instant::<Monotonic>::now() + Delta::from_millis(3000));
+ }
+ }
+ vino_debug!(
+ "vino: scanout connector={} initial ARM+keyframe accepted ({} B on the wire)\n",
+ connector,
+ wire_len
+ );
+ // The dock expects two stream-commit messages on EP02 immediately after accepting the
+ // video arm burst.
+ for _ in 0..2 {
+ match data.send_cp(dev, 0x16, 0, |ctr| crate::cp::stream_commit(ctr, connector)) {
+ Ok(()) => vino_debug!("vino: stream-commit connector={} ok\n", connector),
+ Err(e) => pr_warn!(
+ "vino: stream-commit connector={} failed ({e:?})\n",
+ connector
+ ),
+ }
+ }
+ }
+
+ // A dedicated video endpoint can cheaply sample status after streaming. On a shared
+ // pipe, the long-lived keepalive already owns this same poll and DLM permits long runs of
+ // pixels without an inline CP transaction. Duplicating it here increases EP02 pressure
+ // and blocks the scanout worker waiting for a reply from the endpoint it just filled.
+ if !data.video_on_ctrl_pipe() {
+ let due = {
+ let mut last = data.last_status_poll.lock();
+ let due = last.is_none_or(|t| t.elapsed().as_millis() >= STATUS_POLL_MIN_MS);
+ if due {
+ *last = Some(Instant::<Monotonic>::now());
+ }
+ due
+ };
+ if due {
+ if let Err(e) =
+ data.send_cp(dev, 0x14, 0, |ctr| crate::cp::device_query_req(ctr, 0x000c))
+ {
+ vino_debug!(
+ "vino: scanout connector={} CP status poll failed ({e:?})\n",
+ connector
+ );
+ }
+ }
+ }
+ // Do not drain here. The eight-URB ring spans frame boundaries; `send()` reaps a
+ // completion when its slot is reused, so transport errors surface after the ring wraps
+ // without introducing a per-frame pipeline bubble.
+ }
+ // Publish the new codec sequence only after every URB for this frame was submitted. A stale
+ // generation or transport failure above leaves the old sequence intact for the next keyframe.
+ //
+ data.scanout_seq.lock()[connector_index] = seq0.wrapping_add(named);
+ // The USB path accepted the complete image. Publish its content shadow only now; every early
+ // return and transport error above deliberately leaves the previous dock-visible state intact.
+ // The frame reached the dock, so every strip it carried has paid one transmission. A full
+ // keyframe is presented twice and rewrites the whole surface, so it clears the ledger outright.
+ {
+ let mut ttl = data.dirty_ttl.lock();
+ if let Some(debt) = ttl[connector_index].as_mut() {
+ pay_damage_debt(debt, full);
+ }
+ }
+ // Publish the content shadow, and with it the encoded body of every strip this frame carried,
+ // so the retransmissions `damage_frames` owes can re-use the bytes instead of re-running the
+ // codec (see `StripHashState::bodies`). Bodies for strips this frame did not touch are carried
+ // forward from the previous state -- they are still what the dock holds, and a later debt pass
+ // may select them. Best-effort throughout: a failed allocation costs a cache miss, never a
+ // frame, so the hashes are published either way.
+ {
+ let mut state = data.strip_hashes.lock();
+ let carried = state[connector_index]
+ .take()
+ .filter(|c| {
+ c.padded_width == padded_width
+ && c.padded_height == padded_height
+ && c.tag == encode_tag
+ })
+ .map(|c| (c.bodies, c.hashes));
+ state[connector_index] = content_hashes.map(|hashes| {
+ let (mut bodies, old) = match carried {
+ Some((b, h)) => (b, Some(h)),
+ None => (KVec::new(), None),
+ };
+ // A carried body is only still valid if that strip's content has not moved since it
+ // was encoded. Every strip whose hash changes IS selected for this frame and so is
+ // overwritten below -- but do not rely on that invariant holding as the selection
+ // logic evolves: a body left paired with a newer hash would be served as a cache hit
+ // and paint stale pixels the dock would then keep, with nothing scheduled to repair
+ // it. Cheap to make airtight, and the failure it prevents is permanent corruption.
+ if let Some(old) = &old {
+ if old.len() == bodies.len() && old.len() == hashes.len() {
+ for i in 0..bodies.len() {
+ if old[i] != hashes[i] {
+ bodies[i] = KVec::new();
+ }
+ }
+ }
+ }
+ if bodies.len() != hashes.len() {
+ bodies = KVec::new();
+ let _ = bodies.reserve(hashes.len(), GFP_KERNEL);
+ while bodies.len() < hashes.len() && bodies.push(KVec::new(), GFP_KERNEL).is_ok() {}
+ }
+ if bodies.len() == hashes.len() {
+ if let Some((coords, strips)) = encoded {
+ let tiles_x = padded_width >> geometry.strip_w_shift();
+ for (&(sx, sy), body) in coords.iter().zip(strips) {
+ let idx = (sy >> geometry.strip_h_shift()) * tiles_x
+ + (sx >> geometry.strip_w_shift());
+ if let Some(slot) = bodies.get_mut(idx) {
+ *slot = body;
+ }
+ }
+ }
+ }
+ StripHashState {
+ padded_width,
+ padded_height,
+ hashes,
+ bodies,
+ tag: encode_tag,
+ }
+ });
+ }
+ // A full keyframe was accepted -- this connector may now send damage deltas until the next
+ // mode-set.
+ if full {
+ data.keyframe_pending
+ .fetch_and(!kf_bit, core::sync::atomic::Ordering::Release);
+ }
+
+ // Charge every presentation, not the frame once: each one is a separate copy on the wire and
+ // the dock decodes all of them.
+ data.charge_stream_budget(last_wire_len.saturating_mul(repeat_count as usize));
+
+ vino_debug!(
+ "vino: scanout connector={} frame ok ({} presentation(s), {} B final write)\n",
+ connector,
+ repeat_count,
+ last_wire_len
+ );
+ Ok(())
+}
+
+/// Convert the mapped XRGB8888 frame to RGB565, Vino-encode it against the previous frame,
+/// and bulk-write the resulting EP08 frame to the dock.
+pub(super) fn encode_and_send(
+ dev: &BoundInterface<'_>,
+ data: &VinoDrmData,
+ connector: u8,
+ src: &Arc<PixelSource>,
+ rotation: plane::Rotation,
+ // The client's changed rectangles (identity rotation only; empty means no pixel update).
+ // `encode_and_send_haar` uses these to send a damage delta (only changed strips) after the
+ // first full keyframe because the dock surface is undefined after a mode set.
+ clips: &[(usize, usize, usize, usize)],
+ w: usize,
+ h: usize,
+) -> Result {
+ // Non-64x16-aligned modes are padded to complete codec strips. The dock clips the padded image
+ // to the active timing, matching the validated 68-band wire layout for 1080-line modes.
+ encode_and_send_haar(dev, data, connector, src, rotation, clips, w, h)
+}
+
+// ---- Encoder ----------------------------------------------------------------
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_scanout)]
+mod tests {
+ use super::*;
+ use crate::*;
+
+ /// An eight-bit desktop driven over a ten-bit link must keep both endpoints exact.
+ ///
+ /// A plain shift leaves full white at 1020 of 1023, which tints every highlight and is the
+ /// kind of error that looks like a panel problem rather than an encoder one.
+ #[test]
+ fn widening_an_eight_bit_sample_keeps_the_endpoints() {
+ assert_eq!(widen_8_to_10(0), 0);
+ assert_eq!(widen_8_to_10(255), 1023);
+ // Mid grey stays mid grey rather than drifting low.
+ assert_eq!(widen_8_to_10(128), 514);
+ // Monotonic, and never outside the ten-bit range.
+ let mut previous = 0;
+ for v in 0..=255u16 {
+ let w = widen_8_to_10(v);
+ assert!(w <= 1023);
+ assert!(v == 0 || w > previous);
+ previous = w;
+ }
+ }
+
+ /// Scattered damage must cost the strips it touches, not the surface.
+ ///
+ /// The rectangle list is an intermediate representation between the per-strip hash comparison
+ /// and the strip coordinates the encoder is given, and it has a ceiling. Built on the strip
+ /// grid, an update spread across the screen fragments into more runs than that ceiling holds
+ /// and the whole surface is sent instead -- roughly thirty times the bytes, on a dock that
+ /// halts its endpoint when it is given too many. Built on the macro-tile grid, which is the
+ /// granularity a touched strip is resent at anyway, the same update stays inside it.
+ #[test]
+ fn scattered_damage_does_not_cost_the_whole_surface() -> Result {
+ let geometry = profile::PROFILE_ELLA.geometry();
+ // 1920x1088, this dock's whole top mode padded to strips: 30 x 68 = 2040 strips.
+ let (padded_width, padded_height) = (1920usize, 1088usize);
+ let tiles_x = padded_width >> geometry.strip_w_shift();
+ let tiles_y = padded_height >> geometry.strip_h_shift();
+ assert_eq!(tiles_x * tiles_y, 2040);
+
+ let mut old: KVVec<u64> = KVVec::new();
+ old.resize(tiles_x * tiles_y, 0, GFP_KERNEL)?;
+ let mut new: KVVec<u64> = KVVec::new();
+ new.resize(tiles_x * tiles_y, 0, GFP_KERNEL)?;
+
+ // Nothing moved: no rectangles, and the caller skips the write entirely.
+ assert!(changed_strip_rects(geometry, &old, &new, padded_width, padded_height)?.is_empty());
+
+ // Every other strip on every other band: 510 changed strips, and 510 separate runs on the
+ // strip grid -- four times the ceiling. On the macro-tile grid they collapse into whole
+ // rows of tiles.
+ let mut changed = 0usize;
+ for ty in (0..tiles_y).step_by(2) {
+ for tx in (0..tiles_x).step_by(2) {
+ new[ty * tiles_x + tx] = 1;
+ changed += 1;
+ }
+ }
+ assert_eq!(changed, 510);
+ let rects = changed_strip_rects(geometry, &old, &new, padded_width, padded_height)?;
+ assert!(!rects.is_empty());
+ let selected =
+ video::haar::damage_strip_coords(geometry, padded_width, padded_height, &rects)?.len();
+ // This pattern really does reach into every macro-tile, so the surface is the right
+ // answer; what matters is that it was reached by rounding and not by giving up.
+ assert_eq!(selected, 2040);
+
+ // A realistic update instead: a window redraw, a cursor and a clock. Three regions, and
+ // the cost is the macro-tiles they cover.
+ for h in new.iter_mut() {
+ *h = 0;
+ }
+ for (x0, y0, x1, y1) in [
+ (4usize, 8usize, 14usize, 30usize),
+ (20, 40, 22, 42),
+ (26, 2, 29, 4),
+ ] {
+ for ty in y0..y1 {
+ for tx in x0..x1 {
+ new[ty * tiles_x + tx] = 1;
+ }
+ }
+ }
+ let rects = changed_strip_rects(geometry, &old, &new, padded_width, padded_height)?;
+ // Every rectangle is macro-tile aligned, so the selector's own rounding adds nothing.
+ // Aligned on every edge the grid has one, and clamped to the surface where it does not:
+ // 1920 is seven whole macro-tiles and half of an eighth.
+ for &(x0, y0, x1, y1) in rects.iter() {
+ assert_eq!(x0 % geometry.macro_w(), 0);
+ assert_eq!(y0 % geometry.macro_h(), 0);
+ assert!(x1 == padded_width || x1 % geometry.macro_w() == 0);
+ assert!(y1 == padded_height || y1 % geometry.macro_h() == 0);
+ }
+ let selected =
+ video::haar::damage_strip_coords(geometry, padded_width, padded_height, &rects)?.len();
+ assert!(selected < 2040 / 2);
+ Ok(())
+ }
+
+ #[test]
+ fn parallel_encoder_matches_serial_for_every_plane_transform() -> Result {
+ use drm::kms::plane::Rotation;
+
+ let transforms = [
+ Rotation::ROTATE_0,
+ Rotation::ROTATE_90,
+ Rotation::ROTATE_180,
+ Rotation::ROTATE_270,
+ Rotation::ROTATE_0 | Rotation::REFLECT_X,
+ Rotation::ROTATE_90 | Rotation::REFLECT_X,
+ Rotation::ROTATE_180 | Rotation::REFLECT_X,
+ Rotation::ROTATE_270 | Rotation::REFLECT_X,
+ Rotation::ROTATE_0 | Rotation::REFLECT_Y,
+ Rotation::ROTATE_90 | Rotation::REFLECT_Y,
+ Rotation::ROTATE_180 | Rotation::REFLECT_Y,
+ Rotation::ROTATE_270 | Rotation::REFLECT_Y,
+ Rotation::ROTATE_0 | Rotation::REFLECT_X | Rotation::REFLECT_Y,
+ Rotation::ROTATE_90 | Rotation::REFLECT_X | Rotation::REFLECT_Y,
+ Rotation::ROTATE_180 | Rotation::REFLECT_X | Rotation::REFLECT_Y,
+ Rotation::ROTATE_270 | Rotation::REFLECT_X | Rotation::REFLECT_Y,
+ ];
+ for transform in transforms {
+ parallel_rotation_matches_serial(transform)?;
+ }
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/stream.rs b/drivers/gpu/drm/vino/drm_sink/stream.rs
new file mode 100644
index 000000000000..1adb37de980e
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/stream.rs
@@ -0,0 +1,470 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Opening a connector's video stream, and the records that frame it.
+//!
+//! Before any pixels reach a connector the dock has to be told what is coming: a ring of buffer
+//! addresses, a decoder configuration, and an opening that differs by family. Every record here is
+//! sealed with the connector's own video key, and the dock silently declines a stream whose
+//! opening it did not expect.
+
+use super::*;
+
+impl VinoDrmData {
+ /// Build one connector's cold video-arm burst, prepended to the first video frame after a mode
+ /// set. Records #0/#1/#4/#5 are plaintext and #6/#7 contain a fixed `type=4` body. Records
+ /// #2/#3/#8/#9 use this connector's video key and nonce, derived from the per-connector SKE
+ /// with `riv_h ^ (0x08 | connector)` in byte 7, and share one block counter. Records #8/#9
+ /// carry the decoder configuration and independent nonces. Build the per-connector video
+ /// stream-open this platform wants in place of the cold ARM burst.
+ ///
+ /// Sealed with the connector's video key like every other video-endpoint message, and prefixed
+ /// to the first frame after a mode set so it reaches the dock before any pixels. Build the
+ /// prefix that opens a connector's video stream after a mode set.
+ ///
+ /// Ridge prefixes a cold ARM burst to the first frame; Navarro prefixes a short sealed
+ /// stream-open. Both occupy the same slot ahead of any pixels, and every submission path must
+ /// pick between them the same way: sending one platform's opening to the other's dock leaves
+ /// the stream unopened, and the dock then watchdog-resets a few seconds later. Build the
+ /// records that close a frame, in this dock's format.
+ pub(super) fn build_frame_trailer(
+ &self,
+ connector: u8,
+ seq0: u32,
+ ) -> crate::video::haar::FrameTrailer {
+ let geometry = self.geometry();
+ if self.video_on_ctrl_pipe() {
+ crate::video::haar::FrameTrailer::one(&crate::video::haar::ella_frame_close(
+ geometry, connector, seq0,
+ ))
+ } else if self.uses_arm_burst() {
+ crate::video::haar::frame_trailer(geometry, connector, seq0)
+ } else {
+ crate::video::haar::navarro_frame_trailer(geometry, connector, seq0)
+ }
+ }
+ /// Build the record that starts a non-prologue DL7400 frame.
+ ///
+ /// Ridge carries its slot transition in the three-record trailer and the DL-3x00 in its single
+ /// closing record, so neither has an opener. Navarro terminates the old frame after its close
+ /// record and starts the next USB transfer with this opener instead.
+ pub(super) fn build_frame_opener(
+ &self,
+ connector: u8,
+ seq0: u32,
+ prologue: bool,
+ ) -> Option<KVec<u8>> {
+ if self.uses_arm_burst() || self.video_on_ctrl_pipe() || prologue {
+ return None;
+ }
+ let mut out = KVec::new();
+ out.extend_from_slice(
+ &crate::video::haar::navarro_frame_opener(self.geometry(), connector, seq0),
+ GFP_KERNEL,
+ )
+ .ok()
+ .map(|()| out)
+ }
+ /// Open a connector's video stream, once per mode generation, ahead of any pixels.
+ ///
+ /// Does nothing on a dock whose opening is the ARM burst carried with the first frame.
+ pub(super) fn send_stream_open(&self, dev: &BoundInterface<'_>, connector: usize) -> Result {
+ let bit = 1u32 << connector;
+ if self.stream_open_pending.load(Ordering::Acquire) & bit == 0 {
+ return Ok(());
+ }
+ let Some(open) = self.build_stream_open_buf(connector)? else {
+ self.stream_open_pending.fetch_and(!bit, Ordering::Release);
+ return Ok(());
+ };
+ let pipe_i = dev.video_pipe_index(connector)?;
+ let mut queue_slot = self.video_q[pipe_i].lock();
+ if queue_slot.is_none() {
+ *queue_slot = Some(dev.video_queue(connector, 8, VIDEO_XFER)?);
+ }
+ let queue = queue_slot
+ .as_mut()
+ .get_mut()
+ .as_mut()
+ .ok_or(kernel::error::code::ENODEV)?;
+ queue.send(dev.io(), &open, crate::timeout())?;
+ self.stream_open_pending.fetch_and(!bit, Ordering::Release);
+ vino_debug!("vino: connector {} video stream opened\n", connector);
+ Ok(())
+ }
+ /// Keep every engaged connector's video endpoint from going quiet long enough for the dock to
+ /// tear the link down.
+ ///
+ /// The DL7400 stops answering -- video *and* control -- about a second after the last byte on a
+ /// video endpoint, whatever it was doing before. A compositor with nothing to redraw leaves
+ /// vino silent well past that, so each connector that has not sent anything for
+ /// [`NAVARRO_KEEPALIVE_MS`] sends the same sealed report DLM pairs with every frame. Called
+ /// from the control keepalive, which already runs for the life of the session.
+ ///
+ /// Only connectors whose video queue is already open are fed: a connector that has never
+ /// streamed has nothing to keep alive, and opening a queue here would start a stream nothing
+ /// follows.
+ pub(crate) fn send_video_keepalive(&self, dev: &BoundInterface<'_>) {
+ if self.uses_arm_burst() || self.shutting_down.load(Ordering::Acquire) {
+ return;
+ }
+ let now = Instant::<Monotonic>::now();
+ for connector in 0..MAX_CONNECTORS {
+ if self.modeset_active[connector].load(Ordering::Acquire) == 0 {
+ continue;
+ }
+ let due = match self.last_video_at.lock()[connector] {
+ Some(last) => (now - last).as_millis() >= NAVARRO_KEEPALIVE_MS,
+ None => false,
+ };
+ if !due {
+ continue;
+ }
+ let frame = self.scanout_seq.lock()[connector];
+ let Ok(report) = self.build_stream_report_buf(connector, frame) else {
+ continue;
+ };
+ let Some(report) = report else { continue };
+ let Ok(pipe_i) = dev.video_pipe_index(connector) else {
+ continue;
+ };
+ let mut queue_slot = self.video_q[pipe_i].lock();
+ let Some(queue) = queue_slot.as_mut().get_mut().as_mut() else {
+ continue;
+ };
+ if queue.send(dev.io(), &report, crate::timeout()).is_ok() {
+ self.last_video_at.lock()[connector] = Some(Instant::<Monotonic>::now());
+ }
+ }
+ }
+ pub(super) fn build_stream_prefix_buf(&self, connector: usize) -> Result<KVec<u8>> {
+ if self.uses_arm_burst() {
+ return self.build_arm_burst_buf(connector);
+ }
+ if self.video_on_ctrl_pipe() {
+ // Sent inside the mode-set bracket instead, where the vendor puts it, so nothing is
+ // owed to the frame. The empty buffer still marks this as the frame that opens the
+ // generation. See `send_stream_prologue`.
+ return Ok(KVec::new());
+ }
+ self.build_navarro_prologue_buf(connector)
+ }
+ /// Write a connector's stream prologue on the control pipe, for a dock that has no video pipe.
+ ///
+ /// The ring descriptor and the decoder configuration are records like any other on such a
+ /// dock, so they can be ordered against the mode-set markers rather than glued to the front of
+ /// a frame. Docks with a pipe of their own carry theirs with the first frame, which is where
+ /// their own captures put it, and get nothing here.
+ pub(super) fn send_stream_prologue(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ self.send_stream_ring(dev, connector)?;
+ self.send_stream_config(dev, connector)?;
+ Ok(())
+ }
+ /// Whether `connector` still owes its shared-pipe stream prologue.
+ fn stream_prologue_pending(&self, connector: u8) -> bool {
+ self.video_on_ctrl_pipe()
+ && usize::from(connector) < MAX_CONNECTORS
+ && self.arm_prefix_pending.load(Ordering::Acquire) & (1u32 << connector) != 0
+ }
+ /// Send only the unsealed ring descriptor of an Ella stream prologue.
+ ///
+ /// DLM does not concatenate this record with the decoder configuration: status and marker
+ /// records sit between them during cold activation. Making the two records independent table
+ /// actions preserves that ordering while the conservative runtime path may still invoke both
+ /// through [`Self::send_stream_prologue`].
+ pub(super) fn send_stream_ring(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ if !self.stream_prologue_pending(connector) {
+ return Ok(());
+ }
+ let ring = crate::video::haar::ella_stream_open(self.geometry(), connector);
+ dev.ctrl_send(&ring, crate::timeout(), GFP_KERNEL)?;
+ vino_debug!(
+ "vino: connector={} stream ring descriptor sent inside the bracket ({} B)\n",
+ connector,
+ ring.len()
+ );
+ Ok(())
+ }
+ /// Send only the sealed decoder configuration of an Ella stream prologue.
+ pub(super) fn send_stream_config(&self, dev: &BoundInterface<'_>, connector: u8) -> Result {
+ if !self.stream_prologue_pending(connector) {
+ return Ok(());
+ }
+ let connector_index = usize::from(connector);
+ let config = self.build_ella_config_buf(connector_index)?;
+ dev.ctrl_send(&config, crate::timeout(), GFP_KERNEL)?;
+ vino_debug!(
+ "vino: connector={} decoder configuration sent inside the bracket ({} B)\n",
+ connector,
+ config.len()
+ );
+ Ok(())
+ }
+ /// The mode header a connector's stream states, built from the mode it was last given.
+ ///
+ /// The surface named here is the padded one the codec actually produces: a mode whose height
+ /// is not a whole number of strips is encoded as the next whole one, and the dock has to be
+ /// told the size it is going to be sent. Every captured mode on a dock with its own video pipe
+ /// is already a whole number of strips, so this only ever rounds on the shared-pipe dock,
+ /// whose 1080-line modes are stated as 1088.
+ /// Whether this connector is being driven at 30 bpp.
+ ///
+ /// The decoder configuration, the set-mode and the codec all state the depth, and all read it
+ /// from [`Self::connector_programmed_ten_bit`] so that they cannot disagree.
+ fn connector_ten_bit(&self, connector: usize) -> bool {
+ u8::try_from(connector).is_ok_and(|c| self.connector_programmed_ten_bit(c))
+ }
+
+ fn stream_mode_header(&self, connector: usize) -> Result<[u8; 26]> {
+ let timing = self
+ .last_timing
+ .lock()
+ .get(connector)
+ .copied()
+ .flatten()
+ .ok_or(ENODEV)?;
+ let geometry = self.geometry();
+ let pad = |value: u16, unit: usize| -> u16 {
+ let unit = unit.max(1) as u16;
+ value.div_ceil(unit).saturating_mul(unit)
+ };
+ Ok(crate::video_arm::mode_header(
+ pad(timing.hactive, geometry.strip_w()),
+ pad(timing.vactive, geometry.strip_h()),
+ self.layout_word(),
+ timing.ten_bit,
+ ))
+ }
+ /// This connector's video sealing key and nonce, as [`set_video_keys`](Self::set_video_keys)
+ /// stored them: the whitened SKE key at the front and the stream content nonce behind it.
+ fn video_seal_key(&self, connector: usize) -> Result<(kernel::crypto::Secret<16>, [u8; 8])> {
+ let keys = self.video_keys.lock();
+ let key = keys.get(connector).ok_or(EINVAL)?;
+ let mut vkey = kernel::crypto::Secret::zeroed();
+ vkey.copy_from_slice(&key[..16]);
+ let mut vnonce = [0u8; 8];
+ vnonce.copy_from_slice(&key[16..24]);
+ Ok((vkey, vnonce))
+ }
+ /// Build the sealed decoder configuration a connector owes ahead of its first frame on a dock
+ /// that shares its control pipe.
+ ///
+ /// The stream itself was announced during CP setup, where the plaintext markers and the sealed
+ /// open could be ordered against the rest of the sequence. What is left is what DLM sends after
+ /// the mode set. The configuration continues the block counter the setup open started, which is
+ /// why it must not be rebuilt from block zero.
+ fn build_ella_config_buf(&self, connector: usize) -> Result<KVec<u8>> {
+ let (vkey, vnonce) = self.video_seal_key(connector)?;
+ let header = self.stream_mode_header(connector)?;
+ let connector_selector = u8::try_from(connector).map_err(|_| EINVAL)?;
+ let geometry = self.geometry();
+ let stream = geometry.stream_id(connector_selector);
+ let config = crate::video_arm::build_config(
+ self.code_tables(),
+ &header,
+ &[],
+ self.connector_ten_bit(connector),
+ )?;
+ let seq = self.take_seal_seq(connector, config.len().div_ceil(16) as u32);
+ crate::cp::seal_video_arm(&vkey, &vnonce, stream, 0x0000, seq, &config)
+ }
+ /// Build the message a connector's video stream opens with, sent alone ahead of everything
+ /// else.
+ ///
+ /// Ridge has none: its ARM burst opens the stream from within the first frame's transfer.
+ ///
+ /// Navarro has none either, for a connector it is about to drive. The short sealed open does
+ /// exist on this dock, but both DLM captures send it only on the stream ids of the connectors
+ /// with no monitor -- `0x17` and `0x1f` while pixels went to connectors 0 and 1 -- each as the
+ /// first and only record on its own stream, sealed with that connector's own key at block 0. A
+ /// driven connector's sealed chain instead opens with the pipe descriptor at block 0.
+ ///
+ /// It must not go out on `stream_id | 0x10`, which for connector 0 is connector 2's stream id,
+ /// sealed with connector 0's key. That both signed another connector's stream with the wrong
+ /// key and, because the prologue then also started at block 0, used the connector's first
+ /// keystream block twice.
+ fn build_stream_open_buf(&self, connector: usize) -> Result<Option<KVec<u8>>> {
+ // A dock that carries video on the control pipe has already opened its streams: the same
+ // record went out inside the CP setup burst, where it can be ordered against the rest of
+ // setup. Sending a second one here would seal a block the dock has already accounted for.
+ if self.uses_arm_burst() || self.video_on_ctrl_pipe() {
+ return Ok(None);
+ }
+ let (vkey, vnonce) = self.video_seal_key(connector)?;
+ let content = crate::cp::navarro_stream_open();
+ let stream = self.geometry().stream_id(connector as u8);
+ let seq = self.take_seal_seq(connector, content.len().div_ceil(16) as u32);
+ Ok(Some(crate::cp::seal_video_arm(
+ &vkey, &vnonce, stream, 0x0002, seq, &content,
+ )?))
+ }
+ /// Build the sealed report a connector owes its stream for one frame.
+ ///
+ /// DLM pairs every frame on the frame sub with one of these on the stream sub, so a stream
+ /// that sends pixels and then falls silent on its stream sub is a stream the dock stops
+ /// believing in. Returns `None` on a dock whose frames carry no such record.
+ ///
+ /// The ordinary `aux=0x000c` form is what DLM sends for all but a handful of frames; the
+ /// `aux=0x0002` form restates the mode and goes out with the frame that carries the prologue,
+ /// which is the frame right after a mode set.
+ pub(super) fn build_stream_report_buf(
+ &self,
+ connector: usize,
+ frame: u32,
+ ) -> Result<Option<KVec<u8>>> {
+ if self.uses_arm_burst() {
+ return Ok(None);
+ }
+ let owed = self.stream_reports_owed.get(connector).ok_or(EINVAL)?;
+ if self.video_on_ctrl_pipe()
+ && (frame < STREAM_REPORT_FRAME
+ || owed
+ .fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| n.checked_sub(1))
+ .is_err())
+ {
+ return Ok(None);
+ }
+ let (vkey, vnonce) = self.video_seal_key(connector)?;
+ let stream = self.geometry().stream_id(connector as u8);
+ let with_mode = self.video_on_ctrl_pipe()
+ || self.arm_prefix_pending.load(Ordering::Acquire) & (1u32 << connector) != 0;
+ let (aux, content): (u16, KVec<u8>) = if with_mode {
+ let header = self.stream_mode_header(connector)?;
+ let mut v = KVec::new();
+ if self.video_on_ctrl_pipe() {
+ v.extend_from_slice(&crate::cp::stream_report_mode_only(&header), GFP_KERNEL)?;
+ (0x0006, v)
+ } else {
+ v.extend_from_slice(&crate::cp::navarro_stream_report_mode(&header), GFP_KERNEL)?;
+ (0x0002, v)
+ }
+ } else {
+ let mut v = KVec::new();
+ v.extend_from_slice(&crate::cp::navarro_stream_report(), GFP_KERNEL)?;
+ (0x000c, v)
+ };
+ let seq = self.take_seal_seq(connector, content.len().div_ceil(16) as u32);
+ Ok(Some(crate::cp::seal_video_arm(
+ &vkey, &vnonce, stream, aux, seq, &content,
+ )?))
+ }
+ /// Build the DL7400 records that precede a connector's first frame.
+ ///
+ /// In wire order: two plaintext stream markers, the sealed pipe descriptor, a plaintext frame
+ /// marker, an unsealed record naming the connector's first and fifth ring addresses, and the
+ /// sealed decoder configuration. Both sealed records draw from the stream's running block
+ /// counter, so on a first arm the descriptor seals at block 0 and the configuration at block
+ /// 19 -- the descriptor's 304 bytes in blocks -- exactly as DLM's `0 -> 19 -> 88` chain does.
+ fn build_navarro_prologue_buf(&self, connector: usize) -> Result<KVec<u8>> {
+ let (vkey, vnonce) = self.video_seal_key(connector)?;
+ let connector_selector = connector as u8;
+ let geometry = self.geometry();
+ let stream = geometry.stream_id(connector_selector);
+ let frame_sub = u16::from(geometry.connector_selector(connector_selector));
+
+ let mut buf = KVec::with_capacity(1600, GFP_KERNEL)?;
+ for sub in [stream, stream | 0x0010] {
+ buf.extend_from_slice(
+ &crate::cp::stream_announce(sub, crate::cp::STREAM_ANNOUNCE_MARKER),
+ GFP_KERNEL,
+ )?;
+ }
+
+ let descriptor = crate::cp::navarro_pipe_descriptor(connector_selector)?;
+ let seal_seq = self.take_seal_seq(connector, descriptor.len().div_ceil(16) as u32);
+ let sealed =
+ crate::cp::seal_video_arm(&vkey, &vnonce, stream, 0x0000, seal_seq, &descriptor)?;
+ buf.extend_from_slice(&sealed, GFP_KERNEL)?;
+
+ buf.extend_from_slice(&crate::cp::stream_announce(frame_sub, 0), GFP_KERNEL)?;
+
+ // Unsealed type-4 record: the connector_selector's first and fifth ring addresses.
+ let mut ring = [0u8; 32];
+ ring[2..4].copy_from_slice(&0x001cu16.to_le_bytes());
+ ring[4..8].copy_from_slice(&4u32.to_le_bytes());
+ ring[8..10].copy_from_slice(&frame_sub.to_le_bytes());
+ ring[10..12].copy_from_slice(&0x0004u16.to_le_bytes());
+ ring[16..19].copy_from_slice(&[0x0a, 0x00, 0x04]);
+ ring[19] = frame_sub as u8;
+ ring[22..26]
+ .copy_from_slice(&crate::cp::navarro_pipe_ring(connector_selector, 0).to_le_bytes());
+ ring[26..30]
+ .copy_from_slice(&crate::cp::navarro_pipe_ring(connector_selector, 4).to_le_bytes());
+ buf.extend_from_slice(&ring, GFP_KERNEL)?;
+
+ let mut tail = [0u8; 14];
+ crate::rng::fill(&mut tail);
+ let config = crate::video_arm::build_config(
+ self.code_tables(),
+ &self.stream_mode_header(connector)?,
+ &tail,
+ self.connector_ten_bit(connector),
+ )?;
+ let seal_seq = self.take_seal_seq(connector, config.len().div_ceil(16) as u32);
+ buf.extend_from_slice(
+ &crate::cp::seal_video_arm(&vkey, &vnonce, stream, 0x000e, seal_seq, &config)?,
+ GFP_KERNEL,
+ )?;
+ Ok(buf)
+ }
+ fn build_arm_burst_buf(&self, connector: usize) -> Result<KVec<u8>> {
+ let (vkey, vnonce) = self.video_seal_key(connector)?;
+ let h = connector as u16;
+ // The sealed records share the video channel's running block counter:
+ // #2 seq0(+1), #3 seq1(+1), #8 seq2(+69), and #9 seq71.
+ let mut seal_seq: u32 = 0;
+ let mut buf = KVec::with_capacity(2560, GFP_KERNEL)?;
+ for (i, &(_wire_type, sub_base, aux, body_len)) in
+ crate::cp::VIDEO_ARM_BURST.iter().enumerate()
+ {
+ let sub = sub_base.wrapping_add(h);
+ match i {
+ 2 | 3 => {
+ // Sealed under the per-connector video key/nonce. Content = the six-byte stream
+ // marker + 10 host-random bytes; seq is the shared block counter (16 B = 1
+ // block each).
+ let content = crate::cp::stream_open(self.stream_marker_kind());
+ let frame =
+ crate::cp::seal_video_arm(&vkey, &vnonce, sub, aux, seal_seq, &content)?;
+ seal_seq += 1;
+ buf.extend_from_slice(&frame, GFP_KERNEL)?;
+ }
+ 6 | 7 => {
+ // type=4 but FIXED plaintext (not encrypted, no MAC): a 32-byte frame whose
+ // Its 16-byte body is fixed, with 0x10 at byte 11.
+ let mut f = [0u8; 32];
+ f[2..4].copy_from_slice(&0x001cu16.to_le_bytes());
+ f[4..8].copy_from_slice(&4u32.to_le_bytes());
+ f[8..10].copy_from_slice(&sub.to_le_bytes());
+ f[10..12].copy_from_slice(&aux.to_le_bytes());
+ f[16..32].copy_from_slice(&[
+ 0x0a, 0x00, 0x04, 0x00, 0, 0, 0, 0, 0, 0, 0, 0x10, 0, 0, 0, 0,
+ ]);
+ buf.extend_from_slice(&f, GFP_KERNEL)?;
+ }
+ 8 | 9 => {
+ debug_assert_eq!(body_len, 1104);
+ let mut nonce = [0u8; 14];
+ crate::rng::fill(&mut nonce);
+ let content = crate::video_arm::build_config(
+ self.code_tables(),
+ &self.stream_mode_header(connector)?,
+ &nonce,
+ self.connector_ten_bit(connector),
+ )?;
+ debug_assert_eq!(content.len(), body_len);
+ let frame =
+ crate::cp::seal_video_arm(&vkey, &vnonce, sub, aux, seal_seq, &content)?;
+ seal_seq += (body_len / 16) as u32;
+ buf.extend_from_slice(&frame, GFP_KERNEL)?;
+ }
+ _ => {
+ // wire_type==2 plaintext records (#0/#1/#4/#5).
+ let body = crate::cp::video_arm_plaintext_body(i, h);
+ let frame = crate::cp::video_arm_plain_frame(sub, &body);
+ buf.extend_from_slice(&frame, GFP_KERNEL)?;
+ }
+ }
+ }
+ Ok(buf)
+ }
+}
diff --git a/drivers/gpu/drm/vino/drm_sink/timeline.rs b/drivers/gpu/drm/vino/drm_sink/timeline.rs
new file mode 100644
index 000000000000..d6db1a2eb0e8
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink/timeline.rs
@@ -0,0 +1,571 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Measured bring-up and mode-set timelines.
+//!
+//! A cold dock will not light from a correct message sequence alone; the vendor's spacing is
+//! part of the contract, and a dock driven faster than this stays dark or resets. Every delay
+//! here was read off a capture of the vendor driver waking the same hardware.
+
+/// Dual-connector cold-wake timeline relative to the connector-0 mode set.
+///
+/// EP02 must remain quiet between `H1_MODE` and `QUIET_END`; the dock also requires a connector-1
+/// EDID probe and fetch before video starts.
+pub(crate) mod cold {
+ pub(crate) const H1_MODE: i64 = 29;
+ /// End of the silent window. Nothing may be sent on EP02 between `H1_MODE` and here.
+ pub(crate) const QUIET_END: i64 = 1016;
+ pub(crate) const H0_VIDEO: i64 = 1159;
+ pub(crate) const H1_VIDEO: i64 = 1233;
+ /// `(offset_ms, connector, sub, state)` stream markers. `sub` 0x2f/0x2e as on the wire.
+ pub(crate) const MARKERS: &[(i64, u8, u16, u8)] = &[
+ (17, 0, 0x2f, 1),
+ (21, 0, 0x2e, 3),
+ (1016, 0, 0x2f, 1),
+ (1021, 1, 0x2f, 1),
+ (1023, 0, 0x2e, 3),
+ (1029, 1, 0x2e, 3),
+ (1056, 0, 0x2f, 1),
+ (1057, 1, 0x2f, 1),
+ (1064, 0, 0x2e, 0),
+ (1124, 1, 0x2e, 3),
+ (1132, 1, 0x2f, 1),
+ (1135, 1, 0x2e, 0),
+ (1195, 0, 0x2f, 0),
+ (1208, 0, 0x2e, 0),
+ (1220, 1, 0x2f, 1),
+ (1225, 1, 0x2e, 0),
+ (1295, 1, 0x2f, 0),
+ (1298, 1, 0x2e, 0),
+ ];
+ /// `id=0x14 sub=0x0c` status polls.
+ pub(crate) const POLLS: &[i64] = &[
+ 5, 26, 1019, 1130, 1192, 1204, 1222, 1235, 1253, 1270, 1287, 1304,
+ ];
+ /// `(offset_ms, connector, is_fetch)` -- `false` is the `0x15/0x20` probe, `true` the
+ /// `0x15/0x21` fetch.
+ pub(crate) const EDID: &[(i64, u8, bool)] = &[(1033, 1, false), (1059, 1, true)];
+ /// Keep both carriers active until downstream clock programming completes.
+ pub(crate) const CARRIER_TAIL_MS: i64 = 800;
+}
+
+/// A dock's cold bring-up choreography, anchored on the first connector's mode set.
+///
+/// Ridge and Navarro differ in more than timing: Navarro opens each connector's bracket with a
+/// state-0 pair before the state-1/3 pair, spaces its two mode sets 757 ms apart rather than 29 ms,
+/// sets connector 0's mode a *second* time shortly before connector 0's video, and streams
+/// connector 1 first. Replaying one dock's timeline at the other leaves the endpoint unarmed. Every
+/// connector field below is a transcript slot, not a connector number: 0 is the first connector an
+/// activation brings up and 1 the second, whichever sockets they occupy. Both timelines were
+/// recorded with the panels in the first two sockets, where the two happen to coincide.
+/// [`VinoDrmData::activate_dual_wake`] resolves them.
+pub(super) struct ColdTimeline {
+ /// Offset of the second slot's mode set.
+ pub(crate) h1_mode: i64,
+ /// End of any silent window on EP02 after the second mode set.
+ pub(crate) quiet_end: i64,
+ /// Slots in the order they start streaming, with the offset each starts at.
+ pub(crate) video: &'static [(usize, i64)],
+ /// Mode sets repeated after the initial pair, as `(offset, slot)`.
+ pub(crate) remode: &'static [(i64, usize)],
+ /// `(offset_ms, slot, sub, state)` stream markers. `sub` 0x2f/0x2e as on the wire.
+ pub(crate) markers: &'static [(i64, u8, u16, u8)],
+ /// `id=0x14 sub=0x0c` status polls.
+ pub(crate) polls: &'static [i64],
+ /// `(offset_ms, slot, is_fetch)` EDID re-reads inside the bracket.
+ pub(crate) edid: &'static [(i64, u8, bool)],
+}
+
+/// Ridge's timeline, as replayed from a D6000 cold bring-up.
+pub(super) static COLD_RIDGE: ColdTimeline = ColdTimeline {
+ h1_mode: cold::H1_MODE,
+ quiet_end: cold::QUIET_END,
+ video: &[(0, cold::H0_VIDEO), (1, cold::H1_VIDEO)],
+ remode: &[],
+ markers: cold::MARKERS,
+ polls: cold::POLLS,
+ edid: cold::EDID,
+};
+
+/// Navarro's timeline, measured from a DLM cold bring-up and anchored on connector 0's real
+/// (`off23 = 2`) mode set, exactly as Ridge's is.
+pub(super) static COLD_NAVARRO: ColdTimeline = ColdTimeline {
+ h1_mode: 10,
+ // DLM polls continuously across this span; there is no silent window to preserve.
+ quiet_end: 11,
+ // Video is not a pair of one-shot events: DLM keeps connector 0's carrier alive throughout the
+ // still-open control bracket, starts connector 1, and continues both through the closing
+ // markers. A gap here makes the dock accept one frame and NAK the next forever. The activation
+ // path uses its pre-encoded carrier; normal scanout replaces it as soon as activation returns.
+ video: &[
+ (0, 122),
+ (0, 124),
+ (0, 134),
+ (0, 171),
+ (0, 192),
+ (0, 199),
+ (0, 235),
+ (0, 252),
+ (1, 272),
+ (0, 277),
+ (1, 293),
+ (1, 303),
+ ],
+ remode: &[],
+ markers: &[
+ (7, 0, 0x2f, 1),
+ (13, 0, 0x2e, 3),
+ (20, 1, 0x2f, 1),
+ (21, 0, 0x2f, 1),
+ (35, 0, 0x2e, 0),
+ (76, 1, 0x2e, 3),
+ (104, 1, 0x2f, 1),
+ (128, 1, 0x2e, 3),
+ (131, 0, 0x2f, 1),
+ (136, 0, 0x2e, 0),
+ (168, 1, 0x2f, 1),
+ (181, 1, 0x2e, 0),
+ (228, 0, 0x2f, 0),
+ (230, 0, 0x2e, 0),
+ (303, 1, 0x2f, 0),
+ (304, 1, 0x2e, 0),
+ ],
+ polls: &[17, 78, 120, 162, 179, 223, 267, 295, 297, 297],
+ // Navarro reads every connector's EDID before the anchor, not inside the bracket.
+ edid: &[],
+};
+
+/// One step of a DL-3x00 dock-wide activation.
+///
+/// Every connector field is an activation slot, not a connector number: slot 0 is the
+/// lowest-numbered activating connector and slot 1 the next, resolved to real connectors at the
+/// point of send, exactly as [`ColdTimeline`]'s are. Steps carry no offsets because the vendor's
+/// are separated by its own frames rather than by a clock; [`DockWideStep::Stream`] states the ones
+/// that matter.
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub(crate) enum DockWideStep {
+ /// `0x48/0x22`, this slot's mode.
+ SetMode(u8),
+ /// `0x16/0x2e` or `0x16/0x2f`, with the state in byte 23.
+ Marker(u8, u16, u8),
+ /// `id=0x14 sub=0x0c` device status.
+ Poll,
+ /// The ring descriptor and decoder configuration that open this slot's stream as one unit.
+ ///
+ /// Kept for the conservative runtime re-arm. The cold Ella transcript separates these two
+ /// records with control traffic and uses [`DockWideStep::Ring`] / [`DockWideStep::Config`]
+ /// instead, so the table can state those producer boundaries exactly.
+ Prologue(u8),
+ /// The unsealed ring descriptor that starts this slot's stream generation.
+ Ring(u8),
+ /// The sealed decoder configuration that follows a slot's ring descriptor.
+ Config(u8),
+ /// This slot's activation carrier -- the flat surface its stream opens with.
+ Carrier(u8),
+ /// Frames the vendor presents on `slot` before its next control record.
+ ///
+ /// The second connector comes up behind a running stream, and this is that stream. A dock
+ /// whose activation is spaced by wall clock rather than by frames has nothing to put here.
+ Stream(u8, u32),
+}
+
+/// How the vendor brings both connectors of a DL-3x00 dock up, measured record for record.
+///
+/// Two rules distinguish it from a driver that simply repeats a per-connector bracket, and both are
+/// load-bearing. A single dock-wide transaction configures *both* connectors before any pixels:
+/// the mode sets are adjacent, and every marker between them addresses the first connector. And
+/// the second connector's sink is held down (`0x2e` state 3) across that transaction, coming up
+/// only once the first is streaming -- with no mode set of its own, because it already has one.
+///
+/// A second independent bracket instead is what the dock stops answering: it acknowledges
+/// everything up to the second bracket's first marker and nothing afterwards, which reads as a
+/// dead dock and ends with every scanout returning `ENODEV`.
+pub(crate) static ELLA_DOCK_WIDE: &[DockWideStep] = &[
+ DockWideStep::SetMode(0),
+ DockWideStep::Marker(0, 0x2f, 1),
+ DockWideStep::Marker(0, 0x2e, 3),
+ DockWideStep::SetMode(1),
+ DockWideStep::Marker(0, 0x2f, 1),
+ DockWideStep::Marker(0, 0x2e, 0),
+ DockWideStep::Marker(1, 0x2f, 1),
+ DockWideStep::Ring(0),
+ DockWideStep::Poll,
+ DockWideStep::Config(0),
+ DockWideStep::Marker(1, 0x2e, 3),
+ DockWideStep::Marker(0, 0x2f, 1),
+ DockWideStep::Marker(0, 0x2e, 0),
+ DockWideStep::Carrier(0),
+ DockWideStep::Stream(0, 2),
+ DockWideStep::Marker(1, 0x2f, 1),
+ DockWideStep::Marker(0, 0x2f, 0),
+ DockWideStep::Stream(0, 2),
+ DockWideStep::Marker(1, 0x2e, 0),
+ DockWideStep::Stream(0, 3),
+ DockWideStep::Poll,
+ DockWideStep::Ring(1),
+ DockWideStep::Marker(0, 0x2e, 0),
+ DockWideStep::Stream(0, 1),
+ DockWideStep::Config(1),
+ DockWideStep::Poll,
+ DockWideStep::Carrier(1),
+ // The second connector's sink is completed the same way the first one's was, and only once its
+ // own carrier is running: assert, bring up, release, bring up again. The first connector
+ // receives exactly this four-marker tail earlier in the transaction, and leaving it off the
+ // second one ends the transaction with that sink half raised.
+ DockWideStep::Marker(1, 0x2f, 1),
+ DockWideStep::Stream(1, 2),
+ DockWideStep::Poll,
+ DockWideStep::Marker(1, 0x2e, 0),
+ DockWideStep::Poll,
+ DockWideStep::Marker(1, 0x2f, 0),
+ DockWideStep::Poll,
+ DockWideStep::Marker(1, 0x2e, 0),
+];
+
+/// How the vendor reconfigures one connector of a DL-3x00 dock while the other one is lit.
+///
+/// This is not the dock-wide sequence with a connector left out. It is shorter, it takes the sink
+/// down *before* the mode set rather than after it, and it touches nothing belonging to the other
+/// connector -- which keeps streaming across the whole of it. Replaying the cold bracket here
+/// instead is what silences the dock: it stops answering at the first marker and does not answer
+/// again.
+///
+/// Measured twice, at two different resolutions, with identical shape both times.
+///
+/// The vendor also re-reads the sink's EDID between the sink-down and the mode set. That is left
+/// out: vino reads EDID on its own schedule, and a fetch issued inside a transaction has nowhere
+/// to deliver its reply.
+pub(crate) static ELLA_RUNTIME_MODE: &[DockWideStep] = &[
+ DockWideStep::Marker(0, 0x2f, 1),
+ DockWideStep::Marker(0, 0x2e, 3),
+ DockWideStep::SetMode(0),
+ DockWideStep::Marker(0, 0x2f, 1),
+ DockWideStep::Marker(0, 0x2e, 0),
+ DockWideStep::Poll,
+ DockWideStep::Prologue(0),
+ DockWideStep::Carrier(0),
+];
+
+/// Reservation-token slots for [`COLD_NAVARRO::markers`] and [`COLD_NAVARRO::polls`].
+///
+/// DLM assigns counters in its per-connector workers before their EP02 writes interleave. Wire AES
+/// sequence remains monotonic, but the echoed inner counters consequently do not: for example the
+/// wire order begins `n, n+1, n+3, n+2, n+5, n+4`. Navarro starts NAKing at the first flattened
+/// counter, so retain that allocation order. These numbers index live reservation tokens; they
+/// are not protocol counters and are never added to a captured/base counter.
+pub(super) static NAVARRO_MARKER_COUNTER_SLOTS: &[usize] =
+ &[1, 2, 4, 6, 8, 7, 10, 12, 11, 14, 16, 17, 20, 21, 26, 27];
+pub(super) static NAVARRO_POLL_COUNTER_SLOTS: &[usize] = &[5, 9, 13, 15, 18, 19, 22, 23, 24, 25];
+pub(super) const NAVARRO_COLD_COUNTERS: usize = 28;
+
+/// One operation in Navarro's cold sink-reset prelude. This is separate from [`ColdTimeline`]: it
+/// runs before the first real mode set and changes downstream EDID/sink state, whereas
+/// `ColdTimeline` brackets already-programmed streams.
+#[derive(Clone, Copy)]
+pub(super) enum NavarroColdOp {
+ Poll,
+ EdidState(u8, u8),
+ Probe(u8),
+ Fetch(u8),
+ /// Tear the downstream sink down. Offset 23 is the literal state `0xff`, not a connector.
+ SinkTeardown(u8),
+ /// Engage the downstream sink.
+ ///
+ /// `id=0x16 sub=0x23` names the connector twice, at offset 22 and at offset 23, which is why it
+ /// is [`crate::cp::edid_sink_state`]`(connector, connector)`. It is a distinct variant rather
+ /// than a state so that the remap below cannot mistake the second selector for a constant: the
+ /// dock acknowledges a mismatched pair and then never enables the sink.
+ Engage(u8),
+ PostEdid(u8),
+ Clear(u8),
+}
+
+impl NavarroColdOp {
+ /// Translate this op's transcript slot into the connector that slot stands for in this
+ /// activation.
+ ///
+ /// The captured sequence names connectors 0 and 1 because that is where DLM's panels were. It
+ /// describes the first and second connector being brought up, whichever sockets those are.
+ pub(crate) fn remap_head(self, remap: &impl Fn(u8) -> u8) -> Self {
+ match self {
+ Self::Poll => Self::Poll,
+ Self::EdidState(h, s) => Self::EdidState(remap(h), s),
+ Self::Probe(h) => Self::Probe(remap(h)),
+ Self::Fetch(h) => Self::Fetch(remap(h)),
+ Self::SinkTeardown(h) => Self::SinkTeardown(remap(h)),
+ Self::Engage(h) => Self::Engage(remap(h)),
+ Self::PostEdid(h) => Self::PostEdid(remap(h)),
+ Self::Clear(h) => Self::Clear(remap(h)),
+ }
+ }
+}
+
+/// Authenticated DLM transaction between Navarro's first clear pair and its first real mode.
+///
+/// Offsets are milliseconds from the first connector-0 clear in
+/// `navarro-dlm-today-124144/wire.pcapng`. Equal offsets deliberately retain wire order. Most
+/// importantly, DLM stops both EDID readers and sends sink state `0xff` immediately after the
+/// first clears, then re-reads and re-engages each sink before its second clear. Omitting this
+/// whole state transition left the video endpoints accepting one bulk transfer and NAKing every
+/// subsequent transfer.
+pub(super) static NAVARRO_COLD_PRELUDE: &[(i64, NavarroColdOp)] = &[
+ (2, NavarroColdOp::EdidState(0, 0)),
+ (3, NavarroColdOp::Probe(0)),
+ (5, NavarroColdOp::EdidState(1, 0)),
+ (5, NavarroColdOp::Probe(1)),
+ (7, NavarroColdOp::SinkTeardown(0)),
+ (7, NavarroColdOp::SinkTeardown(1)),
+ (8, NavarroColdOp::Poll),
+ (30, NavarroColdOp::Poll),
+ (50, NavarroColdOp::Poll),
+ (69, NavarroColdOp::Poll),
+ (87, NavarroColdOp::Poll),
+ (105, NavarroColdOp::Poll),
+ (123, NavarroColdOp::Poll),
+ (143, NavarroColdOp::Poll),
+ (162, NavarroColdOp::Poll),
+ (181, NavarroColdOp::Poll),
+ (201, NavarroColdOp::Poll),
+ (219, NavarroColdOp::Poll),
+ (237, NavarroColdOp::Poll),
+ (255, NavarroColdOp::Poll),
+ (273, NavarroColdOp::Poll),
+ (293, NavarroColdOp::Poll),
+ (312, NavarroColdOp::Poll),
+ (328, NavarroColdOp::Poll),
+ (329, NavarroColdOp::Poll),
+ (329, NavarroColdOp::Poll),
+ (329, NavarroColdOp::Poll),
+ (330, NavarroColdOp::Poll),
+ (330, NavarroColdOp::Poll),
+ (1216, NavarroColdOp::Poll),
+ (1233, NavarroColdOp::Poll),
+ (1315, NavarroColdOp::Poll),
+ (1650, NavarroColdOp::Poll),
+ (1667, NavarroColdOp::Poll),
+ (1750, NavarroColdOp::Poll),
+ (1755, NavarroColdOp::Probe(0)),
+ (1757, NavarroColdOp::EdidState(0, 1)),
+ (1758, NavarroColdOp::Probe(0)),
+ (1805, NavarroColdOp::Fetch(0)),
+ (1810, NavarroColdOp::Engage(0)),
+ (1822, NavarroColdOp::Clear(0)),
+ (1827, NavarroColdOp::Poll),
+ (1850, NavarroColdOp::Probe(1)),
+ (1853, NavarroColdOp::EdidState(1, 1)),
+ (1856, NavarroColdOp::Probe(1)),
+ (1902, NavarroColdOp::PostEdid(0)),
+ (1903, NavarroColdOp::Fetch(1)),
+ (1907, NavarroColdOp::Engage(1)),
+ (1930, NavarroColdOp::Clear(1)),
+ (1934, NavarroColdOp::Poll),
+ (1956, NavarroColdOp::Poll),
+ (1975, NavarroColdOp::Poll),
+ (1994, NavarroColdOp::Poll),
+ (2003, NavarroColdOp::Poll),
+ (2005, NavarroColdOp::Poll),
+ (2007, NavarroColdOp::PostEdid(1)),
+ (2016, NavarroColdOp::Poll),
+];
+
+/// Navarro tears both pipe descriptors down first, then executes
+/// [`NAVARRO_COLD_PRELUDE`] before programming the first real mode.
+pub(super) const NAVARRO_PRIME_CLEAR_H1_MS: i64 = 2;
+
+/// One status-poll interval, the gap the prelude's own trailing polls run at.
+pub(super) const NAVARRO_REAL_MODE_SETTLE_MS: i64 = 20;
+
+/// When the cold prelude's last operation is due.
+pub(crate) const NAVARRO_COLD_PRELUDE_END_MS: i64 =
+ NAVARRO_COLD_PRELUDE[NAVARRO_COLD_PRELUDE.len() - 1].0;
+
+/// Where the first real mode goes, measured from the start of activation.
+///
+/// DLM's capture puts it at 2978 ms, but DLM's own prelude finishes at 2016 ms; the ~960 ms
+/// between is dead air in that capture rather than something the dock asked for, and it is over
+/// half a 5.59 s enumerate-to-pixels bring-up. Follow the end of the prelude plus one poll
+/// interval, so the two cannot drift apart when the prelude changes.
+pub(crate) const NAVARRO_REAL_MODE_H0_MS: i64 =
+ NAVARRO_COLD_PRELUDE_END_MS + NAVARRO_REAL_MODE_SETTLE_MS;
+
+/// How long the KMS worker waits for the rest of a multi-connector atomic commit's mode sets.
+///
+/// Bounded so a genuine single-connector commit costs at most this before proceeding.
+pub(super) const MODESET_BATCH_SETTLE_MS: i64 = 20;
+
+/// Number of back-to-back presentations of one already-encoded full frame while a newly-mode-set
+/// downstream is training.
+pub(crate) const COLD_TRAINING_PRESENTATIONS: u32 = 8;
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+use kernel::prelude::kunit_tests;
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_timeline)]
+mod tests {
+ use super::*;
+ use crate::*;
+
+ /// A mode is bounded by link rate, not by refresh, except where the dock itself clamps.
+ ///
+ /// The first real mode follows the cold prelude; it does not sit at a copied capture offset.
+ ///
+ /// DLM's capture put its first real mode at 2978 ms while its own prelude finished at
+ /// 2016 ms. Deriving the offset keeps the two from drifting apart, and this pins the
+ /// direction: the mode must come *after* the prelude's last op, and must not reintroduce the
+ /// second of dead air that made it 53% of a bring-up.
+ #[test]
+ fn the_first_real_mode_follows_the_prelude_rather_than_the_capture() {
+ let last = NAVARRO_COLD_PRELUDE_END_MS;
+ assert_eq!(last, 2016);
+ assert!(NAVARRO_REAL_MODE_H0_MS > last);
+ assert!(NAVARRO_REAL_MODE_H0_MS - last <= 100);
+ }
+
+ /// The DL-3x00 dock-wide activation, step for step against the vendor's record stream.
+ ///
+ /// The order below is transcribed from a decrypted DLM bring-up; the vendor's frames between
+ /// its control records are what the settles stand in for. Two properties of it are what the
+ /// dock actually enforces, and both are checked separately afterwards, because a plausible
+ /// reordering breaks them without changing any single step.
+ #[test]
+ fn ella_dock_wide_matches_the_dlm_capture() -> Result {
+ use super::DockWideStep::*;
+ let want = [
+ SetMode(0), // vendor #86
+ Marker(0, 0x2f, 1), // #87
+ Marker(0, 0x2e, 3), // #88
+ SetMode(1), // #89, inside the first connector's bracket
+ Marker(0, 0x2f, 1), // #90
+ Marker(0, 0x2e, 0), // #91
+ Marker(1, 0x2f, 1), // #92
+ Ring(0), // #93 ring descriptor
+ Poll, // #94, between the two records of the prologue
+ Config(0), // #95 decoder configuration
+ Marker(1, 0x2e, 3), // #96, the second connector's sink held down
+ Marker(0, 0x2f, 1), // #97
+ Marker(0, 0x2e, 0), // #98, the last record before pixels
+ Carrier(0), // #99
+ Stream(0, 2), // #99..#227, the first connector streaming
+ Marker(1, 0x2f, 1), // #229
+ Marker(0, 0x2f, 0), // #247
+ Stream(0, 2), // #248..#429
+ Marker(1, 0x2e, 0), // #431, the second connector's sink up behind a running stream
+ Stream(0, 3), // #432..#629
+ Poll, // #632
+ Ring(1), // #633 ring descriptor, ahead of the marker
+ Marker(0, 0x2e, 0), // #634
+ Stream(0, 1), // one first-connector frame, #635..#732
+ Config(1), // #733 decoder configuration
+ Poll, // #734, the last record before the second connector's pixels
+ Carrier(1), // #735
+ // The second connector's sink is completed exactly as the first one's was, and only
+ // behind its own running carrier. Leaving this off ends the transaction with that
+ // sink half raised.
+ Marker(1, 0x2f, 1), // #737
+ Stream(1, 2), // #738..#742, two more carrier frames
+ Poll, // #743
+ Marker(1, 0x2e, 0), // #744
+ Poll, // #745
+ Marker(1, 0x2f, 0), // #746
+ Poll, // #747
+ Marker(1, 0x2e, 0), // #748
+ ];
+ assert_eq!(ELLA_DOCK_WIDE, &want[..]);
+ Ok(())
+ }
+
+ /// Both connectors are configured before either sends a pixel, and only one mode set each.
+ ///
+ /// A second bracket around a second mode set is what the dock stops answering: it acknowledges
+ /// every record up to that bracket's first marker and nothing afterwards.
+ #[test]
+ fn ella_dock_wide_sets_both_modes_before_any_pixels() -> Result {
+ use super::DockWideStep::*;
+ let mut modes = [0u32; 2];
+ for step in ELLA_DOCK_WIDE {
+ match *step {
+ SetMode(slot) => modes[usize::from(slot)] += 1,
+ // Every carrier finds both connectors already configured, and each exactly once.
+ Carrier(_) => assert_eq!(modes, [1, 1]),
+ _ => {}
+ }
+ }
+ assert_eq!(modes, [1, 1]);
+ Ok(())
+ }
+
+ /// The second connector's sink stays down across the first connector's carrier.
+ ///
+ /// The vendor takes it down inside the dock-wide bracket and brings it up only once the first
+ /// connector is streaming. A sink brought up early is a connector the dock is scanning out
+ /// while its stream has neither a ring descriptor nor a decoder configuration.
+ #[test]
+ fn ella_dock_wide_holds_the_second_sink_down_until_the_first_streams() -> Result {
+ use super::DockWideStep::*;
+ let mut down = false;
+ let mut streaming = false;
+ let mut up_after_streaming = false;
+ for step in ELLA_DOCK_WIDE {
+ match *step {
+ Marker(1, 0x2e, 3) => down = true,
+ Marker(1, 0x2e, 0) => {
+ // Up only after it was taken down, and only once the first is streaming.
+ assert!(down);
+ assert!(streaming);
+ up_after_streaming = true;
+ }
+ Carrier(0) => streaming = true,
+ // Neither the second stream's opening records nor its pixels reach a downed sink.
+ Carrier(1) => assert!(up_after_streaming),
+ Ring(1) | Config(1) | Prologue(1) => assert!(up_after_streaming),
+ _ => {}
+ }
+ }
+ assert!(up_after_streaming);
+ Ok(())
+ }
+
+ /// Reconfiguring one DL-3x00 connector while the other is lit, against the vendor's own.
+ ///
+ /// Transcribed from two vendor reconfigurations at different resolutions, whose records agree
+ /// step for step. The sink-down before the mode set is the part a reader is most likely to
+ /// think redundant with the cold table's, where it comes after.
+ #[test]
+ fn ella_runtime_mode_matches_the_dlm_capture() -> Result {
+ use super::DockWideStep::*;
+ let want = [
+ Marker(0, 0x2f, 1), // vendor #38348 / #44725
+ Marker(0, 0x2e, 3), // #38349 / #44726, sink down ahead of the mode set
+ SetMode(0), // #38353 / #44729
+ Marker(0, 0x2f, 1), // #38354 / #44730
+ Marker(0, 0x2e, 0), // #38355 / #44731, sink up
+ Poll, // #38356 / #44732
+ Prologue(0), // #38357 / #44733
+ Carrier(0), // #38358 / #44734 onward
+ ];
+ assert_eq!(ELLA_RUNTIME_MODE, &want[..]);
+ Ok(())
+ }
+
+ /// A runtime reconfiguration names one connector and never the other.
+ ///
+ /// The connector that is already lit keeps streaming across the whole sequence; a marker
+ /// addressed to it here would bracket a stream the dock is mid-frame on.
+ #[test]
+ fn ella_runtime_mode_touches_one_connector() -> Result {
+ use super::DockWideStep::*;
+ for step in ELLA_RUNTIME_MODE {
+ let slot = match *step {
+ SetMode(slot)
+ | Marker(slot, _, _)
+ | Prologue(slot)
+ | Ring(slot)
+ | Config(slot)
+ | Carrier(slot) => slot,
+ Poll | Stream(_, _) => continue,
+ };
+ assert_eq!(slot, 0);
+ }
+ Ok(())
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 9/13] drm/vino: add the dock activation and scanout path
2026-08-26 16:37 ` [PATCH v3 9/13] drm/vino: add the dock activation and scanout path Mike Lothian
@ 2026-08-26 16:59 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 16:59 UTC (permalink / raw)
To: Mike Lothian; +Cc: dri-devel, ojeda
Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] Sleeping functions are called in the atomic commit path (`atomic_update`), causing "scheduling while atomic" panics.
- [High] Global encode workqueue is never destroyed, leaking kernel threads on module unload.
- [High] Permanent endpoint wedging due to unhandled queue transmission errors during activation and keepalive.
- [High] Missing bracket unwind and `programmed_timing` cleanup on partial dual-wake failures.
- [High] Race condition in workqueue completion prevents `ShadowSurface` recycling, causing massive per-frame memory reallocations.
- [High] Global EDID target variables are mutated without synchronization, causing dropped monitors during concurrent modesets.
- [High] Lockless checks in `activate_head` allow concurrent execution, leading to hardware sequence corruption and AES desynchronization.
- [High] Global `video_staging` Mutex is held across synchronous USB operations, causing severe cross-monitor starvation.
- [High] Incorrect bitmask check for reflection flags treats reflected modes as identity, corrupting display updates.
- [Medium] Unhandled USB receive errors cause a 64ms CPU busy-wait loop in control message routines.
--
commit 75ffbefbbe278fc4d8fd7640be258d86e92c774e
Author: Mike Lothian <mike@fireburn.co.uk>
drm/vino: add the dock activation and scanout path
Add the dock-facing half of the sink, bringing up a connector's downstream
sink through a sequenced stream brackets, carrier frames, and mode sets.
> diff --git a/drivers/gpu/drm/vino/drm_sink/activation.rs b/drivers/gpu/drm/vino/drm_sink/activation.rs
> new file mode 100644
> index 0000000000000..e505d80ed80d8
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/activation.rs
[ ... ]
> + self.send_stream_open(dev, connector_index)?;
> + // A stream that is being opened here starts its ring and its frame counter from the
> + // beginning, whatever the connector reached before. Arming the prologue already resets
> + // this, but an activation that is retried arms once and presents several times, so a
> + // connector could open a stream and immediately tell the dock it was filling a later
> + // slot with a later frame number -- and the dock scans out a slot nothing wrote.
> + self.scanout_seq.lock()[connector_index] = 0;
> + }
> + let startup = arm.is_some();
> + let seq0 = self.scanout_seq.lock()[connector_index];
> + let started = Instant::<Monotonic>::now();
> + let mut repeat = 0u32;
> + // Presentations that named a ring slot, which is what the frame counter counts. See
> + // `names_ring_slot`.
> + let mut named = 0u32;
[Severity: High]
Does this code race to update the sequence numbers?
If two threads concurrently execute submit_prompt_training() due to the
lockless activate_head() invocation, won't they interleave CP control
messages and perform concurrent overlapping read-modify-write operations on
scanout_seq?
[ ... ]
> + let wire_len = arm_slice.len()
> + + opener_slice.len()
> + + report_slice.len()
> + + params_slice.len()
> + + image_len
> + + trailer.len();
> + {
> + // One writer owns a shared pipe for the whole frame; see `own_pipe`.
> + let _pipe = self.own_pipe();
> + let mut staging_slots = self.video_staging.lock();
> + let staging_slot = &mut staging_slots[connector_index];
> + if staging_slot.is_none() {
> + let mut staging = KVec::new();
> + staging.resize(xfer, 0, GFP_KERNEL)?;
> + *staging_slot = Some(staging);
> + }
> + let staging = staging_slot.as_mut().ok_or(kernel::error::code::ENOMEM)?;
> +
> + let mut queue_slot = self.video_q[pipe_i].lock();
[Severity: High]
Does holding this global Mutex stall all other monitors?
In submit_prompt_training(), the global video_staging.lock() is acquired to
obtain the staging buffer. But instead of extracting the buffer, the lock is
held for the duration of the while wire_off < wire_len loop below.
Since the loop synchronously calls queue.send() with a timeout, won't this
indefinitely block any other connector attempting to render a frame in
encode_and_send_haar() (which also requires video_staging.lock()) until
this sequence finishes?
[ ... ]
> + // DLM's mixed transport: prologue chunk zero is reaped below, then the rest
> + // and all ordinary frames are pipelined.
> + queue.send(dev.io(), dst, crate::timeout())?;
[Severity: High]
Could this leave the video queue stuck in an error state permanently?
If queue.send() returns a transient USB transport error during
submit_prompt_training(), the ? operator bubbles it up without calling
retire_failed_video_queue().
Unlike the main encode_and_send_haar() scanout path, this fails to clear the
USB halt condition, which wedges the endpoint for all subsequent frames.
[ ... ]
> + let (sent, started) = match timeline {
> + Ok(state) => state,
> + Err(e) => {
> + for connector in 0..MAX_CONNECTORS {
> + if sent & (1u32 << connector) != 0
> + && self.modeset_active[connector].load(Ordering::Acquire) == keys[connector]
> + {
> + self.modeset_active[connector].store(0, Ordering::Release);
> + }
> + }
> + // The choreography opens every activating connector's bracket well before it sets a
> + // mode, so unwind on `valid` rather than `sent`: a connector that failed before its
> + // mode set is still open on the dock.
> + for connector in 0..MAX_CONNECTORS {
> + if valid & (1u32 << connector) != 0 {
> + self.unwind_bracket(dev, connector as u8);
> + }
> + }
> + return Err(e);
[Severity: High]
Does this error path fail to clean up programmed_timing?
In activate_dual_wake(), when timeline returns an Err, the code correctly
calls unwind_bracket() but forgets to clear programmed_timing before unwinding.
Could this desynchronize the link state?
> + }
> + };
> + if sent.count_ones() < 2 {
> + for connector in 0..MAX_CONNECTORS {
> + if sent & (1u32 << connector) != 0
> + && self.modeset_active[connector].load(Ordering::Acquire) == keys[connector]
> + {
> + self.modeset_active[connector].store(0, Ordering::Release);
> + }
> + }
> + return Ok(false);
> + }
[Severity: High]
Is this missing a bracket unwind and programmed_timing cleanup?
When sent.count_ones() < 2 (e.g. one connector failed earlier), the code
resets modeset_active and returns Ok(false). However, unlike activate_dock_wide
and the Err(e) branch above, it doesn't call unwind_bracket(dev, connector) or
clear programmed_timing.
Does this leave the dock hardware in a partially open bracket state?
> diff --git a/drivers/gpu/drm/vino/drm_sink/cp_session.rs b/drivers/gpu/drm/vino/drm_sink/cp_session.rs
> new file mode 100644
> index 0000000000000..36b3a46026a55
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/cp_session.rs
[ ... ]
> + let mut reply = KVec::from_elem(0u8, 4096, GFP_KERNEL)?;
> + let deadline = Instant::<Monotonic>::now() + Delta::from_millis(64);
> + let mut matched = 0usize;
> + let (mut reaped, mut undecodable) = (0u32, 0u32);
> + let (mut seen_id, mut seen_sub, mut seen_counter) = (0u16, 0u16, 0u16);
> + loop {
> + let got = if let Some(q) = link.ep84_q.as_mut() {
> + match q.recv(dev.io(), &mut reply, crate::cp_reply_timeout()) {
> + Ok(Some(n)) => n,
> + Ok(None) => 0,
> + Err(_) => break,
> + }
> + } else {
> + dev.ctrl_recv(&mut reply, crate::cp_reply_timeout(), GFP_KERNEL)
> + .unwrap_or(0)
> + };
[Severity: Medium]
Will this loop spin indefinitely on USB receive errors?
In send_cp_reply(), if dev.ctrl_recv() encounters a transport error, it is
masked by unwrap_or(0). This results in got = 0, which bypasses the break
condition inside match q.recv and immediately loops again to evaluate the
64ms timeout deadline.
Could this cause the kernel thread to busy-wait in a tight loop without
yielding the CPU when the device stops responding?
> diff --git a/drivers/gpu/drm/vino/drm_sink/presence.rs b/drivers/gpu/drm/vino/drm_sink/presence.rs
> new file mode 100644
> index 0000000000000..6e58f5729223c
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/presence.rs
[ ... ]
> + pub(crate) fn reengage_connector(
> + &self,
> + io: &BoundInterface<'_>,
> + connector: u8,
> + ) -> Result<bool> {
> + let socket = connector + 1;
> + self.set_self_blanked(connector as usize, false);
> + // A connector that is not answering is exactly a connector that may be sitting in an open
> + // bracket, where the dock has disengaged its EDID handler and every probe below would go
> + // unanswered. The state is the dock's and it survives a re-enumeration, so a fresh session
> + // cannot know it is owed: assert the closed state rather than infer it. A connector already
> + // closed ignores this.
> + self.close_bracket_before_probe(io, connector);
> + self.edid_target.store(connector as u32, Ordering::Release);
> + *self.edid_caught.lock() = None;
[Severity: High]
Is it safe to mutate these global EDID variables without synchronization?
In reengage_connector(), self.edid_target and self.edid_caught are modified.
Since this is invoked from close_blank_bracket() during activate_head(), it
can run concurrently for two independent connectors via separate scanout
workers.
If Thread B overwrites Thread A's edid_target, won't Thread A's incoming
EDID response on the shared CP session be misattributed to Thread B,
causing dropped monitors during concurrent modesets?
[ ... ]
> + let mut reply = KVec::from_elem(0u8, 4096, GFP_KERNEL).ok()?;
> + let deadline = Instant::<Monotonic>::now() + Delta::from_millis(64);
> + let got = loop {
> + let n = match link.ep84_q.as_mut() {
> + Some(q) => match q.recv(dev.io(), &mut reply, crate::cp_reply_timeout()) {
> + Ok(Some(n)) => n,
> + Ok(None) => 0,
> + Err(_) => return None,
> + },
> + None => dev
> + .ctrl_recv(&mut reply, crate::cp_reply_timeout(), GFP_KERNEL)
> + .unwrap_or(0),
> + };
[Severity: Medium]
Is it possible for this loop to busy-wait and flood the USB subsystem?
Similarly to send_cp_reply(), if the fallback dev.ctrl_recv() is used in
send_presence_probe(), any transport error is swallowed by unwrap_or(0). The
loop will continue to spin until the 64ms deadline expires.
> diff --git a/drivers/gpu/drm/vino/drm_sink/scanout.rs b/drivers/gpu/drm/vino/drm_sink/scanout.rs
> new file mode 100644
> index 0000000000000..2edd29cffaf6b
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/scanout.rs
[ ... ]
> + data.last_frame.lock()[connector_index] = Some(Instant::<Monotonic>::now());
> + let returned = Arc::into_unique_or_drop(src).map(|src| {
> + let mut src = core::pin::Pin::into_inner(src);
> + ShadowSurface {
> + w: source_w,
> + h: source_h,
> + pixels: core::mem::replace(&mut src.pixels, KVVec::new()),
> + hashes: core::mem::replace(&mut src.hashes, KVVec::new()),
> + band,
> + }
> + });
[Severity: High]
Does this fail to recycle the ShadowSurface buffer pool?
In run_pending_scanout(), Arc::into_unique_or_drop(src) expects a strong_count
of 1. But because of the race condition in EncodeChunk::run(), the background
thread might still hold its reference.
If into_unique_or_drop() fails, the large memory buffers (up to 15MB) are
dropped instead of returning to the pool. Could this cause heavy continuous
GFP_KERNEL churn and potential OOMs?
[ ... ]
> +pub(super) fn snapshot_to_shadow(
> + geometry: crate::video::haar::Geometry,
> + slot: &mut Option<ShadowSurface>,
> + source: &kms::framebuffer::FramebufferVMapOwned<VinoObject>,
> + w: usize,
> + h: usize,
> +) -> Result {
[ ... ]
> + let mut fresh = false;
> + if !matches!(slot, Some(s) if s.w == w && s.h == h) {
> + let mut pixels: KVVec<u8> = KVVec::new();
> + pixels.resize(need, 0, GFP_KERNEL)?;
> + let mut hashes: KVVec<u64> = KVVec::new();
> + hashes.resize(tiles_x * tiles_y, 0, GFP_KERNEL)?;
> + let mut band: KVVec<u8> = KVVec::new();
> + band.resize(band_len, 0, GFP_KERNEL)?;
[Severity: High]
Can this sequence trigger a scheduling-while-atomic panic?
When VinoPlane::atomic_update executes data.queue_scanout(), it synchronously
calls snapshot_to_shadow() from within the atomic context. Here,
snapshot_to_shadow() dynamically allocates large KVVec buffers using
GFP_KERNEL, which is a sleeping allocation.
Does this break the DRM subsystem requirement that atomic commit paths must
not sleep?
[ ... ]
> +pub(super) fn read_cursor_bgra(
> + fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
> + w: usize,
> + h: usize,
> +) -> Result<KVec<u8>> {
> + let vmap = fb.vmap::<VinoObject>()?;
> + let view = vmap.view();
> + let pitch = vmap.pitch();
> + let row = w.checked_mul(4).ok_or(EINVAL)?;
> + let len = row.checked_mul(h).ok_or(EINVAL)?;
> + let mut out = KVec::new();
> + out.resize(len, 0, GFP_KERNEL)?;
[Severity: High]
Does this code call a sleeping function inside an atomic context?
In VinoPlane::atomic_update, the atomic commit path acquires a sleeping Mutex
(plane.cursor_last.lock()) and calls read_cursor_bgra(). Here in
read_cursor_bgra(), it allocates with GFP_KERNEL. Since the DRM subsystem
forbids sleeping inside atomic_update callbacks, could this cause deadlocks or
scheduling-while-atomic panics during cursor updates?
[ ... ]
> +fn encode_queue() -> Option<&'static workqueue::Queue> {
> + static ENCODE_WQ: kernel::sync::SetOnce<workqueue::OwnedQueue> = kernel::sync::SetOnce::new();
> + if let Some(q) = ENCODE_WQ.as_ref() {
> + return Some(q);
> + }
[Severity: High]
Does this code leak the workqueue threads when the module is unloaded?
ENCODE_WQ is statically allocated using kernel::sync::SetOnce. In Rust, static
variables do not automatically run Drop destructors. Without a module-level
teardown mechanism, doesn't the unbound workqueue remain in the system
forever, permanently leaking resources?
[ ... ]
> +impl WorkItem for EncodeChunk {
> + type Pointer = Arc<EncodeChunk>;
> +
> + fn run(this: Arc<EncodeChunk>) {
> + if let Ok(strips) = encode_coords(this.geometry, &this.src, &this.coords) {
> + *this.out.lock() = strips;
> + }
> + // Complete unconditionally. On failure `out` stays short and the joiner detects that by
> + // length -- but it must never be left blocked on a completion that cannot fire.
> + this.done.complete_all();
> + }
> +}
[Severity: High]
Can this complete_all() call race with the scanout thread's Arc recycling?
If this.done.complete_all() is signaled before the background workqueue thread
returns and drops its Arc<EncodeChunk> reference, the waiting scanout thread
might wake up immediately.
[ ... ]
> + if data.modeset_active[connector_index].load(core::sync::atomic::Ordering::Acquire) != want {
> + // A failed command-worker activation leaves the desired generation intact. This worker is
> + // sleepable, so retry the same transaction before submitting its pending framebuffer.
> + let timing = cached.ok_or(EINVAL)?;
> + data.activate_head(dev, connector, &timing, want)?;
[Severity: High]
Can this lockless check cause concurrent hardware programming?
If the command worker processes KmsCmd::ModeSet and calls activate_head(), and
the deferred scanout worker evaluates this lockless modeset_active != want
check, both threads might enter activate_head() simultaneously.
Without a per-connector lock preventing both from executing the hardware
transaction, won't this result in scrambled hardware programming and AES
decryption failures?
[ ... ]
> + // Full keyframe vs damage delta. A mode-set requires a keyframe; rotation/reflection remains
> + // conservative because the content shadow is deliberately stored in unrotated framebuffer
> + // space. For identity rotation, compare the actual framebuffer instead of trusting optional
> + // FB_DAMAGE_CLIPS: KWin commonly changes framebuffer objects without publishing that blob.
> + let kf_bit = 1u32 << connector_index;
> + let identity = rotation.angle() == plane::Rotation::ROTATE_0
> + && !rotation.contains(plane::Rotation::REFLECT_X | plane::Rotation::REFLECT_Y);
[Severity: High]
Is this bitmask check for reflection flags correct?
In encode_and_send_haar(), the identity fast-path evaluates
!rotation.contains(plane::Rotation::REFLECT_X | plane::Rotation::REFLECT_Y).
Because contains() returns true only if ALL specified bits are present, a plane
with only REFLECT_X or only REFLECT_Y evaluates to false, making identity = true.
Doesn't this bypass the conservative full-frame update for single-axis
reflections, passing unreflected framebuffer damage coordinates to
damage_strip_coords() and corrupting the display updates?
> diff --git a/drivers/gpu/drm/vino/drm_sink/stream.rs b/drivers/gpu/drm/vino/drm_sink/stream.rs
> new file mode 100644
> index 0000000000000..1adb37de980e0
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/drm_sink/stream.rs
[ ... ]
> + pub(super) fn send_stream_open(&self, dev: &BoundInterface<'_>, connector: usize) -> Result {
> + let bit = 1u32 << connector;
> + if self.stream_open_pending.load(Ordering::Acquire) & bit == 0 {
> + return Ok(());
> + }
> + let Some(open) = self.build_stream_open_buf(connector)? else {
> + self.stream_open_pending.fetch_and(!bit, Ordering::Release);
> + return Ok(());
> + };
> + let pipe_i = dev.video_pipe_index(connector)?;
> + let mut queue_slot = self.video_q[pipe_i].lock();
> + if queue_slot.is_none() {
> + *queue_slot = Some(dev.video_queue(connector, 8, VIDEO_XFER)?);
> + }
> + let queue = queue_slot
> + .as_mut()
> + .get_mut()
> + .as_mut()
> + .ok_or(kernel::error::code::ENODEV)?;
> + queue.send(dev.io(), &open, crate::timeout())?;
[Severity: High]
Does this error path also skip retiring the failed video queue?
If queue.send() fails in send_stream_open(), the error is propagated without
resetting the queue state, leading to the same permanent endpoint wedge
mentioned above.
[ ... ]
> + pub(crate) fn send_video_keepalive(&self, dev: &BoundInterface<'_>) {
> + if self.uses_arm_burst() || self.shutting_down.load(Ordering::Acquire) {
> + return;
> + }
[ ... ]
> + let Some(queue) = queue_slot.as_mut().get_mut().as_mut() else {
> + continue;
> + };
> + if queue.send(dev.io(), &report, crate::timeout()).is_ok() {
> + self.last_video_at.lock()[connector] = Some(Instant::<Monotonic>::now());
> + }
> + }
> + }
[Severity: High]
Is it safe to ignore queue.send() errors here?
By ignoring the error in send_video_keepalive(), the broken queue state
is left active. When the next foreground frame tries to use this queue, it
will fail since retire_failed_video_queue() was never invoked.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=9
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 10/13] drm/vino: read the dock's firmware version, and update it over DFU
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (8 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 9/13] drm/vino: add the dock activation and scanout path Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 16:58 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 11/13] drm/vino: add the USB driver frontend Mike Lothian
` (2 subsequent siblings)
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, linux-kernel
A DisplayLink dock does not advance bcdDevice when its firmware changes, so
that cannot answer whether an update is due. The version lives in a 16-byte
vendor descriptor inside the configuration descriptor, alongside a platform
name that selects the package targeting the hardware; the shipped
*-release.spkg packages carry theirs in a tagged table under `VE`, at no
fixed offset -- 100 in one shipped image and 24574 in another.
Add both parsers, request the matching firmware by the vendor's own
filename so a distribution can install the package unmodified, and update
only when the packaged version is strictly newer. A current dock therefore
costs one descriptor read and no writes, and a missing firmware file is not
an error: a dock runs on the firmware it shipped with.
The transfer is USB DFU 1.1 on the dock's DFU interface, matching the
vendor updater recorded end to end: DFU_DETACH, then the package verbatim
in 4096-byte blocks with an ascending wValue, each followed by
DFU_GETSTATUS, then a zero-length DFU_DNLOAD to manifest. There is no bus
reset between the detach and the first block -- the dock takes the download
in its runtime interface -- and it re-enumerates once the image is
manifested.
The functional descriptor clears bitCanUpload, so the running image cannot
be read back and there is no host-side restore path; and it clears
bitManifestationTolerant. Hence the strictly-newer rule and the package
magic check before any byte is written.
Userspace can also push an image on demand through the firmware loader's
upload interface, for a dock whose packaged firmware a distribution does
not ship.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/firmware.rs | 670 +++++++++++++++++++++++++++++++
1 file changed, 670 insertions(+)
create mode 100644 drivers/gpu/drm/vino/firmware.rs
diff --git a/drivers/gpu/drm/vino/firmware.rs b/drivers/gpu/drm/vino/firmware.rs
new file mode 100644
index 000000000000..13c9cff55ae4
--- /dev/null
+++ b/drivers/gpu/drm/vino/firmware.rs
@@ -0,0 +1,670 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Dock firmware: identity, package parsing, and the USB DFU update.
+//!
+//! A DisplayLink dock carries its firmware version in a vendor descriptor rather than in
+//! `bcdDevice`, which does not change across an update. The shipped `*-release.spkg` packages carry
+//! theirs in a tagged table. Comparing the two says whether an update is due; the transfer itself
+//! is textbook USB DFU 1.1 on the dock's DFU interface.
+
+use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
+use kernel::device::Device;
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+use kernel::firmware::Firmware;
+use kernel::prelude::*;
+use kernel::sync::{Arc, ArcBorrow};
+use kernel::time::Delta;
+use kernel::usb;
+
+/// The dock's DFU interface: `bInterfaceClass 0xfe`, `bInterfaceSubClass 1`, and the interface
+/// number every DFU class request is addressed to.
+pub(crate) const DFU_INTERFACE: u8 = 1;
+
+/// Vendor descriptor carrying the platform name and running firmware version.
+///
+/// `bcdDevice` does not change when a dock is updated, so it cannot answer "is an update due".
+/// This descriptor can: it is 16 bytes, `[len, 0x40, major, minor, patch, ..., name(8)]`, and the
+/// name selects the package that targets this hardware.
+pub(crate) const DESCRIPTOR_IDENTITY: u8 = 0x40;
+const IDENTITY_LEN: usize = 16;
+const IDENTITY_NAME: usize = 8;
+
+/// A dock's platform name and the firmware version it is running.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) struct Identity {
+ pub(crate) version: Version,
+ name: [u8; IDENTITY_NAME],
+}
+
+/// A three-part firmware version, ordered major-minor-patch.
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub(crate) struct Version(pub(crate) u8, pub(crate) u8, pub(crate) u8);
+
+impl kernel::fmt::Display for Version {
+ fn fmt(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result {
+ write!(f, "{}.{}.{}", self.0, self.1, self.2)
+ }
+}
+
+impl Identity {
+ /// Parse the identity descriptor out of a device's raw configuration descriptors.
+ pub(crate) fn parse(raw: &[u8]) -> Option<Self> {
+ let mut i = 0usize;
+ while i + 2 <= raw.len() {
+ let len = usize::from(raw[i]);
+ if len < 2 || i + len > raw.len() {
+ return None;
+ }
+ if raw[i + 1] == DESCRIPTOR_IDENTITY && len >= IDENTITY_LEN {
+ let d = &raw[i..i + IDENTITY_LEN];
+ let mut name = [0u8; IDENTITY_NAME];
+ name.copy_from_slice(&d[8..16]);
+ return Some(Self {
+ version: Version(d[2], d[3], d[4]),
+ name,
+ });
+ }
+ i += len;
+ }
+ None
+ }
+
+ /// The platform name, trimmed of its padding.
+ pub(crate) fn platform(&self) -> &[u8] {
+ let end = self
+ .name
+ .iter()
+ .position(|&c| c == 0 || c == b' ')
+ .unwrap_or(IDENTITY_NAME);
+ &self.name[..end]
+ }
+
+ /// Which dock family this is.
+ pub(crate) fn family(&self) -> Option<Family> {
+ Family::from_identity(self.platform())
+ }
+
+ /// The firmware file that targets this platform, under `/lib/firmware`.
+ pub(crate) fn firmware_name(&self) -> Option<&'static CStr> {
+ Some(self.family()?.firmware_name())
+ }
+}
+
+impl kernel::fmt::Display for Identity {
+ /// Names the hardware the way its documentation does, falling back to the raw identity tag
+ /// for a device this driver does not recognise.
+ fn fmt(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result {
+ match self.family() {
+ Some(family) => write!(f, "{}", family.description()),
+ None => match core::str::from_utf8(self.platform()) {
+ Ok(name) => write!(f, "unrecognised device {name}"),
+ Err(_) => write!(f, "unrecognised device {:02x?}", self.platform()),
+ },
+ }
+ }
+}
+
+/// A dock family: the hardware a firmware package targets.
+///
+/// Only `NavaDock` has been read off real hardware; the spellings for the other three come from
+/// the vendor's firmware packages and are unverified.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum Family {
+ /// DL-3x00 dock, e.g. the HP 3005pr.
+ Ella,
+ /// DL-6xxx dock, e.g. the Dell D6000.
+ Ridge,
+ /// DL-7400 quad dock.
+ Navarro,
+ /// Firefly monitor.
+ Firefly,
+}
+
+impl Family {
+ /// From the device's identity descriptor name.
+ pub(crate) fn from_identity(name: &[u8]) -> Option<Self> {
+ match name {
+ b"NavaDock" => Some(Self::Navarro),
+ b"Ridge" | b"RidgeDoc" => Some(Self::Ridge),
+ b"Ella" | b"EllaDock" => Some(Self::Ella),
+ b"Firefly" | b"FflyMoni" => Some(Self::Firefly),
+ _ => None,
+ }
+ }
+
+ /// How this family is described in a log line, including what kind of device it is.
+ ///
+ /// The device's own identity string is an eight-character tag -- "NavaDock", "FflyMoni" --
+ /// which is not what the hardware is called anywhere else.
+ fn description(self) -> &'static str {
+ match self {
+ Self::Navarro => "Navarro dock",
+ Self::Ridge => "Ridge dock",
+ Self::Ella => "Ella dock",
+ Self::Firefly => "Firefly monitor",
+ }
+ }
+
+ /// The file that carries this family's firmware, under `/lib/firmware`.
+ ///
+ /// Named after the package DisplayLink ships for the platform, so a distribution can drop the
+ /// vendor's own file in unmodified.
+ pub(crate) fn firmware_name(self) -> &'static CStr {
+ match self {
+ Self::Navarro => c"vino/navarro-dock-release.spkg",
+ Self::Ridge => c"vino/ridge-dock-release.spkg",
+ Self::Ella => c"vino/ella-dock-release.spkg",
+ Self::Firefly => c"vino/firefly-monitor-release.spkg",
+ }
+ }
+
+ /// From a package's `RD` tag.
+ pub(crate) fn from_package(rd: &[u8]) -> Option<Self> {
+ Self::from_identity(rd)
+ }
+}
+
+/// The dock family a package targets, from its `RD` tag.
+///
+/// This is what stops a Ridge image being written to a Navarro dock: the package says who it is
+/// for, so an image pushed in by hand can be checked without trusting the filename.
+pub(crate) fn package_family(image: &[u8]) -> Option<Family> {
+ const TAG: [u8; 4] = [b'R', b'D', 8, 0];
+ let w = image
+ .windows(TAG.len() + 8)
+ .find(|w| w[..TAG.len()] == TAG)?;
+ let name = &w[4..12];
+ let end = name.iter().position(|&c| c == 0).unwrap_or(name.len());
+ Family::from_package(&name[..end])
+}
+
+/// Standard `GET_DESCRIPTOR` for the configuration descriptor, which the identity blob rides in.
+const GET_DESCRIPTOR: u8 = 6;
+const DEVICE_TO_HOST_STANDARD: u8 = 0x80;
+const DESCRIPTOR_CONFIG: u16 = 0x0200;
+/// Enough for any configuration this hardware reports; the dock's is well under 1 KiB.
+const CONFIG_DESCRIPTOR_MAX: usize = 1024;
+
+/// Read the dock's identity by walking its configuration descriptor.
+///
+/// The blob is a vendor descriptor inside the configuration, not a separately addressable one, so
+/// the whole configuration is fetched and walked. `usb_device::rawdescriptors` holds the same bytes
+/// already, but no binding exposes them and one standard control read costs nothing on probe.
+pub(crate) fn read_identity(io: &usb::Io<'_>) -> Result<Identity> {
+ let mut connector = [0u8; 9];
+ io.control_recv(
+ GET_DESCRIPTOR,
+ DEVICE_TO_HOST_STANDARD,
+ DESCRIPTOR_CONFIG,
+ 0,
+ &mut connector,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ let total = usize::from(u16::from_le_bytes([connector[2], connector[3]]));
+ if total < connector.len() || total > CONFIG_DESCRIPTOR_MAX {
+ return Err(EINVAL);
+ }
+ let mut all = KVec::with_capacity(total, GFP_KERNEL)?;
+ all.resize(total, 0u8, GFP_KERNEL)?;
+ io.control_recv(
+ GET_DESCRIPTOR,
+ DEVICE_TO_HOST_STANDARD,
+ DESCRIPTOR_CONFIG,
+ 0,
+ &mut all,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ Identity::parse(&all).ok_or(ENODEV)
+}
+
+/// The version a `.spkg` will install.
+///
+/// The package is a tagged table: a two-byte ASCII tag, a `u16` length, then the value. `VE` holds
+/// the three version bytes. Its offset differs per package -- 100 in one shipped image and 24574 in
+/// another -- so it is searched for rather than assumed.
+pub(crate) fn package_version(image: &[u8]) -> Option<Version> {
+ const TAG: [u8; 4] = [b'V', b'E', 3, 0];
+ image
+ .windows(TAG.len() + 3)
+ .find(|w| w[..TAG.len()] == TAG)
+ .map(|w| Version(w[4], w[5], w[6]))
+}
+
+/// `.spkg` container magic, checked before anything is written to a dock.
+const PACKAGE_MAGIC: &[u8; 4] = b"ELLA";
+
+/// Whether `image` is a firmware package at all.
+pub(crate) fn is_package(image: &[u8]) -> bool {
+ image.len() > 8 && &image[..4] == PACKAGE_MAGIC
+}
+
+// USB DFU 1.1 class requests, on the dock's DFU interface.
+const DFU_OUT: u8 = 0x21; // host-to-device, class, interface
+const DFU_IN: u8 = 0xa1; // device-to-host, class, interface
+const DFU_DETACH: u8 = 0;
+const DFU_DNLOAD: u8 = 1;
+const DFU_GETSTATUS: u8 = 3;
+
+/// Payload per `DFU_DNLOAD`.
+///
+/// The DFU functional descriptor advertises a 16384-byte `wTransferSize`, but the vendor's own
+/// updater sends 4096 and the dock is only known to accept that.
+const BLOCK: usize = 4096;
+
+/// `wValue` of `DFU_DETACH`, in milliseconds. The vendor sends 100.
+const DETACH_TIMEOUT_MS: u16 = 100;
+
+/// How long a single control transfer may take.
+const XFER_TIMEOUT: Delta = Delta::from_secs(5);
+
+/// `DFU_GETSTATUS` reply: `bStatus, bwPollTimeout[3], bState, iString`.
+const STATUS_LEN: usize = 6;
+const STATUS_OK: u8 = 0;
+const STATE_DNLOAD_IDLE: u8 = 5;
+const STATE_DNBUSY: u8 = 4;
+const STATE_MANIFEST_SYNC: u8 = 6;
+const STATE_MANIFEST: u8 = 7;
+
+/// Poll `DFU_GETSTATUS` until the dock leaves `dfuDNBUSY`, honouring its own poll timeout.
+fn wait_ready(io: &usb::Io<'_>, iface: u16) -> Result<u8> {
+ for _ in 0..1000 {
+ let mut st = [0u8; STATUS_LEN];
+ io.control_recv(
+ DFU_GETSTATUS,
+ DFU_IN,
+ 0,
+ iface,
+ &mut st,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ if st[0] != STATUS_OK {
+ pr_err!(
+ "vino: firmware update rejected: DFU status {} in state {}\n",
+ st[0],
+ st[4]
+ );
+ return Err(EIO);
+ }
+ // bwPollTimeout is a 24-bit little-endian millisecond count the device asks us to wait.
+ let poll = u32::from(st[1]) | u32::from(st[2]) << 8 | u32::from(st[3]) << 16;
+ if st[4] != STATE_DNBUSY && st[4] != STATE_MANIFEST {
+ return Ok(st[4]);
+ }
+ kernel::time::delay::fsleep(Delta::from_millis(poll.clamp(1, 1000) as i64));
+ }
+ Err(ETIMEDOUT)
+}
+
+/// Write `image` to the dock over USB DFU.
+///
+/// This is not reversible from the host. The DFU functional descriptor clears
+/// `bitCanUpload`, so the running firmware cannot be read back and there is nothing to restore
+/// from; and it sets `bitManifestationTolerant = 0`, so the dock re-enumerates when the image is
+/// manifested. An interrupted write leaves the dock with a partial image.
+///
+/// The sequence is the vendor updater's, recorded end to end: `DFU_DETACH`, then the whole package
+/// verbatim in 4096-byte blocks with an ascending `wValue`, each followed by `DFU_GETSTATUS`, then
+/// a zero-length `DFU_DNLOAD` to manifest. There is no bus reset between the detach and the first
+/// block -- the dock accepts the download in its runtime interface.
+pub(crate) fn flash(io: &usb::Io<'_>, iface: u16, image: &[u8]) -> Result {
+ if !is_package(image) {
+ pr_err!("vino: refusing to flash: not a DisplayLink firmware package\n");
+ return Err(EINVAL);
+ }
+ let blocks = image.len().div_ceil(BLOCK);
+ if blocks > usize::from(u16::MAX) {
+ return Err(EFBIG);
+ }
+ pr_info!(
+ "vino: flashing {} bytes of dock firmware in {} block(s) -- do not disconnect\n",
+ image.len(),
+ blocks
+ );
+
+ io.control_send(
+ DFU_DETACH,
+ DFU_OUT,
+ DETACH_TIMEOUT_MS,
+ iface,
+ &[],
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+
+ for (n, chunk) in image.chunks(BLOCK).enumerate() {
+ io.control_send(
+ DFU_DNLOAD,
+ DFU_OUT,
+ n as u16,
+ iface,
+ chunk,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ let state = wait_ready(io, iface)?;
+ if state != STATE_DNLOAD_IDLE {
+ pr_err!("vino: firmware block {n} left the dock in DFU state {state}\n");
+ return Err(EIO);
+ }
+ }
+
+ // Zero-length download: the image is complete, manifest it. The dock re-enumerates from here,
+ // so a failure to read status back afterwards is expected rather than an error.
+ io.control_send(DFU_DNLOAD, DFU_OUT, 0, iface, &[], XFER_TIMEOUT, GFP_KERNEL)?;
+ match wait_ready(io, iface) {
+ Ok(state) if state == STATE_MANIFEST_SYNC || state == STATE_DNLOAD_IDLE => {}
+ Ok(state) => pr_info!("vino: dock manifested in DFU state {state}\n"),
+ Err(_) => pr_info!("vino: dock stopped answering after manifest, as it re-enumerates\n"),
+ }
+ pr_info!("vino: dock firmware written; it will re-enumerate to run it\n");
+ Ok(())
+}
+
+/// Decide whether `dev` needs an update, and apply one if it does.
+///
+/// An update is applied only when the packaged version is strictly newer than the one the dock
+/// reports, so the ordinary path on a current dock is one descriptor read and no writes at all. A
+/// missing firmware file is not an error: a dock runs perfectly well on the firmware it shipped
+/// with, and most systems will never carry the package.
+#[cfg(not(CONFIG_RUST_FW_LOADER_ABSTRACTIONS))]
+pub(crate) fn update_if_newer(
+ _io: &usb::Io<'_>,
+ dev: &Device,
+ identity: &Identity,
+ _iface: u16,
+ _force: bool,
+) -> Result {
+ vino_dev_debug!(
+ dev,
+ "dock firmware {}; updates need CONFIG_RUST_FW_LOADER_ABSTRACTIONS\n",
+ identity.version
+ );
+ Ok(())
+}
+
+/// Automatic updates attempted for one device: the device-name hash in the high 32 bits, the
+/// attempt count in the low 32.
+///
+/// A dock that re-enumerates still reporting the old version is otherwise rewritten on every
+/// probe, without end. One slot is enough, because the device that is looping is the one being
+/// probed; another device simply takes the slot over and starts its own count.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+static UPDATE_ATTEMPTS: AtomicU64 = AtomicU64::new(0);
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+const ATTEMPT_KEY_MASK: u64 = 0xffff_ffff_0000_0000;
+
+/// How many automatic writes one device is given before the driver leaves it on what it runs.
+///
+/// A write that takes effect is visible on the very next probe, so a device that has been given
+/// this many and still reports the old version is not going to accept another one.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+const MAX_UPDATE_ATTEMPTS: u32 = 2;
+
+/// FNV-1a over the device name, which only has to tell one device's slot from another's. The name
+/// is the bus path, so it survives the re-enumeration a write causes.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+fn attempt_key(dev: &Device) -> u64 {
+ let mut h: u32 = 0x811c_9dc5;
+ for b in dev.name().to_bytes() {
+ h ^= u32::from(*b);
+ h = h.wrapping_mul(0x0100_0193);
+ }
+ u64::from(h) << 32
+}
+
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+/// Whether `dev` has an automatic write left, without consuming one.
+fn update_attempts_left(dev: &Device) -> bool {
+ let key = attempt_key(dev);
+ let slot = UPDATE_ATTEMPTS.load(Ordering::Acquire);
+ slot & ATTEMPT_KEY_MASK != key || (slot as u32) < MAX_UPDATE_ATTEMPTS
+}
+
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+/// Count another automatic write against `dev`, or report that it has had its share.
+fn claim_update_attempt(dev: &Device) -> bool {
+ let key = attempt_key(dev);
+ let slot = UPDATE_ATTEMPTS.load(Ordering::Acquire);
+ let count = if slot & ATTEMPT_KEY_MASK == key {
+ slot as u32
+ } else {
+ 0
+ };
+ if count >= MAX_UPDATE_ATTEMPTS {
+ return false;
+ }
+ UPDATE_ATTEMPTS.store(key | u64::from(count + 1), Ordering::Release);
+ true
+}
+
+/// Forget the attempts recorded against `dev`, once it is seen running what the package offers.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+fn clear_update_attempts(dev: &Device) {
+ let key = attempt_key(dev);
+ if UPDATE_ATTEMPTS.load(Ordering::Acquire) & ATTEMPT_KEY_MASK == key {
+ UPDATE_ATTEMPTS.store(0, Ordering::Release);
+ }
+}
+
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+pub(crate) fn update_if_newer(
+ io: &usb::Io<'_>,
+ dev: &Device,
+ identity: &Identity,
+ iface: u16,
+ force: bool,
+) -> Result {
+ let Some(name) = identity.firmware_name() else {
+ return Ok(());
+ };
+ let Ok(fw) = Firmware::request_nowarn(name, dev) else {
+ vino_dev_debug!(
+ dev,
+ "no {} available; leaving the dock on {}\n",
+ name,
+ identity.version
+ );
+ return Ok(());
+ };
+ let Some(packaged) = package_version(fw.data()) else {
+ dev_warn!(dev, "{} carries no version tag; ignoring it\n", name);
+ return Ok(());
+ };
+ if packaged <= identity.version && !force {
+ clear_update_attempts(dev);
+ vino_dev_debug!(dev, "firmware is current ({} offers {})\n", name, packaged);
+ return Ok(());
+ }
+ // A write that does not take leaves the dock reporting the old version, which is the same
+ // state that asked for the write in the first place. Bound it, or the dock is rewritten on
+ // every probe for as long as it stays plugged in. The forced path is a deliberate act and is
+ // not limited.
+ if !force && !claim_update_attempt(dev) {
+ dev_warn!(
+ dev,
+ "dock still reports firmware {} after {} update attempt(s); leaving it alone\n",
+ identity.version,
+ MAX_UPDATE_ATTEMPTS
+ );
+ return Ok(());
+ }
+ dev_info!(
+ dev,
+ "updating dock firmware {} -> {}{}\n",
+ identity.version,
+ packaged,
+ if force { " (forced)" } else { "" }
+ );
+ flash(io, iface, fw.data())
+}
+
+/// Whether the automatic check will write this device the next time it runs.
+///
+/// A write reboots the dock, so the display function asks before establishing a control session
+/// that the dock is about to drop. Deliberately does not consume an attempt; the write does that.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+pub(crate) fn update_pending(dev: &Device, identity: &Identity, force: bool) -> bool {
+ let Some(name) = identity.firmware_name() else {
+ return false;
+ };
+ let Ok(fw) = Firmware::request_nowarn(name, dev) else {
+ return false;
+ };
+ // Mirror every test the write itself makes, so this cannot claim a write that will not happen
+ // and leave the display function unbound waiting for it.
+ if !is_package(fw.data()) {
+ return false;
+ }
+ let Some(packaged) = package_version(fw.data()) else {
+ return false;
+ };
+ if packaged <= identity.version && !force {
+ return false;
+ }
+ force || update_attempts_left(dev)
+}
+
+#[cfg(not(CONFIG_RUST_FW_LOADER_ABSTRACTIONS))]
+pub(crate) fn update_pending(_dev: &Device, _identity: &Identity, _force: bool) -> bool {
+ false
+}
+
+/// The `/sys/class/firmware/vino-<dock>/` upload interface.
+///
+/// This is the deliberate path: userspace hands vino an image and it is written, whatever version
+/// it is. That is what makes a re-flash of the running version, or an attempted downgrade,
+/// possible at all -- [`update_if_newer`] refuses both by design.
+///
+/// The checks below are the only thing between a mistyped `cat` and a dock that no longer works:
+/// the DFU interface does not support upload, so the running image cannot be read back and there
+/// is nothing to restore from.
+pub(crate) struct Upload;
+
+/// What an upload needs to reach the dock, and the cancel flag userspace can set.
+pub(crate) struct UploadCtx {
+ /// The window the dock's I/O is issued through, so a write can be refused once it closes.
+ pub(crate) window: Arc<usb::IoWindow>,
+ /// Set by `cancel`, observed between blocks.
+ pub(crate) cancelled: AtomicBool,
+ /// The family this dock is, which an image must match.
+ pub(crate) family: Family,
+}
+
+impl kernel::firmware::upload::Upload for Upload {
+ type Data = Arc<UploadCtx>;
+
+ fn prepare(
+ ctx: ArcBorrow<'_, UploadCtx>,
+ image: &[u8],
+ ) -> core::result::Result<(), kernel::firmware::upload::Error> {
+ use kernel::firmware::upload::Error as UErr;
+ ctx.cancelled.store(false, Ordering::Release);
+ if !is_package(image) {
+ pr_err!("vino: refusing upload: not a DisplayLink firmware package\n");
+ return Err(UErr::InvalidFirmware);
+ }
+ // The package says which dock it is for. Writing another family's image is the one
+ // mistake here that cannot be undone, so it is refused before the dock is touched.
+ match package_family(image) {
+ Some(f) if f == ctx.family => {}
+ _ => {
+ pr_err!("vino: refusing upload: image is not for this dock family\n");
+ return Err(UErr::InvalidFirmware);
+ }
+ }
+ if let Some(v) = package_version(image) {
+ pr_info!("vino: upload accepted: firmware {v}\n");
+ }
+ Ok(())
+ }
+
+ fn write(
+ ctx: ArcBorrow<'_, UploadCtx>,
+ image: &[u8],
+ offset: u32,
+ _chunk: &[u8],
+ ) -> core::result::Result<u32, kernel::firmware::upload::Error> {
+ use kernel::firmware::upload::Error as UErr;
+ // The dock takes the package as a whole -- block numbers are an index into the image, and
+ // the transfer ends with a manifest -- so it is written in one pass on the first call
+ // rather than chunk by chunk as the core offers it.
+ if offset != 0 {
+ return Ok(image.len() as u32 - offset);
+ }
+ if ctx.cancelled.load(Ordering::Acquire) {
+ return Err(UErr::Canceled);
+ }
+ let Ok(io) = ctx.window.enter() else {
+ return Err(UErr::Hardware);
+ };
+ match flash(&io, u16::from(DFU_INTERFACE), image) {
+ Ok(()) => Ok(image.len() as u32),
+ Err(e) => {
+ pr_err!("vino: firmware upload failed ({e:?})\n");
+ Err(UErr::ReadWrite)
+ }
+ }
+ }
+
+ fn poll_complete(
+ _ctx: ArcBorrow<'_, UploadCtx>,
+ ) -> core::result::Result<(), kernel::firmware::upload::Error> {
+ // `flash()` is synchronous and has already polled the dock's own DFU status to completion.
+ Ok(())
+ }
+
+ fn cancel(ctx: ArcBorrow<'_, UploadCtx>) {
+ // Runs on another thread. The write loop reads this between blocks; a transfer already in
+ // flight still completes, because abandoning one mid-image is what leaves a dock unusable.
+ ctx.cancelled.store(true, Ordering::Release);
+ pr_info!("vino: firmware upload cancellation requested\n");
+ }
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_firmware)]
+mod tests {
+ use super::*;
+ use kernel::error::code::EINVAL;
+
+ #[test]
+ fn firmware_identity_and_package_versions_parse() -> Result {
+ // The dock's identity descriptor, as read from a DL-7400: 16 bytes, type 0x40, the running
+ // version at offsets 2..5, and the platform name at 8..16.
+ let raw = [
+ 0x09u8, 0x02, 0x20, 0x00, 0x01, 0x01, 0x00, 0x80,
+ 0x32, // a config descriptor first
+ 0x10, 0x40, 0x0c, 0x02, 0x1a, 0x0b, 0x03, 0x22, b'N', b'a', b'v', b'a', b'D', b'o',
+ b'c', b'k',
+ ];
+ let id = Identity::parse(&raw).ok_or(EINVAL)?;
+ assert_eq!(id.version, Version(12, 2, 26));
+ assert_eq!(id.platform(), b"NavaDock");
+ assert_eq!(
+ id.firmware_name().ok_or(EINVAL)?,
+ c"vino/navarro-dock-release.spkg"
+ );
+
+ // A package's version is a `VE` tag with a three-byte value, at no fixed offset -- the
+ // shipped images carry it at 100 and at 24574.
+ let mut pkg = KVec::new();
+ pkg.extend_from_slice(b"ELLA\0\0\0\0", GFP_KERNEL)?;
+ pkg.extend_from_slice(&[0u8; 64], GFP_KERNEL)?;
+ pkg.extend_from_slice(b"VE\x03\0", GFP_KERNEL)?;
+ pkg.extend_from_slice(&[12, 2, 27], GFP_KERNEL)?;
+ assert!(is_package(&pkg));
+ assert_eq!(package_version(&pkg).ok_or(EINVAL)?, Version(12, 2, 27));
+
+ // Ordering is major-minor-patch, which is what decides whether an update is due.
+ assert!(Version(12, 2, 27) > Version(12, 2, 26));
+ assert!(Version(12, 3, 0) > Version(12, 2, 99));
+ assert!(Version(13, 0, 0) > Version(12, 9, 9));
+ assert!(!(Version(12, 2, 26) > Version(12, 2, 26)));
+
+ // Anything that is not a package must be refused before a byte reaches the dock.
+ assert!(!is_package(b"not a firmware image"));
+ Ok(())
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 10/13] drm/vino: read the dock's firmware version, and update it over DFU
2026-08-26 16:37 ` [PATCH v3 10/13] drm/vino: read the dock's firmware version, and update it over DFU Mike Lothian
@ 2026-08-26 16:58 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 16:58 UTC (permalink / raw)
To: Mike Lothian; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The automatic update path in `update_if_newer` fails to verify that the firmware package matches the dock's hardware family, allowing a mismatched firmware file to permanently brick the dock.
- [Medium] Infinite update loop and data race due to a single shared non-atomic rate-limit slot (`UPDATE_ATTEMPTS`).
--
commit 872c2fc3f8d6d143ff1f9c6026fbab419e5a21e3
Author: Mike Lothian <mike@fireburn.co.uk>
Subject: drm/vino: read the dock's firmware version, and update it over DFU
A DisplayLink dock does not advance bcdDevice when its firmware changes. This
patch adds parsers for the dock's identity descriptor and shipped firmware
package, and applies updates automatically over USB DFU if the packaged
version is strictly newer. It also provides a manual upload interface.
> diff --git a/drivers/gpu/drm/vino/firmware.rs b/drivers/gpu/drm/vino/firmware.rs
> new file mode 100644
> index 0000000000000..13c9cff55ae48
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/firmware.rs
[ ... ]
> +fn claim_update_attempt(dev: &Device) -> bool {
> + let key = attempt_key(dev);
> + let slot = UPDATE_ATTEMPTS.load(Ordering::Acquire);
> + let count = if slot & ATTEMPT_KEY_MASK == key {
> + slot as u32
> + } else {
> + 0
> + };
> + if count >= MAX_UPDATE_ATTEMPTS {
> + return false;
> + }
> + UPDATE_ATTEMPTS.store(key | u64::from(count + 1), Ordering::Release);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: Medium]
Could this non-atomic read-modify-write on the shared global UPDATE_ATTEMPTS
variable cause an infinite update loop?
If two docks are connected that require an update, and both fail to apply it
successfully, they might re-enumerate and probe continuously. Because
claim_update_attempt() loads and then stores on a single global slot, it looks
like the two docks might continuously evict each other's attempt counts.
This could prevent either dock from reaching MAX_UPDATE_ATTEMPTS to break
the loop, which might lead to constant USB bus resets and flash memory wear-out.
Similarly, clear_update_attempts() performs a non-atomic check-and-clear
that might race with this logic and clear another device's freshly recorded
attempts:
if UPDATE_ATTEMPTS.load(Ordering::Acquire) & ATTEMPT_KEY_MASK == key {
UPDATE_ATTEMPTS.store(0, Ordering::Release);
}
[ ... ]
> +pub(crate) fn update_if_newer(
> + io: &usb::Io<'_>,
> + dev: &Device,
> + identity: &Identity,
> + iface: u16,
> + force: bool,
> +) -> Result {
[ ... ]
> + let Some(packaged) = package_version(fw.data()) else {
> + dev_warn!(dev, "{} carries no version tag; ignoring it\n", name);
> + return Ok(());
> + };
> + if packaged <= identity.version && !force {
> + clear_update_attempts(dev);
> + vino_dev_debug!(dev, "firmware is current ({} offers {})\n", name, packaged);
> + return Ok(());
> + }
[ ... ]
> + dev_info!(
> + dev,
> + "updating dock firmware {} -> {}{}\n",
> + identity.version,
> + packaged,
> + if force { " (forced)" } else { "" }
> + );
> + flash(io, iface, fw.data())
^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: High]
Should this automatic update path verify that the firmware package matches
the dock's hardware family before flashing?
The manual upload path (Upload::prepare) uses package_family(image) and
refuses to flash if the family does not match the hardware. However,
update_if_newer() only checks the version and relies on the filename.
If a mismatched firmware file is inadvertently placed in /lib/firmware/vino/,
it appears this would silently flash the incompatible image and permanently
render the dock unusable, circumventing the safety check implemented for
manual uploads.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=10
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 11/13] drm/vino: add the USB driver frontend
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (9 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 10/13] drm/vino: read the dock's firmware version, and update it over DFU Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 17:01 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 12/13] drm/vino: allow the driver to be built Mike Lothian
2026-08-26 16:37 ` [PATCH v3 13/13] Documentation/gpu: document the Vino driver Mike Lothian
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Benno Lossin, Gary Guo, linux-kernel,
rust-for-linux
Vino drives DisplayLink DL3 docks as ordinary DRM devices, replacing the
out-of-tree EVDI module and the closed-source DisplayLinkManager daemon.
Add the probe and its lifecycle: bind the display *function* rather than a
product ID, so a dock that postdates this driver is still offered to it;
place the device by family; run bring-up and the presence and EDID worker
off the probe path; and tear down without leaving a DRM minor or a URB
behind.
DisplayLink's own udev rules match vendor 17e9 and then trigger on the
interface, with no product test anywhere. Reverse engineering the wire
protocol found the same split independently: interface protocol 0x03 is a
DL3 display function, and 0x00 is the older udl hardware, which is a
different driver's problem.
The presence worker absorbs a sink that drops and returns within a second
or two, because the dock does that on its own and the repair costs a
dock-wide re-activation. A sink that keeps flapping is not settling: the
dock reports the connector present while nothing drives it, and the panel
stays dark through a bring-up that reports success. Repair that one by
taking the connector away, so the compositor puts it back and the mode set
that answers re-drives the sink. The number of repairs is bounded, so a
dock that flaps as a matter of course cannot hold the driver in a loop of
re-activations, which would leave neither panel lit.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
drivers/gpu/drm/vino/vino.rs | 1747 ++++++++++++++++++++++++++++++++++
1 file changed, 1747 insertions(+)
create mode 100644 drivers/gpu/drm/vino/vino.rs
diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs
new file mode 100644
index 000000000000..0813faa43c03
--- /dev/null
+++ b/drivers/gpu/drm/vino/vino.rs
@@ -0,0 +1,1747 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (C) 2026 Mike Lothian
+
+//! DRM/KMS driver for DisplayLink DL3 docks.
+//!
+//! Vino drives the Dell Universal Dock D6000 using a clean-room implementation of its USB control,
+//! HDCP authentication and compressed video protocols. Each device owns its control session and
+//! exposes two atomic KMS pipelines backed by shmem GEM objects.
+
+use kernel::{
+ alloc::flags::GFP_KERNEL,
+ alloc::Flags,
+ device::{self, Core},
+ drm,
+ drm::display::hdcp as drm_hdcp,
+ error::code::{EBUSY, EINVAL, ENODEV, EPROTO, ETIMEDOUT},
+ prelude::*,
+ sync::{aref::ARef, new_mutex, Arc, Mutex},
+ time::{
+ delay::{fsleep, udelay},
+ Delta, Instant, Monotonic,
+ },
+ usb,
+ workqueue::{impl_has_work, new_work, Work, WorkItem},
+};
+
+/// Whether the load-time `debug` parameter requested verbose protocol and scanout diagnostics.
+pub(crate) fn debug_enabled() -> bool {
+ *crate::module_parameters::debug.value() != 0
+}
+
+/// Whether this one module load may disclose its ephemeral session material for a wire capture.
+///
+/// This is deliberately separate from ordinary debug logging: the values make a usbmon trace
+/// decryptable and must never appear during a normal load.
+fn trace_crypto_enabled() -> bool {
+ *crate::module_parameters::trace_crypto.value() != 0
+}
+
+/// Emit a driver diagnostic only when the load-time `debug` parameter is nonzero.
+macro_rules! vino_debug {
+ ($($arg:tt)*) => {
+ if crate::debug_enabled() {
+ kernel::pr_info!($($arg)*);
+ }
+ };
+}
+
+/// Device-prefixed counterpart to [`vino_debug`].
+macro_rules! vino_dev_debug {
+ ($dev:expr, $($arg:tt)*) => {
+ if crate::debug_enabled() {
+ kernel::dev_info!($dev, $($arg)*);
+ }
+ };
+}
+
+/// A byte slice on one line, as `08 0a 08 0a`.
+///
+/// `{:#04x?}` renders an array one element per line: the alternate flag asks the derived `Debug`
+/// for pretty output, which in a log line is unreadable.
+pub(crate) struct HexList<'a>(pub(crate) &'a [u8]);
+
+impl kernel::fmt::Display for HexList<'_> {
+ fn fmt(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result {
+ for (i, byte) in self.0.iter().enumerate() {
+ write!(f, "{}{byte:02x}", if i == 0 { "" } else { " " })?;
+ }
+ Ok(())
+ }
+}
+
+/// DisplayLink vendor id.
+const VID_DISPLAYLINK: u16 = 0x17e9;
+/// Dell Universal Dock D6000 (DL3 family) product id.
+const PID_D6000: u16 = 0x6006;
+/// WAVLINK DL7400 and relatives: "Universal DP Quad Display Docking 16G", identity tail
+/// `NavaDock`, i.e. the Navarro platform on DL-7000 silicon.
+const PID_DL7400: u16 = 0x7000;
+
+/// Dock identification and the per-dock parameters the rest of the driver reads.
+mod profile;
+/// USB endpoint resolution and the I/O handle transfers go through.
+mod usb_link;
+
+pub(crate) use profile::{DockProfile, EP_CTRL_IN, EP_CTRL_OUT};
+pub(crate) use usb_link::{Endpoints, UsbLink, EP84_BUF};
+
+/// USB transfer timeout used during session setup.
+fn timeout() -> Delta {
+ Delta::from_millis(1000)
+}
+
+/// Short timeout for draining a per-message control reply after a runtime `send_cp`.
+///
+/// EP84 remains in lockstep with EP02, but not every message elicits a reply. A NAK or timeout
+/// therefore means that there is nothing to drain and must not stall scanout or keepalive work.
+pub(crate) fn cp_reply_timeout() -> Delta {
+ Delta::from_millis(8)
+}
+
+/// Time allowed for the downstream receiver to calculate H' during repeater authentication.
+///
+/// The dock acknowledges `AKE_No_Stored_km` before that calculation is complete, so an
+/// acknowledgment cannot be used as the readiness signal.
+// The DL7400's downstream receiver produces H' about 235--240 ms after AKE_No_Stored_km in the
+// working DLM transaction. Wake just before that result instead of advancing after an arbitrary
+// shorter quiet window; `wait_per_connector_push(0x07)` below remains the actual completion gate.
+const HDCP_HPRIME_WAIT_US: i64 = 220_000;
+
+/// How long a connector's EDID fetch waits for the dock's asynchronous reply.
+///
+/// The `id=0x194` push follows the fetch acknowledgment by several messages, so the reply to the
+/// fetch itself proves nothing. Two seconds is what a cold downstream DDC read has been seen to
+/// take; a connector with nothing plugged into it spends the whole window and then reports no EDID,
+/// which is the correct answer for it.
+const EDID_REPLY_WAIT: Delta = Delta::from_secs(2);
+
+/// Wait until `anchor` is at least `target_us` old.
+fn hold_until(anchor: Instant<Monotonic>, target_us: i64) {
+ const SPIN_MARGIN_US: i64 = 400;
+ let now = anchor.elapsed().as_micros_ceil();
+ if now >= target_us {
+ return;
+ }
+ if target_us - now > SPIN_MARGIN_US {
+ fsleep(Delta::from_micros(target_us - now - SPIN_MARGIN_US));
+ }
+ let now = anchor.elapsed().as_micros_ceil();
+ if now < target_us {
+ udelay(Delta::from_micros(target_us - now));
+ }
+}
+
+mod ake;
+mod color;
+mod cp;
+mod crypto;
+mod firmware;
+mod hdcp;
+mod proto;
+mod rng;
+mod video;
+mod video_arm;
+
+/// The state a completed HDCP 2.2 AKE leaves for control-plane setup.
+struct Session {
+ ks: kernel::crypto::Secret<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>,
+ riv: [u8; drm_hdcp::RIV_LEN],
+ /// Next inner sequence counter after the AKE messages sent by [`run_ake`].
+ next_ctr: u16,
+ /// Receiver key retained for each downstream repeater authentication.
+ rsa: kernel::crypto::akcipher::RsaPublicKey,
+ rxid_list: KVec<u8>,
+}
+
+/// Tally of one [`drain_ep84`](VinoDriver::drain_ep84) sweep.
+///
+/// An acknowledgment is counted only after its inner header decrypts successfully. A tagged
+/// frame which does not decrypt is counted separately as a rejection.
+#[derive(Default, Clone, Copy)]
+struct Ep84Drain {
+ reads: usize,
+ acks: usize,
+ rejects: usize,
+ /// Sticky EDID-readiness result across combined sweeps.
+ edid_ready: bool,
+ /// Inner counter echoed by a per-connector display-capability reply.
+ display_cap_ctr: Option<u16>,
+ /// Fresh per-connector `rrx` used by downstream repeater authentication.
+ per_connector_rrx: Option<[u8; drm_hdcp::RRX_LEN]>,
+ /// Bit `msg_id` is set for every downstream-HDCP push observed in this sweep.
+ per_connector_seen: u32,
+ per_connector_repeater: Option<bool>,
+ per_connector_hprime: Option<[u8; drm_hdcp::H_PRIME_LEN]>,
+ per_connector_lprime: Option<[u8; drm_hdcp::L_PRIME_LEN]>,
+ /// Navarro's receiver-list payload is nine authenticated list-header bytes followed by V'.
+ per_connector_v: Option<([u8; 9], [u8; drm_hdcp::V_PRIME_HALF_LEN])>,
+ per_connector_auth_status: Option<u8>,
+ per_connector_mprime: Option<[u8; drm_hdcp::H_PRIME_LEN]>,
+}
+
+impl Ep84Drain {
+ /// Fold another sweep's counts into this running total.
+ fn add(&mut self, o: Ep84Drain) {
+ self.reads += o.reads;
+ self.acks += o.acks;
+ self.rejects += o.rejects;
+ self.edid_ready |= o.edid_ready;
+ self.display_cap_ctr = self.display_cap_ctr.or(o.display_cap_ctr);
+ self.per_connector_rrx = self.per_connector_rrx.or(o.per_connector_rrx);
+ self.per_connector_seen |= o.per_connector_seen;
+ self.per_connector_repeater = self.per_connector_repeater.or(o.per_connector_repeater);
+ self.per_connector_hprime = self.per_connector_hprime.or(o.per_connector_hprime);
+ self.per_connector_lprime = self.per_connector_lprime.or(o.per_connector_lprime);
+ self.per_connector_v = self.per_connector_v.or(o.per_connector_v);
+ self.per_connector_auth_status = self
+ .per_connector_auth_status
+ .or(o.per_connector_auth_status);
+ self.per_connector_mprime = self.per_connector_mprime.or(o.per_connector_mprime);
+ }
+
+ fn observe_perhead(&mut self, push: cp::PerheadHdcpPush) {
+ if push.msg_id < 32 {
+ self.per_connector_seen |= 1u32 << push.msg_id;
+ }
+ match push.msg_id {
+ // AKE_Send_Cert: the first vendor payload byte is the repeater flag.
+ 0x03 if push.payload_len >= 1 => {
+ self.per_connector_repeater = Some(push.payload[0] != 0);
+ }
+ 0x06 if push.payload_len >= drm_hdcp::RRX_LEN => {
+ let mut v = [0u8; drm_hdcp::RRX_LEN];
+ v.copy_from_slice(&push.payload[..drm_hdcp::RRX_LEN]);
+ self.per_connector_rrx = Some(v);
+ }
+ 0x07 if push.payload_len >= drm_hdcp::H_PRIME_LEN => {
+ let mut v = [0u8; drm_hdcp::H_PRIME_LEN];
+ v.copy_from_slice(&push.payload[..drm_hdcp::H_PRIME_LEN]);
+ self.per_connector_hprime = Some(v);
+ }
+ 0x0a if push.payload_len >= drm_hdcp::L_PRIME_LEN => {
+ let mut v = [0u8; drm_hdcp::L_PRIME_LEN];
+ v.copy_from_slice(&push.payload[..drm_hdcp::L_PRIME_LEN]);
+ self.per_connector_lprime = Some(v);
+ }
+ // ReceiverID_List: RxInfo/seq/list header (9 bytes), V' (16 bytes), padding.
+ 0x0c if push.payload_len >= 9 + drm_hdcp::V_PRIME_HALF_LEN => {
+ let mut list = [0u8; 9];
+ let mut vprime = [0u8; drm_hdcp::V_PRIME_HALF_LEN];
+ list.copy_from_slice(&push.payload[..9]);
+ vprime.copy_from_slice(&push.payload[9..9 + drm_hdcp::V_PRIME_HALF_LEN]);
+ self.per_connector_v = Some((list, vprime));
+ }
+ // DisplayLink prefixes ReceiverAuthStatus with one vendor status byte. The HDCP
+ // value is payload[1] (`00 04` in all four working DLM per-connector exchanges).
+ 0x12 if push.payload_len >= 2 => {
+ self.per_connector_auth_status = Some(push.payload[1]);
+ }
+ 0x11 if push.payload_len >= drm_hdcp::H_PRIME_LEN => {
+ let mut v = [0u8; drm_hdcp::H_PRIME_LEN];
+ v.copy_from_slice(&push.payload[..drm_hdcp::H_PRIME_LEN]);
+ self.per_connector_mprime = Some(v);
+ }
+ _ => {}
+ }
+ }
+
+ fn saw_perhead(&self, msg_id: u8) -> bool {
+ msg_id < 32 && self.per_connector_seen & (1u32 << msg_id) != 0
+ }
+}
+
+mod drm_sink;
+
+/// The USB driver itself. Stateless: everything per-binding lives in [`VinoBoundData`].
+/// Log what this device is, and what it exposes, before any protocol runs.
+///
+/// A DisplayLink generation is not identifiable from the USB IDs alone -- the DL3 protocol vino
+/// speaks does not apply to a DL-1x5 part, and the first sign of that is a control session timing
+/// out long after bind succeeded. Printing the descriptor and the endpoint inventory up front means
+/// a report from unfamiliar hardware carries what is needed to place it, without a debug build:
+/// `bcdDevice` is the vendor's revision, and the endpoint list distinguishes a full DL3 control
+/// device (bulk OUT 0x02 + bulk IN 0x84 + video 0x08) from a part that only has one bulk pipe.
+fn log_device_identity(
+ cdev: &device::Device<Core<'_>>,
+ intf: &usb::Interface<Core<'_>>,
+ ifnum: u8,
+) {
+ // The descriptor describes the whole device, so print it once rather than per interface.
+ if ifnum == 0 {
+ let dev: &usb::Device<Core<'_>> = intf.as_ref();
+ let vid = dev.vendor_id();
+ let pid = dev.product_id();
+ let bcd = dev.bcd_device();
+ let usb_bcd = dev.bcd_usb();
+ vino_dev_debug!(
+ cdev,
+ "USB {vid:04x}:{pid:04x} bcdDevice {:x}.{:02x} bcdUSB {:x}.{:02x} speed {}\n",
+ bcd >> 8,
+ bcd & 0xff,
+ usb_bcd >> 8,
+ usb_bcd & 0xff,
+ dev.speed_str()
+ );
+ // The USB core only caches these when the device answered the string requests.
+ match (dev.manufacturer(), dev.product()) {
+ (Some(m), Some(p)) => vino_dev_debug!(cdev, "{m} {p}\n"),
+ (None, Some(p)) => vino_dev_debug!(cdev, "{p}\n"),
+ (Some(m), None) => vino_dev_debug!(cdev, "{m} (no product string)\n"),
+ (None, None) => vino_dev_debug!(cdev, "no manufacturer/product strings\n"),
+ }
+ }
+ for ep in intf.cur_altsetting().endpoints() {
+ let dir = match ep.endpoint_dir() {
+ kernel::usb::ch9::Direction::In => "in",
+ kernel::usb::ch9::Direction::Out => "out",
+ };
+ let kind = match ep.endpoint_type() {
+ usb::EndpointType::Control => "control",
+ usb::EndpointType::Isoc => "isoc",
+ usb::EndpointType::Bulk => "bulk",
+ usb::EndpointType::Int => "int",
+ };
+ // bEndpointAddress as the descriptor carries it: number plus the direction bit.
+ let addr = ep.endpoint_number()
+ | match ep.endpoint_dir() {
+ kernel::usb::ch9::Direction::In => 0x80,
+ kernel::usb::ch9::Direction::Out => 0,
+ };
+ vino_dev_debug!(cdev, " ep {addr:#04x} {kind}-{dir} maxp {}\n", ep.maxp());
+ }
+}
+
+struct VinoDriver;
+
+/// Per-bound-interface driver state.
+///
+/// Carries the DRM [`Registration`](drm::Registration), whose lifetime is tied to this bound
+/// device, so unbinding unregisters the card through the accepted registration teardown rather
+/// than a driver-local force-unplug.
+struct VinoBoundData {
+ _intf: ARef<usb::Interface>,
+ /// The registered DRM card, dropped on unbind.
+ ///
+ /// `None` only on idle non-control interfaces. On the control interface it owns the DRM
+ /// registration and provides `disconnect()` access to the device state.
+ registration: Option<drm::Registration<'static, drm_sink::VinoDrmDriver>>,
+ /// Owned handle to the deferred bring-up work (control interface only). `disconnect()` takes
+ /// the option under the mutex before synchronously cancelling the work and unplugging DRM.
+ /// The mutex itself is heap-pinned because kernel locks must not move after initialization.
+ bringup: Pin<KBox<Mutex<Option<Arc<BringUp>>>>>,
+ /// The `/sys/class/firmware/` upload interface, on the DFU interface only.
+ ///
+ /// Held here so it is unregistered when the interface unbinds: the upload callbacks reach the
+ /// dock through the I/O window, which closes at the same time.
+ _fw_upload: Option<kernel::firmware::upload::Registration<firmware::Upload>>,
+ /// Backing store for the name `_fw_upload` was registered under.
+ ///
+ /// `firmware_upload_register` keeps the pointer it is handed rather than copying the string,
+ /// so the name has to outlive the registration. Declared after it so it is dropped second.
+ _fw_upload_name: Option<KBox<kernel::str::CString>>,
+}
+
+/// Deferred bring-up work item.
+///
+/// The device's dedicated session queue keeps blocking authentication and steady-state control I/O
+/// out of the USB probe path and the shared system workqueues.
+#[pin_data]
+struct BringUp {
+ ddev: ARef<drm_sink::VinoDrmDevice>,
+ /// Which dock this is. The bring-up sequence differs by platform (see [`DockProfile`]), and
+ /// the work item runs long after `probe` has returned, so it carries the profile itself.
+ profile: &'static DockProfile,
+ #[pin]
+ work: Work<BringUp>,
+}
+
+impl_has_work! {
+ impl HasWork<Self> for BringUp { self.work }
+}
+
+impl BringUp {
+ fn new(
+ ddev: ARef<drm_sink::VinoDrmDevice>,
+ profile: &'static DockProfile,
+ ) -> Result<Arc<Self>> {
+ Arc::pin_init(
+ pin_init!(BringUp {
+ ddev,
+ profile,
+ work <- new_work!("vino::bring_up"),
+ }),
+ GFP_KERNEL,
+ )
+ }
+}
+
+/// How often one connector's sink has flapped, and how often vino has repaired it.
+///
+/// A sink that drops and returns within a second or two heals on its own, and a repair costs a
+/// dock-wide re-activation, so a single flap is absorbed. One that keeps flapping is not settling:
+/// after the dock is handed to another host and back it reports a connector present while nothing
+/// drives its sink, and the panel stays dark through a bring-up that reports success. Measured on a
+/// lit dock, no flap at all over seventy seconds; on one left dark that way, nine a minute on both
+/// connectors.
+#[derive(Copy, Clone)]
+struct FlapTracker {
+ seen: u32,
+ since: Option<Instant<Monotonic>>,
+ repairs: u32,
+}
+
+impl FlapTracker {
+ /// Flaps inside [`Self::WINDOW`] after which the sink is repaired rather than absorbed.
+ const REPAIR_COUNT: u32 = 3;
+ const WINDOW_MS: i64 = 60_000;
+ /// Repairs one connector may take before vino leaves it alone.
+ ///
+ /// A dock that flaps as a matter of course must not be able to hold vino in a loop of
+ /// re-activations: a bounded few and then silence is recoverable, an unbounded stream is worse
+ /// than the fault it is answering.
+ const REPAIR_LIMIT: u32 = 3;
+
+ const fn new() -> Self {
+ Self {
+ seen: 0,
+ since: None,
+ repairs: 0,
+ }
+ }
+
+ /// Record a flap that healed on its own, and say whether this is the one to repair on.
+ fn healed(&mut self, now: Instant<Monotonic>) -> bool {
+ if self
+ .since
+ .is_none_or(|t| (now - t).as_millis() >= Self::WINDOW_MS)
+ {
+ self.since = Some(now);
+ self.seen = 0;
+ }
+ self.seen += 1;
+ if self.seen < Self::REPAIR_COUNT || self.repairs >= Self::REPAIR_LIMIT {
+ return false;
+ }
+ self.seen = 0;
+ self.since = None;
+ self.repairs += 1;
+ true
+ }
+}
+
+impl WorkItem for BringUp {
+ type Pointer = Arc<BringUp>;
+
+ fn run(this: Arc<BringUp>) {
+ let profile = this.profile;
+ let data: &drm_sink::VinoDrmData = &this.ddev;
+ // Naming the interface needs no I/O token, so the retry loop below can log without
+ // holding one.
+ let cdev: &device::Device = data.io.interface().as_ref();
+ let ddev = &this.ddev;
+ // Establish the transport, authenticate the link and configure the encrypted control
+ // session before publishing the connectors. A transient failure must not leave an
+ // otherwise bound device inert until it is physically replugged.
+ // A dock can refuse a session outright: it answers every control request while NAKing the
+ // first EP02 bulk write until it times out. Back off to about half a minute before giving
+ // the device up.
+ const SESSION_ATTEMPTS: usize = 8;
+ let mut established = false;
+ for attempt in 1..=SESSION_ATTEMPTS {
+ if data.is_shutting_down() {
+ return;
+ }
+ // The token is taken per attempt and dropped before the backoff. Holding one across a
+ // sleep that reaches seconds means a device reset cannot quiesce the driver: the USB
+ // core's pre-reset waits for the last token, the reset that would recover a dock which
+ // has stopped answering waits behind this loop, and an unbind waits behind the reset.
+ let Ok(link) = UsbLink::open(&data.io, data.endpoints) else {
+ return;
+ };
+ let dev = &link;
+ let result = (|| -> Result {
+ VinoDriver::bring_up(dev, profile)?;
+ vino_dev_debug!(cdev, "plaintext session initialized\n");
+ let mut session = VinoDriver::run_ake(dev)?;
+ vino_dev_debug!(cdev, "HDCP AKE + LC + SKE complete\n");
+
+ let mut edid_out: Option<KVec<u8>> = None;
+ let mut edid_connectors: [Option<KVec<u8>>; VinoDriver::CP_SETUP_CONNECTORS] =
+ core::array::from_fn(|_| None);
+ let mut video_keys = core::array::from_fn(|_| kernel::crypto::Secret::zeroed());
+ let mut connectors_present = [false; VinoDriver::CP_SETUP_CONNECTORS];
+ let mut discovery_deferred = [false; VinoDriver::CP_SETUP_CONNECTORS];
+ let mut stream_opened = 0u32;
+ let (n, wseq_end, ctr_end) = VinoDriver::send_cp_setup(
+ dev,
+ profile,
+ &mut session,
+ &mut edid_out,
+ &mut edid_connectors,
+ &mut video_keys,
+ &mut connectors_present,
+ &mut discovery_deferred,
+ &mut stream_opened,
+ )?;
+ vino_dev_debug!(cdev, "encrypted control setup complete ({n} messages)\n");
+
+ // `send_cp_setup` only returns after an authenticated reply proves that the dock
+ // engaged the session. Publish it before connector state so runtime recovery can
+ // immediately finish any per-connector discovery transaction that was deferred.
+ let drm_dev: &drm_sink::VinoDrmDevice = ddev;
+ let data: &drm_sink::VinoDrmData = drm_dev;
+ data.set_cp_engaged(true);
+ data.publish_session(
+ dev,
+ &session.ks,
+ &session.riv,
+ wseq_end,
+ ctr_end,
+ profile.protocol.ep84_queue_depth,
+ );
+ // Only the connectors whose stream this burst actually opened have consumed their
+ // first sealed block. A connector with no sink yet is opened by whatever drives it
+ // later, and must still start its chain at block zero.
+ data.set_video_keys(video_keys, stream_opened);
+ // The silence watchdog cannot live on this thread: this is the thread it exists
+ // to notice has stopped running.
+ data.start_cp_watchdog(drm_dev);
+
+ // One line naming what setup found on every physical socket, including the ones
+ // this dock does not drive as distinct streams. Which socket a monitor is in is
+ // otherwise invisible from dmesg, and it decides whether a dark output is a sink
+ // problem at all: a connector vino never drives cannot light whatever is plugged
+ // into it. `cap` is the socket's DISPLAY-CAP push, `edid` its raw EDID; the pair
+ // distinguishes an empty socket from one whose sink cannot be read.
+ for connector in 0..usize::from(profile.topology.connectors) {
+ vino_dev_debug!(
+ cdev,
+ "socket {} -- cap:{} edid:{} deferred:{} driven:{}\n",
+ connector + 1,
+ if connectors_present[connector] {
+ "yes"
+ } else {
+ "no "
+ },
+ if edid_connectors[connector].is_some() {
+ "yes"
+ } else {
+ "no "
+ },
+ if discovery_deferred[connector] {
+ "yes"
+ } else {
+ "no "
+ },
+ if data.runtime_connector(connector) {
+ "yes"
+ } else {
+ "no "
+ },
+ );
+ }
+
+ // Cache complete per-connector discovery results before emitting the single initial
+ // hotplug event. A timed-out connector remains absent and the keepalive's existing
+ // bounded re-engagement path retries it without discarding the live session.
+ for (connector, slot) in edid_connectors
+ .into_iter()
+ .enumerate()
+ .take(usize::from(profile.topology.connectors))
+ {
+ if discovery_deferred[connector] {
+ continue;
+ }
+ let have_edid = slot.is_some();
+ if let Some(blob) = slot {
+ let n = blob.len();
+ data.set_edid(connector, blob);
+ vino_dev_debug!(
+ cdev,
+ "cached socket {socket} EDID ({n} bytes)\n",
+ socket = connector + 1
+ );
+ }
+ // A recovered EDID is the presence signal on both platforms. Publishing a
+ // connector without one puts a fallback mode into an empty socket and makes
+ // the dock lay out buffers for an output that does not exist.
+ if have_edid {
+ data.set_connected(connector);
+ dev_info!(
+ cdev,
+ "socket {socket} monitor connected\n",
+ socket = connector + 1
+ );
+ }
+ }
+
+ // Navarro normally receives each EDID on the fetch drain, exactly as DLM does, but
+ // after a dock re-enumeration a response can arrive seconds late. Publishing that
+ // partial topology lets userspace mode-set one connector while its sibling is still
+ // arriving, which resets this dock, so retry the deferred connectors before the
+ // single initial hotplug. They are interleaved: a connector that never answers must
+ // not hold up one that would. A normal setup sends no additional control messages.
+ if profile.protocol.per_connector_onehot {
+ /// How long the deferred connectors are retried before the topology is
+ /// published.
+ const INITIAL_RECOVERY_MS: i64 = 6000;
+ /// Extra time granted after a connector answers, for a sibling close behind it.
+ const SIBLING_GRACE_MS: i64 = 1500;
+
+ let mut pending: [bool; VinoDriver::CP_SETUP_CONNECTORS] =
+ core::array::from_fn(|connector| {
+ connector < usize::from(profile.topology.connectors)
+ && discovery_deferred[connector]
+ && data.runtime_connector(connector)
+ && !data.connector_present(connector)
+ });
+ // One probe answers "is this socket empty?"; a re-engage is seven messages
+ // carrying ~575 ms of mandated delay. A probe that cannot answer is not
+ // evidence of absence, so only a definite negative stands a connector down.
+ for connector in 0..VinoDriver::CP_SETUP_CONNECTORS {
+ if pending[connector]
+ && data.probe_connector_present(dev, connector as u8) == Some(false)
+ {
+ pending[connector] = false;
+ }
+ }
+
+ let started = Instant::<Monotonic>::now();
+ let mut give_up = started + Delta::from_millis(INITIAL_RECOVERY_MS);
+ let expired = |give_up: Instant<Monotonic>| {
+ (Instant::<Monotonic>::now() - give_up).as_millis() >= 0
+ };
+ let mut pass = 0u32;
+ while pending.iter().any(|p| *p)
+ && !data.is_shutting_down()
+ && !expired(give_up)
+ {
+ pass += 1;
+ for connector in 0..VinoDriver::CP_SETUP_CONNECTORS {
+ // Tested per connector, not per pass: a re-engage the dock ignores
+ // costs seconds, so a pass across four of them would run well past the
+ // window before anything looked at it.
+ if !pending[connector] || expired(give_up) {
+ continue;
+ }
+ // Nothing to recover where the dock reports no presence: the connector
+ // is offered and driven without an EDID, and re-engaging asserts the
+ // closed bracket, which resets a sink that is already lit.
+ if !data.reports_presence() {
+ pending[connector] = false;
+ continue;
+ }
+ if let Ok(true) = data.reengage_connector(dev, connector as u8) {
+ data.set_connected(connector);
+ pending[connector] = false;
+ vino_dev_debug!(
+ cdev,
+ "socket {socket} monitor connected during initial \
+ recovery (pass {pass})\n",
+ socket = connector + 1
+ );
+ let grace = Instant::<Monotonic>::now()
+ + Delta::from_millis(SIBLING_GRACE_MS);
+ if (grace - give_up).as_millis() > 0 {
+ give_up = grace;
+ }
+ }
+ }
+ fsleep(Delta::from_millis(250));
+ }
+ let waited = (Instant::<Monotonic>::now() - started).as_millis();
+ for connector in 0..VinoDriver::CP_SETUP_CONNECTORS {
+ if pending[connector] {
+ dev_warn!(
+ cdev,
+ "socket {socket} never answered its EDID fetch \
+ ({pass} passes over {waited} ms); publishing without it\n",
+ socket = connector + 1
+ );
+ }
+ }
+ }
+ Ok(())
+ })();
+
+ drop(link);
+ match result {
+ Ok(()) => {
+ established = true;
+ break;
+ }
+ Err(e) if attempt < SESSION_ATTEMPTS => {
+ let backoff = 250i64 << (attempt - 1).min(5);
+ dev_warn!(
+ cdev,
+ "control-session attempt {attempt}/{SESSION_ATTEMPTS} failed \
+ ({e:?}); retrying in {backoff} ms\n"
+ );
+ fsleep(Delta::from_millis(backoff));
+ }
+ Err(e) => dev_err!(
+ cdev,
+ "control session failed after {SESSION_ATTEMPTS} attempts ({e:?})\n"
+ ),
+ }
+ }
+ if !established {
+ return;
+ }
+ let Ok(link) = UsbLink::open(&data.io, data.endpoints) else {
+ return;
+ };
+ let dev = &link;
+ {
+ let drm_dev: &drm_sink::VinoDrmDevice = ddev;
+ // Ridge needs a bounded training interval before userspace can submit a mode set.
+ // Navarro's working transcript has already performed its fixed status sequence in
+ // `send_cp_setup`; another 1.3 seconds inserted ~84 messages before its first clear.
+ if data.cp_engaged() && !profile.protocol.per_connector_onehot {
+ let data: &drm_sink::VinoDrmData = drm_dev;
+ let start = Instant::<Monotonic>::now();
+ let window = Delta::from_millis(1300);
+ let mut polls = 0u32;
+ while Instant::<Monotonic>::now() - start < window && !data.is_shutting_down() {
+ let _ = data.send_cp(dev, 0x14, 0, |ctr| cp::device_query_req(ctr, 0x000c));
+ polls += 1;
+ fsleep(Delta::from_millis(15));
+ }
+ vino_dev_debug!(cdev, "link ready after {polls} status polls\n");
+ }
+ // Whether userspace has been given a connector to drive. The hold below keeps the
+ // control link to itself until the mode set that answers this topology arrives, so it
+ // is only meaningful once there is one to answer. Arming it over an empty topology
+ // silences the link, and the downstream recovery below with it, for as long as the
+ // escape allows -- which is exactly the period a monitor still waking up needs to be
+ // asked for its EDID again.
+ let mut topology_published =
+ (0..data.connector_count()).any(|connector| data.connector_present(connector));
+ if data.dock_wide_modeset() && topology_published {
+ data.hold_cp_for_initial_modeset();
+ }
+ // This is the first point common to every generation at which KMS may touch the dock:
+ // encrypted setup and initial discovery are complete, Ella/Ridge have finished their
+ // pre-mode-set readiness interval, and Navarro's setup-to-first-mode-set hold is armed.
+ // Publish before hotplug so any atomic state userspace produces from that event sees
+ // it.
+ data.publish_kms_activation_ready(drm_dev);
+ drm_dev.hotplug_event();
+ vino_dev_debug!(cdev, "encrypted control session ready\n");
+
+ // The dock requires a continuous control dialogue for the lifetime of the session.
+ let data: &drm_sink::VinoDrmData = drm_dev;
+ vino_dev_debug!(cdev, "starting control keepalive\n");
+ let mut sent = 0u32;
+ // Heartbeats have an independent fixed cadence alongside the status queries.
+ const HEARTBEAT_PERIOD: Delta = Delta::from_secs(3);
+ let mut next_heartbeat = Instant::<Monotonic>::now() + HEARTBEAT_PERIOD;
+ // Probe downstream presence slowly and debounce transitions.
+ // How often status is queried is the dock's business, not this loop's: where video
+ // shares this endpoint each query is bytes queued against a frame and a reply the dock
+ // has to produce mid-scanout. See `DockProfile::status_period_ms`.
+ let status_period = Delta::from_millis(data.status_period_ms());
+ let mut next_status = Instant::<Monotonic>::now();
+ const PRESENCE_PERIOD: Delta = Delta::from_millis(1000);
+ let mut next_presence = Instant::<Monotonic>::now() + PRESENCE_PERIOD;
+ let mut connector_known = [false; VinoDriver::CP_SETUP_CONNECTORS];
+ // The presence probe's last verdict per connector, or `None` until it has answered
+ // once. Distinguishes "no monitor here" from "not asked yet", which the blind
+ // re-engage retry below needs and `connector_known` cannot express.
+ let mut connector_probed: [Option<bool>; VinoDriver::CP_SETUP_CONNECTORS] =
+ [None; VinoDriver::CP_SETUP_CONNECTORS];
+ // Whether this connector has ever had a monitor in this session. Standing the EDID
+ // recovery down means never asking that socket again, so it is only ever right for a
+ // sink that was there and went away. Applied to a socket that has not answered yet it
+ // is a guess about hardware the dock has not finished looking at, and a monitor still
+ // waking up loses its whole session to it.
+ let mut connector_ever_known = [false; VinoDriver::CP_SETUP_CONNECTORS];
+ // Blind sink re-engagements left for a socket that has never had a monitor; see the
+ // negative-probe branch below. Bounded because an engage is seven paced messages and
+ // an empty socket must not pay for them for the life of the session. Ten attempts
+ // at the four-second retry cadence is forty seconds of trying, which is what a D6000
+ // needs: fewer recovers its sink only sometimes.
+ const BLIND_ENGAGE_ATTEMPTS: u8 = 10;
+ let mut blind_engage_left = [BLIND_ENGAGE_ATTEMPTS; VinoDriver::CP_SETUP_CONNECTORS];
+ let mut connector_debounce = [0u8; VinoDriver::CP_SETUP_CONNECTORS];
+ // Floor on the gap between presence probes. A downstream event brings the probe
+ // forward; without a floor it would run once per loop iteration for as long as the
+ // dock keeps talking.
+ const PRESENCE_MIN_GAP: Delta = Delta::from_millis(50);
+ /// How long a connector must read absent before its monitor is called removed.
+ ///
+ /// A removal has to be debounced in TIME, not in probes: every `id=0x44` reply sets the
+ /// downstream-event flag, and a presence probe's own reply *is* an `id=0x44`, so the
+ /// watcher kept pulling itself forward to `PRESENCE_MIN_GAP` and "two consecutive
+ /// contrary reads" fired 132 ms after the first negative.
+ ///
+ /// Measured on a lit, idle DL-7400: the absent runs are 0.11 s to 2.29 s, twenty-nine
+ /// of them over three minutes, reaching 2.46 s around a mode change.
+ ///
+ /// The debounce is only half of it. Those blips are the dock really dropping the sink,
+ /// and letting the connector disappear is what repairs them: the compositor re-enables
+ /// the output and the resulting mode set relights the panel. Debouncing alone leaves
+ /// the connector dark for good, so this works only together with
+ /// `repair_flapped_connector`.
+ const PRESENCE_REMOVE_MS: i64 = 5000;
+ let mut connector_absent_since: [Option<Instant<Monotonic>>;
+ VinoDriver::CP_SETUP_CONNECTORS] = [None; VinoDriver::CP_SETUP_CONNECTORS];
+ // Whether a connector's current run of negative probes has lasted long enough to be
+ // acted on, starting the run if this is its first answer.
+ //
+ // Both the removal path and the EDID-recovery stand-down need this and for the same
+ // reason, so they share one notion of it.
+ let sustained_absent = |run: &mut Option<Instant<Monotonic>>| -> bool {
+ let since = *run.get_or_insert_with(Instant::<Monotonic>::now);
+ (Instant::<Monotonic>::now() - since).as_millis() >= PRESENCE_REMOVE_MS
+ };
+ // A recovered sink need not emit a uniquely identifiable event, so a connector whose
+ // discovery was deferred is retried at a bounded cadence until the probe answers.
+ const REENGAGE_RETRY: Delta = Delta::from_millis(4000);
+ let mut next_reengage = [Instant::<Monotonic>::now(); VinoDriver::CP_SETUP_CONNECTORS];
+ let mut flap = [FlapTracker::new(); VinoDriver::CP_SETUP_CONNECTORS];
+ /// Settling period after re-engagement during which a negative probe is ignored.
+ const PRESENCE_GRACE: Delta = Delta::from_millis(10_000);
+ let mut presence_grace = [Instant::<Monotonic>::now(); VinoDriver::CP_SETUP_CONNECTORS];
+ /// Quiet window a runtime arrival waits out before userspace is told.
+ ///
+ /// A mode set is dock-wide, so two connectors announced separately make the compositor
+ /// reconfigure the dock twice and it re-enumerates. Each arrival restarts the window
+ /// and one event covers the burst. A removal is announced immediately.
+ const HOTPLUG_COALESCE: Delta = Delta::from_millis(1500);
+ let mut hotplug_due: Option<Instant<Monotonic>> = None;
+ // When the current run of silent probes started; only read while `connector_silent >
+ // 0`.
+ for h in 0..data.connector_count() {
+ if !data.runtime_connector(h) {
+ continue;
+ }
+ connector_known[h] = data.connector_present(h);
+ connector_ever_known[h] = connector_known[h];
+ }
+ // A normal hotplug commit claims this hold almost immediately. Keep a bounded escape
+ // for a userspace session which elects not to light either connector at all.
+ //
+ // Timed, not counted: an iteration of the hold is a push drain of up to eight
+ // one-millisecond reads, so counting iterations as milliseconds overstates the escape
+ // by three to nine times and leaves the link silent for a good fraction of a minute.
+ const INITIAL_MODESET_QUIET: Delta = Delta::from_millis(5000);
+ let mut initial_quiet_until: Option<Instant<Monotonic>> = None;
+ // The cold activation owns EP02 for several seconds. Deadlines which expire while it
+ // owns the link must be re-based when it releases it; otherwise the first post-close
+ // loop sends an overdue heartbeat and presence probes ahead of the status dialogue.
+ // DLM instead continues with status counters 184, 185, ... immediately after its
+ // closing markers.
+ let mut timeline_was_exclusive = false;
+ while !data.is_shutting_down() {
+ // The dock stopped answering and the session was abandoned. Take the outputs down
+ // rather than poll a link that cannot carry anything: userspace can move its
+ // windows off a connector that has disappeared, but not off one that is merely
+ // frozen. Recovery is a replug, which rebinds and starts a fresh session.
+ if !data.cp_link_alive() {
+ data.drop_connectors_with_session(drm_dev);
+ break;
+ }
+ if data.initial_modeset_quiet() {
+ // Quiet means no unsolicited EP02 writes; DLM still has its one EP84 reader
+ // continuously posted and reaped. Keep draining pushes while userspace is
+ // preparing the first mode set so that transaction does not begin behind a
+ // multi-second status backlog.
+ data.drain_cp_pushes(dev, 8);
+ let deadline = *initial_quiet_until
+ .get_or_insert_with(|| Instant::<Monotonic>::now() + INITIAL_MODESET_QUIET);
+ if (Instant::<Monotonic>::now() - deadline).as_millis() >= 0 {
+ initial_quiet_until = None;
+ data.release_initial_modeset_quiet();
+ vino_dev_debug!(
+ cdev,
+ "no initial mode set after {} ms; releasing control keepalive\n",
+ INITIAL_MODESET_QUIET.as_millis()
+ );
+ } else {
+ fsleep(Delta::from_millis(1));
+ continue;
+ }
+ }
+ // Mode-set markers and video activation form one exclusive transaction.
+ if data.cp_timeline_exclusive() {
+ timeline_was_exclusive = true;
+ // The KMS worker owns EP02, but it releases `cp_link` between scheduled
+ // writes. Reap asynchronous EP84 traffic in those gaps just as DLM's reader
+ // thread does; request replies remain protected because `send_cp_reply`
+ // holds the mutex until it sees the matching counter.
+ data.drain_cp_pushes(dev, 8);
+ fsleep(Delta::from_millis(1));
+ continue;
+ }
+ if timeline_was_exclusive {
+ let resumed = Instant::<Monotonic>::now();
+ next_heartbeat = resumed + HEARTBEAT_PERIOD;
+ next_presence = resumed + PRESENCE_PERIOD;
+ timeline_was_exclusive = false;
+ }
+ if (Instant::<Monotonic>::now() - next_status).as_millis() >= 0 {
+ if data
+ .send_cp(dev, 0x14, 0, |ctr| cp::device_query_req(ctr, 0x000c))
+ .is_ok()
+ {
+ sent += 1;
+ }
+ next_status = Instant::<Monotonic>::now() + status_period;
+ }
+ // Compare through the signed `Delta` returned by subtracting
+ // two instants.
+ let now = Instant::<Monotonic>::now();
+ if (now - next_heartbeat).as_millis() >= 0 {
+ let _ = data.send_cp(dev, 0x16, 0, cp::heartbeat);
+ // Advance from the previous deadline so a slow send does not cause drift.
+ next_heartbeat = next_heartbeat + HEARTBEAT_PERIOD;
+ if (now - next_heartbeat).as_millis() > 0 {
+ next_heartbeat = now + HEARTBEAT_PERIOD; // fell far behind; resynchronise
+ }
+ }
+ // Consume asynchronous pushes instead of leaving them for the next paired read.
+ const MAX_UNPAIRED_DRAIN: usize = 4;
+ data.drain_cp_pushes(dev, MAX_UNPAIRED_DRAIN);
+ // Recover a connector whose setup-time discovery was deferred or timed out. This is
+ // a recovery, not a poll: once the presence probe has answered for a connector that
+ // answer is authoritative and this stands down, or an empty socket costs seven
+ // unanswered CP messages every `REENGAGE_RETRY` for the life of the session.
+ {
+ let now_r = Instant::<Monotonic>::now();
+ for h in 0..data.connector_count() {
+ let socket = h + 1;
+ if !data.runtime_connector(h) {
+ continue;
+ }
+ if connector_probed[h] == Some(false) {
+ continue;
+ }
+ if connector_known[h] || (now_r - next_reengage[h]).as_millis() < 0 {
+ continue;
+ }
+ // Where the dock says nothing about what is plugged in, this recovery has
+ // no signal to act on and its cost is visible: `reengage_connector` asserts
+ // the closed bracket first, so re-running it every `REENGAGE_RETRY` resets
+ // a sink that is already lit and driven, and the panel flashes.
+ if !data.reports_presence() {
+ continue;
+ }
+ // A blanked connector's sink is idle because vino asked for it. Re-engaging
+ // it here would also clear `self_blanked`, since `reengage_connector` does
+ // so on entry, and the connector would then be torn down mid-blank.
+ if data.is_self_blanked(h) {
+ continue;
+ }
+ next_reengage[h] = Instant::<Monotonic>::now() + REENGAGE_RETRY;
+ // Same trade as the initial recovery: one cheap probe instead of seven
+ // paced messages. It also keeps an empty socket's retry from interleaving
+ // ~575 ms of engage traffic into a mode-set transaction on another
+ // connector, which is measurable as delayed activation, not merely as
+ // noise.
+ if data.probe_connector_present(dev, h as u8) == Some(false) {
+ // Do not stand down on the first negative. A recovered EDID is this
+ // dock's presence signal; the probe is a weaker one that reports a lit
+ // sink absent for up to 2.5 s at a time. Latching here costs a monitor
+ // slow to answer at bring-up its whole session: it is never asked for
+ // an EDID again, and only re-enumerating the dock brings it back.
+ //
+ // So hold a negative to the same evidence a removal needs, and only
+ // for a socket that has had a monitor in it. A socket that has never
+ // answered goes on being probed: the re-engage is skipped either way
+ // while the answer is negative, so that costs one probe message per
+ // `REENGAGE_RETRY` and buys the case this whole path exists for -- a
+ // panel that is still coming out of standby when the dock is first
+ // asked about it.
+ if connector_ever_known[h]
+ && sustained_absent(&mut connector_absent_since[h])
+ {
+ connector_probed[h] = Some(false);
+ }
+ // A socket that has never had a monitor is where a negative answer is
+ // worth least. This dock reports a connector absent precisely while its
+ // EDID handler is not engaged for that connector, and engaging it is
+ // what the call below does -- so waiting for a positive first is
+ // waiting for the thing the re-engage produces. Spend a bounded number
+ // of blind attempts there, and only while nothing on this dock is lit,
+ // so a dock that is already driving a panel never has engage traffic
+ // interleaved into its mode sets.
+ let nothing_lit = !connector_known.iter().any(|&k| k);
+ // Where one EDID handler serves every connector, engaging it for this
+ // one takes it away from the connector that has it, and the fetch that
+ // follows returns that connector's monitor -- which is then published
+ // here as this socket's, so a single monitor appears to move between
+ // sockets and each move tears its connector down. A negative answer is
+ // the whole answer on such a dock: the engage the discovery path
+ // already ran is what makes it truthful.
+ if data.shared_edid_handler() {
+ continue;
+ }
+ if connector_ever_known[h] || !nothing_lit || blind_engage_left[h] == 0
+ {
+ continue;
+ }
+ blind_engage_left[h] -= 1;
+ }
+ connector_absent_since[h] = None;
+ vino_dev_debug!(
+ cdev,
+ "socket {socket} absent -- retrying the sink re-engage\n"
+ );
+ // A valid EDID proves presence even while the generic status reply still
+ // reflects an unengaged EDID handler.
+ match data.reengage_connector(dev, h as u8) {
+ Ok(true) => {
+ data.set_connected(h);
+ connector_known[h] = true;
+ connector_ever_known[h] = true;
+ connector_debounce[h] = 0;
+ presence_grace[h] = Instant::<Monotonic>::now() + PRESENCE_GRACE;
+ vino_dev_debug!(
+ cdev,
+ "socket {socket} monitor connected after sink re-engagement\n"
+ );
+ hotplug_due = Some(Instant::<Monotonic>::now() + HOTPLUG_COALESCE);
+ }
+ Ok(false) => {}
+ Err(e) => {
+ vino_dev_debug!(
+ cdev,
+ "socket {socket} sink re-engagement failed ({e:?})\n"
+ )
+ }
+ }
+ next_presence = Instant::<Monotonic>::now();
+ }
+ }
+ // A topology push brings presence probing forward. Do not also cancel the
+ // absent-connector re-engage backoff here: Navarro emits an `id=0x44` reply for
+ // every ordinary presence probe, and `drain_cp_pushes` deliberately reports that as
+ // a downstream event. Resetting `next_reengage` on each such reply turned two
+ // empty sockets into a continuous engage/EDID loop instead of the documented
+ // four-second retry cadence. The probe below observes an actual arrival and then
+ // re-engages that specific connector immediately.
+ if data.take_downstream_event() {
+ // Bring the probe forward, but never below `PRESENCE_MIN_GAP`.
+ let soonest = Instant::<Monotonic>::now() + PRESENCE_MIN_GAP;
+ if (next_presence - soonest).as_millis() > 0 {
+ next_presence = soonest;
+ }
+ }
+ let now_p = Instant::<Monotonic>::now();
+ if (now_p - next_presence).as_millis() >= 0 {
+ next_presence = now_p + PRESENCE_PERIOD;
+ for h in 0..data.connector_count() {
+ let socket = h + 1;
+ if !data.runtime_connector(h) {
+ continue;
+ }
+ // A missing reply carries no status bit, so it is not evidence that this
+ // monitor disappeared; wait for a decodable negative instead of tearing
+ // down a live connector.
+ let Some(present) = data.probe_connector_present(dev, h as u8) else {
+ continue;
+ };
+ // Nothing this probe says about a connector vino blanked is news, in either
+ // direction: the absence is vino's own doing, and this dock also flaps a
+ // blanked sink back to *present*, which would re-engage it and clear
+ // `self_blanked` -- leaving the next sustained negative free to tear the
+ // connector down mid-blank. The flag is cleared by the wake, in
+ // `atomic_enable`.
+ if data.is_self_blanked(h) {
+ connector_debounce[h] = 0;
+ connector_absent_since[h] = None;
+ continue;
+ }
+ // The probe has spoken for this connector, so the blind re-engage retry
+ // above stands down for it -- but only a *positive* answer is authoritative
+ // straight away. A negative one has to outlast `PRESENCE_REMOVE_MS`, and
+ // has to be about a socket that has had a monitor in it, for the same
+ // reasons it does above.
+ if present {
+ connector_probed[h] = Some(true);
+ // The absent run is cleared below, not here: the "flap healed on its
+ // own" line reads it with `take()` and would never fire again.
+ } else if connector_ever_known[h]
+ && sustained_absent(&mut connector_absent_since[h])
+ {
+ connector_probed[h] = Some(false);
+ }
+ if present == connector_known[h] {
+ connector_debounce[h] = 0;
+ // The sink came back before the removal deadline, so the connector was
+ // never dropped and nothing downstream will re-drive this connector.
+ // The dock has forgotten it, so vino has to put it back itself. Do not
+ // re-drive the connector here. Most of these blips heal on their own --
+ // the dock brings the sink back within a second or two -- and a repair
+ // costs a full dock-wide re-activation, four seconds of cold
+ // choreography for both panels. Firing one per flap puts the dock into
+ // a permanent re-activation loop, one every five to fifteen seconds,
+ // and neither panel stays lit. Absorbing the blip is the whole point; a
+ // drop that does *not* heal still falls through to the timed removal
+ // below.
+ if present && connector_absent_since[h].take().is_some() {
+ let now = Instant::<Monotonic>::now();
+ if flap[h].healed(now) {
+ // Take the connector away so the compositor puts it back: the
+ // mode set that answers is what re-drives the sink, and it is
+ // the same repair a sustained absence gets below.
+ connector_known[h] = false;
+ connector_debounce[h] = 0;
+ next_reengage[h] = now + REENGAGE_RETRY;
+ data.set_disconnected(h);
+ dev_info!(
+ cdev,
+ "socket {socket} sink will not settle; dropping the connector so it is re-driven\n"
+ );
+ hotplug_due = None;
+ drm_dev.hotplug_event();
+ next_presence = Instant::<Monotonic>::now() + PRESENCE_PERIOD;
+ continue;
+ }
+ vino_dev_debug!(
+ cdev,
+ "socket {socket} sink flap healed on its own\n"
+ );
+ }
+ continue;
+ }
+ // Inside the settling window after a recovery, a negative answer is not
+ // evidence -- see `PRESENCE_GRACE`.
+ if !present
+ && (Instant::<Monotonic>::now() - presence_grace[h]).as_millis() < 0
+ {
+ connector_debounce[h] = 0;
+ connector_absent_since[h] = None;
+ continue;
+ }
+ if present {
+ // Two consecutive contrary reads before announcing an arrival.
+ connector_absent_since[h] = None;
+ connector_debounce[h] = connector_debounce[h].saturating_add(1);
+ if connector_debounce[h] < 2 {
+ continue;
+ }
+ } else {
+ // A removal must be sustained: the dock reports a lit sink absent for
+ // seconds at a time. Counting probes instead of time does not work --
+ // every `id=0x44` reply sets the downstream-event flag, and a probe's
+ // own reply is one, so the watcher pulls itself forward and "two
+ // contrary reads" fires 132 ms after the first negative.
+ if !sustained_absent(&mut connector_absent_since[h]) {
+ continue;
+ }
+ connector_absent_since[h] = None;
+ }
+ connector_debounce[h] = 0;
+ if present {
+ // An attempt that came back without an EDID has already answered, and
+ // this path never read the deadline it set: the attempt clears the
+ // debounce, two more probes rebuild it, and a socket the dock calls
+ // present with nothing plugged into it re-engages every two seconds
+ // for the life of the session. Seven paced control messages, on a dock
+ // whose vendor sends one status query in the same interval and shares
+ // the endpoint with its pixels.
+ if (Instant::<Monotonic>::now() - next_reengage[h]).as_millis() < 0 {
+ continue;
+ }
+ // Re-engage the downstream sink before accepting another mode set.
+ match data.reengage_connector(dev, h as u8) {
+ Ok(true) => {}
+ Ok(false) => {
+ next_reengage[h] = Instant::<Monotonic>::now() + REENGAGE_RETRY;
+ continue;
+ }
+ Err(e) => {
+ vino_dev_debug!(
+ cdev,
+ "socket {socket} sink re-engagement failed ({e:?})\n"
+ );
+ next_reengage[h] = Instant::<Monotonic>::now() + REENGAGE_RETRY;
+ continue;
+ }
+ }
+ data.set_connected(h);
+ connector_known[h] = true;
+ connector_ever_known[h] = true;
+ flap[h] = FlapTracker::new();
+ next_reengage[h] = Instant::<Monotonic>::now() + REENGAGE_RETRY;
+ presence_grace[h] = Instant::<Monotonic>::now() + PRESENCE_GRACE;
+ dev_info!(cdev, "socket {socket} monitor connected\n");
+ // Same downstream readiness wait as a fresh bring-up before notifying
+ // userspace, so KWin's mode-set lands on a settled downstream link.
+ let rs = Instant::<Monotonic>::now();
+ while (Instant::<Monotonic>::now() - rs).as_millis() < 1300
+ && !data.is_shutting_down()
+ {
+ let _ = data
+ .send_cp(dev, 0x14, 0, |ctr| cp::device_query_req(ctr, 0x000c));
+ fsleep(Delta::from_millis(15));
+ }
+ hotplug_due = Some(Instant::<Monotonic>::now() + HOTPLUG_COALESCE);
+ } else {
+ connector_known[h] = false;
+ next_reengage[h] = Instant::<Monotonic>::now() + REENGAGE_RETRY;
+ data.set_disconnected(h);
+ dev_info!(cdev, "socket {socket} monitor disconnected\n");
+ // This event also covers any arrival still waiting out its window.
+ hotplug_due = None;
+ drm_dev.hotplug_event();
+ }
+ // Re-baseline the heartbeat/presence deadlines skipped during the wait.
+ next_presence = Instant::<Monotonic>::now() + PRESENCE_PERIOD;
+ }
+ }
+ // Announce a settled burst of arrivals as one topology change.
+ if let Some(due) = hotplug_due {
+ if (Instant::<Monotonic>::now() - due).as_millis() >= 0 {
+ hotplug_due = None;
+ // A monitor whose sink was not ready at bring-up is published here
+ // instead, and the mode set answering it is still this session's first.
+ // It needs the same quiet link a bring-up gives its own: the activation
+ // is dock-wide, and the re-engage retries aimed at the sockets that are
+ // genuinely empty are ~575 ms of paced traffic each, landing in the
+ // middle of it otherwise.
+ if data.dock_wide_modeset() && !topology_published {
+ initial_quiet_until = None;
+ data.hold_cp_for_initial_modeset();
+ }
+ topology_published = true;
+ drm_dev.hotplug_event();
+ }
+ }
+ // A dock that tears the link down over a silent video endpoint needs feeding even
+ // when the compositor has nothing to redraw.
+ data.send_video_keepalive(dev);
+ // A dock whose video shares this endpoint has its scanout workers stood down for
+ // the duration of every control message, and a worker that bails does not re-arm
+ // itself. Wake them here, where a device handle is in hand, so a connector with
+ // nothing else to trigger it still resumes.
+ if data.video_on_ctrl_pipe() {
+ data.enqueue_scanout_all(drm_dev);
+ }
+ fsleep(Delta::from_millis(13));
+ }
+ vino_dev_debug!(cdev, "CP keepalive finished ({sent} polls)\n");
+ }
+ }
+}
+
+/// Control-session bring-up: plaintext init, link AKE, and the sealed per-connector setup.
+mod session;
+
+/// Which DisplayLink function an interface exposes, i.e. why this driver was offered it.
+///
+/// This is what the ID table carries. A table of product IDs cannot say anything useful about
+/// hardware nobody has tested, but the interface descriptor says what a function *is*, and that
+/// is stable across every dock in the family.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum Function {
+ /// The DL3 display function: the control endpoints and every video endpoint.
+ Display,
+ /// The DFU interface, which carries the identity descriptor and firmware updates.
+ Dfu,
+}
+
+/// Vendor-specific class, which every DisplayLink display function uses.
+const CLASS_VENDOR: u8 = 0xff;
+/// Interface protocol of a DL3 display function. `0x00` is the old `udl` hardware, which is a
+/// different driver's problem, so keying on this excludes it for free.
+const PROTOCOL_DL3: u8 = 0x03;
+/// Application-specific class, subclass and protocol of a USB DFU runtime interface.
+const CLASS_DFU: (u8, u8, u8) = (0xfe, 0x01, 0x01);
+
+// DisplayLink's own udev rules match `17e9/*` and then trigger on the interface, with no product
+// test anywhere. Reverse engineering found the same split independently. Binding the *function*
+// rather than a list of tested products is what lets a dock nobody here owns come up; the
+// identity descriptor read in `probe` is the safety valve that keeps that honest.
+kernel::usb_device_table!(
+ USB_TABLE,
+ MODULE_USB_TABLE,
+ <VinoDriver as usb::Driver>::IdInfo,
+ [
+ (
+ usb::DeviceId::from_vendor_and_interface_info(
+ VID_DISPLAYLINK,
+ CLASS_VENDOR,
+ 0x00,
+ PROTOCOL_DL3
+ ),
+ Function::Display
+ ),
+ (
+ usb::DeviceId::from_vendor_and_interface_info(
+ VID_DISPLAYLINK,
+ CLASS_DFU.0,
+ CLASS_DFU.1,
+ CLASS_DFU.2
+ ),
+ Function::Dfu
+ ),
+ ]
+);
+
+impl usb::Driver for VinoDriver {
+ type IdInfo = Function;
+ type Data<'bound> = VinoBoundData;
+ const ID_TABLE: usb::IdTable<Self::IdInfo> = &USB_TABLE;
+ // The dock goes on scanning out its last decoded frame for as long as it is powered, so a
+ // driver that simply stops talking leaves both monitors lit on a frozen desktop. Telling them
+ // to power down is the last thing this driver does, and it can only be done while the
+ // interface's endpoints still exist -- which by default they do not by the time any callback
+ // runs. `quiesce` cancels every outstanding transfer itself.
+ const SOFT_UNBIND: bool = true;
+
+ fn probe<'bound>(
+ intf: &'bound usb::Interface<Core<'_>>,
+ _id: &usb::DeviceId,
+ info: &'bound Self::IdInfo,
+ io: Arc<usb::IoWindow>,
+ ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
+ let cdev: &device::Device<Core<'_>> = intf.as_ref();
+ // The control endpoints (0x02/0x84) and the whole HDCP session live on the display
+ // function -- drive bring-up only there so the preamble and AKE do not run once per
+ // interface and pollute the dock's state machine. An interface with no active alternate
+ // setting has no endpoints to drive.
+ let function = *info;
+ let ifnum = intf.number().ok_or(ENODEV)?;
+ log_device_identity(cdev, intf, ifnum);
+
+ // What this hardware *is*, asked of the hardware. `read_identity` walks the ordinary
+ // configuration descriptor: one standard control transfer, no session and no crypto, so
+ // it works at probe on either interface and long before the dock will talk to anyone.
+ let identity = io
+ .enter()
+ .and_then(|link| firmware::read_identity(&link))
+ .ok();
+ let identity_family = identity.as_ref().and_then(firmware::Identity::family);
+
+ // Writing firmware the dock does not need is a deliberate act: its DFU interface does
+ // not support upload, so there is no way to read the running image back and nothing to
+ // restore from if the write goes wrong.
+ let force = *crate::module_parameters::force_flash.value() != 0;
+ if function == Function::Dfu {
+ // Every DFU request is addressed to this interface. A failed check is not fatal: a
+ // dock runs perfectly well on the firmware it shipped with.
+ match (identity.as_ref().ok_or(ENODEV)).and_then(|id| {
+ let link = io.enter()?;
+ dev_info!(cdev, "{id} running firmware {}\n", id.version);
+ firmware::update_if_newer(&link, cdev, id, u16::from(ifnum), force)
+ }) {
+ Ok(()) => {}
+ Err(e) => dev_info!(cdev, "dock firmware check skipped ({e:?})\n"),
+ }
+ }
+ // The manual path: userspace writes an image and vino flashes it, whatever version it is.
+ // This is how a re-flash of the running version or a downgrade is done at all, since the
+ // automatic check refuses both. Published only on the DFU interface, and only for a dock
+ // whose family is recognised -- an image for another family is refused in `prepare`.
+ let mut fw_upload_name: Option<KBox<kernel::str::CString>> = None;
+ let fw_upload = if function == Function::Dfu {
+ match identity_family {
+ Some(family) => {
+ let ctx = Arc::new(
+ firmware::UploadCtx {
+ window: io.clone(),
+ cancelled: core::sync::atomic::AtomicBool::new(false),
+ family,
+ },
+ GFP_KERNEL,
+ )?;
+ // Named per device, not `vino-dock`. The name becomes a device name inside
+ // the shared `firmware` class, so a fixed one lets only the first dock
+ // register and leaves the node saying nothing about which dock it flashes --
+ // with two docks attached that is a route to flashing the wrong one.
+ let name = kernel::str::CString::try_from_fmt(kernel::prelude::fmt!(
+ "vino-dock-{}",
+ cdev.name()
+ ))
+ .and_then(|n| KBox::new(n, GFP_KERNEL).map_err(Into::into));
+ match name {
+ Ok(name) => match kernel::firmware::upload::Registration::new(
+ &THIS_MODULE,
+ cdev,
+ &name,
+ ctx,
+ ) {
+ Ok(reg) => {
+ dev_info!(
+ cdev,
+ "firmware upload available at /sys/class/firmware/{}\n",
+ &**name
+ );
+ fw_upload_name = Some(name);
+ Some(reg)
+ }
+ Err(e) => {
+ dev_warn!(cdev, "no firmware upload interface ({e:?})\n");
+ None
+ }
+ },
+ Err(e) => {
+ dev_warn!(cdev, "no firmware upload interface ({e:?})\n");
+ None
+ }
+ }
+ }
+ None => None,
+ }
+ } else {
+ None
+ };
+ if function == Function::Dfu {
+ vino_dev_debug!(
+ cdev,
+ "bound interface {ifnum} (idle -- control is the display function)\n"
+ );
+ return Ok(VinoBoundData {
+ _intf: intf.into(),
+ registration: None,
+ bringup: KBox::pin_init(new_mutex!(None), GFP_KERNEL)?,
+ _fw_upload: fw_upload,
+ _fw_upload_name: fw_upload_name,
+ });
+ }
+
+ // The DFU interface is probed independently of this one and writes firmware in its own
+ // probe, which reboots the dock. Establishing a control session against a dock that is
+ // about to drop off the bus only produces timeouts and a device that is torn down and
+ // rebuilt, so leave it alone: the dock re-enumerates on the new firmware and this probe
+ // runs again with nothing pending. The attempt limit is what stops that repeating.
+ if let Some(id) = identity.as_ref() {
+ if firmware::update_pending(cdev, id, force) {
+ dev_info!(
+ cdev,
+ "dock firmware update pending; the display function binds once it has run\n"
+ );
+ return Err(ENODEV);
+ }
+ }
+
+ // The safety valve for matching on the interface rather than on a product ID. A dock that
+ // answers with a family nobody here has driven is declined by name, so its owner gets a
+ // log line and a report to send instead of a driver guessing at its wire format -- and
+ // the way a dock rejects a guess is to reset itself. A dock that could not be *asked*
+ // falls back to the product-ID quirk table, because a transient descriptor read must not
+ // cost a working device its display.
+ let profile = match identity_family {
+ Some(family) => match profile::for_family(family) {
+ Some(profile) => profile,
+ None => {
+ let id = identity.as_ref().ok_or(ENODEV)?;
+ dev_info!(
+ cdev,
+ "{id} is not a family this driver drives yet; declining. \
+ A report makes it supportable: Documentation/gpu/vino.rst\n"
+ );
+ return Err(ENODEV);
+ }
+ },
+ None => {
+ let usbdev: &usb::Device<Core<'_>> = intf.as_ref();
+ match profile::for_product(usbdev.product_id()) {
+ Some(profile) => {
+ dev_warn!(
+ cdev,
+ "identity descriptor unreadable; using the quirk entry for \
+ {:04x}\n",
+ usbdev.product_id()
+ );
+ profile
+ }
+ None => {
+ dev_info!(
+ cdev,
+ "no identity descriptor and no quirk entry; declining. \
+ A report makes it supportable: Documentation/gpu/vino.rst\n"
+ );
+ return Err(ENODEV);
+ }
+ }
+ }
+ };
+ // One line per bind, naming the hardware the driver decided it is holding. On unfamiliar
+ // hardware this is what says whether the dock was recognised or fell back to a stranger's
+ // profile, so it stays out of the debug gate. The endpoint map that follows from it is a
+ // debug detail.
+ dev_info!(cdev, "{}\n", profile.name);
+ vino_dev_debug!(
+ cdev,
+ "video endpoints {}, 10-bit capable {}\n",
+ HexList(&profile.topology.video_endpoints),
+ profile.capabilities.hdr_capable
+ );
+ // Register the DRM/KMS device on the control interface. Keep a refcounted interface handle
+ // in the bound data while the DRM device retains the I/O window used by its workers.
+ let intf_ref: ARef<usb::Interface> = intf.into();
+
+ // Resolve the dock's endpoints against the display function's descriptor once, so every
+ // later transfer names a direction/type-checked endpoint instead of a bare address.
+ let (endpoints, connectors) = Endpoints::resolve(intf, profile)?;
+ if connectors != profile.topology.connectors {
+ dev_warn!(
+ cdev,
+ "{connectors} connector(s) backed by video endpoints, not the {} this \
+ profile describes; driving what the device exposes\n",
+ profile.topology.connectors
+ );
+ }
+
+ // DRM device lifecycle: allocate an `UnregisteredDevice`, wire up the KMS pipeline on it
+ // while still unregistered, then register it. The `Registration` is stored in the bound
+ // data below, so the card is unregistered by the ordered unbind rather than by a
+ // driver-local force-unplug.
+ let unreg = drm::UnregisteredDevice::<drm_sink::VinoDrmDriver>::new(
+ intf,
+ // The ten-bit and cursor flags and the connector count have to arrive here, not in the
+ // profile block below: the KMS objects are built during this call, and they decide
+ // then whether to offer a 10-bit format, the HDR connector properties and a cursor
+ // plane, and how many connectors to build at all.
+ drm_sink::VinoDrmData::new(
+ io.clone(),
+ endpoints,
+ profile.capabilities.hdr_capable,
+ profile.capabilities.hw_cursor,
+ connectors,
+ ),
+ &THIS_MODULE,
+ )?;
+ // `Core` derefs to `Bound`; name the context explicitly so `as_ref()`
+ // resolves to the bound parent required by DRM registration.
+ let bound_intf: &usb::Interface<device::Bound> = intf;
+ let parent: &device::Device<device::Bound> = bound_intf.as_ref();
+ let registration = drm::Registration::new_static(parent, unreg, (), 0)?;
+ let ddev: ARef<drm_sink::VinoDrmDevice> = registration.device().into();
+ vino_dev_debug!(cdev, "DRM/KMS device registered\n");
+
+ // The session preamble, HDCP authentication and control setup use blocking USB transfers.
+ // Run them on the device's ordered session queue so probe can return immediately. The work
+ // item owns the DRM device, and the bound data retains a handle so quiesce can cancel or
+ // flush it before the I/O window closes.
+ // Gate video on what this platform's video path is known to accept.
+ {
+ let d: &drm_sink::VinoDrmData = &ddev;
+ // `force_video` exists to answer one question on a dock whose profile disables video:
+ // whether the platform actually requires its sealed stream-open, or whether correct
+ // record framing alone is enough. It is off by default because the way a dock rejects
+ // a malformed video write is to reset itself, taking the control session with it.
+ // This device's codec geometry, passed into every codec call made on its behalf.
+ // It is per device because two docks of different generations lay a strip's sixteen
+ // blocks over different pixels; see `video::haar::Geometry`.
+ d.set_codec_geometry(
+ profile.protocol.strip_blocks_x,
+ profile.protocol.interlaced_bands,
+ profile.protocol.band_parity_bit,
+ profile.protocol.connector_selector_shift,
+ profile.protocol.stream_id_mask,
+ profile.protocol.dock_buffers,
+ profile.protocol.code_tables,
+ profile.protocol.steady_record_sub_bit,
+ );
+ d.set_frame_delivery(profile.protocol.frame_delivery);
+ d.set_probe_bracket(profile.protocol.probe_bracket);
+ d.set_stream_pacing(profile.protocol.stream_pacing);
+ d.set_mode_limits(
+ profile.capabilities.pixel_budget,
+ profile.capabilities.max_refresh_hz,
+ profile.capabilities.max_connector_clock_khz,
+ );
+ d.set_mode_behaviour(profile);
+ d.set_video_on_ctrl_pipe(profile.topology.video_on_ctrl_pipe);
+ d.set_frame_period_ms(profile.protocol.frame_period_ms);
+ d.set_carrier_frames(profile.protocol.carrier_frames);
+ d.set_status_period_ms(profile.protocol.status_period_ms);
+ d.set_arm_burst(profile.protocol.arm_burst);
+ d.set_allocation(&profile.protocol.allocation);
+ d.set_reports_presence(profile.protocol.reports_presence);
+ d.set_shared_edid_handler(profile.quirks.shared_edid_handler);
+ d.set_split_full_packet_frame(profile.quirks.split_full_packet_frame);
+ d.set_video_stream_desc(
+ profile.protocol.layout_word,
+ profile.protocol.stream_marker_kind,
+ profile.protocol.code_tables,
+ );
+ d.set_sink_down_state(profile.protocol.sink_down_state);
+ d.set_post_mode_sink_states(profile.protocol.post_mode_sink_states);
+ d.set_pre_mode_sink_state(profile.protocol.pre_mode_sink_state);
+ }
+ let bringup = BringUp::new(ddev.clone(), profile)?;
+ let bringup_slot = KBox::pin_init(new_mutex!(Some(bringup.clone())), GFP_KERNEL)?;
+
+ let data: &drm_sink::VinoDrmData = &ddev;
+ data.session_queue().enqueue(bringup).map_err(|_| EBUSY)?;
+
+ Ok(VinoBoundData {
+ _intf: intf_ref,
+ registration: Some(registration),
+ bringup: bringup_slot,
+ // The upload interface lives on the DFU interface, not the control one.
+ _fw_upload: None,
+ _fw_upload_name: None,
+ })
+ }
+
+ fn pre_reset<'bound>(
+ _intf: &'bound usb::Interface<Core<'_>>,
+ data: Pin<&VinoBoundData>,
+ ) -> Result {
+ if let Some(reg) = data.registration.as_ref() {
+ let drm_data: &drm_sink::VinoDrmData = reg.device();
+ drm_data.stop_for_reset();
+ }
+ Ok(())
+ }
+
+ /// Ask the USB core to rebind this interface once the reset has completed.
+ ///
+ /// The only state that makes this dock usable is the content-protection session, and the reset
+ /// is what destroyed it. There is nothing to restore and no way to establish a new session
+ /// except through probe, so a driver that returns success here stays bound to a dock that will
+ /// never answer again. A non-zero return marks the interface for rebinding, which unbinds and
+ /// probes it afresh.
+ fn post_reset<'bound>(
+ intf: &'bound usb::Interface<Core<'_>>,
+ _data: Pin<&VinoBoundData>,
+ ) -> Result {
+ let dev: &device::Device<Core<'_>> = intf.as_ref();
+ dev_info!(dev, "reset complete; rebinding for a fresh session\n");
+ Err(ENODEV)
+ }
+
+ fn quiesce<'bound>(_intf: &'bound usb::Interface<Core<'_>>, data: Pin<&VinoBoundData>) {
+ if let Some(reg) = data.registration.as_ref() {
+ let drm_data: &drm_sink::VinoDrmData = reg.device();
+ // The last chance to tell the dock anything. This hook runs while the interface is
+ // still bound, whereas `disconnect()` runs after I/O has been revoked -- and the stop
+ // flag published below makes every control transfer refuse by design, because a
+ // transfer issued into a disconnect deadlocks `usb_hub_wq`. So the sinks are parked
+ // here, first, or not at all.
+ drm_data.park_sinks();
+ // Publish the producers' stop flag before waiting on anything. This is only the flag:
+ // the teardown that must not run until USB I/O is quiesced (vblank timers, the
+ // device's self-reference cycles) still happens in `shutdown()` further down.
+ drm_data.begin_shutdown();
+ }
+
+ // Take the sole driver-owned bring-up handle. The queued work holds its own Arc until it
+ // runs or is cancelled, while this local Arc keeps the embedded Work pinned and live
+ // throughout the `cancel_sync` below.
+ let bringup = data.bringup.lock().take();
+
+ // Flush the deferred bring-up before the interface is unbound: `cancel_sync` dequeues it
+ // if pending and blocks until it returns if already running, so no USB I/O races the
+ // unbind. Safe when the work already finished or never ran -- it then simply reports that
+ // nothing was pending. The reclaimed `Arc<BringUp>` (returned only if the work was still
+ // queued) is dropped here.
+ if let Some(work) = bringup.as_ref() {
+ drop(work.work.cancel_sync());
+ }
+
+ // `bringup` drops here, releasing its DRM reference before I/O is revoked.
+ }
+
+ fn disconnect<'bound>(intf: &'bound usb::Interface<Core<'_>>, data: Pin<&VinoBoundData>) {
+ let dev: &device::Device<Core<'_>> = intf.as_ref();
+
+ // Stop every producer. The DRM device itself is unregistered by `Registration`'s `Drop`
+ // when the bound data is released -- the accepted registration teardown already calls
+ // `drm_dev_unplug()`, so there is no driver-local force-unplug here any more.
+ //
+ // Take the device through the registration. `shutdown()` also breaks
+ // vblank self-references before registration teardown.
+ if let Some(reg) = data.registration.as_ref() {
+ let drm_data: &drm_sink::VinoDrmData = reg.device();
+ drm_data.shutdown();
+ }
+ dev_info!(dev, "disconnected\n");
+ }
+}
+
+kernel::module_usb_driver! {
+ type: VinoDriver,
+ name: "vino",
+ authors: ["Mike Lothian"],
+ description: "DisplayLink DL3 (Vino) open driver",
+ license: "GPL v2",
+ params: {
+ debug: u8 {
+ default: 0,
+ description: "Enable verbose Vino protocol and scanout diagnostics",
+ },
+ trace_crypto: u8 {
+ default: 0,
+ description: "Diagnostic: disclose one session's keys to decrypt a USB capture",
+ },
+ rtc_utc_offset_minutes: i32 {
+ default: 0,
+ description: "Minutes east of UTC, for a dock's real-time clock",
+ },
+ force_flash: u8 {
+ default: 0,
+ description: "Write the packaged dock firmware even if the dock is not older",
+ },
+ edid_override: u8 {
+ default: 0,
+ description: "Bitmask of connectors whose EDID comes from DRM's override",
+ },
+ },
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_presence_flap)]
+mod tests {
+ use super::*;
+
+ /// Flaps `n` times `apart_ms` apart, and reports how many repairs that asked for.
+ fn flaps(n: u32, apart_ms: i64) -> u32 {
+ let mut tracker = FlapTracker::new();
+ let start = Instant::<Monotonic>::now();
+ let mut repairs = 0;
+ for i in 0..n {
+ if tracker.healed(start + Delta::from_millis(apart_ms * i64::from(i))) {
+ repairs += 1;
+ }
+ }
+ repairs
+ }
+
+ #[test]
+ fn a_blip_is_absorbed_and_sustained_flapping_is_repaired() {
+ // One flap, and a second a long way after it, are blips: the dock brings the sink back on
+ // its own and a repair would cost a dock-wide re-activation for nothing.
+ assert_eq!(flaps(1, 0), 0);
+ assert_eq!(flaps(2, 1_000), 0);
+ // Flaps spread wider than the window never accumulate, however many there are.
+ assert_eq!(flaps(20, FlapTracker::WINDOW_MS), 0);
+
+ // A sink that will not settle is repaired. Nine a minute is what a connector left dark by a
+ // warm plug produces, and it asks for a repair rather than being absorbed forever.
+ assert!(flaps(FlapTracker::REPAIR_COUNT, 1_000) > 0);
+ assert!(flaps(30, 6_500) > 0);
+ }
+
+ #[test]
+ fn a_flapping_dock_cannot_hold_vino_in_a_repair_loop() {
+ // The repair is a dock-wide re-activation. However long the flapping goes on, the number of
+ // them is bounded: a dock that flaps as a matter of course gets a few and then silence.
+ assert_eq!(flaps(10_000, 1_000), FlapTracker::REPAIR_LIMIT);
+ }
+
+ #[test]
+ fn a_connector_that_comes_back_starts_again() {
+ // The tracker is reset when a connector is re-established, so a dock that misbehaves once
+ // is still repairable the next time rather than having spent its budget for the session.
+ let mut tracker = FlapTracker::new();
+ let now = Instant::<Monotonic>::now();
+ for _ in 0..FlapTracker::REPAIR_LIMIT * FlapTracker::REPAIR_COUNT {
+ tracker.healed(now);
+ }
+ assert!(!tracker.healed(now));
+ tracker = FlapTracker::new();
+ for _ in 0..FlapTracker::REPAIR_COUNT - 1 {
+ assert!(!tracker.healed(now));
+ }
+ assert!(tracker.healed(now));
+ }
+}
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 11/13] drm/vino: add the USB driver frontend
2026-08-26 16:37 ` [PATCH v3 11/13] drm/vino: add the USB driver frontend Mike Lothian
@ 2026-08-26 17:01 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 17:01 UTC (permalink / raw)
To: Mike Lothian; +Cc: ojeda, dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Calling sleeping functions in Plane and CRTC atomic callbacks
- [Low] Non-compliant import formatting.
--
commit 8a75fe9a885fe8920f6bb62eb588d5aa17fbe92e
Author: Mike Lothian <mike@fireburn.co.uk>
drm/vino: add the USB driver frontend
Vino drives DisplayLink DL3 docks as ordinary DRM devices, replacing the
out-of-tree EVDI module and the closed-source DisplayLinkManager daemon.
It adds the probe lifecycle, places the device by family, and handles
presence and EDID workers off the probe path. Sinks that drop and return
quickly are absorbed to prevent dock-wide re-activations.
> diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs
> new file mode 100644
> index 0000000000000..0813faa43c031
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/vino.rs
> @@ -0,0 +1,1747 @@
> +// SPDX-License-Identifier: GPL-2.0
> +// SPDX-FileCopyrightText: Copyright (C) 2026 Mike Lothian
> +
> +//! DRM/KMS driver for DisplayLink DL3 docks.
> +//!
> +//! Vino drives the Dell Universal Dock D6000 using a clean-room implementation of its USB control,
> +//! HDCP authentication and compressed video protocols. Each device owns its control session and
> +//! exposes two atomic KMS pipelines backed by shmem GEM objects.
> +
> +use kernel::{
> + alloc::flags::GFP_KERNEL,
> + alloc::Flags,
> + device::{self, Core},
[Severity: Low]
Does this import block violate the vertical import style requirement?
The kernel Rust coding guidelines strictly require a vertical import style
instead of nested brace groups.
[ ... ]
Also in drivers/gpu/drm/vino/drm_sink/mode_objects.rs:
[Severity: High]
Does this call read_cursor_bgra() in VinoPlane::atomic_update() from atomic
context?
VinoPlane::atomic_update():
if let Ok(bgra) = read_cursor_bgra(fb, usize::from(w), usize::from(h)) {
read_cursor_bgra() allocates memory with GFP_KERNEL and invokes vmap(), both
of which can sleep. Plane and CRTC atomic callbacks run in atomic context
where sleeping is forbidden, which can cause kernel warnings and deadlocks.
[Severity: High]
Does queue_scanout() sleep here while in atomic context?
VinoPlane::atomic_update():
data.queue_scanout(
dev,
fb,
PendingScanout { ... },
);
It acquires the data.shadow Mutex and calls SourceBindingCache::get(), which
allocates with GFP_KERNEL and calls vmap().
[Severity: High]
Does this acquire a sleeping Mutex inside VinoCrtc::atomic_enable()?
VinoCrtc::atomic_enable():
let mut pinned = crtc.vblank_pinned.lock();
CRTC atomic callbacks run in atomic context where sleeping is forbidden.
[Severity: High]
Do these acquire multiple sleeping Mutexes inside VinoCrtc::atomic_disable()?
VinoCrtc::atomic_disable():
drop(crtc.vblank_pinned.lock().take());
...
data.pending_scanout.lock()[connector as usize] = None;
data.settle_repaint.lock()[connector as usize] = None;
data.shadow[connector as usize].lock().discard();
CRTC atomic callbacks run in atomic context, so acquiring sleeping locks here
is forbidden and can cause kernel warnings or deadlocks.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=11
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 12/13] drm/vino: allow the driver to be built
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (10 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 11/13] drm/vino: add the USB driver frontend Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 17:04 ` sashiko-bot
2026-08-26 16:37 ` [PATCH v3 13/13] Documentation/gpu: document the Vino driver Mike Lothian
12 siblings, 1 reply; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, linux-kernel, rust-for-linux
Add the Kconfig entry, the Makefile rule and the MAINTAINERS record, so the
preceding commits become a module.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
MAINTAINERS | 6 +++++
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/vino/Kconfig | 42 +++++++++++++++++++++++++++++++++++
drivers/gpu/drm/vino/Makefile | 2 ++
5 files changed, 52 insertions(+)
create mode 100644 drivers/gpu/drm/vino/Kconfig
create mode 100644 drivers/gpu/drm/vino/Makefile
diff --git a/MAINTAINERS b/MAINTAINERS
index 2b548bba6525..79ccf112fabd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8537,6 +8537,12 @@ S: Supported
T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
F: drivers/gpu/drm/udl/
+DRM DRIVER FOR VINO DISPLAYLINK DL3 DEVICES
+M: Mike Lothian <mike@fireburn.co.uk>
+L: dri-devel@lists.freedesktop.org
+S: Maintained
+F: drivers/gpu/drm/vino/
+
DRM DRIVER FOR VIRTUAL KERNEL MODESETTING (VKMS)
M: Louis Chauvet <louis.chauvet@bootlin.com>
R: Haneen Mohammed <hamohammed.sa@gmail.com>
diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index 323422861e8f..ac73fc425c59 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -358,6 +358,7 @@ source "drivers/gpu/drm/vboxvideo/Kconfig"
source "drivers/gpu/drm/vc4/Kconfig"
source "drivers/gpu/drm/verisilicon/Kconfig"
source "drivers/gpu/drm/vgem/Kconfig"
+source "drivers/gpu/drm/vino/Kconfig"
source "drivers/gpu/drm/virtio/Kconfig"
source "drivers/gpu/drm/vkms/Kconfig"
source "drivers/gpu/drm/vmwgfx/Kconfig"
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index e635fcffd379..29ab96f6e8de 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -185,6 +185,7 @@ obj-$(CONFIG_DRM_VC4) += vc4/
obj-$(CONFIG_DRM_VMWGFX)+= vmwgfx/
obj-$(CONFIG_DRM_VGEM) += vgem/
obj-$(CONFIG_DRM_VKMS) += vkms/
+obj-$(CONFIG_DRM_VINO) += vino/
obj-$(CONFIG_DRM_NOUVEAU) +=nouveau/
# nova-drm is built from drivers/gpu/Makefile together with nova-core.
obj-$(CONFIG_DRM_EXYNOS) +=exynos/
diff --git a/drivers/gpu/drm/vino/Kconfig b/drivers/gpu/drm/vino/Kconfig
new file mode 100644
index 000000000000..fcc761b4654b
--- /dev/null
+++ b/drivers/gpu/drm/vino/Kconfig
@@ -0,0 +1,42 @@
+# SPDX-License-Identifier: GPL-2.0
+config DRM_VINO
+ tristate "DisplayLink DL3 (Vino) open driver"
+ depends on USB
+ depends on DRM
+ depends on RUST
+ # RUST_DRM_GEM_SHMEM_HELPER depends on MMU; inherit that dependency here so the
+ # select below cannot generate an unmet-direct-dependency warning on nommu.
+ depends on MMU
+ select DRM_KMS_HELPER
+ select RUST_DRM_GEM_SHMEM_HELPER
+ select CRYPTO_RSA
+ select RUST_CRYPTO_AKCIPHER
+ select RUST_CRYPTO_LIB_AES
+ select RUST_CRYPTO_LIB_SHA256
+ select FW_LOADER
+ select RUST_FW_LOADER_ABSTRACTIONS
+ select FW_UPLOAD
+ help
+ Open in-kernel Rust driver for DisplayLink DL3 docks: the DL-3x00
+ port replicators, the Dell Universal Dock D6000 and other DL-6xxx
+ docks, and the DL-7400 quad-display docks. A dock is matched on its
+ USB function rather than on a product id, and identified from the
+ vendor descriptor it carries.
+
+ It binds the dock over USB, runs the HDCP 2.2 control plane, mode-set
+ and the Vino codec, and registers a DRM/KMS sink that scans out to the
+ dock's video endpoints.
+
+ To compile this as a module, choose M here: the module is called vino.
+
+ If unsure, say N.
+
+config DRM_VINO_KUNIT_TEST
+ bool "KUnit tests for the Vino driver"
+ depends on DRM_VINO && KUNIT
+ help
+ Build Vino's protocol, crypto and codec unit tests into the driver.
+ The tests run when the driver is loaded, so this option is intended
+ for driver development and test kernels rather than normal use.
+
+ If in doubt, say N.
diff --git a/drivers/gpu/drm/vino/Makefile b/drivers/gpu/drm/vino/Makefile
new file mode 100644
index 000000000000..6e39668040f3
--- /dev/null
+++ b/drivers/gpu/drm/vino/Makefile
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_DRM_VINO) += vino.o
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 12/13] drm/vino: allow the driver to be built
2026-08-26 16:37 ` [PATCH v3 12/13] drm/vino: allow the driver to be built Mike Lothian
@ 2026-08-26 17:04 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 17:04 UTC (permalink / raw)
To: Mike Lothian; +Cc: ojeda, dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] Selecting CRYPTO_RSA without inheriting its dependency on CRYPTO causes unmet direct dependency warnings.
- [Low] The Kconfig selects non-existent config symbols: RUST_CRYPTO_AKCIPHER, RUST_CRYPTO_LIB_AES, and RUST_CRYPTO_LIB_SHA256.
--
commit 580f8ac72cc85f194a430cb59459a83ca73e95c1
Author: Mike Lothian <mike@fireburn.co.uk>
drm/vino: allow the driver to be built
This commit adds the Kconfig entry, Makefile rule, and MAINTAINERS record
for the DRM driver for DisplayLink DL3 devices, allowing the driver to be
built as a module.
> diff --git a/drivers/gpu/drm/vino/Kconfig b/drivers/gpu/drm/vino/Kconfig
> new file mode 100644
> index 0000000000000..fcc761b4654b7
> --- /dev/null
> +++ b/drivers/gpu/drm/vino/Kconfig
> @@ -0,0 +1,42 @@
> +# SPDX-License-Identifier: GPL-2.0
> +config DRM_VINO
> + tristate "DisplayLink DL3 (Vino) open driver"
> + depends on USB
> + depends on DRM
> + depends on RUST
> + # RUST_DRM_GEM_SHMEM_HELPER depends on MMU; inherit that dependency here so the
> + # select below cannot generate an unmet-direct-dependency warning on nommu.
> + depends on MMU
> + select DRM_KMS_HELPER
> + select RUST_DRM_GEM_SHMEM_HELPER
> + select CRYPTO_RSA
[Severity: Low]
Will this select statement cause unmet direct dependency warnings if the
CRYPTO core is disabled?
Selecting CRYPTO_RSA without inheriting its dependency on CRYPTO can cause
Kconfig warnings and potential build failures (linking errors for missing
crypto core functions) if CRYPTO_RSA is forcibly enabled while the CRYPTO
core is disabled.
> + select RUST_CRYPTO_AKCIPHER
> + select RUST_CRYPTO_LIB_AES
> + select RUST_CRYPTO_LIB_SHA256
[Severity: Low]
Are these Kconfig symbols actually defined in the kernel tree?
It appears that RUST_CRYPTO_AKCIPHER, RUST_CRYPTO_LIB_AES, and
RUST_CRYPTO_LIB_SHA256 do not exist anywhere in Kconfig. Selecting
non-existent symbols has no effect, which can lead to silent build failures
(like missing Rust bindings for crypto) because the intended dependencies
are not actually enabled.
> + select FW_LOADER
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=12
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 13/13] Documentation/gpu: document the Vino driver
2026-08-26 16:37 [PATCH v3 0/13] drm/vino: a Rust driver for DisplayLink DL3 docks Mike Lothian
` (11 preceding siblings ...)
2026-08-26 16:37 ` [PATCH v3 12/13] drm/vino: allow the driver to be built Mike Lothian
@ 2026-08-26 16:37 ` Mike Lothian
2026-08-26 17:10 ` sashiko-bot
2026-08-26 17:28 ` Randy Dunlap
12 siblings, 2 replies; 24+ messages in thread
From: Mike Lothian @ 2026-08-26 16:37 UTC (permalink / raw)
To: dri-devel
Cc: Mike Lothian, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Jonathan Corbet, Shuah Khan,
Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, Nathan Chancellor,
Nick Desaulniers, Bill Wendling, Justin Stitt, linux-doc,
linux-kernel, rust-for-linux, llvm
Describe how a dock is identified and placed by family rather than by
product ID, what the driver implements in-kernel in place of EVDI and
DisplayLinkManager, and the module parameters an unfamiliar dock or a
converter with broken DDC may need.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
Documentation/gpu/drivers.rst | 1 +
Documentation/gpu/vino.rst | 254 ++++++++++++++++++++++++++++++++++
MAINTAINERS | 1 +
3 files changed, 256 insertions(+)
create mode 100644 Documentation/gpu/vino.rst
diff --git a/Documentation/gpu/drivers.rst b/Documentation/gpu/drivers.rst
index 20d2c454aa1d..4b524d6980c4 100644
--- a/Documentation/gpu/drivers.rst
+++ b/Documentation/gpu/drivers.rst
@@ -17,6 +17,7 @@ GPU Driver Documentation
tve200
v3d
vc4
+ vino
vkms
bridge/dw-hdmi
xen-front
diff --git a/Documentation/gpu/vino.rst b/Documentation/gpu/vino.rst
new file mode 100644
index 000000000000..98e82297a82a
--- /dev/null
+++ b/Documentation/gpu/vino.rst
@@ -0,0 +1,254 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+==========================
+Vino DisplayLink DL3 driver
+==========================
+
+Vino is a Rust DRM/KMS driver for DisplayLink DL3 USB display devices. Three
+hardware families are supported: Ella (DL-3x00 silicon, e.g. the HP 3005pr),
+Ridge (DL-6xxx silicon, e.g. the Dell Universal Dock D6000) and Navarro
+(DL-7000 silicon, e.g. the DL-7400 quad-display docks). Each family has a
+profile carrying the endpoints, strip geometry, connector count, link limits
+and pacing of its hardware; the rest of the driver reads those values rather
+than branching on the model.
+
+Device identification
+=====================
+
+The driver binds to a DisplayLink *function*, not to a list of product IDs: any
+device with vendor ``17e9`` exposing an interface of class ``0xff``, subclass
+``0``, protocol ``0x03`` (a DL3 display function; ``0x00`` is the older ``udl``
+hardware), plus that device's USB DFU interface. This matches what the vendor's
+own udev rules key on, and means a dock nobody has tested is offered to the
+driver rather than ignored.
+
+Which family a device belongs to is then read from the device itself. Every
+DisplayLink dock carries a sixteen-byte vendor descriptor, type ``0x40``, in
+its ordinary configuration descriptor, holding the running firmware version and
+an eight-character platform name -- ``NavaDock``, ``RidgeDoc`` and so on. It is
+read with a standard ``GET_DESCRIPTOR``, needing no session and no crypto, so
+identification happens at probe.
+
+A device whose identity names a family the driver cannot drive is declined by
+name, so its owner gets a log line and something worth reporting instead of a
+driver guessing at an unknown wire format. A device whose identity cannot be
+*read* falls back to a small product-ID quirk table, so a transient descriptor
+failure does not cost a known dock its displays.
+
+The number of connectors comes from how many of the family's video endpoints
+the device actually exposes, bounded by the profile. A dock in a known family
+with fewer outputs is therefore driven with the outputs it has.
+
+The driver owns the USB device and implements the dock's initialization,
+HDCP 2.2 authentication, encrypted control protocol, downstream monitor
+management, mode programming, cursor updates, video compression, and USB
+submission in the kernel. It does not use EVDI or a userspace display daemon.
+
+Configuration
+=============
+
+The driver is selected by ``CONFIG_DRM_VINO``. It requires Rust, USB, DRM,
+MMU support, the Rust DRM shmem helper, and the kernel crypto primitives used
+by HDCP 2.2.
+
+``CONFIG_DRM_VINO=m`` builds ``vino.ko``.
+
+Verbose protocol and scanout diagnostics are disabled by default. Pass
+``debug=1`` when loading the module to enable them::
+
+ modprobe vino debug=1
+
+Errors, connection changes, and session state remain visible without this
+parameter.
+
+The remaining module parameters are for recovery and diagnosis:
+
+``edid_override``
+ Bitmask of connectors whose EDID the dock cannot read -- typically a monitor
+ behind a DP-to-HDMI converter that mangles DDC -- and which are described by
+ DRM's own EDID override instead.
+
+``force_flash``
+ Write the packaged dock firmware even when the dock already runs that version
+ or newer. See `Firmware updates`_.
+
+``rtc_utc_offset_minutes``
+ Local offset from UTC, in minutes east, used when synchronizing a Navarro
+ dock's real-time clock.
+
+``trace_crypto``
+ Discloses the ephemeral control and video keys of one session so that a
+ ``usbmon`` capture can be decrypted. For protocol work only.
+
+The optional ``CONFIG_DRM_VINO_KUNIT_TEST`` setting builds the driver's KUnit
+suite. It is intended for development kernels and is disabled by default.
+
+KMS model
+=========
+
+A dock exposes independent display connectors: two on the D6000, four on the
+DL-7400, where they are multiplexed over two video endpoints. Each has:
+
+* one primary plane using ``DRM_FORMAT_XRGB8888``, and ``DRM_FORMAT_XRGB2101010``
+ as well where the dock's link carries ten bits per channel;
+* one cursor plane using ``DRM_FORMAT_ARGB8888``;
+* one CRTC, encoder, and connector;
+* a downstream EDID channel; and
+* a bulk-OUT video endpoint, which two connectors may share.
+
+Atomic commits record the latest desired state and wake an ordered,
+device-owned control queue. Blocking USB transactions are never issued from
+the atomic callback. A transient control failure retains the desired
+generation and retries it; a newer atomic state always supersedes an older
+retry.
+
+The primary plane supports the four DRM rotations and both reflection axes in
+all valid combinations. Rotated and reflected frames are conservatively sent
+as full updates. Their independent codec strips are still encoded in parallel;
+identity scanout additionally supports damage updates and encoded-strip reuse.
+
+Mode validation
+===============
+
+A mode reaches the dock as a timing plus two control words describing its sync
+polarity and its CTA video identification code. Those words are taken verbatim
+from decrypted vendor captures for the timings a capture covers -- 1920x1080 at
+60 and 120 Hz, and 2560x1440 CVT-RB at 60 and 120 Hz -- and derived from the
+mode's own sync flags and VIC otherwise, so a monitor's native timing is driven
+rather than approximated.
+
+A mode is refused when it exceeds the ceilings its dock's profile names: the
+highest pixel clock a single connector may carry, an optional refresh-rate cap
+where the vendor driver is known to clamp, and the dock-wide pixel budget. The
+budget is shared, so the atomic check enforces the combined rate of the
+connectors a commit leaves enabled, while mode validation refuses only a single
+mode too large for the whole dock.
+
+Framebuffer ownership and damage
+================================
+
+The dock cannot scan out a GEM object directly. Vino therefore copies changed
+strips from the shmem framebuffer into driver-owned snapshots before the
+atomic commit completes. The compositor may reuse its source buffer after
+that snapshot without racing the encoder.
+
+Each head retains at most four validated, owned shmem mappings, matching a
+typical compositor swapchain. Repeated flips reuse those prepared mappings;
+round-robin eviction and DPMS teardown keep pinned memory bounded. This is the
+USB-display equivalent of preparing buffers before submission and requires no
+driver-specific userspace API.
+
+Encoding and USB submission run asynchronously on per-head workers. Damage is
+tracked against the last frame successfully submitted to the dock, not merely
+against the previous atomic commit. This preserves changes when commits are
+coalesced or a transfer fails.
+
+A strip whose content changes is charged one transmission for each buffer the
+dock rotates through, plus one, and remains selected until that debt is paid.
+One presentation reaches exactly one of those buffers, so a strip delivered to
+only some of them would leave the panel alternating between old and new
+content. A surface with no change and no debt outstanding sends nothing.
+
+The video path keeps a bounded, persistent USB request ring. The first
+presentation after a mode change carries the decoder arm sequence and the
+opening frame in one USB request, as required by the receiver.
+
+Control and authentication
+==========================
+
+One per-device session owns the HDCP and encrypted-control counters, keys,
+nonces, EP02 submissions, and EP84 replies. The control queue serializes
+transactions so a KMS update cannot interleave with a heartbeat or monitor
+operation.
+
+Initial transport, authentication, and encrypted-control setup is retried after
+transient failures. Once authentication has succeeded, a timeout while
+discovering one downstream monitor does not discard the live session. That
+head remains disconnected until the bounded runtime re-engagement path
+obtains a valid EDID.
+
+HDCP message identifiers and HDMI mode matching use the DRM display helpers.
+AES, AES-CMAC, SHA-256, HMAC-SHA256, and RSA operations use kernel crypto
+interfaces. Session material is stored per device and is not exposed through
+a driver-specific userspace API.
+
+Colour management
+=================
+
+The CRTC advertises ``CTM`` and a 256-entry ``GAMMA_LUT``, and both are applied
+in software during encoding. A dock has no colour hardware to program, so a
+compositor correcting through those properties -- as GNOME's Night Light and
+KDE's Night Colour do -- would otherwise have nowhere to put the correction on
+this output while native outputs are corrected normally.
+
+Colour depth and HDR
+====================
+
+On a dock whose silicon carries ten bits per channel, each connector also
+advertises ``max bpc``, ``Colorspace`` and ``HDR_OUTPUT_METADATA``. The wire
+format is one codec parameterised by sample depth rather than two: an HDR
+frame differs from an SDR one only in how deep its samples are, in the escape
+ceilings the entropy coder is allowed to reach, and in the transfer function
+and colorimetry stated in the mode set.
+
+The link's depth is taken from ``max bpc`` together with the PQ transfer
+function the compositor attached, not from the committed framebuffer's format:
+driving a ten-bit link from an eight-bit surface is ordinary, and a sample is
+widened into the deeper link after it is decoded. A ten-bit connector costs the
+dock a third more bandwidth per pixel, so the shared budget is priced at the
+deepest connector in use, and where a pair of modes does not fit at ten bits
+the depth gives way rather than the mode -- a compositor handed ``EINVAL``
+disables the output instead of asking for a shallower link.
+
+Firmware updates
+================
+
+A dock reports the firmware it is running in a vendor descriptor, and the
+vendor's ``*-release.spkg`` packages state the version they carry. If the
+kernel firmware loader can supply a package that is newer than what the dock
+is running, vino writes it over USB DFU before opening a control session.
+
+Two deliberate paths exist alongside that. ``force_flash=1`` writes the
+packaged image even when the dock already runs that version or a newer one,
+which is how a dock left in a bad state is recovered. The
+``/sys/class/firmware/vino-<dock>/`` upload interface takes an image from
+userspace and writes whatever version it is, which is the only way to re-flash
+the running version or to go back to an older one. Both refuse an image that is
+not a DisplayLink package or is for another dock family: the DFU interface
+supports no upload, so the running image cannot be read back and there is
+nothing to restore from.
+
+Monitor handling
+================
+
+EDID reads are tunneled through the dock's encrypted control protocol. An
+unsolicited downstream event schedules a fresh presence probe. Removal,
+reattachment, and a connector powered down by userspace are tracked separately
+so a deliberate power transition is not reported as a physical unplug.
+
+The initial driver does not register a virtual I2C adapter for DDC/CI monitor
+controls.
+
+Disconnect
+==========
+
+USB I/O is guarded by a revocable I/O window. Disconnect closes that window
+before workers and request queues are drained, preventing new transfers from
+starting during teardown. The owned DRM registration, timers, work queues,
+frame snapshots, and USB queues are then released by their Rust owners.
+
+Testing and validation
+======================
+
+With ``CONFIG_DRM_VINO_KUNIT_TEST=y``, the protocol and codec have KUnit
+coverage for cryptographic known-answer tests, captured control-message
+vectors, mode profiles, decoder-arm framing, codec boundaries, damage
+selection, USB record construction, and serial-versus-parallel output for all
+rotation and reflection combinations.
+
+A focused compile check is::
+
+ make LLVM=1 rust/kernel.o drivers/gpu/drm/vino/vino.o
+
+External-module ``modpost`` also needs a completed kernel build with a
+matching ``Module.symvers``.
diff --git a/MAINTAINERS b/MAINTAINERS
index 79ccf112fabd..1b0d496897ea 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8541,6 +8541,7 @@ DRM DRIVER FOR VINO DISPLAYLINK DL3 DEVICES
M: Mike Lothian <mike@fireburn.co.uk>
L: dri-devel@lists.freedesktop.org
S: Maintained
+F: Documentation/gpu/vino.rst
F: drivers/gpu/drm/vino/
DRM DRIVER FOR VIRTUAL KERNEL MODESETTING (VKMS)
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v3 13/13] Documentation/gpu: document the Vino driver
2026-08-26 16:37 ` [PATCH v3 13/13] Documentation/gpu: document the Vino driver Mike Lothian
@ 2026-08-26 17:10 ` sashiko-bot
2026-08-26 17:28 ` Randy Dunlap
1 sibling, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-08-26 17:10 UTC (permalink / raw)
To: Mike Lothian; +Cc: dri-devel, ojeda
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Sleeping functions (GFP_KERNEL allocations and Mutex locks) are called from atomic KMS callbacks.
- [Low] Sphinx build warning due to title overline and underline being shorter than the title text.
--
commit fb3eb13f9dac446a54542b31ed570e00753ca4c1
Author: Mike Lothian <mike@fireburn.co.uk>
Documentation/gpu: document the Vino driver
Describe how a dock is identified and placed by family rather than by
product ID, what the driver implements in-kernel in place of EVDI and
DisplayLinkManager, and the module parameters an unfamiliar dock or a
converter with broken DDC may need.
> diff --git a/Documentation/gpu/vino.rst b/Documentation/gpu/vino.rst
> new file mode 100644
> index 0000000000000..98e82297a82a9
> --- /dev/null
> +++ b/Documentation/gpu/vino.rst
> @@ -0,0 +1,254 @@
> +.. SPDX-License-Identifier: GPL-2.0-only
> +
> +==========================
> +Vino DisplayLink DL3 driver
> +==========================
[Severity: Low]
Does this title formatting violate reStructuredText syntax requirements?
The overline and underline are 26 characters long, but the title text is 27
characters long, which can cause a Sphinx build warning.
[Severity: High]
Can these sleeping functions be called from an atomic context?
In read_cursor_bgra(), a GFP_KERNEL allocation is made, which can sleep:
drivers/gpu/drm/vino/drm_sink/scanout.rs:read_cursor_bgra() {
...
out.resize(len, 0, GFP_KERNEL)?;
...
}
It appears this is called from the atomic VinoPlane::atomic_update callback.
Similarly, do these code paths acquire a sleepable kernel Mutex during atomic
contexts?
drivers/gpu/drm/vino/drm_sink/dispatch.rs:queue_scanout() {
...
let pending = self.pending_scanout.lock();
...
let mut pool = self.shadow[connector].lock();
...
}
This appears to be called in an atomic plane update context.
drivers/gpu/drm/vino/drm_sink/dispatch.rs:queue_cmd() {
...
let mut pending = self.pending_kms.lock();
...
}
This seems to be called from the atomic_enable and atomic_disable callbacks.
drivers/gpu/drm/vino/drm_sink.rs:update_color() {
...
let changed = if let Some(slot) = self.color.lock().get_mut(connector) {
...
}
This also appears to be called from VinoCrtc::atomic_enable and atomic_disable.
Calling sleeping functions like GFP_KERNEL allocations and Mutex locks from
atomic context can lead to might_sleep kernel warnings, system instability,
and deadlocks.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826163913.7052-1-mike@fireburn.co.uk?part=13
^ permalink raw reply [flat|nested] 24+ messages in thread* Re: [PATCH v3 13/13] Documentation/gpu: document the Vino driver
2026-08-26 16:37 ` [PATCH v3 13/13] Documentation/gpu: document the Vino driver Mike Lothian
2026-08-26 17:10 ` sashiko-bot
@ 2026-08-26 17:28 ` Randy Dunlap
1 sibling, 0 replies; 24+ messages in thread
From: Randy Dunlap @ 2026-08-26 17:28 UTC (permalink / raw)
To: Mike Lothian, dri-devel
Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Shuah Khan, Miguel Ojeda,
Boqun Feng, Gary Guo, Björn Roy Baron, Benno Lossin,
Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
Onur Özkan, Nathan Chancellor, Nick Desaulniers,
Bill Wendling, Justin Stitt, linux-doc, linux-kernel,
rust-for-linux, llvm
On 8/26/26 9:37 AM, Mike Lothian wrote:
> diff --git a/Documentation/gpu/vino.rst b/Documentation/gpu/vino.rst
> new file mode 100644
> index 000000000000..98e82297a82a
> --- /dev/null
> +++ b/Documentation/gpu/vino.rst
> @@ -0,0 +1,254 @@
> +.. SPDX-License-Identifier: GPL-2.0-only
> +
> +==========================
> +Vino DisplayLink DL3 driver
> +==========================
Documentation/gpu/vino.rst:3: WARNING: Title overline too short.
==========================
Vino DisplayLink DL3 driver
========================== [docutils]
Extend both the overline and the underline lines by one '='.
--
~Randy
^ permalink raw reply [flat|nested] 24+ messages in thread