A from-scratch Rust reimplementation of DisplayLink's EVDI kernel module: a virtual DRM/KMS display whose framebuffer is pulled by a userspace client (DisplayLinkManager via libevdi) rather than scanned out to local hardware.
The driver registers a single-head atomic-KMS output with a primary and a cursor plane, creates cards on demand through /sys/devices/evdi (optionally bound to a USB dock so they appear and disappear with it), and exposes the libevdi private ioctl ABI (CONNECT / REQUEST_UPDATE / GRABPIX / DDCCI_RESPONSE / ENABLE_CURSOR_EVENTS) plus the drm_event output stream. GRABPIX tracks FB_DAMAGE_CLIPS and hands back only the changed rectangles per frame. It identifies itself to userspace as "evdi" for compatibility with the existing libevdi / DisplayLinkManager stack. Built on the safe KMS mode-object layer; consumes the binding fixes/helpers in the preceding patch. It is a real file tree under drivers/gpu/drm/evdi/ and also maintained as the standalone "revdi" project. NOT FOR UPSTREAM SUBMISSION as posted -- it depends on the not-yet-merged Rust DRM/KMS bindings and is carried here for local integration. Signed-off-by: Mike Lothian <[email protected]> Assisted-by: Claude:claude-opus-4-8 [Claude-Code] --- drivers/gpu/drm/Kconfig | 2 + drivers/gpu/drm/Makefile | 1 + drivers/gpu/drm/evdi/Kconfig | 20 + drivers/gpu/drm/evdi/Makefile | 2 + drivers/gpu/drm/evdi/ddcci.rs | 43 ++ drivers/gpu/drm/evdi/evdi.rs | 405 ++++++++++++++++++ drivers/gpu/drm/evdi/ioctl.rs | 248 +++++++++++ drivers/gpu/drm/evdi/kms.rs | 729 ++++++++++++++++++++++++++++++++ drivers/gpu/drm/evdi/painter.rs | 199 +++++++++ drivers/gpu/drm/evdi/uapi.rs | 210 +++++++++ 10 files changed, 1859 insertions(+) create mode 100644 drivers/gpu/drm/evdi/Kconfig create mode 100644 drivers/gpu/drm/evdi/Makefile create mode 100644 drivers/gpu/drm/evdi/ddcci.rs create mode 100644 drivers/gpu/drm/evdi/evdi.rs create mode 100644 drivers/gpu/drm/evdi/ioctl.rs create mode 100644 drivers/gpu/drm/evdi/kms.rs create mode 100644 drivers/gpu/drm/evdi/painter.rs create mode 100644 drivers/gpu/drm/evdi/uapi.rs diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig index 8fb9c9f787ba..e855f6f7bced 100644 --- a/drivers/gpu/drm/Kconfig +++ b/drivers/gpu/drm/Kconfig @@ -362,6 +362,8 @@ source "drivers/gpu/drm/virtio/Kconfig" source "drivers/gpu/drm/vkms/Kconfig" source "drivers/gpu/drm/vmwgfx/Kconfig" source "drivers/gpu/drm/vino/Kconfig" + +source "drivers/gpu/drm/evdi/Kconfig" source "drivers/gpu/drm/xe/Kconfig" source "drivers/gpu/drm/xen/Kconfig" source "drivers/gpu/drm/xlnx/Kconfig" diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile index 42a04359cc63..3f22c7131e51 100644 --- a/drivers/gpu/drm/Makefile +++ b/drivers/gpu/drm/Makefile @@ -186,6 +186,7 @@ obj-$(CONFIG_DRM_VMWGFX)+= vmwgfx/ obj-$(CONFIG_DRM_VGEM) += vgem/ obj-$(CONFIG_DRM_VKMS) += vkms/ obj-$(CONFIG_DRM_VINO) += vino/ +obj-$(CONFIG_DRM_EVDI) += evdi/ obj-$(CONFIG_DRM_NOUVEAU) +=nouveau/ obj-$(CONFIG_DRM_NOVA) += nova/ obj-$(CONFIG_DRM_EXYNOS) +=exynos/ diff --git a/drivers/gpu/drm/evdi/Kconfig b/drivers/gpu/drm/evdi/Kconfig new file mode 100644 index 000000000000..3177edc02dc5 --- /dev/null +++ b/drivers/gpu/drm/evdi/Kconfig @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0 +config DRM_EVDI + tristate "Extensible Virtual Display Interface (Rust)" + depends on DRM + depends on RUST + select DRM_KMS_HELPER + select DRM_GEM_SHMEM_HELPER + help + Rust reimplementation of DisplayLink's EVDI: a virtual DRM/KMS display + whose framebuffer is pulled by a userspace client (DisplayLinkManager + via libevdi) rather than scanned out to local hardware. Cards are + created on demand through /sys/devices/evdi and can be bound to a USB + dock so they appear/disappear with it. + + The module identifies itself to userspace as "evdi" for compatibility + with the existing libevdi / DisplayLinkManager stack. + + To compile this as a module, choose M here: the module is called evdi. + + If unsure, say N. diff --git a/drivers/gpu/drm/evdi/Makefile b/drivers/gpu/drm/evdi/Makefile new file mode 100644 index 000000000000..9c5e1a66337c --- /dev/null +++ b/drivers/gpu/drm/evdi/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_DRM_EVDI) += evdi.o diff --git a/drivers/gpu/drm/evdi/ddcci.rs b/drivers/gpu/drm/evdi/ddcci.rs new file mode 100644 index 000000000000..e12c919f82af --- /dev/null +++ b/drivers/gpu/drm/evdi/ddcci.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// DDC/CI over a virtual I2C adapter. Monitor-control tools (ddcutil, the desktop brightness +// slider, ...) issue I2C transfers to the DDC/CI address on the adapter EVDI registers; those are +// forwarded to the DisplayLinkManager client as DDCCI_DATA events, and the client's reply comes +// back through the DDCCI_RESPONSE ioctl (see `EvdiDrmData::ddcci_transfer`/`ddcci_respond`). + +use kernel::{i2c, prelude::*, sync::aref::ARef}; + +use crate::kms::{EvdiDrmData, EvdiDrmDevice, DDCCI_ADDRESS}; + +/// The EVDI DDC/CI I2C bus controller. +pub(crate) struct EvdiI2c; + +impl i2c::BusController for EvdiI2c { + type Context = ARef<EvdiDrmDevice>; + + fn master_xfer(dev: &ARef<EvdiDrmDevice>, msgs: &mut [i2c::Msg]) -> Result<usize> { + let ddev: &EvdiDrmDevice = dev; + let data: &EvdiDrmData = ddev; + let mut transferred = 0usize; + for msg in msgs.iter_mut() { + // Only DDC/CI traffic is forwarded to the client; ignore anything else on the bus. + if msg.addr() != DDCCI_ADDRESS { + continue; + } + let addr = msg.addr(); + let flags = msg.flags(); + let is_read = msg.is_read(); + if data + .ddcci_transfer(ddev, addr, flags, is_read, msg.buf_mut()) + .is_ok() + { + transferred += 1; + } + } + Ok(transferred) + } + + fn functionality(_dev: &ARef<EvdiDrmDevice>) -> u32 { + i2c::FUNC_I2C + } +} diff --git a/drivers/gpu/drm/evdi/evdi.rs b/drivers/gpu/drm/evdi/evdi.rs new file mode 100644 index 000000000000..735fa1cbefa3 --- /dev/null +++ b/drivers/gpu/drm/evdi/evdi.rs @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! EVDI — Extensible Virtual Display Interface, Rust implementation. +//! +//! A from-scratch Rust rewrite of the DisplayLink `evdi` kernel module. It presents virtual DRM +//! displays whose scanout is pulled by a userspace daemon (DisplayLinkManager) over a stable +//! ioctl + `drm_event` ABI. The module name and that ABI are kept identical to the C driver so +//! existing userspace (libevdi/DLM) keeps working unchanged. +//! +//! Structure: +//! - [`uapi`]: the DLM-facing ABI ([`evdi_drm.h`] mirror). +//! - [`kms`]: the DRM/KMS device (one virtual head, GEM-shmem dumb buffers). +//! - [`painter`]: connection state + `drm_event` delivery. +//! - [`ioctl`]: the five driver-private ioctls. +//! - This file: the platform driver (one DRM card per `evdi` platform device) and the module. + +use kernel::{ + device::{self, Core}, + drm, platform, + prelude::*, + sync::{aref::ARef, new_mutex, Mutex}, + types::Opaque, + ThisModule, +}; + +mod ddcci; +mod ioctl; +mod kms; +mod painter; +mod uapi; + +use kms::{EvdiDrmData, EvdiDrmDevice, EvdiDrmDriver}; + +/// Per-bound-device data held by the platform driver: the registered DRM card, whose lifetime is +/// otherwise tied to the bound platform device via devres. Dropped when the platform device unbinds. +struct BoundData { + _ddev: Option<ARef<EvdiDrmDevice>>, + /// The DDC/CI virtual I2C adapter (dropped → `i2c_del_adapter` on unbind). + _i2c: Option<Pin<KBox<kernel::i2c::BusAdapter<ddcci::EvdiI2c>>>>, +} + +/// The `evdi` platform driver. It binds to the `evdi` platform devices created by [`EvdiModule`] +/// (and, later, by the sysfs `add` attribute) and registers a DRM/KMS card parented to each. +struct EvdiPlatformDriver; + +impl platform::Driver for EvdiPlatformDriver { + type IdInfo = (); + type Data<'bound> = BoundData; + + fn probe<'bound>( + pdev: &'bound platform::Device<Core<'_>>, + _info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound { + let cdev: &device::Device<Core<'_>> = pdev.as_ref(); + + // Allocate an unregistered DRM device parented to this platform device, wire up its KMS + // pipeline (`KmsDriver::probe` runs inside `UnregisteredDevice::new`), then register it and + // tie its lifetime to the bound platform device via devres. + let ddev: Option<ARef<EvdiDrmDevice>> = + match drm::UnregisteredDevice::<EvdiDrmDriver>::new(pdev, EvdiDrmData::new()) { + Ok(unreg) => match drm::driver::Registration::new_foreign_owned(unreg, cdev, (), 0) + { + Ok(reg) => { + dev_info!(cdev, "evdi: DRM/KMS card registered\n"); + Some(reg.into()) + } + Err(e) => { + dev_err!(cdev, "evdi: DRM registration failed ({e:?})\n"); + None + } + }, + Err(e) => { + dev_err!(cdev, "evdi: DRM device allocation failed ({e:?})\n"); + None + } + }; + + // Register the DDC/CI virtual I2C adapter, parented to this platform device, so + // monitor-control tools can reach the display. Non-fatal. + let i2c = + ddev.as_ref().and_then(|d| { + match kernel::i2c::BusAdapter::<ddcci::EvdiI2c>::new( + c"DisplayLink I2C Adapter", + cdev, + d.clone(), + ) { + Ok(a) => Some(a), + Err(e) => { + dev_err!( + cdev, + "evdi: DDC/CI I2C adapter registration failed ({e:?})\n" + ); + None + } + } + }); + + Ok(BoundData { + _ddev: ddev, + _i2c: i2c, + }) + } +} + +/// One created card: its registered `evdi` platform device plus the USB device it was attached to +/// (`null` for a generic, non-USB card). The USB pointer is only ever compared for equality (never +/// dereferenced) — used to match the dock when it is unplugged (`USB_DEVICE_REMOVE`). +struct CardEntry { + _dev: platform::RegisteredDevice, + usb: *mut kernel::bindings::usb_device, +} + +// SAFETY: `usb` is only ever compared, never dereferenced; `RegisteredDevice` is itself `Send`. +unsafe impl Send for CardEntry {} + +/// The device registry behind the sysfs control interface +/// (`/sys/devices/evdi/{count,add,remove_all}`): the set of `evdi` platform devices created on +/// demand by the DisplayLink daemon or udev. Stored as the sysfs root device's driver data. +#[pin_data(PinnedDrop)] +struct EvdiRegistry { + #[pin] + devices: Mutex<KVec<CardEntry>>, + /// USB-removal notifier. When the dock a card is attached to is unplugged, its DRM device would + /// otherwise linger — keeping the compositor's fds open and the module refcount pinned — so this + /// tears the card down. `container_of` recovers the `EvdiRegistry` in the callback; registered in + /// [`EvdiRegistry::new`], unregistered in the [`PinnedDrop`]. + #[pin] + notifier: Opaque<kernel::bindings::notifier_block>, +} + +// SAFETY: `notifier` is a `notifier_block` owned by the kernel's internally-synchronized USB notifier +// chain once registered; we only touch it single-threaded (register in `new`, unregister in +// `PinnedDrop`) and never mutate it otherwise. `devices` is mutex-guarded. So the registry is safe to +// share and move across threads. +unsafe impl Send for EvdiRegistry {} +// SAFETY: See `Send`. +unsafe impl Sync for EvdiRegistry {} + +/// USB notifier callback: on `USB_DEVICE_REMOVE`, tear down any card attached to the removed device. +unsafe extern "C" fn usb_remove_notify( + nb: *mut kernel::bindings::notifier_block, + action: usize, + data: *mut core::ffi::c_void, +) -> core::ffi::c_int { + if action as u32 == kernel::bindings::USB_DEVICE_REMOVE { + // SAFETY: `nb` is the `notifier` field of a live `EvdiRegistry` — registered in `new` and + // unregistered in `PinnedDrop` before the struct is freed, so the container is valid here. + let registry = unsafe { + &*kernel::container_of!( + nb.cast::<Opaque<kernel::bindings::notifier_block>>(), + EvdiRegistry, + notifier + ) + }; + registry.remove_usb(data as *mut kernel::bindings::usb_device); + } + kernel::bindings::NOTIFY_DONE as core::ffi::c_int +} + +/// Maximum bus/port depth of a parsed `usb:` device address. +const MAX_USB_ADDR: usize = 8; + +/// Scratch used to locate a USB device by its bus/port address while iterating `usb_for_each_dev`. +struct UsbPath { + addr: [u32; MAX_USB_ADDR], + len: usize, + found: *mut kernel::bindings::usb_device, +} + +/// `usb_for_each_dev` callback: match `data` (a [`UsbPath`]) against `usb`'s bus/port chain, +/// walking parents leaf-first exactly like the C evdi. Stores the device and returns 1 on a match. +unsafe extern "C" fn match_usb_path( + usb: *mut kernel::bindings::usb_device, + data: *mut core::ffi::c_void, +) -> core::ffi::c_int { + // SAFETY: `usb_for_each_dev` is called below with a `&mut UsbPath` as `data` and passes a live + // `usb_device` for `usb`. + let m = unsafe { &mut *(data as *mut UsbPath) }; + let mut pdev = usb; + let mut i = m.len as isize - 1; + while !pdev.is_null() && i >= 0 && (i as usize) < MAX_USB_ADDR { + // SAFETY: `pdev` walks the valid parent chain of a live `usb_device`. + let mut port = unsafe { (*pdev).portnum } as u32; + if port == 0 { + // SAFETY: a live `usb_device` always has a valid `bus`. + port = unsafe { (*(*pdev).bus).busnum } as u32; + } + if port != m.addr[i as usize] { + return 0; + } + // SAFETY: as above. + let parent = unsafe { (*pdev).parent }; + if parent.is_null() && i == 0 { + m.found = usb; + return 1; + } + pdev = parent; + i -= 1; + } + 0 +} + +impl EvdiRegistry { + fn new() -> impl PinInit<Self, Error> { + try_pin_init!(Self { + devices <- new_mutex!(KVec::new()), + notifier <- Opaque::ffi_init(|slot: *mut kernel::bindings::notifier_block| { + // SAFETY: `slot` is this field's stable final address. Fill in the callback and link + // it onto the USB notifier chain; `usb_register_notify` keeps no other reference. + unsafe { + (*slot).notifier_call = Some(usb_remove_notify); + (*slot).next = core::ptr::null_mut(); + (*slot).priority = 0; + kernel::bindings::usb_register_notify(slot); + } + }), + }) + } + + /// Tear down every card attached to `usb` (the just-removed USB device). Runs in the USB + /// notifier's sleepable process context; the platform devices are unregistered after the + /// `devices` lock is released so the (sleeping) teardown does not run under the lock. + fn remove_usb(&self, usb: *mut kernel::bindings::usb_device) { + let mut to_drop: KVec<CardEntry> = KVec::new(); + { + let mut devices = self.devices.lock(); + let mut i = 0; + while i < devices.len() { + if devices[i].usb == usb { + if let Ok(entry) = devices.remove(i) { + let _ = to_drop.push(entry, GFP_KERNEL); + } + } else { + i += 1; + } + } + } + drop(to_drop); + } + + /// Create a card for the USB device named `token` (e.g. `"2-1.1"`), giving its platform device a + /// `device` sysfs symlink to that USB device — the pairing libevdi/DisplayLinkManager look up. + fn add_usb_device(&self, token: &str) -> Result { + // Parse "<bus>-<port>[.<port>...]" into a bus/port address (bus first, leaf last). + let mut m = UsbPath { + addr: [0; MAX_USB_ADDR], + len: 0, + found: core::ptr::null_mut(), + }; + let mut dash = token.split('-'); + let bus = dash.next().ok_or(EINVAL)?; + m.addr[m.len] = bus.parse::<u32>().map_err(|_| EINVAL)?; + m.len += 1; + for p in dash.next().ok_or(EINVAL)?.split('.') { + if m.len >= MAX_USB_ADDR { + return Err(EINVAL); + } + m.addr[m.len] = p.parse::<u32>().map_err(|_| EINVAL)?; + m.len += 1; + } + + // SAFETY: `usb_for_each_dev` calls `match_usb_path` for each live USB device with our + // `&mut m`; it retains no reference, so `m.found` is only used synchronously below. + unsafe { + kernel::bindings::usb_for_each_dev( + &raw mut m as *mut core::ffi::c_void, + Some(match_usb_path), + ) + }; + let usb = m.found; + if usb.is_null() { + return Err(EINVAL); + } + + let regdev = platform::RegisteredDevice::new(c"evdi", platform::DEVID_AUTO, None, 0)?; + let pdev = + regdev.device() as *const platform::Device as *mut kernel::bindings::platform_device; + // SAFETY: `pdev` is the platform device just registered above; `usb` is the matched, still + // live USB device. `sysfs_create_link` copies `name` and takes no lasting reference; the + // link is removed automatically when the platform device's kobject is destroyed on drop. + let ret = unsafe { + kernel::bindings::sysfs_create_link( + &raw mut (*pdev).dev.kobj, + &raw mut (*usb).dev.kobj, + c"device".as_char_ptr(), + ) + }; + kernel::error::to_result(ret)?; + self.devices + .lock() + .push(CardEntry { _dev: regdev, usb }, GFP_KERNEL)?; + Ok(()) + } +} + +#[pinned_drop] +impl PinnedDrop for EvdiRegistry { + fn drop(self: Pin<&mut Self>) { + // Stop USB-removal callbacks before the card list is torn down. `usb_unregister_notify` + // waits for any in-flight callback, so no `remove_usb` can race the field drops that follow. + // SAFETY: `notifier` was registered exactly once in `new`. + unsafe { kernel::bindings::usb_unregister_notify(self.notifier.get()) }; + } +} + +impl kernel::sysfs::DeviceAttributes for EvdiRegistry { + const ATTRS: &'static [kernel::sysfs::Attr] = &[ + kernel::sysfs::Attr::ro(c"count"), + kernel::sysfs::Attr::wo(c"add"), + kernel::sysfs::Attr::wo(c"remove_all"), + ]; + + fn show(&self, name: &CStr, buf: &mut [u8]) -> Result<usize> { + if name == c"count" { + write_uint(buf, self.devices.lock().len()) + } else { + Err(EINVAL) + } + } + + fn store(&self, name: &CStr, buf: &[u8]) -> Result { + if name == c"add" { + let text = core::str::from_utf8(buf).map_err(|_| EINVAL)?.trim(); + // DisplayLinkManager (via libevdi) creates a card for a specific dock by writing + // "usb:<bus>-<port>[.<port>...]:<intf>"; a bare integer creates that many generic cards. + if let Some(rest) = text.strip_prefix("usb:") { + let token = rest.split(':').next().unwrap_or("").trim(); + self.add_usb_device(token) + } else { + let count: u32 = text.parse().map_err(|_| EINVAL)?; + let mut devices = self.devices.lock(); + for _ in 0..count { + let dev = + platform::RegisteredDevice::new(c"evdi", platform::DEVID_AUTO, None, 0)?; + devices.push( + CardEntry { + _dev: dev, + usb: core::ptr::null_mut(), + }, + GFP_KERNEL, + )?; + } + Ok(()) + } + } else if name == c"remove_all" { + self.devices.lock().clear(); + Ok(()) + } else { + Err(EINVAL) + } + } +} + +/// Format `v` as decimal followed by a newline into `buf`, returning the byte count written. +fn write_uint(buf: &mut [u8], mut v: usize) -> Result<usize> { + let mut tmp = [0u8; 24]; + let mut i = tmp.len(); + loop { + i -= 1; + tmp[i] = b'0' + (v % 10) as u8; + v /= 10; + if v == 0 { + break; + } + } + let digits = &tmp[i..]; + let total = digits.len() + 1; + if total > buf.len() { + return Err(EINVAL); + } + buf[..digits.len()].copy_from_slice(digits); + buf[digits.len()] = b'\n'; + Ok(total) +} + +/// The module state. +/// +/// Field/drop order matters: `_sysfs` (which owns the device registry, and thus every card) is +/// declared first so it is dropped -- unregistering every card -- *before* the driver registration. +#[pin_data] +struct EvdiModule { + _sysfs: Option<KBox<kernel::sysfs::AttributeGroup<EvdiRegistry>>>, + #[pin] + _driver: kernel::driver::Registration<platform::Adapter<EvdiPlatformDriver>>, +} + +impl kernel::InPlaceModule for EvdiModule { + fn init(module: &'static ThisModule) -> impl PinInit<Self, Error> { + pr_info!("evdi: Rust EVDI loading\n"); + try_pin_init!(Self { + _sysfs: kernel::sysfs::AttributeGroup::register_root(c"evdi", module, EvdiRegistry::new()) + .inspect_err(|e| pr_err!("evdi: sysfs root registration failed ({e:?})\n")) + .ok(), + _driver <- kernel::driver::Registration::new(c"evdi", module), + }) + } +} + +module! { + type: EvdiModule, + name: "evdi", + authors: ["Mike Lothian"], + description: "Extensible Virtual Display Interface (Rust)", + license: "GPL", +} diff --git a/drivers/gpu/drm/evdi/ioctl.rs b/drivers/gpu/drm/evdi/ioctl.rs new file mode 100644 index 000000000000..46cecf2279ec --- /dev/null +++ b/drivers/gpu/drm/evdi/ioctl.rs @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// EVDI driver-private ioctls (libevdi / DisplayLinkManager entry points), declared via +// `kernel::declare_drm_ioctls_ext!` in `kms.rs`. Each handler runs inside a +// `drm_dev_enter/exit` critical section (returns `ENODEV` if the card was unplugged). + +use kernel::{ + device::Bound, + drm, + error::code::EAGAIN, + platform, + prelude::*, + uaccess::{UserPtr, UserSlice}, +}; + +use crate::kms::{EvdiDrmData, EvdiDrmDriver, EvdiDrmFile}; +use crate::uapi; + +/// Maximum EDID blob we accept from userspace (128-byte base block + up to 255 extensions). +const EDID_MAX: usize = 32 * 1024; + +type Dev = drm::Device<EvdiDrmDriver>; +type Parent = platform::Device<Bound>; +type File = drm::File<EvdiDrmFile>; + +/// `DRM_IOCTL_EVDI_CONNECT`: the DLM client connects (with an EDID) or disconnects the +/// virtual display. Registers/clears the event receiver for this device. +pub(crate) fn connect( + dev: &Dev, + _parent: &Parent, + _reg: &(), + arg: &mut uapi::DrmEvdiConnect, + file: &File, +) -> Result<u32> { + let data: &EvdiDrmData = dev; + if arg.connected != 0 { + // Copy the EDID the client supplied so the connector's mode list reflects the real + // monitor. A zero-length/NULL EDID is allowed (connector falls back to a default mode). + let len = arg.edid_length as usize; + let mut edid = KVec::new(); + if len > 0 && arg.edid != 0 { + if len > EDID_MAX { + return Err(EINVAL); + } + UserSlice::new(UserPtr::from_ptr(arg.edid as *mut kernel::ffi::c_void), len) + .read_all(&mut edid, GFP_KERNEL)?; + } + + // Register this file as the event receiver, then mark connected and publish the EDID + // (which fires a hotplug so the compositor re-probes the connector's modes). + data.events.connect(file); + file.inner() + .connected + .store(true, core::sync::atomic::Ordering::Release); + data.painter.lock().connected = true; + if !edid.is_empty() { + data.set_edid(dev, edid); + } + } else { + data.events.disconnect(); + file.inner() + .connected + .store(false, core::sync::atomic::Ordering::Release); + data.painter.lock().connected = false; + // Drop the EDID so the connector reports disconnected until the next CONNECT. + data.clear_edid(dev); + } + Ok(0) +} + +/// `DRM_IOCTL_EVDI_REQUEST_UPDATE`: the client asks to be told (via UPDATE_READY) when the +/// next frame is ready to grab. +pub(crate) fn request_update( + dev: &Dev, + _parent: &Parent, + _reg: &(), + _arg: &mut uapi::DrmEvdiRequestUpdate, + _file: &File, +) -> Result<u32> { + // If an ungrabbed frame is already waiting, return 1 so the client grabs immediately (libevdi's + // `grabImmediately`); otherwise return 0 and let the next flip's UPDATE_READY wake it. We do NOT + // send an event from here — that self-triggering request->event->grab->request cycle was the + // busy-loop. Every real flip still signals via `atomic_update`, so content updates propagate. + let data: &EvdiDrmData = dev; + if data.painter.lock().frame_dirty { + Ok(1) + } else { + Ok(0) + } +} + +/// `DRM_IOCTL_EVDI_GRABPIX`: copy the current scanout framebuffer to the client's buffer. +/// +/// The pixels are read through the safe [`FramebufferVmap::as_bytes`] slice and written to +/// userspace one row at a time (bounded by both the framebuffer and the client's buffer geometry), +/// so no oversized or per-pixel `copy_to_user` is performed. +pub(crate) fn grabpix( + dev: &Dev, + _parent: &Parent, + _reg: &(), + arg: &mut uapi::DrmEvdiGrabpix, + _file: &File, +) -> Result<u32> { + // XRGB8888: 4 bytes per pixel (the only format the primary plane advertises). + const BPP: usize = 4; + + let data: &EvdiDrmData = dev; + // No frame has been flipped in yet: tell the client to try again. + let Some(fb) = data.scanout_fb() else { + return Err(EAGAIN); + }; + + // Take the regions accumulated since the last grab and mark the frame consumed. + let mut dmg = { + let mut p = data.painter.lock(); + p.frame_dirty = false; + let d = p.damage; + p.damage.clear(); + d + }; + + let map = fb.vmap()?; + let src = map.as_bytes(); + let src_pitch = fb.pitch(0) as usize; + let fb_w = fb.width() as i32; + let fb_h = fb.height() as i32; + + let dst_stride = arg.buf_byte_stride as usize; + if arg.buffer == 0 || dst_stride == 0 { + return Err(EINVAL); + } + let max_w = core::cmp::min(fb_w, (dst_stride / BPP) as i32); + let max_h = core::cmp::min(fb_h, arg.buf_height as i32); + + // Nothing recorded (the client polled without a flip): fall back to one full-frame rectangle. + if dmg.count == 0 { + dmg.rects[0] = (0, 0, fb_w, fb_h); + dmg.count = 1; + } + + // Clamp each rectangle to the framebuffer and the client's buffer, dropping empty ones. + let mut rects = [(0i32, 0i32, 0i32, 0i32); crate::painter::MAX_DAMAGE_RECTS]; + let mut count = 0usize; + for &(rx1, ry1, rx2, ry2) in &dmg.rects[..dmg.count] { + let x1 = rx1.clamp(0, max_w); + let y1 = ry1.clamp(0, max_h); + let x2 = rx2.clamp(x1, max_w); + let y2 = ry2.clamp(y1, max_h); + if x2 > x1 && y2 > y1 { + rects[count] = (x1, y1, x2, y2); + count += 1; + } + } + if count == 0 { + return Ok(0); + } + // The client's `rects` buffer holds `num_rects` entries; if we somehow have more, coalesce into + // a single bounding box so we never overrun it. + let cap = (arg.num_rects.max(1) as usize).min(crate::painter::MAX_DAMAGE_RECTS); + if count > cap { + let mut bb = rects[0]; + for &(x1, y1, x2, y2) in &rects[1..count] { + bb = (bb.0.min(x1), bb.1.min(y1), bb.2.max(x2), bb.3.max(y2)); + } + rects[0] = bb; + count = 1; + } + + // Report the rectangles so DLM transmits exactly them (`struct drm_clip_rect` = 4x u16). Leaving + // num_rects/rects unset makes DLM decide nothing changed and send nothing (black). + if arg.rects != 0 { + let mut buf = [0u8; crate::painter::MAX_DAMAGE_RECTS * 8]; + for (i, &(x1, y1, x2, y2)) in rects[..count].iter().enumerate() { + let o = i * 8; + buf[o..o + 2].copy_from_slice(&(x1 as u16).to_ne_bytes()); + buf[o + 2..o + 4].copy_from_slice(&(y1 as u16).to_ne_bytes()); + buf[o + 4..o + 6].copy_from_slice(&(x2 as u16).to_ne_bytes()); + buf[o + 6..o + 8].copy_from_slice(&(y2 as u16).to_ne_bytes()); + } + UserSlice::new( + UserPtr::from_ptr(arg.rects as *mut kernel::ffi::c_void), + count * 8, + ) + .writer() + .write_slice(&buf[..count * 8])?; + } + // The IOWR arg is copied back to userspace, so the count reaches DLM. + arg.num_rects = count as i32; + + // Copy each changed rectangle into the client's (persistent, full-frame) buffer at the same + // position, so DLM's frame accumulates the per-grab deltas. + for &(x1, y1, x2, y2) in &rects[..count] { + let xoff = x1 as usize * BPP; + let span = (x2 - x1) as usize * BPP; + for y in y1 as usize..y2 as usize { + let so = y * src_pitch + xoff; + if so + span > src.len() { + break; + } + let dst = arg + .buffer + .checked_add(y * dst_stride + xoff) + .ok_or(EINVAL)?; + UserSlice::new(UserPtr::from_ptr(dst as *mut kernel::ffi::c_void), span) + .writer() + .write_slice(&src[so..so + span])?; + } + } + Ok(0) +} + +/// `DRM_IOCTL_EVDI_DDCCI_RESPONSE`: the client returns a DDC/CI reply to be handed back to +/// the in-kernel I2C adapter. +pub(crate) fn ddcci_response( + dev: &Dev, + _parent: &Parent, + _reg: &(), + arg: &mut uapi::DrmEvdiDdcciResponse, + _file: &File, +) -> Result<u32> { + // Copy the client's DDC/CI reply (capped at the 64-byte DDC/CI payload) and hand it to the + // waiting I2C transfer. + let len = core::cmp::min(arg.buffer_length as usize, uapi::DDCCI_BUFFER_SIZE); + let mut resp = KVec::new(); + if len > 0 && arg.buffer != 0 { + UserSlice::new( + UserPtr::from_ptr(arg.buffer as *mut kernel::ffi::c_void), + len, + ) + .read_all(&mut resp, GFP_KERNEL)?; + } + let data: &EvdiDrmData = dev; + data.ddcci_respond(resp); + Ok(0) +} + +/// `DRM_IOCTL_EVDI_ENABLE_CURSOR_EVENTS`: toggle delivery of CURSOR_SET/CURSOR_MOVE events. +pub(crate) fn enable_cursor_events( + dev: &Dev, + _parent: &Parent, + _reg: &(), + arg: &mut uapi::DrmEvdiEnableCursorEvents, + _file: &File, +) -> Result<u32> { + let data: &EvdiDrmData = dev; + data.painter.lock().cursor_events_enabled = arg.enable != 0; + Ok(0) +} diff --git a/drivers/gpu/drm/evdi/kms.rs b/drivers/gpu/drm/evdi/kms.rs new file mode 100644 index 000000000000..1d39163ddf3b --- /dev/null +++ b/drivers/gpu/drm/evdi/kms.rs @@ -0,0 +1,729 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// The EVDI DRM/KMS device: registers a `struct drm_device` presenting one virtual +// display head (CRTC + primary plane + virtual encoder + virtual connector) with +// GEM-shmem dumb buffers, built on the safe KMS mode-object layer (`kernel::drm::kms`). +// +// Unlike a real display driver, EVDI's scanout is *pulled* by userspace: the +// DisplayLinkManager daemon grabs framebuffer pixels via the GRABPIX ioctl and is +// told when to do so through `drm_event`s (see `painter.rs`). The KMS callbacks here +// therefore translate atomic commits into those events rather than programming any +// hardware. + +use core::sync::atomic::{AtomicBool, AtomicI64, AtomicPtr, Ordering}; +use kernel::{ + bindings, + drm, + drm::event::EventChannel, + drm::kms::{ + connector::{self, ConnectorGuard}, + crtc::{self, AsRawCrtc as _, CrtcAtomicCommit, RawCrtc as _, RawCrtcState as _}, + encoder, + framebuffer::Framebuffer, + plane::{self, PlaneAtomicCommit, RawPlaneState as _}, + vblank::{RawVblankCrtcState as _, VblankGuard, VblankSupport, VblankTimestamp}, + KmsDriver, ModeConfigGuard, ModeConfigInfo, ModeObject as _, NewKmsDevice, Probing, + }, + impl_has_hr_timer, + interrupt::LocalInterruptDisabled, + prelude::*, + sync::{aref::ARef, new_condvar, new_mutex, new_spinlock, Arc, ArcBorrow, CondVar, + CondVarTimeoutResult, Mutex, SpinLock}, + time::{ + hrtimer::{ + ArcHrTimerHandle, HrTimer, HrTimerCallback, HrTimerCallbackContext, HrTimerPointer, + HrTimerRestart, RelativeMode, + }, + Delta, Monotonic, + }, + types::ForLt, +}; + +use crate::painter::PainterState; +use crate::uapi; +use kernel::error::code::{ERESTARTSYS, ETIMEDOUT}; + +/// DDC/CI slave address on the virtual I2C bus (as used by monitor-control tools). +pub(crate) const DDCCI_ADDRESS: u16 = 0x37; +/// How long a DDC/CI transfer waits for the userspace client's reply. +const DDCCI_TIMEOUT_MS: u32 = 50; +/// Maximum DDC/CI payload carried in one event. +const DDCCI_BUFFER_SIZE: usize = 64; + +/// `DRM_FORMAT_XRGB8888` (`fourcc_code('X','R','2','4')`) — EVDI scans out 32bpp. +pub(crate) const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258; +static PRIMARY_FORMATS: [u32; 1] = [DRM_FORMAT_XRGB8888]; + +/// `DRM_FORMAT_ARGB8888` (`fourcc_code('A','R','2','4')`) — cursor bitmaps carry alpha. +pub(crate) const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241; +static CURSOR_FORMATS: [u32; 1] = [DRM_FORMAT_ARGB8888]; + +/// Fallback mode advertised before the DLM client delivers an EDID via CONNECT. +const FALLBACK_W: u32 = 1024; +const FALLBACK_H: u32 = 768; + +// libevdi (`is_evdi_compatible`) requires `major == 1 && minor >= 9`; match the C evdi's version so +// DisplayLinkManager accepts the card. Bumped past the 1.0.0 that made libevdi reject every device. +const INFO: drm::DriverInfo = drm::DriverInfo { + major: 1, + minor: 14, + patchlevel: 16, + name: c"evdi", + desc: c"Extensible Virtual Display Interface", +}; + +/// The EVDI DRM driver marker type. +pub(crate) struct EvdiDrmDriver; + +/// Convenience alias for our concrete `drm::Device`. +pub(crate) type EvdiDrmDevice = drm::Device<EvdiDrmDriver>; + +/// DRM device-private data: the painter (connection + event delivery + pixel grab +/// bookkeeping) and a stashed pointer to our single connector so CONNECT can push a +/// fresh EDID into its mode list. +#[pin_data] +pub(crate) struct EvdiDrmData { + /// Event channel to the connected DLM client (`drm_event` delivery). + #[pin] + pub(crate) events: EventChannel, + /// Painter state (connection status, cached EDID, cursor-events flag, dirty rects). + #[pin] + pub(crate) painter: Mutex<PainterState>, + /// The framebuffer currently flipped in on the primary plane, refcounted so GRABPIX can map + /// and copy it after the atomic commit that set it has returned. `None` until the first flip. + #[pin] + pub(crate) scanout: Mutex<Option<ARef<Framebuffer<EvdiDrmDriver>>>>, + /// Slot for a DDC/CI reply from the client, guarding the request/response handshake between + /// the I2C `master_xfer` (which waits) and the DDCCI_RESPONSE ioctl (which fills + notifies). + #[pin] + pub(crate) ddcci_resp: Mutex<Option<KVec<u8>>>, + #[pin] + pub(crate) ddcci_cv: CondVar, + /// Pointer to our connector's driver data, stashed during `KmsDriver::probe` so + /// CONNECT can reach its cached-EDID slot without walking DRM's mode-object list. + /// Written once during single-threaded probe; read-only thereafter. + pub(crate) connector: core::sync::atomic::AtomicPtr<EvdiConnector>, +} + +impl EvdiDrmData { + pub(crate) fn new() -> impl PinInit<Self, Error> { + try_pin_init!(Self { + events <- EventChannel::new(kernel::static_lock_class!()), + painter <- new_mutex!(PainterState::new()), + scanout <- new_mutex!(None), + ddcci_resp <- new_mutex!(None), + ddcci_cv <- new_condvar!(), + connector: core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()), + }) + } + + /// Send one DDC/CI message to the connected client and wait for its reply. `buf` carries the + /// write payload (or, for `is_read`, is filled with the reply, up to its length). Returns + /// `ETIMEDOUT` if the client does not respond in time. + pub(crate) fn ddcci_transfer( + &self, + dev: &EvdiDrmDevice, + addr: u16, + flags: u16, + is_read: bool, + buf: &mut [u8], + ) -> Result { + let mut ev = uapi::DrmEvdiEventDdcciData { + base: uapi::DrmEvent { + type_: 0, + length: 0, + }, + buffer: [0u8; DDCCI_BUFFER_SIZE], + buffer_length: buf.len() as u32, + flags, + address: addr, + }; + let n = core::cmp::min(buf.len(), DDCCI_BUFFER_SIZE); + ev.buffer[..n].copy_from_slice(&buf[..n]); + + // Hold the slot lock across send+wait so the reply cannot be signalled before we wait + // (no lost wakeup): `ddcci_respond` can only run once `wait_*` has released the lock. + let mut slot = self.ddcci_resp.lock(); + *slot = None; + self.events.send(dev, ev)?; + let jiffies = kernel::time::msecs_to_jiffies(DDCCI_TIMEOUT_MS); + match self.ddcci_cv.wait_interruptible_timeout(&mut slot, jiffies) { + CondVarTimeoutResult::Woken { .. } => { + if is_read { + if let Some(resp) = slot.take() { + let m = core::cmp::min(resp.len(), buf.len()); + buf[..m].copy_from_slice(&resp[..m]); + } + } + Ok(()) + } + CondVarTimeoutResult::Timeout => Err(ETIMEDOUT), + CondVarTimeoutResult::Signal { .. } => Err(ERESTARTSYS), + } + } + + /// Store a DDC/CI reply from the client (DDCCI_RESPONSE ioctl) and wake the waiting transfer. + pub(crate) fn ddcci_respond(&self, resp: KVec<u8>) { + *self.ddcci_resp.lock() = Some(resp); + self.ddcci_cv.notify_one(); + } + + /// Record the framebuffer currently on the primary plane (bumping its refcount) so a later + /// GRABPIX can map it. Called from the plane's atomic commit. + pub(crate) fn set_scanout(&self, fb: Option<&Framebuffer<EvdiDrmDriver>>) { + *self.scanout.lock() = fb.map(ARef::from); + } + + /// Take a refcounted handle to the current scanout framebuffer, if any, for GRABPIX to map. + pub(crate) fn scanout_fb(&self) -> Option<ARef<Framebuffer<EvdiDrmDriver>>> { + self.scanout.lock().clone() + } + + /// Install a new EDID blob (from CONNECT) into the connector and fire a hotplug so + /// the compositor re-probes the connector's mode list. + pub(crate) fn set_edid(&self, dev: &EvdiDrmDevice, blob: KVec<u8>) { + let ptr = self.connector.load(core::sync::atomic::Ordering::Acquire); + // SAFETY: `connector` was stashed during probe and points to a `EvdiConnector` + // that lives as long as the device; null until probe completes. + let Some(connector) = (unsafe { ptr.as_ref() }) else { + return; + }; + *connector.cached_edid.lock() = Some(blob); + dev.hotplug_event(); + } + + /// Drop the connector's cached EDID (on CONNECT disconnect) and fire a hotplug so the connector + /// reports disconnected again -- see [`EvdiConnector::detect`]. + pub(crate) fn clear_edid(&self, dev: &EvdiDrmDevice) { + let ptr = self.connector.load(core::sync::atomic::Ordering::Acquire); + // SAFETY: as in `set_edid` -- `connector` was stashed during probe and outlives the device. + let Some(connector) = (unsafe { ptr.as_ref() }) else { + return; + }; + *connector.cached_edid.lock() = None; + dev.hotplug_event(); + } +} + +/// GEM object inner data. Empty: the shmem-backed object wires +/// `drm_gem_shmem_dumb_create`, so `DRM_IOCTL_MODE_CREATE_DUMB` works and the GRABPIX +/// ioctl can `vmap` the resulting framebuffer to copy pixels to userspace. +#[pin_data] +pub(crate) struct EvdiObject {} + +impl drm::gem::DriverObject for EvdiObject { + type Driver = EvdiDrmDriver; + type Args = (); + + fn new<Ctx: drm::DeviceContext>( + _dev: &drm::Device<EvdiDrmDriver, Ctx>, + _size: usize, + _args: (), + ) -> impl PinInit<Self, Error> { + try_pin_init!(EvdiObject {}) + } +} + +/// Per-open DRM client state. Empty of data, but its lifetime pins the module for the +/// duration of an open DRM file (the DLM daemon holds `/dev/dri/cardN` open across the +/// whole session; the Rust DRM `file_operations` are built with `owner = NULL`, so +/// without this an `rmmod` while DLM is running would free the fops under the open fd). +#[pin_data(PinnedDrop)] +pub(crate) struct EvdiDrmFile { + /// The owning device, so file close can reach the event channel to disconnect. + dev: ARef<EvdiDrmDevice>, + /// Whether this file issued CONNECT and is thus the event receiver. Set by the CONNECT ioctl; + /// on close a connected file clears the channel so no event is ever sent to a closed file. + pub(crate) connected: core::sync::atomic::AtomicBool, +} + +impl drm::file::DriverFile for EvdiDrmFile { + type Driver = EvdiDrmDriver; + + fn open(dev: &drm::Device<Self::Driver>) -> Result<Pin<KBox<Self>>> { + let file = KBox::try_pin_init( + try_pin_init!(Self { + dev: dev.into(), + connected: core::sync::atomic::AtomicBool::new(false), + }), + GFP_KERNEL, + )?; + // SAFETY: executing inside this module's own DRM `open` callback, so the module + // is live; the reference is released 1:1 in `PinnedDrop`. + unsafe { kernel::bindings::__module_get(crate::THIS_MODULE.as_ptr()) }; + Ok(file) + } +} + +#[pinned_drop] +impl PinnedDrop for EvdiDrmFile { + fn drop(self: Pin<&mut Self>) { + // If this file was the connected event receiver, clear the channel so nothing is ever + // delivered to it after close (the receiver pointer must not outlive the file). + if self.connected.load(core::sync::atomic::Ordering::Acquire) { + self.dev.events.disconnect(); + } + // Release the module reference taken in `open`. + // SAFETY: balances the `__module_get` in `open`. + unsafe { kernel::bindings::module_put(crate::THIS_MODULE.as_ptr()) }; + } +} + +#[vtable] +impl drm::Driver for EvdiDrmDriver { + type Data = EvdiDrmData; + type File = EvdiDrmFile; + type Object<Ctx: drm::DeviceContext> = drm::gem::shmem::Object<EvdiObject, Ctx>; + type ParentDevice<Ctx: kernel::device::DeviceContext> = kernel::platform::Device<Ctx>; + type RegistrationData = ForLt!(()); + type Kms = Self; + + const INFO: drm::DriverInfo = INFO; + + kernel::declare_drm_ioctls_ext! { + (EVDI_CONNECT, crate::uapi::DrmEvdiConnect, + crate::uapi::DRM_IOCTL_EVDI_CONNECT, 0, crate::ioctl::connect), + (EVDI_REQUEST_UPDATE, crate::uapi::DrmEvdiRequestUpdate, + crate::uapi::DRM_IOCTL_EVDI_REQUEST_UPDATE, 0, crate::ioctl::request_update), + (EVDI_GRABPIX, crate::uapi::DrmEvdiGrabpix, + crate::uapi::DRM_IOCTL_EVDI_GRABPIX, 0, crate::ioctl::grabpix), + (EVDI_DDCCI_RESPONSE, crate::uapi::DrmEvdiDdcciResponse, + crate::uapi::DRM_IOCTL_EVDI_DDCCI_RESPONSE, 0, crate::ioctl::ddcci_response), + (EVDI_ENABLE_CURSOR_EVENTS, crate::uapi::DrmEvdiEnableCursorEvents, + crate::uapi::DRM_IOCTL_EVDI_ENABLE_CURSOR_EVENTS, 0, crate::ioctl::enable_cursor_events), + } +} + +#[vtable] +impl KmsDriver for EvdiDrmDriver { + type Connector = EvdiConnector; + type Plane = EvdiPlane; + type Crtc = EvdiCrtc; + type Encoder = EvdiEncoder; + + fn mode_config_info(_dev: &drm::Device<Self, drm::Uninit>) -> Result<ModeConfigInfo> { + Ok(ModeConfigInfo { + min_resolution: (0, 0), + max_resolution: (8192, 8192), + max_cursor: (64, 64), + preferred_depth: 32, + preferred_fourcc: Some(DRM_FORMAT_XRGB8888), + }) + } + + fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result { + let primary = plane::UnregisteredPlane::<EvdiPlane>::new( + dev, + 1, + &PRIMARY_FORMATS, + None, + plane::Type::Primary, + None, + false, + )?; + // Advertise FB_DAMAGE_CLIPS so the compositor reports which region it changed each frame. + // Without it `damage_merged` always returns the whole plane, so GRABPIX reports a full-frame + // rect and DLM re-transmits the entire 2560x1440 buffer every update (jerky). + // SAFETY: `primary` is a valid, not-yet-registered plane; the property is added before + // registration (in the `Probing` phase). + unsafe { + kernel::bindings::drm_plane_enable_fb_damage_clips(plane::AsRawPlane::as_raw(primary)) + }; + // Hardware cursor plane: cursor movement is then forwarded to the DLM client as CURSOR_MOVE + // events (the daemon composites the cursor itself) instead of forcing a primary-plane + // repaint + full grab. `max_cursor` (mode_config_info) bounds the size the compositor will + // put here; larger cursors (e.g. KDE's shake-to-locate) fall back to a software cursor + // composited into the framebuffer, which the primary path grabs normally. + let cursor = plane::UnregisteredPlane::<EvdiPlane>::new( + dev, + 1, + &CURSOR_FORMATS, + None, + plane::Type::Cursor, + None, + true, + )?; + let crtc_obj = + crtc::UnregisteredCrtc::<EvdiCrtc>::new(dev, primary, Some(&cursor), None, ())?; + let enc = encoder::UnregisteredEncoder::<EvdiEncoder>::new( + dev, + encoder::Type::Virtual, + crtc_obj.mask(), + 0, + None, + (), + )?; + // Use DVI-I (matching the C evdi) rather than Virtual: `__drm_connector_init` skips + // `drm_connector_attach_edid_property()` for VIRTUAL/WRITEBACK connectors, and without that + // property `drm_edid_connector_update()` can't populate `edid_blob_ptr`, so + // `drm_edid_connector_add_modes()` would return 0 modes for a perfectly valid EDID. + let conn = + connector::UnregisteredConnector::<EvdiConnector>::new(dev, connector::Type::DviI, ())?; + conn.attach_encoder(&*enc)?; + let data: &EvdiDrmData = dev; + data.connector.store( + &**conn as *const EvdiConnector as *mut EvdiConnector, + core::sync::atomic::Ordering::Release, + ); + Ok(()) + } +} + +// ---- CRTC ------------------------------------------------------------------- + +/// A software vblank source: an hrtimer that fires once per frame and drives +/// `drm_crtc_handle_vblank()`, so the atomic helpers pace page-flips against a real vblank +/// (via `drm_crtc_arm_vblank_event()` in [`EvdiCrtc::atomic_flush`]) instead of completing them +/// immediately with a fake vblank -- which is what makes updates smooth rather than bursty. +/// +/// The timer free-runs once started (re-arming every tick); `enabled` gates whether it actually +/// delivers vblanks, so DPMS off/on is a cheap flag flip rather than a start/stop. It is cancelled +/// when the owning [`EvdiCrtc`] (and thus the [`ArcHrTimerHandle`]) is dropped at teardown, before +/// the `drm_crtc` it points at is freed. +#[pin_data] +pub(crate) struct VblankTimer { + #[pin] + timer: HrTimer<Self>, + /// The `drm_crtc` to deliver vblanks to (set when vblank is first enabled). + crtc: AtomicPtr<bindings::drm_crtc>, + /// One scanout frame in nanoseconds (from the mode's `framedur_ns`). + interval_ns: AtomicI64, + /// Whether vblanks should currently be delivered (toggled by enable/disable_vblank). + enabled: AtomicBool, + /// Whether the free-running timer has been started yet. + started: AtomicBool, +} + +impl VblankTimer { + fn new() -> impl PinInit<Self> { + pin_init!(VblankTimer { + timer <- HrTimer::new(), + crtc: AtomicPtr::new(core::ptr::null_mut()), + interval_ns: AtomicI64::new(16_666_666), // ~60 Hz until a mode sets it + enabled: AtomicBool::new(false), + started: AtomicBool::new(false), + }) + } +} + +impl HrTimerCallback for VblankTimer { + type Pointer<'a> = Arc<Self>; + + fn run(this: ArcBorrow<'_, Self>, mut ctx: HrTimerCallbackContext<'_, Self>) -> HrTimerRestart { + let crtc = this.crtc.load(Ordering::Relaxed); + if !crtc.is_null() && this.enabled.load(Ordering::Relaxed) { + // SAFETY: `crtc` is the `drm_crtc` stored in `enable_vblank` while the device is live; + // the timer is cancelled (its handle dropped) before the crtc is freed at teardown. + unsafe { bindings::drm_crtc_handle_vblank(crtc) }; + } + let interval = this.interval_ns.load(Ordering::Relaxed).max(1_000_000); + ctx.forward_now(Delta::from_nanos(interval)); + HrTimerRestart::Restart + } +} + +impl_has_hr_timer! { + impl HasHrTimer<Self> for VblankTimer { + mode: RelativeMode<Monotonic>, field: self.timer + } +} + +#[pin_data] +pub(crate) struct EvdiCrtc { + /// The software vblank source for this CRTC. + vblank: Arc<VblankTimer>, + /// Keeps the timer running; dropping it (at CRTC teardown) cancels the timer. + #[pin] + vblank_handle: SpinLock<Option<ArcHrTimerHandle<VblankTimer>>>, +} + +#[derive(Clone, Default)] +pub(crate) struct EvdiCrtcState; + +impl crtc::DriverCrtcState for EvdiCrtcState { + type Crtc = EvdiCrtc; +} + +#[vtable] +impl crtc::DriverCrtc for EvdiCrtc { + type Args = (); + type Driver = EvdiDrmDriver; + type State = EvdiCrtcState; + type VblankImpl = Self; + + fn new( + _device: &drm::Device<Self::Driver, drm::Uninit>, + _args: &(), + ) -> impl PinInit<Self, Error> { + try_pin_init!(EvdiCrtc { + vblank: Arc::pin_init(VblankTimer::new(), GFP_KERNEL)?, + vblank_handle <- new_spinlock!(None), + }) + } + + /// Display turning on: enable vblank delivery, then tell the DLM client DPMS-on + the mode. + fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) { + let crtc = commit.crtc(); + crtc.vblank_on(); + let dev = crtc.drm_dev(); + let data: &EvdiDrmData = dev; + let new = commit.take_new_state(); + let mode = new.mode(); + crate::painter::notify_dpms(data, dev, crate::painter::DPMS_ON); + crate::painter::notify_mode_changed( + data, + dev, + mode.hdisplay() as i32, + mode.vdisplay() as i32, + mode.vrefresh(), + 32, + DRM_FORMAT_XRGB8888, + ); + } + + /// Display turning off: stop vblank delivery and tell the DLM client DPMS-off. + fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) { + let crtc = commit.crtc(); + crtc.vblank_off(); + let dev = crtc.drm_dev(); + let data: &EvdiDrmData = dev; + crate::painter::notify_dpms(data, dev, crate::painter::DPMS_OFF); + } + + /// Arm the page-flip completion event to be sent by the next vblank tick, so userspace is paced + /// to the refresh rate rather than signalled immediately. + fn atomic_flush(commit: CrtcAtomicCommit<'_, Self>) { + let crtc = commit.crtc(); + let mut new = commit.take_new_state(); + if let Some(pending) = new.get_pending_vblank_event() { + match crtc.vblank_get() { + Ok(vbl_ref) => pending.arm(vbl_ref), + // Vblank couldn't be enabled (e.g. mid-teardown): fall back to sending now. + Err(_) => pending.send(), + } + } + } +} + +impl VblankSupport for EvdiCrtc { + type Crtc = EvdiCrtc; + + fn enable_vblank( + crtc: &crtc::Crtc<Self::Crtc>, + vblank_guard: &VblankGuard<'_, Self::Crtc>, + _irq: &LocalInterruptDisabled, + ) -> Result { + let data: &EvdiCrtc = crtc; + // Track the mode's real frame duration so the tick matches the negotiated refresh rate. + let fd = vblank_guard.frame_duration(); + if fd > 0 { + data.vblank.interval_ns.store(fd as i64, Ordering::Relaxed); + } + data.vblank.crtc.store(crtc.as_raw(), Ordering::Relaxed); + data.vblank.enabled.store(true, Ordering::Relaxed); + // Start the free-running timer the first time vblank is enabled. + if !data.vblank.started.swap(true, Ordering::Relaxed) { + let interval = data.vblank.interval_ns.load(Ordering::Relaxed); + let handle = data.vblank.clone().start(Delta::from_nanos(interval)); + *data.vblank_handle.lock() = Some(handle); + } + Ok(()) + } + + fn disable_vblank( + crtc: &crtc::Crtc<Self::Crtc>, + _vblank_guard: &VblankGuard<'_, Self::Crtc>, + _irq: &LocalInterruptDisabled, + ) { + let data: &EvdiCrtc = crtc; + data.vblank.enabled.store(false, Ordering::Relaxed); + } + + fn get_vblank_timestamp( + _crtc: &crtc::Crtc<Self::Crtc>, + _in_vblank_irq: bool, + ) -> Option<VblankTimestamp> { + // Let DRM estimate the timestamp from the mode timings. + None + } +} + +// ---- Planes (primary + cursor) ---------------------------------------------- +// +// The safe KMS layer allows a single `DriverPlane` type per driver, so one `EvdiPlane` type serves +// both the primary and cursor planes, told apart by `is_cursor` (set from the plane's `Args`). + +#[pin_data] +pub(crate) struct EvdiPlane { + /// Whether this is the cursor plane (vs. the primary scanout plane). + is_cursor: bool, +} + +#[derive(Clone, Default)] +pub(crate) struct EvdiPlaneState; + +impl plane::DriverPlaneState for EvdiPlaneState { + type Plane = EvdiPlane; +} + +#[vtable] +impl plane::DriverPlane for EvdiPlane { + type Args = bool; + type Driver = EvdiDrmDriver; + type State = EvdiPlaneState; + + fn new( + _device: &drm::Device<Self::Driver, drm::Uninit>, + is_cursor: bool, + ) -> impl PinInit<Self, Error> { + try_pin_init!(EvdiPlane { is_cursor }) + } + + /// A new framebuffer was flipped in. + /// + /// For the primary plane, EVDI records the new scanout buffer and signals the DLM client to + /// grab it (UPDATE_READY). For the cursor plane it forwards the cursor position (CURSOR_MOVE) + /// and, when the bitmap changes, its geometry + a GEM handle the client can map (CURSOR_SET) -- + /// the DisplayLink daemon composites the cursor itself. + fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) { + let plane = commit.plane(); + let dev = plane.drm_dev(); + let data: &EvdiDrmData = dev; + + if !plane.is_cursor { + // Primary: record the new scanout buffer plus each region the compositor changed + // (`for_each_damage_clip`, relative to the old state), accumulate them for GRABPIX, and + // signal the client. UPDATE_READY fires on every real flip so updates propagate; the + // busy-loop is avoided by *not* re-signalling from REQUEST_UPDATE. Reporting the exact + // changed rectangles lets DLM transmit small deltas instead of the whole 2560x1440 frame. + let (old, new) = commit.take_old_new_state(); + let fb = new.framebuffer::<EvdiDrmDriver>(); + data.set_scanout(fb); + if fb.is_some() { + { + let mut p = data.painter.lock(); + new.for_each_damage_clip(old, |r| p.damage.push((r.x1, r.y1, r.x2, r.y2))); + p.frame_dirty = true; + } + crate::painter::notify_update_ready(data, dev); + } + return; + } + + // Cursor: only forward events if the client asked for them. + let new = commit.take_new_state(); + if !data.painter.lock().cursor_events_enabled { + return; + } + crate::painter::notify_cursor_move(data, dev, new.crtc_x(), new.crtc_y()); + match new.framebuffer::<EvdiDrmDriver>() { + Some(fb) => { + // Create a handle to the cursor bitmap in the connected client's file so the daemon + // can map it (may sleep; runs under the event channel's receiver mutex). + let handle = match data + .events + .with_receiver::<EvdiDrmFile, _>(|file| fb.create_handle(file)) + { + Some(Ok(h)) => h, + _ => 0, + }; + crate::painter::notify_cursor_set( + data, + dev, + new.hotspot_x(), + new.hotspot_y(), + fb.width(), + fb.height(), + handle != 0, + handle, + fb.height() * fb.pitch(0), + fb.format(), + fb.pitch(0), + ); + } + // Cursor disabled (no framebuffer): tell the client to hide it. + None => crate::painter::notify_cursor_set(data, dev, 0, 0, 0, 0, false, 0, 0, 0, 0), + } + } +} + +// ---- Encoder ---------------------------------------------------------------- + +#[pin_data] +pub(crate) struct EvdiEncoder; + +#[vtable] +impl encoder::DriverEncoder for EvdiEncoder { + type Driver = EvdiDrmDriver; + type Args = (); + + fn new( + _device: &drm::Device<Self::Driver, drm::Uninit>, + _args: (), + ) -> impl PinInit<Self, Error> { + try_pin_init!(EvdiEncoder {}) + } +} + +// ---- Connector -------------------------------------------------------------- + +#[pin_data] +pub(crate) struct EvdiConnector { + /// EDID delivered by the DLM client through CONNECT (`None` until connected). + #[pin] + pub(crate) cached_edid: Mutex<Option<KVec<u8>>>, +} + +#[derive(Clone, Default)] +pub(crate) struct EvdiConnectorState; + +impl connector::DriverConnectorState for EvdiConnectorState { + type Connector = EvdiConnector; +} + +#[vtable] +impl connector::DriverConnector for EvdiConnector { + type Args = (); + type Driver = EvdiDrmDriver; + type State = EvdiConnectorState; + + fn new( + _device: &drm::Device<Self::Driver, drm::Uninit>, + _args: (), + ) -> impl PinInit<Self, Error> { + try_pin_init!(EvdiConnector { + cached_edid <- new_mutex!(Option::<KVec<u8>>::None), + }) + } + + /// Install the DLM-provided EDID when present, else advertise a fallback mode list so + /// the connector stays usable before CONNECT. + 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; + let n = connector.add_modes_noedid((FALLBACK_W, FALLBACK_H)); + connector.set_preferred_mode((FALLBACK_W, FALLBACK_H)); + n + } + + /// Report the display as connected only once the DLM client has delivered an EDID (via CONNECT). + /// Advertising the fallback mode as "connected" before the real EDID arrives makes the + /// compositor configure the output at the wrong resolution and then reconfigure/disable it when + /// the EDID (and native modes) show up -- which manifested as the dock output freezing on its + /// first frame. This mirrors the C evdi's disconnected-until-EDID hotplug sequencing. + fn detect(connector: &connector::Connector<Self>, _force: bool) -> connector::Status { + if connector.cached_edid.lock().is_some() { + connector::Status::Connected + } else { + connector::Status::Disconnected + } + } +} diff --git a/drivers/gpu/drm/evdi/painter.rs b/drivers/gpu/drm/evdi/painter.rs new file mode 100644 index 000000000000..33208cd51a1c --- /dev/null +++ b/drivers/gpu/drm/evdi/painter.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// The EVDI "painter": the per-device connection + event-delivery bookkeeping that +// bridges the KMS callbacks (and ioctls) to the DisplayLinkManager userspace client. +// +// Events are delivered through the DRM-core `drm_event` mechanism (the safe +// `kernel::drm::event::EventChannel` binding), which serializes delivery against file +// close under `event_lock` — so an event can never be sent to a client that has just +// disconnected. + +use kernel::drm::event::EventPayload; + +use crate::kms::{EvdiDrmData, EvdiDrmDevice}; +use crate::uapi; + +/// DPMS mode codes as understood by the DLM client (matching `DRM_MODE_DPMS_*`). +pub(crate) const DPMS_ON: i32 = 0; +pub(crate) const DPMS_OFF: i32 = 3; + +/// Mutable per-device painter state, guarded by a mutex in [`EvdiDrmData`]. +/// +/// The connected client's EDID is the connector's `cached_edid` (the source of truth for the mode +/// list), so it is not duplicated here. +pub(crate) struct PainterState { + /// Whether a DLM client has issued CONNECT. + pub(crate) connected: bool, + /// Whether the client asked to receive cursor events. + pub(crate) cursor_events_enabled: bool, + /// A frame has been flipped in but not yet grabbed (the C evdi's `num_dirts > 0`). Lets + /// REQUEST_UPDATE answer "grab now" (ioctl returns 1) when fresh pixels are already waiting, + /// instead of self-triggering an UPDATE_READY event (which busy-loops the client). + pub(crate) frame_dirty: bool, + /// The individual regions changed since the last GRABPIX, accumulated across flips. GRABPIX + /// reports and copies exactly these rectangles (so DLM transmits just the deltas rather than the + /// whole 2560x1440 frame), then clears them. + pub(crate) damage: Damage, +} + +/// Maximum number of distinct damage rectangles tracked between grabs (mirrors the C evdi's +/// `MAX_DIRTS`); on overflow they collapse into a single bounding box. +pub(crate) const MAX_DAMAGE_RECTS: usize = 16; + +/// Accumulated frame damage: up to [`MAX_DAMAGE_RECTS`] changed rectangles `(x1, y1, x2, y2)` since +/// the last GRABPIX. `count == 0` means nothing was recorded (GRABPIX falls back to a full frame). +#[derive(Copy, Clone)] +pub(crate) struct Damage { + pub(crate) rects: [(i32, i32, i32, i32); MAX_DAMAGE_RECTS], + pub(crate) count: usize, +} + +impl Damage { + pub(crate) const fn new() -> Self { + Self { + rects: [(0, 0, 0, 0); MAX_DAMAGE_RECTS], + count: 0, + } + } + + /// Record a changed rectangle. When the list is full, collapse everything (including `r`) into a + /// single bounding box so a burst of damage never drops updates -- it just gets coarser. + pub(crate) fn push(&mut self, r: (i32, i32, i32, i32)) { + if self.count < MAX_DAMAGE_RECTS { + self.rects[self.count] = r; + self.count += 1; + return; + } + let mut bb = r; + for i in 0..self.count { + let (x1, y1, x2, y2) = self.rects[i]; + bb = (bb.0.min(x1), bb.1.min(y1), bb.2.max(x2), bb.3.max(y2)); + } + self.rects[0] = bb; + self.count = 1; + } + + pub(crate) fn clear(&mut self) { + self.count = 0; + } +} + +impl PainterState { + pub(crate) fn new() -> Self { + Self { + connected: false, + cursor_events_enabled: false, + frame_dirty: false, + damage: Damage::new(), + } + } +} + +// SAFETY: each of these event structs is `#[repr(C)]` with a `drm_event`-layout header +// (`uapi::DrmEvent`, identical to `bindings::drm_event`) as its first field, is `Copy` +// plain-old-data, and is copied verbatim to userspace. See `uapi.rs`. +unsafe impl EventPayload for uapi::DrmEvdiEventUpdateReady { + const TYPE: u32 = uapi::DRM_EVDI_EVENT_UPDATE_READY; +} +// SAFETY: See above. +unsafe impl EventPayload for uapi::DrmEvdiEventDpms { + const TYPE: u32 = uapi::DRM_EVDI_EVENT_DPMS; +} +// SAFETY: See above. +unsafe impl EventPayload for uapi::DrmEvdiEventModeChanged { + const TYPE: u32 = uapi::DRM_EVDI_EVENT_MODE_CHANGED; +} +// SAFETY: See above. +unsafe impl EventPayload for uapi::DrmEvdiEventCrtcState { + const TYPE: u32 = uapi::DRM_EVDI_EVENT_CRTC_STATE; +} +// SAFETY: See above. +unsafe impl EventPayload for uapi::DrmEvdiEventCursorSet { + const TYPE: u32 = uapi::DRM_EVDI_EVENT_CURSOR_SET; +} +// SAFETY: See above. +unsafe impl EventPayload for uapi::DrmEvdiEventCursorMove { + const TYPE: u32 = uapi::DRM_EVDI_EVENT_CURSOR_MOVE; +} +// SAFETY: See above. +unsafe impl EventPayload for uapi::DrmEvdiEventDdcciData { + const TYPE: u32 = uapi::DRM_EVDI_EVENT_DDCCI_DATA; +} + +/// Zeroed `drm_event` header; [`EventChannel::send`] overwrites `type`/`length`. +const fn hdr() -> uapi::DrmEvent { + uapi::DrmEvent { + type_: 0, + length: 0, + } +} + +/// Tell the DLM client a fresh frame is ready to be grabbed (`UPDATE_READY`). +pub(crate) fn notify_update_ready(data: &EvdiDrmData, dev: &EvdiDrmDevice) { + let ev = uapi::DrmEvdiEventUpdateReady { base: hdr() }; + let _ = data.events.send(dev, ev); +} + +/// Tell the DLM client the display's DPMS power state changed. +pub(crate) fn notify_dpms(data: &EvdiDrmData, dev: &EvdiDrmDevice, mode: i32) { + let ev = uapi::DrmEvdiEventDpms { base: hdr(), mode }; + let _ = data.events.send(dev, ev); +} + +/// Tell the DLM client the cursor moved to `(x, y)`. +pub(crate) fn notify_cursor_move(data: &EvdiDrmData, dev: &EvdiDrmDevice, x: i32, y: i32) { + let ev = uapi::DrmEvdiEventCursorMove { base: hdr(), x, y }; + let _ = data.events.send(dev, ev); +} + +/// Tell the DLM client the cursor bitmap/geometry changed. `buffer_handle` is a GEM handle to the +/// cursor bitmap in the client's file (0 = no bitmap / cursor hidden). +#[allow(clippy::too_many_arguments)] +pub(crate) fn notify_cursor_set( + data: &EvdiDrmData, + dev: &EvdiDrmDevice, + hot_x: i32, + hot_y: i32, + width: u32, + height: u32, + enabled: bool, + buffer_handle: u32, + buffer_length: u32, + pixel_format: u32, + stride: u32, +) { + let ev = uapi::DrmEvdiEventCursorSet { + base: hdr(), + hot_x, + hot_y, + width, + height, + enabled: enabled as u8, + buffer_handle, + buffer_length, + pixel_format, + stride, + }; + let _ = data.events.send(dev, ev); +} + +/// Tell the DLM client the negotiated mode changed. +pub(crate) fn notify_mode_changed( + data: &EvdiDrmData, + dev: &EvdiDrmDevice, + hdisplay: i32, + vdisplay: i32, + vrefresh: i32, + bits_per_pixel: i32, + pixel_format: u32, +) { + let ev = uapi::DrmEvdiEventModeChanged { + base: hdr(), + hdisplay, + vdisplay, + vrefresh, + bits_per_pixel, + pixel_format, + }; + let _ = data.events.send(dev, ev); +} diff --git a/drivers/gpu/drm/evdi/uapi.rs b/drivers/gpu/drm/evdi/uapi.rs new file mode 100644 index 000000000000..75b245f20d9b --- /dev/null +++ b/drivers/gpu/drm/evdi/uapi.rs @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Userspace ABI for the EVDI DRM driver. +// +// This is a byte-for-byte Rust mirror of the C uAPI header +// `evdi/module/evdi_drm.h` (DisplayLink (UK) Ltd.). It is reproduced here rather +// than pulled from `kernel::uapi` because EVDI is an out-of-tree driver with its +// own private ioctl range; the struct layouts and ioctl numbers MUST stay +// identical to the C module so existing userspace (libevdi / DisplayLinkManager) +// keeps working unchanged. +// +// Every structure is `#[repr(C)]`; user pointers are represented as `usize` so the +// native (LP64/ILP32) ABI width matches the C `T __user *` exactly. Compile-time +// assertions at the bottom pin each `size_of` to the value the C ABI produces. + +#![allow(dead_code)] + +use kernel::drm::ioctl::{IO, IOWR}; + +/// DRM driver-private ioctl base (`DRM_COMMAND_BASE`). +pub(crate) const DRM_COMMAND_BASE: u32 = 0x40; + +// --- Output event type codes (driver -> libevdi), `drm_event::type` values. --- +pub(crate) const DRM_EVDI_EVENT_UPDATE_READY: u32 = 0x8000_0000; +pub(crate) const DRM_EVDI_EVENT_DPMS: u32 = 0x8000_0001; +pub(crate) const DRM_EVDI_EVENT_MODE_CHANGED: u32 = 0x8000_0002; +pub(crate) const DRM_EVDI_EVENT_CRTC_STATE: u32 = 0x8000_0003; +pub(crate) const DRM_EVDI_EVENT_CURSOR_SET: u32 = 0x8000_0004; +pub(crate) const DRM_EVDI_EVENT_CURSOR_MOVE: u32 = 0x8000_0005; +pub(crate) const DRM_EVDI_EVENT_DDCCI_DATA: u32 = 0x8000_0006; + +/// Fixed DDC/CI payload size carried by [`DrmEvdiEventDdcciData`]. +pub(crate) const DDCCI_BUFFER_SIZE: usize = 64; + +/// `struct drm_event` — the common header every DRM event starts with. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvent { + pub type_: u32, + pub length: u32, +} + +/// `struct drm_evdi_event_update_ready`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEventUpdateReady { + pub base: DrmEvent, +} + +/// `struct drm_evdi_event_dpms`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEventDpms { + pub base: DrmEvent, + pub mode: i32, +} + +/// `struct drm_evdi_event_mode_changed`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEventModeChanged { + pub base: DrmEvent, + pub hdisplay: i32, + pub vdisplay: i32, + pub vrefresh: i32, + pub bits_per_pixel: i32, + pub pixel_format: u32, +} + +/// `struct drm_evdi_event_crtc_state`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEventCrtcState { + pub base: DrmEvent, + pub state: i32, +} + +/// `struct drm_evdi_event_cursor_set`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEventCursorSet { + pub base: DrmEvent, + pub hot_x: i32, + pub hot_y: i32, + pub width: u32, + pub height: u32, + pub enabled: u8, + pub buffer_handle: u32, + pub buffer_length: u32, + pub pixel_format: u32, + pub stride: u32, +} + +/// `struct drm_evdi_event_cursor_move`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEventCursorMove { + pub base: DrmEvent, + pub x: i32, + pub y: i32, +} + +/// `struct drm_evdi_event_ddcci_data`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEventDdcciData { + pub base: DrmEvent, + pub buffer: [u8; DDCCI_BUFFER_SIZE], + pub buffer_length: u32, + pub flags: u16, + pub address: u16, +} + +// --- Input ioctl argument structures (libevdi -> driver). --- + +/// `struct drm_evdi_connect`. `edid` is a `const unsigned char __user *`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiConnect { + pub connected: i32, + pub dev_index: i32, + pub edid: usize, + pub edid_length: u32, + pub pixel_area_limit: u32, + pub pixel_per_second_limit: u32, +} + +/// `struct drm_evdi_request_update`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiRequestUpdate { + pub reserved: i32, +} + +/// `enum drm_evdi_grabpix_mode` (C `int`). +pub(crate) const EVDI_GRABPIX_MODE_RECTS: i32 = 0; +pub(crate) const EVDI_GRABPIX_MODE_DIRTY: i32 = 1; + +/// `struct drm_evdi_grabpix`. `buffer` and `rects` are `__user` pointers. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiGrabpix { + pub mode: i32, + pub buf_width: i32, + pub buf_height: i32, + pub buf_byte_stride: i32, + pub buffer: usize, + pub num_rects: i32, + pub rects: usize, +} + +/// `struct drm_evdi_ddcci_response`. `buffer` is a `const unsigned char __user *`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiDdcciResponse { + pub buffer: usize, + pub buffer_length: u32, + pub result: u8, +} + +/// `struct drm_evdi_enable_cursor_events`. +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct DrmEvdiEnableCursorEvents { + pub base: DrmEvent, + pub enable: u8, +} + +// --- Input ioctl command numbers (`DRM_IOCTL_EVDI_*`). --- +pub(crate) const DRM_EVDI_CONNECT: u32 = 0x00; +pub(crate) const DRM_EVDI_REQUEST_UPDATE: u32 = 0x01; +pub(crate) const DRM_EVDI_GRABPIX: u32 = 0x02; +pub(crate) const DRM_EVDI_DDCCI_RESPONSE: u32 = 0x03; +pub(crate) const DRM_EVDI_ENABLE_CURSOR_EVENTS: u32 = 0x04; + +/// `DRM_IOCTL_EVDI_CONNECT`. +pub(crate) const DRM_IOCTL_EVDI_CONNECT: u32 = + IOWR::<DrmEvdiConnect>(DRM_COMMAND_BASE + DRM_EVDI_CONNECT); +/// `DRM_IOCTL_EVDI_REQUEST_UPDATE`. +pub(crate) const DRM_IOCTL_EVDI_REQUEST_UPDATE: u32 = + IOWR::<DrmEvdiRequestUpdate>(DRM_COMMAND_BASE + DRM_EVDI_REQUEST_UPDATE); +/// `DRM_IOCTL_EVDI_GRABPIX`. +pub(crate) const DRM_IOCTL_EVDI_GRABPIX: u32 = + IOWR::<DrmEvdiGrabpix>(DRM_COMMAND_BASE + DRM_EVDI_GRABPIX); +/// `DRM_IOCTL_EVDI_DDCCI_RESPONSE`. +pub(crate) const DRM_IOCTL_EVDI_DDCCI_RESPONSE: u32 = + IOWR::<DrmEvdiDdcciResponse>(DRM_COMMAND_BASE + DRM_EVDI_DDCCI_RESPONSE); +/// `DRM_IOCTL_EVDI_ENABLE_CURSOR_EVENTS`. +pub(crate) const DRM_IOCTL_EVDI_ENABLE_CURSOR_EVENTS: u32 = + IOWR::<DrmEvdiEnableCursorEvents>(DRM_COMMAND_BASE + DRM_EVDI_ENABLE_CURSOR_EVENTS); + +// Silence unused-import warnings until the no-arg `IO` helper is needed. +const _: fn(u32) -> u32 = IO; + +// --- ABI size assertions: these must match the C `sizeof` on LP64. --- +const _: () = { + assert!(core::mem::size_of::<DrmEvent>() == 8); + assert!(core::mem::size_of::<DrmEvdiEventUpdateReady>() == 8); + assert!(core::mem::size_of::<DrmEvdiEventDpms>() == 12); + assert!(core::mem::size_of::<DrmEvdiEventModeChanged>() == 28); + assert!(core::mem::size_of::<DrmEvdiEventCrtcState>() == 12); + assert!(core::mem::size_of::<DrmEvdiEventCursorSet>() == 44); + assert!(core::mem::size_of::<DrmEvdiEventCursorMove>() == 16); + assert!(core::mem::size_of::<DrmEvdiEventDdcciData>() == 80); + assert!(core::mem::size_of::<DrmEvdiConnect>() == 32); + assert!(core::mem::size_of::<DrmEvdiRequestUpdate>() == 4); + assert!(core::mem::size_of::<DrmEvdiGrabpix>() == 40); + assert!(core::mem::size_of::<DrmEvdiDdcciResponse>() == 16); + assert!(core::mem::size_of::<DrmEvdiEnableCursorEvents>() == 12); +}; -- 2.55.0
