uterm_video: add DRM backend
authorDavid Herrmann <dh.herrmann@googlemail.com>
Fri, 23 Mar 2012 10:45:49 +0000 (11:45 +0100)
committerDavid Herrmann <dh.herrmann@googlemail.com>
Fri, 23 Mar 2012 10:45:49 +0000 (11:45 +0100)
uterm_video can work with any backend so we need a DRM backend to get the
same functionality as our previous compositor/output API.

Signed-off-by: David Herrmann <dh.herrmann@googlemail.com>
Makefile.am
configure.ac
src/uterm_internal.h
src/uterm_video_drm.c [new file with mode: 0644]

index d5387e1..31e1840 100644 (file)
@@ -53,7 +53,8 @@ libkmscon_core_la_SOURCES = \
        src/terminal.c src/terminal.h \
        src/pty.c src/pty.h \
        src/uterm.h src/uterm_internal.h \
-       src/uterm_video.c
+       src/uterm_video.c \
+       src/uterm_video_drm.c
 
 if USE_XKBCOMMON
 libkmscon_core_la_SOURCES += \
index d760160..21adf8e 100644 (file)
@@ -118,5 +118,7 @@ if test ! x$debug = xyes ; then
         AC_DEFINE([NDEBUG], [1], [No Debug])
 fi
 
+AC_DEFINE([UTERM_HAVE_DRM], [1], [Use DRM uterm backend])
+
 AC_CONFIG_FILES([Makefile])
 AC_OUTPUT([src/genshader.c])
index 53387d2..3534ccb 100644 (file)
@@ -275,4 +275,9 @@ static inline bool video_is_awake(const struct uterm_video *video)
        return video->flags & VIDEO_AWAKE;
 }
 
+static inline bool video_need_hotplug(const struct uterm_video *video)
+{
+       return video->flags & VIDEO_HOTPLUG;
+}
+
 #endif /* UTERM_INTERNAL_H */
diff --git a/src/uterm_video_drm.c b/src/uterm_video_drm.c
new file mode 100644 (file)
index 0000000..ba4c3ff
--- /dev/null
@@ -0,0 +1,893 @@
+/*
+ * uterm - Linux User-Space Terminal
+ *
+ * Copyright (c) 2011-2012 David Herrmann <dh.herrmann@googlemail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files
+ * (the "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * DRM Video backend
+ */
+
+#define EGL_EGLEXT_PROTOTYPES
+#define GL_GLEXT_PROTOTYPES
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <gbm.h>
+#include <GL/gl.h>
+#include <GL/glext.h>
+#include <inttypes.h>
+#include <libudev.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <xf86drm.h>
+/* #include <xf86drmMode.h> TODO: missing header protection */
+#include "eloop.h"
+#include "log.h"
+#include "uterm.h"
+#include "uterm_internal.h"
+
+#define LOG_SUBSYSTEM "video_drm"
+
+static const char *mode_get_name(const struct uterm_mode *mode)
+{
+       return mode->drm.info.name;
+}
+
+static unsigned int mode_get_width(const struct uterm_mode *mode)
+{
+       return mode->drm.info.hdisplay;
+}
+
+static unsigned int mode_get_height(const struct uterm_mode *mode)
+{
+       return mode->drm.info.vdisplay;
+}
+
+static int init_rb(struct uterm_display *disp, struct drm_rb *rb)
+{
+       unsigned int stride, handle;
+       int ret;
+       struct uterm_video *video = disp->video;
+
+       rb->bo = gbm_bo_create(video->drm.gbm,
+                               disp->current_mode->drm.info.hdisplay,
+                               disp->current_mode->drm.info.vdisplay,
+                               GBM_BO_FORMAT_XRGB8888,
+                               GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+       if (!rb->bo) {
+               log_err("cannot create gbm-bo");
+               return -EFAULT;
+       }
+
+       stride = gbm_bo_get_pitch(rb->bo);
+       handle = gbm_bo_get_handle(rb->bo).u32;
+
+       /* TODO: how can we choose 24/32 dynamically? */
+       ret = drmModeAddFB(video->drm.fd,
+                                       disp->current_mode->drm.info.hdisplay,
+                                       disp->current_mode->drm.info.vdisplay,
+                                       24, 32, stride, handle, &rb->fb);
+       if (ret) {
+               log_err("cannot add drm-fb");
+               ret = -EFAULT;
+               goto err_gbm;
+       }
+
+       rb->image = eglCreateImageKHR(video->drm.disp, NULL,
+                                       EGL_NATIVE_PIXMAP_KHR, rb->bo, NULL);
+       if (!rb->image) {
+               log_err("cannot create egl image");
+               ret = -EFAULT;
+               goto err_fb;
+       }
+
+       glGenRenderbuffers(1, &rb->rb);
+       glBindRenderbuffer(GL_RENDERBUFFER, rb->rb);
+       glEGLImageTargetRenderbufferStorageOES(GL_RENDERBUFFER, rb->image);
+
+       return 0;
+
+err_fb:
+       drmModeRmFB(video->drm.fd, rb->fb);
+err_gbm:
+       gbm_bo_destroy(rb->bo);
+       return ret;
+}
+
+static void destroy_rb(struct uterm_display *disp, struct drm_rb *rb)
+{
+       glBindRenderbuffer(GL_RENDERBUFFER, 0);
+       glDeleteRenderbuffers(1, &rb->rb);
+       eglDestroyImageKHR(disp->video->drm.disp, rb->image);
+       drmModeRmFB(disp->video->drm.fd, rb->fb);
+       gbm_bo_destroy(rb->bo);
+}
+
+static int find_crtc(struct uterm_video *video, drmModeRes *res,
+                                                       drmModeEncoder *enc)
+{
+       int i, crtc;
+       struct uterm_display *iter;
+
+       for (i = 0; i < res->count_crtcs; ++i) {
+               if (enc->possible_crtcs & (1 << i)) {
+                       crtc = res->crtcs[i];
+                       for (iter = video->displays; iter; iter = iter->next) {
+                               if (iter->drm.crtc_id == crtc)
+                                       break;
+                       }
+                       if (!iter)
+                               return crtc;
+               }
+       }
+
+       return -1;
+}
+
+static int display_activate(struct uterm_display *disp, struct uterm_mode *mode)
+{
+       struct uterm_video *video = disp->video;
+       int ret, crtc, i;
+       drmModeRes *res;
+       drmModeConnector *conn;
+       drmModeEncoder *enc;
+
+       if (!video || !video_is_awake(video) || !mode)
+               return -EINVAL;
+       if (display_is_online(disp))
+               return 0;
+
+       log_info("activating display %p to %ux%u", disp,
+                       mode->drm.info.hdisplay, mode->drm.info.vdisplay);
+
+       res = drmModeGetResources(video->drm.fd);
+       if (!res) {
+               log_err("cannot get resources for display %p", disp);
+               return -EFAULT;
+       }
+       conn = drmModeGetConnector(video->drm.fd, disp->drm.conn_id);
+       if (!conn) {
+               log_err("cannot get connector for display %p", disp);
+               drmModeFreeResources(res);
+               return -EFAULT;
+       }
+
+       crtc = -1;
+       for (i = 0; i < conn->count_encoders; ++i) {
+               enc = drmModeGetEncoder(video->drm.fd, conn->encoders[i]);
+               if (!enc)
+                       continue;
+               crtc = find_crtc(video, res, enc);
+               drmModeFreeEncoder(enc);
+               if (crtc >= 0)
+                       break;
+       }
+
+       drmModeFreeConnector(conn);
+       drmModeFreeResources(res);
+
+       if (crtc < 0) {
+               log_warn("cannot find crtc for new display");
+               return -ENODEV;
+       }
+
+       disp->drm.crtc_id = crtc;
+       disp->drm.current_rb = 0;
+       disp->current_mode = mode;
+       disp->drm.saved_crtc = drmModeGetCrtc(video->drm.fd,
+                                                       disp->drm.crtc_id);
+
+       ret = init_rb(disp, &disp->drm.rb[0]);
+       if (ret)
+               goto err_saved;
+
+       ret = init_rb(disp, &disp->drm.rb[1]);
+       if (ret)
+               goto err_rb;
+
+       glGenFramebuffers(1, &disp->drm.fb);
+       glBindFramebuffer(GL_FRAMEBUFFER, disp->drm.fb);
+       glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                       GL_RENDERBUFFER, disp->drm.rb[1].rb);
+       if (glCheckFramebufferStatus(GL_FRAMEBUFFER) !=
+                                               GL_FRAMEBUFFER_COMPLETE) {
+               log_err("cannot create gl-framebuffer");
+               ret = -EFAULT;
+               goto err_fb;
+       }
+
+       ret = drmModeSetCrtc(video->drm.fd, disp->drm.crtc_id,
+                       disp->drm.rb[0].fb, 0, 0, &disp->drm.conn_id, 1,
+                                               &disp->current_mode->drm.info);
+       if (ret) {
+               log_err("cannot set drm-crtc");
+               ret = -EFAULT;
+               goto err_fb;
+       }
+
+       disp->flags |= DISPLAY_ONLINE;
+       return 0;
+
+err_fb:
+       glBindFramebuffer(GL_FRAMEBUFFER, 0);
+       glDeleteFramebuffers(1, &disp->drm.fb);
+       destroy_rb(disp, &disp->drm.rb[1]);
+err_rb:
+       destroy_rb(disp, &disp->drm.rb[0]);
+err_saved:
+       disp->current_mode = NULL;
+       if (disp->drm.saved_crtc) {
+               drmModeFreeCrtc(disp->drm.saved_crtc);
+               disp->drm.saved_crtc = NULL;
+       }
+       return ret;
+}
+
+static void display_deactivate(struct uterm_display *disp)
+{
+       if (!display_is_online(disp))
+               return;
+
+       if (disp->drm.saved_crtc) {
+               if (disp->video->flags & VIDEO_AWAKE) {
+                       drmModeSetCrtc(disp->video->drm.fd,
+                                       disp->drm.saved_crtc->crtc_id,
+                                       disp->drm.saved_crtc->buffer_id,
+                                       disp->drm.saved_crtc->x,
+                                       disp->drm.saved_crtc->y,
+                                       &disp->drm.conn_id,
+                                       1,
+                                       &disp->drm.saved_crtc->mode);
+               }
+               drmModeFreeCrtc(disp->drm.saved_crtc);
+               disp->drm.saved_crtc = NULL;
+       }
+
+       glBindFramebuffer(GL_FRAMEBUFFER, 0);
+       glDeleteFramebuffers(1, &disp->drm.fb);
+       destroy_rb(disp, &disp->drm.rb[1]);
+       destroy_rb(disp, &disp->drm.rb[0]);
+       disp->current_mode = NULL;
+       disp->flags &= ~(DISPLAY_ONLINE | DISPLAY_VSYNC);
+       log_info("deactivating display %p", disp);
+}
+
+static int display_set_dpms(struct uterm_display *disp, int state)
+{
+       int i, ret, set;
+       drmModeConnector *conn;
+       drmModePropertyRes *prop;
+
+       if (!display_is_conn(disp) || !video_is_awake(disp->video))
+               return -EINVAL;
+
+       switch (state) {
+       case UTERM_DPMS_ON:
+               set = DRM_MODE_DPMS_ON;
+               break;
+       case UTERM_DPMS_STANDBY:
+               set = DRM_MODE_DPMS_STANDBY;
+               break;
+       case UTERM_DPMS_SUSPEND:
+               set = DRM_MODE_DPMS_SUSPEND;
+               break;
+       case UTERM_DPMS_OFF:
+               set = DRM_MODE_DPMS_OFF;
+               break;
+       default:
+               return -EINVAL;
+       }
+
+       log_info("setting DPMS of display %p to %s", disp,
+                       uterm_dpms_to_name(state));
+
+       conn = drmModeGetConnector(disp->video->drm.fd, disp->drm.conn_id);
+       if (!conn) {
+               log_err("cannot get display connector");
+               return -EFAULT;
+       }
+
+       for (i = 0; i < conn->count_props; ++i) {
+               prop = drmModeGetProperty(disp->video->drm.fd, conn->props[i]);
+               if (!prop)
+                       continue;
+
+               if (!strcmp(prop->name, "DPMS")) {
+                       ret = drmModeConnectorSetProperty(disp->video->drm.fd,
+                               disp->drm.conn_id, prop->prop_id, set);
+                       if (ret) {
+                               log_info("cannot set DPMS");
+                               ret = -EFAULT;
+                       }
+                       drmModeFreeProperty(prop);
+                       break;
+               }
+               drmModeFreeProperty(prop);
+       }
+
+       if (i == conn->count_props) {
+               ret = 0;
+               log_warn("display does not support DPMS");
+               state = UTERM_DPMS_UNKNOWN;
+       }
+
+       drmModeFreeConnector(conn);
+       disp->dpms = state;
+       return ret;
+}
+
+static int display_use(struct uterm_display *disp)
+{
+       if (!display_is_online(disp))
+               return -EINVAL;
+
+       /* TODO: we need triple buffering as a VSYNC may still be pending */
+       glBindFramebuffer(GL_FRAMEBUFFER, disp->drm.fb);
+       glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+               GL_RENDERBUFFER, disp->drm.rb[disp->drm.current_rb ^ 1].rb);
+       if (glCheckFramebufferStatus(GL_FRAMEBUFFER) !=
+                                               GL_FRAMEBUFFER_COMPLETE) {
+               log_warn("cannot set gl-renderbuffer");
+               return -EFAULT;
+       }
+
+       return 0;
+}
+
+static int display_swap(struct uterm_display *disp)
+{
+       int ret;
+
+       if (!display_is_conn(disp) || !video_is_awake(disp->video))
+               return -EINVAL;
+       if (disp->dpms != UTERM_DPMS_ON)
+               return -EINVAL;
+
+       /* TODO: is glFlush sufficient here? */
+       glFinish();
+
+       errno = 0;
+       disp->drm.current_rb ^= 1;
+       ret = drmModePageFlip(disp->video->drm.fd, disp->drm.crtc_id,
+                               disp->drm.rb[disp->drm.current_rb].fb,
+                               DRM_MODE_PAGE_FLIP_EVENT, disp);
+       if (ret) {
+               log_warn("page-flip failed %d %d", ret, errno);
+               return -EFAULT;
+       }
+       uterm_display_ref(disp);
+       disp->flags |= DISPLAY_VSYNC;
+
+       return 0;
+}
+
+static int get_dpms(struct uterm_display *disp, drmModeConnector *conn)
+{
+       int i, ret;
+       drmModePropertyRes *prop;
+
+       for (i = 0; i < conn->count_props; ++i) {
+               prop = drmModeGetProperty(disp->video->drm.fd, conn->props[i]);
+               if (!prop)
+                       continue;
+
+               if (!strcmp(prop->name, "DPMS")) {
+                       switch (conn->prop_values[i]) {
+                       case DRM_MODE_DPMS_ON:
+                               ret = UTERM_DPMS_ON;
+                               break;
+                       case DRM_MODE_DPMS_STANDBY:
+                               ret = UTERM_DPMS_STANDBY;
+                               break;
+                       case DRM_MODE_DPMS_SUSPEND:
+                               ret = UTERM_DPMS_SUSPEND;
+                               break;
+                       case DRM_MODE_DPMS_OFF:
+                       default:
+                               ret = UTERM_DPMS_OFF;
+                       }
+
+                       drmModeFreeProperty(prop);
+                       return ret;
+               }
+               drmModeFreeProperty(prop);
+       }
+
+       if (i == conn->count_props)
+               log_warn("display does not support DPMS");
+       return UTERM_DPMS_UNKNOWN;
+}
+
+static void bind_display(struct uterm_video *video, drmModeRes *res,
+                                                       drmModeConnector *conn)
+{
+       struct uterm_display *disp;
+       struct uterm_mode *mode;
+       int ret, i;
+
+       ret = display_new(&disp, &drm_display_ops);
+       if (ret)
+               return;
+
+       for (i = 0; i < conn->count_modes; ++i) {
+               ret = mode_new(&mode, &drm_mode_ops);
+               if (ret)
+                       continue;
+               mode->drm.info = conn->modes[i];
+               mode->next = disp->modes;
+               disp->modes = mode;
+
+               /* TODO: more sophisticated default-mode selection */
+               if (!disp->default_mode)
+                       disp->default_mode = mode;
+       }
+
+       if (!disp->modes) {
+               log_warn("no valid mode for display found");
+               uterm_display_unref(disp);
+               return;
+       }
+
+       disp->video = video;
+       disp->drm.conn_id = conn->connector_id;
+       disp->flags |= DISPLAY_AVAILABLE;
+       disp->next = video->displays;
+       video->displays = disp;
+       disp->dpms = get_dpms(disp, conn);
+       log_info("display %p DPMS is %s", disp,
+                       uterm_dpms_to_name(disp->dpms));
+}
+
+static void unbind_display(struct uterm_display *disp)
+{
+       if (!display_is_conn(disp))
+               return;
+
+       display_deactivate(disp);
+       disp->video = NULL;
+       disp->flags &= ~DISPLAY_AVAILABLE;
+       uterm_display_unref(disp);
+}
+
+static int get_id(struct udev_device *dev)
+{
+       const char *name;
+       char *end;
+       int devnum;
+
+       name = udev_device_get_sysname(dev);
+       if (!name)
+               return -ENODEV;
+       if (strncmp(name, "card", 4) || !name[4])
+               return -ENODEV;
+
+       devnum = strtol(&name[4], &end, 10);
+       if (devnum < 0 || *end)
+               return -ENODEV;
+
+       return devnum;
+}
+
+static void page_flip_handler(int fd, unsigned int frame, unsigned int sec,
+                                               unsigned int usec, void *data)
+{
+       struct uterm_display *disp = data;
+
+       disp->flags &= ~DISPLAY_VSYNC;
+       uterm_display_unref(disp);
+}
+
+static void event(struct ev_fd *fd, int mask, void *data)
+{
+       struct uterm_video *video = data;
+       drmEventContext ev;
+
+       if (mask & (EV_HUP | EV_ERR)) {
+               log_err("error or hangup on DRM fd");
+               ev_eloop_rm_fd(video->drm.efd);
+               video->drm.efd = NULL;
+               return;
+       }
+
+       if (mask & EV_READABLE) {
+               memset(&ev, 0, sizeof(ev));
+               ev.version = DRM_EVENT_CONTEXT_VERSION;
+               ev.page_flip_handler = page_flip_handler;
+               drmHandleEvent(video->drm.fd, &ev);
+       }
+}
+
+static int init_device(struct uterm_video *video, struct udev_device *dev)
+{
+       const char *node, *ext;
+       int ret;
+       EGLint major, minor;
+       EGLenum api;
+       static const EGLint ctx_att[] =
+               { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
+       struct drm_video *drm = &video->drm;
+
+       log_info("probing %s", udev_device_get_sysname(dev));
+
+       node = udev_device_get_devnode(dev);
+       if (!node) {
+               log_err("cannot get device node");
+               return -ENODEV;
+       }
+
+       drm->id = get_id(dev);
+       if (drm->id < 0) {
+               log_err("cannot get device id");
+               return -ENODEV;
+       }
+
+       drm->fd = open(node, O_RDWR | O_CLOEXEC);
+       if (drm->fd < 0) {
+               log_err("cannot open drm device %s (%d): %m", node, errno);
+               return -EFAULT;
+       }
+       drmDropMaster(drm->fd);
+
+       drm->gbm = gbm_create_device(drm->fd);
+       if (!drm->gbm) {
+               log_err("cannot create drm device for %s", node);
+               ret = -EFAULT;
+               goto err_close;
+       }
+
+       drm->disp = eglGetDisplay((EGLNativeDisplayType) drm->gbm);
+       if (!drm->disp) {
+               log_err("cannot retrieve egl display for %s", node);
+               ret = -EFAULT;
+               goto err_gbm;
+       }
+
+       ret = eglInitialize(drm->disp, &major, &minor);
+       if (!ret) {
+               log_err("cannot init egl display for %s", node);
+               ret = -EFAULT;
+               goto err_gbm;
+       }
+
+       ext = eglQueryString(drm->disp, EGL_EXTENSIONS);
+       if (!ext || !strstr(ext, "EGL_KHR_surfaceless_opengl")) {
+               log_err("surfaceless opengl not supported");
+               ret = -EFAULT;
+               goto err_disp;
+       }
+
+       api = EGL_OPENGL_ES_API;
+       /* TODO: allow api = EGL_OPENGL_API */
+       if (!eglBindAPI(api)) {
+               log_err("cannot bind opengl-es api");
+               ret = -EFAULT;
+               goto err_disp;
+       }
+
+       drm->ctx = eglCreateContext(drm->disp, NULL, EGL_NO_CONTEXT, ctx_att);
+       if (!drm->ctx) {
+               log_err("cannot create egl context");
+               ret = -EFAULT;
+               goto err_disp;
+       }
+
+       /*
+        * We allow only one global video object. See uterm_video_new for the
+        * reasons. If we every change this we need proper context-management
+        * and should remove this call here.
+        */
+       if (!eglMakeCurrent(drm->disp, EGL_NO_SURFACE, EGL_NO_SURFACE,
+                                                               drm->ctx)) {
+               log_err("cannot activate egl context");
+               ret = -EFAULT;
+               goto err_ctx;
+       }
+
+       ret = ev_eloop_new_fd(video->eloop, &drm->efd, drm->fd,
+                               EV_READABLE, event, video);
+       if (ret)
+               goto err_ctx;
+
+       video->flags |= VIDEO_HOTPLUG;
+       log_info("new drm device via %s", node);
+
+       return 0;
+
+err_ctx:
+       eglDestroyContext(drm->disp, drm->ctx);
+err_disp:
+       eglTerminate(drm->disp);
+err_gbm:
+       gbm_device_destroy(drm->gbm);
+err_close:
+       close(drm->fd);
+       return ret;
+}
+
+static int video_init(struct uterm_video *video)
+{
+       struct udev_enumerate *e;
+       struct udev_list_entry *name;
+       const char *path;
+       struct udev_device *dev;
+       int ret;
+
+       ret = udev_monitor_filter_add_match_subsystem_devtype(video->umon,
+                                                       "drm", "drm_minor");
+       if (ret) {
+               log_err("cannot add udev filter (%d): %m", ret);
+               return -EFAULT;
+       }
+
+       ret = udev_monitor_enable_receiving(video->umon);
+       if (ret) {
+               log_err("cannot start udev_monitor (%d): %m", ret);
+               return -EFAULT;
+       }
+
+       e = udev_enumerate_new(video->udev);
+       if (!e) {
+               log_err("cannot create udev_enumerate object");
+               return -EFAULT;
+       }
+
+       ret = udev_enumerate_add_match_subsystem(e, "drm");
+       if (ret) {
+               log_err("cannot add udev match (%d): %m", ret);
+               ret = -EFAULT;
+               goto err_enum;
+       }
+       ret = udev_enumerate_add_match_sysname(e, "card[0-9]*");
+       if (ret) {
+               log_err("cannot add udev match (%d): %m", ret);
+               ret = -EFAULT;
+               goto err_enum;
+       }
+       ret = udev_enumerate_scan_devices(e);
+       if (ret) {
+               log_err("cannot scan udev devices (%d): %m", ret);
+               ret = -EFAULT;
+               goto err_enum;
+       }
+
+       udev_list_entry_foreach(name, udev_enumerate_get_list_entry(e)) {
+               path = udev_list_entry_get_name(name);
+               if (!path || !*path)
+                       continue;
+               dev = udev_device_new_from_syspath(video->udev, path);
+               if (!dev)
+                       continue;
+
+               /* TODO: more sophisticated device selection */
+               ret = init_device(video, dev);
+               udev_device_unref(dev);
+               if (!ret)
+                       goto done;
+       }
+
+       ret = -ENODEV;
+       log_err("no drm device found");
+       goto err_enum;
+
+done:
+       ret = 0;
+err_enum:
+       udev_enumerate_unref(e);
+       return ret;
+}
+
+static void video_destroy(struct uterm_video *video)
+{
+       struct drm_video *drm = &video->drm;
+       struct uterm_display *disp;
+
+       while ((disp = video->displays)) {
+               video->displays = disp->next;
+               disp->next = NULL;
+               unbind_display(disp);
+       }
+
+       log_info("free drm device");
+       ev_eloop_rm_fd(drm->efd);
+       eglDestroyContext(drm->disp, drm->ctx);
+       eglTerminate(drm->disp);
+       gbm_device_destroy(drm->gbm);
+       drmDropMaster(drm->fd);
+       close(drm->fd);
+}
+
+static int hotplug(struct uterm_video *video)
+{
+       drmModeRes *res;
+       drmModeConnector *conn;
+       struct uterm_display *disp, *tmp;
+       unsigned int i;
+
+       if (!video_is_awake(video) || !video_need_hotplug(video))
+               return 0;
+
+       res = drmModeGetResources(video->drm.fd);
+       if (!res) {
+               log_err("cannot retrieve drm resources");
+               return -EACCES;
+       }
+
+       for (disp = video->displays; disp; disp = disp->next)
+               disp->flags &= ~DISPLAY_AVAILABLE;
+
+       for (i = 0; i < res->count_connectors; ++i) {
+               conn = drmModeGetConnector(video->drm.fd, res->connectors[i]);
+               if (!conn)
+                       continue;
+               if (conn->connection == DRM_MODE_CONNECTED) {
+                       for (disp = video->displays; disp; disp = disp->next) {
+                               if (disp->drm.conn_id == res->connectors[i]) {
+                                       disp->flags |= DISPLAY_AVAILABLE;
+                                       break;
+                               }
+                       }
+                       if (!disp)
+                               bind_display(video, res, conn);
+               }
+               drmModeFreeConnector(conn);
+       }
+
+       drmModeFreeResources(res);
+
+       while (video->displays) {
+               tmp = video->displays;
+               if (tmp->flags & DISPLAY_AVAILABLE)
+                       break;
+
+               video->displays = tmp->next;
+               tmp->next = NULL;
+               unbind_display(tmp);
+       }
+       for (disp = video->displays; disp && disp->next; ) {
+               tmp = disp->next;
+               if (tmp->flags & DISPLAY_AVAILABLE) {
+                       disp = tmp;
+               } else {
+                       disp->next = tmp->next;
+                       tmp->next = NULL;
+                       unbind_display(tmp);
+               }
+       }
+
+       video->flags &= ~VIDEO_HOTPLUG;
+       return 0;
+}
+
+static int monitor(struct uterm_video *video)
+{
+       struct udev_device *dev;
+       const char *action, *val;
+       unsigned int id;
+
+       dev = udev_monitor_receive_device(video->umon);
+       if (!dev) {
+               log_warn("cannot receive device from udev_monitor");
+               return 0;
+       }
+
+       action = udev_device_get_action(dev);
+       if (!action)
+               goto ignore;
+       if (strcmp(action, "change"))
+               goto ignore;
+
+       val = udev_device_get_property_value(dev, "HOTPLUG");
+       if (strcmp(val, "1"))
+               goto ignore;
+
+       id = get_id(dev);
+       if (id != video->drm.id)
+               goto ignore;
+
+       video->flags |= VIDEO_HOTPLUG;
+ignore:
+       udev_device_unref(dev);
+       return 0;
+}
+
+static int video_poll(struct uterm_video *video, int mask)
+{
+       int ret;
+
+       if (mask & (EV_HUP | EV_ERR)) {
+               log_err("udev_monitor closed unexpectedly");
+               return -EFAULT;
+       }
+
+       if (mask & EV_READABLE) {
+               ret = monitor(video);
+               if (ret)
+                       return ret;
+       }
+
+       return hotplug(video);
+}
+
+static void video_sleep(struct uterm_video *video)
+{
+       if (!video_is_awake(video))
+               return;
+
+       drmDropMaster(video->drm.fd);
+       video->flags &= ~VIDEO_AWAKE;
+}
+
+static int video_wake_up(struct uterm_video *video)
+{
+       int ret;
+
+       if (video_is_awake(video))
+               return 0;
+
+       ret = drmSetMaster(video->drm.fd);
+       if (ret) {
+               log_err("cannot set DRM-master");
+               return -EACCES;
+       }
+
+       video->flags |= VIDEO_AWAKE;
+       ret = hotplug(video);
+       if (ret) {
+               video->flags &= ~VIDEO_AWAKE;
+               drmDropMaster(video->drm.fd);
+               return ret;
+       }
+
+       return 0;
+}
+
+const struct mode_ops drm_mode_ops = {
+       .init = NULL,
+       .destroy = NULL,
+       .get_name = mode_get_name,
+       .get_width = mode_get_width,
+       .get_height = mode_get_height,
+};
+
+const struct display_ops drm_display_ops = {
+       .init = NULL,
+       .destroy = NULL,
+       .activate = display_activate,
+       .deactivate = display_deactivate,
+       .set_dpms = display_set_dpms,
+       .use = display_use,
+       .swap = display_swap,
+};
+
+const struct video_ops drm_video_ops = {
+       .init = video_init,
+       .destroy = video_destroy,
+       .segfault = NULL, /* TODO: reset all saved CRTCs on segfault */
+       .poll = video_poll,
+       .sleep = video_sleep,
+       .wake_up = video_wake_up,
+};