diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index 483059a..b351798 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -223,6 +223,8 @@ source "drivers/gpu/drm/hisilicon/Kconfig"

 source "drivers/gpu/drm/mediatek/Kconfig"

+source "drivers/gpu/drm/udrm/Kconfig"
+
 # Keep legacy drivers last

 menuconfig DRM_LEGACY
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index 25c7204..29175bc 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -86,3 +86,4 @@ obj-$(CONFIG_DRM_FSL_DCU) += fsl-dcu/
 obj-$(CONFIG_DRM_ETNAVIV) += etnaviv/
 obj-$(CONFIG_DRM_ARCPGU)+= arc/
 obj-y			+= hisilicon/
+obj-y			+= udrm/
diff --git a/drivers/gpu/drm/udrm/Kconfig b/drivers/gpu/drm/udrm/Kconfig
new file mode 100644
index 0000000..41faa4b
--- /dev/null
+++ b/drivers/gpu/drm/udrm/Kconfig
@@ -0,0 +1,9 @@
+config DRM_USER
+	tristate "Support for userspace DRM drivers"
+	depends on DRM
+	select DRM_KMS_HELPER
+	select DRM_KMS_CMA_HELPER
+	select VIDEOMODE_HELPERS
+	help
+	  Choose this option if you have a userspace DRM driver.
+	  If M is selected the module will be called tinydrm.
diff --git a/drivers/gpu/drm/udrm/Makefile b/drivers/gpu/drm/udrm/Makefile
new file mode 100644
index 0000000..9eb8c27
--- /dev/null
+++ b/drivers/gpu/drm/udrm/Makefile
@@ -0,0 +1,4 @@
+ccflags-y += -I$(src)/include
+
+udrm-y := udrm-dev.o udrm-drv.o udrm-fb.o udrm-pipe.o
+obj-$(CONFIG_DRM_USER) += udrm.o
diff --git a/drivers/gpu/drm/udrm/udrm-dev.c b/drivers/gpu/drm/udrm/udrm-dev.c
new file mode 100644
index 0000000..eb19b7b
--- /dev/null
+++ b/drivers/gpu/drm/udrm/udrm-dev.c
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <linux/completion.h>
+#include <linux/dma-buf.h>
+#include <linux/fs.h>
+#include <linux/idr.h>
+#include <linux/init.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+
+#include <uapi/drm/udrm.h>
+
+#include "udrm.h"
+
+static struct miscdevice udrm_misc;
+
+int udrm_send_event(struct udrm_device *udev, void *ev_in)
+{
+	struct udrm_event *ev = ev_in;
+	unsigned long time_left;
+	int ret = 0;
+
+	mutex_lock(&udev->dev_lock);
+
+	DRM_DEBUG("IN ev->type=%u, ev->length=%u\n", ev->type, ev->length);
+
+	if (!udev->initialized) {
+		DRM_ERROR("Not initialized\n");
+		ret = -ENODEV;
+		goto out_unlock;
+	}
+
+	ev = kmemdup(ev, ev->length, GFP_KERNEL);
+	if (!ev) {
+		ret = -ENOMEM;
+		goto out_unlock;
+	}
+
+	reinit_completion(&udev->completion);
+
+	ret = mutex_lock_interruptible(&udev->mutex);
+	if (ret) {
+		kfree(ev);
+		goto out_unlock;
+	}
+	udev->ev = ev;
+	mutex_unlock(&udev->mutex);
+
+	wake_up_interruptible(&udev->waitq);
+
+	time_left = wait_for_completion_timeout(&udev->completion, 5 * HZ);
+	ret = udev->event_ret;
+	if (!time_left) {
+		DRM_ERROR("timeout waiting for reply\n");
+		ret = -ETIMEDOUT;
+	}
+
+out_unlock:
+	mutex_unlock(&udev->dev_lock);
+
+	DRM_DEBUG("OUT ret=%d, event_ret=%d\n", ret, udev->event_ret);
+
+	return ret;
+}
+
+static void udrm_release_work(struct work_struct *work)
+{
+	struct udrm_device *udev = container_of(work, struct udrm_device,
+						    release_work);
+	struct drm_device *drm = &udev->drm;
+
+	//drm_device_set_unplugged(drm);
+
+	udev->initialized = false;
+	udev->event_ret = -ENODEV;
+	complete(&udev->completion);
+
+	while (drm->open_count) {
+		DRM_DEBUG_KMS("open_count=%d\n", drm->open_count);
+		msleep(1000);
+	}
+
+	udrm_drm_unregister(udev);
+}
+
+static int udrm_open(struct inode *inode, struct file *file)
+{
+	struct udrm_device *udev;
+
+	udev = kzalloc(sizeof(*udev), GFP_KERNEL);
+	if (!udev)
+		return -ENOMEM;
+
+	mutex_init(&udev->mutex);
+	init_waitqueue_head(&udev->waitq);
+	init_completion(&udev->completion);
+	idr_init(&udev->idr);
+	INIT_WORK(&udev->release_work, udrm_release_work);
+
+	file->private_data = udev;
+	nonseekable_open(inode, file);
+
+	return 0;
+}
+
+static ssize_t udrm_write(struct file *file, const char __user *buffer,
+			   size_t count, loff_t *ppos)
+{
+	struct udrm_device *udev = file->private_data;
+	int ret, event_ret;
+
+	if (!udev->initialized)
+		return -EINVAL;
+
+	if (!count)
+		return 0;
+
+	if (count != sizeof(int))
+		return -EINVAL;
+
+	if (copy_from_user(&event_ret, buffer, sizeof(int)))
+		return -EFAULT;
+
+	ret = mutex_lock_interruptible(&udev->mutex);
+	if (ret)
+		return ret;
+
+	udev->event_ret = event_ret;
+	complete(&udev->completion);
+
+	mutex_unlock(&udev->mutex);
+
+	return count;
+}
+
+static ssize_t udrm_read(struct file *file, char __user *buffer, size_t count,
+			  loff_t *ppos)
+{
+	struct udrm_device *udev = file->private_data;
+	ssize_t ret;
+
+	if (!count)
+		return 0;
+
+	do {
+		ret = mutex_lock_interruptible(&udev->mutex);
+		if (ret)
+			return ret;
+
+		if (!udev->ev && (file->f_flags & O_NONBLOCK)) {
+			ret = -EAGAIN;
+		} else if (udev->ev) {
+			if (count < udev->ev->length)
+				ret = -EINVAL;
+			else if (copy_to_user(buffer, udev->ev, udev->ev->length))
+				ret = -EFAULT;
+			else
+				ret = udev->ev->length;
+			kfree(udev->ev);
+			udev->ev = NULL;
+		}
+
+		mutex_unlock(&udev->mutex);
+
+		if (ret)
+			break;
+
+		if (!(file->f_flags & O_NONBLOCK))
+			ret = wait_event_interruptible(udev->waitq, udev->ev);
+	} while (ret == 0);
+
+	return ret;
+}
+
+static unsigned int udrm_poll(struct file *file, poll_table *wait)
+{
+	struct udrm_device *udev = file->private_data;
+
+	poll_wait(file, &udev->waitq, wait);
+
+	if (udev->ev)
+		return POLLIN | POLLRDNORM;
+
+	return 0;
+}
+
+static int udrm_release(struct inode *inode, struct file *file)
+{
+	struct udrm_device *udev = file->private_data;
+
+	if (udev->initialized)
+		schedule_work(&udev->release_work);
+
+	return 0;
+}
+
+static long udrm_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+	struct udrm_device *udev = file->private_data;
+	struct udrm_dev_create dev_create;
+	uint32_t *formats;
+	int ret;
+
+	switch (cmd) {
+	case UDRM_DEV_CREATE:
+		if (copy_from_user(&dev_create, (void __user *)arg,
+				   sizeof(dev_create)))
+			return -EFAULT;
+
+		if (!dev_create.formats || !dev_create.num_formats)
+			return -EINVAL;
+
+		formats = memdup_user((void __user *)
+				      (uintptr_t) dev_create.formats,
+				      dev_create.num_formats * sizeof(*formats));
+		if (IS_ERR(formats))
+			return PTR_ERR(formats);
+
+		udev->initialized = true;
+		ret = udrm_drm_register(udev, &dev_create, formats,
+					dev_create.num_formats);
+		kfree(formats);
+		if (ret) {
+			udev->initialized = false;
+			return ret;
+		}
+
+		if (copy_to_user((void __user *)arg, &dev_create,
+				 sizeof(dev_create)))
+			return -EFAULT;
+		break;
+	default:
+		ret = -ENOTTY;
+		break;
+	}
+
+	return ret;
+}
+
+static const struct file_operations udrm_fops = {
+	.owner		= THIS_MODULE,
+	.open		= udrm_open,
+	.release	= udrm_release,
+	.read		= udrm_read,
+	.write		= udrm_write,
+	.poll		= udrm_poll,
+
+	.unlocked_ioctl	= udrm_ioctl,
+/* FIXME
+#ifdef CONFIG_COMPAT
+	.compat_ioctl	= udrm_compat_ioctl,
+#endif
+*/
+	.llseek		= no_llseek,
+};
+
+static struct miscdevice udrm_misc = {
+	.fops		= &udrm_fops,
+	.minor		= MISC_DYNAMIC_MINOR,
+	.name		= "udrm",
+};
+module_misc_device(udrm_misc);
+
+MODULE_AUTHOR("Noralf Trønnes");
+MODULE_DESCRIPTION("Userspace driver support for DRM");
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/udrm/udrm-drv.c b/drivers/gpu/drm/udrm/udrm-drv.c
new file mode 100644
index 0000000..c3363c3
--- /dev/null
+++ b/drivers/gpu/drm/udrm/udrm-drv.c
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_gem_cma_helper.h>
+#include <drm/drm_fb_cma_helper.h>
+#include <drm/drm_fb_helper.h>
+#include <linux/dma-buf.h>
+
+#include <uapi/drm/udrm.h>
+
+#include "udrm.h"
+
+static void udrm_lastclose(struct drm_device *drm)
+{
+	struct udrm_device *udev = drm_to_udrm(drm);
+
+	DRM_DEBUG_KMS("initialized=%u, fbdev_used=%u\n", udev->initialized,
+		      udev->fbdev_used);
+
+	if (udev->fbdev_used)
+		drm_fbdev_cma_restore_mode(udev->fbdev_cma);
+	else
+		drm_crtc_force_disable_all(drm);
+}
+
+static void udrm_gem_cma_free_object(struct drm_gem_object *gem_obj)
+{
+	if (gem_obj->import_attach) {
+		struct drm_gem_cma_object *cma_obj;
+
+		cma_obj = to_drm_gem_cma_obj(gem_obj);
+		dma_buf_vunmap(gem_obj->import_attach->dmabuf, cma_obj->vaddr);
+		cma_obj->vaddr = NULL;
+	}
+
+	drm_gem_cma_free_object(gem_obj);
+}
+
+static struct drm_gem_object *
+udrm_gem_cma_prime_import_sg_table(struct drm_device *drm,
+				      struct dma_buf_attachment *attach,
+				      struct sg_table *sgt)
+{
+	struct drm_gem_cma_object *cma_obj;
+	struct drm_gem_object *obj;
+	void *vaddr;
+
+	vaddr = dma_buf_vmap(attach->dmabuf);
+	if (!vaddr) {
+		DRM_ERROR("Failed to vmap PRIME buffer\n");
+		return ERR_PTR(-ENOMEM);
+	}
+
+	obj = drm_gem_cma_prime_import_sg_table(drm, attach, sgt);
+	if (IS_ERR(obj)) {
+		dma_buf_vunmap(attach->dmabuf, vaddr);
+		return obj;
+	}
+
+	cma_obj = to_drm_gem_cma_obj(obj);
+	cma_obj->vaddr = vaddr;
+
+	return obj;
+}
+
+static int udrm_prime_handle_to_fd_ioctl(struct drm_device *dev, void *data,
+					     struct drm_file *file_priv)
+{
+	struct drm_prime_handle *args = data;
+
+	/* FIXME: only the userspace driver should use this */
+
+	/* check flags are valid */
+	if (args->flags & ~(DRM_CLOEXEC | DRM_RDWR))
+		return -EINVAL;
+
+	return dev->driver->prime_handle_to_fd(dev, file_priv, args->handle,
+					       args->flags, &args->fd);
+}
+
+static const struct drm_ioctl_desc udrm_ioctls[] = {
+	DRM_IOCTL_DEF_DRV(UDRM_PRIME_HANDLE_TO_FD, udrm_prime_handle_to_fd_ioctl, DRM_CONTROL_ALLOW|DRM_UNLOCKED),
+};
+
+static const struct file_operations udrm_drm_fops = {
+	.owner		= THIS_MODULE,
+	.open		= drm_open,
+	.release	= drm_release,
+	.unlocked_ioctl	= drm_ioctl,
+#ifdef CONFIG_COMPAT
+	.compat_ioctl	= drm_compat_ioctl,
+#endif
+	.poll		= drm_poll,
+	.read		= drm_read,
+	.llseek		= no_llseek,
+	.mmap		= drm_gem_cma_mmap,
+};
+
+static void udrm_dirty_work(struct work_struct *work)
+{
+	struct udrm_device *udev = container_of(work, struct udrm_device,
+						   dirty_work);
+	struct drm_framebuffer *fb = udev->pipe.plane.fb;
+	struct drm_crtc *crtc = &udev->pipe.crtc;
+
+	if (fb)
+		fb->funcs->dirty(fb, NULL, 0, 0, NULL, 0);
+
+	if (udev->event) {
+		DRM_DEBUG_KMS("crtc event\n");
+		spin_lock_irq(&crtc->dev->event_lock);
+		drm_crtc_send_vblank_event(crtc, udev->event);
+		spin_unlock_irq(&crtc->dev->event_lock);
+		udev->event = NULL;
+	}
+}
+
+static const struct drm_mode_config_funcs udrm_mode_config_funcs = {
+	.fb_create = udrm_fb_create,
+	.atomic_check = drm_atomic_helper_check,
+	.atomic_commit = drm_atomic_helper_commit,
+};
+
+static int udrm_drm_init(struct udrm_device *udev, char *drv_name)
+{
+	struct drm_driver *drv = &udev->driver;
+	struct drm_device *drm = &udev->drm;
+	int ret;
+
+	drv->name = kstrdup(drv_name, GFP_KERNEL);
+	if (!drv->name)
+		return -ENOMEM;
+
+	drv->driver_features	= DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME |
+				  DRIVER_ATOMIC;
+	drv->gem_free_object		= udrm_gem_cma_free_object;
+	drv->gem_vm_ops			= &drm_gem_cma_vm_ops;
+	drv->prime_handle_to_fd		= drm_gem_prime_handle_to_fd;
+	drv->prime_fd_to_handle		= drm_gem_prime_fd_to_handle;
+	drv->gem_prime_import		= drm_gem_prime_import;
+	drv->gem_prime_export		= drm_gem_prime_export;
+	drv->gem_prime_get_sg_table	= drm_gem_cma_prime_get_sg_table;
+	drv->gem_prime_import_sg_table	= udrm_gem_cma_prime_import_sg_table;
+	drv->gem_prime_vmap		= drm_gem_cma_prime_vmap;
+	drv->gem_prime_vunmap		= drm_gem_cma_prime_vunmap;
+	drv->gem_prime_mmap		= drm_gem_cma_prime_mmap;
+	drv->dumb_create		= drm_gem_cma_dumb_create;
+	drv->dumb_map_offset		= drm_gem_cma_dumb_map_offset;
+	drv->dumb_destroy		= drm_gem_dumb_destroy;
+	drv->fops			= &udrm_drm_fops;
+	drv->lastclose			= udrm_lastclose;
+
+	drv->ioctls		= udrm_ioctls;
+	drv->num_ioctls		= ARRAY_SIZE(udrm_ioctls);
+
+	drv->desc		= "DRM userspace driver support";
+	drv->date		= "20161119";
+	drv->major		= 1;
+	drv->minor		= 0;
+
+	INIT_WORK(&udev->dirty_work, udrm_dirty_work);
+	mutex_init(&udev->dev_lock);
+
+	ret = drm_dev_init(drm, drv, NULL);
+	if (ret)
+		return ret;
+
+	drm_mode_config_init(drm);
+	drm->mode_config.funcs = &udrm_mode_config_funcs;
+
+	return 0;
+}
+
+static void udrm_drm_fini(struct udrm_device *udev)
+{
+	struct drm_device *drm = &udev->drm;
+
+	DRM_DEBUG_KMS("udrm_drm_fini\n");
+
+	mutex_destroy(&udev->dev_lock);
+	drm_mode_config_cleanup(drm);
+	drm_dev_unref(drm);
+}
+
+static int udrm_buf_get(struct udrm_device *udev, int fd, u32 mode,
+			   uint32_t *formats, unsigned int num_formats)
+{
+	int i, max_cpp = 0;
+	size_t len;
+
+	if (mode & UDRM_BUF_MODE_EMUL_XRGB8888) {
+		if (formats[0] != DRM_FORMAT_RGB565)
+			return -EINVAL;
+		udev->emulate_xrgb8888_format = formats[0];
+	}
+
+	for (i = 0; i < num_formats; i++) {
+		if (udev->emulate_xrgb8888_format &&
+		    formats[i] == DRM_FORMAT_XRGB8888)
+			continue;
+		max_cpp = max(max_cpp, drm_format_plane_cpp(formats[i], 0));
+	}
+
+	if (!max_cpp)
+		return -EINVAL;
+
+	len = udev->display_mode.hdisplay * udev->display_mode.vdisplay * max_cpp;
+
+	udev->dmabuf = dma_buf_get(fd);
+	if (IS_ERR(udev->dmabuf))
+		return PTR_ERR(udev->dmabuf);
+
+	if (len > udev->dmabuf->size) {
+		dma_buf_put(udev->dmabuf);
+		return -EINVAL;
+	}
+
+	/* FIXME is dma_buf_attach() necessary when there's no device? */
+
+	udev->buf_mode = mode;
+	udev->buf_fd = fd;
+
+	return 0;
+}
+
+static void fbdev_init_work(struct work_struct *work)
+{
+	struct udrm_device *udev = container_of(work, struct udrm_device,
+						fbdev_init_work);
+	int ret;
+
+	ret = udrm_fbdev_init(udev);
+	if (ret)
+		DRM_ERROR("Failed to initialize fbdev: %d\n", ret);
+
+}
+
+int udrm_drm_register(struct udrm_device *udev,
+		      struct udrm_dev_create *dev_create,
+		      uint32_t *formats, unsigned int num_formats)
+{
+	struct drm_device *drm;
+	int ret;
+
+	ret = drm_mode_convert_umode(&udev->display_mode, &dev_create->mode);
+	if (ret)
+		return ret;
+
+	drm_mode_debug_printmodeline(&udev->display_mode);
+
+	if (dev_create->buf_mode) {
+		ret = udrm_buf_get(udev, dev_create->buf_fd, dev_create->buf_mode,
+				   formats, num_formats);
+		if (ret)
+			return ret;
+	}
+
+	ret = udrm_drm_init(udev, dev_create->name);
+	if (ret)
+		goto err_put_dmabuf;
+
+	drm = &udev->drm;
+	drm->mode_config.funcs = &udrm_mode_config_funcs;
+
+	ret = udrm_display_pipe_init(udev, DRM_MODE_CONNECTOR_VIRTUAL,
+				     formats, num_formats);
+	if (ret)
+		goto err_fini;
+
+	drm->mode_config.preferred_depth = drm_format_plane_cpp(formats[0], 0) * 8;
+
+	drm_mode_config_reset(drm);
+
+	DRM_DEBUG_KMS("preferred_depth=%u\n", drm->mode_config.preferred_depth);
+
+	ret = drm_dev_register(drm, 0);
+	if (ret)
+		goto err_fini;
+
+	/*
+	 * fbdev initialization generates events, so to avoid having to queue
+	 * up events or use a multithreading userspace driver, let a worker do
+	 * it so userspace can be ready for the events.
+	 */
+	INIT_WORK(&udev->fbdev_init_work, fbdev_init_work);
+	schedule_work(&udev->fbdev_init_work);
+
+	dev_create->index = drm->primary->index;
+
+	return 0;
+
+err_put_dmabuf:
+	if (udev->dmabuf)
+		dma_buf_put(udev->dmabuf);
+err_fini:
+	udrm_drm_fini(udev);
+
+	return ret;
+}
+
+void udrm_drm_unregister(struct udrm_device *udev)
+{
+	struct drm_device *drm = &udev->drm;
+
+	DRM_DEBUG_KMS("udrm_drm_unregister\n");
+
+	cancel_work_sync(&udev->fbdev_init_work);
+	drm_crtc_force_disable_all(drm);
+	cancel_work_sync(&udev->dirty_work);
+	udrm_fbdev_fini(udev);
+	drm_dev_unregister(drm);
+
+	if (udev->dmabuf)
+		dma_buf_put(udev->dmabuf);
+
+	udrm_drm_fini(udev);
+}
diff --git a/drivers/gpu/drm/udrm/udrm-fb.c b/drivers/gpu/drm/udrm/udrm-fb.c
new file mode 100644
index 0000000..2c6a33d
--- /dev/null
+++ b/drivers/gpu/drm/udrm/udrm-fb.c
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <drm/drmP.h>
+#include <drm/drm_crtc.h>
+#include <drm/drm_fb_cma_helper.h>
+#include <drm/drm_fb_helper.h>
+#include <linux/dma-buf.h>
+
+#include <uapi/drm/udrm.h>
+
+#include "udrm.h"
+
+static void tinydrm_merge_clips(struct drm_clip_rect *dst,
+			 struct drm_clip_rect *src, unsigned int num_clips,
+			 unsigned int flags, u32 max_width, u32 max_height)
+{
+	unsigned int i;
+
+	if (!src || !num_clips) {
+		dst->x1 = 0;
+		dst->x2 = max_width;
+		dst->y1 = 0;
+		dst->y2 = max_height;
+		return;
+	}
+
+	dst->x1 = ~0;
+	dst->y1 = ~0;
+	dst->x2 = 0;
+	dst->y2 = 0;
+
+	for (i = 0; i < num_clips; i++) {
+		if (flags & DRM_MODE_FB_DIRTY_ANNOTATE_COPY)
+			i++;
+		dst->x1 = min(dst->x1, src[i].x1);
+		dst->x2 = max(dst->x2, src[i].x2);
+		dst->y1 = min(dst->y1, src[i].y1);
+		dst->y2 = max(dst->y2, src[i].y2);
+	}
+
+	if (dst->x2 > max_width || dst->y2 > max_height ||
+	    dst->x1 >= dst->x2 || dst->y1 >= dst->y2) {
+		DRM_DEBUG_KMS("Illegal clip: x1=%u, x2=%u, y1=%u, y2=%u\n",
+			      dst->x1, dst->x2, dst->y1, dst->y2);
+		dst->x1 = 0;
+		dst->y1 = 0;
+		dst->x2 = max_width;
+		dst->y2 = max_height;
+	}
+}
+
+static void udrm_buf_memcpy(void *dst, void *vaddr, unsigned int pitch,
+			    unsigned int cpp, struct drm_clip_rect *clip)
+{
+	void *src = vaddr + (clip->y1 * pitch) + (clip->x1 * cpp);
+	size_t len = (clip->x2 - clip->x1) * cpp;
+	unsigned int y;
+
+	for (y = clip->y1; y < clip->y2; y++) {
+		memcpy(dst, src, len);
+		src += pitch;
+		dst += len;
+	}
+}
+
+static void udrm_buf_swab16(u16 *dst, void *vaddr, unsigned int pitch,
+			    struct drm_clip_rect *clip)
+{
+	unsigned int x, y;
+	u16 *src;
+
+	for (y = clip->y1; y < clip->y2; y++) {
+		src = vaddr + (y * pitch);
+		src += clip->x1;
+		for (x = clip->x1; x < clip->x2; x++)
+			*dst++ = swab16(*src++);
+	}
+}
+
+static void udrm_buf_emul_xrgb888(void *dst, void *vaddr, unsigned int pitch,
+			u32 buf_mode, struct drm_clip_rect *clip)
+{
+	bool swap = (buf_mode & 7) == UDRM_BUF_MODE_SWAP_BYTES;
+	u16 val16, *dst16 = dst;
+	unsigned int x, y;
+	u32 *src;
+
+	for (y = clip->y1; y < clip->y2; y++) {
+		src = vaddr + (y * pitch);
+		src += clip->x1;
+		for (x = clip->x1; x < clip->x2; x++) {
+			val16 = ((*src & 0x00F80000) >> 8) |
+				((*src & 0x0000FC00) >> 5) |
+				((*src & 0x000000F8) >> 3);
+			src++;
+			if (swap)
+				*dst16++ = swab16(val16);
+			else
+				*dst16++ = val16;
+		}
+	}
+}
+
+static bool udrm_fb_dirty_buf_copy(struct udrm_device *udev,
+				   struct drm_framebuffer *fb,
+				   struct drm_clip_rect *clip)
+{
+	struct drm_gem_cma_object *cma_obj = drm_fb_cma_get_gem_obj(fb, 0);
+	unsigned int cpp = drm_format_plane_cpp(fb->pixel_format, 0);
+	unsigned int pitch = fb->pitches[0];
+	void *dst, *src = cma_obj->vaddr;
+	int ret = 0;
+
+	if (cma_obj->base.import_attach) {
+		ret = dma_buf_begin_cpu_access(cma_obj->base.import_attach->dmabuf,
+					       DMA_FROM_DEVICE);
+		if (ret)
+			return false;
+	}
+
+	dst = dma_buf_vmap(udev->dmabuf);
+	if (!dst) {
+		ret = -ENOMEM;
+		goto out_end_access;
+	}
+
+	if (udev->emulate_xrgb8888_format &&
+	    fb->pixel_format == DRM_FORMAT_XRGB8888) {
+		udrm_buf_emul_xrgb888(dst, src, pitch, udev->buf_mode, clip);
+		goto out;
+	}
+
+	switch (udev->buf_mode & 7) {
+	case UDRM_BUF_MODE_PLAIN_COPY:
+		udrm_buf_memcpy(dst, src, pitch, cpp, clip);
+		break;
+	case UDRM_BUF_MODE_SWAP_BYTES:
+		/* FIXME support more */
+		if (cpp == 2)
+			udrm_buf_swab16(dst, src, pitch, clip);
+		else
+			ret = -EINVAL;
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+out:
+	dma_buf_vunmap(udev->dmabuf, dst);
+out_end_access:
+	if (cma_obj->base.import_attach)
+		ret = dma_buf_end_cpu_access(cma_obj->base.import_attach->dmabuf,
+					     DMA_FROM_DEVICE);
+
+	return ret ? false : true;
+}
+
+static int udrm_fb_dirty(struct drm_framebuffer *fb,
+			     struct drm_file *file_priv,
+			     unsigned int flags, unsigned int color,
+			     struct drm_clip_rect *clips,
+			     unsigned int num_clips)
+{
+	struct udrm_device *udev = drm_to_udrm(fb->dev);
+	struct drm_mode_fb_dirty_cmd *dirty;
+	struct udrm_event_fb_dirty *ev;
+	struct drm_clip_rect clip;
+	size_t size_clips, size;
+	int ret;
+
+	/* don't return -EINVAL, xorg will stop flushing */
+	if (!udev->prepared)
+		return 0;
+
+	/* fbdev can flush even when we're not interested */
+	if (udev->pipe.plane.fb != fb)
+		return 0;
+
+	/* Make sure to flush everything the first time */
+	if (!udev->enabled) {
+		clips = NULL;
+		num_clips = 0;
+	}
+
+	udev->enabled = true;
+
+	/*
+	 * FIXME: are there any apps/libs that pass more than one clip rect?
+	 *        should we support passing multi clips to the driver?
+	 */
+	tinydrm_merge_clips(&clip, clips, num_clips, flags,
+			    fb->width, fb->height);
+	clips = &clip;
+	num_clips = 1;
+
+	DRM_DEBUG("Flushing [FB:%d] x1=%u, x2=%u, y1=%u, y2=%u\n", fb->base.id,
+		  clips->x1, clips->x2, clips->y1, clips->y2);
+
+	if (udev->dmabuf && num_clips == 1)
+		udrm_fb_dirty_buf_copy(udev, fb, clips);
+
+	size_clips = num_clips * sizeof(struct drm_clip_rect);
+	size = sizeof(struct udrm_event_fb_dirty) + size_clips;
+	ev = kzalloc(size, GFP_KERNEL);
+	if (!ev)
+		return -ENOMEM;
+
+	ev->base.type = UDRM_EVENT_FB_DIRTY;
+	ev->base.length = size;
+	dirty = &ev->fb_dirty_cmd;
+
+	dirty->fb_id = fb->base.id;
+	dirty->flags = flags;
+	dirty->color = color;
+	dirty->num_clips = num_clips;
+
+	if (num_clips)
+		memcpy(ev->clips, clips, size_clips);
+
+	ret = udrm_send_event(udev, ev);
+	if (ret)
+		pr_err_ratelimited("Failed to update display %d\n", ret);
+
+	return ret;
+}
+
+static void udrm_fb_destroy(struct drm_framebuffer *fb)
+{
+	struct udrm_device *udev = drm_to_udrm(fb->dev);
+	struct udrm_event_fb ev = {
+		.base = {
+			.type = UDRM_EVENT_FB_DESTROY,
+			.length = sizeof(ev),
+		},
+	};
+	struct drm_framebuffer *iter;
+	int id;
+
+	DRM_DEBUG_KMS("[FB:%d]\n", fb->base.id);
+
+	idr_for_each_entry(&udev->idr, iter, id) {
+		if (fb == iter)
+			break;
+	}
+
+	if (!iter) {
+		DRM_ERROR("failed to find idr\n");
+		goto out;
+	}
+
+	ev.fb_id = id;
+	idr_remove(&udev->idr, id);
+
+	udrm_send_event(udev, &ev);
+out:
+	drm_fb_cma_destroy(fb);
+}
+
+static const struct drm_framebuffer_funcs udrm_fb_funcs = {
+	.destroy	= udrm_fb_destroy,
+	.create_handle	= drm_fb_cma_create_handle,
+	.dirty		= udrm_fb_dirty,
+};
+
+static int udrm_fb_create_event(struct drm_framebuffer *fb)
+{
+	struct udrm_device *udev = drm_to_udrm(fb->dev);
+	struct udrm_event_fb ev = {
+		.base = {
+			.type = UDRM_EVENT_FB_CREATE,
+			.length = sizeof(ev),
+		},
+		.fb_id = fb->base.id,
+	};
+	int ret;
+
+	DRM_DEBUG_KMS("[FB:%d]\n", fb->base.id);
+
+	/* Needed because the id is gone in &drm_framebuffer_funcs->destroy */
+	ret = idr_alloc(&udev->idr, fb, fb->base.id, fb->base.id + 1, GFP_KERNEL);
+	if (ret < 1) {
+		DRM_ERROR("[FB:%d]: failed to allocate idr %d\n", fb->base.id, ret);
+		return ret;
+	}
+
+	ret = udrm_send_event(udev, &ev);
+
+	return ret;
+}
+
+struct drm_framebuffer *
+udrm_fb_create(struct drm_device *drm, struct drm_file *file_priv,
+		   const struct drm_mode_fb_cmd2 *mode_cmd)
+{
+	struct drm_framebuffer *fb;
+	int ret;
+
+	fb = drm_fb_cma_create_with_funcs(drm, file_priv, mode_cmd,
+					  &udrm_fb_funcs);
+	if (IS_ERR(fb))
+		return fb;
+
+	DRM_DEBUG_KMS("[FB:%d] pixel_format: %s\n", fb->base.id,
+		      drm_get_format_name(fb->pixel_format));
+
+	ret = udrm_fb_create_event(fb);
+
+	return fb;
+}
+
+static int udrm_fbdev_create(struct drm_fb_helper *helper,
+				struct drm_fb_helper_surface_size *sizes)
+{
+	struct udrm_device *udev = drm_to_udrm(helper->dev);
+	int ret;
+
+	ret = drm_fbdev_cma_create_with_funcs(helper, sizes, &udrm_fb_funcs);
+	if (ret)
+		return ret;
+
+	strncpy(helper->fbdev->fix.id, helper->dev->driver->name, 16);
+	udev->fbdev_helper = helper;
+
+	DRM_DEBUG_KMS("fbdev: [FB:%d] pixel_format=%s\n", helper->fb->base.id,
+		      drm_get_format_name(helper->fb->pixel_format));
+
+	udrm_fb_create_event(helper->fb);
+
+	return 0;
+}
+
+static const struct drm_fb_helper_funcs udrm_fb_helper_funcs = {
+	.fb_probe = udrm_fbdev_create,
+};
+
+int udrm_fbdev_init(struct udrm_device *udev)
+{
+	struct drm_device *drm = &udev->drm;
+	struct drm_fbdev_cma *fbdev;
+	int bpp;
+
+	DRM_DEBUG_KMS("\n");
+
+	bpp = drm->mode_config.preferred_depth;
+	fbdev = drm_fbdev_cma_init_with_funcs(drm, bpp ? bpp : 32,
+					      drm->mode_config.num_crtc,
+					      drm->mode_config.num_connector,
+					      &udrm_fb_helper_funcs);
+	if (IS_ERR(fbdev))
+		return PTR_ERR(fbdev);
+
+	udev->fbdev_cma = fbdev;
+
+	return 0;
+}
+
+void udrm_fbdev_fini(struct udrm_device *udev)
+{
+	drm_fbdev_cma_fini(udev->fbdev_cma);
+	udev->fbdev_cma = NULL;
+	udev->fbdev_helper = NULL;
+}
diff --git a/drivers/gpu/drm/udrm/udrm-pipe.c b/drivers/gpu/drm/udrm/udrm-pipe.c
new file mode 100644
index 0000000..be110cd
--- /dev/null
+++ b/drivers/gpu/drm/udrm/udrm-pipe.c
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <drm/drmP.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_crtc_helper.h>
+#include <drm/drm_fb_helper.h>
+#include <drm/drm_modes.h>
+#include <drm/drm_simple_kms_helper.h>
+
+#include <uapi/drm/udrm.h>
+
+#include "udrm.h"
+
+static int udrm_connector_get_modes(struct drm_connector *connector)
+{
+	struct udrm_device *udev = drm_to_udrm(connector->dev);
+	struct drm_display_mode *mode = &udev->display_mode;
+
+	mode = drm_mode_duplicate(connector->dev, mode);
+	if (!mode) {
+		DRM_ERROR("Failed to duplicate mode\n");
+		return 0;
+	}
+
+	if (mode->name[0] == '\0')
+		drm_mode_set_name(mode);
+
+	mode->type |= DRM_MODE_TYPE_PREFERRED;
+	drm_mode_probed_add(connector, mode);
+
+	if (mode->width_mm) {
+		connector->display_info.width_mm = mode->width_mm;
+		connector->display_info.height_mm = mode->height_mm;
+	}
+
+	return 1;
+}
+
+static const struct drm_connector_helper_funcs udrm_connector_hfuncs = {
+	.get_modes = udrm_connector_get_modes,
+	.best_encoder = drm_atomic_helper_best_encoder,
+};
+
+static enum drm_connector_status
+udrm_connector_detect(struct drm_connector *connector, bool force)
+{
+	if (drm_device_is_unplugged(connector->dev))
+		return connector_status_disconnected;
+
+	return connector->status;
+}
+
+static const struct drm_connector_funcs udrm_connector_funcs = {
+	.dpms = drm_atomic_helper_connector_dpms,
+	.reset = drm_atomic_helper_connector_reset,
+	.detect = udrm_connector_detect,
+	.fill_modes = drm_helper_probe_single_connector_modes,
+	.destroy = drm_connector_cleanup,
+	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+	.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
+};
+
+static void udrm_display_pipe_enable(struct drm_simple_display_pipe *pipe,
+				     struct drm_crtc_state *crtc_state)
+{
+	struct udrm_device *udev = pipe_to_udrm(pipe);
+	struct udrm_event ev = {
+		.type = UDRM_EVENT_PIPE_ENABLE,
+		.length = sizeof(ev),
+	};
+
+	DRM_DEBUG_KMS("\n");
+	udev->prepared = true;
+	udrm_send_event(udev, &ev);
+}
+
+static void udrm_display_pipe_disable(struct drm_simple_display_pipe *pipe)
+{
+	struct udrm_device *udev = pipe_to_udrm(pipe);
+	struct udrm_event ev = {
+		.type = UDRM_EVENT_PIPE_DISABLE,
+		.length = sizeof(ev),
+	};
+
+	DRM_DEBUG_KMS("\n");
+	udev->prepared = false;
+	udev->enabled = false;
+	udrm_send_event(udev, &ev);
+}
+
+static void udrm_display_pipe_update(struct drm_simple_display_pipe *pipe,
+				 struct drm_plane_state *old_state)
+{
+	struct udrm_device *udev = pipe_to_udrm(pipe);
+	struct drm_framebuffer *fb = pipe->plane.state->fb;
+	struct drm_crtc *crtc = &udev->pipe.crtc;
+
+	if (!fb)
+		DRM_DEBUG_KMS("fb unset\n");
+	else if (fb != old_state->fb)
+		DRM_DEBUG_KMS("fb changed\n");
+	else
+		DRM_DEBUG_KMS("No fb change\n");
+
+	if (fb && (fb != old_state->fb)) {
+		pipe->plane.fb = fb;
+
+		if (crtc->state->event) {
+			udev->event = crtc->state->event;
+			crtc->state->event = NULL;
+		}
+
+		schedule_work(&udev->dirty_work);
+	}
+
+	if (crtc->state->event) {
+		DRM_DEBUG_KMS("crtc event\n");
+		spin_lock_irq(&crtc->dev->event_lock);
+		drm_crtc_send_vblank_event(crtc, crtc->state->event);
+		spin_unlock_irq(&crtc->dev->event_lock);
+		crtc->state->event = NULL;
+	}
+
+	if (udev->fbdev_helper && fb == udev->fbdev_helper->fb)
+		udev->fbdev_used = true;
+}
+
+static const struct drm_simple_display_pipe_funcs udrm_pipe_funcs = {
+	.enable = udrm_display_pipe_enable,
+	.disable = udrm_display_pipe_disable,
+	.update = udrm_display_pipe_update,
+};
+
+int udrm_display_pipe_init(struct udrm_device *udev,
+			  int connector_type,
+			  const uint32_t *formats,
+			  unsigned int format_count)
+{
+	const struct drm_display_mode *mode = &udev->display_mode;
+	struct drm_connector *connector = &udev->connector;
+	struct drm_device *drm = &udev->drm;
+	int ret;
+
+	drm->mode_config.min_width = mode->hdisplay;
+	drm->mode_config.max_width = mode->hdisplay;
+	drm->mode_config.min_height = mode->vdisplay;
+	drm->mode_config.max_height = mode->vdisplay;
+
+	drm_connector_helper_add(connector, &udrm_connector_hfuncs);
+	ret = drm_connector_init(drm, connector, &udrm_connector_funcs,
+				 connector_type);
+	if (ret)
+		return ret;
+
+	connector->status = connector_status_connected;
+
+	ret = drm_simple_display_pipe_init(drm, &udev->pipe, &udrm_pipe_funcs,
+					   formats, format_count, connector);
+	if (ret)
+		drm_connector_cleanup(connector);
+
+	return ret;
+}
diff --git a/drivers/gpu/drm/udrm/udrm.h b/drivers/gpu/drm/udrm/udrm.h
new file mode 100644
index 0000000..e1a0191
--- /dev/null
+++ b/drivers/gpu/drm/udrm/udrm.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef __LINUX_TINYDRM_H
+#define __LINUX_TINYDRM_H
+
+#include <drm/drm_crtc.h>
+#include <drm/drm_gem_cma_helper.h>
+#include <drm/drm_simple_kms_helper.h>
+
+struct udrm_device {
+	struct drm_device drm;
+	struct drm_driver driver;
+	struct drm_simple_display_pipe pipe;
+	struct drm_display_mode	display_mode;
+	struct drm_connector connector;
+	struct work_struct dirty_work;
+	struct mutex dev_lock;
+	bool prepared;
+	bool enabled;
+
+	struct drm_fbdev_cma *fbdev_cma;
+	struct drm_fb_helper *fbdev_helper;
+	struct work_struct fbdev_init_work;
+	bool fbdev_used;
+
+	struct drm_pending_vblank_event *event;
+
+	struct idr		idr;
+
+	struct mutex		mutex;
+	wait_queue_head_t	waitq;
+	struct completion	completion;
+
+	struct udrm_event	*ev;
+	int			event_ret;
+
+	u32 buf_mode;
+	u32 emulate_xrgb8888_format;
+	struct dma_buf *dmabuf;
+	int buf_fd;
+
+	bool			initialized;
+	struct work_struct	release_work;
+};
+
+static inline struct udrm_device *
+drm_to_udrm(struct drm_device *drm)
+{
+	return container_of(drm, struct udrm_device, drm);
+}
+
+static inline struct udrm_device *
+pipe_to_udrm(struct drm_simple_display_pipe *pipe)
+{
+	return container_of(pipe, struct udrm_device, pipe);
+}
+
+int udrm_send_event(struct udrm_device *udev, void *ev_in);
+
+int udrm_drm_register(struct udrm_device *udev,
+		      struct udrm_dev_create *dev_create,
+		      uint32_t *formats, unsigned int num_formats);
+void udrm_drm_unregister(struct udrm_device *udev);
+
+int
+udrm_display_pipe_init(struct udrm_device *tdev,
+			  int connector_type,
+			  const uint32_t *formats,
+			  unsigned int format_count);
+
+struct drm_framebuffer *
+udrm_fb_create(struct drm_device *drm, struct drm_file *file_priv,
+		  const struct drm_mode_fb_cmd2 *mode_cmd);
+int udrm_fbdev_init(struct udrm_device *tdev);
+void udrm_fbdev_fini(struct udrm_device *tdev);
+
+#endif /* __LINUX_TINYDRM_H */
diff --git a/include/uapi/drm/udrm.h b/include/uapi/drm/udrm.h
new file mode 100644
index 0000000..66cd2b56
--- /dev/null
+++ b/include/uapi/drm/udrm.h
@@ -0,0 +1,78 @@
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef _UAPI__UDRM_H_
+#define _UAPI__UDRM_H_
+
+#if defined(__KERNEL__)
+#include <uapi/drm/drm_mode.h>
+#include <linux/types.h>
+#else
+#include <linux/types.h>
+#include <drm/drm_mode.h>
+#endif
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+#define UDRM_MAX_NAME_SIZE    80
+
+/* FIXME: Update Documentation/ioctl/ioctl-number.txt */
+#define UDRM_IOCTL_BASE       0xB5
+
+#define UDRM_BUF_MODE_NONE		0
+#define UDRM_BUF_MODE_PLAIN_COPY	1
+#define UDRM_BUF_MODE_SWAP_BYTES	2
+
+#define UDRM_BUF_MODE_EMUL_XRGB8888	BIT(8)
+
+struct udrm_dev_create {
+	char name[UDRM_MAX_NAME_SIZE];
+	struct drm_mode_modeinfo mode;
+	__u64 formats;
+	__u32 num_formats;
+	__u32 buf_mode;
+	__s32 buf_fd;
+
+	__u32 index;
+};
+
+#define UDRM_DEV_CREATE       _IOWR(UDRM_IOCTL_BASE, 1, struct udrm_dev_create)
+
+struct udrm_event {
+	__u32 type;
+	__u32 length;
+};
+
+#define UDRM_EVENT_PIPE_ENABLE	1
+#define UDRM_EVENT_PIPE_DISABLE	2
+
+#define UDRM_EVENT_FB_CREATE	3
+#define UDRM_EVENT_FB_DESTROY	4
+
+struct udrm_event_fb {
+	struct udrm_event base;
+	__u32 fb_id;
+};
+
+#define UDRM_EVENT_FB_DIRTY 	5
+
+struct udrm_event_fb_dirty {
+	struct udrm_event base;
+	struct drm_mode_fb_dirty_cmd fb_dirty_cmd;
+	struct drm_clip_rect clips[];
+};
+
+#define UDRM_PRIME_HANDLE_TO_FD 0x01
+#define DRM_IOCTL_UDRM_PRIME_HANDLE_TO_FD    DRM_IOWR(DRM_COMMAND_BASE + UDRM_PRIME_HANDLE_TO_FD, struct drm_prime_handle)
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* _UAPI__UDRM_H_ */
