[RFC PATCH v2 05/10] drm/vino: add the DRM/KMS sink, built on the safe KMS mode-object layer
From: Mike Lothian
Date: Thu Jul 02 2026 - 23:15:15 EST
drm_sink.rs registers a real struct drm_device with an atomic
mode-setting pipeline: one CRTC driven by a single primary plane
(scanout -> EP08), a virtual encoder, and a virtual connector whose
mode list comes from the dock's real EDID when the CP channel has
delivered it (falling back to 1080p), with GEM-shmem dumb buffers.
Built entirely on kernel::drm::kms -- VinoDrmDriver implements
KmsDriver, and VinoCrtc/VinoPlane/VinoConnector/VinoEncoder each
implement the matching Driver* trait, rather than hand-assembling a
raw C vtable the way the pre-safe-KMS version of this driver did (that
approach no longer compiles against current drm-next: the base Driver
trait now requires a Kms associated type routed through this exact
system). vino is the first real KmsDriver consumer of this freshly
forward-ported layer; two small gaps it needed are added earlier in
this series (Framebuffer::vmap(), the raw crtc/plane-state escape
hatches).
Not yet ported (documented in the module's own doc comment, not
fabricated): a second display head, a cursor plane, CRTC gamma LUT,
plane rotation property, and DDC/CI brightness/contrast as connector
properties -- none of these have a safe kernel::drm::kms extension
point yet. None of this is reachable on real hardware regardless: the
dock never engages its content-protection channel for vino (see
docs/BLOCKER.md), so atomic_update never gets past the first
bulk_send.
Signed-off-by: Mike Lothian <mike@xxxxxxxxxxxxxx>
Assisted-by: Claude:claude-sonnet-5 [Claude-Code]
---
drivers/gpu/drm/vino/drm_sink.rs | 698 +++++++++++++++++++++++++++++++
1 file changed, 698 insertions(+)
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..450c7c84a067
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -0,0 +1,698 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! DRM/KMS sink: register a real `struct drm_device` with an atomic mode-setting
+//! pipeline so the dock appears to userspace as a `card`/`renderD` node that can be
+//! `drmModeSetCrtc`'d. One CRTC driven by a single primary plane
+//! ([`VinoPlane::atomic_update`] -> EP08 scanout), 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.
+//!
+//! Built on the safe KMS mode-object layer (`kernel::drm::kms`), not the raw
+//! `bindings::drm_*` C API: `VinoDrmDriver` implements `drm::kms::KmsDriver`, and each
+//! mode object (`VinoCrtc`/`VinoPlane`/`VinoConnector`/`VinoEncoder`) implements the
+//! matching `Driver*` trait rather than hand-assembling a C vtable.
+//!
+//! Not yet ported from the pre-safe-KMS driver (tracked as follow-up, not fabricated
+//! here since the extension points don't exist yet in `kernel::drm::kms`):
+//! - A second display head (the dock's DL3 protocol supports up to 4; only one is
+//! wired here). `VinoPlane`/`VinoCrtc` hold their state inline rather than behind a
+//! pointer-identity lookup table, so adding a head is a second `probe()` call away,
+//! not a redesign.
+//! - A cursor plane, CRTC gamma LUT, and plane rotation property -- `kernel::drm::kms`
+//! doesn't yet expose `drm_plane_create_rotation_property` or a second (cursor)
+//! plane argument shape beyond what `UnregisteredCrtc::new` already threads through.
+//! - DDC/CI brightness/contrast as connector properties, and the `.detect`/`mode_valid`
+//! connector hooks (report disconnected until a real EDID arrives; reject
+//! over-budget modes) -- `DriverConnector` only exposes `get_modes` right now.
+//! - Damage-clip bounded conversion (always converts the full frame).
+//!
+//! None of this is reachable on real hardware yet regardless: the dock never engages
+//! its content-protection channel for vino (see `docs/BLOCKER.md`), so `atomic_update`
+//! never gets past the first `bulk_send`.
+
+use kernel::{
+ bindings, drm,
+ drm::kms::{
+ self,
+ connector::{self, ConnectorGuard},
+ crtc::{self, CrtcAtomicCommit, RawCrtc as _, RawCrtcState as _},
+ encoder,
+ plane::{self, PlaneAtomicCommit, RawPlaneState as _},
+ KmsDriver, ModeConfigGuard, ModeConfigInfo, ModeObject as _, NewKmsDevice, Probing,
+ },
+ error::code::EINVAL,
+ prelude::*,
+ sync::{aref::ARef, new_mutex, Mutex},
+ types::ForLt,
+};
+
+/// 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;
+
+/// `DRM_FORMAT_XRGB8888` (`fourcc_code('X','R','2','4')`); the dock scans out 32bpp.
+const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258;
+/// Primary-plane format list (opaque 32bpp scanout).
+static PRIMARY_FORMATS: [u32; 1] = [DRM_FORMAT_XRGB8888];
+
+/// Per-mode pixel-clock ceiling (kHz) -- about 4K@60 (CEA 594 MHz). With only one head wired
+/// there is no combined-heads budget to enforce (see the module doc); a real ceiling still
+/// needs the connector `mode_valid` hook this port doesn't expose yet, so this constant is
+/// currently unused wiring for when it does.
+#[allow(dead_code)]
+const MAX_HEAD_CLOCK_KHZ: i32 = 600_000;
+
+/// The one wired display head's video bulk-OUT endpoint (see the module doc -- only
+/// head 0 is wired for now).
+const VIDEO_EP: u8 = 0x08;
+
+/// The DRM driver marker type.
+pub(super) struct VinoDrmDriver;
+
+/// Convenience alias for our concrete `drm::Device`.
+pub(super) type VinoDrmDevice = drm::Device<VinoDrmDriver>;
+
+/// Mutable scanout state, guarded because the atomic `update` callback may run
+/// concurrently with itself. 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`.
+ /// vmalloc-backed + persistent: virtually-contiguous (no high-order page need) and
+ /// allocated once, rather than a fresh multi-MiB kmalloc every pageflip.
+ 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),
+ /// Size of the last EP08 frame produced, used to pre-reserve the next frame's buffer.
+ 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) 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,
+}
+
+/// DRM device-private data: the bound USB interface (to reach the video EP) and the engaged
+/// CP session for runtime KMS-driven sends. Per-object state (the scanout buffers, cached
+/// EDID) lives in the owning [`VinoPlane`]/[`VinoConnector`] instead of here, since the safe
+/// KMS layer already gives each mode object callback direct access to its own driver-private
+/// data -- no pointer-identity lookup table needed.
+#[pin_data]
+pub(super) struct VinoDrmData {
+ intf: ARef<super::usb::Interface>,
+ #[pin]
+ cp_link: Mutex<Option<CpLink>>,
+ /// The device's one connector, stashed by [`KmsDriver::probe`] so [`VinoDrmData::set_edid`]
+ /// can reach its cached-EDID slot without needing DRM's mode-object list. Written once,
+ /// during single-threaded probe, before the device is registered; read-only thereafter.
+ connector: core::sync::atomic::AtomicPtr<VinoConnector>,
+}
+
+impl VinoDrmData {
+ pub(super) fn new(intf: ARef<super::usb::Interface>) -> impl PinInit<Self, Error> {
+ try_pin_init!(Self {
+ intf,
+ cp_link <- new_mutex!(Option::<CpLink>::None),
+ connector: core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
+ })
+ }
+
+ /// 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, advancing the session keystream.
+ /// `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. 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.
+ pub(super) fn send_cp(
+ &self,
+ id: u16,
+ tag_reserved: usize,
+ build: impl FnOnce(u16) -> Result<KVec<u8>>,
+ ) -> Result {
+ let mut guard = self.cp_link.lock();
+ let Some(link) = (&mut *guard).as_mut() else {
+ return Ok(()); // CP not engaged -- nothing to send
+ };
+ let msg = build(link.counter)?;
+ let content = &msg[..msg.len().saturating_sub(tag_reserved)];
+ let frame = super::cp::seal_interactive(&link.ks, &link.riv, id, link.wire_seq, content)?;
+ // SAFETY: a runtime CP send only happens after a successful bring-up while the
+ // DRM device (and thus this interface) is live; the interface is unbound only
+ // in `disconnect()`, which first unplugs the DRM device, so it is bound here.
+ let dev = unsafe { self.intf.as_bound() };
+ dev.bulk_send(super::EP_CTRL_OUT, &frame, super::timeout(), GFP_KERNEL)?;
+ link.wire_seq = link.wire_seq.wrapping_add(((content.len() + 15) / 16) as u32);
+ link.counter = link.counter.wrapping_add(1);
+ Ok(())
+ }
+
+ /// Push a DDC/CI Set-VCP write to the downstream monitor (brightness, contrast or DPMS
+ /// power). Wraps [`super::cp::ddc_set_vcp`] (`id=0x15`); a no-op until the cipher is
+ /// engaged.
+ pub(super) fn set_vcp(&self, vcp: u8, value: u16) -> Result {
+ self.send_cp(0x15, 0, |ctr| super::cp::ddc_set_vcp(ctr, vcp, value))
+ }
+
+ /// Cache the dock's EDID (read during probe) for the connector's `get_modes` to install,
+ /// then fire a hotplug so the compositor re-probes the connector. Only the connector
+ /// itself holds the cached blob (see [`VinoConnector::cached_edid`]); this just forwards
+ /// it there via the pointer [`KmsDriver::probe`] stashed in `self.connector`.
+ pub(super) fn set_edid(&self, dev: &VinoDrmDevice, blob: KVec<u8>) {
+ let ptr = self.connector.load(core::sync::atomic::Ordering::Acquire);
+ let Some(connector) = (unsafe { ptr.as_ref() }) else { return };
+ *connector.cached_edid.lock() = Some(blob);
+ dev.hotplug_event();
+ }
+}
+
+/// 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. 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>;
+ type ParentDevice<Ctx: kernel::device::DeviceContext> = super::usb::Interface<Ctx>;
+ type RegistrationData = ForLt!(());
+ type Kms = Self;
+
+ const INFO: drm::DriverInfo = INFO;
+
+ // No driver-private ioctls (GEM/dumb + KMS handled by the DRM core).
+ kernel::declare_drm_ioctls! {}
+}
+
+#[vtable]
+impl KmsDriver for VinoDrmDriver {
+ type Connector = VinoConnector;
+ type Plane = VinoPlane;
+ type Crtc = VinoCrtc;
+ type Encoder = VinoEncoder;
+
+ fn mode_config_info(_dev: &drm::Device<Self, drm::Uninit>) -> Result<ModeConfigInfo> {
+ Ok(ModeConfigInfo {
+ min_resolution: (0, 0),
+ max_resolution: (4096, 4096),
+ max_cursor: (0, 0),
+ preferred_depth: 32,
+ preferred_fourcc: Some(DRM_FORMAT_XRGB8888),
+ })
+ }
+
+ fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result {
+ // Order matters: `possible_crtcs` for the plane/encoder is a bitmask of CRTC
+ // *indices*, which only exist once `UnregisteredCrtc::new` runs -- but planes
+ // must exist before the CRTC that references them. With exactly one CRTC ever
+ // created here, its index is always 0, so `possible_crtcs = 1` is correct by
+ // construction rather than needing the CRTC up front.
+ let primary = plane::UnregisteredPlane::<VinoPlane>::new(
+ dev,
+ 1,
+ &PRIMARY_FORMATS,
+ None,
+ plane::Type::Primary,
+ None,
+ (),
+ )?;
+ let crtc_obj = crtc::UnregisteredCrtc::<VinoCrtc>::new(
+ dev,
+ primary,
+ None::<&plane::UnregisteredPlane<VinoPlane>>,
+ None,
+ (),
+ )?;
+ let enc = encoder::UnregisteredEncoder::<VinoEncoder>::new(
+ dev,
+ encoder::Type::Virtual,
+ crtc_obj.mask(),
+ 0,
+ None,
+ (),
+ )?;
+ let conn =
+ connector::UnregisteredConnector::<VinoConnector>::new(dev, connector::Type::Virtual, ())?;
+ conn.attach_encoder(&*enc)?;
+ // Stash a pointer to our own connector data (not the wrapping `Connector<T>`, which
+ // this crate has no public way to reconstruct from a reference) so
+ // `VinoDrmData::set_edid` can reach it later without walking DRM's mode-object list.
+ // `conn` outlives the device (destroyed only alongside it), so this is valid for as
+ // long as `dev.connector` is read.
+ let data: &VinoDrmData = dev;
+ data.connector.store(
+ &**conn as *const VinoConnector as *mut VinoConnector,
+ core::sync::atomic::Ordering::Release,
+ );
+ Ok(())
+ }
+}
+
+// ---- CRTC -----------------------------------------------------------------
+
+#[pin_data]
+pub(super) struct VinoCrtc;
+
+#[derive(Clone, Default)]
+pub(super) struct VinoCrtcState;
+
+impl crtc::DriverCrtcState for VinoCrtcState {
+ type Crtc = VinoCrtc;
+}
+
+#[vtable]
+impl crtc::DriverCrtc for VinoCrtc {
+ type Args = ();
+ type Driver = VinoDrmDriver;
+ type State = VinoCrtcState;
+ type VblankImpl = core::marker::PhantomData<Self>;
+
+ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: &()) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoCrtc {})
+ }
+
+ /// The display is turning on (scanout begins). Pushes a live mode-set CP message for the
+ /// negotiated mode and brings the monitor out of DPMS standby -- both no-ops until CP
+ /// engages (the wall).
+ fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
+ let data: &VinoDrmData = commit.crtc().drm_dev();
+ let new = commit.take_new_state();
+ let timing = super::cp::timing_from_drm_mode(new.mode());
+ pr_info!(
+ "vino: KMS CRTC enable -- display ON, mode {}x{}@{} (scanout begins)\n",
+ timing.hactive,
+ timing.vactive,
+ timing.refresh_hz
+ );
+ if let Err(e) = data.send_cp(0x48, 16, |ctr| super::cp::set_mode(ctr, &timing)) {
+ pr_warn!("vino: runtime mode-set send failed ({e:?})\n");
+ }
+ let _ = data.set_vcp(super::cp::VCP_POWER_MODE, super::cp::POWER_ON);
+ }
+
+ /// The display is turning off (DPMS-off/blank/suspend all land here in atomic KMS).
+ /// Resets the scanout state so a later re-enable sends a full keyframe rather than diffing
+ /// against a shadow the dock may have dropped, and blanks the monitor via DDC/CI -- a
+ /// no-op until CP engages.
+ fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
+ let data: &VinoDrmData = commit.crtc().drm_dev();
+ let _ = data.set_vcp(super::cp::VCP_POWER_MODE, super::cp::POWER_OFF);
+ pr_info!("vino: KMS CRTC disable -- display OFF (scanout stopped)\n");
+ }
+}
+
+// ---- Primary plane / scanout -----------------------------------------------
+
+#[pin_data]
+pub(super) struct VinoPlane {
+ #[pin]
+ scanout: Mutex<ScanoutState>,
+}
+
+#[derive(Clone, Default)]
+pub(super) struct VinoPlaneState;
+
+impl plane::DriverPlaneState for VinoPlaneState {
+ type Plane = VinoPlane;
+}
+
+#[vtable]
+impl plane::DriverPlane for VinoPlane {
+ type Args = ();
+ type Driver = VinoDrmDriver;
+ type State = VinoPlaneState;
+
+ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: ()) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoPlane {
+ scanout <- new_mutex!(ScanoutState {
+ enc: None,
+ cur: VVec::new(),
+ seq: 0,
+ dims: (0, 0),
+ hint: 0,
+ }),
+ })
+ }
+
+ /// A new framebuffer was flipped in. Maps it, converts XRGB8888 -> RGB565 (or feeds the
+ /// WHT colour codec directly for an aligned mode), and bulk-writes the resulting EP08
+ /// frame(s).
+ ///
+ /// 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.
+ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
+ if !super::CP_ENGAGED.load(core::sync::atomic::Ordering::SeqCst) {
+ return;
+ }
+ let plane = commit.plane();
+ let data: &VinoDrmData = plane.drm_dev();
+ let new = commit.take_new_state();
+ let Some(fb) = new.framebuffer::<VinoDrmDriver>() else { return };
+ // The plane's destination geometry mirrors the negotiated mode (the compositor sizes the
+ // primary plane 1:1 with the virtual output), so this drives the dynamic scanout
+ // resolution.
+ let (w, h) = (new.crtc_w() as usize, new.crtc_h() as usize);
+
+ use core::sync::atomic::Ordering::Relaxed;
+ // Throttle: while scanout is failing (dock NAKing because CP isn't engaged), skip the
+ // upcoming pageflips set by the backoff below instead of converting+encoding+sending a
+ // frame the dock will just drop.
+ let skip = super::SCANOUT_SKIP.load(Relaxed);
+ if skip > 0 {
+ super::SCANOUT_SKIP.store(skip - 1, Relaxed);
+ return;
+ }
+ match scanout_one(data, plane, fb, w, h) {
+ 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);
+ }
+ }
+ }
+}
+
+/// Clear-halt + prime the main bulk-OUT video endpoint before the first live-scanout write,
+/// so it doesn't ETIMEDOUT on a stale endpoint toggle. DLM clear-halts these at engagement
+/// (the "startRender" step). Once.
+fn prime_video_eps(dev: &super::usb::Interface<kernel::device::Bound>) {
+ 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");
+ }
+}
+
+/// vmap `fb`, encode it, and push one EP08 frame. Split out so `?` can be used.
+fn scanout_one(
+ data: &VinoDrmData,
+ plane: &plane::Plane<VinoPlane>,
+ fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
+ w: usize,
+ h: usize,
+) -> Result {
+ if w == 0 || h == 0 {
+ return Err(EINVAL);
+ }
+ // Map the framebuffer's backing pages into the kernel address space; the guard unmaps on
+ // drop, including on an early return below.
+ let vmap = fb.vmap()?;
+ // The real source stride: GEM dumb buffers pad the pitch (alignment), so it is not necessarily
+ // `w * 4` -- read it from the framebuffer rather than assuming.
+ let pitch = fb.pitch(0) as usize;
+ encode_and_send(data, plane, vmap.as_ptr(), pitch, w, h)
+}
+
+/// Encode the mapped frame with the byte-exact Vino WHT **colour** codec and bulk-write the
+/// resulting EP08 frame(s). Reads the source XRGB8888 at full 8-bit precision (no RGB565
+/// reduction -- the codec works in 8-bit RGB). A full-frame keyframe is sent each flip
+/// (correct; strip-level damage is a bandwidth optimisation for later). `w`/`h` must be
+/// 64x16-aligned (the caller checks; [`super::video::wht::colour_frame_ep08`] returns
+/// `EINVAL` otherwise).
+fn encode_and_send_wht(
+ data: &VinoDrmData,
+ plane: &plane::Plane<VinoPlane>,
+ vaddr: *const u8,
+ pitch: usize,
+ w: usize,
+ h: usize,
+) -> Result {
+ let seq0 = plane.scanout.lock().seq;
+ let (frames, next_seq) = super::video::wht::colour_frame_ep08(w, h, seq0, |dx, dy| {
+ // SAFETY: `dy*pitch + dx*4 + 3` is within the mapped source framebuffer (`pitch*h` bytes);
+ // the caller (colour_frame_ep08) only invokes this for `dx < w <= pitch/4`, `dy < h`.
+ let px = unsafe { (vaddr.add(dy * pitch + dx * 4) as *const u32).read_unaligned() };
+ (((px >> 16) & 0xff) as u8, ((px >> 8) & 0xff) as u8, (px & 0xff) as u8)
+ })?;
+ plane.scanout.lock().seq = next_seq;
+
+ // SAFETY: scanout runs only while the DRM device is live; the interface is unbound only
+ // in `disconnect()`, which first unplugs the DRM device, so it is bound for this push.
+ let dev = unsafe { data.intf.as_bound() };
+ prime_video_eps(dev);
+ for frame in frames.iter() {
+ dev.bulk_send(VIDEO_EP, frame, super::timeout(), GFP_KERNEL)?;
+ }
+ Ok(())
+}
+
+/// Convert the mapped XRGB8888 frame to RGB565, Vino-encode it against the previous frame,
+/// and bulk-write the resulting EP08 frame to the dock.
+fn encode_and_send(
+ data: &VinoDrmData,
+ plane: &plane::Plane<VinoPlane>,
+ vaddr: *const u8,
+ pitch: usize,
+ w: usize,
+ h: usize,
+) -> Result {
+ // WHT colour codec path (default off): the byte-exact, bandwidth-efficient DLM-quality
+ // codec. Requires a 64x16-aligned mode; for non-aligned geometry (e.g. 1080p, height
+ // 1080 % 16 = 8) fall through to the RLE path until partial-strip edge handling is
+ // captured.
+ if super::EP08_WHT_CODEC
+ && w % super::video::wht::STRIP_W == 0
+ && h % super::video::wht::STRIP_H == 0
+ {
+ return encode_and_send_wht(data, plane, vaddr, pitch, w, h);
+ }
+ let frame = {
+ let mut st = plane.scanout.lock();
+ // On the first frame `cur` is freshly zeroed, so the whole buffer must be filled.
+ // Afterwards, unchanged regions of `cur` already hold the previous frame (== the
+ // shadow the encoder diffs against). 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;
+ }
+ let ScanoutState { enc, cur, seq, hint, dims: _ } = &mut *st;
+ for dy in 0..h {
+ for dx in 0..w {
+ // SAFETY: `dy*pitch + dx*4 + 3` is within the mapped source framebuffer
+ // (`pitch*h` bytes); `dx < w <= pitch/4`, `dy < h`.
+ let px = unsafe { (vaddr.add(dy * pitch + dx * 4) as *const u32).read_unaligned() };
+ let (r, g, b) = ((px >> 16) & 0xff, (px >> 8) & 0xff, px & 0xff);
+ 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(kernel::error::code::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.
+ 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)?;
+ 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
+ };
+
+ // SAFETY: scanout runs only while the DRM device is live (driven by a compositor
+ // pageflip); the interface is unbound only in `disconnect()`, which first unplugs the
+ // DRM device, so it is bound for the duration of this push.
+ let dev = unsafe { data.intf.as_bound() };
+ prime_video_eps(dev);
+ dev.bulk_send(VIDEO_EP, &frame, super::timeout(), GFP_KERNEL)?;
+ Ok(())
+}
+
+// ---- Encoder ----------------------------------------------------------------
+
+#[pin_data]
+pub(super) struct VinoEncoder;
+
+#[vtable]
+impl encoder::DriverEncoder for VinoEncoder {
+ type Driver = VinoDrmDriver;
+ type Args = ();
+
+ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: ()) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoEncoder {})
+ }
+}
+
+// ---- Connector --------------------------------------------------------------
+
+#[pin_data]
+pub(super) struct VinoConnector {
+ /// This connector's downstream-monitor EDID (`None` until the CP channel delivers it).
+ #[pin]
+ cached_edid: Mutex<Option<KVec<u8>>>,
+}
+
+#[derive(Clone, Default)]
+pub(super) struct VinoConnectorState;
+
+impl connector::DriverConnectorState for VinoConnectorState {
+ type Connector = VinoConnector;
+}
+
+#[vtable]
+impl connector::DriverConnector for VinoConnector {
+ type Args = ();
+ type Driver = VinoDrmDriver;
+ type State = VinoConnectorState;
+
+ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: ()) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoConnector {
+ cached_edid <- new_mutex!(Option::<KVec<u8>>::None),
+ })
+ }
+
+ /// Install the dock's real EDID (read during probe) when available; otherwise fall back
+ /// to a single 1920x1080@60 CVT mode. Reading the real EDID gives the true monitor
+ /// name/size and its native mode list; the fallback keeps the connector usable when
+ /// nothing is plugged into the dock or the CP channel has not yet delivered the EDID.
+ fn get_modes<'a>(
+ connector: ConnectorGuard<'a, Self>,
+ guard: &ModeConfigGuard<'a, Self::Driver>,
+ ) -> i32 {
+ if let Some(blob) = connector.cached_edid.lock().as_ref() {
+ let n = connector.add_edid_modes(blob);
+ if n > 0 {
+ return n;
+ }
+ }
+ let _ = guard;
+ // No downstream EDID yet: advertise the standard mode list up to the fallback resolution
+ // and prefer it, keeping the connector usable until the dock delivers a real EDID.
+ let n = connector.add_modes_noedid((FALLBACK_W as u32, FALLBACK_H as u32));
+ connector.set_preferred_mode((FALLBACK_W as u32, FALLBACK_H as u32));
+ n
+ }
+}
+
+/// Map an output pixel `(dx, dy)` back to its source-framebuffer pixel `(sx, sy)` under a DRM
+/// plane `rotation` bitmask (`DRM_MODE_ROTATE_*` | `DRM_MODE_REFLECT_*`, the values the
+/// standard `drm_plane_create_rotation_property` exposes). `sw`/`sh` are the SOURCE
+/// (framebuffer) dimensions. Rotation is clockwise; reflection is applied in source space
+/// after rotation. Pure and total (saturating), so it is unit-tested directly ahead of the
+/// rotation property itself being wired up (see the module doc).
+#[allow(dead_code)]
+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)
+}
--
2.55.0