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 <[email protected]>
---
 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(())
+    }
+}

Reply via email to