/* * uterm - Linux User-Space Terminal * * Copyright (c) 2011-2012 David Herrmann * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * DRM Video backend */ #define EGL_EGLEXT_PROTOTYPES #define GL_GLEXT_PROTOTYPES #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* #include TODO: missing header protection */ #include "eloop.h" #include "log.h" #include "uterm.h" #include "uterm_internal.h" #define LOG_SUBSYSTEM "video_drm" static const char *mode_get_name(const struct uterm_mode *mode) { return mode->drm.info.name; } static unsigned int mode_get_width(const struct uterm_mode *mode) { return mode->drm.info.hdisplay; } static unsigned int mode_get_height(const struct uterm_mode *mode) { return mode->drm.info.vdisplay; } static int init_rb(struct uterm_display *disp, struct drm_rb *rb) { unsigned int stride, handle; int ret; struct uterm_video *video = disp->video; rb->bo = gbm_bo_create(video->drm.gbm, disp->current_mode->drm.info.hdisplay, disp->current_mode->drm.info.vdisplay, GBM_BO_FORMAT_XRGB8888, GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING); if (!rb->bo) { log_err("cannot create gbm-bo"); return -EFAULT; } stride = gbm_bo_get_pitch(rb->bo); handle = gbm_bo_get_handle(rb->bo).u32; /* TODO: how can we choose 24/32 dynamically? */ ret = drmModeAddFB(video->drm.fd, disp->current_mode->drm.info.hdisplay, disp->current_mode->drm.info.vdisplay, 24, 32, stride, handle, &rb->fb); if (ret) { log_err("cannot add drm-fb"); ret = -EFAULT; goto err_gbm; } rb->image = eglCreateImageKHR(video->drm.disp, NULL, EGL_NATIVE_PIXMAP_KHR, rb->bo, NULL); if (!rb->image) { log_err("cannot create egl image"); ret = -EFAULT; goto err_fb; } glGenRenderbuffers(1, &rb->rb); glBindRenderbuffer(GL_RENDERBUFFER, rb->rb); glEGLImageTargetRenderbufferStorageOES(GL_RENDERBUFFER, rb->image); return 0; err_fb: drmModeRmFB(video->drm.fd, rb->fb); err_gbm: gbm_bo_destroy(rb->bo); return ret; } static void destroy_rb(struct uterm_display *disp, struct drm_rb *rb) { glBindRenderbuffer(GL_RENDERBUFFER, 0); glDeleteRenderbuffers(1, &rb->rb); eglDestroyImageKHR(disp->video->drm.disp, rb->image); drmModeRmFB(disp->video->drm.fd, rb->fb); gbm_bo_destroy(rb->bo); } static int find_crtc(struct uterm_video *video, drmModeRes *res, drmModeEncoder *enc) { int i, crtc; struct uterm_display *iter; for (i = 0; i < res->count_crtcs; ++i) { if (enc->possible_crtcs & (1 << i)) { crtc = res->crtcs[i]; for (iter = video->displays; iter; iter = iter->next) { if (iter->drm.crtc_id == crtc) break; } if (!iter) return crtc; } } return -1; } static int display_activate(struct uterm_display *disp, struct uterm_mode *mode) { struct uterm_video *video = disp->video; int ret, crtc, i; drmModeRes *res; drmModeConnector *conn; drmModeEncoder *enc; if (!video || !video_is_awake(video) || !mode) return -EINVAL; if (display_is_online(disp)) return 0; log_info("activating display %p to %ux%u", disp, mode->drm.info.hdisplay, mode->drm.info.vdisplay); res = drmModeGetResources(video->drm.fd); if (!res) { log_err("cannot get resources for display %p", disp); return -EFAULT; } conn = drmModeGetConnector(video->drm.fd, disp->drm.conn_id); if (!conn) { log_err("cannot get connector for display %p", disp); drmModeFreeResources(res); return -EFAULT; } crtc = -1; for (i = 0; i < conn->count_encoders; ++i) { enc = drmModeGetEncoder(video->drm.fd, conn->encoders[i]); if (!enc) continue; crtc = find_crtc(video, res, enc); drmModeFreeEncoder(enc); if (crtc >= 0) break; } drmModeFreeConnector(conn); drmModeFreeResources(res); if (crtc < 0) { log_warn("cannot find crtc for new display"); return -ENODEV; } disp->drm.crtc_id = crtc; disp->drm.current_rb = 0; disp->current_mode = mode; disp->drm.saved_crtc = drmModeGetCrtc(video->drm.fd, disp->drm.crtc_id); ret = init_rb(disp, &disp->drm.rb[0]); if (ret) goto err_saved; ret = init_rb(disp, &disp->drm.rb[1]); if (ret) goto err_rb; glGenFramebuffers(1, &disp->drm.fb); glBindFramebuffer(GL_FRAMEBUFFER, disp->drm.fb); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, disp->drm.rb[1].rb); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { log_err("cannot create gl-framebuffer"); ret = -EFAULT; goto err_fb; } ret = drmModeSetCrtc(video->drm.fd, disp->drm.crtc_id, disp->drm.rb[0].fb, 0, 0, &disp->drm.conn_id, 1, &disp->current_mode->drm.info); if (ret) { log_err("cannot set drm-crtc"); ret = -EFAULT; goto err_fb; } disp->flags |= DISPLAY_ONLINE; return 0; err_fb: glBindFramebuffer(GL_FRAMEBUFFER, 0); glDeleteFramebuffers(1, &disp->drm.fb); destroy_rb(disp, &disp->drm.rb[1]); err_rb: destroy_rb(disp, &disp->drm.rb[0]); err_saved: disp->current_mode = NULL; if (disp->drm.saved_crtc) { drmModeFreeCrtc(disp->drm.saved_crtc); disp->drm.saved_crtc = NULL; } return ret; } static void display_deactivate(struct uterm_display *disp) { if (!display_is_online(disp)) return; if (disp->drm.saved_crtc) { if (disp->video->flags & VIDEO_AWAKE) { drmModeSetCrtc(disp->video->drm.fd, disp->drm.saved_crtc->crtc_id, disp->drm.saved_crtc->buffer_id, disp->drm.saved_crtc->x, disp->drm.saved_crtc->y, &disp->drm.conn_id, 1, &disp->drm.saved_crtc->mode); } drmModeFreeCrtc(disp->drm.saved_crtc); disp->drm.saved_crtc = NULL; } glBindFramebuffer(GL_FRAMEBUFFER, 0); glDeleteFramebuffers(1, &disp->drm.fb); destroy_rb(disp, &disp->drm.rb[1]); destroy_rb(disp, &disp->drm.rb[0]); disp->current_mode = NULL; disp->flags &= ~(DISPLAY_ONLINE | DISPLAY_VSYNC); log_info("deactivating display %p", disp); } static int display_set_dpms(struct uterm_display *disp, int state) { int i, ret, set; drmModeConnector *conn; drmModePropertyRes *prop; if (!display_is_conn(disp) || !video_is_awake(disp->video)) return -EINVAL; switch (state) { case UTERM_DPMS_ON: set = DRM_MODE_DPMS_ON; break; case UTERM_DPMS_STANDBY: set = DRM_MODE_DPMS_STANDBY; break; case UTERM_DPMS_SUSPEND: set = DRM_MODE_DPMS_SUSPEND; break; case UTERM_DPMS_OFF: set = DRM_MODE_DPMS_OFF; break; default: return -EINVAL; } log_info("setting DPMS of display %p to %s", disp, uterm_dpms_to_name(state)); conn = drmModeGetConnector(disp->video->drm.fd, disp->drm.conn_id); if (!conn) { log_err("cannot get display connector"); return -EFAULT; } for (i = 0; i < conn->count_props; ++i) { prop = drmModeGetProperty(disp->video->drm.fd, conn->props[i]); if (!prop) continue; if (!strcmp(prop->name, "DPMS")) { ret = drmModeConnectorSetProperty(disp->video->drm.fd, disp->drm.conn_id, prop->prop_id, set); if (ret) { log_info("cannot set DPMS"); ret = -EFAULT; } drmModeFreeProperty(prop); break; } drmModeFreeProperty(prop); } if (i == conn->count_props) { ret = 0; log_warn("display does not support DPMS"); state = UTERM_DPMS_UNKNOWN; } drmModeFreeConnector(conn); disp->dpms = state; return ret; } static int display_use(struct uterm_display *disp) { if (!display_is_online(disp)) return -EINVAL; /* TODO: we need triple buffering as a VSYNC may still be pending */ glBindFramebuffer(GL_FRAMEBUFFER, disp->drm.fb); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, disp->drm.rb[disp->drm.current_rb ^ 1].rb); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { log_warn("cannot set gl-renderbuffer"); return -EFAULT; } return 0; } static int display_swap(struct uterm_display *disp) { int ret; if (!display_is_conn(disp) || !video_is_awake(disp->video)) return -EINVAL; if (disp->dpms != UTERM_DPMS_ON) return -EINVAL; /* TODO: is glFlush sufficient here? */ glFinish(); errno = 0; disp->drm.current_rb ^= 1; ret = drmModePageFlip(disp->video->drm.fd, disp->drm.crtc_id, disp->drm.rb[disp->drm.current_rb].fb, DRM_MODE_PAGE_FLIP_EVENT, disp); if (ret) { log_warn("page-flip failed %d %d", ret, errno); return -EFAULT; } uterm_display_ref(disp); disp->flags |= DISPLAY_VSYNC; return 0; } static int get_dpms(struct uterm_display *disp, drmModeConnector *conn) { int i, ret; drmModePropertyRes *prop; for (i = 0; i < conn->count_props; ++i) { prop = drmModeGetProperty(disp->video->drm.fd, conn->props[i]); if (!prop) continue; if (!strcmp(prop->name, "DPMS")) { switch (conn->prop_values[i]) { case DRM_MODE_DPMS_ON: ret = UTERM_DPMS_ON; break; case DRM_MODE_DPMS_STANDBY: ret = UTERM_DPMS_STANDBY; break; case DRM_MODE_DPMS_SUSPEND: ret = UTERM_DPMS_SUSPEND; break; case DRM_MODE_DPMS_OFF: default: ret = UTERM_DPMS_OFF; } drmModeFreeProperty(prop); return ret; } drmModeFreeProperty(prop); } if (i == conn->count_props) log_warn("display does not support DPMS"); return UTERM_DPMS_UNKNOWN; } static void bind_display(struct uterm_video *video, drmModeRes *res, drmModeConnector *conn) { struct uterm_display *disp; struct uterm_mode *mode; int ret, i; ret = display_new(&disp, &drm_display_ops); if (ret) return; for (i = 0; i < conn->count_modes; ++i) { ret = mode_new(&mode, &drm_mode_ops); if (ret) continue; mode->drm.info = conn->modes[i]; mode->next = disp->modes; disp->modes = mode; /* TODO: more sophisticated default-mode selection */ if (!disp->default_mode) disp->default_mode = mode; } if (!disp->modes) { log_warn("no valid mode for display found"); uterm_display_unref(disp); return; } disp->video = video; disp->drm.conn_id = conn->connector_id; disp->flags |= DISPLAY_AVAILABLE; disp->next = video->displays; video->displays = disp; disp->dpms = get_dpms(disp, conn); log_info("display %p DPMS is %s", disp, uterm_dpms_to_name(disp->dpms)); } static void unbind_display(struct uterm_display *disp) { if (!display_is_conn(disp)) return; display_deactivate(disp); disp->video = NULL; disp->flags &= ~DISPLAY_AVAILABLE; uterm_display_unref(disp); } static int get_id(struct udev_device *dev) { const char *name; char *end; int devnum; name = udev_device_get_sysname(dev); if (!name) return -ENODEV; if (strncmp(name, "card", 4) || !name[4]) return -ENODEV; devnum = strtol(&name[4], &end, 10); if (devnum < 0 || *end) return -ENODEV; return devnum; } static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data) { struct uterm_display *disp = data; disp->flags &= ~DISPLAY_VSYNC; uterm_display_unref(disp); } static void event(struct ev_fd *fd, int mask, void *data) { struct uterm_video *video = data; drmEventContext ev; if (mask & (EV_HUP | EV_ERR)) { log_err("error or hangup on DRM fd"); ev_eloop_rm_fd(video->drm.efd); video->drm.efd = NULL; return; } if (mask & EV_READABLE) { memset(&ev, 0, sizeof(ev)); ev.version = DRM_EVENT_CONTEXT_VERSION; ev.page_flip_handler = page_flip_handler; drmHandleEvent(video->drm.fd, &ev); } } static int init_device(struct uterm_video *video, struct udev_device *dev) { const char *node, *ext; int ret; EGLint major, minor; EGLenum api; static const EGLint ctx_att[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; struct drm_video *drm = &video->drm; log_info("probing %s", udev_device_get_sysname(dev)); node = udev_device_get_devnode(dev); if (!node) { log_err("cannot get device node"); return -ENODEV; } drm->id = get_id(dev); if (drm->id < 0) { log_err("cannot get device id"); return -ENODEV; } drm->fd = open(node, O_RDWR | O_CLOEXEC); if (drm->fd < 0) { log_err("cannot open drm device %s (%d): %m", node, errno); return -EFAULT; } drmDropMaster(drm->fd); drm->gbm = gbm_create_device(drm->fd); if (!drm->gbm) { log_err("cannot create drm device for %s", node); ret = -EFAULT; goto err_close; } drm->disp = eglGetDisplay((EGLNativeDisplayType) drm->gbm); if (!drm->disp) { log_err("cannot retrieve egl display for %s", node); ret = -EFAULT; goto err_gbm; } ret = eglInitialize(drm->disp, &major, &minor); if (!ret) { log_err("cannot init egl display for %s", node); ret = -EFAULT; goto err_gbm; } ext = eglQueryString(drm->disp, EGL_EXTENSIONS); if (!ext || !strstr(ext, "EGL_KHR_surfaceless_opengl")) { log_err("surfaceless opengl not supported"); ret = -EFAULT; goto err_disp; } api = EGL_OPENGL_ES_API; /* TODO: allow api = EGL_OPENGL_API */ if (!eglBindAPI(api)) { log_err("cannot bind opengl-es api"); ret = -EFAULT; goto err_disp; } drm->ctx = eglCreateContext(drm->disp, NULL, EGL_NO_CONTEXT, ctx_att); if (!drm->ctx) { log_err("cannot create egl context"); ret = -EFAULT; goto err_disp; } /* * We allow only one global video object. See uterm_video_new for the * reasons. If we every change this we need proper context-management * and should remove this call here. */ if (!eglMakeCurrent(drm->disp, EGL_NO_SURFACE, EGL_NO_SURFACE, drm->ctx)) { log_err("cannot activate egl context"); ret = -EFAULT; goto err_ctx; } ret = ev_eloop_new_fd(video->eloop, &drm->efd, drm->fd, EV_READABLE, event, video); if (ret) goto err_ctx; video->flags |= VIDEO_HOTPLUG; log_info("new drm device via %s", node); return 0; err_ctx: eglDestroyContext(drm->disp, drm->ctx); err_disp: eglTerminate(drm->disp); err_gbm: gbm_device_destroy(drm->gbm); err_close: close(drm->fd); return ret; } static int video_init(struct uterm_video *video) { struct udev_enumerate *e; struct udev_list_entry *name; const char *path; struct udev_device *dev; int ret; ret = udev_monitor_filter_add_match_subsystem_devtype(video->umon, "drm", "drm_minor"); if (ret) { log_err("cannot add udev filter (%d): %m", ret); return -EFAULT; } ret = udev_monitor_enable_receiving(video->umon); if (ret) { log_err("cannot start udev_monitor (%d): %m", ret); return -EFAULT; } e = udev_enumerate_new(video->udev); if (!e) { log_err("cannot create udev_enumerate object"); return -EFAULT; } ret = udev_enumerate_add_match_subsystem(e, "drm"); if (ret) { log_err("cannot add udev match (%d): %m", ret); ret = -EFAULT; goto err_enum; } ret = udev_enumerate_add_match_sysname(e, "card[0-9]*"); if (ret) { log_err("cannot add udev match (%d): %m", ret); ret = -EFAULT; goto err_enum; } ret = udev_enumerate_scan_devices(e); if (ret) { log_err("cannot scan udev devices (%d): %m", ret); ret = -EFAULT; goto err_enum; } udev_list_entry_foreach(name, udev_enumerate_get_list_entry(e)) { path = udev_list_entry_get_name(name); if (!path || !*path) continue; dev = udev_device_new_from_syspath(video->udev, path); if (!dev) continue; /* TODO: more sophisticated device selection */ ret = init_device(video, dev); udev_device_unref(dev); if (!ret) goto done; } ret = -ENODEV; log_err("no drm device found"); goto err_enum; done: ret = 0; err_enum: udev_enumerate_unref(e); return ret; } static void video_destroy(struct uterm_video *video) { struct drm_video *drm = &video->drm; struct uterm_display *disp; while ((disp = video->displays)) { video->displays = disp->next; disp->next = NULL; unbind_display(disp); } log_info("free drm device"); ev_eloop_rm_fd(drm->efd); eglDestroyContext(drm->disp, drm->ctx); eglTerminate(drm->disp); gbm_device_destroy(drm->gbm); drmDropMaster(drm->fd); close(drm->fd); } static int hotplug(struct uterm_video *video) { drmModeRes *res; drmModeConnector *conn; struct uterm_display *disp, *tmp; unsigned int i; if (!video_is_awake(video) || !video_need_hotplug(video)) return 0; res = drmModeGetResources(video->drm.fd); if (!res) { log_err("cannot retrieve drm resources"); return -EACCES; } for (disp = video->displays; disp; disp = disp->next) disp->flags &= ~DISPLAY_AVAILABLE; for (i = 0; i < res->count_connectors; ++i) { conn = drmModeGetConnector(video->drm.fd, res->connectors[i]); if (!conn) continue; if (conn->connection == DRM_MODE_CONNECTED) { for (disp = video->displays; disp; disp = disp->next) { if (disp->drm.conn_id == res->connectors[i]) { disp->flags |= DISPLAY_AVAILABLE; break; } } if (!disp) bind_display(video, res, conn); } drmModeFreeConnector(conn); } drmModeFreeResources(res); while (video->displays) { tmp = video->displays; if (tmp->flags & DISPLAY_AVAILABLE) break; video->displays = tmp->next; tmp->next = NULL; unbind_display(tmp); } for (disp = video->displays; disp && disp->next; ) { tmp = disp->next; if (tmp->flags & DISPLAY_AVAILABLE) { disp = tmp; } else { disp->next = tmp->next; tmp->next = NULL; unbind_display(tmp); } } video->flags &= ~VIDEO_HOTPLUG; return 0; } static int monitor(struct uterm_video *video) { struct udev_device *dev; const char *action, *val; unsigned int id; dev = udev_monitor_receive_device(video->umon); if (!dev) { log_warn("cannot receive device from udev_monitor"); return 0; } action = udev_device_get_action(dev); if (!action) goto ignore; if (strcmp(action, "change")) goto ignore; val = udev_device_get_property_value(dev, "HOTPLUG"); if (strcmp(val, "1")) goto ignore; id = get_id(dev); if (id != video->drm.id) goto ignore; video->flags |= VIDEO_HOTPLUG; ignore: udev_device_unref(dev); return 0; } static int video_poll(struct uterm_video *video, int mask) { int ret; if (mask & (EV_HUP | EV_ERR)) { log_err("udev_monitor closed unexpectedly"); return -EFAULT; } if (mask & EV_READABLE) { ret = monitor(video); if (ret) return ret; } return hotplug(video); } static void video_sleep(struct uterm_video *video) { if (!video_is_awake(video)) return; drmDropMaster(video->drm.fd); video->flags &= ~VIDEO_AWAKE; } static int video_wake_up(struct uterm_video *video) { int ret; if (video_is_awake(video)) return 0; ret = drmSetMaster(video->drm.fd); if (ret) { log_err("cannot set DRM-master"); return -EACCES; } video->flags |= VIDEO_AWAKE; ret = hotplug(video); if (ret) { video->flags &= ~VIDEO_AWAKE; drmDropMaster(video->drm.fd); return ret; } return 0; } const struct mode_ops drm_mode_ops = { .init = NULL, .destroy = NULL, .get_name = mode_get_name, .get_width = mode_get_width, .get_height = mode_get_height, }; const struct display_ops drm_display_ops = { .init = NULL, .destroy = NULL, .activate = display_activate, .deactivate = display_deactivate, .set_dpms = display_set_dpms, .use = display_use, .swap = display_swap, }; const struct video_ops drm_video_ops = { .init = video_init, .destroy = video_destroy, .segfault = NULL, /* TODO: reset all saved CRTCs on segfault */ .poll = video_poll, .sleep = video_sleep, .wake_up = video_wake_up, };