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