Add the drm_sink module: register a real struct drm_device with a hand-rolled atomic mode-setting pipeline so the dock appears to userspace as a mode-settable card/renderD node -- one CRTC driven by a primary plane and a cursor plane, a virtual encoder, and a virtual connector whose mode list comes from the dock's real EDID (falling back to 1080p), with GEM-shmem dumb buffers and drm_gem_fb_create framebuffers.
probe() now allocates and registers the DRM device on the control interface; the bring-up work item caches the dock EDID on it and, once (if) the CP engages, publishes the live session so the KMS callbacks can emit runtime CP (mode-set on a modeset, cursor on motion). On every page-flip the primary plane's update vmaps the framebuffer, encodes it with the Vino codec and pushes it to the EP08 video endpoint, gated on CP_ENGAGED so frames are only sent once the dock's cipher is live. Signed-off-by: Mike Lothian <[email protected]> Assisted-by: Claude:claude-opus-4-8 [Claude-Code] --- drivers/gpu/drm/vino/drm_sink.rs | 1333 ++++++++++++++++++++++++++++++ drivers/gpu/drm/vino/vino.rs | 145 +++- 2 files changed, 1458 insertions(+), 20 deletions(-) create mode 100644 drivers/gpu/drm/vino/drm_sink.rs diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs new file mode 100644 index 000000000000..afbf883fba36 --- /dev/null +++ b/drivers/gpu/drm/vino/drm_sink.rs @@ -0,0 +1,1333 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Phase 3 (DRM/KMS sink): register a real `struct drm_device` with a full atomic +//! mode-setting pipeline so the dock appears to userspace as a `card`/`renderD` node +//! that can be `drmModeSetCrtc`'d. A hand-rolled atomic pipeline: one CRTC driven by a +//! primary plane (`primary_atomic_update` -> EP08 scanout) and a cursor plane +//! (`cursor_atomic_update` -> cursor CP), a virtual encoder, and a virtual connector whose +//! mode list comes from the dock's real EDID (falling back to 1080p), with GEM-shmem dumb +//! buffers and `drm_gem_fb_create` framebuffers. (Earlier this was `drm_simple_display_pipe`, +//! swapped out because that helper is primary-plane-only and can't carry a cursor plane.) +//! The scanout/cursor writes are gated on the CP-arming blocker (see `docs/BLOCKER.md`). The +//! KMS C bindings are pulled in by +//! `patches/drm/0001` (bindings_helper.h headers + the `Driver::FEAT_MODESET` / +//! `Driver::FEAT_ATOMIC` flags + a public `Device::as_raw`); see `patches/README.md`. + +use core::ptr; +use kernel::{ + bindings, drm, + error::{ + code::{EINVAL, ENOMEM}, + to_result, + }, + prelude::*, + sync::{aref::ARef, new_mutex, Mutex}, + types::Opaque, +}; + +/// Fallback connector mode advertised by `get_modes` when the dock has not delivered a real +/// downstream EDID yet. The live scanout geometry follows the actual framebuffer/negotiated +/// mode (see `scanout_one`), so this is only the no-EDID default, not a hard scanout limit. +const FALLBACK_W: i32 = 1920; +const FALLBACK_H: i32 = 1080; + +/// The DRM driver marker type. +pub(super) struct VinoDrmDriver; + +/// Convenience alias for our concrete `drm::Device`. +pub(super) type VinoDrmDevice = drm::Device<VinoDrmDriver>; + +/// `DRM_FORMAT_XRGB8888` (`fourcc_code('X','R','2','4')`); the dock scans out 32bpp. +const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258; +/// `DRM_FORMAT_ARGB8888` (`fourcc_code('A','R','2','4')`); the cursor sprite carries alpha. +const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241; +/// Primary-plane format list (opaque 32bpp scanout). +static PRIMARY_FORMATS: [u32; 1] = [DRM_FORMAT_XRGB8888]; +/// Cursor-plane format list (alpha sprite). ARGB8888 little-endian memory order is +/// `B,G,R,A` per pixel -- already the BGRA byte layout `cp::cursor_image` wants. +static CURSOR_FORMATS: [u32; 1] = [DRM_FORMAT_ARGB8888]; +/// Hardware cursor sprite size (the dock cursor is 64x64; `DRM_CAP_CURSOR_WIDTH/HEIGHT`). +const CURSOR_SIZE: u32 = 64; +/// `GAMMA_LUT` size advertised on the CRTC. 256 entries matches the 8-bit scanout channels; +/// the LUT is applied host-side in the scanout conversion (see `read_gamma_lut`). +const GAMMA_SIZE: u32 = 256; + +/// Per-mode pixel-clock ceiling (kHz) for a single head -- about 4K@60 (CEA 594 MHz). +/// `mode_valid` prunes any single mode above this from a connector's advertised list. +const MAX_HEAD_CLOCK_KHZ: i32 = 600_000; +/// Combined pixel-clock budget (kHz) summed over all *active* heads -- the dock's DL3 link +/// ceiling. This is a deliberately conservative *raw* pixel-rate proxy (DisplayLink compresses +/// the stream, so the true USB budget is higher and content-dependent; the WHT codec exists for +/// the tight cases). At 1 GHz it admits one 4K@60, 4K@60 + QHD@60, or dual-QHD@60, and rejects +/// dual-4K -- matching the D6000's real multi-monitor envelope. Tune to taste / per dock +/// model. +const MAX_TOTAL_CLOCK_KHZ: i64 = 1_000_000; + +/// Mutable scanout state, guarded because the atomic `update` callback may run +/// concurrently with itself across heads. Holds the stateful Vino encoder (created +/// lazily on the first flip, once the buffer geometry is known) and the EP08 frame +/// sequence counter. +pub(super) struct ScanoutState { + enc: Option<super::video::Encoder>, + /// Reusable `width*height` RGB565 conversion buffer, allocated once alongside `enc`. + /// Previously `encode_and_send` did a fresh `KVec::with_capacity(w*h)` on every pageflip; + /// at 1080p that is a ~4 MiB *contiguous* kmalloc (order 11), above the allocator's limit, + /// so the page allocator WARNed and returned `ENOMEM` every frame. vmalloc-backed + + /// persistent: virtually-contiguous (no high-order page need) and allocated once. + cur: VVec<u16>, + seq: u32, + /// Geometry (`width`, `height`) the encoder/`cur` were allocated for. The scanout follows + /// the live framebuffer size, so a mode switch re-allocates them when this no longer + /// matches. + dims: (usize, usize), + /// The [`super::cp::Timing`] of the mode the compositor last enabled on the CRTC, captured + /// in [`crtc_atomic_enable`] via [`super::cp::timing_from_drm_mode`]. This is the + /// multi-mode hook: + /// userspace can pick *any* mode from the EDID-derived list and the chosen + /// `drm_display_mode` + /// is recorded here so the live mode-set CP message reflects it (rather than always the + /// EDID-preferred timing). The CP send itself is gated on the engagement wall + session + /// plumbing (see the doc note in `crtc_atomic_enable`). + active_timing: Option<super::cp::Timing>, + /// Size of the last EP08 frame produced, used to pre-reserve the next frame's + /// buffer. The encoded stream size is stable frame-to-frame (it tracks the damage + /// area), so seeding `KVec::with_capacity` from it makes the encode grow the buffer + /// at most once instead of reallocating repeatedly as runs are appended. + hint: usize, +} + +/// The live CP session the bring-up work item publishes once the dock engages the cipher +/// (`acks > 0`), so the KMS callbacks can seal+send runtime CP messages -- a mode-set when the +/// compositor switches mode, a cursor message on pointer motion -- that continue the SAME +/// keystream the bring-up setup left off at. `wire_seq` is the AES-CTR block counter (advanced +/// by the content blocks of each send; the appended Dl3Cmac tag is not part of the keystream) +/// and `counter` the dock-echoed inner CP counter. Both advance per send under the mutex. +pub(super) struct CpLink { + ks: [u8; 16], + riv: [u8; 8], + wire_seq: u32, + counter: u16, +} + +/// Number of display heads the D6000 dock drives. The protocol routes video by endpoint +/// (head 0 -> EP 0x08, head 1 -> EP 0x0a; heads 2/3 -> 0x0b/0x0c are documented but their CP +/// riv / EDID-request encoding is unconfirmed, so only 2 heads are wired). The DL3 CP stream +/// selects the head via the riv `byte0 ^ 0x80` (head 0 base / head 1 flipped). +pub(super) const NHEADS: usize = 2; +/// Per-head video bulk-OUT endpoint (`PROTOCOL.md`). Index by [`Head::index`]. +const HEAD_EP: [u8; NHEADS] = [0x08, 0x0a]; + +/// One display head: its own CRTC + primary plane (scanout) + cursor plane + encoder + +/// connector, plus per-head scanout state and cached monitor EDID. The vtables are shared +/// across heads (in [`VinoDrmData`]); the callbacks recover the head from the C object +/// pointer. All C objects are zeroed at init and filled by [`kms_init`]. +#[pin_data] +pub(super) struct Head { + /// 0-based head index. Selects the video EP ([`HEAD_EP`]) and the CP riv (head 1 flips + /// the riv `byte0`); see the scanout EP and [`VinoDrmData::send_cp`]. + index: u8, + /// One-shot: this head's cursor `create` (sprite dimensions) was sent before its first + /// image upload (per head -- the global one would skip head 1's create). + cursor_primed: core::sync::atomic::AtomicBool, + #[pin] + scanout: Mutex<ScanoutState>, + /// This head's downstream-monitor EDID (`None` until the CP channel delivers it). Only + /// head 0's EDID is read during bring-up; per-head EDID requests are unconfirmed, so + /// head 1 falls back to a fixed CVT mode in `get_modes`. + #[pin] + cached_edid: Mutex<Option<KVec<u8>>>, + #[pin] + crtc: Opaque<bindings::drm_crtc>, + #[pin] + primary: Opaque<bindings::drm_plane>, + #[pin] + cursor: Opaque<bindings::drm_plane>, + #[pin] + encoder: Opaque<bindings::drm_encoder>, + #[pin] + connector: Opaque<bindings::drm_connector>, +} + +// SAFETY: as for `VinoDrmData` below -- the embedded C KMS objects are written only during +// single-threaded probe and thereafter serialised by the DRM core's own locks. +unsafe impl Send for Head {} +// SAFETY: see the `Send` impl above. +unsafe impl Sync for Head {} + +impl Head { + fn new(index: u8) -> impl PinInit<Self, Error> { + fn z<T>() -> impl PinInit<Opaque<T>, Error> { + // SAFETY: an all-zero C KMS object is a valid starting point (all callback + // pointers NULL); `kms_init` populates the rest via raw pointers under `unsafe`. + Opaque::try_ffi_init(|p: *mut T| { + unsafe { ptr::write_bytes(p, 0, 1) }; + Ok(()) + }) + } + try_pin_init!(Self { + index, + cursor_primed: core::sync::atomic::AtomicBool::new(false), + scanout <- new_mutex!(ScanoutState { + enc: None, + cur: VVec::new(), + seq: 0, + dims: (0, 0), + active_timing: None, + hint: 0, + }), + cached_edid <- new_mutex!(Option::<KVec<u8>>::None), + crtc <- z(), + primary <- z(), + cursor <- z(), + encoder <- z(), + connector <- z(), + }) + } + + /// This head's video bulk-OUT endpoint. + fn video_ep(&self) -> u8 { + HEAD_EP[self.index as usize] + } + + /// Fire a hotplug on this head's connector so the compositor re-probes [`detect`]. + fn fire_hotplug(&self) { + // SAFETY: called after `drm_dev_register`; the embedded connector is initialised + // and its `dev` is our live drm_device. Safe from process context. + unsafe { + let dev = (*self.connector.get()).dev; + if !dev.is_null() { + bindings::drm_kms_helper_hotplug_event(dev); + } + } + } +} + +/// DRM device-private data. Holds [`NHEADS`] display [`Head`]s (each a CRTC + primary + +/// cursor plane + encoder + connector) and the shared KMS vtables inline, so they keep +/// stable addresses for the device's lifetime. All C objects zeroed at init; filled by +/// [`kms_init`]. Also keeps the bound USB interface (to reach the video EPs) and the engaged +/// CP session. +#[pin_data] +pub(super) struct VinoDrmData { + intf: ARef<super::usb::Interface>, + /// The engaged CP session for runtime KMS-driven sends (`None` until + /// [`VinoDrmData::publish_session`]). See [`CpLink`] and [`VinoDrmData::send_cp`]. + #[pin] + cp_link: Mutex<Option<CpLink>>, + #[pin] + head0: Head, + #[pin] + head1: Head, + // Shared vtables (one set for all heads; the callbacks recover the head from the C + // object pointer). One `drm_plane_funcs` for both planes; per-plane helper funcs because + // the primary's `atomic_update` scans out while the cursor's sends cursor CP. + #[pin] + conn_funcs: Opaque<bindings::drm_connector_funcs>, + #[pin] + conn_helper: Opaque<bindings::drm_connector_helper_funcs>, + #[pin] + crtc_funcs: Opaque<bindings::drm_crtc_funcs>, + #[pin] + crtc_helper: Opaque<bindings::drm_crtc_helper_funcs>, + #[pin] + plane_funcs: Opaque<bindings::drm_plane_funcs>, + #[pin] + primary_helper: Opaque<bindings::drm_plane_helper_funcs>, + #[pin] + cursor_helper: Opaque<bindings::drm_plane_helper_funcs>, + #[pin] + encoder_funcs: Opaque<bindings::drm_encoder_funcs>, + #[pin] + mode_cfg_funcs: Opaque<bindings::drm_mode_config_funcs>, +} + +// SAFETY: the embedded C KMS objects are written only during single-threaded +// `probe()` (before `drm_dev_register`), and thereafter are owned and serialised by +// the DRM core under its own modeset/atomic locks -- Rust never aliases them again. +// This is the conventional assertion for drivers embedding C KMS state until the +// kernel grows safe Rust KMS abstractions. +unsafe impl Send for VinoDrmData {} +// SAFETY: see the `Send` impl above. +unsafe impl Sync for VinoDrmData {} + +impl VinoDrmData { + /// Zero-initialise all embedded C objects (so each `Option<fn>` vtable slot is + /// `None`); [`kms_init`] then fills in only the callbacks we implement. `intf` + /// is the bound USB interface, kept so the scanout path can reach EP08. + pub(super) fn new(intf: ARef<super::usb::Interface>) -> impl PinInit<Self, Error> { + fn z<T>() -> impl PinInit<Opaque<T>, Error> { + // SAFETY: an all-zero C KMS object / funcs table is a valid starting + // point (all callback pointers NULL); the `_init` helpers populate the + // rest, and we only read it back through raw pointers under `unsafe`. + Opaque::try_ffi_init(|p: *mut T| { + unsafe { ptr::write_bytes(p, 0, 1) }; + Ok(()) + }) + } + try_pin_init!(Self { + intf, + cp_link <- new_mutex!(Option::<CpLink>::None), + head0 <- Head::new(0), + head1 <- Head::new(1), + conn_funcs <- z(), + conn_helper <- z(), + crtc_funcs <- z(), + crtc_helper <- z(), + plane_funcs <- z(), + primary_helper <- z(), + cursor_helper <- z(), + encoder_funcs <- z(), + mode_cfg_funcs <- z(), + }) + } + + /// The display heads, in index order. + fn heads(&self) -> [&Head; NHEADS] { + [&self.head0, &self.head1] + } + + /// Recover the [`Head`] that owns a given C KMS object, by pointer identity. Used by the + /// connector/CRTC/plane callbacks (which receive a raw C pointer) to find their head. + fn head_by_connector(&self, c: *mut bindings::drm_connector) -> Option<&Head> { + self.heads().into_iter().find(|h| h.connector.get() == c) + } + fn head_by_crtc(&self, c: *mut bindings::drm_crtc) -> Option<&Head> { + self.heads().into_iter().find(|h| h.crtc.get() == c) + } + fn head_by_primary(&self, p: *mut bindings::drm_plane) -> Option<&Head> { + self.heads().into_iter().find(|h| h.primary.get() == p) + } + fn head_by_cursor(&self, p: *mut bindings::drm_plane) -> Option<&Head> { + self.heads().into_iter().find(|h| h.cursor.get() == p) + } + + /// Cache the dock's EDID (read during probe) on head 0 for [`get_modes`] to install, then + /// fire a hotplug so the compositor re-probes the connector -- which now reports connected + /// (see [`detect`]) and exposes the monitor's real mode list. Only head 0's downstream EDID + /// is read during bring-up (per-head EDID requests are unconfirmed). + pub(super) fn set_edid(&self, blob: KVec<u8>) { + *self.head0.cached_edid.lock() = Some(blob); + self.head0.fire_hotplug(); + } + + /// Fire a hotplug on every head's connector so the compositor re-probes [`detect`] -- used + /// after the bring-up work item completes to expose the live-scanout outputs. + pub(super) fn fire_hotplug(&self) { + for h in self.heads() { + h.fire_hotplug(); + } + } + + /// Publish the engaged CP session so the KMS callbacks can send runtime CP messages. + /// Called once by the bring-up work item after the dock acks (`acks > 0`). + /// `wire_seq`/`counter` are the next free values past the bring-up CP setup. + pub(super) fn publish_session(&self, ks: &[u8; 16], riv: &[u8; 8], wire_seq: u32, counter: u16) { + *self.cp_link.lock() = Some(CpLink { ks: *ks, riv: *riv, wire_seq, counter }); + } + + /// Seal and send one interactive CP message on EP02 for head `head_index`, advancing the + /// session keystream. The DL3 CP stream selects the head via the riv `byte0 ^ 0x80` (head 0 + /// base, head 1 flipped). `build(counter)` produces the inner CP message for the + /// dock-echoed `counter` it is handed (e.g. [`super::cp::set_mode`]); `tag_reserved` + /// trailing bytes are dropped before the live Dl3Cmac is appended (set-mode reserves a + /// 16-byte placeholder; messages with no placeholder pass 0). Returns `Ok(())` as a + /// **no-op when CP is not engaged**. The `cp_link` mutex serialises concurrent KMS + /// callbacks. Runs from the atomic-commit context (same as the scanout), so the blocking + /// `bulk_send` is fine. NOTE: head 1's `wire_seq`/counter sharing with head 0 is an + /// assumption (both share `cp_link`); the riv differs so the keystreams differ. Unconfirmed + /// (CP-wall-gated) -- revisit when the dock engages. + pub(super) fn send_cp( + &self, + head_index: u8, + id: u16, + tag_reserved: usize, + build: impl FnOnce(u16) -> Result<KVec<u8>>, + ) -> Result { + let mut guard = self.cp_link.lock(); + // `&mut *guard` forces the guard's `DerefMut` to `&mut Option<CpLink>` so `as_mut` + // resolves to `Option::as_mut` (the guard has its own inherent `as_mut`). + let Some(link) = (&mut *guard).as_mut() else { + return Ok(()); // CP not engaged -- nothing to send + }; + // Head select: head 1's CP stream flips the riv byte0 (see `decode_any`/CP-HANDSHAKE). + let mut riv = link.riv; + if head_index == 1 { + riv[0] ^= 0x80; + } + let msg = build(link.counter)?; + let content = &msg[..msg.len().saturating_sub(tag_reserved)]; + let frame = super::cp::seal_interactive(&link.ks, &riv, id, link.wire_seq, content)?; + let dev: &super::usb::Device = self.intf.as_ref(); + dev.bulk_send(super::EP_CTRL_OUT, &frame, super::timeout())?; + // Advance the AES-CTR block counter by the content blocks only (the appended Dl3Cmac + // tag is sent in clear, not keystreamed) and bump the dock-echoed inner counter. + link.wire_seq = link.wire_seq.wrapping_add(((content.len() + 15) / 16) as u32); + link.counter = link.counter.wrapping_add(1); + Ok(()) + } +} + +/// GEM object inner data. Empty: the shmem-backed `drm::gem::shmem::Object` (which +/// wires `drm_gem_shmem_dumb_create`, so userspace `DRM_IOCTL_MODE_CREATE_DUMB` +/// works) is enough until the EP08 scanout path consumes the framebuffers. +#[pin_data] +pub(super) struct VinoObject {} + +impl drm::gem::DriverObject for VinoObject { + type Driver = VinoDrmDriver; + type Args = (); + + fn new<Ctx: drm::DeviceContext>( + _dev: &drm::Device<VinoDrmDriver, Ctx>, + _size: usize, + _args: (), + ) -> impl PinInit<Self, Error> { + try_pin_init!(VinoObject {}) + } +} + +/// Per-open DRM client state. Empty of driver data, but its lifetime is used to +/// pin the module for the duration of an open DRM file (see [`VinoDrmFile::open`]). +#[pin_data(PinnedDrop)] +pub(super) struct VinoDrmFile {} + +impl drm::file::DriverFile for VinoDrmFile { + type Driver = VinoDrmDriver; + + fn open(_dev: &drm::Device<Self::Driver>) -> Result<Pin<KBox<Self>>> { + let file = KBox::try_pin_init(try_pin_init!(Self {}), GFP_KERNEL)?; + // Pin this module while a DRM file is open. The Rust DRM `file_operations` + // are built with `owner = NULL` (drm/gem/mod.rs `create_fops`), so the DRM + // core's `try_module_get(fops->owner)` on open is a no-op: an open card fd + // does NOT keep the driver loaded. Unloading vino (rmmod, or USB teardown at + // shutdown) while a compositor still holds `/dev/dri/cardN` then frees the + // module's `.rodata` -- where the fops live -- under that open fd, so the next + // ioctl/close dereferences freed memory and oopses the kernel (observed: KWin + // UAF in `__x64_sys_ioctl` / `put_files_struct`, "recursive fault, reboot + // needed"). Take an explicit module reference here, released 1:1 in + // `PinnedDrop` (run by `postclose_callback` on file close), to restore the + // pin the NULL `fops.owner` drops. Remove once the binding sets `fops.owner`. + // SAFETY: we are executing inside this module's own DRM `open` callback, so + // the module is live; taking an extra reference via `__module_get` is sound. + unsafe { bindings::__module_get(crate::THIS_MODULE.as_ptr()) }; + Ok(file) + } +} + +#[pinned_drop] +impl PinnedDrop for VinoDrmFile { + fn drop(self: Pin<&mut Self>) { + // Release the module reference taken in `open` (balanced one-per-open-file). + // SAFETY: balances the `__module_get` in `open`; `THIS_MODULE` is valid for + // the lifetime of the module. + unsafe { bindings::module_put(crate::THIS_MODULE.as_ptr()) }; + } +} + +const INFO: drm::DriverInfo = drm::DriverInfo { + major: 0, + minor: 1, + patchlevel: 0, + name: c"vino", + desc: c"DisplayLink DL3 (Dell D6000) DRM driver", +}; + +#[vtable] +impl drm::Driver for VinoDrmDriver { + type Data = VinoDrmData; + type File = VinoDrmFile; + type Object<Ctx: drm::DeviceContext> = drm::gem::shmem::Object<VinoObject, Ctx>; + + const INFO: drm::DriverInfo = INFO; + // Atomic KMS driver (CRTC/plane/connector via the simple display pipe). + // Mirrors the FEAT_RENDER idiom added by patches/drm/0001. + const FEAT_MODESET: bool = true; + const FEAT_ATOMIC: bool = true; + + // No driver-private ioctls (GEM/dumb + KMS handled by the DRM core). + kernel::declare_drm_ioctls! {} +} + +// ---- KMS C callbacks ------------------------------------------------------ + +/// Install a real EDID blob on the connector via the standard DRM EDID +/// infrastructure and return the number of modes added (0 on failure). This +/// reuses the kernel helpers -- no synthetic EDID. See CONTROL-PLANE.md. +fn install_edid(connector: *mut bindings::drm_connector, blob: &[u8]) -> i32 { + // SAFETY: `blob` is a valid byte buffer; `drm_edid_alloc` copies it. + let edid = unsafe { bindings::drm_edid_alloc(blob.as_ptr().cast(), blob.len()) }; + if edid.is_null() { + return 0; + } + // SAFETY: `connector` is valid during probe; `edid` is freshly allocated above. + unsafe { bindings::drm_edid_connector_update(connector, edid) }; + // SAFETY: connector valid; adds the EDID-derived modes, returns the count. + let n = unsafe { bindings::drm_edid_connector_add_modes(connector) }; + // SAFETY: `edid` was allocated by `drm_edid_alloc` and is no longer needed. + unsafe { bindings::drm_edid_free(edid) }; + n +} + +/// Connector `.mode_valid`: reject any single mode whose pixel clock exceeds the per-head +/// ceiling ([`MAX_HEAD_CLOCK_KHZ`], ~4K@60), so the compositor never offers an over-spec mode +/// on +/// one head. The *combined* across-heads budget is enforced separately at commit by +/// [`vino_atomic_check`]. +unsafe extern "C" fn mode_valid( + _connector: *mut bindings::drm_connector, + mode: *const bindings::drm_display_mode, +) -> bindings::drm_mode_status { + // SAFETY: `mode` is a valid drm_display_mode for the duration of the call. + let clock = unsafe { (*mode).clock }; + if clock > MAX_HEAD_CLOCK_KHZ { + bindings::drm_mode_status_MODE_CLOCK_HIGH + } else { + bindings::drm_mode_status_MODE_OK + } +} + +/// `mode_config.funcs.atomic_check`: run the standard atomic checks, then reject the commit if +/// the **combined** pixel clock of all active heads would exceed the dock's USB/DL3 budget +/// ([`MAX_TOTAL_CLOCK_KHZ`]) -- e.g. two simultaneous 4K modes. For each head, the proposed +/// (new) CRTC state is used when the head is part of this commit, else its current committed +/// state; only `enable && active` heads count. +unsafe extern "C" fn vino_atomic_check( + dev: *mut bindings::drm_device, + state: *mut bindings::drm_atomic_commit, +) -> i32 { + // SAFETY: `dev`/`state` are valid for the duration of the atomic check. + let rc = unsafe { bindings::drm_atomic_helper_check(dev, state) }; + if rc != 0 { + return rc; + } + // SAFETY: `dev` is our live, registered drm_device. + let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev) }; + let mut total_khz: i64 = 0; + for head in data.heads() { + let crtc = head.crtc.get(); + // SAFETY: read-only new-state accessor (a `rust_helper`, exposed without the prefix); + // NULL when this head is not in the commit -- then fall back to its current state. + let mut cs = unsafe { bindings::drm_atomic_get_new_crtc_state(state, crtc) }; + if cs.is_null() { + // SAFETY: `crtc` is initialised; `.state` is its current committed state (or NULL). + cs = unsafe { (*crtc).state }; + } + if cs.is_null() { + continue; + } + // SAFETY: `cs` is a live drm_crtc_state. + let (enable, active, clock) = unsafe { ((*cs).enable, (*cs).active, (*cs).mode.clock) }; + if enable && active { + total_khz += clock as i64; + } + } + if total_khz > MAX_TOTAL_CLOCK_KHZ { + pr_warn!( + "vino: modeset rejected -- combined {total_khz} kHz pixel clock over the {} kHz dock budget\n", + MAX_TOTAL_CLOCK_KHZ + ); + return EINVAL.to_errno(); + } + 0 +} + +/// Connector `.get_modes`: install the dock's real EDID (read during probe) when +/// available; otherwise fall back to a single 1920x1080@60 CVT mode. Reading the +/// real EDID gives the true monitor name/size and its native mode list (see the +/// EDID Read path); the fallback keeps the connector usable when nothing is +/// plugged into the dock or the CP channel has not yet delivered the EDID. +unsafe extern "C" fn get_modes(connector: *mut bindings::drm_connector) -> i32 { + // SAFETY: `connector` is a valid, initialised connector during probe. + let dev = unsafe { (*connector).dev }; + // SAFETY: `dev` is our live, registered drm_device. + let ddev = unsafe { VinoDrmDevice::from_raw(dev) }; + let data: &VinoDrmData = ddev; + if let Some(head) = data.head_by_connector(connector) { + let guard = head.cached_edid.lock(); + if let Some(blob) = guard.as_ref() { + let n = install_edid(connector, blob); + if n > 0 { + return n; + } + } + } + // Fallback: single FALLBACK_W x FALLBACK_H @60 CVT mode, marked preferred. + // SAFETY: `dev` is a valid drm_device; drm_cvt_mode allocates a mode. + let mode = unsafe { + bindings::drm_cvt_mode(dev, FALLBACK_W, FALLBACK_H, 60, false, false, false) + }; + if mode.is_null() { + return 0; + } + // SAFETY: `mode` is freshly allocated and owned by the connector after add. + unsafe { bindings::drm_mode_probed_add(connector, mode) }; + // SAFETY: connector is valid; set the fallback mode as preferred. + unsafe { bindings::drm_set_preferred_mode(connector, FALLBACK_W, FALLBACK_H) }; + 1 +} + +/// Connector `.detect`: report **disconnected** until the dock's downstream EDID has +/// actually been read over the CP channel, then **connected**. A virtual connector +/// that always reports connected makes the compositor light up a phantom output it +/// cannot drive -- no pixels reach the dock until the CP/EP08 path is up -- which froze +/// KWin on plug (SSH stayed alive; unplug recovered it). Gating on a real EDID mirrors +/// how `gud`/`udl` report monitor presence; [`VinoDrmData::set_edid`] fires a hotplug +/// so the compositor re-probes and enables the output once the EDID arrives. +unsafe extern "C" fn detect( + connector: *mut bindings::drm_connector, + _force: bool, +) -> bindings::drm_connector_status { + // SAFETY: `connector` is a valid connector embedded in our DRM device-private. + let dev = unsafe { (*connector).dev }; + // SAFETY: `dev` is our live, registered drm_device. + let ddev = unsafe { VinoDrmDevice::from_raw(dev) }; + let data: &VinoDrmData = ddev; + let live_ready = super::CP_ENGAGED.load(core::sync::atomic::Ordering::SeqCst); + let has_edid = data + .head_by_connector(connector) + .is_some_and(|h| h.cached_edid.lock().is_some()); + if has_edid || live_ready { + // Force-connect (with the get_modes 1080p fallback) so a compositor drives the CRTC and + // `primary_atomic_update` fires live frames -- but only once CP is engaged (not merely + // after + // bring-up): connecting the output makes the compositor push EP08 video, and doing that + // before the dock has engaged CP makes it fault and USB-reset in a loop (the "EP08 + // write + // wedges the hub" mode). So stay disconnected until CP is up -- or a real EDID arrived. + bindings::drm_connector_status_connector_status_connected + } else { + bindings::drm_connector_status_connector_status_disconnected + } +} + +/// CRTC `.atomic_enable`: the display is turning on (scanout begins). Captures the mode the +/// compositor selected -- any entry from the connector's full EDID-derived list -- as a +/// set-mode [`super::cp::Timing`] in [`ScanoutState::active_timing`] and pushes a live +/// mode-set CP message for it (no-op until CP engages). The geometry change is also honoured +/// by the scanout path (`encode_and_send` re-inits on `dims` change). +unsafe extern "C" fn crtc_atomic_enable( + crtc: *mut bindings::drm_crtc, + _state: *mut bindings::drm_atomic_commit, +) { + // SAFETY: in `.atomic_enable` the crtc and its committed `state` are valid; `state->mode` + // is a live drm_display_mode and `timing_from_drm_mode` only reads it. + let cs = unsafe { (*crtc).state }; + if cs.is_null() { + return; + } + let timing = unsafe { super::cp::timing_from_drm_mode(&(*cs).mode) }; + pr_info!( + "vino: KMS CRTC enable -- display ON, mode {}x{}@{} (scanout begins)\n", + timing.hactive, timing.vactive, timing.refresh_hz + ); + // SAFETY: `crtc` is valid; its `dev` is our live drm_device. + let dev = unsafe { (*crtc).dev }; + if dev.is_null() { + return; + } + // SAFETY: `dev` is our registered drm_device. + let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev) }; + let Some(head) = data.head_by_crtc(crtc) else { + return; + }; + head.scanout.lock().active_timing = Some(timing); + // Push a live mode-set for the chosen mode (on this head's CP stream) so the dock switches + // to it at runtime, not just the EDID-preferred mode the bring-up setup sent. `set_mode` + // reserves a 16-byte tag placeholder. A no-op until the cipher is engaged. + if let Err(e) = data.send_cp(head.index, 0x48, 16, |ctr| super::cp::set_mode(ctr, &timing)) { + pr_warn!("vino: head{} runtime mode-set send failed ({e:?})\n", head.index); + } +} + +/// CRTC `.atomic_disable`: the display is turning off. +/// CRTC `.atomic_disable`: the display is turning off -- DPMS-off / blank / suspend all land +/// here in atomic KMS (the compositor clears the CRTC `active` state). The compositor stops +/// page-flipping, so no new frames are pushed; this resets the head's scanout state so a later +/// re-enable (DPMS-on) re-inits the encoder and sends a **full keyframe** rather than diffing +/// against a shadow the dock may have dropped while blanked, and re-uploads the cursor sprite. +/// +/// The dock holds the last frame when video stops (it has its own scanout buffer), so the +/// monitor freezes the last image rather than going black; a true backlight-standby would need +/// a dock power command that is not decoded (DLM's `Standby`/`Suspend`/`TempPowerOff` are +/// internal, vtable-dispatched events with no wire frame -- the same dead-end as gamma). +unsafe extern "C" fn crtc_atomic_disable( + crtc: *mut bindings::drm_crtc, + _state: *mut bindings::drm_atomic_commit, +) { + // SAFETY: in `.atomic_disable` the crtc and its `dev` are valid. + let dev = unsafe { (*crtc).dev }; + if dev.is_null() { + return; + } + // SAFETY: `dev` is our registered drm_device. + let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev) }; + let Some(head) = data.head_by_crtc(crtc) else { + return; + }; + { + let mut st = head.scanout.lock(); + st.enc = None; // force a full re-init + keyframe on the next enable + st.dims = (0, 0); + } + head.cursor_primed + .store(false, core::sync::atomic::Ordering::SeqCst); + pr_info!("vino: KMS CRTC disable -- head{} display OFF (scanout stopped)\n", head.index); +} + +/// Cursor plane `.atomic_update`: the cursor sprite and/or position changed. Sends the cursor +/// CP messages (create once + image when a sprite framebuffer is present, then a move). Gated +/// on CP engagement; a no-op on current hardware (the CP wall). See [`cursor_send`]. +unsafe extern "C" fn cursor_atomic_update( + plane: *mut bindings::drm_plane, + _state: *mut bindings::drm_atomic_commit, +) { + if !super::CP_ENGAGED.load(core::sync::atomic::Ordering::SeqCst) { + return; + } + // SAFETY: in `.atomic_update` the plane and its committed state are valid for the commit. + let (dev_raw, fb, w, h, cx, cy) = unsafe { + let st = (*plane).state; + if st.is_null() { + return; + } + ( + (*plane).dev, + (*st).fb, + (*st).crtc_w as usize, + (*st).crtc_h as usize, + (*st).crtc_x, + (*st).crtc_y, + ) + }; + // SAFETY: `dev_raw` is our live, registered drm_device. + let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev_raw) }; + let Some(head) = data.head_by_cursor(plane) else { + return; + }; + if let Err(e) = cursor_send(data, head, fb, w, h, cx, cy) { + pr_warn!("vino: head{} cursor update failed ({e:?})\n", head.index); + } +} + +/// Primary plane `.atomic_update`: a new framebuffer was flipped in -- the scanout hook. +/// Maps the framebuffer, converts XRGB8888 -> RGB565, Vino-encodes the changed +/// region against the previous frame, and bulk-writes the EP08 video frame. +/// +/// The EP08 write only happens once the dock has engaged CP (see `docs/BLOCKER.md`): +/// until then the dock NAKs/stalls EP08, so a normal module load must not push frames on +/// every flip and thrash the dock. With the CP-engagement wall unsolved this never fires +/// on real hardware. +unsafe extern "C" fn primary_atomic_update( + plane: *mut bindings::drm_plane, + _state: *mut bindings::drm_atomic_commit, +) { + // Don't touch EP08 until the dock has engaged CP. Pushing video (and the one-shot + // clear_halt of EPs 8/10/11/12) at a dock with a dead CP channel makes it fault and + // USB-reset, which re-probes the driver in a ~2.7 s loop. + if !super::CP_ENGAGED.load(core::sync::atomic::Ordering::SeqCst) { + return; + } + // SAFETY: in `.atomic_update` the plane and its committed state are valid; the plane + // state and its framebuffer are valid for the duration of the commit. + let (dev_raw, fb, w, h, damage, rotation) = unsafe { + let st = (*plane).state; + if st.is_null() { + return; + } + // Plane destination geometry == the negotiated mode (the compositor sizes the primary + // plane 1:1 with a virtual output), so this drives the dynamic scanout resolution. + let (w, h) = ((*st).crtc_w as usize, (*st).crtc_h as usize); + ((*plane).dev, (*st).fb, w, h, damage_bbox(st), (*st).rotation) + }; + if fb.is_null() { + return; + } + // Recover our device-private data + this plane's head from the raw drm_device. + // SAFETY: `dev_raw` is our live, registered drm_device. + let ddev = unsafe { VinoDrmDevice::from_raw(dev_raw) }; + let data: &VinoDrmData = ddev; + let Some(head) = data.head_by_primary(plane) else { + return; + }; + + use core::sync::atomic::Ordering::Relaxed; + // Throttle: while scanout is failing (dock NAKing because CP isn't engaged), skip the + // upcoming pageflips set by the backoff below instead of converting+encoding+sending a + // frame the dock will just drop. The backoff is shared across heads (a coarse global rate + // limit) -- fine while it never fires on real hardware (CP wall). + let skip = super::SCANOUT_SKIP.load(Relaxed); + if skip > 0 { + super::SCANOUT_SKIP.store(skip - 1, Relaxed); + return; + } + // Read this head's CRTC GAMMA_LUT (if a compositor set one) and apply it host-side in the + // conversion below -- there is no dock-side gamma message (see `read_gamma_lut`). + let gamma = read_gamma_lut(head); + match scanout_one(data, head, fb, w, h, damage, rotation, gamma.as_ref()) { + Ok(()) => { + let n = super::SCANOUT_FAILS.swap(0, Relaxed); + super::SCANOUT_SKIP.store(0, Relaxed); + if n > 0 { + pr_info!("vino: scanout recovered after {n} failed frame(s)\n"); + } + } + Err(e) => { + // The dock NAKs every EP08 write (EPROTO) until CP engages -- expected and not + // actionable. Log the first failure and then at exponentially sparser points so + // dmesg isn't flooded, and back off the scanout rate. + let n = super::SCANOUT_FAILS.fetch_add(1, Relaxed) + 1; + if n == 1 || n.is_power_of_two() { + pr_err!("vino: scanout frame failed ({e:?}) [x{n}] -- throttling\n"); + } + // Linear backoff capped at 120 frames (~2 s @ 60 Hz) between probe attempts, so + // recovery (CP engaging) is still detected within ~2 s while idle CPU stays low. + super::SCANOUT_SKIP.store(core::cmp::min(n, 120), Relaxed); + } + } +} + +/// Map an output pixel `(dx, dy)` back to its source-framebuffer pixel `(sx, sy)` under a DRM +/// plane `rotation` bitmask (`DRM_MODE_ROTATE_*` | `DRM_MODE_REFLECT_*`, the values the +/// standard `drm_plane_create_rotation_property` exposes). `sw`/`sh` are the SOURCE +/// (framebuffer) dimensions; the output dimensions are `(sw, sh)` for 0 deg/180 deg and `(sh, sw)` +/// for 90 deg/270 deg (the caller swaps source vs output accordingly). Rotation is clockwise; +/// reflection is applied in source space after rotation. Pure + total (saturating), so it is +/// unit-tested directly. Used by [`encode_and_send`] to honour the connector's rotation +/// property -- DLM rotates host-side, vino rotates in the scanout encode. +pub(super) fn rot_src( + rotation: u32, + dx: usize, + dy: usize, + sw: usize, + sh: usize, +) -> (usize, usize) { + let xmax = sw.saturating_sub(1); + let ymax = sh.saturating_sub(1); + let rot = rotation & bindings::DRM_MODE_ROTATE_MASK; + let (mut sx, mut sy) = if rot == bindings::DRM_MODE_ROTATE_90 { + (dy, ymax.saturating_sub(dx)) + } else if rot == bindings::DRM_MODE_ROTATE_180 { + (xmax.saturating_sub(dx), ymax.saturating_sub(dy)) + } else if rot == bindings::DRM_MODE_ROTATE_270 { + (xmax.saturating_sub(dy), dx) + } else { + (dx, dy) // ROTATE_0 / unset + }; + if rotation & bindings::DRM_MODE_REFLECT_X != 0 { + sx = xmax.saturating_sub(sx); + } + if rotation & bindings::DRM_MODE_REFLECT_Y != 0 { + sy = ymax.saturating_sub(sy); + } + (sx, sy) +} + +/// True if `rotation` swaps width/height (90 deg or 270 deg), so the source framebuffer is +/// portrait while the scanned-out display is landscape (or vice versa). +fn rotation_swaps_dims(rotation: u32) -> bool { + let r = rotation & bindings::DRM_MODE_ROTATE_MASK; + r == bindings::DRM_MODE_ROTATE_90 || r == bindings::DRM_MODE_ROTATE_270 +} + +/// True if `rotation` is anything other than the identity (plain 0 deg), i.e. the scanout must +/// remap every pixel and cannot take the damage-clip fast path. +fn rotation_active(rotation: u32) -> bool { + rotation != 0 && rotation != bindings::DRM_MODE_ROTATE_0 +} + +/// Damage bounding box (pixels, clamped to the scanout) for this atomic update, or +/// `None` meaning "convert the whole frame". Reads the standard `FB_DAMAGE_CLIPS` blob the +/// compositor attaches to the plane state and unions its rects. The Vino encoder already +/// shadow-diffs against the previous frame, so unchanged regions emit nothing regardless; +/// the win here is skipping the XRGB8888->RGB565 conversion of those regions. Returns `None` +/// when no damage is advertised, `ignore_damage_clips` is set, or the union is degenerate +/// (all treated as a full-frame update). +/// +/// SAFETY: `st` must be a valid `drm_plane_state` for the duration of the call. +unsafe fn damage_bbox( + st: *const bindings::drm_plane_state, +) -> Option<(usize, usize, usize, usize)> { + // SAFETY: caller guarantees `st` is a live plane state. + let (blob, ignore) = unsafe { ((*st).fb_damage_clips, (*st).ignore_damage_clips) }; + if ignore || blob.is_null() { + return None; + } + // SAFETY: `blob` is non-null and lives as long as the plane state. + let (data, len) = unsafe { ((*blob).data as *const bindings::drm_mode_rect, (*blob).length) }; + let n = len / core::mem::size_of::<bindings::drm_mode_rect>(); + if data.is_null() || n == 0 { + return None; + } + let (mut x0, mut y0, mut x1, mut y1) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN); + for i in 0..n { + // SAFETY: `i < n`, the rect array length implied by `blob.length`. + let r = unsafe { &*data.add(i) }; + x0 = x0.min(r.x1); + y0 = y0.min(r.y1); + x1 = x1.max(r.x2); + y1 = y1.max(r.y2); + } + // Clamp to the plane's destination geometry and reject empty/degenerate boxes (fall back to + // a full frame). Read crtc_w/crtc_h off the plane state so the clamp tracks the live mode, + // not a fixed 1080p. + // SAFETY: caller guarantees `st` is a live plane state. + let (pw, ph) = unsafe { ((*st).crtc_w as i32, (*st).crtc_h as i32) }; + let cx0 = x0.clamp(0, pw) as usize; + let cy0 = y0.clamp(0, ph) as usize; + let cx1 = x1.clamp(0, pw) as usize; + let cy1 = y1.clamp(0, ph) as usize; + if cx1 <= cx0 || cy1 <= cy0 { + return None; + } + Some((cx0, cy0, cx1, cy1)) +} + +/// Read the CRTC's `GAMMA_LUT` and flatten it into three 256-entry 8-bit lookup tables +/// (R, G, B), or `None` when no gamma is set (the common case -- the conversion then runs at +/// full speed). DLM gamma-corrects pixels **host-side** before encoding; the DL3 dock has no +/// gamma CP message (the `NotifyGammaCurve`/`SetGammaMode` handlers are DLM-internal, +/// vtable-dispatched, and emit no wire frame -- confirmed against the decompile and every +/// capture), so vino applies the LUT in the scanout exactly like it applies `rotation`. The +/// blob holds `n` `drm_color_lut` entries (u16 per channel); 8-bit input `i` maps to entry +/// `i*(n-1)/255` and takes that entry's high 8 bits. +fn read_gamma_lut(head: &Head) -> Option<[[u8; 256]; 3]> { + // SAFETY: `head.crtc` was initialised in `kms_init`; its committed `state` and the + // gamma_lut blob it references are valid for the duration of the atomic commit. + let blob = unsafe { + let cs = (*head.crtc.get()).state; + if cs.is_null() { + return None; + } + (*cs).gamma_lut + }; + if blob.is_null() { + return None; + } + // SAFETY: `blob` is a live drm_property_blob for the commit; `data`/`length` are valid. + let (ptr, len) = + unsafe { ((*blob).data as *const bindings::drm_color_lut, (*blob).length) }; + let n = len / core::mem::size_of::<bindings::drm_color_lut>(); + if ptr.is_null() || n == 0 { + return None; + } + let mut t = [[0u8; 256]; 3]; + for i in 0..256usize { + let idx = if n == 1 { 0 } else { i * (n - 1) / 255 }; + // SAFETY: `idx < n`, within the blob's `n` `drm_color_lut` entries. + let e = unsafe { &*ptr.add(idx) }; + t[0][i] = (e.red >> 8) as u8; + t[1][i] = (e.green >> 8) as u8; + t[2][i] = (e.blue >> 8) as u8; + } + Some(t) +} + +/// vmap `fb`, encode it, and push one EP08 frame. Split out so `?` can be used. `damage` +/// bounds the XRGB8888->RGB565 conversion to the changed region (see [`damage_bbox`]). +fn scanout_one( + data: &VinoDrmData, + head: &Head, + fb: *mut bindings::drm_framebuffer, + w: usize, + h: usize, + damage: Option<(usize, usize, usize, usize)>, + rotation: u32, + gamma: Option<&[[u8; 256]; 3]>, +) -> Result { + // `w`/`h` are the plane's destination (displayed) geometry (== the negotiated mode), + // threaded in from `primary_atomic_update`, so the scanout follows the live mode (e.g. the + // dock's + // native 4K) instead of a hardcoded 1080p. `drm_framebuffer` is opaque in the bindings, so + // the geometry comes from the plane state; our XRGB8888 buffers are packed. Under a + // 90 deg/270 deg + // `rotation` the source framebuffer is portrait relative to the display, so its row pitch + // tracks the *source* width -- `encode_and_send` derives that from `rotation`. + if w == 0 || h == 0 { + return Err(EINVAL); + } + + // Map the framebuffer's backing pages into the kernel address space. + // SAFETY: `iosys_map` is POD (a pointer union + bool); all-zero is a valid, + // "not mapped" value that `drm_gem_fb_vmap` overwrites for present planes. + let mut map: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() }; + let mut dmap: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() }; + // SAFETY: `fb` is a valid framebuffer with GEM-backed storage. + to_result(unsafe { bindings::drm_gem_fb_vmap(fb, map.as_mut_ptr(), dmap.as_mut_ptr()) })?; + + // SAFETY: plane 0's CPU virtual address, valid until `drm_gem_fb_vunmap`. + let vaddr = unsafe { map[0].__bindgen_anon_1.vaddr } as *const u8; + let result = if vaddr.is_null() { + Err(EINVAL) + } else { + encode_and_send(data, head, vaddr, w, h, damage, rotation, gamma) + }; + + // SAFETY: balances the vmap above with the same `map`. + unsafe { bindings::drm_gem_fb_vunmap(fb, map.as_mut_ptr()) }; + result +} + +/// Convert the mapped XRGB8888 frame to RGB565, Vino-encode it against the previous +/// frame, and bulk-write the resulting EP08 frame to the dock. +fn encode_and_send( + data: &VinoDrmData, + head: &Head, + vaddr: *const u8, + w: usize, + h: usize, + damage: Option<(usize, usize, usize, usize)>, + rotation: u32, + gamma: Option<&[[u8; 256]; 3]>, +) -> Result { + // Convert XRGB8888 (LE bytes B,G,R,X) -> RGB565 and encode, all under the scanout lock. + // The conversion fills a PERSISTENT `cur` buffer (allocated once with the encoder) in + // place -- no per-frame ~4 MiB kmalloc, which is what was failing with ENOMEM and flooding + // the log. The encoder's shadow buffer is mutable state, so the lock is needed regardless. + let frame = { + let mut st = head.scanout.lock(); + // On the first frame `cur` is freshly zeroed, so the whole buffer must be filled + // regardless of the advertised damage (a partial fill would scan out black around + // the damage box). Afterwards, unchanged regions of `cur` already hold the previous + // frame (== the shadow the encoder diffs against), so converting only the damage box + // is correct and skips the rest of the XRGB8888->RGB565 work. + // Re-initialise the encoder/shadow/conversion buffers on the first frame AND whenever + // the framebuffer geometry changes (a mode switch), so they always match `cur`'s size. + let first = st.enc.is_none() || st.dims != (w, h); + if first { + st.enc = Some(super::video::Encoder::new(w, h, super::video::Mode::Rle)?); + st.cur = VVec::from_elem(0u16, w * h, GFP_KERNEL)?; + st.dims = (w, h); + st.hint = 0; // previous frame's size no longer applies at the new geometry + } + // Source framebuffer geometry: a 90 deg/270 deg rotation makes the source portrait relative + // to the displayed `w`x`h`, so its packed row pitch tracks the *source* width. + let (sw, sh) = if rotation_swaps_dims(rotation) { (h, w) } else { (w, h) }; + let pitch = sw * 4; + // Damage clips are in source coordinates and don't map cleanly through a rotation, so + // convert the whole frame on a (re)allocation OR whenever a rotation/reflection is in + // effect; the encoder still shadow-diffs, so unchanged pixels emit nothing regardless. + // A gamma LUT recolours every pixel, so it also forces a full convert (still no extra + // wire traffic -- the recoloured output is identical frame-to-frame for static + // content, + // so the shadow-diff emits nothing for unchanged regions). + let full = first || rotation_active(rotation) || gamma.is_some(); + let (x0, y0, x1, y1) = if full { (0, 0, w, h) } else { damage.unwrap_or((0, 0, w, h)) }; + // Split-borrow the fields so the in-place fill and the &mut encode can coexist. + let ScanoutState { enc, cur, seq, hint, dims: _, active_timing: _ } = &mut *st; + for dy in y0..y1 { + for dx in x0..x1 { + // Output pixel (dx,dy) -> source pixel under the plane rotation/reflection. + let (sx, sy) = rot_src(rotation, dx, dy, sw, sh); + // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source framebuffer + // (`sw*sh*4` bytes); `rot_src` guarantees `sx < sw`, `sy < sh`. + let px = + unsafe { (vaddr.add(sy * pitch + sx * 4) as *const u32).read_unaligned() }; + let (mut r, mut g, mut b) = + (((px >> 16) & 0xff) as usize, ((px >> 8) & 0xff) as usize, (px & 0xff) as usize); + // Apply the CRTC gamma LUT host-side (DLM gamma-corrects pixels before + // encoding -- there is no dock-side gamma CP message; see `read_gamma_lut`). + if let Some(t) = gamma { + r = t[0][r] as usize; + g = t[1][g] as usize; + b = t[2][b] as usize; + } + cur[dy * w + dx] = + (((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)) as u16; + } + } + let s = *seq; + *seq = seq.wrapping_add(1); + let enc = enc.as_mut().ok_or(ENOMEM)?; + // Encode straight into the outgoing frame buffer: reserve the EP08 header up + // front, append the codec stream in place, then back-patch the header now that + // the payload length is known. This replaces a two-allocation/extra-copy path + // (encode -> KVec, then frame_to_ep08 -> second KVec) with a single buffer, + // and `hint` pre-sizes it from the last frame so the encode rarely reallocates. + const HDR: usize = super::video::EP08_HDR_LEN; + let mut frame = KVec::with_capacity((*hint).max(HDR + 64), GFP_KERNEL)?; + frame.extend_from_slice(&[0u8; HDR], GFP_KERNEL)?; // header placeholder + enc.encode_into(&*cur, &mut frame)?; + let payload_len = frame.len() - HDR; + super::video::write_ep08_header(&mut frame[..HDR], payload_len, s); + *hint = frame.len(); + frame + }; + + // Push the frame to this head's video endpoint (lock released). + let dev: &super::usb::Device = data.intf.as_ref(); + // First live-scanout frame: clear-halt the four iface-0 bulk-OUT video endpoints + // (0x08 main + 0x0a/0x0b/0x0c aux, covering every head) so the first write doesn't + // ETIMEDOUT on a stale endpoint toggle. DLM clear-halts these at engagement (the + // "startRender" step). Once, globally. + if !super::EP08_SCANOUT_PRIMED.swap(true, core::sync::atomic::Ordering::SeqCst) { + for ep in [0x08u8, 0x0a, 0x0b, 0x0c] { + let _ = dev.clear_halt(ep); + } + pr_info!("vino: video endpoints primed (clear-halt 8/10/11/12)\n"); + } + // Head 0 -> EP 0x08, head 1 -> EP 0x0a (see `HEAD_EP`). + dev.bulk_send(head.video_ep(), &frame, super::timeout())?; + Ok(()) +} + +/// Send the cursor CP messages for the current sprite + position (called from +/// [`cursor_atomic_update`]). `fb` is the cursor sprite framebuffer (`None`/null = hidden), +/// `w`x`h` its size, `(cx, cy)` the on-CRTC position. Sends `create` (once, the constant +/// sprite size), `image` (the ARGB8888 sprite -- its little-endian memory bytes are already +/// the +/// `B,G,R,A` order the dock wants, copied row-by-row to honour the framebuffer pitch), then a +/// `move`. Every send routes through [`VinoDrmData::send_cp`], so all of this is a no-op until +/// the CP cipher engages (the wall). A hidden cursor currently just re-issues a move (a +/// dedicated hide message is a future refinement). +fn cursor_send( + data: &VinoDrmData, + head: &Head, + fb: *mut bindings::drm_framebuffer, + w: usize, + h: usize, + cx: i32, + cy: i32, +) -> Result { + let hid = head.index; // cursor messages carry the head id at off22; CP routes by head too + let (mx, my) = ( + cx.clamp(0, u16::MAX as i32) as u16, + cy.clamp(0, u16::MAX as i32) as u16, + ); + if fb.is_null() || w == 0 || h == 0 { + // Hidden cursor: no sprite to upload, just track the position. + return data.send_cp(hid, 0x1a, 0, |ctr| super::cp::cursor_move(ctr, hid, mx, my)); + } + // Declare the sprite dimensions once per head, then upload the bitmap. We don't diff sprite + // content yet, so the image is re-sent on every sprite-present update. + if !head.cursor_primed.swap(true, core::sync::atomic::Ordering::SeqCst) { + data.send_cp(hid, 0x1b, 0, |ctr| super::cp::cursor_create(ctr, w as u16, h as u16))?; + } + // vmap the sprite; copy `w*h*4` BGRA bytes row-by-row (the source pitch is `w*4` for our + // packed cursor buffers, but copying per-row keeps it correct if that ever changes). + let pitch = w * 4; + // SAFETY: `iosys_map` is POD; all-zero is a valid "not mapped" value `drm_gem_fb_vmap` + // fills. + let mut map: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() }; + let mut dmap: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() }; + // SAFETY: `fb` is a valid cursor framebuffer with GEM-backed storage. + to_result(unsafe { bindings::drm_gem_fb_vmap(fb, map.as_mut_ptr(), dmap.as_mut_ptr()) })?; + // SAFETY: plane 0's CPU virtual address, valid until `drm_gem_fb_vunmap`. + let vaddr = unsafe { map[0].__bindgen_anon_1.vaddr } as *const u8; + let res = if vaddr.is_null() { + Err(EINVAL) + } else { + (|| -> Result { + let mut bgra = KVec::with_capacity(w * h * 4, GFP_KERNEL)?; + for y in 0..h { + // SAFETY: `[y*pitch, y*pitch + w*4)` is within the mapped `h*pitch` sprite. + let row = unsafe { core::slice::from_raw_parts(vaddr.add(y * pitch), w * 4) }; + bgra.extend_from_slice(row, GFP_KERNEL)?; + } + data.send_cp(hid, 0x1c, 0, |ctr| { + super::cp::cursor_image(ctr, w as u16, h as u16, &bgra) + }) + })() + }; + // SAFETY: balances the vmap above with the same `map`. + unsafe { bindings::drm_gem_fb_vunmap(fb, map.as_mut_ptr()) }; + res?; + data.send_cp(hid, 0x1a, 0, |ctr| super::cp::cursor_move(ctr, hid, mx, my)) +} + +/// Wire up the atomic KMS pipeline on `ddev` (called after `drm::Device::new` and +/// before `drm_dev_register`). Sets `mode_config`, builds the virtual connector, +/// and initialises the atomic CRTC + primary/cursor planes + virtual encoder. +pub(super) fn kms_init<C: drm::DeviceContext>( + ddev: &drm::Device<VinoDrmDriver, C>, +) -> Result { + let raw = ddev.as_raw(); + // Deref `drm::Device<T>` -> `T::Data` to reach the embedded C objects. + let data: &VinoDrmData = ddev; + + // SAFETY: `raw` is a valid, not-yet-registered drm_device; the funcs/objects + // referenced below live in device-private memory (`data`) for its lifetime. + unsafe { + to_result(bindings::drmm_mode_config_init(raw))?; + + let mc = &mut (*raw).mode_config; + mc.min_width = 0; + mc.min_height = 0; + mc.max_width = 4096; + mc.max_height = 4096; + // Advertise a 64x64 hardware cursor (the dock's cursor sprite size) so userspace + // drives the cursor plane instead of compositing the pointer into the framebuffer. + mc.cursor_width = CURSOR_SIZE; + mc.cursor_height = CURSOR_SIZE; + let mcf = data.mode_cfg_funcs.get(); + (*mcf).fb_create = Some(bindings::drm_gem_fb_create); + // `vino_atomic_check` = the standard atomic check + the combined cross-head USB + // bandwidth budget (rejects e.g. two simultaneous 4K modes). + (*mcf).atomic_check = Some(vino_atomic_check); + (*mcf).atomic_commit = Some(bindings::drm_atomic_helper_commit); + mc.funcs = mcf; + + // ---- Shared vtables (one set for every head; the callbacks recover the head from + // the C object pointer). Plane/CRTC `atomic_check` are left NULL: a virtual sink + // accepts any configuration, and the helpers still invoke `atomic_update`/ + // `atomic_enable` because the objects are assigned to the CRTC. + + // Connector funcs + helper. We report presence from the cached EDID (see `detect`) + // and deliver HPD ourselves (`set_edid`/`fire_hotplug`). + let cf = data.conn_funcs.get(); + (*cf).fill_modes = Some(bindings::drm_helper_probe_single_connector_modes); + (*cf).detect = Some(detect); + (*cf).destroy = Some(bindings::drm_connector_cleanup); + (*cf).reset = Some(bindings::drm_atomic_helper_connector_reset); + (*cf).atomic_duplicate_state = + Some(bindings::drm_atomic_helper_connector_duplicate_state); + (*cf).atomic_destroy_state = + Some(bindings::drm_atomic_helper_connector_destroy_state); + (*data.conn_helper.get()).get_modes = Some(get_modes); + // Prune any single mode above the per-head pixel-clock ceiling (~4K@60). + (*data.conn_helper.get()).mode_valid = Some(mode_valid); + + // One `drm_plane_funcs` shared by both planes; per-plane helper funcs (the primary's + // `atomic_update` scans out, the cursor's sends cursor CP). + let plf = data.plane_funcs.get(); + (*plf).update_plane = Some(bindings::drm_atomic_helper_update_plane); + (*plf).disable_plane = Some(bindings::drm_atomic_helper_disable_plane); + (*plf).destroy = Some(bindings::drm_plane_cleanup); + (*plf).reset = Some(bindings::drm_atomic_helper_plane_reset); + (*plf).atomic_duplicate_state = + Some(bindings::drm_atomic_helper_plane_duplicate_state); + (*plf).atomic_destroy_state = Some(bindings::drm_atomic_helper_plane_destroy_state); + (*data.primary_helper.get()).atomic_update = Some(primary_atomic_update); + (*data.cursor_helper.get()).atomic_update = Some(cursor_atomic_update); + + // CRTC funcs + helper. + let crf = data.crtc_funcs.get(); + (*crf).set_config = Some(bindings::drm_atomic_helper_set_config); + (*crf).page_flip = Some(bindings::drm_atomic_helper_page_flip); + (*crf).destroy = Some(bindings::drm_crtc_cleanup); + (*crf).reset = Some(bindings::drm_atomic_helper_crtc_reset); + (*crf).atomic_duplicate_state = + Some(bindings::drm_atomic_helper_crtc_duplicate_state); + (*crf).atomic_destroy_state = Some(bindings::drm_atomic_helper_crtc_destroy_state); + let crh = data.crtc_helper.get(); + (*crh).atomic_enable = Some(crtc_atomic_enable); + (*crh).atomic_disable = Some(crtc_atomic_disable); + + // Encoder funcs. + (*data.encoder_funcs.get()).destroy = Some(bindings::drm_encoder_cleanup); + + // Build each head's objects (connector + primary/cursor planes + CRTC + encoder). + for head in data.heads() { + build_head(raw, data, head)?; + } + + drm_mode_config_reset(raw); + } + Ok(()) +} + +/// Build one head's KMS objects -- connector + primary plane (scanout) + cursor plane + CRTC + +/// virtual encoder -- using the shared vtables already filled in `data`. Each is a complete +/// independent output (its own CRTC), so the compositor sees [`NHEADS`] monitors and routes +/// each to its own video EP / CP stream (see [`Head`]). +/// +/// SAFETY: `raw` is a valid, not-yet-registered drm_device; the `data`/`head` C objects live +/// in device-private memory for its lifetime. +unsafe fn build_head(raw: *mut bindings::drm_device, data: &VinoDrmData, head: &Head) -> Result { + // SAFETY: see the function contract; every object/vtable below is device-private memory. + unsafe { + // Connector. + let conn = head.connector.get(); + to_result(bindings::drm_connector_init( + raw, + conn, + data.conn_funcs.get(), + bindings::DRM_MODE_CONNECTOR_VIRTUAL as i32, + ))?; + (*conn).helper_private = data.conn_helper.get(); + (*conn).polled = bindings::DRM_CONNECTOR_POLL_HPD as u8; + + // Primary plane (XRGB8888 scanout). `possible_crtcs` is fixed up once the CRTC exists. + let primary = head.primary.get(); + to_result(bindings::drm_universal_plane_init( + raw, + primary, + 0, + data.plane_funcs.get(), + PRIMARY_FORMATS.as_ptr(), + PRIMARY_FORMATS.len() as u32, + ptr::null(), + bindings::drm_plane_type_DRM_PLANE_TYPE_PRIMARY, + ptr::null(), + ))?; + (*primary).helper_private = data.primary_helper.get(); + + // Cursor plane (ARGB8888 sprite). + let cursor = head.cursor.get(); + to_result(bindings::drm_universal_plane_init( + raw, + cursor, + 0, + data.plane_funcs.get(), + CURSOR_FORMATS.as_ptr(), + CURSOR_FORMATS.len() as u32, + ptr::null(), + bindings::drm_plane_type_DRM_PLANE_TYPE_CURSOR, + ptr::null(), + ))?; + (*cursor).helper_private = data.cursor_helper.get(); + + // CRTC with both planes, plus a GAMMA_LUT (applied host-side in `read_gamma_lut`). + let crtc = head.crtc.get(); + to_result(bindings::drm_crtc_init_with_planes( + raw, + crtc, + primary, + cursor, + data.crtc_funcs.get(), + ptr::null(), + ))?; + (*crtc).helper_private = data.crtc_helper.get(); + bindings::drm_crtc_enable_color_mgmt(crtc, 0, false, GAMMA_SIZE); + + // The CRTC now has an index: bind both planes and the encoder to it. + let crtc_mask = 1u32 << (*crtc).index; + (*primary).possible_crtcs = crtc_mask; + (*cursor).possible_crtcs = crtc_mask; + + // Virtual encoder bound to this head's connector. + let encoder = head.encoder.get(); + to_result(bindings::drm_encoder_init( + raw, + encoder, + data.encoder_funcs.get(), + bindings::DRM_MODE_ENCODER_VIRTUAL as i32, + ptr::null(), + ))?; + (*encoder).possible_crtcs = crtc_mask; + to_result(bindings::drm_connector_attach_encoder(conn, encoder))?; + + // Rotation property on the primary plane (DLM rotates host-side; vino remaps in the + // scanout encode -- see `rot_src`). Canonical helper; non-fatal on failure. + let supported = bindings::DRM_MODE_ROTATE_0 + | bindings::DRM_MODE_ROTATE_90 + | bindings::DRM_MODE_ROTATE_180 + | bindings::DRM_MODE_ROTATE_270 + | bindings::DRM_MODE_REFLECT_X + | bindings::DRM_MODE_REFLECT_Y; + let rc = bindings::drm_plane_create_rotation_property( + primary, + bindings::DRM_MODE_ROTATE_0, + supported, + ); + if rc != 0 { + pr_warn!("vino: head{} rotation property unavailable ({rc})\n", head.index); + } + } + Ok(()) +} + +/// Thin wrapper so the `unsafe` block above reads cleanly. +unsafe fn drm_mode_config_reset(raw: *mut bindings::drm_device) { + // SAFETY: `raw` is a valid drm_device with mode_config initialised. + unsafe { bindings::drm_mode_config_reset(raw) }; +} diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs index e9e6324b717b..1091dcc992c7 100644 --- a/drivers/gpu/drm/vino/vino.rs +++ b/drivers/gpu/drm/vino/vino.rs @@ -44,6 +44,7 @@ use kernel::{ alloc::flags::GFP_KERNEL, bindings, + drm, device::{self, Core}, error::code::{ENODEV, EINVAL}, prelude::*, @@ -79,6 +80,24 @@ fn timeout() -> Delta { /// hardware -- the dock runs the whole plaintext handshake but never engages the encrypted CP. static CP_ENGAGED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); +/// One-shot: clear-halt + prime the video endpoints before the first live-scanout EP08 write. +static EP08_SCANOUT_PRIMED: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + +/// Consecutive failed live-scanout frames, for log rate-limiting. Until CP engages, the dock +/// NAKs every EP08 write (EPROTO), so without this every compositor pageflip would spam dmesg. +static SCANOUT_FAILS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); + +/// Pageflip throttle: number of upcoming pageflips to skip before the next scanout attempt +/// (a backoff while the dock NAKs). A single successful frame clears it. +static SCANOUT_SKIP: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); + +/// Set once the bring-up work item finishes (AKE/CP attempt done). `detect` only connects the +/// live-scanout connector AFTER this, so a compositor enabling the output cannot start EP08 +/// scanout on top of the still-running AKE on the same USB device. +static BRINGUP_COMPLETE: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + mod proto; mod crypto; mod rng; @@ -103,9 +122,13 @@ struct Session { cap_announce: KVec<u8>, } +mod drm_sink; + /// Per-bound-interface driver state. struct VinoDriver { _intf: ARef<usb::Interface>, + /// The registered `drm::Device` (only on the control interface, iface 0). + _ddev: Option<ARef<drm_sink::VinoDrmDevice>>, } /// Deferred bring-up work item: the bring-up sequence run on the system workqueue instead @@ -115,6 +138,7 @@ struct VinoDriver { #[pin_data] struct BringUp { intf: ARef<usb::Interface>, + ddev: Option<ARef<drm_sink::VinoDrmDevice>>, #[pin] work: Work<BringUp>, } @@ -124,10 +148,14 @@ impl HasWork<Self> for BringUp { self.work } } impl BringUp { - fn new(intf: ARef<usb::Interface>) -> Result<Arc<Self>> { + fn new( + intf: ARef<usb::Interface>, + ddev: Option<ARef<drm_sink::VinoDrmDevice>>, + ) -> Result<Arc<Self>> { Arc::pin_init( pin_init!(BringUp { intf, + ddev, work <- new_work!("vino::bring_up"), }), GFP_KERNEL, @@ -141,39 +169,73 @@ impl WorkItem for BringUp { fn run(this: Arc<BringUp>) { let cdev: &device::Device = this.intf.as_ref(); let dev: &usb::Device = this.intf.as_ref(); - // WIP scaffold: plaintext bring-up, the clean-room HDCP 2.2 AKE/LC/SKE, then the - // post-SKE CP setup. Bind regardless of the outcome -- there is no display path until - // the dock engages the encrypted control plane, which it currently never does (see the - // "help wanted" note at the top of the file). The DRM sink lands in a later patch. + let ddev = &this.ddev; + // WIP scaffold: attempt the plaintext bring-up, then the clean-room HDCP 2.2 + // AKE/LC/SKE, then the post-SKE CP setup. Bind regardless of the outcome -- there + // is no display path until the dock engages the encrypted control plane, which it + // currently never does (see the "help wanted" note at the top of the file). match VinoDriver::bring_up(dev) { Ok(()) => { dev_info!(cdev, "vino: plaintext session init OK\n"); match VinoDriver::run_ake(dev) { Ok(session) => { dev_info!(cdev, "vino: HDCP AKE + LC + SKE complete (session keyed)\n"); + // Dev diagnostic: the live session key/riv, so the dock's encrypted + // EP84 replies can be decoded offline from a usbmon capture. Behind + // pr_debug, so compiled out unless dynamic debug is enabled. pr_debug!("vino: SESSION ks={:02x?} riv={:02x?}\n", &session.ks, &session.riv); - // Phase 2c: drive the post-SKE CP setup. send_cp_setup re-seals DLM's - // captured setup template under THIS session's live ks/riv and sends it; - // `acks` counts the dock's encrypted wsub=0x45 replies. THIS IS THE WALL: - // on a cold dock `acks` stays 0 -- the dock runs the entire plaintext - // handshake but never engages the encrypted CP. + + // Phase 2c: drive the post-SKE CP setup. send_cp_setup re-seals + // DLM's captured setup template under THIS session's live ks/riv and + // sends it; `acks` counts the dock's encrypted wsub=0x45 replies. + // THIS IS THE WALL: on a cold dock `acks` stays 0 -- the dock runs the + // entire plaintext handshake but never engages the encrypted CP. let mut edid_out: Option<KVec<u8>> = None; match VinoDriver::send_cp_setup(dev, &session, &mut edid_out) { - Ok((n, acks, _wseq_end, _ctr_end)) => { + Ok((n, acks, wseq_end, ctr_end)) => { dev_info!(cdev, "vino: CP setup sent -- {n} messages, {acks} dock CP acks (wsub=0x45)\n"); - // CP engagement gates EP08 video (added in a later patch): until - // the dock acks, pushing pixels at it wedges the hub. + // CP engagement gates EP08 video: until the dock acks, pushing + // pixels at it wedges the hub. CP_ENGAGED.store(acks > 0, core::sync::atomic::Ordering::SeqCst); + // Publish the engaged session to the DRM device so the KMS + // callbacks + // can send runtime CP (mode-set on a modeset, cursor on motion), + // continuing this keystream. Only when the dock actually engaged. + if acks > 0 { + if let Some(d) = ddev.as_ref() { + let data: &drm_sink::VinoDrmData = d; + data.publish_session( + &session.ks, &session.riv, wseq_end, ctr_end, + ); + } + } } Err(e) => dev_info!(cdev, "vino: CP setup incomplete ({e:?}) -- WIP\n"), } + // Cache the dock's EDID on the DRM device (when the CP channel + // delivered it) so the connector's get_modes installs the real + // monitor descriptor via the standard DRM EDID helpers. + if let (Some(blob), Some(d)) = (edid_out, ddev.as_ref()) { + let n = blob.len(); + let data: &drm_sink::VinoDrmData = d; + data.set_edid(blob); + dev_info!(cdev, "vino: cached dock EDID for connector ({n} bytes)\n"); + } } Err(e) => dev_info!(cdev, "vino: HDCP AKE incomplete ({e:?}) -- WIP\n"), } } Err(e) => dev_info!(cdev, "vino: session init incomplete ({e:?}) -- WIP\n"), } + // Bring-up attempt finished: allow the live-scanout connector to report connected + // and let a compositor drive EP08 frames, without racing the handshake. + BRINGUP_COMPLETE.store(true, core::sync::atomic::Ordering::SeqCst); + if let Some(d) = ddev.as_ref() { + let data: &drm_sink::VinoDrmData = d; + data.fire_hotplug(); + dev_info!(cdev, "vino: bring-up complete -- live-scanout connector now connected\n"); + } } } @@ -1596,16 +1658,59 @@ fn probe<'bound>( return Err(ENODEV); } dev_info!(cdev, "vino: bound D6000 interface {ifnum} (idle -- control is iface 0)\n"); - return Ok(Self { _intf: intf.into() }); + return Ok(Self { _intf: intf.into(), _ddev: None }); } dev_info!(cdev, "vino: bound DisplayLink D6000 -- plaintext session bring-up\n"); - // Bring-up is blocking synchronous USB I/O; hand it to the system workqueue so - // probe() returns immediately and userspace stays responsive. The work item holds - // a refcounted handle to the interface, so the bulk endpoints outlive probe(); USB - // I/O after an intervening disconnect simply errors and is logged. + // Phase 3: register a real DRM/KMS device on the control interface so the dock + // shows up as a mode-settable `card`/`renderD` node (atomic KMS via the simple + // display pipe, one 1080p virtual connector, GEM-shmem dumb buffers). Non-fatal: + // bring-up still proceeds (and the interface still binds) if any step fails, so + // a DRM-core hiccup can't regress the USB session work. + // Hold a refcounted handle to the bound interface; one copy goes into the DRM + // device-private (for the EP08 scanout path), one stays in `VinoDriver`. let intf_ref: ARef<usb::Interface> = intf.into(); - match BringUp::new(intf_ref.clone()) { + // DRM device lifecycle (drm-rust API): allocate an `UnregisteredDevice`, wire up + // the KMS pipeline on it while still unregistered, then hand it to + // `Registration::new_foreign_owned` (which registers it and ties its lifetime to + // the bound USB device via devres, returning a borrowed `&Device`). + let ddev: Option<ARef<drm_sink::VinoDrmDevice>> = + match drm::UnregisteredDevice::<drm_sink::VinoDrmDriver>::new( + cdev, + drm_sink::VinoDrmData::new(intf_ref.clone()), + ) { + Ok(unreg) => match drm_sink::kms_init(&unreg) { + Ok(()) => match drm::driver::Registration::new_foreign_owned(unreg, cdev, 0) { + Ok(reg_dev) => { + dev_info!(cdev, "vino: DRM+KMS device registered (card node live, 1080p)\n"); + Some(reg_dev.into()) + } + Err(e) => { + dev_info!(cdev, "vino: DRM registration failed ({e:?}) -- continuing without card node\n"); + None + } + }, + Err(e) => { + dev_info!(cdev, "vino: KMS init failed ({e:?}) -- continuing without card node\n"); + None + } + }, + Err(e) => { + dev_info!(cdev, "vino: drm::UnregisteredDevice::new failed ({e:?}) -- continuing\n"); + None + } + }; + + // Bring-up (preamble + HDCP AKE + ~6 s of lockstep CP replay) is all blocking + // synchronous USB I/O. Running it inline here pins the USB driver-model probe + // thread while the DRM card node is already registered and live, which stalled + // the compositor (KWin) on first plug until the dock was physically yanked. Hand + // it to the system workqueue so `probe()` returns immediately and userspace KMS + // stays responsive. The work item holds refcounted handles to the interface (for + // the bulk endpoints) and the DRM device (for EDID caching), so they outlive + // `probe()`; USB I/O after an intervening disconnect simply errors and is logged, + // exactly like any other failed bring-up step. + match BringUp::new(intf_ref.clone(), ddev.clone()) { Ok(work) => { let _ = workqueue::system().enqueue(work); dev_info!(cdev, "vino: bring-up queued on system workqueue\n"); @@ -1613,7 +1718,7 @@ fn probe<'bound>( Err(e) => dev_info!(cdev, "vino: failed to queue bring-up ({e:?}) -- WIP\n"), } - Ok(Self { _intf: intf_ref }) + Ok(Self { _intf: intf_ref, _ddev: ddev }) } fn disconnect<'bound>(intf: &'bound usb::Interface<Core<'_>>, _data: Pin<&Self>) { -- 2.54.0
