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 <[email protected]> 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
