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


Reply via email to