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