uterm: drm: activate context before swapping
[platform/upstream/kmscon.git] / src / uterm_drm_shared.c
index bef6ff8..ec486ab 100644 (file)
  */
 
 #include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 #include <xf86drm.h>
 #include <xf86drmMode.h>
-#include "log.h"
+#include "shl_log.h"
+#include "shl_timer.h"
 #include "uterm_drm_shared_internal.h"
 #include "uterm_video.h"
 #include "uterm_video_internal.h"
@@ -93,3 +97,715 @@ const struct mode_ops uterm_drm_mode_ops = {
        .get_width = uterm_drm_mode_get_width,
        .get_height = uterm_drm_mode_get_height,
 };
+
+int uterm_drm_set_dpms(int fd, uint32_t conn_id, int state)
+{
+       int i, ret, set;
+       drmModeConnector *conn;
+       drmModePropertyRes *prop;
+
+       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;
+       }
+
+       conn = drmModeGetConnector(fd, conn_id);
+       if (!conn) {
+               log_err("cannot get display connector");
+               return -EFAULT;
+       }
+
+       ret = state;
+       for (i = 0; i < conn->count_props; ++i) {
+               prop = drmModeGetProperty(fd, conn->props[i]);
+               if (!prop) {
+                       log_error("cannot get DRM property (%d): %m", errno);
+                       continue;
+               }
+
+               if (!strcmp(prop->name, "DPMS")) {
+                       ret = drmModeConnectorSetProperty(fd, 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) {
+               log_warn("display does not support DPMS");
+               ret = UTERM_DPMS_UNKNOWN;
+       }
+
+       drmModeFreeConnector(conn);
+       return ret;
+}
+
+int uterm_drm_get_dpms(int fd, drmModeConnector *conn)
+{
+       int i, ret;
+       drmModePropertyRes *prop;
+
+       for (i = 0; i < conn->count_props; ++i) {
+               prop = drmModeGetProperty(fd, conn->props[i]);
+               if (!prop) {
+                       log_error("cannot get DRM property (%d): %m", errno);
+                       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;
+}
+
+int uterm_drm_display_init(struct uterm_display *disp, void *data)
+{
+       struct uterm_drm_display *d;
+
+       d = malloc(sizeof(*d));
+       if (!d)
+               return -ENOMEM;
+       memset(d, 0, sizeof(*d));
+       disp->data = d;
+       d->data = data;
+
+       return 0;
+}
+
+void uterm_drm_display_destroy(struct uterm_display *disp)
+{
+       free(disp->data);
+}
+
+int uterm_drm_display_activate(struct uterm_display *disp, int fd)
+{
+       struct uterm_video *video = disp->video;
+       struct uterm_drm_display *ddrm = disp->data;
+       drmModeRes *res;
+       drmModeConnector *conn;
+       drmModeEncoder *enc;
+       int crtc, i;
+
+       res = drmModeGetResources(fd);
+       if (!res) {
+               log_err("cannot get resources for display %p", disp);
+               return -EFAULT;
+       }
+       conn = drmModeGetConnector(fd, ddrm->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(fd, conn->encoders[i]);
+               if (!enc)
+                       continue;
+               crtc = uterm_drm_video_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;
+       }
+
+       ddrm->crtc_id = crtc;
+       if (ddrm->saved_crtc)
+               drmModeFreeCrtc(ddrm->saved_crtc);
+       ddrm->saved_crtc = drmModeGetCrtc(fd, ddrm->crtc_id);
+
+       return 0;
+}
+
+void uterm_drm_display_deactivate(struct uterm_display *disp, int fd)
+{
+       struct uterm_drm_display *ddrm = disp->data;
+
+       uterm_drm_display_wait_pflip(disp);
+
+       if (ddrm->saved_crtc) {
+               if (disp->video->flags & VIDEO_AWAKE) {
+                       drmModeSetCrtc(fd, ddrm->saved_crtc->crtc_id,
+                                      ddrm->saved_crtc->buffer_id,
+                                      ddrm->saved_crtc->x,
+                                      ddrm->saved_crtc->y,
+                                      &ddrm->conn_id, 1,
+                                      &ddrm->saved_crtc->mode);
+               }
+               drmModeFreeCrtc(ddrm->saved_crtc);
+               ddrm->saved_crtc = NULL;
+       }
+
+       ddrm->crtc_id = 0;
+       disp->flags &= ~(DISPLAY_VSYNC | DISPLAY_ONLINE | DISPLAY_PFLIP);
+}
+
+int uterm_drm_display_set_dpms(struct uterm_display *disp, int state)
+{
+       int ret;
+       struct uterm_drm_display *ddrm = disp->data;
+       struct uterm_drm_video *vdrm = disp->video->data;
+
+       log_info("setting DPMS of display %p to %s", disp,
+                uterm_dpms_to_name(state));
+
+       ret = uterm_drm_set_dpms(vdrm->fd, ddrm->conn_id, state);
+       if (ret < 0)
+               return ret;
+
+       disp->dpms = ret;
+       return 0;
+}
+
+int uterm_drm_display_wait_pflip(struct uterm_display *disp)
+{
+       struct uterm_video *video = disp->video;
+       int ret;
+       unsigned int timeout = 1000; /* 1s */
+
+       if ((disp->flags & DISPLAY_PFLIP) || !(disp->flags & DISPLAY_VSYNC))
+               return 0;
+
+       do {
+               ret = uterm_drm_video_wait_pflip(video, &timeout);
+               if (ret < 1)
+                       break;
+               else if ((disp->flags & DISPLAY_PFLIP))
+                       break;
+       } while (timeout > 0);
+
+       if (ret < 0)
+               return ret;
+       if (ret == 0 || !timeout) {
+               log_warning("timeout waiting for page-flip on display %p",
+                           disp);
+               return -ETIMEDOUT;
+       }
+
+       return 0;
+}
+
+int uterm_drm_display_swap(struct uterm_display *disp, uint32_t fb,
+                          bool immediate)
+{
+       struct uterm_drm_display *ddrm = disp->data;
+       struct uterm_video *video = disp->video;
+       struct uterm_drm_video *vdrm = video->data;
+       int ret;
+       drmModeModeInfo *mode;
+
+       if (disp->dpms != UTERM_DPMS_ON)
+               return -EINVAL;
+
+       if (immediate) {
+               ret = uterm_drm_display_wait_pflip(disp);
+               if (ret)
+                       return ret;
+
+               mode = uterm_drm_mode_get_info(disp->current_mode);
+               ret = drmModeSetCrtc(vdrm->fd, ddrm->crtc_id, fb, 0, 0,
+                                    &ddrm->conn_id, 1, mode);
+               if (ret) {
+                       log_error("cannot set DRM-CRTC (%d): %m", errno);
+                       return -EFAULT;
+               }
+       } else {
+               if ((disp->flags & DISPLAY_VSYNC))
+                       return -EBUSY;
+
+               ret = drmModePageFlip(vdrm->fd, ddrm->crtc_id, fb,
+                                     DRM_MODE_PAGE_FLIP_EVENT, disp);
+               if (ret) {
+                       log_error("cannot page-flip on DRM-CRTC (%d): %m",
+                                 errno);
+                       return -EFAULT;
+               }
+
+               uterm_display_ref(disp);
+               disp->flags |= DISPLAY_VSYNC;
+       }
+
+       return 0;
+}
+
+static void uterm_drm_display_pflip(struct uterm_display *disp)
+{
+       struct uterm_drm_video *vdrm = disp->video->data;
+
+       disp->flags &= ~(DISPLAY_PFLIP | DISPLAY_VSYNC);
+       if (vdrm->page_flip)
+               vdrm->page_flip(disp);
+
+       DISPLAY_CB(disp, UTERM_PAGE_FLIP);
+}
+
+static void display_event(int fd, unsigned int frame, unsigned int sec,
+                         unsigned int usec, void *data)
+{
+       struct uterm_display *disp = data;
+
+       if (disp->video && (disp->flags & DISPLAY_VSYNC))
+               disp->flags |= DISPLAY_PFLIP;
+
+       uterm_display_unref(disp);
+}
+
+static int uterm_drm_video_read_events(struct uterm_video *video)
+{
+       struct uterm_drm_video *vdrm = video->data;
+       drmEventContext ev;
+       int ret;
+
+       /* TODO: DRM subsystem does not support non-blocking reads and it also
+        * doesn't return 0/-1 if the device is dead. This can lead to serious
+        * deadlocks in userspace if we read() after a device was unplugged. Fix
+        * this upstream and then make this code actually loop. */
+       memset(&ev, 0, sizeof(ev));
+       ev.version = DRM_EVENT_CONTEXT_VERSION;
+       ev.page_flip_handler = display_event;
+       errno = 0;
+       ret = drmHandleEvent(vdrm->fd, &ev);
+
+       if (ret < 0 && errno != EAGAIN)
+               return -EFAULT;
+
+       return 0;
+}
+
+static void do_pflips(struct ev_eloop *eloop, void *unused, void *data)
+{
+       struct uterm_video *video = data;
+       struct uterm_display *disp;
+       struct shl_dlist *iter;
+
+       shl_dlist_for_each(iter, &video->displays) {
+               disp = shl_dlist_entry(iter, struct uterm_display, list);
+               if ((disp->flags & DISPLAY_PFLIP))
+                       uterm_drm_display_pflip(disp);
+       }
+}
+
+static void io_event(struct ev_fd *fd, int mask, void *data)
+{
+       struct uterm_video *video = data;
+       struct uterm_drm_video *vdrm = video->data;
+       struct uterm_display *disp;
+       struct shl_dlist *iter;
+       int ret;
+
+       /* TODO: forward HUP to caller */
+       if (mask & (EV_HUP | EV_ERR)) {
+               log_err("error or hangup on DRM fd");
+               ev_eloop_rm_fd(vdrm->efd);
+               vdrm->efd = NULL;
+               return;
+       }
+
+       if (!(mask & EV_READABLE))
+               return;
+
+       ret = uterm_drm_video_read_events(video);
+       if (ret)
+               return;
+
+       shl_dlist_for_each(iter, &video->displays) {
+               disp = shl_dlist_entry(iter, struct uterm_display, list);
+               if ((disp->flags & DISPLAY_PFLIP))
+                       uterm_drm_display_pflip(disp);
+       }
+}
+
+static void vt_timeout(struct ev_timer *timer, uint64_t exp, void *data)
+{
+       struct uterm_video *video = data;
+       struct uterm_drm_video *vdrm = video->data;
+       struct uterm_display *disp;
+       struct shl_dlist *iter;
+       int r;
+
+       r = uterm_drm_video_wake_up(video);
+       if (!r) {
+               ev_timer_update(vdrm->vt_timer, NULL);
+               shl_dlist_for_each(iter, &video->displays) {
+                       disp = shl_dlist_entry(iter, struct uterm_display, list);
+                       VIDEO_CB(video, disp, UTERM_REFRESH);
+               }
+       }
+}
+
+void uterm_drm_video_arm_vt_timer(struct uterm_video *video)
+{
+       struct uterm_drm_video *vdrm = video->data;
+       struct itimerspec spec;
+
+       spec.it_value.tv_sec = 0;
+       spec.it_value.tv_nsec = 20L * 1000L * 1000L; /* 20ms */
+       spec.it_interval = spec.it_value;
+
+       ev_timer_update(vdrm->vt_timer, &spec);
+}
+
+int uterm_drm_video_init(struct uterm_video *video, const char *node,
+                        const struct display_ops *display_ops,
+                        uterm_drm_page_flip_t pflip, void *data)
+{
+       struct uterm_drm_video *vdrm;
+       int ret;
+
+       log_info("new drm device via %s", node);
+
+       vdrm = malloc(sizeof(*vdrm));
+       if (!vdrm)
+               return -ENOMEM;
+       memset(vdrm, 0, sizeof(*vdrm));
+       video->data = vdrm;
+       vdrm->data = data;
+       vdrm->page_flip = pflip;
+       vdrm->display_ops = display_ops;
+
+       vdrm->fd = open(node, O_RDWR | O_CLOEXEC | O_NONBLOCK);
+       if (vdrm->fd < 0) {
+               log_err("cannot open drm device %s (%d): %m", node, errno);
+               ret = -EFAULT;
+               goto err_free;
+       }
+       /* TODO: fix the race-condition with DRM-Master-on-open */
+       drmDropMaster(vdrm->fd);
+
+       ret = ev_eloop_new_fd(video->eloop, &vdrm->efd, vdrm->fd, EV_READABLE,
+                             io_event, video);
+       if (ret)
+               goto err_close;
+
+       ret = shl_timer_new(&vdrm->timer);
+       if (ret)
+               goto err_fd;
+
+       ret = ev_eloop_new_timer(video->eloop, &vdrm->vt_timer, NULL,
+                                vt_timeout, video);
+       if (ret)
+               goto err_timer;
+
+       video->flags |= VIDEO_HOTPLUG;
+       return 0;
+
+err_timer:
+       shl_timer_free(vdrm->timer);
+err_fd:
+       ev_eloop_rm_fd(vdrm->efd);
+err_close:
+       close(vdrm->fd);
+err_free:
+       free(vdrm);
+       return ret;
+}
+
+void uterm_drm_video_destroy(struct uterm_video *video)
+{
+       struct uterm_drm_video *vdrm = video->data;
+
+       ev_eloop_rm_timer(vdrm->vt_timer);
+       ev_eloop_unregister_idle_cb(video->eloop, do_pflips, video, EV_SINGLE);
+       shl_timer_free(vdrm->timer);
+       ev_eloop_rm_fd(vdrm->efd);
+       close(vdrm->fd);
+       free(video->data);
+}
+
+int uterm_drm_video_find_crtc(struct uterm_video *video, drmModeRes *res,
+                             drmModeEncoder *enc)
+{
+       int i, crtc;
+       struct uterm_display *iter;
+       struct uterm_drm_display *ddrm;
+       struct shl_dlist *it;
+
+       for (i = 0; i < res->count_crtcs; ++i) {
+               if (enc->possible_crtcs & (1 << i)) {
+                       crtc = res->crtcs[i];
+                       shl_dlist_for_each(it, &video->displays) {
+                               iter = shl_dlist_entry(it,
+                                                      struct uterm_display,
+                                                      list);
+                               ddrm = iter->data;
+                               if (ddrm->crtc_id == crtc)
+                                       break;
+                       }
+                       if (it == &video->displays)
+                               return crtc;
+               }
+       }
+
+       return -1;
+}
+
+static void bind_display(struct uterm_video *video, drmModeRes *res,
+                        drmModeConnector *conn)
+{
+       struct uterm_drm_video *vdrm = video->data;
+       struct uterm_display *disp;
+       struct uterm_drm_display *ddrm;
+       struct uterm_mode *mode;
+       int ret, i;
+
+       ret = display_new(&disp, vdrm->display_ops);
+       if (ret)
+               return;
+       ddrm = disp->data;
+
+       for (i = 0; i < conn->count_modes; ++i) {
+               ret = mode_new(&mode, &uterm_drm_mode_ops);
+               if (ret)
+                       continue;
+
+               uterm_drm_mode_set(mode, &conn->modes[i]);
+
+               ret = uterm_mode_bind(mode, disp);
+               if (ret) {
+                       uterm_mode_unref(mode);
+                       continue;
+               }
+
+               /* TODO: more sophisticated default-mode selection */
+               if (!disp->default_mode)
+                       disp->default_mode = mode;
+
+               uterm_mode_unref(mode);
+       }
+
+       if (shl_dlist_empty(&disp->modes)) {
+               log_warn("no valid mode for display found");
+               ret = -EFAULT;
+               goto err_unref;
+       }
+
+       ddrm->conn_id = conn->connector_id;
+       disp->flags |= DISPLAY_AVAILABLE;
+       disp->dpms = uterm_drm_get_dpms(vdrm->fd, conn);
+
+       log_info("display %p DPMS is %s", disp,
+                uterm_dpms_to_name(disp->dpms));
+
+       ret = uterm_display_bind(disp, video);
+       if (ret)
+               goto err_unref;
+
+       uterm_display_unref(disp);
+       return;
+
+err_unref:
+       uterm_display_unref(disp);
+       return;
+}
+
+int uterm_drm_video_hotplug(struct uterm_video *video,
+                           bool read_dpms, bool modeset)
+{
+       struct uterm_drm_video *vdrm = video->data;
+       drmModeRes *res;
+       drmModeConnector *conn;
+       struct uterm_display *disp;
+       struct uterm_drm_display *ddrm;
+       int i, dpms;
+       struct shl_dlist *iter, *tmp;
+
+       if (!video_is_awake(video) || !video_need_hotplug(video))
+               return 0;
+
+       log_debug("testing DRM hotplug status");
+
+       res = drmModeGetResources(vdrm->fd);
+       if (!res) {
+               log_err("cannot retrieve drm resources");
+               return -EACCES;
+       }
+
+       shl_dlist_for_each(iter, &video->displays) {
+               disp = shl_dlist_entry(iter, struct uterm_display, list);
+               disp->flags &= ~DISPLAY_AVAILABLE;
+       }
+
+       for (i = 0; i < res->count_connectors; ++i) {
+               conn = drmModeGetConnector(vdrm->fd, res->connectors[i]);
+               if (!conn)
+                       continue;
+               if (conn->connection != DRM_MODE_CONNECTED) {
+                       drmModeFreeConnector(conn);
+                       continue;
+               }
+
+               shl_dlist_for_each(iter, &video->displays) {
+                       disp = shl_dlist_entry(iter, struct uterm_display,
+                                              list);
+                       ddrm = disp->data;
+
+                       if (ddrm->conn_id != res->connectors[i])
+                               continue;
+
+                       disp->flags |= DISPLAY_AVAILABLE;
+                       if (!display_is_online(disp))
+                               break;
+
+                       if (read_dpms) {
+                               dpms = uterm_drm_get_dpms(vdrm->fd, conn);
+                               if (dpms != disp->dpms) {
+                                       log_debug("DPMS state for display %p changed",
+                                                 disp);
+                                       uterm_drm_display_set_dpms(disp, disp->dpms);
+                               }
+                       }
+
+                       if (modeset) {
+                               log_debug("re-activate display %p", disp);
+                               uterm_display_use(disp, NULL);
+                               uterm_display_swap(disp, true);
+                       }
+
+                       break;
+               }
+
+               if (iter == &video->displays)
+                       bind_display(video, res, conn);
+
+               drmModeFreeConnector(conn);
+       }
+
+       drmModeFreeResources(res);
+
+       shl_dlist_for_each_safe(iter, tmp, &video->displays) {
+               disp = shl_dlist_entry(iter, struct uterm_display, list);
+               if (!(disp->flags & DISPLAY_AVAILABLE))
+                       uterm_display_unbind(disp);
+       }
+
+       video->flags &= ~VIDEO_HOTPLUG;
+       return 0;
+}
+
+int uterm_drm_video_wake_up(struct uterm_video *video)
+{
+       int ret;
+       struct uterm_drm_video *vdrm = video->data;
+
+       ret = drmSetMaster(vdrm->fd);
+       if (ret) {
+               log_err("cannot set DRM-master");
+               return -EACCES;
+       }
+
+       video->flags |= VIDEO_AWAKE | VIDEO_HOTPLUG;
+       ret = uterm_drm_video_hotplug(video, true, true);
+       if (ret) {
+               drmDropMaster(vdrm->fd);
+               return ret;
+       }
+
+       return 0;
+}
+
+void uterm_drm_video_sleep(struct uterm_video *video)
+{
+       struct uterm_drm_video *vdrm = video->data;
+
+       drmDropMaster(vdrm->fd);
+       ev_timer_drain(vdrm->vt_timer, NULL);
+       ev_timer_update(vdrm->vt_timer, NULL);
+}
+
+int uterm_drm_video_poll(struct uterm_video *video)
+{
+       video->flags |= VIDEO_HOTPLUG;
+       return uterm_drm_video_hotplug(video, false, false);
+}
+
+/* Waits for events on DRM fd for \mtimeout milliseconds and returns 0 if the
+ * timeout expired, -ERR on errors and 1 if a page-flip event has been read.
+ * \mtimeout is adjusted to the remaining time. */
+int uterm_drm_video_wait_pflip(struct uterm_video *video,
+                              unsigned int *mtimeout)
+{
+       struct uterm_drm_video *vdrm = video->data;
+       struct pollfd pfd;
+       int ret;
+       uint64_t elapsed;
+
+       shl_timer_start(vdrm->timer);
+
+       memset(&pfd, 0, sizeof(pfd));
+       pfd.fd = vdrm->fd;
+       pfd.events = POLLIN;
+
+       log_debug("waiting for pageflip on %p", video);
+       ret = poll(&pfd, 1, *mtimeout);
+
+       elapsed = shl_timer_stop(vdrm->timer);
+       *mtimeout = *mtimeout - (elapsed / 1000 + 1);
+
+       if (ret < 0) {
+               log_error("poll() failed on DRM fd (%d): %m", errno);
+               return -EFAULT;
+       } else if (!ret) {
+               log_warning("timeout waiting for page-flip on %p", video);
+               return 0;
+       } else if ((pfd.revents & POLLIN)) {
+               ret = uterm_drm_video_read_events(video);
+               if (ret)
+                       return ret;
+
+               ret = ev_eloop_register_idle_cb(video->eloop, do_pflips,
+                                               video, EV_ONESHOT | EV_SINGLE);
+               if (ret)
+                       return ret;
+
+               return 1;
+       } else {
+               log_debug("poll() HUP/ERR on DRM fd (%d)", pfd.revents);
+               return -EFAULT;
+       }
+}