summaryrefslogtreecommitdiff
path: root/src/til_fb.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/til_fb.c')
-rw-r--r--src/til_fb.c442
1 files changed, 442 insertions, 0 deletions
diff --git a/src/til_fb.c b/src/til_fb.c
new file mode 100644
index 0000000..3b604e4
--- /dev/null
+++ b/src/til_fb.c
@@ -0,0 +1,442 @@
+#include <assert.h>
+#include <pthread.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "til_fb.h"
+#include "til_settings.h"
+#include "til_util.h"
+
+/* Copyright (C) 2016-2017 Vito Caputo <vcaputo@pengaru.com> */
+
+
+/* I've used a separate thread for page-flipping duties because the libdrm api
+ * (and related kernel ioctl) for page flips doesn't appear to support queueing
+ * multiple flip requests. In this use case we aren't interactive and wish to
+ * just accumulate rendered pages until we run out of spare pages, allowing the
+ * renderer to get as far ahead of vsync as possible, and certainly never
+ * blocked waiting for vsync unless there's no spare page available for drawing
+ * into.
+ *
+ * In lieu of a queueing mechanism on the drm fd, we must submit the next page
+ * once the currently submitted page is flipped to - it's at that moment we
+ * won't get EBUSY from the ioctl any longer. Without a dedicated thread
+ * submitting flip requests and synchronously consuming their flip events,
+ * we're liable to introduce latency in the page flip submission if implemented
+ * in a more opportunistic manner whenever the fb api is entered from the
+ * render loop.
+ *
+ * If the kernel simply let us queue multiple flip requests we could maintain
+ * our submission queue entirely in the drm fd, and get available pages from
+ * the drm event handler once our pool of pages is depleted. The kernel on
+ * vsync could check the fd to see if another flip is queued and there would be
+ * the least latency possible in submitting the flips - the least likely to
+ * miss a vsync. This would also elide the need for synchronization in
+ * userspace between the renderer and the flipper thread, since there would no
+ * longer be a flipper thread.
+ *
+ * Let me know if you're aware of a better way with existing mainline drm!
+ *
+ *
+ * XXX: til_fb_new() used to create a thread which did the equivalent of til_fb_flip()
+ * continuously in a loop. This posed a problem for the sdl_fb backend, due to
+ * the need for event pumping in the page flip hook. SDL internally uses TLS
+ * and requires that the same thread which initialized SDL call the event
+ * functions. To satisfy this requirement, the body of the flipper thread loop
+ * has been moved to the til_fb_flip() function. Rototiller's main thread is
+ * expected to call this repeatedly, turning it effectively into the flipper
+ * thread. This required rototiller to move what was previously the main
+ * thread's duties - page rendering dispatch, to a separate thread.
+ */
+
+
+/* Most of til_fb_page_t is kept private, the public part is
+ * just an til_fb_fragment_t describing the whole page.
+ */
+typedef struct _til_fb_page_t _til_fb_page_t;
+struct _til_fb_page_t {
+ void *ops_page;
+
+ _til_fb_page_t *next, *previous;
+ til_fb_page_t public_page;
+};
+
+typedef struct til_fb_t {
+ const til_fb_ops_t *ops;
+ void *ops_context;
+ int n_pages;
+
+ pthread_mutex_t rebuild_mutex;
+ int rebuild_pages; /* counter of pages needing a rebuild */
+
+ _til_fb_page_t *active_page; /* page currently displayed */
+
+ pthread_mutex_t ready_mutex;
+ pthread_cond_t ready_cond;
+ _til_fb_page_t *ready_pages_head; /* next pages to flip to */
+ _til_fb_page_t *ready_pages_tail;
+
+ pthread_mutex_t inactive_mutex;
+ pthread_cond_t inactive_cond;
+ _til_fb_page_t *inactive_pages_head; /* finished pages available for (re)use */
+ _til_fb_page_t *inactive_pages_tail;
+
+ unsigned put_pages_count;
+} til_fb_t;
+
+#ifndef container_of
+#define container_of(_ptr, _type, _member) \
+ (_type *)((void *)(_ptr) - offsetof(_type, _member))
+#endif
+
+
+/* Consumes ready pages queued via til_fb_page_put(), submits them to drm to flip
+ * on vsync. Produces inactive pages from those replaced, making them
+ * available to til_fb_page_get(). */
+int til_fb_flip(til_fb_t *fb)
+{
+ _til_fb_page_t *next_active_page;
+ int r;
+
+ /* wait for a flip req, submit the req page for flip on vsync, wait for it to flip before making the
+ * active page inactive/available, repeat.
+ */
+ pthread_mutex_lock(&fb->ready_mutex);
+ while (!fb->ready_pages_head)
+ pthread_cond_wait(&fb->ready_cond, &fb->ready_mutex);
+
+ next_active_page = fb->ready_pages_head;
+ fb->ready_pages_head = next_active_page->next;
+ if (!fb->ready_pages_head)
+ fb->ready_pages_tail = NULL;
+ pthread_mutex_unlock(&fb->ready_mutex);
+
+ /* submit the next active page for page flip on vsync, and wait for it. */
+ r = fb->ops->page_flip(fb, fb->ops_context, next_active_page->ops_page);
+ if (r < 0) /* TODO: vet this: what happens to this page? */
+ return r;
+
+ /* now that we're displaying a new page, make the previously active one inactive so rendering can reuse it */
+ pthread_mutex_lock(&fb->inactive_mutex);
+ fb->active_page->next = fb->inactive_pages_head;
+ fb->inactive_pages_head = fb->active_page;
+ fb->inactive_pages_head->previous = NULL;
+ if (fb->inactive_pages_head->next)
+ fb->inactive_pages_head->next->previous = fb->inactive_pages_head;
+ else
+ fb->inactive_pages_tail = fb->inactive_pages_head;
+
+ /* before setting the renderer loose, check if there's more page rebuilding needed,
+ * and if there is do as much as possible here in the inactive set. Note it's important
+ * that the renderer take pages from the tail, and we always replenish inactive at the
+ * head, as well as rebuild pages from the head.
+ */
+ pthread_mutex_lock(&fb->rebuild_mutex);
+ for (_til_fb_page_t *p = fb->inactive_pages_head; p && fb->rebuild_pages > 0; p = p->next) {
+ fb->ops->page_free(fb, fb->ops_context, p->ops_page);
+ p->ops_page = fb->ops->page_alloc(fb, fb->ops_context, &p->public_page);
+ fb->rebuild_pages--;
+ }
+ pthread_mutex_unlock(&fb->rebuild_mutex);
+
+ pthread_cond_signal(&fb->inactive_cond);
+ pthread_mutex_unlock(&fb->inactive_mutex);
+
+ fb->active_page = next_active_page;
+
+ return 0;
+}
+
+
+/* acquire the fb, making page the visible page */
+static int til_fb_acquire(til_fb_t *fb, _til_fb_page_t *page)
+{
+ int ret;
+
+ ret = fb->ops->acquire(fb, fb->ops_context, page->ops_page);
+ if (ret < 0)
+ return ret;
+
+ fb->active_page = page;
+
+ return 0;
+}
+
+
+/* release the fb, making the visible page inactive */
+static void til_fb_release(til_fb_t *fb)
+{
+ fb->ops->release(fb, fb->ops_context);
+
+ /* XXX: this is getting silly, either add a doubly linked list header or
+ * at least use some functions for this local to this file.
+ */
+ fb->active_page->next = fb->inactive_pages_head;
+ fb->inactive_pages_head = fb->active_page;
+ fb->inactive_pages_head->previous = NULL;
+ if (fb->inactive_pages_head->next)
+ fb->inactive_pages_head->next->previous = fb->inactive_pages_head;
+ else
+ fb->inactive_pages_tail = fb->inactive_pages_head;
+
+ fb->active_page = NULL;
+}
+
+
+/* creates a framebuffer page */
+static void til_fb_page_new(til_fb_t *fb)
+{
+ _til_fb_page_t *page;
+
+ page = calloc(1, sizeof(_til_fb_page_t));
+ assert(page);
+
+ page->ops_page = fb->ops->page_alloc(fb, fb->ops_context, &page->public_page);
+
+ pthread_mutex_lock(&fb->inactive_mutex);
+ page->next = fb->inactive_pages_head;
+ fb->inactive_pages_head = page;
+ if (fb->inactive_pages_head->next)
+ fb->inactive_pages_head->next->previous = fb->inactive_pages_head;
+ else
+ fb->inactive_pages_tail = fb->inactive_pages_head;
+ pthread_mutex_unlock(&fb->inactive_mutex);
+
+}
+
+
+static void _til_fb_page_free(til_fb_t *fb, _til_fb_page_t *page)
+{
+ fb->ops->page_free(fb, fb->ops_context, page->ops_page);
+
+ free(page);
+}
+
+
+/* get the next inactive page from the fb, waiting if necessary. */
+static inline _til_fb_page_t * _til_fb_page_get(til_fb_t *fb)
+{
+ _til_fb_page_t *page;
+
+ /* As long as n_pages is >= 3 this won't block unless we're submitting
+ * pages faster than vhz.
+ */
+ pthread_mutex_lock(&fb->inactive_mutex);
+ while (!(page = fb->inactive_pages_tail))
+ pthread_cond_wait(&fb->inactive_cond, &fb->inactive_mutex);
+ fb->inactive_pages_tail = page->previous;
+ if (fb->inactive_pages_tail)
+ fb->inactive_pages_tail->next = NULL;
+ else
+ fb->inactive_pages_head = NULL;
+ pthread_mutex_unlock(&fb->inactive_mutex);
+
+ page->next = page->previous = NULL;
+ page->public_page.fragment.zeroed = 0;
+
+ return page;
+}
+
+
+/* public interface */
+til_fb_page_t * til_fb_page_get(til_fb_t *fb)
+{
+ return &(_til_fb_page_get(fb)->public_page);
+}
+
+
+/* put a page into the fb, queueing for display */
+static inline void _til_fb_page_put(til_fb_t *fb, _til_fb_page_t *page)
+{
+ pthread_mutex_lock(&fb->ready_mutex);
+ if (fb->ready_pages_tail)
+ fb->ready_pages_tail->next = page;
+ else
+ fb->ready_pages_head = page;
+
+ fb->ready_pages_tail = page;
+ pthread_cond_signal(&fb->ready_cond);
+ pthread_mutex_unlock(&fb->ready_mutex);
+}
+
+
+/* public interface */
+
+/* put a page into the fb, queueing for display */
+void til_fb_page_put(til_fb_t *fb, til_fb_page_t *page)
+{
+ fb->put_pages_count++;
+
+ _til_fb_page_put(fb, container_of(page, _til_fb_page_t, public_page));
+}
+
+
+/* get (and reset) the current count of put pages */
+void til_fb_get_put_pages_count(til_fb_t *fb, unsigned *count)
+{
+ *count = fb->put_pages_count;
+ fb->put_pages_count = 0;
+}
+
+
+/* free the fb and associated resources */
+til_fb_t * til_fb_free(til_fb_t *fb)
+{
+ if (fb) {
+ if (fb->active_page)
+ til_fb_release(fb);
+
+ /* TODO: free all the pages */
+
+ if (fb->ops->shutdown && fb->ops_context)
+ fb->ops->shutdown(fb, fb->ops_context);
+
+ pthread_mutex_destroy(&fb->ready_mutex);
+ pthread_cond_destroy(&fb->ready_cond);
+ pthread_mutex_destroy(&fb->inactive_mutex);
+ pthread_cond_destroy(&fb->inactive_cond);
+
+ free(fb);
+ }
+
+ return NULL;
+}
+
+
+/* create a new fb instance */
+int til_fb_new(const til_fb_ops_t *ops, til_settings_t *settings, int n_pages, til_fb_t **res_fb)
+{
+ _til_fb_page_t *page;
+ til_fb_t *fb;
+ int r;
+
+ assert(ops);
+ assert(ops->page_alloc);
+ assert(ops->page_free);
+ assert(ops->page_flip);
+ assert(n_pages > 1);
+ assert(res_fb);
+
+ /* XXX: page-flipping is the only supported rendering model, requiring 2+ pages. */
+ if (n_pages < 2)
+ return -EINVAL;
+
+ fb = calloc(1, sizeof(til_fb_t));
+ if (!fb)
+ return -ENOMEM;
+
+ fb->ops = ops;
+ if (ops->init) {
+ r = ops->init(settings, &fb->ops_context);
+ if (r < 0)
+ goto fail;
+ }
+
+ for (int i = 0; i < n_pages; i++)
+ til_fb_page_new(fb);
+
+ fb->n_pages = n_pages;
+
+ pthread_mutex_init(&fb->ready_mutex, NULL);
+ pthread_cond_init(&fb->ready_cond, NULL);
+ pthread_mutex_init(&fb->inactive_mutex, NULL);
+ pthread_cond_init(&fb->inactive_cond, NULL);
+ pthread_mutex_init(&fb->rebuild_mutex, NULL);
+
+ page = _til_fb_page_get(fb);
+ if (!page) {
+ r = -ENOMEM;
+ goto fail;
+ }
+
+ r = til_fb_acquire(fb, page);
+ if (r < 0)
+ goto fail;
+
+ *res_fb = fb;
+
+ return r;
+
+fail:
+ til_fb_free(fb);
+
+ return r;
+}
+
+
+/* This informs the fb to reconstruct its pages as they become inactive,
+ * giving the backend an opportunity to reconfigure them before they get
+ * rendered to again. It's intended to be used in response to window
+ * resizes.
+ */
+void til_fb_rebuild(til_fb_t *fb)
+{
+ assert(fb);
+
+ /* TODO: this could easily be an atomic counter since we have no need for waiting */
+ pthread_mutex_lock(&fb->rebuild_mutex);
+ fb->rebuild_pages = fb->n_pages;
+ pthread_mutex_unlock(&fb->rebuild_mutex);
+}
+
+
+/* helpers for fragmenting incrementally */
+int til_fb_fragment_slice_single(const til_fb_fragment_t *fragment, unsigned n_fragments, unsigned number, til_fb_fragment_t *res_fragment)
+{
+ unsigned slice = fragment->height / n_fragments;
+ unsigned yoff = slice * number;
+ unsigned pitch;
+
+ if (yoff >= fragment->height)
+ return 0;
+
+ res_fragment->buf = ((void *)fragment->buf) + yoff * fragment->pitch;
+ res_fragment->x = fragment->x;
+ res_fragment->y = yoff;
+ res_fragment->width = fragment->width;
+ res_fragment->height = MIN(fragment->height - yoff, slice);
+ res_fragment->frame_width = fragment->frame_width;
+ res_fragment->frame_height = fragment->frame_height;
+ res_fragment->stride = fragment->stride;
+ res_fragment->pitch = fragment->pitch;
+ res_fragment->number = number;
+ res_fragment->zeroed = fragment->zeroed;
+
+ return 1;
+}
+
+
+int til_fb_fragment_tile_single(const til_fb_fragment_t *fragment, unsigned tile_size, unsigned number, til_fb_fragment_t *res_fragment)
+{
+ unsigned w = fragment->width / tile_size, h = fragment->height / tile_size;
+ unsigned x, y, xoff, yoff;
+
+ if (w * tile_size < fragment->width)
+ w++;
+
+ if (h * tile_size < fragment->height)
+ h++;
+
+ y = number / w;
+ if (y >= h)
+ return 0;
+
+ x = number - (y * w);
+
+ xoff = x * tile_size;
+ yoff = y * tile_size;
+
+ res_fragment->buf = (void *)fragment->buf + (yoff * fragment->pitch) + (xoff * 4);
+ res_fragment->x = fragment->x + xoff;
+ res_fragment->y = fragment->y + yoff;
+ res_fragment->width = MIN(fragment->width - xoff, tile_size);
+ res_fragment->height = MIN(fragment->height - yoff, tile_size);
+ res_fragment->frame_width = fragment->frame_width;
+ res_fragment->frame_height = fragment->frame_height;
+ res_fragment->stride = fragment->stride + ((fragment->width - res_fragment->width) * 4);
+ res_fragment->pitch = fragment->pitch;
+ res_fragment->number = number;
+
+ return 1;
+}
© All Rights Reserved