Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH 0/3] Add support for Valkyrie USB LCD displays
@ 2026-10-28 13:25 ZiAo Wang
  2026-10-28 13:25 ` [PATCH 1/3] drm/format-helper: Add XRGB8888 to UYVY conversion ZiAo Wang
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: ZiAo Wang @ 2026-10-28 13:25 UTC (permalink / raw)
  To: dri-devel
  Cc: maarten.lankhorst, mripard, tzimmermann, airlied, simona, jikos,
	bentiss, linux-input, linux-kernel, Ho Jie Feng, ZiAo Wang

Hi,

This series adds a DRM/KMS driver for Valkyrie USB LCD displays.

Vendor website here:
https://www.my-valkyrie.com

This driver is devloped from scratch via black-box analysis on
vendor software. Currently only supports two variants, I'm sure
there are more variants, but I don't have them.

The devices use a vendor-specific USB protocol. The driver exposes
them as standard DRM outputs and converts XRGB8888 framebuffer data
to UYVY, the only format device accepts.

Patch 1 was authored by Ho Jie Feng and provides the XRGB8888-to-UYVY
conversion helper used by the driver. It was previously posted but
was not merged because it lacked an in-tree user. It is included
here without changes to the original code, with my Signed-off-by
added as the submitter. Ho Jie Feng is CC'd on this series.

Original posting:
https://lore.kernel.org/lkml/20261019154820.68659-2-hjf3108@gmail.com/

Patch 2 prevents the HID subsystem from claiming the display control
interface. Patch 3 adds the Valkyrie DRM driver.

I wish merging this series through the DRM tree, with an Ack
from the HID maintainers for patch 2.

I tested both supported variants on physical hardware using the
Sway compositor and verified that rendering, suspend, and reset
work correctly on all of them.

Thanks,
ZiAo Wang

Ho Jie Feng (1):
  drm/format-helper: Add XRGB8888 to UYVY conversion

ZiAo Wang (2):
  HID: quirks: Ignore Valkyrie USB LCD devices
  drm/tiny: add driver for Valkyrie USB LCD displays

 MAINTAINERS                           |    7 +
 drivers/gpu/drm/drm_format_helper.c   |   73 ++
 drivers/gpu/drm/drm_format_internal.h |   21 +
 drivers/gpu/drm/tiny/Kconfig          |   15 +
 drivers/gpu/drm/tiny/Makefile         |    1 +
 drivers/gpu/drm/tiny/valkyrie-lcd.c   | 1413 +++++++++++++++++++++++++
 drivers/hid/hid-ids.h                 |    3 +
 drivers/hid/hid-quirks.c              |    1 +
 include/drm/drm_format_helper.h       |    4 +
 9 files changed, 1538 insertions(+)
 create mode 100644 drivers/gpu/drm/tiny/valkyrie-lcd.c


base-commit: 151a134a4e6ec66f69db8750c66006f5207f7a9d
-- 
2.56.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/3] drm/format-helper: Add XRGB8888 to UYVY conversion
  2026-10-28 13:25 [PATCH 0/3] Add support for Valkyrie USB LCD displays ZiAo Wang
@ 2026-10-28 13:25 ` ZiAo Wang
  2026-10-28 13:25 ` [PATCH 2/3] HID: quirks: Ignore Valkyrie USB LCD devices ZiAo Wang
  2026-10-28 13:25 ` [PATCH 3/3] drm/tiny: add driver for Valkyrie USB LCD displays ZiAo Wang
  2 siblings, 0 replies; 6+ messages in thread
From: ZiAo Wang @ 2026-10-28 13:25 UTC (permalink / raw)
  To: dri-devel
  Cc: maarten.lankhorst, mripard, tzimmermann, airlied, simona, jikos,
	bentiss, linux-input, linux-kernel, Ho Jie Feng, ZiAo Wang

From: Ho Jie Feng <hjf3108@gmail.com>

Add a helper for devices that use packed UYVY but accept XRGB8888
framebuffers.

Signed-off-by: Ho Jie Feng <hjf3108@gmail.com>
Signed-off-by: ZiAo Wang <ziaowang2007@gmail.com>
---
 drivers/gpu/drm/drm_format_helper.c   | 73 +++++++++++++++++++++++++++
 drivers/gpu/drm/drm_format_internal.h | 21 ++++++++
 include/drm/drm_format_helper.h       |  4 ++
 3 files changed, 98 insertions(+)

diff --git a/drivers/gpu/drm/drm_format_helper.c b/drivers/gpu/drm/drm_format_helper.c
index 6cddf05c49..606733ca43 100644
--- a/drivers/gpu/drm/drm_format_helper.c
+++ b/drivers/gpu/drm/drm_format_helper.c
@@ -1379,3 +1379,76 @@ void drm_fb_xrgb8888_to_gray2(struct iosys_map *dst, const unsigned int *dst_pit
 }
 EXPORT_SYMBOL(drm_fb_xrgb8888_to_gray2);
 
+static void drm_fb_xrgb8888_to_uyvy_line(void *dbuf, const void *sbuf, unsigned int width)
+{
+	u8 *transfer_buffer = dbuf;
+	const __le32 *temp_buffer = sbuf;
+	unsigned int i, dst_offset = 0;
+	unsigned int pixel1, pixel2;
+	unsigned int r1, g1, b1, r2, g2, b2;
+	unsigned int r, g, b;
+	unsigned int v, y1, u, y2;
+
+	for (i = 0; i < width; i += 2) {
+		pixel1 = le32_to_cpup(&temp_buffer[i]);
+		pixel2 = le32_to_cpup(&temp_buffer[i + 1]);
+
+		r1 = (pixel1 & 0x00ff0000) >> 16;
+		g1 = (pixel1 & 0x0000ff00) >> 8;
+		b1 =  pixel1 & 0x000000ff;
+		r2 = (pixel2 & 0x00ff0000) >> 16;
+		g2 = (pixel2 & 0x0000ff00) >> 8;
+		b2 =  pixel2 & 0x000000ff;
+
+		y1 = drm_pixel_rgb_to_y_bt601(r1, g1, b1);
+		y2 = drm_pixel_rgb_to_y_bt601(r2, g2, b2);
+
+		r = r1 + r2;
+		g = g1 + g2;
+		b = b1 + b2;
+
+		v = drm_pixel_rgb2_to_v_bt601(r, g, b);
+		u = drm_pixel_rgb2_to_u_bt601(r, g, b);
+
+		transfer_buffer[dst_offset++] = u;
+		transfer_buffer[dst_offset++] = y1;
+		transfer_buffer[dst_offset++] = v;
+		transfer_buffer[dst_offset++] = y2;
+	}
+}
+
+/**
+ * drm_fb_xrgb8888_to_uyvy - Convert XRGB8888 to UYVY clip buffer
+ * @dst: Array of UYVY destination buffers
+ * @dst_pitch: Array of numbers of bytes between the start of two consecutive scanlines
+ *             within @dst; can be NULL if scanlines are stored next to each other.
+ * @src: Array of XRGB8888 source buffers
+ * @fb: DRM framebuffer
+ * @clip: Clip rectangle area to copy; x1 and x2 must be even
+ * @state: Transform and conversion state
+ *
+ * This function copies parts of a framebuffer to display memory and converts the
+ * color format during the process. The parameters @dst, @dst_pitch and @src refer
+ * to arrays. Each array must have at least as many entries as there are planes in
+ * @fb's format. Each entry stores the value for the format's respective color plane
+ * at the same index.
+ *
+ * This function does not apply clipping on @dst (i.e. the destination is at the
+ * top-left corner).
+ *
+ * Drivers can use this function for UYVY devices that don't support XRGB8888
+ * natively. ITU BT.601 is being used for the RGB -> YUV conversion.
+ */
+void drm_fb_xrgb8888_to_uyvy(struct iosys_map *dst, const unsigned int *dst_pitch,
+			     const struct iosys_map *src, const struct drm_framebuffer *fb,
+			     const struct drm_rect *clip, struct drm_format_conv_state *state)
+{
+	static const u8 dst_pixsize[DRM_FORMAT_MAX_PLANES] = {
+		2,
+	};
+
+	drm_fb_xfrm(dst, dst_pitch, dst_pixsize, src, fb, clip, false, state,
+		    drm_fb_xrgb8888_to_uyvy_line);
+}
+EXPORT_SYMBOL(drm_fb_xrgb8888_to_uyvy);
+
diff --git a/drivers/gpu/drm/drm_format_internal.h b/drivers/gpu/drm/drm_format_internal.h
index ce29dd05bc..7a4cfb0d18 100644
--- a/drivers/gpu/drm/drm_format_internal.h
+++ b/drivers/gpu/drm/drm_format_internal.h
@@ -159,6 +159,27 @@ static inline u32 drm_pixel_xrgb8888_to_abgr2101010(u32 pix)
 	       drm_pixel_xrgb8888_to_xbgr2101010(pix);
 }
 
+static inline u32 drm_pixel_rgb_to_y_bt601(u32 r, u32 g, u32 b)
+{
+	const u32 luma = (16U << 16) + 16829 * r + 33039 * g + 6416 * b + 32768;
+
+	return luma >> 16;
+}
+
+static inline u32 drm_pixel_rgb2_to_u_bt601(u32 r2, u32 g2, u32 b2)
+{
+	const u32 u = (128U << 17) - 9714 * r2 - 19071 * g2 + 28784 * b2 + 65536;
+
+	return u >> 17;
+}
+
+static inline u32 drm_pixel_rgb2_to_v_bt601(u32 r2, u32 g2, u32 b2)
+{
+	const u32 v = (128U << 17) + 28784 * r2 - 24103 * g2 - 4681 * b2 + 65536;
+
+	return v >> 17;
+}
+
 /*
  * Conversion from ARGB8888
  */
diff --git a/include/drm/drm_format_helper.h b/include/drm/drm_format_helper.h
index 2b5c1aef80..e50f96e074 100644
--- a/include/drm/drm_format_helper.h
+++ b/include/drm/drm_format_helper.h
@@ -136,4 +136,8 @@ void drm_fb_xrgb8888_to_gray2(struct iosys_map *dst, const unsigned int *dst_pit
 			      const struct iosys_map *src, const struct drm_framebuffer *fb,
 			      const struct drm_rect *clip, struct drm_format_conv_state *state);
 
+void drm_fb_xrgb8888_to_uyvy(struct iosys_map *dst, const unsigned int *dst_pitch,
+			     const struct iosys_map *src, const struct drm_framebuffer *fb,
+			     const struct drm_rect *clip, struct drm_format_conv_state *state);
+
 #endif /* __LINUX_DRM_FORMAT_HELPER_H */
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/3] HID: quirks: Ignore Valkyrie USB LCD devices
  2026-10-28 13:25 [PATCH 0/3] Add support for Valkyrie USB LCD displays ZiAo Wang
  2026-10-28 13:25 ` [PATCH 1/3] drm/format-helper: Add XRGB8888 to UYVY conversion ZiAo Wang
@ 2026-10-28 13:25 ` ZiAo Wang
  2026-10-28 13:41   ` Jie Feng
  2026-10-28 13:25 ` [PATCH 3/3] drm/tiny: add driver for Valkyrie USB LCD displays ZiAo Wang
  2 siblings, 1 reply; 6+ messages in thread
From: ZiAo Wang @ 2026-10-28 13:25 UTC (permalink / raw)
  To: dri-devel
  Cc: maarten.lankhorst, mripard, tzimmermann, airlied, simona, jikos,
	bentiss, linux-input, linux-kernel, Ho Jie Feng, ZiAo Wang

Valkyrie USB LCD devices (0x345f:0x9132) expose a HID interface
for vendor-specific display control commands and a separate USB
interface for bulk framebuffer transfers.

The HID interface is not intended for input and should be managed
by the dedicated USB display driver instead of the HID subsystem.

Add the device to the HID quirk table with HID_QUIRK_IGNORE to
prevent the HID driver from binding to it.

Signed-off-by: ZiAo Wang <ziaowang2007@gmail.com>
---
 drivers/hid/hid-ids.h    | 3 +++
 drivers/hid/hid-quirks.c | 1 +
 2 files changed, 4 insertions(+)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 91319aa651..d135ea8f06 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1704,4 +1704,7 @@
 #define USB_VENDOR_ID_HUAWEI			0x12d1
 #define USB_DEVICE_ID_HUAWEI_CD30KBD		0x109b
 
+#define USB_VENDOR_ID_VALKYRIE			0x345f
+#define USB_PRODUCT_ID_VALKYRIE_LCD		0x9132
+
 #endif
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 793db001a6..70ada7320c 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -226,6 +226,7 @@ static const struct hid_device_id hid_quirks[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_YJXCHIP, USB_DEVICE_ID_YJXCHIP_MOUSE),
 		HID_QUIRK_ALWAYS_POLL },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_GROUP_AUDIO), HID_QUIRK_NOGET },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_VALKYRIE, USB_PRODUCT_ID_VALKYRIE_LCD), HID_QUIRK_IGNORE },
 
 	{ 0 }
 };
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/3] drm/tiny: add driver for Valkyrie USB LCD displays
  2026-10-28 13:25 [PATCH 0/3] Add support for Valkyrie USB LCD displays ZiAo Wang
  2026-10-28 13:25 ` [PATCH 1/3] drm/format-helper: Add XRGB8888 to UYVY conversion ZiAo Wang
  2026-10-28 13:25 ` [PATCH 2/3] HID: quirks: Ignore Valkyrie USB LCD devices ZiAo Wang
@ 2026-10-28 13:25 ` ZiAo Wang
  2026-10-28 14:02   ` sashiko-bot
  2 siblings, 1 reply; 6+ messages in thread
From: ZiAo Wang @ 2026-10-28 13:25 UTC (permalink / raw)
  To: dri-devel
  Cc: maarten.lankhorst, mripard, tzimmermann, airlied, simona, jikos,
	bentiss, linux-input, linux-kernel, Ho Jie Feng, ZiAo Wang

Valkyrie USB LCD displays rely on vendor-specific Windows software
to display images and do not expose a standard display interface,
preventing them from being used as regular displays on Linux.

Add a DRM/KMS driver developed from scratch by reverse-engineering
the vendor's software and USB protocol. The driver exposes these
devices as standard DRM display outputs with fixed native modes,
initially supporting the 320x240 and 360x960 variants.

The driver initializes and controls the displays through USB
control transfers, and converts and streams framebuffer contents
as UYVY data over a USB bulk OUT endpoint in chunks of up to
65536 bytes.

Use GEM shmem buffers and atomic modesetting to integrate with
the DRM subsystem, and provide fbdev emulation for compatibility
with legacy framebuffer applications.

Signed-off-by: ZiAo Wang <ziaowang2007@gmail.com>
---
 MAINTAINERS                         |    7 +
 drivers/gpu/drm/tiny/Kconfig        |   15 +
 drivers/gpu/drm/tiny/Makefile       |    1 +
 drivers/gpu/drm/tiny/valkyrie-lcd.c | 1413 +++++++++++++++++++++++++++
 4 files changed, 1436 insertions(+)
 create mode 100644 drivers/gpu/drm/tiny/valkyrie-lcd.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 365bec8eb7..1b91f813bb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8815,6 +8815,13 @@ S:	Supported
 T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
 F:	drivers/gpu/drm/udl/
 
+DRM DRIVER FOR VALKYRIE USB LCD DISPLAYS
+M:	ZiAo Wang <ziaowang2007@gmail.com>
+L:	dri-devel@lists.freedesktop.org
+S:	Maintained
+T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F:	drivers/gpu/drm/tiny/valkyrie-lcd.c
+
 DRM DRIVER FOR VIRTUAL KERNEL MODESETTING (VKMS)
 M:	Louis Chauvet <louis.chauvet@bootlin.com>
 R:	Haneen Mohammed <hamohammed.sa@gmail.com>
diff --git a/drivers/gpu/drm/tiny/Kconfig b/drivers/gpu/drm/tiny/Kconfig
index f0e72d4b6a..1e4be6d72c 100644
--- a/drivers/gpu/drm/tiny/Kconfig
+++ b/drivers/gpu/drm/tiny/Kconfig
@@ -98,6 +98,21 @@ config DRM_PIXPAPER
 
 	  If M is selected, the module will be built as pixpaper.ko.
 
+config DRM_VALKYRIE_LCD
+	tristate "DRM support for Valkyrie USB LCDs"
+	depends on DRM && USB && MMU
+	select DRM_CLIENT_SELECTION
+	select DRM_KMS_HELPER
+	select DRM_GEM_SHMEM_HELPER
+	help
+	  Enable support for Valkyrie USB LCD displays with USB ID 345f:9132.
+	  The driver provides DRM/KMS output for the 320x240 and 360x960
+	  display variants. It supports native UYVY and software-converted
+	  XRGB8888 framebuffers.
+
+	  To compile this driver as a module, choose M here. The module
+	  will be called valkyrie-lcd.
+
 config TINYDRM_HX8357D
 	tristate "DRM support for HX8357D display panels"
 	depends on DRM && SPI
diff --git a/drivers/gpu/drm/tiny/Makefile b/drivers/gpu/drm/tiny/Makefile
index 48d30bf615..e768abb4dc 100644
--- a/drivers/gpu/drm/tiny/Makefile
+++ b/drivers/gpu/drm/tiny/Makefile
@@ -7,6 +7,7 @@ obj-$(CONFIG_DRM_CIRRUS_QEMU)		+= cirrus-qemu.o
 obj-$(CONFIG_DRM_GM12U320)		+= gm12u320.o
 obj-$(CONFIG_DRM_PANEL_MIPI_DBI)	+= panel-mipi-dbi.o
 obj-$(CONFIG_DRM_PIXPAPER)              += pixpaper.o
+obj-$(CONFIG_DRM_VALKYRIE_LCD)		+= valkyrie-lcd.o
 obj-$(CONFIG_TINYDRM_HX8357D)		+= hx8357d.o
 obj-$(CONFIG_TINYDRM_ILI9163)		+= ili9163.o
 obj-$(CONFIG_TINYDRM_ILI9225)		+= ili9225.o
diff --git a/drivers/gpu/drm/tiny/valkyrie-lcd.c b/drivers/gpu/drm/tiny/valkyrie-lcd.c
new file mode 100644
index 0000000000..6478e92462
--- /dev/null
+++ b/drivers/gpu/drm/tiny/valkyrie-lcd.c
@@ -0,0 +1,1413 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/mutex.h>
+#include <linux/stddef.h>
+#include <linux/math.h>
+#include <linux/gfp_types.h>
+#include <linux/minmax.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+#include <linux/jiffies.h>
+#include <linux/unaligned.h>
+#include <linux/container_of.h>
+#include <linux/array_size.h>
+#include <linux/delay.h>
+#include <linux/completion.h>
+#include <linux/types.h>
+#include <linux/err.h>
+#include <linux/atomic.h>
+#include <linux/lockdep.h>
+
+#include <linux/usb.h>
+#include <linux/usb/ch9.h>
+#include <linux/hid.h>
+#include <linux/hiddev.h>
+
+#include <drm/drm_drv.h>
+#include <drm/drm_device.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_print.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_format_helper.h>
+#include <drm/drm_gem.h>
+#include <drm/drm_gem_atomic_helper.h>
+#include <drm/drm_gem_shmem_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_fbdev_shmem.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_vblank.h>
+#include <drm/drm_vblank_helper.h>
+#include <drm/clients/drm_client_setup.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_crtc.h>
+
+#define VK_VENDOR_ID 0x345f
+#define VK_PRODUCT_ID 0x9132
+
+#define VK_FRAME_EP_NUMBER 0x04
+#define VK_SERIAL_ADDR 0x001fe0
+
+#define VK_A6_POLL_INTERVAL_US 1000
+#define VK_A6_POLL_MAX 100
+
+#define VK_SEND_CHUNK_TIMEOUT 1000
+#define VK_SEND_BUF_SIZE 65536
+
+#define VK_FRAME_FORMAT DRM_FORMAT_UYVY
+
+#define VK_TIMEOUT_FLUSH_INTERVAL_MS 3000
+
+MODULE_AUTHOR("ZiAo Wang");
+MODULE_DESCRIPTION("Valkyrie USB LCD DRM driver");
+MODULE_LICENSE("GPL");
+
+struct vk_lcd_device_info {
+	/* Identification data */
+	u8 serial[32] __nonstring;
+	u8 label_0030[2];
+	u8 label_f000[3];
+
+	/* Panel parameters */
+	u32 width, height, fps;
+	u32 width_mm, height_mm;
+	u16 video_code;
+	u16 complex_switch_addr;
+	u8 complex_switch_mask;
+	bool complex_switch_active_high;
+	u8 frame_header[8], frame_trailer[8];
+};
+
+static const struct vk_lcd_device_info vk_lcd_devices[];
+
+struct vk_driver {
+	struct usb_interface *intf;
+	struct usb_device *dev;
+	struct usb_endpoint_descriptor *frame_epd;
+	const struct vk_lcd_device_info *device_info;
+
+	/* Serializes all USB I/O and protects io_blocking and device_state. */
+	struct mutex io_lock;
+	bool io_blocking;
+	bool device_state;
+
+	struct drm_device drm;
+	bool drm_suspended;
+	struct drm_plane plane;
+	struct drm_encoder encoder;
+	struct vk_crtc_driver *crtc;
+	struct vk_connector_driver *connector;
+};
+
+/* On success, return with io_lock held; on failure, release it. */
+static inline bool vk_io_start(struct vk_driver *drv)
+{
+	mutex_lock(&drv->io_lock);
+	if (drv->io_blocking) {
+		mutex_unlock(&drv->io_lock);
+		return false;
+	}
+	return true;
+}
+
+static inline void vk_io_end(struct vk_driver *drv)
+{
+	lockdep_assert_held(&drv->io_lock);
+	mutex_unlock(&drv->io_lock);
+}
+
+static int vk_probe(struct usb_interface *intf, const struct usb_device_id *id);
+static void vk_disconnect(struct usb_interface *intf);
+static int vk_suspend(struct usb_interface *intf, pm_message_t message);
+static int vk_resume(struct usb_interface *intf);
+static int vk_reset_resume(struct usb_interface *intf);
+static int vk_pre_reset(struct usb_interface *intf);
+static int vk_post_reset(struct usb_interface *intf);
+static const struct usb_device_id vk_usb_ids[] = {
+	{ USB_DEVICE_INTERFACE_NUMBER(VK_VENDOR_ID, VK_PRODUCT_ID, 3) },
+	{}
+};
+MODULE_DEVICE_TABLE(usb, vk_usb_ids);
+
+static struct usb_driver vk_usb_driver = {
+	.name = "valkyrie_lcd",
+	.id_table = vk_usb_ids,
+	.probe = vk_probe,
+	.disconnect = vk_disconnect,
+	.supports_autosuspend = 0,
+	.suspend = vk_suspend,
+	.resume = vk_resume,
+	.reset_resume = vk_reset_resume,
+	.pre_reset = vk_pre_reset,
+	.post_reset = vk_post_reset,
+};
+module_usb_driver(vk_usb_driver);
+
+DEFINE_DRM_GEM_FOPS(vk_drm_fops);
+static struct drm_driver vk_drm_driver = {
+	.name = "valkyrie_lcd",
+	.desc = "Valkyrie LCD DRM driver",
+	.major = 1,
+	.minor = 0,
+	.patchlevel = 0,
+	.driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC,
+	DRM_GEM_SHMEM_DRIVER_OPS,
+	DRM_FBDEV_SHMEM_DRIVER_OPS,
+	.fops = &vk_drm_fops,
+};
+
+static const u32 vk_plane_formats[] = {
+	DRM_FORMAT_XRGB8888,
+	DRM_FORMAT_UYVY,
+};
+
+static const struct drm_mode_config_funcs vk_mode_config_funcs = {
+	.fb_create = drm_gem_fb_create_with_dirty,
+	.atomic_check = drm_atomic_helper_check,
+	.atomic_commit = drm_atomic_helper_commit
+};
+
+static const struct drm_plane_funcs vk_plane_funcs = {
+	.update_plane = drm_atomic_helper_update_plane,
+	.disable_plane = drm_atomic_helper_disable_plane,
+	DRM_GEM_SHADOW_PLANE_FUNCS,
+	.destroy = drm_plane_cleanup
+};
+
+static int vk_plane_atomic_check(struct drm_plane *plane,
+				 struct drm_atomic_commit *state);
+static void vk_plane_atomic_update(struct drm_plane *plane,
+				   struct drm_atomic_commit *state)
+{
+	// All work is done in vk_crtc_atomic_flush
+}
+
+static const struct drm_plane_helper_funcs vk_plane_helper_funcs = {
+	DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
+	.atomic_check = vk_plane_atomic_check,
+	.atomic_update = vk_plane_atomic_update
+};
+
+struct vk_send_chunk {
+	struct urb urb;
+	void *buf;
+	int errno;
+	struct completion flight;
+};
+
+static int vk_send_chunk_init(struct drm_device *drm,
+			      struct vk_send_chunk *chunk)
+{
+	init_completion(&chunk->flight);
+	usb_init_urb(&chunk->urb);
+	chunk->buf = drmm_kzalloc(drm, VK_SEND_BUF_SIZE, GFP_KERNEL);
+	if (!chunk->buf)
+		return -ENOMEM;
+	return 0;
+}
+
+struct vk_crtc_driver {
+	struct drm_crtc crtc;
+	struct delayed_work timeout_flush;
+
+	struct vk_send_chunk send_chunk[2];
+};
+
+static const struct drm_crtc_funcs vk_crtc_funcs = {
+	.set_config = drm_atomic_helper_set_config,
+	.page_flip = drm_atomic_helper_page_flip,
+	.atomic_create_state = drm_atomic_helper_crtc_create_state,
+	.atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
+	.atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
+	DRM_CRTC_VBLANK_TIMER_FUNCS
+};
+
+static enum drm_mode_status
+vk_crtc_mode_valid(struct drm_crtc *crtc, const struct drm_display_mode *mode);
+static int vk_crtc_atomic_check(struct drm_crtc *crtc,
+				struct drm_atomic_commit *state);
+static void vk_crtc_atomic_flush(struct drm_crtc *crtc,
+				 struct drm_atomic_commit *state);
+static void vk_crtc_atomic_enable(struct drm_crtc *crtc,
+				  struct drm_atomic_commit *state)
+{
+	drm_crtc_vblank_on(crtc);
+}
+
+static void vk_crtc_atomic_disable(struct drm_crtc *crtc,
+				   struct drm_atomic_commit *state)
+{
+	drm_crtc_vblank_off(crtc);
+}
+
+static const struct drm_crtc_helper_funcs vk_crtc_helper_funcs = {
+	.mode_valid = vk_crtc_mode_valid,
+	.atomic_check = vk_crtc_atomic_check,
+	.atomic_flush = vk_crtc_atomic_flush,
+	.atomic_enable = vk_crtc_atomic_enable,
+	.atomic_disable = vk_crtc_atomic_disable,
+};
+
+struct vk_connector_driver {
+	struct drm_connector connector;
+
+	struct usb_device *usb_dev;
+	unsigned int data_pipe;
+	const struct vk_lcd_device_info *device_info;
+};
+
+static const struct drm_connector_funcs vk_connector_funcs = {
+	.atomic_create_state = drm_atomic_helper_connector_create_state,
+	.fill_modes = drm_helper_probe_single_connector_modes,
+	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+	.atomic_destroy_state = drm_atomic_helper_connector_destroy_state
+};
+
+static int vk_connector_get_modes(struct drm_connector *connector);
+static const struct drm_connector_helper_funcs vk_connector_helper_funcs = {
+	.get_modes = vk_connector_get_modes,
+};
+
+static const struct drm_encoder_funcs vk_encoder_funcs = {
+	.destroy = drm_encoder_cleanup,
+};
+
+static int vk_device_info_find_or_verify(struct vk_driver *drv);
+static int vk_device_bootstrap(struct vk_driver *drv);
+static int vk_device_enable_pre(struct vk_driver *drv);
+static int vk_device_enable_post(struct vk_driver *drv);
+static int vk_device_disable(struct vk_driver *drv);
+static int vk_device_reset(struct vk_driver *drv);
+
+static int vk_plane_atomic_check(struct drm_plane *plane,
+				 struct drm_atomic_commit *state)
+{
+	struct drm_plane_state *new_plane_state =
+		drm_atomic_get_new_plane_state(state, plane);
+	struct drm_crtc_state *new_crtc_state;
+	int ret;
+
+	if (!new_plane_state->fb)
+		return 0;
+
+	if (!new_plane_state->crtc)
+		return -EINVAL;
+	new_crtc_state =
+		drm_atomic_get_new_crtc_state(state, new_plane_state->crtc);
+	if (!new_crtc_state)
+		return -EINVAL;
+
+	ret = drm_atomic_helper_check_plane_state(new_plane_state,
+						  new_crtc_state,
+						  DRM_PLANE_NO_SCALING,
+						  DRM_PLANE_NO_SCALING, false,
+						  false);
+	if (ret)
+		return ret;
+
+	if (new_plane_state->rotation != DRM_MODE_ROTATE_0) {
+		drm_dbg_kms(plane->dev, "Unsupported plane rotation %#x\n",
+			    new_plane_state->rotation);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static enum drm_mode_status
+vk_crtc_mode_valid(struct drm_crtc *crtc, const struct drm_display_mode *mode)
+{
+	struct vk_driver *drv = container_of(crtc->dev, struct vk_driver, drm);
+	struct drm_display_mode preferred = {
+		DRM_MODE_INIT(drv->device_info->fps,
+			      drv->device_info->width,
+			      drv->device_info->height,
+			      drv->device_info->width_mm,
+			      drv->device_info->height_mm)
+	};
+
+	return drm_crtc_helper_mode_valid_fixed(crtc, mode, &preferred);
+}
+
+static int vk_crtc_atomic_check(struct drm_crtc *crtc,
+				struct drm_atomic_commit *state)
+{
+	struct vk_driver *drv = container_of(crtc->dev, struct vk_driver, drm);
+	struct drm_crtc_state *new_crtc_state =
+		drm_atomic_get_new_crtc_state(state, crtc);
+	struct drm_plane *plane = &drv->plane;
+	struct vk_connector_driver *connector_drv = drv->connector;
+	struct drm_plane_state *new_plane_state;
+
+	if (!new_crtc_state->enable || !new_crtc_state->active)
+		return 0;
+
+	if (new_crtc_state->plane_mask != drm_plane_mask(plane) ||
+	    new_crtc_state->connector_mask !=
+		    drm_connector_mask(&connector_drv->connector)) {
+		drm_dbg_kms(crtc->dev,
+			    "Active CRTC requires the primary plane and USB connector\n");
+		return -EINVAL;
+	}
+
+	new_plane_state = drm_atomic_get_new_plane_state(state, plane);
+	if (!new_plane_state)
+		new_plane_state = plane->state;
+	if (!new_plane_state || new_plane_state->crtc != crtc ||
+	    !new_plane_state->fb) {
+		drm_dbg_kms(crtc->dev,
+			    "Active CRTC has no framebuffer on its primary plane\n");
+		return -EINVAL;
+	}
+
+	if (new_plane_state->crtc_w != connector_drv->device_info->width ||
+	    new_plane_state->crtc_h != connector_drv->device_info->height) {
+		drm_dbg_kms(crtc->dev,
+			    "Unsupported scanout size %ux%u (expected %ux%u)\n",
+			    new_plane_state->crtc_w, new_plane_state->crtc_h,
+			    connector_drv->device_info->width,
+			    connector_drv->device_info->height);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int vk_connector_get_modes(struct drm_connector *connector)
+{
+	struct vk_connector_driver *drv =
+		container_of(connector, struct vk_connector_driver, connector);
+	struct drm_display_mode mode = {
+		DRM_MODE_INIT(drv->device_info->fps,
+			      drv->device_info->width,
+			      drv->device_info->height,
+			      drv->device_info->width_mm,
+			      drv->device_info->height_mm)
+	};
+	return drm_connector_helper_get_modes_fixed(connector, &mode);
+}
+
+static void vk_timeout_atomic_submit(struct work_struct *work)
+{
+	struct vk_crtc_driver *crtc_drv =
+		container_of(to_delayed_work(work), struct vk_crtc_driver,
+			     timeout_flush);
+	struct drm_crtc *crtc = &crtc_drv->crtc;
+	struct drm_device *drm = crtc->dev;
+	struct drm_modeset_acquire_ctx ctx;
+	struct drm_atomic_commit *state;
+	struct drm_crtc_state *crtc_state;
+	int ret = 0;
+
+	drm_modeset_acquire_init(&ctx, 0);
+
+	state = drm_atomic_commit_alloc(drm);
+	if (!state) {
+		ret = -ENOMEM;
+		goto fail_alloc;
+	}
+
+	state->acquire_ctx = &ctx;
+	state->allow_modeset = false;
+
+	do {
+		crtc_state = drm_atomic_get_crtc_state(state, crtc);
+		if (IS_ERR(crtc_state))
+			ret = PTR_ERR(crtc_state);
+		else if (crtc_state->active)
+			ret = drm_atomic_nonblocking_commit(state);
+		else
+			break;
+
+		if (ret != -EDEADLK)
+			break;
+		drm_atomic_commit_clear(state);
+		ret = drm_modeset_backoff(&ctx);
+	} while (!ret);
+	drm_atomic_commit_put(state);
+
+fail_alloc:
+	drm_modeset_drop_locks(&ctx);
+	drm_modeset_acquire_fini(&ctx);
+
+	if (ret == -EBUSY)
+		return;
+
+	if (ret)
+		drm_warn_ratelimited(crtc->dev, "Timeout refresh commit failed: %d\n", ret);
+}
+
+static int vk_scan_frame(struct vk_crtc_driver *crtc_drv,
+			 struct vk_connector_driver *connector_drv,
+			 struct drm_shadow_plane_state *plane);
+static void vk_crtc_atomic_flush(struct drm_crtc *crtc,
+				 struct drm_atomic_commit *state)
+{
+	struct vk_driver *drv = container_of(crtc->dev, struct vk_driver, drm);
+	struct vk_crtc_driver *crtc_drv = drv->crtc;
+	struct drm_crtc_state *old_crtc_state =
+		drm_atomic_get_old_crtc_state(state, crtc);
+	struct drm_crtc_state *new_crtc_state =
+		drm_atomic_get_new_crtc_state(state, crtc);
+	struct drm_plane *plane = &drv->plane;
+	struct vk_connector_driver *connector_drv = drv->connector;
+	struct drm_plane_state *new_plane_state =
+		drm_atomic_get_new_plane_state(state, plane);
+	int ret = 0, ret2;
+	int enter_idx;
+	bool new_enable = new_crtc_state->enable && new_crtc_state->active,
+	     old_enable = old_crtc_state->enable && old_crtc_state->active;
+	unsigned long irqflags;
+
+	if (!new_plane_state)
+		new_plane_state = plane->state;
+
+	if (!drm_dev_enter(crtc->dev, &enter_idx))
+		goto drm_fail;
+	if (!vk_io_start(drv)) {
+		drm_dbg_ratelimited(crtc->dev,
+				    "Skipping CRTC update while display I/O is blocked\n");
+		goto vkio_fail;
+	}
+
+	if (new_enable != old_enable)
+		drm_dbg_kms(crtc->dev, "CRTC active state: %d -> %d\n",
+			    old_enable, new_enable);
+
+	if (!new_enable) {
+		if (drv->device_state)
+			ret = vk_device_disable(drv);
+	} else {
+		bool enabling = !drv->device_state;
+
+		/* Testing found a ~10-second timeout; refresh every 3 seconds. */
+		mod_delayed_work(system_dfl_wq, &crtc_drv->timeout_flush,
+				 msecs_to_jiffies(VK_TIMEOUT_FLUSH_INTERVAL_MS));
+		if (enabling)
+			ret = vk_device_enable_pre(drv);
+		ret2 = vk_scan_frame(crtc_drv, connector_drv,
+				     to_drm_shadow_plane_state(new_plane_state));
+		ret = ret ? ret : ret2;
+		if (enabling) {
+			ret2 = vk_device_enable_post(drv);
+			ret = ret ? ret : ret2;
+		}
+	}
+	if (ret && ret != -ENODEV && ret != -ESHUTDOWN)
+		drm_err_ratelimited(crtc->dev,
+				    "Display update failed (active=%d): %d\n",
+				    new_enable, ret);
+	vk_io_end(drv);
+vkio_fail:
+	drm_dev_exit(enter_idx);
+drm_fail:
+	if (new_crtc_state->event) {
+		spin_lock_irqsave(&crtc->dev->event_lock, irqflags);
+		drm_crtc_send_vblank_event(crtc, new_crtc_state->event);
+		new_crtc_state->event = NULL;
+		spin_unlock_irqrestore(&crtc->dev->event_lock, irqflags);
+	}
+}
+
+static int vk_init_drm(struct vk_driver *drv)
+{
+	int ret;
+	struct drm_device *drm = &drv->drm;
+	struct drm_mode_config *mode_config = &drm->mode_config;
+	struct vk_connector_driver *connector;
+	struct vk_crtc_driver *crtc;
+
+	drm_dbg_driver(drm, "Initializing DRM resources\n");
+
+	ret = drmm_mode_config_init(drm);
+	if (ret)
+		return ret;
+
+	mode_config->funcs = &vk_mode_config_funcs;
+	mode_config->max_height = drv->device_info->height;
+	mode_config->min_height = drv->device_info->height;
+	mode_config->max_width = drv->device_info->width;
+	mode_config->min_width = drv->device_info->width;
+	mode_config->preferred_depth = 24;
+	mode_config->prefer_shadow = true;
+
+	ret = drm_universal_plane_init(drm, &drv->plane, BIT(0),
+				       &vk_plane_funcs, vk_plane_formats,
+				       ARRAY_SIZE(vk_plane_formats), NULL,
+				       DRM_PLANE_TYPE_PRIMARY, NULL);
+	if (ret)
+		return ret;
+	drm_plane_helper_add(&drv->plane, &vk_plane_helper_funcs);
+	drm_plane_enable_fb_damage_clips(&drv->plane);
+
+	crtc = drmm_crtc_alloc_with_planes(drm, struct vk_crtc_driver, crtc,
+					   &drv->plane, NULL, &vk_crtc_funcs,
+					   NULL);
+	if (IS_ERR(crtc))
+		return PTR_ERR(crtc);
+	drm_crtc_helper_add(&crtc->crtc, &vk_crtc_helper_funcs);
+	INIT_DELAYED_WORK(&crtc->timeout_flush, vk_timeout_atomic_submit);
+	drv->crtc = crtc;
+	for (size_t i = 0; i < ARRAY_SIZE(crtc->send_chunk); i++) {
+		ret = vk_send_chunk_init(drm, &crtc->send_chunk[i]);
+		if (ret)
+			return ret;
+	}
+
+	ret = drm_encoder_init(drm, &drv->encoder, &vk_encoder_funcs,
+			       DRM_MODE_ENCODER_NONE, NULL);
+	if (ret)
+		return ret;
+	drv->encoder.possible_crtcs = drm_crtc_mask(&crtc->crtc);
+
+	connector = drmm_kzalloc(drm, sizeof(struct vk_connector_driver),
+				 GFP_KERNEL);
+	if (!connector)
+		return -ENOMEM;
+	ret = drmm_connector_init(drm, &connector->connector,
+				  &vk_connector_funcs, DRM_MODE_CONNECTOR_USB,
+				  NULL);
+	if (ret)
+		return ret;
+	drm_connector_helper_add(&connector->connector,
+				 &vk_connector_helper_funcs);
+	connector->usb_dev = drv->dev;
+	connector->data_pipe =
+		usb_sndbulkpipe(drv->dev, drv->frame_epd->bEndpointAddress);
+	connector->device_info = drv->device_info;
+	ret = drm_connector_attach_encoder(&connector->connector,
+					   &drv->encoder);
+	if (ret)
+		return ret;
+	drv->connector = connector;
+
+	ret = drm_vblank_init(drm, 1);
+	if (ret)
+		return ret;
+
+	return drm_mode_config_create_initial_state(drm);
+}
+
+static int vk_init_usb(struct vk_driver *drv, struct usb_interface *intf)
+{
+	struct usb_host_interface *altsetting = intf->cur_altsetting;
+
+	drv->intf = intf;
+	usb_set_intfdata(intf, drv);
+
+	drv->dev = interface_to_usbdev(intf);
+
+	for (size_t i = 0; i < altsetting->desc.bNumEndpoints; i++) {
+		struct usb_endpoint_descriptor *epd =
+			&altsetting->endpoint[i].desc;
+		if (usb_endpoint_num(epd) == VK_FRAME_EP_NUMBER &&
+		    usb_endpoint_is_bulk_out(epd)) {
+			drv->frame_epd = epd;
+			break;
+		}
+	}
+	if (!drv->frame_epd) {
+		drm_err(&drv->drm, "Bulk OUT endpoint %u not found\n",
+			VK_FRAME_EP_NUMBER);
+		return -ENODEV;
+	}
+
+	drm_dbg_driver(&drv->drm,
+		       "Using bulk OUT endpoint %#x (max packet %u)\n",
+		       drv->frame_epd->bEndpointAddress,
+		       usb_endpoint_maxp(drv->frame_epd));
+
+	return 0;
+}
+
+static void vk_deinit_usb(struct vk_driver *drv)
+{
+	usb_set_intfdata(drv->intf, NULL);
+}
+
+static int vk_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+	struct vk_driver *drv = devm_drm_dev_alloc(&intf->dev, &vk_drm_driver,
+						   struct vk_driver, drm);
+	int ret;
+
+	if (IS_ERR(drv))
+		return dev_err_probe(&intf->dev, PTR_ERR(drv),
+				     "Failed to allocate DRM device\n");
+	mutex_init(&drv->io_lock);
+
+	ret = vk_init_usb(drv, intf);
+	if (ret)
+		goto fail;
+
+	mutex_lock(&drv->io_lock);
+
+	ret = vk_device_info_find_or_verify(drv);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to identify display: %d\n", ret);
+		goto fail_io;
+	}
+	drm_info(&drv->drm, "Detected %ux%u Valkyrie USB LCD (%u Hz)\n",
+		 drv->device_info->width, drv->device_info->height,
+		 drv->device_info->fps);
+
+	ret = vk_device_bootstrap(drv);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to initialize display: %d\n", ret);
+		goto fail_io;
+	}
+
+	drv->io_blocking = false;
+	mutex_unlock(&drv->io_lock);
+
+	ret = vk_init_drm(drv);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to initialize DRM resources: %d\n",
+			ret);
+		goto fail;
+	}
+	ret = drm_dev_register(&drv->drm, 0);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to register DRM device: %d\n", ret);
+		goto fail;
+	}
+	drm_client_setup(&drv->drm, NULL);
+
+	return 0;
+fail_io:
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+fail:
+	vk_deinit_usb(drv);
+	return ret;
+}
+
+static void vk_disconnect(struct usb_interface *intf)
+{
+	struct vk_driver *drv = usb_get_intfdata(intf);
+	int ret;
+
+	drm_dbg_driver(&drv->drm, "Disconnecting display\n");
+
+	drm_dev_get(&drv->drm);
+	drm_dev_unplug(&drv->drm);
+
+	mutex_lock(&drv->io_lock);
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+
+	cancel_delayed_work_sync(&drv->crtc->timeout_flush);
+
+	if (drv->drm.mode_config.suspend_state) {
+		drm_atomic_commit_put(drv->drm.mode_config.suspend_state);
+		drv->drm.mode_config.suspend_state = NULL;
+	}
+
+	drm_dev_put(&drv->drm);
+
+	mutex_lock(&drv->io_lock);
+	drv->io_blocking = true;
+	/* Re-enable the display so its observed ~10-second timeout resets it. */
+	ret = vk_device_reset(drv);
+	if (ret)
+		drm_dbg_driver(&drv->drm,
+			       "Display reset during disconnect failed: %d\n",
+			       ret);
+	mutex_unlock(&drv->io_lock);
+	vk_deinit_usb(drv);
+}
+
+static int __vk_resume_device(struct usb_interface *intf, bool rebootstrap,
+			      bool reidentify)
+{
+	struct vk_driver *drv = usb_get_intfdata(intf);
+	int ret = 0;
+
+	drm_dbg_driver(&drv->drm,
+		       "Resuming display (reinitialize=%d, reidentify=%d, DRM suspended=%d)\n",
+		       rebootstrap, reidentify, drv->drm_suspended);
+
+	mutex_lock(&drv->io_lock);
+
+	if (reidentify) {
+		ret = vk_device_info_find_or_verify(drv);
+		if (ret) {
+			drm_err(&drv->drm,
+				"Failed to verify display after USB reset: %d\n",
+				ret);
+			goto fail;
+		}
+	}
+
+	if (rebootstrap) {
+		ret = vk_device_bootstrap(drv);
+		if (ret) {
+			drm_err(&drv->drm,
+				"Failed to initialize display after USB reset: %d\n",
+				ret);
+			goto fail;
+		}
+	}
+
+	drv->io_blocking = false;
+	mutex_unlock(&drv->io_lock);
+
+	if (drv->drm_suspended) {
+		ret = drm_mode_config_helper_resume(&drv->drm);
+		drv->drm_suspended = false;
+		if (ret)
+			drm_err(&drv->drm, "Failed to restore DRM state: %d\n",
+				ret);
+	} else {
+		drm_dbg_driver(&drv->drm,
+			       "No saved DRM state; scheduling an immediate refresh\n");
+		schedule_delayed_work(&drv->crtc->timeout_flush, 0);
+	}
+
+	if (!ret)
+		drm_dbg_driver(&drv->drm, "Display I/O resumed\n");
+
+	return ret;
+
+fail:
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+	return ret;
+}
+
+static int __vk_suspend_device(struct usb_interface *intf, bool force)
+{
+	struct vk_driver *drv = usb_get_intfdata(intf);
+	int ret = 0;
+
+	drm_dbg_driver(&drv->drm,
+		       "Suspending display (force=%d, DRM suspended=%d)\n",
+		       force, drv->drm_suspended);
+
+	if (!drv->drm_suspended) {
+		ret = drm_mode_config_helper_suspend(&drv->drm);
+		if (ret)
+			drm_err(&drv->drm, "Failed to suspend DRM state: %d\n",
+				ret);
+		if (ret && !force)
+			return ret;
+		if (!ret)
+			drv->drm_suspended = true;
+	}
+
+	mutex_lock(&drv->io_lock);
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+	cancel_delayed_work_sync(&drv->crtc->timeout_flush);
+	drm_dbg_driver(&drv->drm, "Display I/O suspended\n");
+	return ret;
+}
+
+static int vk_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	return __vk_suspend_device(intf, true);
+}
+
+static int vk_resume(struct usb_interface *intf)
+{
+	return __vk_resume_device(intf, false, false);
+}
+
+static int vk_reset_resume(struct usb_interface *intf)
+{
+	return __vk_resume_device(intf, true, true);
+}
+
+static int vk_pre_reset(struct usb_interface *intf)
+{
+	return __vk_suspend_device(intf, false);
+}
+
+static int vk_post_reset(struct usb_interface *intf)
+{
+	return __vk_resume_device(intf, true, false);
+}
+
+static void
+__vk_conv_to_buf(struct vk_send_chunk *curr,
+		 struct drm_shadow_plane_state *plane_state, size_t *i,
+		 unsigned long *x, unsigned long *y,
+		 const struct vk_lcd_device_info *device_info,
+		 const struct drm_format_info *src_format_info,
+		 const struct drm_format_info *dst_format_info)
+{
+	struct drm_rect clip;
+	struct iosys_map dst;
+	unsigned int dst_pitch = device_info->width * dst_format_info->cpp[0];
+
+	while (*y < device_info->height && *i < VK_SEND_BUF_SIZE) {
+		size_t line_avail = device_info->width - *x,
+		       buf_avail = DIV_ROUND_DOWN_ULL(VK_SEND_BUF_SIZE - *i,
+						      dst_format_info->cpp[0]);
+		/* UYVY encodes pixels in pairs, so round down to an even count. */
+		size_t pixs_to_send = ALIGN_DOWN(min(line_avail, buf_avail),
+						 2);
+		size_t bytes_to_send = pixs_to_send * dst_format_info->cpp[0];
+
+		if (!bytes_to_send)
+			return;
+
+		clip.y1 = *y;
+		clip.y2 = *y + 1;
+		clip.x1 = *x;
+		clip.x2 = *x + pixs_to_send;
+		iosys_map_set_vaddr(&dst, curr->buf + *i);
+
+		switch (src_format_info->format) {
+		case DRM_FORMAT_XRGB8888:
+			drm_fb_xrgb8888_to_uyvy(&dst, &dst_pitch,
+						&plane_state->data[0],
+						plane_state->base.fb, &clip,
+						&plane_state->fmtcnv_state);
+			break;
+		case DRM_FORMAT_UYVY:
+			drm_fb_memcpy(&dst, &dst_pitch, &plane_state->data[0],
+				      plane_state->base.fb, &clip);
+			break;
+		}
+		*i += bytes_to_send;
+		if (clip.x2 != device_info->width) {
+			*x = clip.x2;
+		} else {
+			*x = 0;
+			(*y)++;
+		}
+	}
+}
+
+static void __vk_urb_finish_callback(struct urb *urb)
+{
+	struct vk_send_chunk *curr =
+		container_of(urb, struct vk_send_chunk, urb);
+	curr->errno = 0;
+	if (urb->transfer_buffer_length != urb->actual_length)
+		curr->errno = -EIO;
+	if (urb->status)
+		curr->errno = urb->status;
+	complete_all(&curr->flight);
+}
+
+static int __vk_wait_chunk_complete(struct vk_send_chunk *chunk,
+				    unsigned long timeout)
+{
+	int ret = 0;
+
+	if (!wait_for_completion_timeout(&chunk->flight, timeout)) {
+		ret = ret ? ret : -ETIMEDOUT;
+		usb_kill_urb(&chunk->urb);
+		complete_all(&chunk->flight);
+	}
+	if (chunk->errno)
+		ret = ret ? ret : chunk->errno;
+	return ret;
+}
+
+static int vk_scan_frame(struct vk_crtc_driver *crtc_drv,
+			 struct vk_connector_driver *connector_drv,
+			 struct drm_shadow_plane_state *plane_state)
+{
+	const struct vk_lcd_device_info *device_info =
+		connector_drv->device_info;
+	struct vk_send_chunk *curr = &crtc_drv->send_chunk[0],
+			     *next = &crtc_drv->send_chunk[1];
+	struct drm_device *drm = crtc_drv->crtc.dev;
+	struct drm_framebuffer *fb = plane_state->base.fb;
+	const struct drm_format_info *src_format_info = fb->format;
+	const struct drm_format_info *dst_format_info =
+		drm_get_format_info(drm, VK_FRAME_FORMAT,
+				    DRM_FORMAT_MOD_LINEAR);
+	unsigned long x = 0, y = 0;
+	const unsigned long wait_timeout =
+		msecs_to_jiffies(VK_SEND_CHUNK_TIMEOUT);
+	size_t i;
+	bool adding_header = true;
+	size_t trailer_i = 0;
+	int ret = 0;
+	int ret2 = 0;
+
+	lockdep_assert_held(&container_of(drm, struct vk_driver, drm)->io_lock);
+	curr->errno = 0;
+	next->errno = 0;
+	complete_all(&curr->flight);
+	complete_all(&next->flight);
+
+	ret = drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE);
+	if (ret) {
+		drm_dbg_ratelimited(drm,
+				    "Failed to begin CPU access to framebuffer %u: %d\n",
+				    fb->base.id, ret);
+		return ret;
+	}
+
+	while (trailer_i < ARRAY_SIZE(device_info->frame_trailer)) {
+		swap(curr, next);
+		i = 0;
+
+		if (adding_header) {
+			memcpy(curr->buf, device_info->frame_header,
+			       sizeof(device_info->frame_header));
+			i += sizeof(device_info->frame_header);
+			adding_header = false;
+		}
+		__vk_conv_to_buf(curr, plane_state, &i, &x, &y, device_info,
+				 src_format_info, dst_format_info);
+
+		if (y == device_info->height) {
+			size_t append_trailer_len =
+				min(ARRAY_SIZE(device_info->frame_trailer) -
+					    trailer_i,
+				    VK_SEND_BUF_SIZE - i);
+			memcpy(curr->buf + i,
+			       &device_info->frame_trailer[trailer_i],
+			       append_trailer_len);
+			i += append_trailer_len;
+			trailer_i += append_trailer_len;
+		}
+
+		usb_fill_bulk_urb(&curr->urb, connector_drv->usb_dev,
+				  connector_drv->data_pipe, curr->buf, i,
+				  __vk_urb_finish_callback, NULL);
+		ret = __vk_wait_chunk_complete(next, wait_timeout);
+		if (ret) {
+			drm_dbg_ratelimited(drm,
+					    "Bulk OUT transfer failed (%d/%d bytes): %d\n",
+					    next->urb.actual_length,
+					    next->urb.transfer_buffer_length, ret);
+			break;
+		}
+		reinit_completion(&curr->flight);
+		ret = usb_submit_urb(&curr->urb, GFP_KERNEL);
+		if (ret) {
+			drm_dbg_ratelimited(drm,
+					    "Failed to submit bulk OUT transfer (%zu bytes): %d\n",
+					    i, ret);
+			curr->errno = ret;
+			complete_all(&curr->flight);
+			break;
+		}
+	}
+	ret2 = __vk_wait_chunk_complete(curr, wait_timeout);
+	if (ret2)
+		drm_dbg_ratelimited(drm, "Last frame chunk failed (%d/%d bytes): %d\n",
+				    curr->urb.actual_length,
+				    curr->urb.transfer_buffer_length, ret2);
+	ret = ret ? ret : ret2;
+
+	/* The device expects a zero-length bulk transfer to terminate each frame. */
+	reinit_completion(&curr->flight);
+	usb_fill_bulk_urb(&curr->urb, connector_drv->usb_dev,
+			  connector_drv->data_pipe, NULL, 0,
+			  __vk_urb_finish_callback, NULL);
+	ret2 = usb_submit_urb(&curr->urb, GFP_KERNEL);
+	if (ret2) {
+		drm_dbg_ratelimited(drm, "Failed to submit frame terminator: %d\n", ret2);
+		curr->errno = ret2;
+		complete_all(&curr->flight);
+	}
+	ret2 = __vk_wait_chunk_complete(curr, wait_timeout);
+	if (ret2)
+		drm_dbg_ratelimited(drm, "Frame terminator failed: %d\n", ret2);
+	ret = ret ? ret : ret2;
+
+	drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
+	return ret;
+}
+
+/*
+ * Feature reports carry 8-byte commands. The helpers below encode observed
+ * register reads/writes and command-completion polling, including the
+ * big-endian address fields. The meaning of several command fields remains
+ * unknown.
+ */
+static int vk_feature_report(struct vk_driver *drv, const u8 request[8],
+			     u8 response[8])
+{
+	int ret;
+
+	lockdep_assert_held(&drv->io_lock);
+
+	ret = usb_control_msg_send(drv->dev, 0, HID_REQ_SET_REPORT,
+				   USB_DIR_OUT | USB_TYPE_CLASS |
+					   USB_RECIP_INTERFACE,
+				   HID_REPORT_TYPE_FEATURE << 8, 0, request, 8,
+				   USB_CTRL_SET_TIMEOUT, GFP_KERNEL);
+	if (ret) {
+		drm_dbg_ratelimited(&drv->drm, "SET_REPORT %8ph failed: %d\n",
+				    request, ret);
+		return ret;
+	}
+
+	if (response) {
+		ret = usb_control_msg_recv(drv->dev, 0, HID_REQ_GET_REPORT,
+					   USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
+					   HID_REPORT_TYPE_FEATURE << 8, 0, response, 8,
+					   USB_CTRL_GET_TIMEOUT, GFP_KERNEL);
+		if (ret)
+			drm_dbg_ratelimited(&drv->drm,
+					    "GET_REPORT for %8ph failed: %d\n",
+					    request, ret);
+	}
+
+	return ret;
+}
+
+static inline int vk_b5_read(struct vk_driver *drv, u16 addr, u8 *val)
+{
+	u8 buf[8] = { 0xB5, 0 };
+	u8 ans[8];
+	int ret;
+
+	put_unaligned_be16(addr, buf + 1);
+
+	ret = vk_feature_report(drv, buf, ans);
+	if (ret)
+		return ret;
+
+	if (memcmp(buf, ans, 3)) {
+		drm_dbg_ratelimited(&drv->drm,
+				    "Unexpected response reading register %#06x: %8ph\n",
+				    addr, ans);
+		return -EIO;
+	}
+
+	if (val)
+		*val = ans[3];
+	return 0;
+}
+
+static inline int vk_b6_write(struct vk_driver *drv, u16 addr, u8 val)
+{
+	u8 buf[8] = { 0xB6, 0 };
+
+	put_unaligned_be16(addr, buf + 1);
+	buf[3] = val;
+
+	return vk_feature_report(drv, buf, NULL);
+}
+
+static inline int vk_f5_read(struct vk_driver *drv, u32 addr, u8 val[8])
+{
+	u8 buf[8] = { 0xF5, 0 };
+
+	if (addr > 0xFFFFFF)
+		return -EINVAL;
+	put_unaligned_be32(addr, buf);
+	buf[0] = 0xF5;
+
+	return vk_feature_report(drv, buf, val);
+}
+
+static inline int vk_a6_command(struct vk_driver *drv, u8 cmd[8], u16 poll,
+				unsigned long poll_delay_us)
+{
+	int ret;
+
+	ret = vk_feature_report(drv, cmd, NULL);
+	if (ret)
+		return ret;
+
+	fsleep(poll_delay_us);
+
+	for (uint i = 0; i < VK_A6_POLL_MAX; i++) {
+		u8 val;
+
+		ret = vk_b5_read(drv, poll, &val);
+		if (ret)
+			return ret;
+		if (!val)
+			return 0;
+		fsleep(VK_A6_POLL_INTERVAL_US);
+	}
+
+	drm_dbg_ratelimited(&drv->drm,
+			    "Command %#04x timed out polling register %#06x\n",
+			    cmd[1], poll);
+	return -ETIMEDOUT;
+}
+
+static inline int vk_a601_set_screen(struct vk_driver *drv)
+{
+	u8 buf[8] = { 0xa6, 0x01, 0, 0, 0, 0, 0x22, 0x00 };
+
+	put_unaligned_be16(drv->device_info->width, buf + 2);
+	put_unaligned_be16(drv->device_info->height, buf + 4);
+
+	return vk_a6_command(drv, buf, 0xc555, 10 * 1000);
+}
+
+static inline int vk_a602_set_screen(struct vk_driver *drv)
+{
+	u8 buf[8] = { 0xa6, 0x02, 0 };
+
+	put_unaligned_be16(drv->device_info->video_code, buf + 2);
+	put_unaligned_be16(drv->device_info->width, buf + 4);
+	put_unaligned_be16(drv->device_info->height, buf + 6);
+
+	return vk_a6_command(drv, buf, 0xc557, 20 * 1000);
+}
+
+static inline int vk_a603_command(struct vk_driver *drv)
+{
+	u8 buf[8] = { 0xa6, 0x03, 0x03, 0 };
+
+	return vk_a6_command(drv, buf, 0xc558, 10 * 1000);
+}
+
+static inline int vk_a604_switch(struct vk_driver *drv, bool on)
+{
+	u8 buf[8] = { 0xa6, 0x04, on ? 1 : 0, 0 };
+	int ret;
+
+	ret = vk_a6_command(drv, buf, 0xc555, 10 * 1000);
+	if (ret)
+		return ret;
+
+	if (on) {
+		u8 f242;
+
+		ret = vk_b6_write(drv, 0xf24e, 0);
+		if (ret)
+			return ret;
+
+		ret = vk_b5_read(drv, 0xf242, &f242);
+		if (ret)
+			return ret;
+		if (f242 & BIT(3)) {
+			drm_dbg_ratelimited(&drv->drm,
+					    "Unexpected status %#04x in register 0xf242\n",
+					    f242);
+			return -EIO;
+		}
+	}
+
+	return 0;
+}
+
+static inline int vk_a605_switch(struct vk_driver *drv, bool on)
+{
+	u8 buf[8] = { 0xa6, 0x05, on ? 1 : 0, 0 };
+
+	return vk_a6_command(drv, buf, 0xc555, 10 * 1000);
+}
+
+static inline int vk_a607_switch(struct vk_driver *drv, bool on)
+{
+	u8 buf[8] = { 0xa6, 0x07, on ? 1 : 0, 0x02, 0 };
+	int ret;
+
+	if (on) {
+		ret = vk_b6_write(drv, 0xdeee, 1);
+		if (ret)
+			return ret;
+	}
+
+	return vk_a6_command(drv, buf, 0xc454, 100 * 1000);
+}
+
+static int vk_complex_switch(struct vk_driver *drv, bool on)
+{
+	int ret;
+	u8 val;
+
+	ret = vk_b5_read(drv, drv->device_info->complex_switch_addr, &val);
+	if (ret)
+		return ret;
+
+	if (drv->device_info->complex_switch_active_high ^ on)
+		val &= ~drv->device_info->complex_switch_mask;
+	else
+		val |= drv->device_info->complex_switch_mask;
+
+	return vk_b6_write(drv, drv->device_info->complex_switch_addr, val);
+}
+
+/*
+ * The sequence and ordering here follow the observed initialization and
+ * enable protocol. The purpose of some device-specific commands is unknown.
+ */
+static int vk_device_bootstrap(struct vk_driver *drv)
+{
+	int ret;
+
+	drm_dbg_driver(&drv->drm, "Initializing %ux%u display\n",
+		       drv->device_info->width, drv->device_info->height);
+
+	drv->device_state = false;
+
+	ret = vk_a603_command(drv);
+	if (ret)
+		return ret;
+
+	ret = vk_a607_switch(drv, true);
+	if (ret)
+		return ret;
+
+	ret = vk_a605_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_complex_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_a604_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_a603_command(drv);
+	if (ret)
+		return ret;
+
+	ret = vk_a601_set_screen(drv);
+	if (ret)
+		return ret;
+
+	return vk_a602_set_screen(drv);
+}
+
+static int vk_device_enable_pre(struct vk_driver *drv)
+{
+	drm_dbg_driver(&drv->drm, "Enabling display\n");
+	drv->device_state = true;
+	return vk_a604_switch(drv, true);
+}
+
+static int vk_device_enable_post(struct vk_driver *drv)
+{
+	int ret;
+
+	ret = vk_complex_switch(drv, true);
+	if (ret)
+		return ret;
+
+	return vk_a605_switch(drv, true);
+}
+
+static int vk_device_disable(struct vk_driver *drv)
+{
+	int ret;
+
+	drm_dbg_driver(&drv->drm, "Disabling display\n");
+
+	drv->device_state = false;
+
+	ret = vk_a605_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_complex_switch(drv, false);
+	if (ret)
+		return ret;
+
+	return vk_a604_switch(drv, false);
+}
+
+static int vk_device_reset(struct vk_driver *drv)
+{
+	int ret = 0;
+	int ret2;
+
+	/*
+	 * The device has no observed shutdown command. Enable it and let its
+	 * timeout reset it later; this function does not wait for that reset.
+	 */
+	ret = vk_device_bootstrap(drv);
+	ret2 = vk_device_enable_pre(drv);
+	ret = ret ? ret : ret2;
+	if (!ret2)
+		ret2 = vk_device_enable_post(drv);
+	return ret ? ret : ret2;
+}
+
+/* Known panel identification data and observed frame markers. */
+static const struct vk_lcd_device_info vk_lcd_devices[] = {
+	{ .serial = "\x5a\xa5\x1aMCVKA080320*0240S244613682\xff\xff\xff",
+	  .label_0030 = { 0x02, 0x05 },
+	  .label_f000 = { 0xb7, 0x16, 0x0a },
+
+	  .width = 320,
+	  .height = 240,
+	  .fps = 30,
+	  .width_mm = 39,
+	  .height_mm = 28,
+	  .video_code = 0x9400,
+	  .complex_switch_addr = 0xf507,
+	  .complex_switch_mask = BIT(1),
+	  .complex_switch_active_high = false,
+	  .frame_header = { 0xff, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0xf0 },
+	  .frame_trailer = { 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } },
+	{ .serial = "\x5a\xa5\x1aMCVKC030360*0960S252309025\xff\xff\xff",
+	  .label_0030 = { 0x02, 0x06 },
+	  .label_f000 = { 0xb7, 0x16, 0x0a },
+
+	  .width = 360,
+	  .height = 960,
+	  .fps = 30,
+	  .width_mm = 54,
+	  .height_mm = 144,
+	  .video_code = 0xa000,
+	  .complex_switch_addr = 0xf005,
+	  .complex_switch_mask = BIT(4),
+	  .complex_switch_active_high = true,
+	  .frame_header = { 0xff, 0x00, 0x00, 0x00, 0x00, 0x16, 0x83, 0xc0 },
+	  .frame_trailer = { 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } },
+};
+
+static int vk_device_info_find_or_verify(struct vk_driver *drv)
+{
+	u8 serial[32] __nonstring;
+	u8 label_0030[2];
+	u8 label_f000[3];
+	int ret;
+
+	for (size_t i = 0; i < 2; i++) {
+		ret = vk_b5_read(drv, 0x0030 + i, &label_0030[i]);
+		if (ret)
+			return ret;
+	}
+	for (size_t i = 0; i < 3; i++) {
+		ret = vk_b5_read(drv, 0xf000 + i, &label_f000[i]);
+		if (ret)
+			return ret;
+	}
+	for (size_t i = 0; i < 4; i++) {
+		ret = vk_f5_read(drv, VK_SERIAL_ADDR + (i * 8),
+				 serial + (i * 8));
+		if (ret)
+			return ret;
+	}
+
+	drm_dbg_driver(&drv->drm,
+		       "Panel ID: 0030=%2ph f000=%3ph identification=%32ph\n",
+		       label_0030, label_f000, serial);
+
+	for (size_t i = drv->device_info ? drv->device_info - vk_lcd_devices :
+					   0;
+	     i < ARRAY_SIZE(vk_lcd_devices); i++) {
+		if (memcmp(vk_lcd_devices[i].label_0030, label_0030,
+			   sizeof(label_0030)) ||
+		    memcmp(vk_lcd_devices[i].label_f000, label_f000,
+			   sizeof(label_f000)) ||
+		    memcmp(vk_lcd_devices[i].serial, serial, sizeof(serial))) {
+			if (drv->device_info) {
+				drm_dbg_driver(&drv->drm,
+					       "Panel identification changed after USB reset\n");
+				return -ENODEV;
+			}
+		} else {
+			drv->device_info = &vk_lcd_devices[i];
+			return 0;
+		}
+	}
+	drm_dbg_driver(&drv->drm, "Unsupported panel identification\n");
+	return -ENODEV;
+}
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH 2/3] HID: quirks: Ignore Valkyrie USB LCD devices
  2026-10-28 13:25 ` [PATCH 2/3] HID: quirks: Ignore Valkyrie USB LCD devices ZiAo Wang
@ 2026-10-28 13:41   ` Jie Feng
  0 siblings, 0 replies; 6+ messages in thread
From: Jie Feng @ 2026-10-28 13:41 UTC (permalink / raw)
  To: ZiAo Wang
  Cc: dri-devel, maarten.lankhorst, mripard, tzimmermann, airlied,
	simona, jikos, bentiss, linux-input, linux-kernel

On Wed, 28 Oct 2026 at 21:26, ZiAo Wang <ziaowang2007@gmail.com> wrote:
>
> Valkyrie USB LCD devices (0x345f:0x9132) expose a HID interface
> for vendor-specific display control commands and a separate USB
> interface for bulk framebuffer transfers.
>
> The HID interface is not intended for input and should be managed
> by the dedicated USB display driver instead of the HID subsystem.
>
> Add the device to the HID quirk table with HID_QUIRK_IGNORE to
> prevent the HID driver from binding to it.
>
> Signed-off-by: ZiAo Wang <ziaowang2007@gmail.com>
> ---
>  drivers/hid/hid-ids.h    | 3 +++
>  drivers/hid/hid-quirks.c | 1 +
>  2 files changed, 4 insertions(+)
>
> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
> index 91319aa651..d135ea8f06 100644
> --- a/drivers/hid/hid-ids.h
> +++ b/drivers/hid/hid-ids.h
> @@ -1704,4 +1704,7 @@
>  #define USB_VENDOR_ID_HUAWEI                   0x12d1
>  #define USB_DEVICE_ID_HUAWEI_CD30KBD           0x109b
>
> +#define USB_VENDOR_ID_VALKYRIE                 0x345f
> +#define USB_PRODUCT_ID_VALKYRIE_LCD            0x9132
This VID and PID pair is used by all macrosilicon usb display adapters,
so it wouldn't just affect this product.

It also doesn't seem to negatively affect the device and is
useful for debugging, so I wouldn't recommend ignoring it.

>  #endif
> diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
> index 793db001a6..70ada7320c 100644
> --- a/drivers/hid/hid-quirks.c
> +++ b/drivers/hid/hid-quirks.c
> @@ -226,6 +226,7 @@ static const struct hid_device_id hid_quirks[] = {
>         { HID_USB_DEVICE(USB_VENDOR_ID_YJXCHIP, USB_DEVICE_ID_YJXCHIP_MOUSE),
>                 HID_QUIRK_ALWAYS_POLL },
>         { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_GROUP_AUDIO), HID_QUIRK_NOGET },
> +       { HID_USB_DEVICE(USB_VENDOR_ID_VALKYRIE, USB_PRODUCT_ID_VALKYRIE_LCD), HID_QUIRK_IGNORE },
>
>         { 0 }
>  };
> --
> 2.56.0
>

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 3/3] drm/tiny: add driver for Valkyrie USB LCD displays
  2026-10-28 13:25 ` [PATCH 3/3] drm/tiny: add driver for Valkyrie USB LCD displays ZiAo Wang
@ 2026-10-28 14:02   ` sashiko-bot
  0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-10-28 14:02 UTC (permalink / raw)
  To: ZiAo Wang, dri-devel; +Cc: linux-input, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] drm/tiny: missing drm_atomic_helper_shutdown() in vk_disconnect()
- [Medium] drm/tiny: missing drm_dev_set_dma_dev() in vk_probe()
- [Medium] drm/tiny: vk_crtc_atomic_check() skips validation when CRTC is inactive
- [Medium] drm/tiny: vk_device_enable_pre() leaves device_state true on failure
- [Medium] drm/tiny: valkyrie-lcd: broken resume recovery when drm suspend fails
--

commit 172f750ba1a198c982441dc0256107aec140e020
Author: ZiAo Wang <ziaowang2007@gmail.com>

drm/tiny: add driver for Valkyrie USB LCD displays

This commit adds a DRM/KMS driver for Valkyrie USB LCD displays (320x240 and
360x960 variants) using GEM shmem buffers, atomic modesetting, USB control
transfers for panel initialization, and bulk OUT transfers for UYVY frame
streaming.

> diff --git a/drivers/gpu/drm/tiny/valkyrie-lcd.c b/drivers/gpu/drm/tiny/valkyrie-lcd.c
> new file mode 100644
> index 0000000000000..6478e9246263b
> --- /dev/null
> +++ b/drivers/gpu/drm/tiny/valkyrie-lcd.c
[ ... ]
> +static int vk_plane_atomic_check(struct drm_plane *plane,
> +				 struct drm_atomic_commit *state)
> +{
> +	struct drm_plane_state *new_plane_state =
> +		drm_atomic_get_new_plane_state(state, plane);
> +	struct drm_crtc_state *new_crtc_state;
> +	int ret;
> +
> +	if (!new_plane_state->fb)
> +		return 0;
> +
> +	if (!new_plane_state->crtc)
> +		return -EINVAL;
> +	new_crtc_state =
> +		drm_atomic_get_new_crtc_state(state, new_plane_state->crtc);
> +	if (!new_crtc_state)
> +		return -EINVAL;
> +
> +	ret = drm_atomic_helper_check_plane_state(new_plane_state,
> +						  new_crtc_state,
> +						  DRM_PLANE_NO_SCALING,
> +						  DRM_PLANE_NO_SCALING, false,
> +						  false);
> +	if (ret)
> +		return ret;
> +
> +	if (new_plane_state->rotation != DRM_MODE_ROTATE_0) {
> +		drm_dbg_kms(plane->dev, "Unsupported plane rotation %#x\n",
> +			    new_plane_state->rotation);
> +		return -EINVAL;
> +	}
> +	return 0;
> +}
[ ... ]
> +static int vk_crtc_atomic_check(struct drm_crtc *crtc,
> +				struct drm_atomic_commit *state)
> +{
> +	struct vk_driver *drv = container_of(crtc->dev, struct vk_driver, drm);
> +	struct drm_crtc_state *new_crtc_state =
> +		drm_atomic_get_new_crtc_state(state, crtc);
> +	struct drm_plane *plane = &drv->plane;
> +	struct vk_connector_driver *connector_drv = drv->connector;
> +	struct drm_plane_state *new_plane_state;
> +
> +	if (!new_crtc_state->enable || !new_crtc_state->active)
> +		return 0;
> +
> +	if (new_crtc_state->plane_mask != drm_plane_mask(plane) ||
> +	    new_crtc_state->connector_mask !=
> +		    drm_connector_mask(&connector_drv->connector)) {
> +		drm_dbg_kms(crtc->dev,
> +			    "Active CRTC requires the primary plane and USB connector\n");
> +		return -EINVAL;
> +	}
> +
> +	new_plane_state = drm_atomic_get_new_plane_state(state, plane);
> +	if (!new_plane_state)
> +		new_plane_state = plane->state;
> +	if (!new_plane_state || new_plane_state->crtc != crtc ||
> +	    !new_plane_state->fb) {
> +		drm_dbg_kms(crtc->dev,
> +			    "Active CRTC has no framebuffer on its primary plane\n");
> +		return -EINVAL;
> +	}

[Severity: Medium]
Should vk_crtc_atomic_check() only return early when !new_crtc_state->enable,
rather than also skipping validation when !new_crtc_state->active?

According to the struct drm_crtc_state documentation, drivers must not consult
new_crtc_state->active in atomic_check to decide whether shared resources are
required because userspace expects a subsequent DPMS ON transition to succeed.

When the CRTC is enabled but inactive (enable is true, active is false),
returning early here allows userspace (for example via DRM_IOCTL_MODE_RMFB or
DRM_IOCTL_MODE_SETPLANE) to detach the primary plane and its framebuffer while
leaving the CRTC enabled with plane_mask == 0. Any subsequent commit setting
active back to true will then fail in vk_crtc_atomic_check() with -EINVAL.

Also, in vk_plane_atomic_check(), returning 0 before calling
drm_atomic_helper_check_plane_state() when !new_plane_state->fb skips clearing
new_plane_state->visible to false when disabling the plane.

[ ... ]
> +	if (!new_enable) {
> +		if (drv->device_state)
> +			ret = vk_device_disable(drv);
> +	} else {
> +		bool enabling = !drv->device_state;
> +
> +		/* Testing found a ~10-second timeout; refresh every 3 seconds. */
> +		mod_delayed_work(system_dfl_wq, &crtc_drv->timeout_flush,
> +				 msecs_to_jiffies(VK_TIMEOUT_FLUSH_INTERVAL_MS));
> +		if (enabling)
> +			ret = vk_device_enable_pre(drv);
> +		ret2 = vk_scan_frame(crtc_drv, connector_drv,
> +				     to_drm_shadow_plane_state(new_plane_state));
> +		ret = ret ? ret : ret2;
> +		if (enabling) {
> +			ret2 = vk_device_enable_post(drv);
> +			ret = ret ? ret : ret2;
> +		}
> +	}

[Severity: Medium]
What happens in vk_crtc_atomic_flush() if vk_device_enable_pre(),
vk_scan_frame(), or vk_device_enable_post() fails when enabling is true?

In vk_device_enable_pre():

	drv->device_state = true;
	return vk_a604_switch(drv, true);

drv->device_state is unconditionally set to true before vk_a604_switch() and
the rest of the enable sequence succeed. If a transient USB error or poll
timeout occurs during vk_device_enable_pre(), vk_crtc_atomic_flush() still
proceeds to call vk_scan_frame() and vk_device_enable_post(), and leaves
drv->device_state set to true. Because drv->device_state stays true,
subsequent page flips and 3-second timeout_flush refreshes will see enabling
as false and will never retry the enable sequence.

Similarly, in vk_device_reset(), vk_device_enable_pre() is called
unconditionally even when vk_device_bootstrap() returns an error.

[ ... ]
> +static int vk_probe(struct usb_interface *intf, const struct usb_device_id *id)
> +{
> +	struct vk_driver *drv = devm_drm_dev_alloc(&intf->dev, &vk_drm_driver,
> +						   struct vk_driver, drm);
> +	int ret;
> +
> +	if (IS_ERR(drv))
> +		return dev_err_probe(&intf->dev, PTR_ERR(drv),
> +				     "Failed to allocate DRM device\n");
> +	mutex_init(&drv->io_lock);
> +
> +	ret = vk_init_usb(drv, intf);
> +	if (ret)
> +		goto fail;

[Severity: Medium]
Does vk_probe() need to configure the DMA device on drv->drm via
usb_intf_get_dma_device(intf) and drm_dev_set_dma_dev()?

vk_drm_driver uses DRM_GEM_SHMEM_DRIVER_OPS, which wires up PRIME DMA-BUF
import via drm_gem_prime_import() -> drm_gem_prime_import_dev(dev, dma_buf,
drm_dev_dma_dev(dev)). Because drv->drm is allocated with &intf->dev as its
parent and a struct usb_interface device has no dma_mask, importing a DMA-BUF
via DRM_IOCTL_PRIME_FD_TO_HANDLE will pass &intf->dev to
dma_buf_map_attachment(), triggering a WARN_ON_ONCE(!dev->dma_mask) and
failing with -EINVAL.

[ ... ]
> +static void vk_disconnect(struct usb_interface *intf)
> +{
> +	struct vk_driver *drv = usb_get_intfdata(intf);
> +	int ret;
> +
> +	drm_dbg_driver(&drv->drm, "Disconnecting display\n");
> +
> +	drm_dev_get(&drv->drm);
> +	drm_dev_unplug(&drv->drm);
> +
> +	mutex_lock(&drv->io_lock);
> +	drv->io_blocking = true;
> +	mutex_unlock(&drv->io_lock);
> +
> +	cancel_delayed_work_sync(&drv->crtc->timeout_flush);
> +
> +	if (drv->drm.mode_config.suspend_state) {
> +		drm_atomic_commit_put(drv->drm.mode_config.suspend_state);
> +		drv->drm.mode_config.suspend_state = NULL;
> +	}
> +
> +	drm_dev_put(&drv->drm);

[Severity: High]
Can skipping drm_atomic_helper_shutdown(&drv->drm) in vk_disconnect() leak the
active shadow-plane vmap and pinned shmem pages on unplug?

When the display is active, drm_gem_begin_shadow_fb_access() vmaps the
framebuffer (incrementing shmem->vmap_use_count, shmem->pages_pin_count, and
shmem->pages_use_count), and vk_crtc_atomic_enable() enables the CRTC vblank
timer. The shadow-plane vmap is only released by drm_gem_end_shadow_fb_access()
when a subsequent atomic commit disables or replaces the plane state.

Because vk_disconnect() never calls drm_atomic_helper_shutdown(&drv->drm), if
the active framebuffer is not explicitly removed by drm_mode_rmfb() before the
final drm_dev_put() (for example when a compositor has called
DRM_IOCTL_MODE_CLOSEFB on its frontbuffer, or when fbdev emulation is not
enabled), drm_mode_config_cleanup() destroys the active shadow_plane_state via
drm_gem_destroy_shadow_plane_state() without calling
drm_gem_end_shadow_fb_access(). When the framebuffer is freed,
drm_gem_shmem_release() hits drm_WARN_ONs for non-zero vmap_use_count,
pages_use_count, and pages_pin_count, leaking the kernel vmap and backing
pages, and drm_vblank_init_release() also warns that vblank->enabled is still
set.

In addition, if vk_timeout_atomic_submit() queued a non-blocking atomic commit
just before cancel_delayed_work_sync(&drv->crtc->timeout_flush), its
commit_work is not drained without drm_atomic_helper_shutdown() before
vk_device_reset() and teardown run.

[ ... ]
> +static int __vk_resume_device(struct usb_interface *intf, bool rebootstrap,
> +			      bool reidentify)
> +{
> +	struct vk_driver *drv = usb_get_intfdata(intf);
> +	int ret = 0;
[ ... ]
> +	drv->io_blocking = false;
> +	mutex_unlock(&drv->io_lock);
> +
> +	if (drv->drm_suspended) {
> +		ret = drm_mode_config_helper_resume(&drv->drm);
> +		drv->drm_suspended = false;
> +		if (ret)
> +			drm_err(&drv->drm, "Failed to restore DRM state: %d\n",
> +				ret);
> +	} else {
> +		drm_dbg_driver(&drv->drm,
> +			       "No saved DRM state; scheduling an immediate refresh\n");
> +		schedule_delayed_work(&drv->crtc->timeout_flush, 0);
> +	}
[ ... ]
> +static int __vk_suspend_device(struct usb_interface *intf, bool force)
> +{
> +	struct vk_driver *drv = usb_get_intfdata(intf);
> +	int ret = 0;
> +
> +	drm_dbg_driver(&drv->drm,
> +		       "Suspending display (force=%d, DRM suspended=%d)\n",
> +		       force, drv->drm_suspended);
> +
> +	if (!drv->drm_suspended) {
> +		ret = drm_mode_config_helper_suspend(&drv->drm);
> +		if (ret)
> +			drm_err(&drv->drm, "Failed to suspend DRM state: %d\n",
> +				ret);
> +		if (ret && !force)
> +			return ret;
> +		if (!ret)
> +			drv->drm_suspended = true;
> +	}
> +
> +	mutex_lock(&drv->io_lock);
> +	drv->io_blocking = true;
> +	mutex_unlock(&drv->io_lock);
> +	cancel_delayed_work_sync(&drv->crtc->timeout_flush);
> +	drm_dbg_driver(&drv->drm, "Display I/O suspended\n");
> +	return ret;
> +}

[Severity: Medium]
In __vk_suspend_device(), if force is true (from vk_suspend()) and
drm_mode_config_helper_suspend() fails while the display is active, can the
display recover on vk_resume()?

When drm_mode_config_helper_suspend() fails, vk_device_disable() is not
called, so the panel remains enabled with drv->device_state == true and
drv->drm_suspended == false while timeout_flush is cancelled across suspend.
Once the panel's ~10-second hardware watchdog expires during suspend and
resets the device, vk_resume() calls __vk_resume_device(intf, false, false)
and takes the !drv->drm_suspended fallback branch to schedule timeout_flush.
However, when timeout_flush runs vk_crtc_atomic_flush(), drv->device_state is
still true, so enabling (!drv->device_state) is false and neither
vk_device_bootstrap() nor vk_device_enable_pre() / vk_device_enable_post() is
executed.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261028132536.375927-1-ziaowang2007@gmail.com?part=3

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-10-28 14:02 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-28 13:25 [PATCH 0/3] Add support for Valkyrie USB LCD displays ZiAo Wang
2026-10-28 13:25 ` [PATCH 1/3] drm/format-helper: Add XRGB8888 to UYVY conversion ZiAo Wang
2026-10-28 13:25 ` [PATCH 2/3] HID: quirks: Ignore Valkyrie USB LCD devices ZiAo Wang
2026-10-28 13:41   ` Jie Feng
2026-10-28 13:25 ` [PATCH 3/3] drm/tiny: add driver for Valkyrie USB LCD displays ZiAo Wang
2026-10-28 14:02   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox