[systemd-commits] 4 commits - configure.ac .gitignore Makefile.am src/libsystemd-terminal src/login

David Herrmann dvdhrm at kemper.freedesktop.org
Fri Sep 19 05:52:07 PDT 2014


 .gitignore                               |    1 
 Makefile.am                              |   18 
 configure.ac                             |    2 
 src/libsystemd-terminal/grdev-drm.c      | 2957 +++++++++++++++++++++++++++++++
 src/libsystemd-terminal/grdev-internal.h |  246 ++
 src/libsystemd-terminal/grdev.c          | 1283 +++++++++++++
 src/libsystemd-terminal/grdev.h          |  187 +
 src/libsystemd-terminal/modeset.c        |  491 +++++
 src/login/logind-session.c               |   21 
 src/login/logind-session.h               |    1 
 src/login/logind.c                       |    4 
 11 files changed, 5208 insertions(+), 3 deletions(-)

New commits:
commit 810626a80de8361dfbe27110d585023b3d6167a6
Author: David Herrmann <dh.herrmann at gmail.com>
Date:   Fri Sep 19 14:48:54 2014 +0200

    terminal: add systemd-modeset debugging tool
    
    The systemd-modeset tool is meant to debug grdev issues. It simply
    displays morphing colors on any found display. This is pretty handy to
    look for tearing in the backends and debug hotplug issues.
    
    Note that this tool requires systemd-logind to be compiled from git
    (there're important fixes that haven't been released, yet).

diff --git a/.gitignore b/.gitignore
index f865087..2889460 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,6 +90,7 @@
 /systemd-logind
 /systemd-machine-id-setup
 /systemd-machined
+/systemd-modeset
 /systemd-modules-load
 /systemd-multi-seat-x
 /systemd-networkd
diff --git a/Makefile.am b/Makefile.am
index be25023..f80ffc6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2987,6 +2987,7 @@ noinst_LTLIBRARIES += \
 
 noinst_PROGRAMS += \
 	systemd-evcat \
+	systemd-modeset \
 	systemd-subterm
 
 unifontdatadir=$(datadir)/unifont
@@ -3045,6 +3046,19 @@ systemd_evcat_LDADD = \
 	libsystemd-shared.la \
 	$(TERMINAL_LIBS)
 
+systemd_modeset_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(TERMINAL_CFLAGS)
+
+systemd_modeset_SOURCES = \
+	src/libsystemd-terminal/modeset.c
+
+systemd_modeset_LDADD = \
+	libsystemd-terminal.la \
+	libsystemd-internal.la \
+	libsystemd-shared.la \
+	$(TERMINAL_LIBS)
+
 systemd_subterm_SOURCES = \
 	src/libsystemd-terminal/subterm.c
 
diff --git a/src/libsystemd-terminal/modeset.c b/src/libsystemd-terminal/modeset.c
new file mode 100644
index 0000000..02ed1a8
--- /dev/null
+++ b/src/libsystemd-terminal/modeset.c
@@ -0,0 +1,491 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright (C) 2014 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  systemd is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+/*
+ * Modeset Testing
+ * The modeset tool attaches to the session of the caller and shows a
+ * test-pattern on all displays of this session. It is meant as debugging tool
+ * for the grdev infrastructure.
+ */
+
+#include <drm_fourcc.h>
+#include <errno.h>
+#include <getopt.h>
+#include <linux/kd.h>
+#include <linux/vt.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <systemd/sd-bus.h>
+#include <systemd/sd-event.h>
+#include <systemd/sd-login.h>
+#include <termios.h>
+#include <unistd.h>
+#include "build.h"
+#include "bus-util.h"
+#include "event-util.h"
+#include "grdev.h"
+#include "grdev-internal.h"
+#include "macro.h"
+#include "sysview.h"
+#include "util.h"
+
+typedef struct Modeset Modeset;
+
+struct Modeset {
+        char *session;
+        char *seat;
+        sd_event *event;
+        sd_bus *bus;
+        sd_event_source *exit_src;
+        sysview_context *sysview;
+        grdev_context *grdev;
+        grdev_session *grdev_session;
+
+        uint8_t r, g, b;
+        bool r_up, g_up, b_up;
+
+        bool my_tty : 1;
+        bool managed : 1;
+};
+
+static int modeset_exit_fn(sd_event_source *source, void *userdata) {
+        Modeset *m = userdata;
+
+        if (m->grdev_session)
+                grdev_session_restore(m->grdev_session);
+
+        return 0;
+}
+
+static Modeset *modeset_free(Modeset *m) {
+        if (!m)
+                return NULL;
+
+        m->grdev_session = grdev_session_free(m->grdev_session);
+        m->grdev = grdev_context_unref(m->grdev);
+        m->sysview = sysview_context_free(m->sysview);
+        m->exit_src = sd_event_source_unref(m->exit_src);
+        m->bus = sd_bus_unref(m->bus);
+        m->event = sd_event_unref(m->event);
+        free(m->seat);
+        free(m->session);
+        free(m);
+
+        return NULL;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Modeset*, modeset_free);
+
+static bool is_my_tty(const char *session) {
+        unsigned int vtnr;
+        struct stat st;
+        long mode;
+        int r;
+
+        /* Using logind's Controller API is highly fragile if there is already
+         * a session controller running. If it is registered as controller
+         * itself, TakeControl will simply fail. But if its a legacy controller
+         * that does not use logind's controller API, we must never register
+         * our own controller. Otherwise, we really mess up the VT. Therefore,
+         * only run in managed mode if there's no-one else.  Furthermore, never
+         * try to access graphics devices if there's someone else. Unlike input
+         * devices, graphics devies cannot be shared easily. */
+
+        if (!isatty(1))
+                return false;
+
+        if (!session)
+                return false;
+
+        r = sd_session_get_vt(session, &vtnr);
+        if (r < 0 || vtnr < 1 || vtnr > 63)
+                return false;
+
+        mode = 0;
+        r = ioctl(1, KDGETMODE, &mode);
+        if (r < 0 || mode != KD_TEXT)
+                return false;
+
+        r = fstat(1, &st);
+        if (r < 0 || minor(st.st_rdev) != vtnr)
+                return false;
+
+        return true;
+}
+
+static int modeset_new(Modeset **out) {
+        _cleanup_(modeset_freep) Modeset *m = NULL;
+        int r;
+
+        assert(out);
+
+        m = new0(Modeset, 1);
+        if (!m)
+                return log_oom();
+
+        r = sd_pid_get_session(getpid(), &m->session);
+        if (r < 0) {
+                log_error("Cannot retrieve logind session: %s", strerror(-r));
+                return r;
+        }
+
+        r = sd_session_get_seat(m->session, &m->seat);
+        if (r < 0) {
+                log_error("Cannot retrieve seat of logind session: %s", strerror(-r));
+                return r;
+        }
+
+        m->my_tty = is_my_tty(m->session);
+        m->managed = m->my_tty && geteuid() > 0;
+
+        m->r = rand() % 0xff;
+        m->g = rand() % 0xff;
+        m->b = rand() % 0xff;
+        m->r_up = m->g_up = m->b_up = true;
+
+        r = sd_event_default(&m->event);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_open_system(&m->bus);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_attach_event(m->bus, m->event, SD_EVENT_PRIORITY_NORMAL);
+        if (r < 0)
+                return r;
+
+        r = sigprocmask_many(SIG_BLOCK, SIGTERM, SIGINT, -1);
+        if (r < 0)
+                return r;
+
+        r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL);
+        if (r < 0)
+                return r;
+
+        r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
+        if (r < 0)
+                return r;
+
+        r = sd_event_add_exit(m->event, &m->exit_src, modeset_exit_fn, m);
+        if (r < 0)
+                return r;
+
+        /* schedule before sd-bus close */
+        r = sd_event_source_set_priority(m->exit_src, -10);
+        if (r < 0)
+                return r;
+
+        r = sysview_context_new(&m->sysview,
+                                SYSVIEW_CONTEXT_SCAN_LOGIND |
+                                SYSVIEW_CONTEXT_SCAN_DRM,
+                                m->event,
+                                m->bus,
+                                NULL);
+        if (r < 0)
+                return r;
+
+        r = grdev_context_new(&m->grdev, m->event, m->bus);
+        if (r < 0)
+                return r;
+
+        *out = m;
+        m = NULL;
+        return 0;
+}
+
+static uint8_t next_color(bool *up, uint8_t cur, unsigned int mod) {
+        uint8_t next;
+
+        /* generate smoothly morphing colors */
+
+        next = cur + (*up ? 1 : -1) * (rand() % mod);
+        if ((*up && next < cur) || (!*up && next > cur)) {
+                *up = !*up;
+                next = cur;
+        }
+
+        return next;
+}
+
+static void modeset_draw(Modeset *m, const grdev_display_target *t) {
+        uint32_t j, k, *b;
+        uint8_t *l;
+
+        assert(t->fb->format == DRM_FORMAT_XRGB8888 || t->fb->format == DRM_FORMAT_ARGB8888);
+        assert(!t->rotate);
+        assert(!t->flip);
+
+        l = t->fb->maps[0];
+        for (j = 0; j < t->height; ++j) {
+                for (k = 0; k < t->width; ++k) {
+                        b = (uint32_t*)l;
+                        b[k] = (0xff << 24) | (m->r << 16) | (m->g << 8) | m->b;
+                }
+
+                l += t->fb->strides[0];
+        }
+}
+
+static void modeset_render(Modeset *m, grdev_display *d) {
+        const grdev_display_target *t;
+
+        m->r = next_color(&m->r_up, m->r, 20);
+        m->g = next_color(&m->g_up, m->g, 10);
+        m->b = next_color(&m->b_up, m->b, 5);
+
+        GRDEV_DISPLAY_FOREACH_TARGET(d, t, 0) {
+                modeset_draw(m, t);
+                grdev_display_flip_target(d, t, 1);
+        }
+
+        grdev_session_commit(m->grdev_session);
+}
+
+static void modeset_grdev_fn(grdev_session *session, void *userdata, grdev_event *ev) {
+        Modeset *m = userdata;
+
+        switch (ev->type) {
+        case GRDEV_EVENT_DISPLAY_ADD:
+                grdev_display_enable(ev->display_add.display);
+                modeset_render(m, ev->display_add.display);
+                break;
+        case GRDEV_EVENT_DISPLAY_REMOVE:
+                break;
+        case GRDEV_EVENT_DISPLAY_CHANGE:
+                modeset_render(m, ev->display_change.display);
+                break;
+        case GRDEV_EVENT_DISPLAY_FRAME:
+                modeset_render(m, ev->display_frame.display);
+                break;
+        }
+}
+
+static int modeset_sysview_fn(sysview_context *c, void *userdata, sysview_event *ev) {
+        unsigned int flags, type;
+        Modeset *m = userdata;
+        sysview_device *d;
+        const char *name;
+        int r;
+
+        switch (ev->type) {
+        case SYSVIEW_EVENT_SESSION_FILTER:
+                if (streq_ptr(m->session, ev->session_filter.id))
+                        return 1;
+
+                break;
+        case SYSVIEW_EVENT_SESSION_ADD:
+                assert(!m->grdev_session);
+
+                name = sysview_session_get_name(ev->session_add.session);
+                flags = 0;
+
+                if (m->managed)
+                        flags |= GRDEV_SESSION_MANAGED;
+
+                r = grdev_session_new(&m->grdev_session,
+                                      m->grdev,
+                                      flags,
+                                      name,
+                                      modeset_grdev_fn,
+                                      m);
+                if (r < 0) {
+                        log_error("Cannot create grdev session: %s", strerror(-r));
+                        return r;
+                }
+
+                if (m->managed) {
+                        r = sysview_session_take_control(ev->session_add.session);
+                        if (r < 0) {
+                                log_error("Cannot request session control: %s", strerror(-r));
+                                return r;
+                        }
+                }
+
+                grdev_session_enable(m->grdev_session);
+
+                break;
+        case SYSVIEW_EVENT_SESSION_REMOVE:
+                if (!m->grdev_session)
+                        return 0;
+
+                grdev_session_restore(m->grdev_session);
+                grdev_session_disable(m->grdev_session);
+                m->grdev_session = grdev_session_free(m->grdev_session);
+                sd_event_exit(m->event, 0);
+                break;
+        case SYSVIEW_EVENT_SESSION_ATTACH:
+                d = ev->session_attach.device;
+                type = sysview_device_get_type(d);
+                if (type == SYSVIEW_DEVICE_DRM)
+                        grdev_session_add_drm(m->grdev_session, sysview_device_get_ud(d));
+
+                break;
+        case SYSVIEW_EVENT_SESSION_DETACH:
+                d = ev->session_detach.device;
+                type = sysview_device_get_type(d);
+                if (type == SYSVIEW_DEVICE_DRM)
+                        grdev_session_remove_drm(m->grdev_session, sysview_device_get_ud(d));
+
+                break;
+        case SYSVIEW_EVENT_SESSION_CONTROL:
+                r = ev->session_control.error;
+                if (r < 0) {
+                        log_error("Cannot acquire session control: %s", strerror(-r));
+                        return r;
+                }
+
+                r = ioctl(1, KDSKBMODE, K_UNICODE);
+                if (r < 0) {
+                        log_error("Cannot set K_UNICODE on stdout: %m");
+                        return -errno;
+                }
+
+                break;
+        }
+
+        return 0;
+}
+
+static int modeset_run(Modeset *m) {
+        struct termios in_attr, saved_attr;
+        int r;
+
+        assert(m);
+
+        if (!m->my_tty) {
+                log_warning("You need to run this program on a free VT");
+                return -EACCES;
+        }
+
+        if (!m->managed && geteuid() > 0)
+                log_warning("You run in unmanaged mode without being root. This is likely to fail..");
+
+        printf("modeset - Show test pattern on selected graphics devices\n"
+               "          Running on seat '%s' in user-session '%s'\n"
+               "          Exit by pressing ^C\n\n",
+               m->seat ? : "seat0", m->session ? : "<none>");
+
+        r = sysview_context_start(m->sysview, modeset_sysview_fn, m);
+        if (r < 0)
+                goto out;
+
+        r = tcgetattr(0, &in_attr);
+        if (r < 0) {
+                r = -errno;
+                goto out;
+        }
+
+        saved_attr = in_attr;
+        in_attr.c_lflag &= ~ECHO;
+
+        r = tcsetattr(0, TCSANOW, &in_attr);
+        if (r < 0) {
+                r = -errno;
+                goto out;
+        }
+
+        r = sd_event_loop(m->event);
+        tcsetattr(0, TCSANOW, &saved_attr);
+        printf("exiting..\n");
+
+out:
+        sysview_context_stop(m->sysview);
+        return r;
+}
+
+static int help(void) {
+        printf("%s [OPTIONS...]\n\n"
+               "Show test pattern on all selected graphics devices.\n\n"
+               "  -h --help               Show this help\n"
+               "     --version            Show package version\n"
+               , program_invocation_short_name);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        enum {
+                ARG_VERSION = 0x100,
+        };
+        static const struct option options[] = {
+                { "help",       no_argument,    NULL, 'h'               },
+                { "version",    no_argument,    NULL, ARG_VERSION       },
+                {},
+        };
+        int c;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+                switch (c) {
+                case 'h':
+                        help();
+                        return 0;
+
+                case ARG_VERSION:
+                        puts(PACKAGE_STRING);
+                        puts(SYSTEMD_FEATURES);
+                        return 0;
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached("Unhandled option");
+                }
+
+        if (argc > optind) {
+                log_error("Too many arguments");
+                return -EINVAL;
+        }
+
+        return 1;
+}
+
+int main(int argc, char *argv[]) {
+        _cleanup_(modeset_freep) Modeset *m = NULL;
+        int r;
+
+        log_set_target(LOG_TARGET_AUTO);
+        log_parse_environment();
+        log_open();
+
+        srand(time(NULL));
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                goto finish;
+
+        r = modeset_new(&m);
+        if (r < 0)
+                goto finish;
+
+        r = modeset_run(m);
+
+finish:
+        return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}

commit f22e0bce3732c1fd005b7a886042394e036bc1b3
Author: David Herrmann <dh.herrmann at gmail.com>
Date:   Fri Sep 19 14:13:06 2014 +0200

    terminal: add grdev DRM backend
    
    The grdev-drm backend manages DRM cards for grdev. Any DRM card with
    DUMB_BUFFER support can be used. So far, our policy is to configure all
    available connectors, but keep pipes inactive as long as users don't
    enable the displays on top.
    
    We hard-code double-buffering so far, but can easily support
    single-buffering or n-buffering. We also require XRGB8888 as format as
    this is required to be supported by all DRM drivers and it is what VTs
    use. This allows us to switch from VTs to grdev via page-flips instead of
    deep modesets.
    
    There is still a lot room for improvements in this backend, but it works
    smoothly so far so more enhanced features can be added later.

diff --git a/Makefile.am b/Makefile.am
index 1931c5d..be25023 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -3008,6 +3008,7 @@ libsystemd_terminal_la_SOURCES = \
 	src/libsystemd-terminal/grdev.h \
 	src/libsystemd-terminal/grdev-internal.h \
 	src/libsystemd-terminal/grdev.c \
+	src/libsystemd-terminal/grdev-drm.c \
 	src/libsystemd-terminal/idev.h \
 	src/libsystemd-terminal/idev-internal.h \
 	src/libsystemd-terminal/idev.c \
diff --git a/src/libsystemd-terminal/grdev-drm.c b/src/libsystemd-terminal/grdev-drm.c
new file mode 100644
index 0000000..3481584
--- /dev/null
+++ b/src/libsystemd-terminal/grdev-drm.c
@@ -0,0 +1,2957 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright (C) 2014 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  systemd is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <libudev.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <systemd/sd-bus.h>
+#include <systemd/sd-event.h>
+#include <unistd.h>
+
+/* Yuck! DRM headers need system headers included first.. but we have to
+ * include it before shared/missing.h to avoid redefining ioctl bits */
+#include <drm.h>
+#include <drm_fourcc.h>
+#include <drm_mode.h>
+
+#include "bus-util.h"
+#include "hashmap.h"
+#include "grdev.h"
+#include "grdev-internal.h"
+#include "macro.h"
+#include "udev-util.h"
+#include "util.h"
+
+#define GRDRM_MAX_TRIES (16)
+
+typedef struct grdrm_object grdrm_object;
+typedef struct grdrm_plane grdrm_plane;
+typedef struct grdrm_connector grdrm_connector;
+typedef struct grdrm_encoder grdrm_encoder;
+typedef struct grdrm_crtc grdrm_crtc;
+
+typedef struct grdrm_fb grdrm_fb;
+typedef struct grdrm_pipe grdrm_pipe;
+typedef struct grdrm_card grdrm_card;
+typedef struct unmanaged_card unmanaged_card;
+typedef struct managed_card managed_card;
+
+/*
+ * Objects
+ */
+
+enum {
+        GRDRM_TYPE_CRTC,
+        GRDRM_TYPE_ENCODER,
+        GRDRM_TYPE_CONNECTOR,
+        GRDRM_TYPE_PLANE,
+        GRDRM_TYPE_CNT
+};
+
+struct grdrm_object {
+        grdrm_card *card;
+        uint32_t id;
+        uint32_t index;
+        unsigned int type;
+        void (*free_fn) (grdrm_object *object);
+
+        bool present : 1;
+        bool assigned : 1;
+};
+
+struct grdrm_plane {
+        grdrm_object object;
+
+        struct {
+                uint32_t used_crtc;
+                uint32_t used_fb;
+                uint32_t gamma_size;
+
+                uint32_t n_crtcs;
+                uint32_t max_crtcs;
+                uint32_t *crtcs;
+                uint32_t n_formats;
+                uint32_t max_formats;
+                uint32_t *formats;
+        } kern;
+};
+
+struct grdrm_connector {
+        grdrm_object object;
+
+        struct {
+                uint32_t type;
+                uint32_t type_id;
+                uint32_t used_encoder;
+                uint32_t connection;
+                uint32_t mm_width;
+                uint32_t mm_height;
+                uint32_t subpixel;
+
+                uint32_t n_encoders;
+                uint32_t max_encoders;
+                uint32_t *encoders;
+                uint32_t n_modes;
+                uint32_t max_modes;
+                struct drm_mode_modeinfo *modes;
+                uint32_t n_props;
+                uint32_t max_props;
+                uint32_t *prop_ids;
+                uint64_t *prop_values;
+        } kern;
+};
+
+struct grdrm_encoder {
+        grdrm_object object;
+
+        struct {
+                uint32_t type;
+                uint32_t used_crtc;
+
+                uint32_t n_crtcs;
+                uint32_t max_crtcs;
+                uint32_t *crtcs;
+                uint32_t n_clones;
+                uint32_t max_clones;
+                uint32_t *clones;
+        } kern;
+};
+
+struct grdrm_crtc {
+        grdrm_object object;
+
+        struct {
+                uint32_t used_fb;
+                uint32_t fb_offset_x;
+                uint32_t fb_offset_y;
+                uint32_t gamma_size;
+
+                uint32_t n_used_connectors;
+                uint32_t max_used_connectors;
+                uint32_t *used_connectors;
+
+                bool mode_set;
+                struct drm_mode_modeinfo mode;
+        } kern;
+
+        struct {
+                bool set;
+                uint32_t fb;
+                uint32_t fb_x;
+                uint32_t fb_y;
+                uint32_t gamma;
+
+                uint32_t n_connectors;
+                uint32_t *connectors;
+
+                bool mode_set;
+                struct drm_mode_modeinfo mode;
+        } old;
+
+        struct {
+                struct drm_mode_modeinfo mode;
+                uint32_t n_connectors;
+                uint32_t max_connectors;
+                uint32_t *connectors;
+        } set;
+
+        grdrm_pipe *pipe;
+
+        bool applied : 1;
+};
+
+#define GRDRM_OBJECT_INIT(_card, _id, _index, _type, _free_fn) ((grdrm_object){ \
+                .card = (_card), \
+                .id = (_id), \
+                .index = (_index), \
+                .type = (_type), \
+                .free_fn = (_free_fn), \
+        })
+
+grdrm_object *grdrm_find_object(grdrm_card *card, uint32_t id);
+int grdrm_object_add(grdrm_object *object);
+grdrm_object *grdrm_object_free(grdrm_object *object);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdrm_object*, grdrm_object_free);
+
+int grdrm_plane_new(grdrm_plane **out, grdrm_card *card, uint32_t id, uint32_t index);
+int grdrm_connector_new(grdrm_connector **out, grdrm_card *card, uint32_t id, uint32_t index);
+int grdrm_encoder_new(grdrm_encoder **out, grdrm_card *card, uint32_t id, uint32_t index);
+int grdrm_crtc_new(grdrm_crtc **out, grdrm_card *card, uint32_t id, uint32_t index);
+
+#define plane_from_object(_obj) container_of((_obj), grdrm_plane, object)
+#define connector_from_object(_obj) container_of((_obj), grdrm_connector, object)
+#define encoder_from_object(_obj) container_of((_obj), grdrm_encoder, object)
+#define crtc_from_object(_obj) container_of((_obj), grdrm_crtc, object)
+
+/*
+ * Framebuffers
+ */
+
+struct grdrm_fb {
+        grdev_fb base;
+        grdrm_card *card;
+        uint32_t id;
+        uint32_t handles[4];
+        uint32_t offsets[4];
+        uint32_t sizes[4];
+        uint32_t flipid;
+};
+
+static int grdrm_fb_new(grdrm_fb **out, grdrm_card *card, const struct drm_mode_modeinfo *mode);
+grdrm_fb *grdrm_fb_free(grdrm_fb *fb);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdrm_fb*, grdrm_fb_free);
+
+#define fb_from_base(_fb) container_of((_fb), grdrm_fb, base)
+
+/*
+ * Pipes
+ */
+
+struct grdrm_pipe {
+        grdev_pipe base;
+        grdrm_crtc *crtc;
+        uint32_t counter;
+};
+
+#define grdrm_pipe_from_base(_e) container_of((_e), grdrm_pipe, base)
+
+#define GRDRM_PIPE_NAME_MAX (GRDRM_CARD_NAME_MAX + 1 + DECIMAL_STR_MAX(uint32_t))
+
+static const grdev_pipe_vtable grdrm_pipe_vtable;
+
+static int grdrm_pipe_new(grdrm_pipe **out, grdrm_crtc *crtc, struct drm_mode_modeinfo *mode, size_t n_fbs);
+
+/*
+ * Cards
+ */
+
+struct grdrm_card {
+        grdev_card base;
+
+        int fd;
+        sd_event_source *fd_src;
+
+        uint32_t n_crtcs;
+        uint32_t n_encoders;
+        uint32_t n_connectors;
+        uint32_t n_planes;
+        uint32_t max_ids;
+        Hashmap *object_map;
+
+        bool async_hotplug : 1;
+        bool running : 1;
+        bool ready : 1;
+        bool cap_dumb : 1;
+        bool cap_monotonic : 1;
+};
+
+struct unmanaged_card {
+        grdrm_card card;
+        char *devnode;
+};
+
+struct managed_card {
+        grdrm_card card;
+        dev_t devnum;
+
+        sd_bus_slot *slot_pause_device;
+        sd_bus_slot *slot_resume_device;
+        sd_bus_slot *slot_take_device;
+
+        bool requested : 1;             /* TakeDevice() was sent */
+        bool acquired : 1;              /* TakeDevice() was successful */
+        bool master : 1;                /* we are DRM-Master */
+};
+
+#define grdrm_card_from_base(_e) container_of((_e), grdrm_card, base)
+#define unmanaged_card_from_base(_e) \
+        container_of(grdrm_card_from_base(_e), unmanaged_card, card)
+#define managed_card_from_base(_e) \
+        container_of(grdrm_card_from_base(_e), managed_card, card)
+
+#define GRDRM_CARD_INIT(_vtable, _session) ((grdrm_card){ \
+                .base = GRDEV_CARD_INIT((_vtable), (_session)), \
+                .fd = -1, \
+                .max_ids = 32, \
+        })
+
+#define GRDRM_CARD_NAME_MAX (6 + DECIMAL_STR_MAX(unsigned) * 2)
+
+static const grdev_card_vtable unmanaged_card_vtable;
+static const grdev_card_vtable managed_card_vtable;
+
+static int grdrm_card_open(grdrm_card *card, int dev_fd);
+static void grdrm_card_close(grdrm_card *card);
+static bool grdrm_card_async(grdrm_card *card, int r);
+
+/*
+ * The page-flip event of the kernel provides 64bit of arbitrary user-data. As
+ * drivers tend to drop events on intermediate deep mode-sets or because we
+ * might receive events during session activation, we try to avoid allocaing
+ * dynamic data on those events. Instead, we safe the CRTC id plus a 32bit
+ * counter in there. This way, we only get 32bit counters, not 64bit, but that
+ * should be more than enough. On the bright side, we no longer care whether we
+ * lose events. No memory leaks will occur.
+ * Modern DRM drivers might be fixed to no longer leak events, but we want to
+ * be safe. And associating dynamically allocated data with those events is
+ * kinda ugly, anyway.
+ */
+
+static uint64_t grdrm_encode_vblank_data(uint32_t id, uint32_t counter) {
+        return id | ((uint64_t)counter << 32);
+}
+
+static void grdrm_decode_vblank_data(uint64_t data, uint32_t *out_id, uint32_t *out_counter) {
+        if (out_id)
+                *out_id = data & 0xffffffffU;
+        if (out_counter)
+                *out_counter = (data >> 32) & 0xffffffffU;
+}
+
+static bool grdrm_modes_compatible(const struct drm_mode_modeinfo *a, const struct drm_mode_modeinfo *b) {
+        assert(a);
+        assert(b);
+
+        /* Test whether both modes are compatible according to our internal
+         * assumptions on modes. This comparison is highly dependent on how
+         * we treat modes in grdrm. If we export mode details, we need to
+         * make this comparison much stricter. */
+
+        if (a->hdisplay != b->hdisplay)
+                return false;
+        if (a->vdisplay != b->vdisplay)
+                return false;
+
+        return true;
+}
+
+/*
+ * Objects
+ */
+
+grdrm_object *grdrm_find_object(grdrm_card *card, uint32_t id) {
+        assert_return(card, NULL);
+
+        return id > 0 ? hashmap_get(card->object_map, UINT32_TO_PTR(id)) : NULL;
+}
+
+int grdrm_object_add(grdrm_object *object) {
+        int r;
+
+        assert(object);
+        assert(object->card);
+        assert(object->id > 0);
+        assert(IN_SET(object->type, GRDRM_TYPE_CRTC, GRDRM_TYPE_ENCODER, GRDRM_TYPE_CONNECTOR, GRDRM_TYPE_PLANE));
+        assert(object->free_fn);
+
+        if (object->index >= 32)
+                log_debug("grdrm: %s: object index exceeds 32bit masks: type=%u, index=%" PRIu32,
+                          object->card->base.name, object->type, object->index);
+
+        r = hashmap_put(object->card->object_map, UINT32_TO_PTR(object->id), object);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+grdrm_object *grdrm_object_free(grdrm_object *object) {
+        if (!object)
+                return NULL;
+
+        assert(object->card);
+        assert(object->id > 0);
+        assert(IN_SET(object->type, GRDRM_TYPE_CRTC, GRDRM_TYPE_ENCODER, GRDRM_TYPE_CONNECTOR, GRDRM_TYPE_PLANE));
+        assert(object->free_fn);
+
+        hashmap_remove_value(object->card->object_map, UINT32_TO_PTR(object->id), object);
+
+        object->free_fn(object);
+        return NULL;
+}
+
+/*
+ * Planes
+ */
+
+static void plane_free(grdrm_object *object) {
+        grdrm_plane *plane = plane_from_object(object);
+
+        free(plane->kern.formats);
+        free(plane->kern.crtcs);
+        free(plane);
+}
+
+int grdrm_plane_new(grdrm_plane **out, grdrm_card *card, uint32_t id, uint32_t index) {
+        _cleanup_(grdrm_object_freep) grdrm_object *object = NULL;
+        grdrm_plane *plane;
+        int r;
+
+        assert(card);
+
+        plane = new0(grdrm_plane, 1);
+        if (!plane)
+                return -ENOMEM;
+
+        object = &plane->object;
+        *object = GRDRM_OBJECT_INIT(card, id, index, GRDRM_TYPE_PLANE, plane_free);
+
+        plane->kern.max_crtcs = 32;
+        plane->kern.crtcs = new0(uint32_t, plane->kern.max_crtcs);
+        if (!plane->kern.crtcs)
+                return -ENOMEM;
+
+        plane->kern.max_formats = 32;
+        plane->kern.formats = new0(uint32_t, plane->kern.max_formats);
+        if (!plane->kern.formats)
+                return -ENOMEM;
+
+        r = grdrm_object_add(object);
+        if (r < 0)
+                return r;
+
+        if (out)
+                *out = plane;
+        object = NULL;
+        return 0;
+}
+
+static int grdrm_plane_resync(grdrm_plane *plane) {
+        grdrm_card *card = plane->object.card;
+        size_t tries;
+        int r;
+
+        assert(plane);
+
+        for (tries = 0; tries < GRDRM_MAX_TRIES; ++tries) {
+                struct drm_mode_get_plane res;
+                grdrm_object *object;
+                bool resized = false;
+                Iterator iter;
+
+                zero(res);
+                res.plane_id = plane->object.id;
+                res.format_type_ptr = PTR_TO_UINT64(plane->kern.formats);
+                res.count_format_types = plane->kern.max_formats;
+
+                r = ioctl(card->fd, DRM_IOCTL_MODE_GETPLANE, &res);
+                if (r < 0) {
+                        r = -errno;
+                        if (r == -ENOENT) {
+                                card->async_hotplug = true;
+                                r = 0;
+                                log_debug("grdrm: %s: plane %u removed during resync", card->base.name, plane->object.id);
+                        } else {
+                                log_debug("grdrm: %s: cannot retrieve plane %u: %m", card->base.name, plane->object.id);
+                        }
+
+                        return r;
+                }
+
+                plane->kern.n_crtcs = 0;
+                memzero(plane->kern.crtcs, sizeof(uint32_t) * plane->kern.max_crtcs);
+
+                HASHMAP_FOREACH(object, card->object_map, iter) {
+                        if (object->type != GRDRM_TYPE_CRTC || object->index >= 32)
+                                continue;
+                        if (!(res.possible_crtcs & (1 << object->index)))
+                                continue;
+                        if (plane->kern.n_crtcs >= 32) {
+                                log_debug("grdrm: %s: possible_crtcs of plane %" PRIu32 " exceeds 32bit mask",
+                                          card->base.name, plane->object.id);
+                                continue;
+                        }
+
+                        plane->kern.crtcs[plane->kern.n_crtcs++] = object->id;
+                }
+
+                if (res.count_format_types > plane->kern.max_formats) {
+                        uint32_t max, *t;
+
+                        max = ALIGN_POWER2(res.count_format_types);
+                        if (!max || max > UINT16_MAX) {
+                                log_debug("grdrm: %s: excessive plane resource limit: %" PRIu32, card->base.name, max);
+                                return -ERANGE;
+                        }
+
+                        t = realloc(plane->kern.formats, sizeof(*t) * max);
+                        if (!t)
+                                return -ENOMEM;
+
+                        plane->kern.formats = t;
+                        plane->kern.max_formats = max;
+                        resized = true;
+                }
+
+                if (resized)
+                        continue;
+
+                plane->kern.n_formats = res.count_format_types;
+                plane->kern.used_crtc = res.crtc_id;
+                plane->kern.used_fb = res.fb_id;
+                plane->kern.gamma_size = res.gamma_size;
+
+                break;
+        }
+
+        if (tries >= GRDRM_MAX_TRIES) {
+                log_debug("grdrm: %s: plane %u not settled for retrieval", card->base.name, plane->object.id);
+                return -EFAULT;
+        }
+
+        return 0;
+}
+
+/*
+ * Connectors
+ */
+
+static void connector_free(grdrm_object *object) {
+        grdrm_connector *connector = connector_from_object(object);
+
+        free(connector->kern.prop_values);
+        free(connector->kern.prop_ids);
+        free(connector->kern.modes);
+        free(connector->kern.encoders);
+        free(connector);
+}
+
+int grdrm_connector_new(grdrm_connector **out, grdrm_card *card, uint32_t id, uint32_t index) {
+        _cleanup_(grdrm_object_freep) grdrm_object *object = NULL;
+        grdrm_connector *connector;
+        int r;
+
+        assert(card);
+
+        connector = new0(grdrm_connector, 1);
+        if (!connector)
+                return -ENOMEM;
+
+        object = &connector->object;
+        *object = GRDRM_OBJECT_INIT(card, id, index, GRDRM_TYPE_CONNECTOR, connector_free);
+
+        connector->kern.max_encoders = 32;
+        connector->kern.encoders = new0(uint32_t, connector->kern.max_encoders);
+        if (!connector->kern.encoders)
+                return -ENOMEM;
+
+        connector->kern.max_modes = 32;
+        connector->kern.modes = new0(struct drm_mode_modeinfo, connector->kern.max_modes);
+        if (!connector->kern.modes)
+                return -ENOMEM;
+
+        connector->kern.max_props = 32;
+        connector->kern.prop_ids = new0(uint32_t, connector->kern.max_props);
+        connector->kern.prop_values = new0(uint64_t, connector->kern.max_props);
+        if (!connector->kern.prop_ids || !connector->kern.prop_values)
+                return -ENOMEM;
+
+        r = grdrm_object_add(object);
+        if (r < 0)
+                return r;
+
+        if (out)
+                *out = connector;
+        object = NULL;
+        return 0;
+}
+
+static int grdrm_connector_resync(grdrm_connector *connector) {
+        grdrm_card *card = connector->object.card;
+        size_t tries;
+        int r;
+
+        assert(connector);
+
+        for (tries = 0; tries < GRDRM_MAX_TRIES; ++tries) {
+                struct drm_mode_get_connector res;
+                bool resized = false;
+                uint32_t max;
+
+                zero(res);
+                res.connector_id = connector->object.id;
+                res.encoders_ptr = PTR_TO_UINT64(connector->kern.encoders);
+                res.props_ptr = PTR_TO_UINT64(connector->kern.prop_ids);
+                res.prop_values_ptr = PTR_TO_UINT64(connector->kern.prop_values);
+                res.count_encoders = connector->kern.max_encoders;
+                res.count_props = connector->kern.max_props;
+
+                /* Retrieve modes only if we have none. This avoids expensive
+                 * EDID reads in the kernel, that can slow down resyncs
+                 * considerably! */
+                if (connector->kern.n_modes == 0) {
+                        res.modes_ptr = PTR_TO_UINT64(connector->kern.modes);
+                        res.count_modes = connector->kern.max_modes;
+                }
+
+                r = ioctl(card->fd, DRM_IOCTL_MODE_GETCONNECTOR, &res);
+                if (r < 0) {
+                        r = -errno;
+                        if (r == -ENOENT) {
+                                card->async_hotplug = true;
+                                r = 0;
+                                log_debug("grdrm: %s: connector %u removed during resync", card->base.name, connector->object.id);
+                        } else {
+                                log_debug("grdrm: %s: cannot retrieve connector %u: %m", card->base.name, connector->object.id);
+                        }
+
+                        return r;
+                }
+
+                if (res.count_encoders > connector->kern.max_encoders) {
+                        uint32_t *t;
+
+                        max = ALIGN_POWER2(res.count_encoders);
+                        if (!max || max > UINT16_MAX) {
+                                log_debug("grdrm: %s: excessive connector resource limit: %" PRIu32, card->base.name, max);
+                                return -ERANGE;
+                        }
+
+                        t = realloc(connector->kern.encoders, sizeof(*t) * max);
+                        if (!t)
+                                return -ENOMEM;
+
+                        connector->kern.encoders = t;
+                        connector->kern.max_encoders = max;
+                        resized = true;
+                }
+
+                if (res.count_modes > connector->kern.max_modes) {
+                        struct drm_mode_modeinfo *t;
+
+                        max = ALIGN_POWER2(res.count_modes);
+                        if (!max || max > UINT16_MAX) {
+                                log_debug("grdrm: %s: excessive connector resource limit: %" PRIu32, card->base.name, max);
+                                return -ERANGE;
+                        }
+
+                        t = realloc(connector->kern.modes, sizeof(*t) * max);
+                        if (!t)
+                                return -ENOMEM;
+
+                        connector->kern.modes = t;
+                        connector->kern.max_modes = max;
+                        resized = true;
+                }
+
+                if (res.count_props > connector->kern.max_props) {
+                        uint32_t *tids;
+                        uint64_t *tvals;
+
+                        max = ALIGN_POWER2(res.count_props);
+                        if (!max || max > UINT16_MAX) {
+                                log_debug("grdrm: %s: excessive connector resource limit: %" PRIu32, card->base.name, max);
+                                return -ERANGE;
+                        }
+
+                        tids = realloc(connector->kern.prop_ids, sizeof(*tids) * max);
+                        if (!tids)
+                                return -ENOMEM;
+                        connector->kern.prop_ids = tids;
+
+                        tvals = realloc(connector->kern.prop_values, sizeof(*tvals) * max);
+                        if (!tvals)
+                                return -ENOMEM;
+                        connector->kern.prop_values = tvals;
+
+                        connector->kern.max_props = max;
+                        resized = true;
+                }
+
+                if (resized)
+                        continue;
+
+                connector->kern.n_encoders = res.count_encoders;
+                connector->kern.n_modes = res.count_modes;
+                connector->kern.n_props = res.count_props;
+                connector->kern.type = res.connector_type;
+                connector->kern.type_id = res.connector_type_id;
+                connector->kern.used_encoder = res.encoder_id;
+                connector->kern.connection = res.connection;
+                connector->kern.mm_width = res.mm_width;
+                connector->kern.mm_height = res.mm_height;
+                connector->kern.subpixel = res.subpixel;
+
+                break;
+        }
+
+        if (tries >= GRDRM_MAX_TRIES) {
+                log_debug("grdrm: %s: connector %u not settled for retrieval", card->base.name, connector->object.id);
+                return -EFAULT;
+        }
+
+        return 0;
+}
+
+/*
+ * Encoders
+ */
+
+static void encoder_free(grdrm_object *object) {
+        grdrm_encoder *encoder = encoder_from_object(object);
+
+        free(encoder->kern.clones);
+        free(encoder->kern.crtcs);
+        free(encoder);
+}
+
+int grdrm_encoder_new(grdrm_encoder **out, grdrm_card *card, uint32_t id, uint32_t index) {
+        _cleanup_(grdrm_object_freep) grdrm_object *object = NULL;
+        grdrm_encoder *encoder;
+        int r;
+
+        assert(card);
+
+        encoder = new0(grdrm_encoder, 1);
+        if (!encoder)
+                return -ENOMEM;
+
+        object = &encoder->object;
+        *object = GRDRM_OBJECT_INIT(card, id, index, GRDRM_TYPE_ENCODER, encoder_free);
+
+        encoder->kern.max_crtcs = 32;
+        encoder->kern.crtcs = new0(uint32_t, encoder->kern.max_crtcs);
+        if (!encoder->kern.crtcs)
+                return -ENOMEM;
+
+        encoder->kern.max_clones = 32;
+        encoder->kern.clones = new0(uint32_t, encoder->kern.max_clones);
+        if (!encoder->kern.clones)
+                return -ENOMEM;
+
+        r = grdrm_object_add(object);
+        if (r < 0)
+                return r;
+
+        if (out)
+                *out = encoder;
+        object = NULL;
+        return 0;
+}
+
+static int grdrm_encoder_resync(grdrm_encoder *encoder) {
+        grdrm_card *card = encoder->object.card;
+        struct drm_mode_get_encoder res;
+        grdrm_object *object;
+        Iterator iter;
+        int r;
+
+        assert(encoder);
+
+        zero(res);
+        res.encoder_id = encoder->object.id;
+
+        r = ioctl(card->fd, DRM_IOCTL_MODE_GETENCODER, &res);
+        if (r < 0) {
+                r = -errno;
+                if (r == -ENOENT) {
+                        card->async_hotplug = true;
+                        r = 0;
+                        log_debug("grdrm: %s: encoder %u removed during resync", card->base.name, encoder->object.id);
+                } else {
+                        log_debug("grdrm: %s: cannot retrieve encoder %u: %m", card->base.name, encoder->object.id);
+                }
+
+                return r;
+        }
+
+        encoder->kern.type = res.encoder_type;
+        encoder->kern.used_crtc = res.crtc_id;
+
+        encoder->kern.n_crtcs = 0;
+        memzero(encoder->kern.crtcs, sizeof(uint32_t) * encoder->kern.max_crtcs);
+
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (object->type != GRDRM_TYPE_CRTC || object->index >= 32)
+                        continue;
+                if (!(res.possible_crtcs & (1 << object->index)))
+                        continue;
+                if (encoder->kern.n_crtcs >= 32) {
+                        log_debug("grdrm: %s: possible_crtcs exceeds 32bit mask", card->base.name);
+                        continue;
+                }
+
+                encoder->kern.crtcs[encoder->kern.n_crtcs++] = object->id;
+        }
+
+        encoder->kern.n_clones = 0;
+        memzero(encoder->kern.clones, sizeof(uint32_t) * encoder->kern.max_clones);
+
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (object->type != GRDRM_TYPE_ENCODER || object->index >= 32)
+                        continue;
+                if (!(res.possible_clones & (1 << object->index)))
+                        continue;
+                if (encoder->kern.n_clones >= 32) {
+                        log_debug("grdrm: %s: possible_encoders exceeds 32bit mask", card->base.name);
+                        continue;
+                }
+
+                encoder->kern.clones[encoder->kern.n_clones++] = object->id;
+        }
+
+        return 0;
+}
+
+/*
+ * Crtcs
+ */
+
+static void crtc_free(grdrm_object *object) {
+        grdrm_crtc *crtc = crtc_from_object(object);
+
+        if (crtc->pipe)
+                grdev_pipe_free(&crtc->pipe->base);
+        free(crtc->set.connectors);
+        free(crtc->old.connectors);
+        free(crtc->kern.used_connectors);
+        free(crtc);
+}
+
+int grdrm_crtc_new(grdrm_crtc **out, grdrm_card *card, uint32_t id, uint32_t index) {
+        _cleanup_(grdrm_object_freep) grdrm_object *object = NULL;
+        grdrm_crtc *crtc;
+        int r;
+
+        assert(card);
+
+        crtc = new0(grdrm_crtc, 1);
+        if (!crtc)
+                return -ENOMEM;
+
+        object = &crtc->object;
+        *object = GRDRM_OBJECT_INIT(card, id, index, GRDRM_TYPE_CRTC, crtc_free);
+
+        crtc->kern.max_used_connectors = 32;
+        crtc->kern.used_connectors = new0(uint32_t, crtc->kern.max_used_connectors);
+        if (!crtc->kern.used_connectors)
+                return -ENOMEM;
+
+        crtc->old.connectors = new0(uint32_t, crtc->kern.max_used_connectors);
+        if (!crtc->old.connectors)
+                return -ENOMEM;
+
+        r = grdrm_object_add(object);
+        if (r < 0)
+                return r;
+
+        if (out)
+                *out = crtc;
+        object = NULL;
+        return 0;
+}
+
+static int grdrm_crtc_resync(grdrm_crtc *crtc) {
+        grdrm_card *card = crtc->object.card;
+        struct drm_mode_crtc res = { .crtc_id = crtc->object.id };
+        int r;
+
+        assert(crtc);
+
+        /* make sure we can cache any combination later */
+        if (card->n_connectors > crtc->kern.max_used_connectors) {
+                uint32_t max, *t;
+
+                max = ALIGN_POWER2(card->n_connectors);
+                if (!max)
+                        return -ENOMEM;
+
+                t = realloc_multiply(crtc->kern.used_connectors, sizeof(*t), max);
+                if (!t)
+                        return -ENOMEM;
+
+                crtc->kern.used_connectors = t;
+                crtc->kern.max_used_connectors = max;
+
+                if (!crtc->old.set) {
+                        crtc->old.connectors = calloc(sizeof(*t), max);
+                        if (!crtc->old.connectors)
+                                return -ENOMEM;
+                }
+        }
+
+        /* GETCRTC doesn't return connectors. We have to read all
+         * encoder-state and deduce the setup ourselves.. */
+        crtc->kern.n_used_connectors = 0;
+
+        r = ioctl(card->fd, DRM_IOCTL_MODE_GETCRTC, &res);
+        if (r < 0) {
+                r = -errno;
+                if (r == -ENOENT) {
+                        card->async_hotplug = true;
+                        r = 0;
+                        log_debug("grdrm: %s: crtc %u removed during resync", card->base.name, crtc->object.id);
+                } else {
+                        log_debug("grdrm: %s: cannot retrieve crtc %u: %m", card->base.name, crtc->object.id);
+                }
+
+                return r;
+        }
+
+        crtc->kern.used_fb = res.fb_id;
+        crtc->kern.fb_offset_x = res.x;
+        crtc->kern.fb_offset_y = res.y;
+        crtc->kern.gamma_size = res.gamma_size;
+        crtc->kern.mode_set = res.mode_valid;
+        crtc->kern.mode = res.mode;
+
+        return 0;
+}
+
+static void grdrm_crtc_assign(grdrm_crtc *crtc, grdrm_connector *connector) {
+        uint32_t n_connectors;
+        int r;
+
+        assert(crtc);
+        assert(!crtc->object.assigned);
+        assert(!connector || !connector->object.assigned);
+
+        /* always mark both as assigned; even if assignments cannot be set */
+        crtc->object.assigned = true;
+        if (connector)
+                connector->object.assigned = true;
+
+        /* we will support hw clone mode in the future */
+        n_connectors = connector ? 1 : 0;
+
+        /* bail out if configuration is preserved */
+        if (crtc->set.n_connectors == n_connectors &&
+            (n_connectors == 0 || crtc->set.connectors[0] == connector->object.id))
+                return;
+
+        crtc->applied = false;
+        crtc->set.n_connectors = 0;
+
+        if (n_connectors > crtc->set.max_connectors) {
+                uint32_t max, *t;
+
+                max = ALIGN_POWER2(n_connectors);
+                if (!max) {
+                        r = -ENOMEM;
+                        goto error;
+                }
+
+                t = realloc(crtc->set.connectors, sizeof(*t) * max);
+                if (!t) {
+                        r = -ENOMEM;
+                        goto error;
+                }
+
+                crtc->set.connectors = t;
+                crtc->set.max_connectors = max;
+        }
+
+        if (connector) {
+                struct drm_mode_modeinfo *m, *pref = NULL;
+                uint32_t i;
+
+                for (i = 0; i < connector->kern.n_modes; ++i) {
+                        m = &connector->kern.modes[i];
+
+                        /* ignore 3D modes by default */
+                        if (m->flags & DRM_MODE_FLAG_3D_MASK)
+                                continue;
+
+                        if (!pref) {
+                                pref = m;
+                                continue;
+                        }
+
+                        /* use PREFERRED over non-PREFERRED */
+                        if ((pref->type & DRM_MODE_TYPE_PREFERRED) &&
+                            !(m->type & DRM_MODE_TYPE_PREFERRED))
+                                continue;
+
+                        /* use DRIVER over non-PREFERRED|DRIVER */
+                        if ((pref->type & DRM_MODE_TYPE_DRIVER) &&
+                            !(m->type & (DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED)))
+                                continue;
+
+                        /* always prefer higher resolution */
+                        if (pref->hdisplay > m->hdisplay ||
+                            (pref->hdisplay == m->hdisplay && pref->vdisplay > m->vdisplay))
+                                continue;
+
+                        pref = m;
+                }
+
+                if (pref) {
+                        crtc->set.mode = *pref;
+                        crtc->set.n_connectors = 1;
+                        crtc->set.connectors[0] = connector->object.id;
+                        log_debug("grdrm: %s: assigned connector %" PRIu32 " to crtc %" PRIu32 " with mode %s",
+                                  crtc->object.card->base.name, connector->object.id, crtc->object.id, pref->name);
+                } else {
+                        log_debug("grdrm: %s: connector %" PRIu32 " to be assigned but has no valid mode",
+                                  crtc->object.card->base.name, connector->object.id);
+                }
+        }
+
+        return;
+
+error:
+        log_debug("grdrm: %s: cannot assign crtc %" PRIu32 ": %s",
+                  crtc->object.card->base.name, crtc->object.id, strerror(-r));
+}
+
+static void grdrm_crtc_expose(grdrm_crtc *crtc) {
+        grdrm_pipe *pipe;
+        grdrm_fb *fb;
+        size_t i;
+        int r;
+
+        assert(crtc);
+        assert(crtc->object.assigned);
+
+        if (crtc->set.n_connectors < 1) {
+                if (crtc->pipe)
+                        grdev_pipe_free(&crtc->pipe->base);
+                crtc->pipe = NULL;
+                return;
+        }
+
+        pipe = crtc->pipe;
+        if (pipe) {
+                if (pipe->base.width != crtc->set.mode.hdisplay ||
+                    pipe->base.height != crtc->set.mode.vdisplay) {
+                        grdev_pipe_free(&pipe->base);
+                        crtc->pipe = NULL;
+                        pipe = NULL;
+                }
+        }
+
+        if (crtc->pipe) {
+                pipe->base.front = NULL;
+                pipe->base.back = NULL;
+                for (i = 0; i < pipe->base.max_fbs; ++i) {
+                        fb = fb_from_base(pipe->base.fbs[i]);
+                        if (fb->id == crtc->kern.used_fb)
+                                pipe->base.front = &fb->base;
+                        else if (!fb->flipid)
+                                pipe->base.back = &fb->base;
+                }
+        } else {
+                r = grdrm_pipe_new(&pipe, crtc, &crtc->set.mode, 2);
+                if (r < 0) {
+                        log_debug("grdrm: %s: cannot create pipe for crtc %" PRIu32 ": %s",
+                                  crtc->object.card->base.name, crtc->object.id, strerror(-r));
+                        return;
+                }
+
+                for (i = 0; i < pipe->base.max_fbs; ++i) {
+                        r = grdrm_fb_new(&fb, crtc->object.card, &crtc->set.mode);
+                        if (r < 0) {
+                                log_debug("grdrm: %s: cannot allocate framebuffer for crtc %" PRIu32 ": %s",
+                                          crtc->object.card->base.name, crtc->object.id, strerror(-r));
+                                grdev_pipe_free(&pipe->base);
+                                return;
+                        }
+
+                        pipe->base.fbs[i] = &fb->base;
+                }
+
+                pipe->base.front = NULL;
+                pipe->base.back = pipe->base.fbs[0];
+                crtc->pipe = pipe;
+        }
+
+        grdev_pipe_ready(&crtc->pipe->base, true);
+}
+
+static void grdrm_crtc_commit(grdrm_crtc *crtc) {
+        struct drm_mode_crtc_page_flip page_flip = { .crtc_id = crtc->object.id };
+        struct drm_mode_crtc set_crtc = { .crtc_id = crtc->object.id };
+        grdrm_card *card = crtc->object.card;
+        grdrm_pipe *pipe;
+        grdev_fb **slot;
+        grdrm_fb *fb;
+        uint32_t cnt;
+        size_t i;
+        int r;
+
+        assert(crtc);
+        assert(crtc->object.assigned);
+
+        pipe = crtc->pipe;
+        if (!pipe) {
+                /* If a crtc is not assigned any connector, we want any
+                 * previous setup to be cleared, so make sure the CRTC is
+                 * disabled. Otherwise, there might be content on the CRTC
+                 * while we run, which is not what we want.
+                 * If you want to avoid modesets on specific CRTCs, you should
+                 * still keep their assignment, but never enable the resulting
+                 * pipe. This way, we wouldn't touch it at all. */
+                if (!crtc->applied) {
+                        crtc->applied = true;
+                        r = ioctl(card->fd, DRM_IOCTL_MODE_SETCRTC, &set_crtc);
+                        if (r < 0) {
+                                r = -errno;
+                                log_debug("grdrm: %s: cannot shutdown crtc %" PRIu32 ": %m",
+                                          card->base.name, crtc->object.id);
+
+                                grdrm_card_async(card, r);
+                                return;
+                        }
+
+                        log_debug("grdrm: %s: crtc %" PRIu32 " applied via shutdown",
+                                  card->base.name, crtc->object.id);
+                }
+
+                return;
+        }
+
+        /* we always fully ignore disabled pipes */
+        if (!pipe->base.enabled)
+                return;
+
+        assert(crtc->set.n_connectors > 0);
+
+        if (pipe->base.flip)
+                slot = &pipe->base.back;
+        else if (!crtc->applied)
+                slot = &pipe->base.front;
+        else
+                return;
+
+        if (!*slot)
+                return;
+
+        fb = fb_from_base(*slot);
+
+        if (crtc->applied || grdrm_modes_compatible(&crtc->kern.mode, &crtc->set.mode)) {
+                cnt = ++pipe->counter ? : ++pipe->counter;
+                page_flip.fb_id = fb->id;
+                page_flip.flags = DRM_MODE_PAGE_FLIP_EVENT;
+                page_flip.user_data = grdrm_encode_vblank_data(crtc->object.id, cnt);
+
+                r = ioctl(card->fd, DRM_IOCTL_MODE_PAGE_FLIP, &page_flip);
+                if (r < 0) {
+                        r = -errno;
+                        log_debug("grdrm: %s: cannot schedule page-flip on crtc %" PRIu32 ": %m",
+                                  card->base.name, crtc->object.id);
+
+                        if (grdrm_card_async(card, r))
+                                return;
+
+                        /* fall through to deep modeset */
+                } else {
+                        if (!crtc->applied) {
+                                log_debug("grdrm: %s: crtc %" PRIu32 " applied via page flip",
+                                          card->base.name, crtc->object.id);
+                                crtc->applied = true;
+                        }
+
+                        pipe->base.flipping = true;
+                        pipe->counter = cnt;
+                        fb->flipid = cnt;
+                        *slot = NULL;
+
+                        if (!pipe->base.back) {
+                                for (i = 0; i < pipe->base.max_fbs; ++i) {
+                                        if (!pipe->base.fbs[i])
+                                                continue;
+
+                                        fb = fb_from_base(pipe->base.fbs[i]);
+                                        if (&fb->base == pipe->base.front)
+                                                continue;
+                                        if (fb->flipid)
+                                                continue;
+
+                                        pipe->base.back = &fb->base;
+                                        break;
+                                }
+                        }
+                }
+        }
+
+        if (!crtc->applied) {
+                set_crtc.set_connectors_ptr = PTR_TO_UINT64(crtc->set.connectors);
+                set_crtc.count_connectors = crtc->set.n_connectors;
+                set_crtc.fb_id = fb->id;
+                set_crtc.x = 0;
+                set_crtc.y = 0;
+                set_crtc.mode_valid = 1;
+                set_crtc.mode = crtc->set.mode;
+
+                r = ioctl(card->fd, DRM_IOCTL_MODE_SETCRTC, &set_crtc);
+                if (r < 0) {
+                        r = -errno;
+                        log_debug("grdrm: %s: cannot set crtc %" PRIu32 ": %m",
+                                  card->base.name, crtc->object.id);
+
+                        grdrm_card_async(card, r);
+                        return;
+                }
+
+                if (!crtc->applied) {
+                        log_debug("grdrm: %s: crtc %" PRIu32 " applied via deep modeset",
+                                  card->base.name, crtc->object.id);
+                        crtc->applied = true;
+                }
+
+                *slot = NULL;
+                pipe->base.front = &fb->base;
+                fb->flipid = 0;
+                ++pipe->counter;
+                pipe->base.flipping = false;
+
+                if (!pipe->base.back) {
+                        for (i = 0; i < pipe->base.max_fbs; ++i) {
+                                if (!pipe->base.fbs[i])
+                                        continue;
+
+                                fb = fb_from_base(pipe->base.fbs[i]);
+                                if (&fb->base == pipe->base.front)
+                                        continue;
+
+                                fb->flipid = 0;
+                                pipe->base.back = &fb->base;
+                                break;
+                        }
+                }
+        }
+
+        pipe->base.flip = false;
+}
+
+static void grdrm_crtc_restore(grdrm_crtc *crtc) {
+        struct drm_mode_crtc set_crtc = { .crtc_id = crtc->object.id };
+        grdrm_card *card = crtc->object.card;
+        int r;
+
+        if (!crtc->old.set)
+                return;
+
+        set_crtc.set_connectors_ptr = PTR_TO_UINT64(crtc->old.connectors);
+        set_crtc.count_connectors = crtc->old.n_connectors;
+        set_crtc.fb_id = crtc->old.fb;
+        set_crtc.x = crtc->old.fb_x;
+        set_crtc.y = crtc->old.fb_y;
+        set_crtc.gamma_size = crtc->old.gamma;
+        set_crtc.mode_valid = crtc->old.mode_set;
+        set_crtc.mode = crtc->old.mode;
+
+        r = ioctl(card->fd, DRM_IOCTL_MODE_SETCRTC, &set_crtc);
+        if (r < 0) {
+                r = -errno;
+                log_debug("grdrm: %s: cannot restore crtc %" PRIu32 ": %m",
+                          card->base.name, crtc->object.id);
+
+                grdrm_card_async(card, r);
+                return;
+        }
+
+        if (crtc->pipe) {
+                ++crtc->pipe->counter;
+                crtc->pipe->base.front = NULL;
+                crtc->pipe->base.flipping = false;
+        }
+
+        log_debug("grdrm: %s: crtc %" PRIu32 " restored", card->base.name, crtc->object.id);
+}
+
+static void grdrm_crtc_flip_complete(grdrm_crtc *crtc, uint32_t counter, struct drm_event_vblank *event) {
+        bool flipped = false;
+        grdrm_pipe *pipe;
+        grdrm_fb *back = NULL;
+        size_t i;
+
+        assert(crtc);
+        assert(event);
+
+        pipe = crtc->pipe;
+        if (!pipe)
+                return;
+
+        /* We got a page-flip event. To be safe, we reset all FBs on the same
+         * pipe that have smaller flipids than the flip we got as we know they
+         * are executed in order. We need to do this to guarantee
+         * queue-overflows or other missed events don't cause starvation.
+         * Furthermore, if we find the exact FB this event is for, *and* this
+         * is the most recent event, we mark it as front FB and raise a
+         * frame event. */
+
+        for (i = 0; i < pipe->base.max_fbs; ++i) {
+                grdrm_fb *fb;
+
+                if (!pipe->base.fbs[i])
+                        continue;
+
+                fb = fb_from_base(pipe->base.fbs[i]);
+                if (counter != 0 && counter == pipe->counter && fb->flipid == counter) {
+                        pipe->base.front = &fb->base;
+                        flipped = true;
+                }
+
+                if (counter - fb->flipid < UINT16_MAX) {
+                        fb->flipid = 0;
+                        back = fb;
+                } else if (fb->flipid == 0) {
+                        back = fb;
+                }
+        }
+
+        if (!pipe->base.back)
+                pipe->base.back = &back->base;
+
+        if (flipped) {
+                crtc->pipe->base.flipping = false;
+                grdev_pipe_frame(&pipe->base);
+        }
+}
+
+/*
+ * Framebuffers
+ */
+
+static int grdrm_fb_new(grdrm_fb **out, grdrm_card *card, const struct drm_mode_modeinfo *mode) {
+        _cleanup_(grdrm_fb_freep) grdrm_fb *fb = NULL;
+        struct drm_mode_create_dumb create_dumb = { };
+        struct drm_mode_map_dumb map_dumb = { };
+        struct drm_mode_fb_cmd2 add_fb = { };
+        unsigned int i;
+        int r;
+
+        assert_return(out, -EINVAL);
+        assert_return(card, -EINVAL);
+
+        fb = new0(grdrm_fb, 1);
+        if (!fb)
+                return -ENOMEM;
+
+        /* TODO: we should choose a compatible format of the previous CRTC
+         * setting to allow page-flip to it. Only choose fallback if the
+         * previous setting was crap (non xrgb32'ish). */
+
+        fb->card = card;
+        fb->base.format = DRM_FORMAT_XRGB8888;
+        fb->base.width = mode->hdisplay;
+        fb->base.height = mode->vdisplay;
+
+        for (i = 0; i < ELEMENTSOF(fb->base.maps); ++i)
+                fb->base.maps[i] = MAP_FAILED;
+
+        create_dumb.width = fb->base.width;
+        create_dumb.height = fb->base.height;
+        create_dumb.bpp = 32;
+
+        r = ioctl(card->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb);
+        if (r < 0) {
+                r = -errno;
+                log_debug("grdrm: %s: cannot create dumb buffer %" PRIu32 "x%" PRIu32": %m",
+                          card->base.name, fb->base.width, fb->base.height);
+                return r;
+        }
+
+        fb->handles[0] = create_dumb.handle;
+        fb->base.strides[0] = create_dumb.pitch;
+        fb->sizes[0] = create_dumb.size;
+
+        map_dumb.handle = fb->handles[0];
+
+        r = ioctl(card->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb);
+        if (r < 0) {
+                r = -errno;
+                log_debug("grdrm: %s: cannot map dumb buffer %" PRIu32 "x%" PRIu32": %m",
+                          card->base.name, fb->base.width, fb->base.height);
+                return r;
+        }
+
+        fb->base.maps[0] = mmap(0, fb->sizes[0], PROT_WRITE, MAP_SHARED, card->fd, map_dumb.offset);
+        if (fb->base.maps[0] == MAP_FAILED) {
+                r = -errno;
+                log_debug("grdrm: %s: cannot memory-map dumb buffer %" PRIu32 "x%" PRIu32": %m",
+                          card->base.name, fb->base.width, fb->base.height);
+                return r;
+        }
+
+        memzero(fb->base.maps[0], fb->sizes[0]);
+
+        add_fb.width = fb->base.width;
+        add_fb.height = fb->base.height;
+        add_fb.pixel_format = fb->base.format;
+        add_fb.flags = 0;
+        memcpy(add_fb.handles, fb->handles, sizeof(fb->handles));
+        memcpy(add_fb.pitches, fb->base.strides, sizeof(fb->base.strides));
+        memcpy(add_fb.offsets, fb->offsets, sizeof(fb->offsets));
+
+        r = ioctl(card->fd, DRM_IOCTL_MODE_ADDFB2, &add_fb);
+        if (r < 0) {
+                r = -errno;
+                log_debug("grdrm: %s: cannot add framebuffer %" PRIu32 "x%" PRIu32": %m",
+                          card->base.name, fb->base.width, fb->base.height);
+                return r;
+        }
+
+        fb->id = add_fb.fb_id;
+
+        *out = fb;
+        fb = NULL;
+        return 0;
+}
+
+grdrm_fb *grdrm_fb_free(grdrm_fb *fb) {
+        unsigned int i;
+
+        if (!fb)
+                return NULL;
+
+        assert(fb->card);
+
+        if (fb->id > 0 && fb->card->fd >= 0)
+                ioctl(fb->card->fd, DRM_IOCTL_MODE_RMFB, fb->id);
+
+        for (i = 0; i < ELEMENTSOF(fb->handles); ++i) {
+                struct drm_mode_destroy_dumb destroy_dumb = { };
+
+                if (fb->base.maps[i] != MAP_FAILED)
+                        munmap(fb->base.maps[i], fb->sizes[i]);
+
+                if (fb->handles[i] > 0 && fb->card->fd >= 0) {
+                        destroy_dumb.handle = fb->handles[i];
+                        ioctl(fb->card->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy_dumb);
+                }
+        }
+
+        free(fb);
+
+        return NULL;
+}
+
+/*
+ * Pipes
+ */
+
+static void grdrm_pipe_name(char *out, grdrm_crtc *crtc) {
+        /* @out must be at least of size GRDRM_PIPE_NAME_MAX */
+        sprintf(out, "%s/%" PRIu32, crtc->object.card->base.name, crtc->object.id);
+}
+
+static int grdrm_pipe_new(grdrm_pipe **out, grdrm_crtc *crtc, struct drm_mode_modeinfo *mode, size_t n_fbs) {
+        _cleanup_(grdev_pipe_freep) grdev_pipe *basepipe = NULL;
+        grdrm_card *card = crtc->object.card;
+        char name[GRDRM_PIPE_NAME_MAX];
+        grdrm_pipe *pipe;
+        int r;
+
+        assert_return(crtc, -EINVAL);
+        assert_return(grdev_is_drm_card(&card->base), -EINVAL);
+
+        pipe = new0(grdrm_pipe, 1);
+        if (!pipe)
+                return -ENOMEM;
+
+        basepipe = &pipe->base;
+        pipe->base = GRDEV_PIPE_INIT(&grdrm_pipe_vtable, &card->base);
+        pipe->crtc = crtc;
+        pipe->base.width = mode->hdisplay;
+        pipe->base.height = mode->vdisplay;
+
+        grdrm_pipe_name(name, crtc);
+        r = grdev_pipe_add(&pipe->base, name, n_fbs);
+        if (r < 0)
+                return r;
+
+        if (out)
+                *out = pipe;
+        basepipe = NULL;
+        return 0;
+}
+
+static void grdrm_pipe_free(grdev_pipe *basepipe) {
+        grdrm_pipe *pipe = grdrm_pipe_from_base(basepipe);
+        size_t i;
+
+        assert(pipe->crtc);
+
+        for (i = 0; i < pipe->base.max_fbs; ++i)
+                if (pipe->base.fbs[i])
+                        grdrm_fb_free(fb_from_base(pipe->base.fbs[i]));
+
+        free(pipe);
+}
+
+static const grdev_pipe_vtable grdrm_pipe_vtable = {
+        .free                   = grdrm_pipe_free,
+};
+
+/*
+ * Cards
+ */
+
+static void grdrm_name(char *out, dev_t devnum) {
+        /* @out must be at least of size GRDRM_CARD_NAME_MAX */
+        sprintf(out, "drm/%u:%u", major(devnum), minor(devnum));
+}
+
+static void grdrm_card_print(grdrm_card *card) {
+        grdrm_object *object;
+        grdrm_crtc *crtc;
+        grdrm_encoder *encoder;
+        grdrm_connector *connector;
+        grdrm_plane *plane;
+        Iterator iter;
+        uint32_t i;
+        char *p, *buf;
+
+        log_debug("grdrm: %s: state dump", card->base.name);
+
+        log_debug("  crtcs:");
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (object->type != GRDRM_TYPE_CRTC)
+                        continue;
+
+                crtc = crtc_from_object(object);
+                log_debug("    (id: %u index: %d)", object->id, object->index);
+
+                if (crtc->kern.mode_set)
+                        log_debug("      mode: %dx%d", crtc->kern.mode.hdisplay, crtc->kern.mode.vdisplay);
+                else
+                        log_debug("      mode: <none>");
+        }
+
+        log_debug("  encoders:");
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (object->type != GRDRM_TYPE_ENCODER)
+                        continue;
+
+                encoder = encoder_from_object(object);
+                log_debug("    (id: %u index: %d)", object->id, object->index);
+
+                if (encoder->kern.used_crtc)
+                        log_debug("      crtc: %u", encoder->kern.used_crtc);
+                else
+                        log_debug("      crtc: <none>");
+
+                buf = malloc((DECIMAL_STR_MAX(uint32_t) + 1) * encoder->kern.n_crtcs + 1);
+                if (buf) {
+                        buf[0] = 0;
+                        p = buf;
+
+                        for (i = 0; i < encoder->kern.n_crtcs; ++i)
+                                p += sprintf(p, " %" PRIu32, encoder->kern.crtcs[i]);
+
+                        log_debug("      possible crtcs:%s", buf);
+                        free(buf);
+                }
+
+                buf = malloc((DECIMAL_STR_MAX(uint32_t) + 1) * encoder->kern.n_clones + 1);
+                if (buf) {
+                        buf[0] = 0;
+                        p = buf;
+
+                        for (i = 0; i < encoder->kern.n_clones; ++i)
+                                p += sprintf(p, " %" PRIu32, encoder->kern.clones[i]);
+
+                        log_debug("      possible clones:%s", buf);
+                        free(buf);
+                }
+        }
+
+        log_debug("  connectors:");
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (object->type != GRDRM_TYPE_CONNECTOR)
+                        continue;
+
+                connector = connector_from_object(object);
+                log_debug("    (id: %u index: %d)", object->id, object->index);
+                log_debug("      type: %" PRIu32 "-%" PRIu32 " connection: %" PRIu32 " subpixel: %" PRIu32 " extents: %" PRIu32 "x%" PRIu32,
+                          connector->kern.type, connector->kern.type_id, connector->kern.connection, connector->kern.subpixel,
+                          connector->kern.mm_width, connector->kern.mm_height);
+
+                if (connector->kern.used_encoder)
+                        log_debug("      encoder: %" PRIu32, connector->kern.used_encoder);
+                else
+                        log_debug("      encoder: <none>");
+
+                buf = malloc((DECIMAL_STR_MAX(uint32_t) + 1) * connector->kern.n_encoders + 1);
+                if (buf) {
+                        buf[0] = 0;
+                        p = buf;
+
+                        for (i = 0; i < connector->kern.n_encoders; ++i)
+                                p += sprintf(p, " %" PRIu32, connector->kern.encoders[i]);
+
+                        log_debug("      possible encoders:%s", buf);
+                        free(buf);
+                }
+
+                for (i = 0; i < connector->kern.n_modes; ++i) {
+                        struct drm_mode_modeinfo *mode = &connector->kern.modes[i];
+                        log_debug("      mode: %" PRIu32 "x%" PRIu32, mode->hdisplay, mode->vdisplay);
+                }
+        }
+
+        log_debug("  planes:");
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (object->type != GRDRM_TYPE_PLANE)
+                        continue;
+
+                plane = plane_from_object(object);
+                log_debug("    (id: %u index: %d)", object->id, object->index);
+                log_debug("      gamma-size: %" PRIu32, plane->kern.gamma_size);
+
+                if (plane->kern.used_crtc)
+                        log_debug("      crtc: %" PRIu32, plane->kern.used_crtc);
+                else
+                        log_debug("      crtc: <none>");
+
+                buf = malloc((DECIMAL_STR_MAX(uint32_t) + 1) * plane->kern.n_crtcs + 1);
+                if (buf) {
+                        buf[0] = 0;
+                        p = buf;
+
+                        for (i = 0; i < plane->kern.n_crtcs; ++i)
+                                p += sprintf(p, " %" PRIu32, plane->kern.crtcs[i]);
+
+                        log_debug("      possible crtcs:%s", buf);
+                        free(buf);
+                }
+
+                buf = malloc((DECIMAL_STR_MAX(unsigned int) + 3) * plane->kern.n_formats + 1);
+                if (buf) {
+                        buf[0] = 0;
+                        p = buf;
+
+                        for (i = 0; i < plane->kern.n_formats; ++i)
+                                p += sprintf(p, " 0x%x", (unsigned int)plane->kern.formats[i]);
+
+                        log_debug("      possible formats:%s", buf);
+                        free(buf);
+                }
+        }
+}
+
+static int grdrm_card_resync(grdrm_card *card) {
+        _cleanup_free_ uint32_t *crtc_ids = NULL, *encoder_ids = NULL, *connector_ids = NULL, *plane_ids = NULL;
+        uint32_t allocated = 0;
+        grdrm_object *object;
+        Iterator iter;
+        size_t tries;
+        int r;
+
+        assert(card);
+
+        card->async_hotplug = false;
+        allocated = 0;
+
+        /* mark existing objects for possible removal */
+        HASHMAP_FOREACH(object, card->object_map, iter)
+                object->present = false;
+
+        for (tries = 0; tries < GRDRM_MAX_TRIES; ++tries) {
+                struct drm_mode_get_plane_res pres;
+                struct drm_mode_card_res res;
+                uint32_t i, max;
+
+                if (allocated < card->max_ids) {
+                        free(crtc_ids);
+                        free(encoder_ids);
+                        free(connector_ids);
+                        free(plane_ids);
+                        crtc_ids = new0(uint32_t, card->max_ids);
+                        encoder_ids = new0(uint32_t, card->max_ids);
+                        connector_ids = new0(uint32_t, card->max_ids);
+                        plane_ids = new0(uint32_t, card->max_ids);
+
+                        if (!crtc_ids || !encoder_ids || !connector_ids || !plane_ids)
+                                return -ENOMEM;
+
+                        allocated = card->max_ids;
+                }
+
+                zero(res);
+                res.crtc_id_ptr = PTR_TO_UINT64(crtc_ids);
+                res.connector_id_ptr = PTR_TO_UINT64(connector_ids);
+                res.encoder_id_ptr = PTR_TO_UINT64(encoder_ids);
+                res.count_crtcs = allocated;
+                res.count_encoders = allocated;
+                res.count_connectors = allocated;
+
+                r = ioctl(card->fd, DRM_IOCTL_MODE_GETRESOURCES, &res);
+                if (r < 0) {
+                        r = -errno;
+                        log_debug("grdrm: %s: cannot retrieve drm resources: %m", card->base.name);
+                        return r;
+                }
+
+                zero(pres);
+                pres.plane_id_ptr = PTR_TO_UINT64(plane_ids);
+                pres.count_planes = allocated;
+
+                r = ioctl(card->fd, DRM_IOCTL_MODE_GETPLANERESOURCES, &pres);
+                if (r < 0) {
+                        r = -errno;
+                        log_debug("grdrm: %s: cannot retrieve drm plane-resources: %m", card->base.name);
+                        return r;
+                }
+
+                max = MAX(MAX(res.count_crtcs, res.count_encoders),
+                          MAX(res.count_connectors, pres.count_planes));
+                if (max > allocated) {
+                        uint32_t n;
+
+                        n = ALIGN_POWER2(max);
+                        if (!n || n > UINT16_MAX) {
+                                log_debug("grdrm: %s: excessive DRM resource limit: %" PRIu32, card->base.name, max);
+                                return -ERANGE;
+                        }
+
+                        /* retry with resized buffers */
+                        card->max_ids = n;
+                        continue;
+                }
+
+                /* mark available objects as present */
+
+                for (i = 0; i < res.count_crtcs; ++i) {
+                        object = grdrm_find_object(card, crtc_ids[i]);
+                        if (object && object->type == GRDRM_TYPE_CRTC) {
+                                object->present = true;
+                                object->index = i;
+                                crtc_ids[i] = 0;
+                        }
+                }
+
+                for (i = 0; i < res.count_encoders; ++i) {
+                        object = grdrm_find_object(card, encoder_ids[i]);
+                        if (object && object->type == GRDRM_TYPE_ENCODER) {
+                                object->present = true;
+                                object->index = i;
+                                encoder_ids[i] = 0;
+                        }
+                }
+
+                for (i = 0; i < res.count_connectors; ++i) {
+                        object = grdrm_find_object(card, connector_ids[i]);
+                        if (object && object->type == GRDRM_TYPE_CONNECTOR) {
+                                object->present = true;
+                                object->index = i;
+                                connector_ids[i] = 0;
+                        }
+                }
+
+                for (i = 0; i < pres.count_planes; ++i) {
+                        object = grdrm_find_object(card, plane_ids[i]);
+                        if (object && object->type == GRDRM_TYPE_PLANE) {
+                                object->present = true;
+                                object->index = i;
+                                plane_ids[i] = 0;
+                        }
+                }
+
+                /* drop removed objects */
+
+                HASHMAP_FOREACH(object, card->object_map, iter)
+                        if (!object->present)
+                                grdrm_object_free(object);
+
+                /* add new objects */
+
+                card->n_crtcs = res.count_crtcs;
+                for (i = 0; i < res.count_crtcs; ++i) {
+                        if (crtc_ids[i] < 1)
+                                continue;
+
+                        r = grdrm_crtc_new(NULL, card, crtc_ids[i], i);
+                        if (r < 0)
+                                return r;
+                }
+
+                card->n_encoders = res.count_encoders;
+                for (i = 0; i < res.count_encoders; ++i) {
+                        if (encoder_ids[i] < 1)
+                                continue;
+
+                        r = grdrm_encoder_new(NULL, card, encoder_ids[i], i);
+                        if (r < 0)
+                                return r;
+                }
+
+                card->n_connectors = res.count_connectors;
+                for (i = 0; i < res.count_connectors; ++i) {
+                        if (connector_ids[i] < 1)
+                                continue;
+
+                        r = grdrm_connector_new(NULL, card, connector_ids[i], i);
+                        if (r < 0)
+                                return r;
+                }
+
+                card->n_planes = pres.count_planes;
+                for (i = 0; i < pres.count_planes; ++i) {
+                        if (plane_ids[i] < 1)
+                                continue;
+
+                        r = grdrm_plane_new(NULL, card, plane_ids[i], i);
+                        if (r < 0)
+                                return r;
+                }
+
+                /* re-sync objects after object_map is synced */
+
+                HASHMAP_FOREACH(object, card->object_map, iter) {
+                        switch (object->type) {
+                        case GRDRM_TYPE_CRTC:
+                                r = grdrm_crtc_resync(crtc_from_object(object));
+                                break;
+                        case GRDRM_TYPE_ENCODER:
+                                r = grdrm_encoder_resync(encoder_from_object(object));
+                                break;
+                        case GRDRM_TYPE_CONNECTOR:
+                                r = grdrm_connector_resync(connector_from_object(object));
+                                break;
+                        case GRDRM_TYPE_PLANE:
+                                r = grdrm_plane_resync(plane_from_object(object));
+                                break;
+                        default:
+                                assert_not_reached("grdrm: invalid object type");
+                                r = 0;
+                        }
+
+                        if (r < 0)
+                                return r;
+
+                        if (card->async_hotplug)
+                                break;
+                }
+
+                /* if modeset objects change during sync, start over */
+                if (card->async_hotplug) {
+                        card->async_hotplug = false;
+                        continue;
+                }
+
+                /* cache crtc/connector relationship */
+                HASHMAP_FOREACH(object, card->object_map, iter) {
+                        grdrm_connector *connector;
+                        grdrm_encoder *encoder;
+                        grdrm_crtc *crtc;
+
+                        if (object->type != GRDRM_TYPE_CONNECTOR)
+                                continue;
+
+                        connector = connector_from_object(object);
+                        if (connector->kern.connection != 1 || connector->kern.used_encoder < 1)
+                                continue;
+
+                        object = grdrm_find_object(card, connector->kern.used_encoder);
+                        if (!object || object->type != GRDRM_TYPE_ENCODER)
+                                continue;
+
+                        encoder = encoder_from_object(object);
+                        if (encoder->kern.used_crtc < 1)
+                                continue;
+
+                        object = grdrm_find_object(card, encoder->kern.used_crtc);
+                        if (!object || object->type != GRDRM_TYPE_CRTC)
+                                continue;
+
+                        crtc = crtc_from_object(object);
+                        assert(crtc->kern.n_used_connectors < crtc->kern.max_used_connectors);
+                        crtc->kern.used_connectors[crtc->kern.n_used_connectors++] = connector->object.id;
+                }
+
+                /* cache old crtc settings for later restore */
+                HASHMAP_FOREACH(object, card->object_map, iter) {
+                        grdrm_crtc *crtc;
+
+                        if (object->type != GRDRM_TYPE_CRTC)
+                                continue;
+
+                        crtc = crtc_from_object(object);
+
+                        /* Save data if it is the first time we refresh the CRTC. This data can
+                         * be used optionally to restore any previous configuration. For
+                         * instance, it allows us to restore VT configurations after we close
+                         * our session again. */
+                        if (!crtc->old.set) {
+                                crtc->old.fb = crtc->kern.used_fb;
+                                crtc->old.fb_x = crtc->kern.fb_offset_x;
+                                crtc->old.fb_y = crtc->kern.fb_offset_y;
+                                crtc->old.gamma = crtc->kern.gamma_size;
+                                crtc->old.n_connectors = crtc->kern.n_used_connectors;
+                                if (crtc->old.n_connectors)
+                                        memcpy(crtc->old.connectors, crtc->kern.used_connectors, sizeof(uint32_t) * crtc->old.n_connectors);
+                                crtc->old.mode_set = crtc->kern.mode_set;
+                                crtc->old.mode = crtc->kern.mode;
+                                crtc->old.set = true;
+                        }
+                }
+
+                /* everything synced */
+                break;
+        }
+
+        if (tries >= GRDRM_MAX_TRIES) {
+                /*
+                 * Ugh! We were unable to sync the DRM card state due to heavy
+                 * hotplugging. This should never happen, so print a debug
+                 * message and bail out. The next uevent will trigger
+                 * this again.
+                 */
+
+                log_debug("grdrm: %s: hotplug-storm when syncing card", card->base.name);
+                return -EFAULT;
+        }
+
+        return 0;
+}
+
+static bool card_configure_crtc(grdrm_crtc *crtc, grdrm_connector *connector) {
+        grdrm_card *card = crtc->object.card;
+        grdrm_encoder *encoder;
+        grdrm_object *object;
+        uint32_t i, j;
+
+        if (crtc->object.assigned || connector->object.assigned)
+                return false;
+        if (connector->kern.connection != 1)
+                return false;
+
+        for (i = 0; i < connector->kern.n_encoders; ++i) {
+                object = grdrm_find_object(card, connector->kern.encoders[i]);
+                if (!object || object->type != GRDRM_TYPE_ENCODER)
+                        continue;
+
+                encoder = encoder_from_object(object);
+                for (j = 0; j < encoder->kern.n_crtcs; ++j) {
+                        if (encoder->kern.crtcs[j] == crtc->object.id) {
+                                grdrm_crtc_assign(crtc, connector);
+                                return true;
+                        }
+                }
+        }
+
+        return false;
+}
+
+static void grdrm_card_configure(grdrm_card *card) {
+        /*
+         * Modeset Configuration
+         * This is where we update our modeset configuration and assign
+         * connectors to CRTCs. This means, each connector that we want to
+         * enable needs a CRTC, disabled (or unavailable) connectors are left
+         * alone in the dark. Once all CRTCs are assigned, the remaining CRTCs
+         * are disabled.
+         * Sounds trivial, but there're several caveats:
+         *
+         *   * Multiple connectors can be driven by the same CRTC. This is
+         *     known as 'hardware clone mode'. Advantage over software clone
+         *     mode is that only a single CRTC is needed to drive multiple
+         *     displays. However, few hardware supports this and it's a huge
+         *     headache to configure on dynamic demands. Therefore, we only
+         *     support it if configured statically beforehand.
+         *
+         *   * CRTCs are not created equal. Some might be much more poweful
+         *     than others, including more advanced plane support. So far, our
+         *     CRTC selection is random. You need to supply static
+         *     configuration if you want special setups. So far, there is no
+         *     proper way to do advanced CRTC selection on dynamic demands. It
+         *     is not really clear which demands require what CRTC, so, like
+         *     everyone else, we do random CRTC selection unless explicitly
+         *     states otherwise.
+         *
+         *   * Each Connector has a list of possible encoders that can drive
+         *     it, and each encoder has a list of possible CRTCs. If this graph
+         *     is a tree, assignment is trivial. However, if not, we cannot
+         *     reliably decide on configurations beforehand. The encoder is
+         *     always selected by the kernel, so we have to actually set a mode
+         *     to know which encoder is used. There is no way to ask the kernel
+         *     whether a given configuration is possible. This will change with
+         *     atomic-modesetting, but until then, we keep our configurations
+         *     simple and assume they work all just fine. If one fails
+         *     unexpectedly, we print a warning and disable it.
+         *
+         * Configuring a card consists of several steps:
+         *
+         *  1) First of all, we apply any user-configuration. If a user wants
+         *     a fixed configuration, we apply it and preserve it.
+         *     So far, we don't support user configuration files, so this step
+         *     is skipped.
+         *
+         *  2) Secondly, we need to apply any quirks from hwdb. Some hardware
+         *     might only support limited configurations or require special
+         *     CRTC/Connector mappings. We read this from hwdb and apply it, if
+         *     present.
+         *     So far, we don't support this as there is no known quirk, so
+         *     this step is skipped.
+         *
+         *  3) As deep modesets are expensive, we try to avoid them if
+         *     possible. Therefore, we read the current configuration from the
+         *     kernel and try to preserve it, if compatible with our demands.
+         *     If not, we break it and reassign it in a following step.
+         *
+         *  4) The main step involves configuring all remaining objects. By
+         *     default, all available connectors are enabled, except for those
+         *     disabled by user-configuration. We lookup a suitable CRTC for
+         *     each connector and assign them. As there might be more
+         *     connectors than CRTCs, we apply some ordering so users can
+         *     select which connectors are more important right now.
+         *     So far, we only apply the default ordering, more might be added
+         *     in the future.
+         */
+
+        grdrm_object *object;
+        grdrm_crtc *crtc;
+        Iterator i, j;
+
+        /* clear assignments */
+        HASHMAP_FOREACH(object, card->object_map, i)
+                object->assigned = false;
+
+        /* preserve existing configurations */
+        HASHMAP_FOREACH(object, card->object_map, i) {
+                if (object->type != GRDRM_TYPE_CRTC || object->assigned)
+                        continue;
+
+                crtc = crtc_from_object(object);
+
+                if (crtc->applied) {
+                        /* If our mode is set, preserve it. If no connector is
+                         * set, modeset either failed or the pipe is unused. In
+                         * both cases, leave it alone. It might be tried again
+                         * below in case there're remaining connectors.
+                         * Otherwise, try restoring the assignments. If they
+                         * are no longer valid, leave the pipe untouched. */
+
+                        if (crtc->set.n_connectors < 1)
+                                continue;
+
+                        assert(crtc->set.n_connectors == 1);
+
+                        object = grdrm_find_object(card, crtc->set.connectors[0]);
+                        if (!object || object->type != GRDRM_TYPE_CONNECTOR)
+                                continue;
+
+                        card_configure_crtc(crtc, connector_from_object(object));
+                } else if (crtc->kern.mode_set && crtc->kern.n_used_connectors != 1) {
+                        /* If our mode is not set on the pipe, we know the kern
+                         * information is valid. Try keeping it. If it's not
+                         * possible, leave the pipe untouched for later
+                         * assignements. */
+
+                        object = grdrm_find_object(card, crtc->kern.used_connectors[0]);
+                        if (!object || object->type != GRDRM_TYPE_CONNECTOR)
+                                continue;
+
+                        card_configure_crtc(crtc, connector_from_object(object));
+                }
+        }
+
+        /* assign remaining objects */
+        HASHMAP_FOREACH(object, card->object_map, i) {
+                if (object->type != GRDRM_TYPE_CRTC || object->assigned)
+                        continue;
+
+                crtc = crtc_from_object(object);
+
+                HASHMAP_FOREACH(object, card->object_map, j) {
+                        if (object->type != GRDRM_TYPE_CONNECTOR)
+                                continue;
+
+                        if (card_configure_crtc(crtc, connector_from_object(object)))
+                                break;
+                }
+
+                if (!crtc->object.assigned)
+                        grdrm_crtc_assign(crtc, NULL);
+        }
+
+        /* expose configuration */
+        HASHMAP_FOREACH(object, card->object_map, i) {
+                if (object->type != GRDRM_TYPE_CRTC)
+                        continue;
+
+                grdrm_crtc_expose(crtc_from_object(object));
+        }
+}
+
+static void grdrm_card_hotplug(grdrm_card *card) {
+        int r;
+
+        assert(card);
+        assert(!card->ready);
+
+        r = grdrm_card_resync(card);
+        if (r < 0) {
+                log_debug("grdrm: %s/%s: cannot re-sync card: %s",
+                          card->base.session->name, card->base.name, strerror(-r));
+                return;
+        }
+
+        grdev_session_pin(card->base.session);
+
+        grdrm_card_print(card);
+        grdrm_card_configure(card);
+        card->ready = true;
+
+        grdev_session_unpin(card->base.session);
+}
+
+static int grdrm_card_io_fn(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        grdrm_card *card = userdata;
+        struct drm_event_vblank *vblank;
+        struct drm_event *event;
+        uint32_t id, counter;
+        grdrm_object *object;
+        char buf[4096];
+        ssize_t l, i;
+
+        if (revents & (EPOLLHUP | EPOLLERR)) {
+                /* Immediately close device on HUP; no need to flush pending
+                 * data.. there're no events we care about here. */
+                log_debug("grdrm: %s/%s: HUP", card->base.session->name, card->base.name);
+                grdrm_card_close(card);
+                return 0;
+        }
+
+        if (revents & (EPOLLIN)) {
+                l = read(card->fd, buf, sizeof(buf));
+                if (l < 0) {
+                        if (errno == EAGAIN || errno == EINTR)
+                                return 0;
+
+                        log_debug("grdrm: %s/%s: read error: %m", card->base.session->name, card->base.name);
+                        grdrm_card_close(card);
+                        return 0;
+                } else if ((size_t)l < sizeof(*event)) {
+                        log_debug("grdrm: %s/%s: short read of %zd bytes", card->base.session->name, card->base.name, l);
+                        return 0;
+                }
+
+                for (i = 0; i < l; i += event->length) {
+                        event = (void*)&buf[i];
+
+                        if (i + event->length > l) {
+                                log_debug("grdrm: %s/%s: truncated event", card->base.session->name, card->base.name);
+                                break;
+                        }
+
+                        switch (event->type) {
+                        case DRM_EVENT_FLIP_COMPLETE:
+                                vblank = (void*)event;
+                                if (event->length < sizeof(*vblank)) {
+                                        log_debug("grdrm: %s/%s: truncated vblank event", card->base.session->name, card->base.name);
+                                        break;
+                                }
+
+                                grdrm_decode_vblank_data(vblank->user_data, &id, &counter);
+                                object = grdrm_find_object(card, id);
+                                if (!object || object->type != GRDRM_TYPE_CRTC)
+                                        break;
+
+                                grdrm_crtc_flip_complete(crtc_from_object(object), counter, vblank);
+                                break;
+                        }
+                }
+        }
+
+        return 0;
+}
+
+static int grdrm_card_add(grdrm_card *card, const char *name) {
+        assert(card);
+        assert(card->fd < 0);
+
+        card->object_map = hashmap_new(&trivial_hash_ops);
+        if (!card->object_map)
+                return -ENOMEM;
+
+        return grdev_card_add(&card->base, name);
+}
+
+static void grdrm_card_destroy(grdrm_card *card) {
+        assert(card);
+        assert(!card->running);
+        assert(card->fd < 0);
+        assert(hashmap_size(card->object_map) == 0);
+
+        hashmap_free(card->object_map);
+}
+
+static void grdrm_card_commit(grdev_card *basecard) {
+        grdrm_card *card = grdrm_card_from_base(basecard);
+        grdrm_object *object;
+        Iterator iter;
+
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (!card->ready)
+                        break;
+
+                if (object->type != GRDRM_TYPE_CRTC)
+                        continue;
+
+                grdrm_crtc_commit(crtc_from_object(object));
+        }
+}
+
+static void grdrm_card_restore(grdev_card *basecard) {
+        grdrm_card *card = grdrm_card_from_base(basecard);
+        grdrm_object *object;
+        Iterator iter;
+
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                if (!card->ready)
+                        break;
+
+                if (object->type != GRDRM_TYPE_CRTC)
+                        continue;
+
+                grdrm_crtc_restore(crtc_from_object(object));
+        }
+}
+
+static void grdrm_card_enable(grdrm_card *card) {
+        assert(card);
+
+        if (card->fd < 0 || card->running)
+                return;
+
+        /* ignore cards without DUMB_BUFFER capability */
+        if (!card->cap_dumb)
+                return;
+
+        assert(card->fd_src);
+
+        log_debug("grdrm: %s/%s: enable", card->base.session->name, card->base.name);
+
+        card->running = true;
+        sd_event_source_set_enabled(card->fd_src, SD_EVENT_ON);
+        grdrm_card_hotplug(card);
+}
+
+static void grdrm_card_disable(grdrm_card *card) {
+        grdrm_object *object;
+        Iterator iter;
+
+        assert(card);
+
+        if (card->fd < 0 || !card->running)
+                return;
+
+        assert(card->fd_src);
+
+        log_debug("grdrm: %s/%s: disable", card->base.session->name, card->base.name);
+
+        card->running = false;
+        card->ready = false;
+        sd_event_source_set_enabled(card->fd_src, SD_EVENT_OFF);
+
+        /* stop all pipes */
+        HASHMAP_FOREACH(object, card->object_map, iter) {
+                grdrm_crtc *crtc;
+
+                if (object->type != GRDRM_TYPE_CRTC)
+                        continue;
+
+                crtc = crtc_from_object(object);
+                crtc->applied = false;
+                if (crtc->pipe)
+                        grdev_pipe_ready(&crtc->pipe->base, false);
+        }
+}
+
+static int grdrm_card_open(grdrm_card *card, int dev_fd) {
+        _cleanup_(grdev_session_unpinp) grdev_session *pin = NULL;
+        _cleanup_close_ int fd = dev_fd;
+        struct drm_get_cap cap;
+        int r, flags;
+
+        assert(card);
+        assert(dev_fd >= 0);
+        assert(card->fd != dev_fd);
+
+        pin = grdev_session_pin(card->base.session);
+        grdrm_card_close(card);
+
+        log_debug("grdrm: %s/%s: open", card->base.session->name, card->base.name);
+
+        r = fd_nonblock(fd, true);
+        if (r < 0)
+                return r;
+
+        r = fd_cloexec(fd, true);
+        if (r < 0)
+                return r;
+
+        flags = fcntl(fd, F_GETFL, 0);
+        if (flags < 0)
+                return -errno;
+        if ((flags & O_ACCMODE) != O_RDWR)
+                return -EACCES;
+
+        r = sd_event_add_io(card->base.session->context->event,
+                            &card->fd_src,
+                            fd,
+                            EPOLLHUP | EPOLLERR | EPOLLIN,
+                            grdrm_card_io_fn,
+                            card);
+        if (r < 0)
+                return r;
+
+        sd_event_source_set_enabled(card->fd_src, SD_EVENT_OFF);
+
+        card->fd = fd;
+        fd = -1;
+
+        /* cache DUMB_BUFFER capability */
+        cap.capability = DRM_CAP_DUMB_BUFFER;
+        cap.value = 0;
+        r = ioctl(card->fd, DRM_IOCTL_GET_CAP, &cap);
+        card->cap_dumb = r >= 0 && cap.value;
+        if (r < 0)
+                log_debug("grdrm: %s/%s: cannot retrieve DUMB_BUFFER capability: %s",
+                          card->base.session->name, card->base.name, strerror(-r));
+        else if (!card->cap_dumb)
+                log_debug("grdrm: %s/%s: DUMB_BUFFER capability not supported",
+                          card->base.session->name, card->base.name);
+
+        /* cache TIMESTAMP_MONOTONIC capability */
+        cap.capability = DRM_CAP_TIMESTAMP_MONOTONIC;
+        cap.value = 0;
+        r = ioctl(card->fd, DRM_IOCTL_GET_CAP, &cap);
+        card->cap_monotonic = r >= 0 && cap.value;
+        if (r < 0)
+                log_debug("grdrm: %s/%s: cannot retrieve TIMESTAMP_MONOTONIC capability: %s",
+                          card->base.session->name, card->base.name, strerror(-r));
+        else if (!card->cap_monotonic)
+                log_debug("grdrm: %s/%s: TIMESTAMP_MONOTONIC is disabled globally, fix this NOW!",
+                          card->base.session->name, card->base.name);
+
+        return 0;
+}
+
+static void grdrm_card_close(grdrm_card *card) {
+        grdrm_object *object;
+
+        if (card->fd < 0)
+                return;
+
+        log_debug("grdrm: %s/%s: close", card->base.session->name, card->base.name);
+
+        grdrm_card_disable(card);
+
+        card->fd_src = sd_event_source_unref(card->fd_src);
+        card->fd = safe_close(card->fd);
+
+        grdev_session_pin(card->base.session);
+        while ((object = hashmap_first(card->object_map)))
+                grdrm_object_free(object);
+        grdev_session_unpin(card->base.session);
+}
+
+static bool grdrm_card_async(grdrm_card *card, int r) {
+        switch (r) {
+        case -EACCES:
+                /* If we get EACCES on runtime DRM calls, we lost DRM-Master
+                 * (or we did something terribly wrong). Immediately disable
+                 * the card, so we stop all pipes and wait to be activated
+                 * again. */
+                grdrm_card_disable(card);
+                break;
+        case -ENOENT:
+                /* DRM objects can be hotplugged at any time. If an object is
+                 * removed that we use, we remember that state so a following
+                 * call can test for this.
+                 * Note that we also get a uevent as followup, this will resync
+                 * the whole device. */
+                card->async_hotplug = true;
+                break;
+        }
+
+        return !card->ready;
+}
+
+/*
+ * Unmanaged Cards
+ * The unmanaged DRM card opens the device node for a given DRM device
+ * directly (/dev/dri/cardX) and thus needs sufficient privileges. It opens
+ * the device only if we really require it and releases it as soon as we're
+ * disabled or closed.
+ * The unmanaged element can be used in all situations where you have direct
+ * access to DRM device nodes. Unlike managed DRM elements, it can be used
+ * outside of user sessions and in emergency situations where logind is not
+ * available.
+ */
+
+static void unmanaged_card_enable(grdev_card *basecard) {
+        unmanaged_card *cu = unmanaged_card_from_base(basecard);
+        int r, fd;
+
+        if (cu->card.fd < 0) {
+                /* try open on activation if it failed during allocation */
+                fd = open(cu->devnode, O_RDWR | O_CLOEXEC | O_NOCTTY | O_NONBLOCK);
+                if (fd < 0) {
+                        /* not fatal; simply ignore the device */
+                        log_debug("grdrm: %s/%s: cannot open node %s: %m",
+                                  basecard->session->name, basecard->name, cu->devnode);
+                        return;
+                }
+
+                /* we might already be DRM-Master by open(); that's fine */
+
+                r = grdrm_card_open(&cu->card, fd);
+                if (r < 0) {
+                        log_debug("grdrm: %s/%s: cannot open: %s",
+                                  basecard->session->name, basecard->name, strerror(-r));
+                        return;
+                }
+        }
+
+        r = ioctl(cu->card.fd, DRM_IOCTL_SET_MASTER, 0);
+        if (r < 0) {
+                log_debug("grdrm: %s/%s: cannot acquire DRM-Master: %m",
+                          basecard->session->name, basecard->name);
+                return;
+        }
+
+        grdrm_card_enable(&cu->card);
+}
+
+static void unmanaged_card_disable(grdev_card *basecard) {
+        unmanaged_card *cu = unmanaged_card_from_base(basecard);
+
+        grdrm_card_disable(&cu->card);
+}
+
+static int unmanaged_card_new(grdev_card **out, grdev_session *session, struct udev_device *ud) {
+        _cleanup_(grdev_card_freep) grdev_card *basecard = NULL;
+        char name[GRDRM_CARD_NAME_MAX];
+        unmanaged_card *cu;
+        const char *devnode;
+        dev_t devnum;
+        int r, fd;
+
+        assert_return(session, -EINVAL);
+        assert_return(ud, -EINVAL);
+
+        devnode = udev_device_get_devnode(ud);
+        devnum = udev_device_get_devnum(ud);
+        if (!devnode || devnum == 0)
+                return -ENODEV;
+
+        grdrm_name(name, devnum);
+
+        cu = new0(unmanaged_card, 1);
+        if (!cu)
+                return -ENOMEM;
+
+        basecard = &cu->card.base;
+        cu->card = GRDRM_CARD_INIT(&unmanaged_card_vtable, session);
+
+        cu->devnode = strdup(devnode);
+        if (!cu->devnode)
+                return -ENOMEM;
+
+        r = grdrm_card_add(&cu->card, name);
+        if (r < 0)
+                return r;
+
+        /* try to open but ignore errors */
+        fd = open(cu->devnode, O_RDWR | O_CLOEXEC | O_NOCTTY | O_NONBLOCK);
+        if (fd < 0) {
+                /* not fatal; allow uaccess based control on activation */
+                log_debug("grdrm: %s/%s: cannot open node %s: %m",
+                          basecard->session->name, basecard->name, cu->devnode);
+        } else {
+                /* We might get DRM-Master implicitly on open(); drop it immediately
+                 * so we acquire it only once we're actually enabled. */
+                ioctl(fd, DRM_IOCTL_DROP_MASTER, 0);
+
+                r = grdrm_card_open(&cu->card, fd);
+                if (r < 0)
+                        log_debug("grdrm: %s/%s: cannot open: %s",
+                                  basecard->session->name, basecard->name, strerror(-r));
+        }
+
+        if (out)
+                *out = basecard;
+        basecard = NULL;
+        return 0;
+}
+
+static void unmanaged_card_free(grdev_card *basecard) {
+        unmanaged_card *cu = unmanaged_card_from_base(basecard);
+
+        assert(!basecard->enabled);
+
+        grdrm_card_close(&cu->card);
+        grdrm_card_destroy(&cu->card);
+        free(cu->devnode);
+        free(cu);
+}
+
+static const grdev_card_vtable unmanaged_card_vtable = {
+        .free                   = unmanaged_card_free,
+        .enable                 = unmanaged_card_enable,
+        .disable                = unmanaged_card_disable,
+        .commit                 = grdrm_card_commit,
+        .restore                = grdrm_card_restore,
+};
+
+/*
+ * Managed Cards
+ * The managed DRM card uses systemd-logind to acquire DRM devices. This
+ * means, we do not open the device node /dev/dri/cardX directly. Instead,
+ * logind passes us a file-descriptor whenever our session is activated. Thus,
+ * we don't need access to the device node directly.
+ * Furthermore, whenever the session is put asleep, logind revokes the
+ * file-descriptor so we loose access to the device.
+ * Managed DRM cards should be preferred over unmanaged DRM cards whenever
+ * you run inside a user session with exclusive device access.
+ */
+
+static void managed_card_enable(grdev_card *card) {
+        managed_card *cm = managed_card_from_base(card);
+
+        /* If the device is manually re-enabled, we try to resume our card
+         * management. Note that we have no control over DRM-Master and the fd,
+         * so we have to take over the state from the last logind event. */
+
+        if (cm->master)
+                grdrm_card_enable(&cm->card);
+}
+
+static void managed_card_disable(grdev_card *card) {
+        managed_card *cm = managed_card_from_base(card);
+
+        /* If the device is manually disabled, we keep the FD but put our card
+         * management asleep. This way, we can wake up at any time, but don't
+         * touch the device while asleep. */
+
+        grdrm_card_disable(&cm->card);
+}
+
+static int managed_card_pause_device_fn(sd_bus *bus,
+                                        sd_bus_message *signal,
+                                        void *userdata,
+                                        sd_bus_error *ret_error) {
+        managed_card *cm = userdata;
+        grdev_session *session = cm->card.base.session;
+        uint32_t major, minor;
+        const char *mode;
+        int r;
+
+        /*
+         * We get PauseDevice() signals from logind whenever a device we
+         * requested was, or is about to be, paused. Arguments are major/minor
+         * number of the device and the mode of the operation.
+         * In case the event is not about our device, we ignore it. Otherwise,
+         * we treat it as asynchronous DRM-DROP-MASTER. Note that we might have
+         * already handled an EACCES error from a modeset ioctl, in which case
+         * we already disabled the device.
+         *
+         * @mode can be one of the following:
+         *   "pause": The device is about to be paused. We must react
+         *            immediately and respond with PauseDeviceComplete(). Once
+         *            we replied, logind will pause the device. Note that
+         *            logind might apply any kind of timeout and force pause
+         *            the device if we don't respond in a timely manner. In
+         *            this case, we will receive a second PauseDevice event
+         *            with @mode set to "force" (or similar).
+         *   "force": The device was disabled forecfully by logind. DRM-Master
+         *            was already dropped. This is just an asynchronous
+         *            notification so we can put the device asleep (in case
+         *            we didn't already notice the dropped DRM-Master).
+         *    "gone": This is like "force" but is sent if the device was
+         *            paused due to a device-removal event.
+         *
+         * We always handle PauseDevice signals as "force" as we properly
+         * support asynchronously dropping DRM-Master, anyway. But in case
+         * logind sent mode "pause", we also call PauseDeviceComplete() to
+         * immediately acknowledge the request.
+         */
+
+        r = sd_bus_message_read(signal, "uus", &major, &minor, &mode);
+        if (r < 0) {
+                log_debug("grdrm: %s/%s: erroneous PauseDevice signal",
+                          session->name, cm->card.base.name);
+                return 0;
+        }
+
+        /* not our device? */
+        if (makedev(major, minor) != cm->devnum)
+                return 0;
+
+        cm->master = false;
+        grdrm_card_disable(&cm->card);
+
+        if (streq(mode, "pause")) {
+                _cleanup_bus_message_unref_ sd_bus_message *m = NULL;
+
+                /*
+                 * Sending PauseDeviceComplete() is racy if logind triggers the
+                 * timeout. That is, if we take too long and logind pauses the
+                 * device by sending a forced PauseDevice, our
+                 * PauseDeviceComplete call will be stray. That's fine, though.
+                 * logind ignores such stray calls. Only if logind also sent a
+                 * further PauseDevice() signal, it might match our call
+                 * incorrectly to the newer PauseDevice(). That's fine, too, as
+                 * we handle that event asynchronously, anyway. Therefore,
+                 * whatever happens, we're fine. Yay!
+                 */
+
+                r = sd_bus_message_new_method_call(session->context->sysbus,
+                                                   &m,
+                                                   "org.freedesktop.login1",
+                                                   session->path,
+                                                   "org.freedesktop.login1.Session",
+                                                   "PauseDeviceComplete");
+                if (r >= 0) {
+                        r = sd_bus_message_append(m, "uu", major, minor);
+                        if (r >= 0)
+                                r = sd_bus_send(session->context->sysbus, m, NULL);
+                }
+
+                if (r < 0)
+                        log_debug("grdrm: %s/%s: cannot send PauseDeviceComplete: %s",
+                                  session->name, cm->card.base.name, strerror(-r));
+        }
+
+        return 0;
+}
+
+static int managed_card_resume_device_fn(sd_bus *bus,
+                                         sd_bus_message *signal,
+                                         void *userdata,
+                                         sd_bus_error *ret_error) {
+        managed_card *cm = userdata;
+        grdev_session *session = cm->card.base.session;
+        uint32_t major, minor;
+        int r, fd;
+
+        /*
+         * We get ResumeDevice signals whenever logind resumed a previously
+         * paused device. The arguments contain the major/minor number of the
+         * related device and a new file-descriptor for the freshly opened
+         * device-node.
+         * If the signal is not about our device, we simply ignore it.
+         * Otherwise, we immediately resume the device. Note that we drop the
+         * new file-descriptor as we already have one from TakeDevice(). logind
+         * preserves the file-context across pause/resume for DRM but only
+         * drops/acquires DRM-Master accordingly. This way, our context (like
+         * DRM-FBs and BOs) is preserved.
+         */
+
+        r = sd_bus_message_read(signal, "uuh", &major, &minor, &fd);
+        if (r < 0) {
+                log_debug("grdrm: %s/%s: erroneous ResumeDevice signal",
+                          session->name, cm->card.base.name);
+                return 0;
+        }
+
+        /* not our device? */
+        if (makedev(major, minor) != cm->devnum)
+                return 0;
+
+        if (cm->card.fd < 0) {
+                /* This shouldn't happen. We should already own an FD from
+                 * TakeDevice(). However, lets be safe and use this FD in case
+                 * we really don't have one. There is no harm in doing this
+                 * and our code works fine this way. */
+                fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+                if (fd < 0) {
+                        log_debug("grdrm: %s/%s: cannot duplicate fd: %m",
+                                  session->name, cm->card.base.name);
+                        return 0;
+                }
+
+                r = grdrm_card_open(&cm->card, fd);
+                if (r < 0) {
+                        log_debug("grdrm: %s/%s: cannot open: %s",
+                                  session->name, cm->card.base.name, strerror(-r));
+                        return 0;
+                }
+        }
+
+        cm->master = true;
+        if (cm->card.base.enabled)
+                grdrm_card_enable(&cm->card);
+
+        return 0;
+}
+
+static int managed_card_setup_bus(managed_card *cm) {
+        grdev_session *session = cm->card.base.session;
+        _cleanup_free_ char *match = NULL;
+        int r;
+
+        match = strjoin("type='signal',"
+                        "sender='org.freedesktop.login1',"
+                        "interface='org.freedesktop.login1.Session',"
+                        "member='PauseDevice',"
+                        "path='", session->path, "'",
+                        NULL);
+        if (!match)
+                return -ENOMEM;
+
+        r = sd_bus_add_match(session->context->sysbus,
+                             &cm->slot_pause_device,
+                             match,
+                             managed_card_pause_device_fn,
+                             cm);
+        if (r < 0)
+                return r;
+
+        free(match);
+        match = strjoin("type='signal',"
+                        "sender='org.freedesktop.login1',"
+                        "interface='org.freedesktop.login1.Session',"
+                        "member='ResumeDevice',"
+                        "path='", session->path, "'",
+                        NULL);
+        if (!match)
+                return -ENOMEM;
+
+        r = sd_bus_add_match(session->context->sysbus,
+                             &cm->slot_resume_device,
+                             match,
+                             managed_card_resume_device_fn,
+                             cm);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int managed_card_take_device_fn(sd_bus *bus,
+                                       sd_bus_message *reply,
+                                       void *userdata,
+                                       sd_bus_error *ret_error) {
+        managed_card *cm = userdata;
+        grdev_session *session = cm->card.base.session;
+        int r, paused, fd;
+
+        cm->slot_take_device = sd_bus_slot_unref(cm->slot_take_device);
+
+        if (sd_bus_message_is_method_error(reply, NULL)) {
+                const sd_bus_error *error = sd_bus_message_get_error(reply);
+
+                log_debug("grdrm: %s/%s: TakeDevice failed: %s: %s",
+                          session->name, cm->card.base.name, error->name, error->message);
+                return 0;
+        }
+
+        cm->acquired = true;
+
+        r = sd_bus_message_read(reply, "hb", &fd, &paused);
+        if (r < 0) {
+                log_debug("grdrm: %s/%s: erroneous TakeDevice reply",
+                          session->name, cm->card.base.name);
+                return 0;
+        }
+
+        fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+        if (fd < 0) {
+                log_debug("grdrm: %s/%s: cannot duplicate fd: %m",
+                          session->name, cm->card.base.name);
+                return 0;
+        }
+
+        r = grdrm_card_open(&cm->card, fd);
+        if (r < 0) {
+                log_debug("grdrm: %s/%s: cannot open: %s",
+                          session->name, cm->card.base.name, strerror(-r));
+                return 0;
+        }
+
+        if (!paused && cm->card.base.enabled)
+                grdrm_card_enable(&cm->card);
+
+        return 0;
+}
+
+static void managed_card_take_device(managed_card *cm) {
+        _cleanup_bus_message_unref_ sd_bus_message *m = NULL;
+        grdev_session *session = cm->card.base.session;
+        int r;
+
+        r = sd_bus_message_new_method_call(session->context->sysbus,
+                                           &m,
+                                           "org.freedesktop.login1",
+                                           session->path,
+                                           "org.freedesktop.login1.Session",
+                                           "TakeDevice");
+        if (r < 0)
+                goto error;
+
+        r = sd_bus_message_append(m, "uu", major(cm->devnum), minor(cm->devnum));
+        if (r < 0)
+                goto error;
+
+        r = sd_bus_call_async(session->context->sysbus,
+                              &cm->slot_take_device,
+                              m,
+                              managed_card_take_device_fn,
+                              cm,
+                              0);
+        if (r < 0)
+                goto error;
+
+        cm->requested = true;
+        return;
+
+error:
+        log_debug("grdrm: %s/%s: cannot send TakeDevice request: %s",
+                  session->name, cm->card.base.name, strerror(-r));
+}
+
+static void managed_card_release_device(managed_card *cm) {
+        _cleanup_bus_message_unref_ sd_bus_message *m = NULL;
+        grdev_session *session = cm->card.base.session;
+        int r;
+
+        /*
+         * If TakeDevice() is pending or was successful, make sure to
+         * release the device again. We don't care for return-values,
+         * so send it without waiting or callbacks.
+         * If a failed TakeDevice() is pending, but someone else took
+         * the device on the same bus-connection, we might incorrectly
+         * release their device. This is an unlikely race, though.
+         * Furthermore, you really shouldn't have two users of the
+         * controller-API on the same session, on the same devices, *AND* on
+         * the same bus-connection. So we don't care for that race..
+         */
+
+        grdrm_card_close(&cm->card);
+        cm->requested = false;
+
+        if (!cm->acquired && !cm->slot_take_device)
+                return;
+
+        cm->slot_take_device = sd_bus_slot_unref(cm->slot_take_device);
+        cm->acquired = false;
+
+        r = sd_bus_message_new_method_call(session->context->sysbus,
+                                           &m,
+                                           "org.freedesktop.login1",
+                                           session->path,
+                                           "org.freedesktop.login1.Session",
+                                           "ReleaseDevice");
+        if (r >= 0) {
+                r = sd_bus_message_append(m, "uu", major(cm->devnum), minor(cm->devnum));
+                if (r >= 0)
+                        r = sd_bus_send(session->context->sysbus, m, NULL);
+        }
+
+        if (r < 0 && r != -ENOTCONN)
+                log_debug("grdrm: %s/%s: cannot send ReleaseDevice: %s",
+                          session->name, cm->card.base.name, strerror(-r));
+}
+
+static int managed_card_new(grdev_card **out, grdev_session *session, struct udev_device *ud) {
+        _cleanup_(grdev_card_freep) grdev_card *basecard = NULL;
+        char name[GRDRM_CARD_NAME_MAX];
+        managed_card *cm;
+        dev_t devnum;
+        int r;
+
+        assert_return(session, -EINVAL);
+        assert_return(session->managed, -EINVAL);
+        assert_return(session->context->sysbus, -EINVAL);
+        assert_return(ud, -EINVAL);
+
+        devnum = udev_device_get_devnum(ud);
+        if (devnum == 0)
+                return -ENODEV;
+
+        grdrm_name(name, devnum);
+
+        cm = new0(managed_card, 1);
+        if (!cm)
+                return -ENOMEM;
+
+        basecard = &cm->card.base;
+        cm->card = GRDRM_CARD_INIT(&managed_card_vtable, session);
+        cm->devnum = devnum;
+
+        r = managed_card_setup_bus(cm);
+        if (r < 0)
+                return r;
+
+        r = grdrm_card_add(&cm->card, name);
+        if (r < 0)
+                return r;
+
+        managed_card_take_device(cm);
+
+        if (out)
+                *out = basecard;
+        basecard = NULL;
+        return 0;
+}
+
+static void managed_card_free(grdev_card *basecard) {
+        managed_card *cm = managed_card_from_base(basecard);
+
+        assert(!basecard->enabled);
+
+        managed_card_release_device(cm);
+        cm->slot_resume_device = sd_bus_slot_unref(cm->slot_resume_device);
+        cm->slot_pause_device = sd_bus_slot_unref(cm->slot_pause_device);
+        grdrm_card_destroy(&cm->card);
+        free(cm);
+}
+
+static const grdev_card_vtable managed_card_vtable = {
+        .free                   = managed_card_free,
+        .enable                 = managed_card_enable,
+        .disable                = managed_card_disable,
+        .commit                 = grdrm_card_commit,
+        .restore                = grdrm_card_restore,
+};
+
+/*
+ * Generic Constructor
+ * Instead of relying on the caller to choose between managed and unmanaged
+ * DRM devices, the grdev_drm_new() constructor does that for you (by
+ * looking at session->managed).
+ */
+
+bool grdev_is_drm_card(grdev_card *basecard) {
+        return basecard && (basecard->vtable == &unmanaged_card_vtable ||
+                            basecard->vtable == &managed_card_vtable);
+}
+
+grdev_card *grdev_find_drm_card(grdev_session *session, dev_t devnum) {
+        char name[GRDRM_CARD_NAME_MAX];
+
+        assert_return(session, NULL);
+        assert_return(devnum != 0, NULL);
+
+        grdrm_name(name, devnum);
+        return grdev_find_card(session, name);
+}
+
+int grdev_drm_card_new(grdev_card **out, grdev_session *session, struct udev_device *ud) {
+        assert_return(session, -EINVAL);
+        assert_return(ud, -EINVAL);
+
+        return session->managed ? managed_card_new(out, session, ud) : unmanaged_card_new(out, session, ud);
+}
diff --git a/src/libsystemd-terminal/grdev-internal.h b/src/libsystemd-terminal/grdev-internal.h
index 7e69c49..0064f0b 100644
--- a/src/libsystemd-terminal/grdev-internal.h
+++ b/src/libsystemd-terminal/grdev-internal.h
@@ -22,6 +22,7 @@
 #pragma once
 
 #include <inttypes.h>
+#include <libudev.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <systemd/sd-bus.h>
@@ -40,6 +41,14 @@ typedef struct grdev_card_vtable        grdev_card_vtable;
 typedef struct grdev_card               grdev_card;
 
 /*
+ * DRM cards
+ */
+
+bool grdev_is_drm_card(grdev_card *card);
+grdev_card *grdev_find_drm_card(grdev_session *session, dev_t devnum);
+int grdev_drm_card_new(grdev_card **out, grdev_session *session, struct udev_device *ud);
+
+/*
  * Displays
  */
 
diff --git a/src/libsystemd-terminal/grdev.c b/src/libsystemd-terminal/grdev.c
index ab1c407..1e02a67 100644
--- a/src/libsystemd-terminal/grdev.c
+++ b/src/libsystemd-terminal/grdev.c
@@ -20,6 +20,7 @@
 ***/
 
 #include <inttypes.h>
+#include <libudev.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <systemd/sd-bus.h>
@@ -30,6 +31,7 @@
 #include "hashmap.h"
 #include "login-shared.h"
 #include "macro.h"
+#include "udev-util.h"
 #include "util.h"
 
 static void pipe_enable(grdev_pipe *pipe);
@@ -1083,6 +1085,68 @@ void grdev_session_restore(grdev_session *session) {
                         card->vtable->restore(card);
 }
 
+void grdev_session_add_drm(grdev_session *session, struct udev_device *ud) {
+        grdev_card *card;
+        dev_t devnum;
+        int r;
+
+        assert(session);
+        assert(ud);
+
+        devnum = udev_device_get_devnum(ud);
+        if (devnum == 0)
+                return;
+
+        card = grdev_find_drm_card(session, devnum);
+        if (card)
+                return;
+
+        r = grdev_drm_card_new(&card, session, ud);
+        if (r < 0) {
+                log_debug("grdev: %s: cannot add DRM device for %s: %s",
+                          session->name, udev_device_get_syspath(ud), strerror(-r));
+                return;
+        }
+
+        session_add_card(session, card);
+}
+
+void grdev_session_remove_drm(grdev_session *session, struct udev_device *ud) {
+        grdev_card *card;
+        dev_t devnum;
+
+        assert(session);
+        assert(ud);
+
+        devnum = udev_device_get_devnum(ud);
+        if (devnum == 0)
+                return;
+
+        card = grdev_find_drm_card(session, devnum);
+        if (!card)
+                return;
+
+        session_remove_card(session, card);
+}
+
+void grdev_session_hotplug_drm(grdev_session *session, struct udev_device *ud) {
+        grdev_card *card;
+        dev_t devnum;
+
+        assert(session);
+        assert(ud);
+
+        devnum = udev_device_get_devnum(ud);
+        if (devnum == 0)
+                return;
+
+        card = grdev_find_drm_card(session, devnum);
+        if (!card)
+                return;
+
+        /* TODO: hotplug card */
+}
+
 static void session_configure(grdev_session *session) {
         grdev_display *display;
         grdev_tile *tile;
diff --git a/src/libsystemd-terminal/grdev.h b/src/libsystemd-terminal/grdev.h
index 2645b12..9924a25 100644
--- a/src/libsystemd-terminal/grdev.h
+++ b/src/libsystemd-terminal/grdev.h
@@ -55,6 +55,7 @@
 
 #include <drm_fourcc.h>
 #include <inttypes.h>
+#include <libudev.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <systemd/sd-bus.h>
@@ -171,6 +172,10 @@ void grdev_session_disable(grdev_session *session);
 void grdev_session_commit(grdev_session *session);
 void grdev_session_restore(grdev_session *session);
 
+void grdev_session_add_drm(grdev_session *session, struct udev_device *ud);
+void grdev_session_remove_drm(grdev_session *session, struct udev_device *ud);
+void grdev_session_hotplug_drm(grdev_session *session, struct udev_device *ud);
+
 /*
  * Contexts
  */

commit 650c5444273993f969b9cd7df9add6ab2df0414e
Author: David Herrmann <dh.herrmann at gmail.com>
Date:   Fri Sep 19 14:05:52 2014 +0200

    terminal: add graphics interface
    
    The grdev layer provides graphics-device access via the
    libsystemd-terminal library. It will be used by all terminal helpers to
    actually access display hardware.
    
    Like idev, the grdev layer is built around session objects. On each
    session object you add/remove graphics devices as they appear and vanish.
    Any device type can be supported via specific card-backends. The exported
    grdev API hides any device details.
    
    Graphics devices are represented by "cards". Those are hidden in the
    session and any pipe-configuration is automatically applied. Out of those,
    we configure displays which are then exported to the API user. Displays
    are meant as lowest hardware entity available outside of grdev. The
    underlying pipe configuration is fully hidden and not accessible from the
    outside. The grdev tiling layer allows almost arbitrary setups out of
    multiple pipes, but so far we only use a small subset of this. More will
    follow.
    
    A grdev-display is meant to represent real connected displays/monitors.
    The upper level screen arrangements are user policy and not controlled by
    grdev. Applications are free to apply any policy they want.
    
    Real card-backends will follow in later patches.

diff --git a/Makefile.am b/Makefile.am
index 5dc17f8..1931c5d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -3005,6 +3005,9 @@ libsystemd_terminal_la_CFLAGS = \
 	$(TERMINAL_CFLAGS)
 
 libsystemd_terminal_la_SOURCES = \
+	src/libsystemd-terminal/grdev.h \
+	src/libsystemd-terminal/grdev-internal.h \
+	src/libsystemd-terminal/grdev.c \
 	src/libsystemd-terminal/idev.h \
 	src/libsystemd-terminal/idev-internal.h \
 	src/libsystemd-terminal/idev.c \
diff --git a/configure.ac b/configure.ac
index fb16904..38a165c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1065,7 +1065,7 @@ AM_CONDITIONAL(ENABLE_MULTI_SEAT_X, [test "$have_multi_seat_x" = "yes"])
 have_terminal=no
 AC_ARG_ENABLE(terminal, AS_HELP_STRING([--enable-terminal], [enable terminal support]))
 if test "x$enable_terminal" = "xyes"; then
-        PKG_CHECK_MODULES([TERMINAL], [ libevdev >= 1.2 xkbcommon >= 0.4 ], [have_terminal=yes])
+        PKG_CHECK_MODULES([TERMINAL], [ libevdev >= 1.2 xkbcommon >= 0.4 libdrm >= 2.4], [have_terminal=yes])
         AS_IF([test "x$have_terminal" != xyes -a "x$enable_terminal" = xyes],
               [AC_MSG_ERROR([*** terminal support requested but required dependencies not available])],
               [test "x$have_terminal" = xyes],
diff --git a/src/libsystemd-terminal/grdev-internal.h b/src/libsystemd-terminal/grdev-internal.h
new file mode 100644
index 0000000..7e69c49
--- /dev/null
+++ b/src/libsystemd-terminal/grdev-internal.h
@@ -0,0 +1,237 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright (C) 2014 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  systemd is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <systemd/sd-bus.h>
+#include <systemd/sd-event.h>
+#include "grdev.h"
+#include "hashmap.h"
+#include "list.h"
+#include "util.h"
+
+typedef struct grdev_tile               grdev_tile;
+typedef struct grdev_display_cache      grdev_display_cache;
+
+typedef struct grdev_pipe_vtable        grdev_pipe_vtable;
+typedef struct grdev_pipe               grdev_pipe;
+typedef struct grdev_card_vtable        grdev_card_vtable;
+typedef struct grdev_card               grdev_card;
+
+/*
+ * Displays
+ */
+
+enum {
+        GRDEV_TILE_LEAF,
+        GRDEV_TILE_NODE,
+        GRDEV_TILE_CNT
+};
+
+struct grdev_tile {
+        LIST_FIELDS(grdev_tile, childs_by_node);
+        grdev_tile *parent;
+        grdev_display *display;
+
+        uint32_t x;
+        uint32_t y;
+        unsigned int rotate;
+        unsigned int flip;
+        uint32_t cache_w;
+        uint32_t cache_h;
+
+        unsigned int type;
+
+        union {
+                struct {
+                        grdev_pipe *pipe;
+                } leaf;
+
+                struct {
+                        size_t n_childs;
+                        LIST_HEAD(grdev_tile, child_list);
+                } node;
+        };
+};
+
+int grdev_tile_new_leaf(grdev_tile **out, grdev_pipe *pipe);
+int grdev_tile_new_node(grdev_tile **out);
+grdev_tile *grdev_tile_free(grdev_tile *tile);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdev_tile*, grdev_tile_free);
+
+struct grdev_display {
+        grdev_session *session;
+        char *name;
+
+        size_t n_leafs;
+        grdev_tile *tile;
+
+        size_t n_pipes;
+        size_t max_pipes;
+
+        uint32_t width;
+        uint32_t height;
+
+        struct grdev_display_cache {
+                grdev_pipe *pipe;
+                grdev_display_target target;
+
+                bool incomplete : 1;
+        } *pipes;
+
+        bool enabled : 1;
+        bool public : 1;
+        bool modified : 1;
+        bool framed : 1;
+};
+
+grdev_display *grdev_find_display(grdev_session *session, const char *name);
+
+int grdev_display_new(grdev_display **out, grdev_session *session, const char *name);
+grdev_display *grdev_display_free(grdev_display *display);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdev_display*, grdev_display_free);
+
+/*
+ * Pipes
+ */
+
+struct grdev_pipe_vtable {
+        void (*free) (grdev_pipe *pipe);
+        void (*enable) (grdev_pipe *pipe);
+        void (*disable) (grdev_pipe *pipe);
+        grdev_fb *(*target) (grdev_pipe *pipe);
+};
+
+struct grdev_pipe {
+        const grdev_pipe_vtable *vtable;
+        grdev_card *card;
+        char *name;
+
+        grdev_tile *tile;
+        grdev_display_cache *cache;
+
+        uint32_t width;
+        uint32_t height;
+
+        size_t max_fbs;
+        grdev_fb *front;
+        grdev_fb *back;
+        grdev_fb **fbs;
+
+        bool enabled : 1;
+        bool running : 1;
+        bool flip : 1;
+        bool flipping : 1;
+};
+
+#define GRDEV_PIPE_INIT(_vtable, _card) ((grdev_pipe){ \
+                .vtable = (_vtable), \
+                .card = (_card), \
+        })
+
+grdev_pipe *grdev_find_pipe(grdev_card *card, const char *name);
+
+int grdev_pipe_add(grdev_pipe *pipe, const char *name, size_t n_fbs);
+grdev_pipe *grdev_pipe_free(grdev_pipe *pipe);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdev_pipe*, grdev_pipe_free);
+
+void grdev_pipe_ready(grdev_pipe *pipe, bool running);
+void grdev_pipe_frame(grdev_pipe *pipe);
+
+/*
+ * Cards
+ */
+
+struct grdev_card_vtable {
+        void (*free) (grdev_card *card);
+        void (*enable) (grdev_card *card);
+        void (*disable) (grdev_card *card);
+        void (*commit) (grdev_card *card);
+        void (*restore) (grdev_card *card);
+};
+
+struct grdev_card {
+        const grdev_card_vtable *vtable;
+        grdev_session *session;
+        char *name;
+
+        Hashmap *pipe_map;
+
+        bool enabled : 1;
+        bool modified : 1;
+};
+
+#define GRDEV_CARD_INIT(_vtable, _session) ((grdev_card){ \
+                .vtable = (_vtable), \
+                .session = (_session), \
+        })
+
+grdev_card *grdev_find_card(grdev_session *session, const char *name);
+
+int grdev_card_add(grdev_card *card, const char *name);
+grdev_card *grdev_card_free(grdev_card *card);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdev_card*, grdev_card_free);
+
+/*
+ * Sessions
+ */
+
+struct grdev_session {
+        grdev_context *context;
+        char *name;
+        char *path;
+        grdev_event_fn event_fn;
+        void *userdata;
+
+        unsigned long n_pins;
+
+        Hashmap *card_map;
+        Hashmap *display_map;
+
+        bool custom : 1;
+        bool managed : 1;
+        bool enabled : 1;
+        bool modified : 1;
+};
+
+grdev_session *grdev_session_pin(grdev_session *session);
+grdev_session *grdev_session_unpin(grdev_session *session);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdev_session*, grdev_session_unpin);
+
+/*
+ * Contexts
+ */
+
+struct grdev_context {
+        unsigned long ref;
+        sd_event *event;
+        sd_bus *sysbus;
+
+        Hashmap *session_map;
+};
diff --git a/src/libsystemd-terminal/grdev.c b/src/libsystemd-terminal/grdev.c
new file mode 100644
index 0000000..ab1c407
--- /dev/null
+++ b/src/libsystemd-terminal/grdev.c
@@ -0,0 +1,1219 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright (C) 2014 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  systemd is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <systemd/sd-bus.h>
+#include <systemd/sd-event.h>
+#include <systemd/sd-login.h>
+#include "grdev.h"
+#include "grdev-internal.h"
+#include "hashmap.h"
+#include "login-shared.h"
+#include "macro.h"
+#include "util.h"
+
+static void pipe_enable(grdev_pipe *pipe);
+static void pipe_disable(grdev_pipe *pipe);
+static void card_modified(grdev_card *card);
+static void session_frame(grdev_session *session, grdev_display *display);
+
+/*
+ * Displays
+ */
+
+static inline grdev_tile *tile_leftmost(grdev_tile *tile) {
+        if (!tile)
+                return NULL;
+
+        while (tile->type == GRDEV_TILE_NODE && tile->node.child_list)
+                tile = tile->node.child_list;
+
+        return tile;
+}
+
+#define TILE_FOREACH(_root, _i) \
+        for (_i = tile_leftmost(_root); _i; _i = tile_leftmost(_i->childs_by_node_next) ? : _i->parent)
+
+#define TILE_FOREACH_SAFE(_root, _i, _next) \
+        for (_i = tile_leftmost(_root); _i && ((_next = tile_leftmost(_i->childs_by_node_next) ? : _i->parent), true); _i = _next)
+
+static void tile_link(grdev_tile *tile, grdev_tile *parent) {
+        grdev_display *display;
+        grdev_tile *t;
+
+        assert(tile);
+        assert(!tile->parent);
+        assert(!tile->display);
+        assert(parent);
+        assert(parent->type == GRDEV_TILE_NODE);
+
+        display = parent->display;
+
+        assert(!display || !display->enabled);
+
+        ++parent->node.n_childs;
+        LIST_PREPEND(childs_by_node, parent->node.child_list, tile);
+        tile->parent = parent;
+
+        if (display) {
+                display->modified = true;
+                TILE_FOREACH(tile, t) {
+                        t->display = display;
+                        if (t->type == GRDEV_TILE_LEAF) {
+                                ++display->n_leafs;
+                                if (display->enabled)
+                                        pipe_enable(t->leaf.pipe);
+                        }
+                }
+        }
+}
+
+static void tile_unlink(grdev_tile *tile) {
+        grdev_tile *parent, *t;
+        grdev_display *display;
+
+        assert(tile);
+
+        display = tile->display;
+        parent = tile->parent;
+        if (!parent) {
+                assert(!display);
+                return;
+        }
+
+        assert(parent->type == GRDEV_TILE_NODE);
+        assert(parent->display == display);
+        assert(parent->node.n_childs > 0);
+
+        --parent->node.n_childs;
+        LIST_REMOVE(childs_by_node, parent->node.child_list, tile);
+        tile->parent = NULL;
+
+        if (display) {
+                display->modified = true;
+                TILE_FOREACH(tile, t) {
+                        t->display = NULL;
+                        if (t->type == GRDEV_TILE_LEAF) {
+                                --display->n_leafs;
+                                t->leaf.pipe->cache = NULL;
+                                pipe_disable(t->leaf.pipe);
+                        }
+                }
+        }
+
+        /* Tile trees are driven by leafs. Internal nodes have no owner, thus,
+         * we must take care to not leave them around. Therefore, whenever we
+         * unlink any part of a tree, we also destroy the parent, in case it's
+         * now stale.
+         * Parents are stale if they have no childs and either have no display
+         * or if they are intermediate nodes (i.e, they have a parent).
+         * This means, you can easily create trees, but you can never partially
+         * move or destruct them so far. They're always reduced to minimal form
+         * if you cut them. This might change later, but so far we didn't need
+         * partial destruction or the ability to move whole trees. */
+
+        if (parent->node.n_childs < 1 && (parent->parent || !parent->display))
+                grdev_tile_free(parent);
+}
+
+static int tile_new(grdev_tile **out) {
+        _cleanup_(grdev_tile_freep) grdev_tile *tile = NULL;
+
+        assert(out);
+
+        tile = new0(grdev_tile, 1);
+        if (!tile)
+                return -ENOMEM;
+
+        tile->type = (unsigned)-1;
+
+        *out = tile;
+        tile = NULL;
+        return 0;
+}
+
+int grdev_tile_new_leaf(grdev_tile **out, grdev_pipe *pipe) {
+        _cleanup_(grdev_tile_freep) grdev_tile *tile = NULL;
+        int r;
+
+        assert_return(pipe, -EINVAL);
+        assert_return(!pipe->tile, -EINVAL);
+
+        r = tile_new(&tile);
+        if (r < 0)
+                return r;
+
+        tile->type = GRDEV_TILE_LEAF;
+        tile->leaf.pipe = pipe;
+
+        if (out)
+                *out = tile;
+        tile = NULL;
+        return 0;
+}
+
+int grdev_tile_new_node(grdev_tile **out) {
+        _cleanup_(grdev_tile_freep) grdev_tile *tile = NULL;
+        int r;
+
+        assert_return(out, -EINVAL);
+
+        r = tile_new(&tile);
+        if (r < 0)
+                return r;
+
+        tile->type = GRDEV_TILE_NODE;
+
+        *out = tile;
+        tile = NULL;
+        return 0;
+}
+
+grdev_tile *grdev_tile_free(grdev_tile *tile) {
+        if (!tile)
+                return NULL;
+
+        tile_unlink(tile);
+
+        switch (tile->type) {
+        case GRDEV_TILE_LEAF:
+                assert(!tile->parent);
+                assert(!tile->display);
+                assert(tile->leaf.pipe);
+
+                break;
+        case GRDEV_TILE_NODE:
+                assert(!tile->parent);
+                assert(!tile->display);
+                assert(tile->node.n_childs == 0);
+
+                break;
+        }
+
+        free(tile);
+
+        return NULL;
+}
+
+grdev_display *grdev_find_display(grdev_session *session, const char *name) {
+        assert_return(session, NULL);
+        assert_return(name, NULL);
+
+        return hashmap_get(session->display_map, name);
+}
+
+int grdev_display_new(grdev_display **out, grdev_session *session, const char *name) {
+        _cleanup_(grdev_display_freep) grdev_display *display = NULL;
+        int r;
+
+        assert(session);
+        assert(name);
+
+        display = new0(grdev_display, 1);
+        if (!display)
+                return -ENOMEM;
+
+        display->session = session;
+
+        display->name = strdup(name);
+        if (!display->name)
+                return -ENOMEM;
+
+        r = grdev_tile_new_node(&display->tile);
+        if (r < 0)
+                return r;
+
+        display->tile->display = display;
+
+        r = hashmap_put(session->display_map, display->name, display);
+        if (r < 0)
+                return r;
+
+        if (out)
+                *out = display;
+        display = NULL;
+        return 0;
+}
+
+grdev_display *grdev_display_free(grdev_display *display) {
+        if (!display)
+                return NULL;
+
+        assert(!display->public);
+        assert(!display->enabled);
+        assert(!display->modified);
+        assert(display->n_leafs == 0);
+        assert(display->n_pipes == 0);
+
+        if (display->name)
+                hashmap_remove_value(display->session->display_map, display->name, display);
+
+        if (display->tile) {
+                display->tile->display = NULL;
+                grdev_tile_free(display->tile);
+        }
+
+        free(display->pipes);
+        free(display->name);
+        free(display);
+
+        return NULL;
+}
+
+bool grdev_display_is_enabled(grdev_display *display) {
+        return display && display->enabled;
+}
+
+void grdev_display_enable(grdev_display *display) {
+        grdev_tile *t;
+
+        assert(display);
+
+        if (!display->enabled) {
+                display->enabled = true;
+                TILE_FOREACH(display->tile, t)
+                        if (t->type == GRDEV_TILE_LEAF)
+                                pipe_enable(t->leaf.pipe);
+        }
+}
+
+void grdev_display_disable(grdev_display *display) {
+        grdev_tile *t;
+
+        assert(display);
+
+        if (display->enabled) {
+                display->enabled = false;
+                TILE_FOREACH(display->tile, t)
+                        if (t->type == GRDEV_TILE_LEAF)
+                                pipe_disable(t->leaf.pipe);
+        }
+}
+
+const grdev_display_target *grdev_display_next_target(grdev_display *display, const grdev_display_target *prev, uint64_t minage) {
+        grdev_display_cache *cache;
+        size_t idx;
+
+        assert_return(display, NULL);
+        assert_return(!display->modified, NULL);
+        assert_return(display->enabled, NULL);
+
+        if (prev) {
+                cache = container_of(prev, grdev_display_cache, target);
+
+                assert(cache->pipe);
+                assert(cache->pipe->tile->display == display);
+                assert(display->pipes >= cache);
+
+                idx = (cache - display->pipes) / sizeof(*cache) + 1;
+        } else {
+                idx = 0;
+        }
+
+        for (cache = display->pipes + idx; idx < display->n_pipes; ++idx, ++cache) {
+                grdev_display_target *target;
+                grdev_pipe *pipe;
+                grdev_fb *fb;
+
+                pipe = cache->pipe;
+                target = &cache->target;
+
+                if (!pipe->running || !pipe->enabled)
+                        continue;
+
+                /* if front-buffer is up-to-date, there's nothing to do */
+                if (minage > 0 && pipe->front && pipe->front->age >= minage)
+                        continue;
+
+                /* find suitable back-buffer */
+                if (!(fb = pipe->back)) {
+                        if (!pipe->vtable->target || !(fb = pipe->vtable->target(pipe)))
+                                continue;
+                }
+
+                /* if back-buffer is up-to-date, schedule flip */
+                if (minage > 0 && fb->age >= minage) {
+                        grdev_display_flip_target(display, target, fb->age);
+                        continue;
+                }
+
+                /* we have an out-of-date back-buffer; return for redraw */
+                target->fb = fb;
+                return target;
+        }
+
+        return NULL;
+}
+
+void grdev_display_flip_target(grdev_display *display, const grdev_display_target *target, uint64_t age) {
+        grdev_display_cache *cache;
+        size_t i;
+
+        assert(display);
+        assert(!display->modified);
+        assert(display->enabled);
+        assert(target);
+        assert(target->fb);
+
+        cache = container_of(target, grdev_display_cache, target);
+
+        assert(cache->pipe);
+        assert(cache->pipe->tile->display == display);
+
+        /* reset age of all FB on overflow */
+        if (age < target->fb->age)
+                for (i = 0; i < cache->pipe->max_fbs; ++i)
+                        if (cache->pipe->fbs[i])
+                                cache->pipe->fbs[i]->age = 0;
+
+        ((grdev_fb*)target->fb)->age = age;
+        cache->pipe->flip = true;
+}
+
+static void display_cache_apply(grdev_display_cache *c, grdev_tile *l) {
+        uint32_t x, y, width, height;
+        grdev_display_target *t;
+
+        assert(c);
+        assert(l);
+        assert(l->cache_w >= c->target.width + c->target.x);
+        assert(l->cache_h >= c->target.height + c->target.y);
+
+        t = &c->target;
+
+        /* rotate child */
+
+        t->rotate = (t->rotate + l->rotate) & 0x3;
+
+        x = t->x;
+        y = t->y;
+        width = t->width;
+        height = t->height;
+
+        switch (l->rotate) {
+        case GRDEV_ROTATE_0:
+                break;
+        case GRDEV_ROTATE_90:
+                t->x = l->cache_h - (height + y);
+                t->y = x;
+                t->width = height;
+                t->height = width;
+                break;
+        case GRDEV_ROTATE_180:
+                t->x = l->cache_w - (width + x);
+                t->y = l->cache_h - (height + y);
+                break;
+        case GRDEV_ROTATE_270:
+                t->x = y;
+                t->y = l->cache_w - (width + x);
+                t->width = height;
+                t->height = width;
+                break;
+        }
+
+        /* flip child */
+
+        t->flip ^= l->flip;
+
+        if (l->flip & GRDEV_FLIP_HORIZONTAL)
+                t->x = l->cache_w - (t->width + t->x);
+        if (l->flip & GRDEV_FLIP_VERTICAL)
+                t->y = l->cache_h - (t->height + t->y);
+
+        /* move child */
+
+        t->x += l->x;
+        t->y += l->y;
+}
+
+static void display_cache_targets(grdev_display *display) {
+        grdev_display_cache *c;
+        grdev_tile *tile;
+
+        assert(display);
+
+        /* depth-first with childs before parent */
+        for (tile = tile_leftmost(display->tile);
+             tile;
+             tile = tile_leftmost(tile->childs_by_node_next) ? : tile->parent) {
+                if (tile->type == GRDEV_TILE_LEAF) {
+                        grdev_pipe *p;
+
+                        /* We're at a leaf and no parent has been cached, yet.
+                         * Copy the pipe information into the target cache and
+                         * update our global pipe-caches if required. */
+
+                        assert(tile->leaf.pipe);
+                        assert(display->n_pipes + 1 <= display->max_pipes);
+
+                        p = tile->leaf.pipe;
+                        c = &display->pipes[display->n_pipes++];
+
+                        zero(*c);
+                        c->pipe = p;
+                        c->pipe->cache = c;
+                        c->target.width = p->width;
+                        c->target.height = p->height;
+                        tile->cache_w = p->width;
+                        tile->cache_h = p->height;
+
+                        /* all new tiles are incomplete due to geometry changes */
+                        c->incomplete = true;
+
+                        display_cache_apply(c, tile);
+                } else {
+                        grdev_tile *child, *l;
+
+                        /* We're now at a node with all it's childs already
+                         * computed (depth-first, child before parent). We
+                         * first need to know the size of our tile, then we
+                         * recurse into all leafs and update their cache. */
+
+                        tile->cache_w = 0;
+                        tile->cache_h = 0;
+
+                        LIST_FOREACH(childs_by_node, child, tile->node.child_list) {
+                                if (child->x + child->cache_w > tile->cache_w)
+                                        tile->cache_w = child->x + child->cache_w;
+                                if (child->y + child->cache_h > tile->cache_h)
+                                        tile->cache_h = child->y + child->cache_h;
+                        }
+
+                        assert(tile->cache_w > 0);
+                        assert(tile->cache_h > 0);
+
+                        TILE_FOREACH(tile, l)
+                                if (l->type == GRDEV_TILE_LEAF)
+                                        display_cache_apply(l->leaf.pipe->cache, tile);
+                }
+        }
+}
+
+static bool display_cache(grdev_display *display) {
+        grdev_tile *tile;
+        size_t n;
+        void *t;
+        int r;
+
+        assert(display);
+
+        if (!display->modified)
+                return false;
+
+        display->modified = false;
+        display->framed = false;
+        display->n_pipes = 0;
+        display->width = 0;
+        display->height = 0;
+
+        if (display->n_leafs < 1)
+                return false;
+
+        TILE_FOREACH(display->tile, tile)
+                if (tile->type == GRDEV_TILE_LEAF)
+                        tile->leaf.pipe->cache = NULL;
+
+        if (display->n_leafs > display->max_pipes) {
+                n = ALIGN_POWER2(display->n_leafs);
+                if (!n) {
+                        r = -ENOMEM;
+                        goto out;
+                }
+
+                t = realloc_multiply(display->pipes, sizeof(*display->pipes), n);
+                if (!t) {
+                        r = -ENOMEM;
+                        goto out;
+                }
+
+                display->pipes = t;
+                display->max_pipes = n;
+        }
+
+        display_cache_targets(display);
+
+        r = 0;
+
+out:
+        if (r < 0)
+                log_debug("grdev: %s/%s: cannot cache pipes: %s",
+                          display->session->name, display->name, strerror(-r));
+        return true;
+}
+
+/*
+ * Pipes
+ */
+
+grdev_pipe *grdev_find_pipe(grdev_card *card, const char *name) {
+        assert_return(card, NULL);
+        assert_return(name, NULL);
+
+        return hashmap_get(card->pipe_map, name);
+}
+
+int grdev_pipe_add(grdev_pipe *pipe, const char *name, size_t n_fbs) {
+        int r;
+
+        assert_return(pipe, -EINVAL);
+        assert_return(pipe->vtable, -EINVAL);
+        assert_return(pipe->vtable->free, -EINVAL);
+        assert_return(pipe->card, -EINVAL);
+        assert_return(pipe->card->session, -EINVAL);
+        assert_return(!pipe->cache, -EINVAL);
+        assert_return(pipe->width > 0, -EINVAL);
+        assert_return(pipe->height > 0, -EINVAL);
+        assert_return(!pipe->enabled, -EINVAL);
+        assert_return(!pipe->running, -EINVAL);
+        assert_return(name, -EINVAL);
+
+        pipe->name = strdup(name);
+        if (!pipe->name)
+                return -ENOMEM;
+
+        if (n_fbs > 0) {
+                pipe->fbs = new0(grdev_fb*, n_fbs);
+                if (!pipe->fbs)
+                        return -ENOMEM;
+
+                pipe->max_fbs = n_fbs;
+        }
+
+        r = grdev_tile_new_leaf(&pipe->tile, pipe);
+        if (r < 0)
+                return r;
+
+        r = hashmap_put(pipe->card->pipe_map, pipe->name, pipe);
+        if (r < 0)
+                return r;
+
+        card_modified(pipe->card);
+        return 0;
+}
+
+grdev_pipe *grdev_pipe_free(grdev_pipe *pipe) {
+        grdev_pipe tmp;
+
+        if (!pipe)
+                return NULL;
+
+        assert(pipe->card);
+        assert(pipe->vtable);
+        assert(pipe->vtable->free);
+
+        if (pipe->name)
+                hashmap_remove_value(pipe->card->pipe_map, pipe->name, pipe);
+        if (pipe->tile)
+                tile_unlink(pipe->tile);
+
+        assert(!pipe->cache);
+
+        tmp = *pipe;
+        pipe->vtable->free(pipe);
+
+        grdev_tile_free(tmp.tile);
+        card_modified(tmp.card);
+        free(tmp.fbs);
+        free(tmp.name);
+
+        return NULL;
+}
+
+static void pipe_enable(grdev_pipe *pipe) {
+        assert(pipe);
+
+        if (!pipe->enabled) {
+                pipe->enabled = true;
+                if (pipe->vtable->enable)
+                        pipe->vtable->enable(pipe);
+        }
+}
+
+static void pipe_disable(grdev_pipe *pipe) {
+        assert(pipe);
+
+        if (pipe->enabled) {
+                pipe->enabled = false;
+                if (pipe->vtable->disable)
+                        pipe->vtable->disable(pipe);
+        }
+}
+
+void grdev_pipe_ready(grdev_pipe *pipe, bool running) {
+        assert(pipe);
+
+        /* grdev_pipe_ready() is used by backends to notify about pipe state
+         * changed. If a pipe is ready, it can be fully used by us (available,
+         * enabled and accessable). Backends can disable pipes at any time
+         * (like for async revocation), but can only enable them from parent
+         * context. Otherwise, we might call user-callbacks recursively. */
+
+        if (pipe->running == running)
+                return;
+
+        pipe->running = running;
+
+        /* runtime events for unused pipes are not interesting */
+        if (pipe->cache) {
+                grdev_display *display = pipe->tile->display;
+
+                assert(display);
+
+                if (running) {
+                        if (pipe->enabled)
+                                session_frame(display->session, display);
+                } else {
+                        pipe->cache->incomplete = true;
+                }
+        }
+}
+
+void grdev_pipe_frame(grdev_pipe *pipe) {
+        grdev_display *display;
+
+        assert(pipe);
+
+        /* if pipe is unused, ignore any frame events */
+        if (!pipe->cache)
+                return;
+
+        display = pipe->tile->display;
+        assert(display);
+
+        if (pipe->enabled)
+                session_frame(display->session, display);
+}
+
+/*
+ * Cards
+ */
+
+grdev_card *grdev_find_card(grdev_session *session, const char *name) {
+        assert_return(session, NULL);
+        assert_return(name, NULL);
+
+        return hashmap_get(session->card_map, name);
+}
+
+int grdev_card_add(grdev_card *card, const char *name) {
+        int r;
+
+        assert_return(card, -EINVAL);
+        assert_return(card->vtable, -EINVAL);
+        assert_return(card->vtable->free, -EINVAL);
+        assert_return(card->session, -EINVAL);
+        assert_return(name, -EINVAL);
+
+        card->name = strdup(name);
+        if (!card->name)
+                return -ENOMEM;
+
+        card->pipe_map = hashmap_new(&string_hash_ops);
+        if (!card->pipe_map)
+                return -ENOMEM;
+
+        r = hashmap_put(card->session->card_map, card->name, card);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+grdev_card *grdev_card_free(grdev_card *card) {
+        grdev_card tmp;
+
+        if (!card)
+                return NULL;
+
+        assert(!card->enabled);
+        assert(card->vtable);
+        assert(card->vtable->free);
+
+        if (card->name)
+                hashmap_remove_value(card->session->card_map, card->name, card);
+
+        tmp = *card;
+        card->vtable->free(card);
+
+        assert(hashmap_size(tmp.pipe_map) == 0);
+
+        hashmap_free(tmp.pipe_map);
+        free(tmp.name);
+
+        return NULL;
+}
+
+static void card_modified(grdev_card *card) {
+        assert(card);
+        assert(card->session->n_pins > 0);
+
+        card->modified = true;
+}
+
+static void grdev_card_enable(grdev_card *card) {
+        assert(card);
+
+        if (!card->enabled) {
+                card->enabled = true;
+                if (card->vtable->enable)
+                        card->vtable->enable(card);
+        }
+}
+
+static void grdev_card_disable(grdev_card *card) {
+        assert(card);
+
+        if (card->enabled) {
+                card->enabled = false;
+                if (card->vtable->disable)
+                        card->vtable->disable(card);
+        }
+}
+
+/*
+ * Sessions
+ */
+
+static void session_raise(grdev_session *session, grdev_event *event) {
+        session->event_fn(session, session->userdata, event);
+}
+
+static void session_raise_display_add(grdev_session *session, grdev_display *display) {
+        grdev_event event = {
+                .type = GRDEV_EVENT_DISPLAY_ADD,
+                .display_add = {
+                        .display = display,
+                },
+        };
+
+        session_raise(session, &event);
+}
+
+static void session_raise_display_remove(grdev_session *session, grdev_display *display) {
+        grdev_event event = {
+                .type = GRDEV_EVENT_DISPLAY_REMOVE,
+                .display_remove = {
+                        .display = display,
+                },
+        };
+
+        session_raise(session, &event);
+}
+
+static void session_raise_display_change(grdev_session *session, grdev_display *display) {
+        grdev_event event = {
+                .type = GRDEV_EVENT_DISPLAY_CHANGE,
+                .display_change = {
+                        .display = display,
+                },
+        };
+
+        session_raise(session, &event);
+}
+
+static void session_raise_display_frame(grdev_session *session, grdev_display *display) {
+        grdev_event event = {
+                .type = GRDEV_EVENT_DISPLAY_FRAME,
+                .display_frame = {
+                        .display = display,
+                },
+        };
+
+        session_raise(session, &event);
+}
+
+static void session_add_card(grdev_session *session, grdev_card *card) {
+        assert(session);
+        assert(card);
+
+        log_debug("grdev: %s: add card '%s'", session->name, card->name);
+
+        /* Cards are not exposed to users, but managed internally. Cards are
+         * enabled if the session is enabled, and will track that state. The
+         * backend can probe the card at any time, but only if enabled. It
+         * will then add pipes according to hardware state.
+         * That is, the card may create pipes as soon as we enable it here. */
+
+        if (session->enabled)
+                grdev_card_enable(card);
+}
+
+static void session_remove_card(grdev_session *session, grdev_card *card) {
+        assert(session);
+        assert(card);
+
+        log_debug("grdev: %s: remove card '%s'", session->name, card->name);
+
+        /* As cards are not exposed, it can never be accessed by outside
+         * users and we can simply remove it. Disabling the card does not
+         * necessarily drop all pipes of the card. This is usually deferred
+         * to card destruction (as pipes are cached as long as FDs remain
+         * open). Therefore, the card destruction might cause pipes, and thus
+         * visible displays, to be removed. */
+
+        grdev_card_disable(card);
+        grdev_card_free(card);
+}
+
+static void session_add_display(grdev_session *session, grdev_display *display) {
+        assert(session);
+        assert(display);
+        assert(!display->enabled);
+
+        log_debug("grdev: %s: add display '%s'", session->name, display->name);
+
+        /* Displays are the main entity for public API users. We create them
+         * independent of card backends and they wrap any underlying display
+         * architecture. Displays are public at all times, thus, may be entered
+         * by outside users at any time. */
+
+        display->public = true;
+        session_raise_display_add(session, display);
+}
+
+static void session_remove_display(grdev_session *session, grdev_display *display) {
+        assert(session);
+        assert(display);
+
+        log_debug("grdev: %s: remove display '%s'", session->name, display->name);
+
+        /* Displays are public, so we have to be careful when removing them.
+         * We first tell users about their removal, disable them and then drop
+         * them. We now, after the notification, no external access will
+         * happen. Therefore, we can release the tiles afterwards safely. */
+
+        if (display->public) {
+                display->public = false;
+                session_raise_display_remove(session, display);
+        }
+
+        grdev_display_disable(display);
+        grdev_display_free(display);
+}
+
+static void session_change_display(grdev_session *session, grdev_display *display) {
+        bool changed;
+
+        assert(session);
+        assert(display);
+
+        changed = display_cache(display);
+
+        if (display->n_leafs == 0)
+                session_remove_display(session, display);
+        else if (!display->public)
+                session_add_display(session, display);
+        else if (changed)
+                session_raise_display_change(session, display);
+        else if (display->framed)
+                session_frame(session, display);
+}
+
+static void session_frame(grdev_session *session, grdev_display *display) {
+        assert(session);
+        assert(display);
+
+        display->framed = false;
+
+        if (!display->enabled || !session->enabled)
+                return;
+
+        if (session->n_pins > 0)
+                display->framed = true;
+        else
+                session_raise_display_frame(session, display);
+}
+
+int grdev_session_new(grdev_session **out,
+                      grdev_context *context,
+                      unsigned int flags,
+                      const char *name,
+                      grdev_event_fn event_fn,
+                      void *userdata) {
+        _cleanup_(grdev_session_freep) grdev_session *session = NULL;
+        int r;
+
+        assert(out);
+        assert(context);
+        assert(name);
+        assert(event_fn);
+        assert_return(session_id_valid(name) == !(flags & GRDEV_SESSION_CUSTOM), -EINVAL);
+        assert_return(!(flags & GRDEV_SESSION_CUSTOM) || !(flags & GRDEV_SESSION_MANAGED), -EINVAL);
+        assert_return(!(flags & GRDEV_SESSION_MANAGED) || context->sysbus, -EINVAL);
+
+        session = new0(grdev_session, 1);
+        if (!session)
+                return -ENOMEM;
+
+        session->context = grdev_context_ref(context);
+        session->custom = flags & GRDEV_SESSION_CUSTOM;
+        session->managed = flags & GRDEV_SESSION_MANAGED;
+        session->event_fn = event_fn;
+        session->userdata = userdata;
+
+        session->name = strdup(name);
+        if (!session->name)
+                return -ENOMEM;
+
+        if (session->managed) {
+                r = sd_bus_path_encode("/org/freedesktop/login1/session",
+                                       session->name, &session->path);
+                if (r < 0)
+                        return r;
+        }
+
+        session->card_map = hashmap_new(&string_hash_ops);
+        if (!session->card_map)
+                return -ENOMEM;
+
+        session->display_map = hashmap_new(&string_hash_ops);
+        if (!session->display_map)
+                return -ENOMEM;
+
+        r = hashmap_put(context->session_map, session->name, session);
+        if (r < 0)
+                return r;
+
+        *out = session;
+        session = NULL;
+        return 0;
+}
+
+grdev_session *grdev_session_free(grdev_session *session) {
+        grdev_card *card;
+
+        if (!session)
+                return NULL;
+
+        grdev_session_disable(session);
+
+        while ((card = hashmap_first(session->card_map)))
+                session_remove_card(session, card);
+
+        assert(hashmap_size(session->display_map) == 0);
+
+        if (session->name)
+                hashmap_remove_value(session->context->session_map, session->name, session);
+
+        hashmap_free(session->display_map);
+        hashmap_free(session->card_map);
+        session->context = grdev_context_unref(session->context);
+        free(session->path);
+        free(session->name);
+        free(session);
+
+        return NULL;
+}
+
+bool grdev_session_is_enabled(grdev_session *session) {
+        return session && session->enabled;
+}
+
+void grdev_session_enable(grdev_session *session) {
+        grdev_card *card;
+        Iterator iter;
+
+        assert(session);
+
+        if (!session->enabled) {
+                session->enabled = true;
+                HASHMAP_FOREACH(card, session->card_map, iter)
+                        grdev_card_enable(card);
+        }
+}
+
+void grdev_session_disable(grdev_session *session) {
+        grdev_card *card;
+        Iterator iter;
+
+        assert(session);
+
+        if (session->enabled) {
+                session->enabled = false;
+                HASHMAP_FOREACH(card, session->card_map, iter)
+                        grdev_card_disable(card);
+        }
+}
+
+void grdev_session_commit(grdev_session *session) {
+        grdev_card *card;
+        Iterator iter;
+
+        assert(session);
+
+        if (!session->enabled)
+                return;
+
+        HASHMAP_FOREACH(card, session->card_map, iter)
+                if (card->vtable->commit)
+                        card->vtable->commit(card);
+}
+
+void grdev_session_restore(grdev_session *session) {
+        grdev_card *card;
+        Iterator iter;
+
+        assert(session);
+
+        if (!session->enabled)
+                return;
+
+        HASHMAP_FOREACH(card, session->card_map, iter)
+                if (card->vtable->restore)
+                        card->vtable->restore(card);
+}
+
+static void session_configure(grdev_session *session) {
+        grdev_display *display;
+        grdev_tile *tile;
+        grdev_card *card;
+        grdev_pipe *pipe;
+        Iterator i, j;
+        int r;
+
+        assert(session);
+
+        /*
+         * Whenever backends add or remove pipes, we set session->modified and
+         * require them to pin the session while modifying it. On release, we
+         * reconfigure the device and re-assign displays to all modified pipes.
+         *
+         * So far, we configure each pipe as a separate display. We do not
+         * support user-configuration, nor have we gotten any reports from
+         * users with multi-pipe monitors (4k on DP-1.2 MST and so on). Until
+         * we get reports, we keep the logic to a minimum.
+         */
+
+        /* create new displays for all unconfigured pipes */
+        HASHMAP_FOREACH(card, session->card_map, i) {
+                if (!card->modified)
+                        continue;
+
+                card->modified = false;
+
+                HASHMAP_FOREACH(pipe, card->pipe_map, j) {
+                        tile = pipe->tile;
+                        if (tile->display)
+                                continue;
+
+                        assert(!tile->parent);
+
+                        display = grdev_find_display(session, pipe->name);
+                        if (display && display->tile) {
+                                log_debug("grdev: %s/%s: occupied display for pipe %s",
+                                          session->name, card->name, pipe->name);
+                                continue;
+                        } else if (!display) {
+                                r = grdev_display_new(&display, session, pipe->name);
+                                if (r < 0) {
+                                        log_debug("grdev: %s/%s: cannot create display for pipe %s: %s",
+                                                  session->name, card->name, pipe->name, strerror(-r));
+                                        continue;
+                                }
+                        }
+
+                        tile_link(pipe->tile, display->tile);
+                }
+        }
+
+        /* update displays */
+        HASHMAP_FOREACH(display, session->display_map, i)
+                session_change_display(session, display);
+}
+
+grdev_session *grdev_session_pin(grdev_session *session) {
+        assert(session);
+
+        ++session->n_pins;
+        return session;
+}
+
+grdev_session *grdev_session_unpin(grdev_session *session) {
+        if (!session)
+                return NULL;
+
+        assert(session->n_pins > 0);
+
+        if (--session->n_pins == 0)
+                session_configure(session);
+
+        return NULL;
+}
+
+/*
+ * Contexts
+ */
+
+int grdev_context_new(grdev_context **out, sd_event *event, sd_bus *sysbus) {
+        _cleanup_(grdev_context_unrefp) grdev_context *context = NULL;
+
+        assert_return(out, -EINVAL);
+        assert_return(event, -EINVAL);
+
+        context = new0(grdev_context, 1);
+        if (!context)
+                return -ENOMEM;
+
+        context->ref = 1;
+        context->event = sd_event_ref(event);
+
+        if (sysbus)
+                context->sysbus = sd_bus_ref(sysbus);
+
+        context->session_map = hashmap_new(&string_hash_ops);
+        if (!context->session_map)
+                return -ENOMEM;
+
+        *out = context;
+        context = NULL;
+        return 0;
+}
+
+static void context_cleanup(grdev_context *context) {
+        assert(hashmap_size(context->session_map) == 0);
+
+        hashmap_free(context->session_map);
+        context->sysbus = sd_bus_unref(context->sysbus);
+        context->event = sd_event_unref(context->event);
+        free(context);
+}
+
+grdev_context *grdev_context_ref(grdev_context *context) {
+        assert_return(context, NULL);
+        assert_return(context->ref > 0, NULL);
+
+        ++context->ref;
+        return context;
+}
+
+grdev_context *grdev_context_unref(grdev_context *context) {
+        if (!context)
+                return NULL;
+
+        assert_return(context->ref > 0, NULL);
+
+        if (--context->ref == 0)
+                context_cleanup(context);
+
+        return NULL;
+}
diff --git a/src/libsystemd-terminal/grdev.h b/src/libsystemd-terminal/grdev.h
new file mode 100644
index 0000000..2645b12
--- /dev/null
+++ b/src/libsystemd-terminal/grdev.h
@@ -0,0 +1,182 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright (C) 2014 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  systemd is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+/*
+ * Graphics Devices
+ * The grdev layer provides generic access to graphics devices. The device
+ * types are hidden in the implementation and exported in a generic way. The
+ * grdev_session object forms the base layer. It loads, configures and prepares
+ * any graphics devices associated with that session. Each session is totally
+ * independent of other sessions and can be controlled separately.
+ * The target devices on a session are called display. A display always
+ * corresponds to a real display regardless how many pipes are needed to drive
+ * that display. That is, an exported display might internally be created out
+ * of arbitrary combinations of target pipes. However, this is meant as
+ * implementation detail and API users must never assume details below the
+ * display-level. That is, a display is the most low-level object exported.
+ * Therefore, pipe-configuration and any low-level modesetting is hidden from
+ * the public API. It is provided by the implementation, and it is the
+ * implementation that decides how pipes are driven.
+ *
+ * The API users are free to ignore specific displays or combine them to create
+ * larger screens. This often requires user-configuration so is dictated by
+ * policy. The underlying pipe-configuration might be affected by these
+ * high-level policies, but is never directly controlled by those. That means,
+ * depending on the displays you use, it might affect how underlying resources
+ * are assigned. However, users can never directly apply policies to the pipes,
+ * but only to displays. In case specific hardware needs quirks on the pipe
+ * level, we support that via hwdb, not via public user configuration.
+ *
+ * Right now, displays are limited to rgb32 memory-mapped framebuffers on the
+ * primary plane. However, the grdev implementation can be easily extended to
+ * allow more powerful access (including hardware-acceleration for 2D and 3D
+ * compositing). So far, this wasn't needed so it is not exposed.
+ */
+
+#pragma once
+
+#include <drm_fourcc.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <systemd/sd-bus.h>
+#include <systemd/sd-event.h>
+#include "util.h"
+
+typedef struct grdev_fb                 grdev_fb;
+typedef struct grdev_display_target     grdev_display_target;
+typedef struct grdev_display            grdev_display;
+
+typedef struct grdev_event              grdev_event;
+typedef struct grdev_session            grdev_session;
+typedef struct grdev_context            grdev_context;
+
+enum {
+        /* clockwise rotation; we treat this is abelian group Z4 with ADD */
+        GRDEV_ROTATE_0                  = 0,
+        GRDEV_ROTATE_90                 = 1,
+        GRDEV_ROTATE_180                = 2,
+        GRDEV_ROTATE_270                = 3,
+};
+
+enum {
+        /* flip states; we treat this as abelian group V4 with XOR */
+        GRDEV_FLIP_NONE                 = 0x0,
+        GRDEV_FLIP_HORIZONTAL           = 0x1,
+        GRDEV_FLIP_VERTICAL             = 0x2,
+};
+
+/*
+ * Displays
+ */
+
+struct grdev_fb {
+        uint32_t width;
+        uint32_t height;
+        uint32_t format;
+        uint64_t age;
+        int32_t strides[4];
+        void *maps[4];
+};
+
+struct grdev_display_target {
+        uint32_t x;
+        uint32_t y;
+        uint32_t width;
+        uint32_t height;
+        unsigned int rotate;
+        unsigned int flip;
+        const grdev_fb *fb;
+};
+
+bool grdev_display_is_enabled(grdev_display *display);
+void grdev_display_enable(grdev_display *display);
+void grdev_display_disable(grdev_display *display);
+
+const grdev_display_target *grdev_display_next_target(grdev_display *display, const grdev_display_target *prev, uint64_t minage);
+void grdev_display_flip_target(grdev_display *display, const grdev_display_target *target, uint64_t age);
+
+#define GRDEV_DISPLAY_FOREACH_TARGET(_display, _t, _minage)                     \
+        for ((_t) = grdev_display_next_target((_display), NULL, (_minage));     \
+             (_t);                                                              \
+             (_t) = grdev_display_next_target((_display), (_t), (_minage)))
+
+/*
+ * Events
+ */
+
+enum {
+        GRDEV_EVENT_DISPLAY_ADD,
+        GRDEV_EVENT_DISPLAY_REMOVE,
+        GRDEV_EVENT_DISPLAY_CHANGE,
+        GRDEV_EVENT_DISPLAY_FRAME,
+};
+
+typedef void (*grdev_event_fn) (grdev_session *session, void *userdata, grdev_event *ev);
+
+struct grdev_event {
+        unsigned int type;
+        union {
+                struct {
+                        grdev_display *display;
+                } display_add, display_remove, display_change;
+
+                struct {
+                        grdev_display *display;
+                } display_frame;
+        };
+};
+
+/*
+ * Sessions
+ */
+
+enum {
+        GRDEV_SESSION_CUSTOM                    = (1 << 0),
+        GRDEV_SESSION_MANAGED                   = (1 << 1),
+};
+
+int grdev_session_new(grdev_session **out,
+                      grdev_context *context,
+                      unsigned int flags,
+                      const char *name,
+                      grdev_event_fn event_fn,
+                      void *userdata);
+grdev_session *grdev_session_free(grdev_session *session);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdev_session*, grdev_session_free);
+
+bool grdev_session_is_enabled(grdev_session *session);
+void grdev_session_enable(grdev_session *session);
+void grdev_session_disable(grdev_session *session);
+
+void grdev_session_commit(grdev_session *session);
+void grdev_session_restore(grdev_session *session);
+
+/*
+ * Contexts
+ */
+
+int grdev_context_new(grdev_context **out, sd_event *event, sd_bus *sysbus);
+grdev_context *grdev_context_ref(grdev_context *context);
+grdev_context *grdev_context_unref(grdev_context *context);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(grdev_context*, grdev_context_unref);

commit 2ec3ff668ff03410e94cfef8e3ee9384a8222211
Author: David Herrmann <dh.herrmann at gmail.com>
Date:   Fri Sep 19 13:26:39 2014 +0200

    login: pause devices before acknowledging VT switches
    
    If a session controller does not need synchronous VT switches, we allow
    them to pass VT control to logind, which acknowledges all VT switches
    unconditionally. This works fine with all sessions using the dbus API,
    but causes out-of-sync device use if we switch to legacy sessions that
    are notified via VT signals. Those are processed before logind notices
    the session-switch via sysfs. Therefore, leaving the old session still
    active for a short amount of time.
    
    This, in fact, may cause the legacy session to prepare graphics devices
    before the old session was deactivated, and thus, maybe causing the old
    session to interfer with graphics device usage.
    
    Fix this by releasing devices immediately before acknowledging VT
    switches. This way, sessions without VT handlers are required to support
    async session switching (which they do in that case, anyway).

diff --git a/src/login/logind-session.c b/src/login/logind-session.c
index eeb58c9..477ac9a 100644
--- a/src/login/logind-session.c
+++ b/src/login/logind-session.c
@@ -1053,6 +1053,27 @@ void session_restore_vt(Session *s) {
         s->vtfd = safe_close(s->vtfd);
 }
 
+void session_leave_vt(Session *s) {
+        assert(s);
+
+        /* This is called whenever we get a VT-switch signal from the kernel.
+         * We acknowledge all of them unconditionally. Note that session are
+         * free to overwrite those handlers and we only register them for
+         * sessions with controllers. Legacy sessions are not affected.
+         * However, if we switch from a non-legacy to a legacy session, we must
+         * make sure to pause all device before acknowledging the switch. We
+         * process the real switch only after we are notified via sysfs, so the
+         * legacy session might have already started using the devices. If we
+         * don't pause the devices before the switch, we might confuse the
+         * session we switch to. */
+
+        if (s->vtfd < 0)
+                return;
+
+        session_device_pause_all(s);
+        ioctl(s->vtfd, VT_RELDISP, 1);
+}
+
 bool session_is_controller(Session *s, const char *sender) {
         assert(s);
 
diff --git a/src/login/logind-session.h b/src/login/logind-session.h
index 9fb0188..a007fb5 100644
--- a/src/login/logind-session.h
+++ b/src/login/logind-session.h
@@ -174,6 +174,7 @@ KillWho kill_who_from_string(const char *s) _pure_;
 
 int session_prepare_vt(Session *s);
 void session_restore_vt(Session *s);
+void session_leave_vt(Session *s);
 
 bool session_is_controller(Session *s, const char *sender);
 int session_set_controller(Session *s, const char *sender, bool force);
diff --git a/src/login/logind.c b/src/login/logind.c
index f1b6a86..8f00c46 100644
--- a/src/login/logind.c
+++ b/src/login/logind.c
@@ -750,11 +750,11 @@ static int manager_vt_switch(sd_event_source *src, const struct signalfd_siginfo
         }
 
         if (active->vtfd >= 0) {
-                ioctl(active->vtfd, VT_RELDISP, 1);
+                session_leave_vt(active);
         } else {
                 LIST_FOREACH(sessions_by_seat, iter, m->seat0->sessions) {
                         if (iter->vtnr == active->vtnr && iter->vtfd >= 0) {
-                                ioctl(iter->vtfd, VT_RELDISP, 1);
+                                session_leave_vt(iter);
                                 break;
                         }
                 }



More information about the systemd-commits mailing list