diff --git a/drivers/gpu/drm/mud/Kconfig b/drivers/gpu/drm/mud/Kconfig
index 440e994ca0a2..b3c6d073cc9c 100644
--- a/drivers/gpu/drm/mud/Kconfig
+++ b/drivers/gpu/drm/mud/Kconfig
@@ -13,3 +13,6 @@ config DRM_MUD
 	help
 	 This is a KMS driver for Multifunction USB Device displays or display
 	 adapters.
+
+config DRM_MUD_GADGET
+	tristate
diff --git a/drivers/gpu/drm/mud/Makefile b/drivers/gpu/drm/mud/Makefile
index d5941d33bcd9..56d2c39ac0eb 100644
--- a/drivers/gpu/drm/mud/Makefile
+++ b/drivers/gpu/drm/mud/Makefile
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
 obj-$(CONFIG_DRM_MUD)			+= mud_drm.o
+obj-$(CONFIG_DRM_MUD_GADGET)		+= mud_drm_gadget.o
diff --git a/drivers/gpu/drm/mud/mud_drm_gadget.c b/drivers/gpu/drm/mud/mud_drm_gadget.c
new file mode 100644
index 000000000000..9395d8b7cefe
--- /dev/null
+++ b/drivers/gpu/drm/mud/mud_drm_gadget.c
@@ -0,0 +1,889 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2020 Noralf Trønnes
+ */
+
+#include <linux/lz4.h>
+#include <linux/module.h>
+#include <linux/regmap_usb.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include <drm/drm_client.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_crtc.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_mode_object.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_rect.h>
+
+#include "mud_drm.h"
+
+/* Temporary debugging aid */
+static unsigned int debug = 8;
+
+#define pdebug(level, fmt, ...)				\
+	if (level <= debug)				\
+		printk(KERN_DEBUG fmt, ##__VA_ARGS__)
+
+struct mud_drm_gadget_connector {
+	struct drm_connector *connector;
+	u32 capabilities;
+	enum drm_connector_status status;
+	unsigned int width_mm;
+	unsigned int height_mm;
+	void *edid;
+	size_t edid_len;
+	struct mud_drm_display_mode *modes;
+	unsigned int num_modes;
+};
+
+struct mud_drm_gadget {
+	struct drm_client_dev client;
+
+	struct mud_drm_gadget_connector *connectors;
+	unsigned int connector_count;
+
+	const u32 *formats;
+	unsigned int format_count;
+
+	struct drm_connector *current_connector;
+	struct mud_drm_display_mode current_mode;
+	u32 current_format;
+
+	unsigned int rect_x;
+	unsigned int rect_y;
+	unsigned int rect_width;
+	unsigned int rect_height;
+
+	struct drm_client_buffer *buffer;
+	struct drm_client_buffer *buffer_check;
+	bool check_ok;
+
+	size_t max_transfer_size;
+	void *work_buf;
+};
+
+static int mud_drm_gadget_probe_connector(struct mud_drm_gadget_connector *mconn)
+{
+	struct drm_connector *connector = mconn->connector;
+	struct drm_device *dev = connector->dev;
+	struct drm_display_mode *mode;
+	unsigned int i = 0;
+	int ret = 0;
+	void *edid;
+
+	pdebug(2, "%s:\n", __func__);
+
+	mutex_lock(&dev->mode_config.mutex);
+
+	connector->funcs->fill_modes(connector,
+				     dev->mode_config.max_width,
+				     dev->mode_config.max_height);
+
+	mconn->width_mm = connector->display_info.width_mm;
+	mconn->height_mm = connector->display_info.height_mm;
+	mconn->status = connector->status;
+
+	mconn->num_modes = 0;
+	list_for_each_entry(mode, &connector->modes, head)
+		mconn->num_modes++;
+
+	pdebug(2, "    num_modes=%u\n", mconn->num_modes);
+
+	if (!mconn->num_modes)
+		goto unlock;
+
+	// FIXME: Checkpatch complains: Reusing the krealloc arg is almost always a bug
+	mconn->modes = krealloc(mconn->modes, mconn->num_modes * sizeof(*mconn->modes), GFP_KERNEL);
+	if (!mconn->modes) {
+		ret = -ENOMEM;
+		mconn->num_modes = 0;
+		goto unlock;
+	}
+
+	list_for_each_entry(mode, &connector->modes, head) {
+		pdebug(2, "    Modeline " DRM_MODE_FMT "\n", DRM_MODE_ARG(mode));
+		mud_drm_from_display_mode(&mconn->modes[i++], mode);
+	}
+
+	if (!connector->edid_blob_ptr)
+		goto unlock;
+
+	edid = connector->edid_blob_ptr->data;
+	mconn->edid_len = connector->edid_blob_ptr->length;
+	pdebug(2, "    edid_len=%zu\n", mconn->edid_len);
+	if (!mconn->edid_len || !edid) {
+		mconn->edid_len = 0;
+		goto unlock;
+	}
+
+	kfree(mconn->edid);
+	mconn->edid = kmemdup(edid, mconn->edid_len, GFP_KERNEL);
+	if (!mconn->edid) {
+		ret = -ENOMEM;
+		mconn->edid_len = 0;
+		goto unlock;
+	}
+
+unlock:
+	mutex_unlock(&dev->mode_config.mutex);
+
+	return ret;
+}
+
+static void mud_drm_gadget_probe_connectors(struct mud_drm_gadget *mdg)
+{
+	unsigned int i;
+
+	for (i = 0; i < mdg->connector_count; i++)
+		mud_drm_gadget_probe_connector(&mdg->connectors[i]);
+}
+
+static int mud_drm_gadget_read_connector(struct mud_drm_gadget *mdg, unsigned int regnr,
+					 void *buf, size_t len)
+{
+	struct mud_drm_gadget_connector *mconn;
+	size_t count = len / sizeof(u32);
+	struct drm_connector *connector;
+	unsigned int index, basereg, i;
+	__le32 *buf32 = buf;
+	size_t offset;
+	u32 val;
+
+	if (regnr < MUD_DRM_REG_CONNECTOR_EDID) {
+		index = (regnr - MUD_DRM_REG_CONNECTOR_TYPE) % MUD_DRM_MAX_CONNECTORS;
+		basereg = regnr - index;
+	} else if (regnr <= MUD_DRM_REG_CONNECTOR_EDID_MAX) {
+		index = (regnr - MUD_DRM_REG_CONNECTOR_EDID) % MUD_DRM_CONNECTOR_EDID_BLOCK_MAX;
+		basereg = MUD_DRM_REG_CONNECTOR_EDID;
+	} else if (regnr <= MUD_DRM_REG_CONNECTOR_MODES_MAX) {
+		index = (regnr - MUD_DRM_REG_CONNECTOR_MODES) % MUD_DRM_CONNECTOR_MODES_BLOCK_MAX;
+		basereg = MUD_DRM_REG_CONNECTOR_MODES;
+	} else {
+		return -EINVAL;
+	}
+
+	pdebug(3, "%s: connector index=%u\n", __func__, index);
+
+	if (index >= mdg->connector_count)
+		return -EINVAL;
+
+	mconn = &mdg->connectors[index];
+	connector = mconn->connector;
+
+	switch (basereg) {
+	case MUD_DRM_REG_CONNECTOR_TV_MODE:
+		// drm_atomic_connector_get_property()
+		drm_modeset_lock(&connector->dev->mode_config.connection_mutex, NULL);
+		val = connector->state ? connector->state->tv.mode : 0;
+		if (!connector->state)
+			pr_err("No Connector state!!!\n");
+		pdebug(4, "    MUD_DRM_REG_CONNECTOR_TV_MODE=%u\n", connector->state->tv.mode);
+		drm_modeset_unlock(&connector->dev->mode_config.connection_mutex);
+		break;
+	case MUD_DRM_REG_CONNECTOR_MODES_COUNT:
+		val = mconn->num_modes;
+		break;
+	case MUD_DRM_REG_CONNECTOR_EDID_LEN:
+		val = mconn->edid_len;
+		break;
+	case MUD_DRM_REG_CONNECTOR_STATUS:
+		val = mconn->status;
+		break;
+	case MUD_DRM_REG_CONNECTOR_CAPS:
+		val = mconn->capabilities;
+		break;
+	case MUD_DRM_REG_CONNECTOR_TYPE:
+		val = mconn->connector->connector_type;
+		break;
+
+	case MUD_DRM_REG_CONNECTOR_EDID:
+		offset = regnr - MUD_DRM_REG_CONNECTOR_EDID - (index * MUD_DRM_CONNECTOR_EDID_BLOCK_MAX);
+
+		pdebug(4, "    MUD_DRM_REG_CONNECTOR_EDID: offset=%zu\n", offset);
+
+		if ((offset + len) > mconn->edid_len)
+			return -EINVAL;
+		memcpy(buf, mconn->edid + offset, len);
+		return 0;
+
+	case MUD_DRM_REG_CONNECTOR_MODES:
+		offset = regnr - MUD_DRM_REG_CONNECTOR_MODES - (index * MUD_DRM_CONNECTOR_MODES_BLOCK_MAX);
+
+		pdebug(4, "    MUD_DRM_REG_CONNECTOR_MODES: offset=%zu, count=%zu\n", offset, count);
+
+		if (offset + count > mconn->num_modes * MUD_DRM_DISPLAY_MODE_FIELDS)
+			return -EINVAL;
+
+		for (i = 0; i < count; i++)
+			buf32[i] = cpu_to_le32(((u32 *)mconn->modes)[i]);
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+
+	if (count != 1)
+		return -EINVAL;
+
+	*buf32 = cpu_to_le32(val);
+
+	return 0;
+}
+
+static int drm_mud_gadget_tv_mode_property(struct drm_device *drm, u32 *num_modes,
+					   const char **names)
+{
+	struct drm_property *property = drm->mode_config.tv_mode_property;
+	struct drm_property_enum *prop_enum;
+	char *buf;
+
+	if (!property)
+		return -EINVAL;
+
+	*num_modes = 0;
+	list_for_each_entry(prop_enum, &property->enum_list, head)
+		(*num_modes)++;
+
+	pdebug(3, "%s: *num_modes=%u\n", __func__, *num_modes);
+
+	if (!names)
+		return 0;
+
+	buf = kcalloc(*num_modes, DRM_PROP_NAME_LEN, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	*names = buf;
+
+	list_for_each_entry(prop_enum, &property->enum_list, head) {
+		strncpy(buf, prop_enum->name, DRM_PROP_NAME_LEN);
+		buf += 32;
+	}
+
+	return 0;
+}
+
+static bool mud_drm_gadget_check_buffer(struct mud_drm_gadget *mdg,
+					struct drm_client_buffer *buffer,
+					struct drm_display_mode *mode)
+{
+	struct drm_framebuffer *fb;
+
+	if (!buffer)
+		return false;
+
+	fb = buffer->fb;
+
+	return fb->format->format == mdg->current_format &&
+	       fb->width == mode->hdisplay && fb->height == mode->vdisplay;
+}
+
+static int mud_drm_gadget_check(struct mud_drm_gadget *mdg)
+{
+	struct drm_format_name_buf format_name;
+	struct drm_client_buffer *buffer;
+	struct drm_display_mode mode;
+	void *vaddr;
+	int ret;
+
+	memset(&mode, 0, sizeof(mode));
+	mud_drm_to_display_mode(&mode, &mdg->current_mode);
+
+	pdebug(3, "%s:\n", __func__);
+	pdebug(3, "        Format %s\n", drm_get_format_name(mdg->current_format, &format_name));
+	pdebug(3, "        Modeline " DRM_MODE_FMT "\n", DRM_MODE_ARG(&mode));
+
+	mdg->check_ok = false;
+
+	if (!mode.hdisplay || !mdg->current_format)
+		return -EINVAL;
+
+	if (mdg->buffer_check) {
+		drm_client_framebuffer_delete(mdg->buffer_check);
+		mdg->buffer_check = NULL;
+	}
+
+	if (!mud_drm_gadget_check_buffer(mdg, mdg->buffer, &mode)) {
+		buffer = drm_client_framebuffer_create(&mdg->client, mode.hdisplay, mode.vdisplay,
+						       mdg->current_format);
+		if (IS_ERR(buffer))
+			return PTR_ERR(buffer);
+
+		vaddr = drm_client_buffer_vmap(buffer);
+		if (IS_ERR(vaddr)) {
+			drm_client_framebuffer_delete(buffer);
+			return PTR_ERR(vaddr);
+		}
+
+		mdg->buffer_check = buffer;
+	} else {
+		buffer = mdg->buffer;
+	}
+
+	ret = drm_client_modeset_set(&mdg->client, mdg->current_connector, &mode, buffer->fb);
+	if (ret)
+		return ret;
+
+	//ret = drm_client_modeset_check(&mdg->client);
+
+	mdg->check_ok = true;
+
+	return 0;
+}
+
+static int mud_drm_gadget_commit(struct mud_drm_gadget *mdg)
+{
+	int ret;
+
+	pdebug(3, "%s: check_ok=%s\n", __func__, mdg->check_ok ? "true" : "false");
+
+	if (!mdg->check_ok)
+		return -EINVAL;
+
+	ret = drm_client_modeset_commit(&mdg->client);
+	if (ret)
+		return ret;
+
+	if (mdg->buffer_check) {
+		drm_client_framebuffer_delete(mdg->buffer);
+		mdg->buffer = mdg->buffer_check;
+		mdg->buffer_check = NULL;
+	}
+
+	return 0;
+}
+
+static size_t mud_drm_gadget_write_buffer_rgb565_to_xrgb8888(struct drm_client_buffer *buffer,
+							     const u16 *src16, size_t len,
+							     unsigned int x1, unsigned int x2,
+							     unsigned int y1, unsigned int y2)
+{
+	unsigned int dst_width = buffer->fb->width;
+	unsigned int src_width = x2 - x1;
+	unsigned int x, y;
+	u32 *dst32;
+	u16 val16;
+
+	pdebug(4, "%s: %ux%u+%u+%u\n", __func__, x2 - x1, y2 - y1, x1, y1);
+
+	/* Get the address, it's already mapped */
+	dst32 = drm_client_buffer_vmap(buffer);
+
+	for (y = y1; y < y2; y++) {
+		for (x = x1; x < x2; x++) {
+			val16 = src16[x + (y * src_width)];
+			dst32[x + (y * dst_width)] = ((val16 & 0xf800) << 8) |
+						     ((val16 & 0x07e0) << 5) |
+						     ((val16 & 0x001f) << 3);
+			len -= 2;
+			if (!len)
+				return 0;
+		}
+	}
+
+	return len;
+}
+
+static size_t mud_drm_gadget_write_buffer_memcpy(struct drm_client_buffer *buffer,
+						 const void *src, size_t len,
+						 unsigned int x1, unsigned int x2,
+						 unsigned int y1, unsigned int y2)
+{
+	unsigned int cpp = buffer->fb->format->cpp[0];
+	size_t dst_pitch = buffer->fb->pitches[0];
+	size_t src_pitch = (x2 - x1) * cpp;
+	void *dst;
+
+	pdebug(5, "%s: %ux%u+%u+%u\n", __func__, x2 - x1, y2 - y1, x1, y1);
+
+	/* Get the address, it's already mapped */
+	dst = drm_client_buffer_vmap(buffer);
+	dst += y1 * dst_pitch;
+	dst += x1 * cpp;
+
+	for (; y1 < y2 && len; y1++) {
+		src_pitch = min(src_pitch, len);
+		memcpy(dst, src, src_pitch);
+		src += src_pitch;
+		dst += dst_pitch;
+		len -= src_pitch;
+	}
+
+	return len;
+}
+
+static int mud_drm_gadget_write_buffer(struct mud_drm_gadget *mdg, const void *buf,
+				       size_t offset, size_t len, u8 compression)
+{
+	struct drm_client_buffer *buffer = mdg->buffer ? mdg->buffer : mdg->buffer_check;
+	unsigned int x1 = mdg->rect_x;
+	unsigned int x2 = x1 + mdg->rect_width;
+	unsigned int y1 = mdg->rect_y;
+	unsigned int y2 = y1 + mdg->rect_height;
+	struct drm_framebuffer *fb;
+	int ret;
+
+	if (!buffer)
+		return -ENOMEM;
+
+	fb = buffer->fb;
+
+	pdebug(2, "%s: [FB:%d] %ux%u+%u+%u\n", __func__, fb->base.id,
+	       mdg->rect_width, mdg->rect_height, mdg->rect_x, mdg->rect_y);
+
+	if (!x2 || !y2 || x2 > fb->width || y2 > fb->height)
+		return -EINVAL;
+
+	if (compression & REGMAP_USB_COMPRESSION_LZ4) {
+		bool direct = !x1 && !y1 && x2 == fb->width && y2 == fb->height &&
+			      (x2 * fb->format->cpp[0]) == fb->pitches[0];
+		void *dest = mdg->work_buf;
+
+		/* if src buffer fits dst buffer, decompress directly into dst */
+		if (direct)
+			dest = drm_client_buffer_vmap(buffer);
+
+		ret = LZ4_decompress_safe(buf, dest, len, mdg->max_transfer_size);
+		pdebug(4, "  decompress len=%zu, ret=%d\n", len, ret);
+		if (ret < 0)
+			return -EIO;
+
+		if (direct)
+			return 0;
+
+		buf = mdg->work_buf;
+		len = ret;
+	}
+
+	if (offset + len > (mdg->rect_width * mdg->rect_height * fb->format->cpp[0])) {
+		pr_err("%s: Buffer doesn't fit rectangle\n", __func__);
+		return -EINVAL;
+	}
+
+	/* offset and len are 32-bit aligned */
+
+	if (offset) {
+		unsigned int buf_cpp = fb->format->cpp[0];
+		unsigned int pix_offset = offset / buf_cpp;
+		unsigned int off_x1 = x1 + (pix_offset % buf_cpp);
+		size_t remain;
+
+		y1 += pix_offset / (x2 - x1);
+		pdebug(6, "    buf_cpp=%u, pix_offset=%u, off_x1=%u, x1=%u, y1=%u\n",
+		       buf_cpp, pix_offset, off_x1, x1, y1);
+		if (y1 >= y2)
+			return -EINVAL;
+
+		if (off_x1 != x1) {
+			remain = mud_drm_gadget_write_buffer_memcpy(buffer, buf, len,
+								    off_x1, x2, y1, y1 + 1);
+			if (!remain)
+				return 0;
+
+			buf += len - remain;
+			len = remain;
+			if (++y1 >= y2)
+				return -EINVAL;
+		}
+	}
+
+	len = mud_drm_gadget_write_buffer_memcpy(buffer, buf, len, x1, x2, y1, y2);
+	if (len)
+		pr_err("%s: Failed to write it all: %zu\n", __func__, len);
+
+	return len ? -EIO : 0;
+}
+
+static int mud_drm_gadget_disable_pipe(struct mud_drm_gadget *mdg)
+{
+	int ret;
+
+	pdebug(2, "%s: buffer=%px buffer_check=%px\n",
+	       __func__, mdg->buffer, mdg->buffer_check);
+
+	drm_client_modeset_set(&mdg->client, NULL, NULL, NULL);
+	ret = drm_client_modeset_commit(&mdg->client);
+	if (ret)
+		return ret;
+
+	drm_client_framebuffer_delete(mdg->buffer_check);
+	drm_client_framebuffer_delete(mdg->buffer);
+	mdg->buffer_check = NULL;
+	mdg->buffer = NULL;
+
+	return 0;
+}
+
+int mud_drm_gadget_writereg(struct mud_drm_gadget *mdg, unsigned int regnr,
+			    const void *buf, size_t len, u8 compression)
+{
+	const __le32 *buf32;
+	size_t count;
+	u32 val;
+	int ret;
+
+	pdebug(2, "%s: regnr=0x%02x, len=%zu\n", __func__, regnr, len);
+
+	if (regnr >= MUD_DRM_REG_BUFFER) {
+		size_t offset = (regnr - MUD_DRM_REG_BUFFER) * sizeof(u32);
+
+		pdebug(3, "    MUD_DRM_REG_BUFFER\n");
+		return mud_drm_gadget_write_buffer(mdg, buf, offset, len, compression);
+	}
+
+	if (compression & REGMAP_USB_COMPRESSION_LZ4) {
+		ret = LZ4_decompress_safe(buf, mdg->work_buf, len, mdg->max_transfer_size);
+		pdebug(4, "  decompress len=%zu, ret=%d\n", len, ret);
+		if (ret < 0)
+			return -EIO;
+
+		buf = mdg->work_buf;
+		len = ret;
+	}
+
+	count = len / sizeof(u32);
+	buf32 = buf;
+
+	while (count--) {
+		val = le32_to_cpup(buf32++);
+
+		if (regnr >= MUD_DRM_REG_SET_MODE && regnr <= MUD_DRM_REG_SET_MODE_MAX) {
+			((u32 *)&mdg->current_mode)[regnr - MUD_DRM_REG_SET_MODE] = val;
+		} else {
+			switch (regnr) {
+			case MUD_DRM_REG_PIPE_ENABLE:
+				pdebug(3, "    MUD_DRM_REG_PIPE_ENABLE: %u\n", val);
+				if (val == 0) {
+					ret = mud_drm_gadget_disable_pipe(mdg);
+					if (ret)
+						return ret;
+				}
+				break;
+
+			case MUD_DRM_REG_RECT_X:
+				mdg->rect_x = val;
+				break;
+			case MUD_DRM_REG_RECT_Y:
+				mdg->rect_y = val;
+				break;
+			case MUD_DRM_REG_RECT_WIDTH:
+				mdg->rect_width = val;
+				break;
+			case MUD_DRM_REG_RECT_HEIGHT:
+				mdg->rect_height = val;
+				break;
+
+			case MUD_DRM_REG_SET_CONNECTOR:
+				if (val >= mdg->connector_count)
+					return -EINVAL;
+
+				pdebug(3, "    MUD_DRM_REG_SET_CONNECTOR: %u\n", val);
+				mdg->current_connector = mdg->connectors[val].connector;
+				break;
+
+			case MUD_DRM_REG_SET_FORMAT:
+				pdebug(3, "    MUD_DRM_REG_SET_FORMAT: %u\n", val);
+				mdg->current_format = val;
+				break;
+
+			case MUD_DRM_REG_SET_COMMIT:
+				pdebug(3, "    MUD_DRM_REG_SET_COMMIT: %u\n", val);
+
+				if (val == MUD_DRM_COMMIT_APPLY)
+					ret = mud_drm_gadget_commit(mdg);
+				else
+					ret = mud_drm_gadget_check(mdg);
+				pdebug(3, "        ret=%d\n", ret);
+				if (ret)
+					return ret;
+				break;
+
+			default:
+				pr_err("%s: unknown register: 0x%x\n", __func__, regnr);
+				return -EINVAL;
+			}
+		}
+
+		regnr++;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL(mud_drm_gadget_writereg);
+
+int mud_drm_gadget_readreg(struct mud_drm_gadget *mdg, unsigned int regnr,
+			   void *buf, size_t *len, u8 compression)
+{
+	struct drm_device *drm = mdg->client.dev;
+	size_t count = *len / sizeof(u32);
+	__le32 *buf32 = buf;
+	u32 val;
+	int ret;
+
+	/*
+	 * FIXME:
+	 * Should we bother with compression? The host honours our choice.
+	 * EDID and modes are the big ones, only on connector probing.
+	 */
+
+	pdebug(2, "%s: regnr=0x%02x, count=%zu\n", __func__, regnr, count);
+
+	if (regnr >= MUD_DRM_REG_CONNECTOR_TYPE && regnr < MUD_DRM_REG_CONNECTOR_MAX)
+		return mud_drm_gadget_read_connector(mdg, regnr, buf, *len);
+
+	if (regnr == MUD_DRM_REG_TV_MODES) {
+		const char *names;
+
+		ret = drm_mud_gadget_tv_mode_property(drm, &val, &names);
+		if (ret)
+			return ret;
+		if (*len != val * DRM_PROP_NAME_LEN)
+			ret = -EINVAL;
+		else
+			memcpy(buf, names, *len);
+		kfree(names);
+		return ret;
+	}
+
+	while (count--) {
+		if (regnr >= MUD_DRM_REG_FORMATS && regnr <= MUD_DRM_REG_FORMATS_MAX &&
+		    regnr < (MUD_DRM_REG_FORMATS + mdg->format_count)) {
+			val = mdg->formats[regnr - MUD_DRM_REG_FORMATS];
+		} else {
+			switch (regnr) {
+			case MUD_DRM_REG_MIN_WIDTH:
+				val = drm->mode_config.min_width;
+				break;
+			case MUD_DRM_REG_MAX_WIDTH:
+				val = drm->mode_config.max_width;
+				break;
+			case MUD_DRM_REG_MIN_HEIGHT:
+				val = drm->mode_config.min_height;
+				break;
+			case MUD_DRM_REG_MAX_HEIGHT:
+				val = drm->mode_config.max_height;
+				break;
+
+			case MUD_DRM_REG_NUM_CONNECTORS:
+				val = mdg->connector_count;
+				break;
+
+			case MUD_DRM_REG_FORMATS_COUNT:
+				val = mdg->format_count;
+				break;
+
+			case MUD_DRM_REG_TV_MODES_COUNT:
+				ret = drm_mud_gadget_tv_mode_property(drm, &val, NULL);
+				if (ret)
+					return ret;
+				break;
+			default:
+				pr_err("%s: unknown register: 0x%x\n", __func__, regnr);
+				return -EINVAL;
+			}
+		}
+
+		*(buf32++) = cpu_to_le32(val);
+		regnr++;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL(mud_drm_gadget_readreg);
+
+void mud_drm_gadget_enable(struct mud_drm_gadget *mdg)
+{
+	pdebug(1, "%s:\n", __func__);
+}
+EXPORT_SYMBOL(mud_drm_gadget_enable);
+
+void mud_drm_gadget_disable(struct mud_drm_gadget *mdg)
+{
+	pdebug(1, "%s:\n", __func__);
+	mud_drm_gadget_disable_pipe(mdg);
+}
+EXPORT_SYMBOL(mud_drm_gadget_disable);
+
+static int drm_mud_gadget_get_formats(struct mud_drm_gadget *mdg)
+{
+	struct drm_device *drm = mdg->client.dev;
+	struct drm_plane *plane;
+	//struct drm_crtc *crtc;
+	unsigned int i;
+	u32 *formats;
+
+	pdebug(1, "%s:\n", __func__);
+	//crtc = drm_crtc_from_index(drm, 0);
+	//plane = crtc->primary;
+
+	drm_for_each_plane(plane, drm) {
+		pdebug(1, "  plane=%px\n", plane);
+		if (plane->type == DRM_PLANE_TYPE_PRIMARY)
+			break;
+	}
+
+	formats = kcalloc(plane->format_count, sizeof(u32), GFP_KERNEL);
+	if (!formats)
+		return -ENOMEM;
+
+	for (i = 0; i < plane->format_count; i++) {
+		struct drm_format_name_buf format_name;
+		const struct drm_format_info *fmt;
+
+		fmt = drm_format_info(plane->format_types[i]);
+		if (fmt->num_planes != 1)
+			continue;
+
+		formats[mdg->format_count++] = plane->format_types[i];
+		pdebug(1, "    %s\n", drm_get_format_name(plane->format_types[i], &format_name));
+	}
+
+	if (mdg->format_count > (MUD_DRM_REG_FORMATS_MAX - MUD_DRM_REG_FORMATS)) {
+		kfree(formats);
+		return -ENOSPC;
+	}
+
+	mdg->formats = formats;
+
+	return 0;
+}
+
+static bool object_has_property(struct drm_mode_object *obj, struct drm_property *property)
+{
+	unsigned int i;
+
+	for (i = 0; i < obj->properties->count; i++)
+		if (obj->properties->properties[i] == property)
+			return true;
+
+	return false;
+}
+
+static void mud_drm_gadget_get_connectors(struct mud_drm_gadget *mdg)
+{
+	struct mud_drm_gadget_connector *connectors = NULL;
+	struct drm_connector_list_iter conn_iter;
+	unsigned int connector_count = 0;
+	struct drm_connector *connector;
+	struct drm_device *drm = mdg->client.dev;
+
+	pdebug(1, "%s:\n", __func__);
+
+	drm_connector_list_iter_begin(drm, &conn_iter);
+	drm_client_for_each_connector_iter(connector, &conn_iter) {
+		struct mud_drm_gadget_connector *tmp, *mconn;
+
+		tmp = krealloc(connectors, (connector_count + 1) * sizeof(*connectors),
+			       GFP_KERNEL | __GFP_ZERO);
+		if (!tmp)
+			break;
+
+		connectors = tmp;
+		drm_connector_get(connector);
+		mconn = &connectors[connector_count++];
+		mconn->connector = connector;
+
+		mconn->capabilities = MUD_DRM_CONNECTOR_CAP_MODE;
+		if (connector->connector_type != DRM_MODE_CONNECTOR_VIRTUAL)
+			mconn->capabilities |= MUD_DRM_CONNECTOR_CAP_EDID;
+		if (object_has_property(&connector->base, drm->mode_config.tv_mode_property))
+			mconn->capabilities |= MUD_DRM_CONNECTOR_CAP_TV;
+	}
+	drm_connector_list_iter_end(&conn_iter);
+
+	mdg->connectors = connectors;
+	mdg->connector_count = connector_count;
+}
+
+static void mud_drm_gadget_client_unregister(struct drm_client_dev *client)
+{
+	struct mud_drm_gadget *mdg = container_of(client, struct mud_drm_gadget, client);
+	unsigned int i;
+
+	pdebug(1, "%s:\n", __func__);
+
+	vfree(mdg->work_buf);
+	kfree(mdg->formats);
+
+	for (i = 0; i < mdg->connector_count; i++) {
+		struct mud_drm_gadget_connector *mconn = &mdg->connectors[i];
+
+		drm_connector_put(mconn->connector);
+		kfree(mconn->modes);
+		kfree(mconn->edid);
+	}
+	kfree(mdg->connectors);
+
+	drm_client_framebuffer_delete(mdg->buffer_check);
+	drm_client_framebuffer_delete(mdg->buffer);
+	drm_client_release(client);
+	kfree(mdg);
+}
+
+static int mud_drm_gadget_client_hotplug(struct drm_client_dev *client)
+{
+	struct mud_drm_gadget *mdg = container_of(client, struct mud_drm_gadget, client);
+
+	pdebug(1, "%s:\n", __func__);
+
+	mud_drm_gadget_probe_connectors(mdg);
+
+	return 0;
+}
+
+static const struct drm_client_funcs mdg_client_funcs = {
+	.owner		= THIS_MODULE,
+	.unregister	= mud_drm_gadget_client_unregister,
+	.hotplug	= mud_drm_gadget_client_hotplug,
+};
+
+struct mud_drm_gadget *mud_drm_gadget_init(unsigned int minor_id,
+					   size_t max_transfer_size)
+{
+	struct mud_drm_gadget *mdg;
+	void *work_buf;
+	int ret;
+
+	pdebug(1, "%s:\n", __func__);
+
+	mdg = kzalloc(sizeof(*mdg), GFP_KERNEL);
+	work_buf = vmalloc(max_transfer_size);
+	if (!mdg || !work_buf) {
+		ret = -ENOMEM;
+		goto error_free;
+	}
+
+	mdg->max_transfer_size = max_transfer_size;
+	mdg->work_buf = work_buf;
+
+	ret = drm_client_init_from_id(minor_id, &mdg->client, "mud-drm-gadget", &mdg_client_funcs);
+	if (0 && ret)
+		goto error_free;
+
+	/* From this point on we can't fail since only a drm_dev_unregister() can unload us */
+
+	if (!ret) {
+		ret = drm_mud_gadget_get_formats(mdg);
+		if (ret)
+			pr_err("ERROR: drm_mud_gadget_get_formats=%d\n", ret);
+
+		mud_drm_gadget_get_connectors(mdg);
+		mud_drm_gadget_probe_connectors(mdg);
+	}
+
+	pdebug(1, "%s: connector_count=%u\n", __func__, mdg->connector_count);
+
+	return mdg;
+
+error_free:
+	vfree(work_buf);
+	kfree(mdg);
+
+	return ERR_PTR(ret);
+}
+EXPORT_SYMBOL(mud_drm_gadget_init);
+
+MODULE_AUTHOR("Noralf Trønnes");
+MODULE_LICENSE("GPL");
diff --git a/drivers/usb/gadget/Kconfig b/drivers/usb/gadget/Kconfig
index d6285146ec76..e30cb039f35d 100644
--- a/drivers/usb/gadget/Kconfig
+++ b/drivers/usb/gadget/Kconfig
@@ -219,6 +219,9 @@ config USB_F_TCM
 config USB_F_MUD
 	tristate
 
+config USB_F_MUD_DRM
+	tristate
+
 config USB_F_MUD_PINS
 	tristate
 
@@ -498,6 +501,15 @@ menuconfig USB_CONFIGFS_F_MUD
 
 if USB_F_MUD
 
+config USB_CONFIGFS_F_MUD_DRM
+	bool "Multifunction USB Device Display"
+	depends on DRM
+	select DRM_MUD_GADGET
+	select USB_F_MUD_DRM
+	select LZ4_DECOMPRESS
+	help
+	  Display support for the Multifunction USB Device.
+
 config USB_CONFIGFS_F_MUD_PINS
 	bool "Multifunction USB Device GPIO"
 	depends on PINCTRL
diff --git a/drivers/usb/gadget/function/Makefile b/drivers/usb/gadget/function/Makefile
index 2e24227fcc12..d0f93ce6bbe9 100644
--- a/drivers/usb/gadget/function/Makefile
+++ b/drivers/usb/gadget/function/Makefile
@@ -52,5 +52,7 @@ usb_f_tcm-y			:= f_tcm.o
 obj-$(CONFIG_USB_F_TCM)		+= usb_f_tcm.o
 usb_f_mud-y			:= f_mud.o mud_regmap.o
 obj-$(CONFIG_USB_F_MUD)		+= usb_f_mud.o
+usb_f_mud_drm-y			:= f_mud_drm.o
+obj-$(CONFIG_USB_F_MUD_DRM)	+= usb_f_mud_drm.o
 usb_f_mud_pins-y		:= f_mud_pins.o
 obj-$(CONFIG_USB_F_MUD_PINS)	+= usb_f_mud_pins.o
diff --git a/drivers/usb/gadget/function/f_mud_drm.c b/drivers/usb/gadget/function/f_mud_drm.c
new file mode 100644
index 000000000000..5e7ba71f3389
--- /dev/null
+++ b/drivers/usb/gadget/function/f_mud_drm.c
@@ -0,0 +1,181 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2020 Noralf Trønnes
+ */
+
+#include <linux/configfs.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/device.h>
+#include <linux/regmap_usb.h>
+#include <linux/slab.h>
+
+#include "f_mud.h"
+#include "../../../gpu/drm/mud/mud_drm.h"
+
+struct f_mud_drm_cell {
+	struct f_mud_cell cell;
+
+	struct mutex lock;
+	int refcnt;
+
+	int drm_dev;
+	const char *backlight_dev;
+
+	struct mud_drm_gadget *mdg;
+};
+
+static inline struct f_mud_drm_cell *ci_to_f_mud_drm_cell(struct config_item *item)
+{
+	return container_of(to_config_group(item), struct f_mud_drm_cell, cell.group);
+}
+
+static inline struct f_mud_drm_cell *cell_to_f_mud_drm_cell(struct f_mud_cell *cell)
+{
+	return container_of(cell, struct f_mud_drm_cell, cell);
+}
+
+static void f_mud_drm_enable(struct f_mud_cell *cell)
+{
+	struct f_mud_drm_cell *pcell = cell_to_f_mud_drm_cell(cell);
+
+	mud_drm_gadget_enable(pcell->mdg);
+}
+
+static void f_mud_drm_disable(struct f_mud_cell *cell)
+{
+	struct f_mud_drm_cell *pcell = cell_to_f_mud_drm_cell(cell);
+
+	mud_drm_gadget_disable(pcell->mdg);
+}
+
+static int f_mud_drm_writereg(struct f_mud_cell *cell, unsigned int regnr,
+			      const void *buf, size_t len, u8 compression)
+{
+	struct f_mud_drm_cell *pcell = cell_to_f_mud_drm_cell(cell);
+
+	return mud_drm_gadget_writereg(pcell->mdg, regnr, buf, len, compression);
+}
+
+static int f_mud_drm_readreg(struct f_mud_cell *cell, unsigned int regnr,
+			     void *buf, size_t *len, u8 compression)
+{
+	struct f_mud_drm_cell *pcell = cell_to_f_mud_drm_cell(cell);
+
+	return mud_drm_gadget_readreg(pcell->mdg, regnr, buf, len, compression);
+}
+
+static int f_mud_drm_bind(struct f_mud_cell *cell)
+{
+	struct f_mud_drm_cell *pcell = cell_to_f_mud_drm_cell(cell);
+	struct mud_drm_gadget *mdg;
+	int drm_dev, ret = 0;
+
+	printk("%s:\n", __func__);
+
+	mutex_lock(&pcell->lock);
+	drm_dev = pcell->drm_dev;
+	printk("    drm_dev=%d\n", pcell->drm_dev);
+	printk("    backlight_dev='%s'\n", pcell->backlight_dev ? pcell->backlight_dev : "");
+
+	mdg = mud_drm_gadget_init(drm_dev, cell->ops->max_transfer_size);
+	if (IS_ERR(mdg)) {
+		ret = PTR_ERR(mdg);
+		goto out;
+	}
+
+	pcell->mdg = mdg;
+	pcell->refcnt++;
+out:
+	mutex_unlock(&pcell->lock);
+
+	return ret;
+}
+
+static void f_mud_drm_unbind(struct f_mud_cell *cell)
+{
+	struct f_mud_drm_cell *pcell = cell_to_f_mud_drm_cell(cell);
+
+	printk("%s:\n", __func__);
+
+	mutex_lock(&pcell->lock);
+	pcell->refcnt--;
+	mutex_unlock(&pcell->lock);
+}
+
+F_MUD_OPT_INT(f_mud_drm_cell, drm_dev, 0, 63);
+F_MUD_OPT_STR(f_mud_drm_cell, backlight_dev);
+
+static struct configfs_attribute *f_mud_drm_attrs[] = {
+	&f_mud_drm_cell_attr_drm_dev,
+	&f_mud_drm_cell_attr_backlight_dev,
+	NULL,
+};
+
+static struct configfs_item_operations f_mud_drm_item_ops = {
+	.release = f_mud_cell_item_release,
+};
+
+static const struct config_item_type f_mud_drm_func_type = {
+	.ct_item_ops	= &f_mud_drm_item_ops,
+	.ct_attrs	= f_mud_drm_attrs,
+	.ct_owner	= THIS_MODULE,
+};
+
+static void f_mud_drm_free(struct f_mud_cell *cell)
+{
+	struct f_mud_drm_cell *pcell = container_of(cell, struct f_mud_drm_cell, cell);
+
+	printk("%s:\n", __func__);
+
+	mutex_destroy(&pcell->lock);
+	kfree(pcell->backlight_dev);
+	kfree(pcell);
+}
+
+static struct f_mud_cell *f_mud_drm_alloc(void)
+{
+	struct f_mud_drm_cell *pcell;
+
+	printk("%s:\n", __func__);
+
+	pcell = kzalloc(sizeof(*pcell), GFP_KERNEL);
+	if (!pcell)
+		return ERR_PTR(-ENOMEM);
+
+	mutex_init(&pcell->lock);
+	config_group_init_type_name(&pcell->cell.group, "", &f_mud_drm_func_type);
+
+	return &pcell->cell;
+}
+
+static const struct f_mud_cell_ops f_mud_drm_ops = {
+	.name = "mud-drm",
+	.owner = THIS_MODULE,
+
+	/*
+	 * FIXME: Support interrupt for connector hotplug event
+	 * Polling should be a fallback.
+	.interrupt_interval_ms = 1000,
+	 */
+
+	.alloc = f_mud_drm_alloc,
+	.free = f_mud_drm_free,
+	.bind = f_mud_drm_bind,
+	.unbind = f_mud_drm_unbind,
+
+	.regval_bytes = 4,
+	.max_transfer_size = KMALLOC_MAX_SIZE,
+	.compression = REGMAP_USB_COMPRESSION_LZ4,
+
+	.enable = f_mud_drm_enable,
+	.disable = f_mud_drm_disable,
+	.readreg = f_mud_drm_readreg,
+	.writereg = f_mud_drm_writereg,
+};
+
+DECLARE_F_MUD_CELL_INIT(f_mud_drm_ops);
+
+MODULE_AUTHOR("Noralf Trønnes");
+MODULE_LICENSE("GPL");
