dcavalca / rpms / systemd

Forked from rpms/systemd 3 months ago
Clone
Blob Blame History Raw
From f22e0bce3732c1fd005b7a886042394e036bc1b3 Mon Sep 17 00:00:00 2001
From: David Herrmann <dh.herrmann@gmail.com>
Date: Fri, 19 Sep 2014 14:13:06 +0200
Subject: [PATCH] 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.
---
 Makefile.am                              |    1 +
 src/libsystemd-terminal/grdev-drm.c      | 2957 ++++++++++++++++++++++++++++++
 src/libsystemd-terminal/grdev-internal.h |    9 +
 src/libsystemd-terminal/grdev.c          |   64 +
 src/libsystemd-terminal/grdev.h          |    5 +
 5 files changed, 3036 insertions(+)
 create mode 100644 src/libsystemd-terminal/grdev-drm.c

diff --git a/Makefile.am b/Makefile.am
index 1931c5d96b..be25023c75 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 0000000000..3481584fbf
--- /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@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 7e69c49b63..0064f0be02 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 ab1c407ecb..1e02a6799c 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 2645b12113..9924a257b6 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
  */