diff options
-rw-r--r-- | Makefile.am | 3 | ||||
-rw-r--r-- | configure.ac | 2 | ||||
-rw-r--r-- | src/libsystemd-terminal/grdev-internal.h | 237 | ||||
-rw-r--r-- | src/libsystemd-terminal/grdev.c | 1219 | ||||
-rw-r--r-- | src/libsystemd-terminal/grdev.h | 182 |
5 files changed, 1642 insertions, 1 deletions
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); |