ryantimwilson / rpms / systemd

Forked from rpms/systemd a month ago
Clone
Blob Blame History Raw
From 650c5444273993f969b9cd7df9add6ab2df0414e Mon Sep 17 00:00:00 2001
From: David Herrmann <dh.herrmann@gmail.com>
Date: Fri, 19 Sep 2014 14:05:52 +0200
Subject: [PATCH] terminal: add graphics interface

The grdev layer provides graphics-device access via the
libsystemd-terminal library. It will be used by all terminal helpers to
actually access display hardware.

Like idev, the grdev layer is built around session objects. On each
session object you add/remove graphics devices as they appear and vanish.
Any device type can be supported via specific card-backends. The exported
grdev API hides any device details.

Graphics devices are represented by "cards". Those are hidden in the
session and any pipe-configuration is automatically applied. Out of those,
we configure displays which are then exported to the API user. Displays
are meant as lowest hardware entity available outside of grdev. The
underlying pipe configuration is fully hidden and not accessible from the
outside. The grdev tiling layer allows almost arbitrary setups out of
multiple pipes, but so far we only use a small subset of this. More will
follow.

A grdev-display is meant to represent real connected displays/monitors.
The upper level screen arrangements are user policy and not controlled by
grdev. Applications are free to apply any policy they want.

Real card-backends will follow in later patches.
---
 Makefile.am                              |    3 +
 configure.ac                             |    2 +-
 src/libsystemd-terminal/grdev-internal.h |  237 ++++++
 src/libsystemd-terminal/grdev.c          | 1219 ++++++++++++++++++++++++++++++
 src/libsystemd-terminal/grdev.h          |  182 +++++
 5 files changed, 1642 insertions(+), 1 deletion(-)
 create mode 100644 src/libsystemd-terminal/grdev-internal.h
 create mode 100644 src/libsystemd-terminal/grdev.c
 create mode 100644 src/libsystemd-terminal/grdev.h

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