PD#158474: add chromiumos drm-tests

Change-Id: I33b220ec40c8147bc6e6228cecff164f8555582f
Signed-off-by: Yalong Liu <yalong.liu@amlogic.com>
diff --git a/drm-tests/.clang-format b/drm-tests/.clang-format
new file mode 100644
index 0000000..2f51f78
--- /dev/null
+++ b/drm-tests/.clang-format
@@ -0,0 +1,10 @@
+BasedOnStyle: Google
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+BreakBeforeBraces: Linux
+ColumnLimit: 100
+IndentWidth: 8
+TabWidth: 8
+UseTab: Always
+Cpp11BracedListStyle: false
diff --git a/drm-tests/Android.mk b/drm-tests/Android.mk
new file mode 100644
index 0000000..1d83aaf
--- /dev/null
+++ b/drm-tests/Android.mk
@@ -0,0 +1,17 @@
+LOCAL_PATH := $(call my-dir)
+
+#########################
+
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES :=  nv12_test.c bo.c dev.c modeset.c
+
+LOCAL_MODULE := nv12_test
+
+LOCAL_C_INCLUDES := \
+	external/libdrm \
+	external/libdrm/include/drm
+LOCAL_CFLAGS := -O2 -g -W -Wall
+LOCAL_SYSTEM_SHARED_LIBRARIES := libc
+LOCAL_SHARED_LIBRARIES := libdrm
+
+include $(BUILD_EXECUTABLE)
diff --git a/drm-tests/Makefile b/drm-tests/Makefile
new file mode 100644
index 0000000..8cb1458
--- /dev/null
+++ b/drm-tests/Makefile
@@ -0,0 +1,66 @@
+# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Pull in chromium os defaults
+OUT ?= $(PWD)/build-opt-local
+
+include common.mk
+
+PC_DEPS = libdrm egl gbm
+PC_CFLAGS := $(shell $(PKG_CONFIG) --cflags $(PC_DEPS))
+PC_LIBS := $(shell $(PKG_CONFIG) --libs $(PC_DEPS))
+
+DRM_LIBS = -lGLESv2
+CFLAGS += $(PC_CFLAGS) -DEGL_EGLEXT_PROTOTYPES -DGL_GLEXT_PROTOTYPES
+CFLAGS += -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE
+LDLIBS += $(PC_LIBS)
+
+all: \
+	CC_BINARY(atomictest) \
+	CC_BINARY(drm_cursor_test) \
+	CC_BINARY(gamma_test) \
+	CC_BINARY(linear_bo_test) \
+	CC_BINARY(mapped_texture_test) \
+	CC_BINARY(mmap_test) \
+	CC_BINARY(null_platform_test) \
+	CC_BINARY(plane_test) \
+	CC_BINARY(stripe) \
+	CC_BINARY(swrast_test) \
+	CC_BINARY(vgem_test)
+
+ifeq ($(USE_VULKAN),1)
+all: CC_BINARY(vk_glow)
+endif
+
+CC_BINARY(drm_cursor_test): drm_cursor_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+
+CC_BINARY(null_platform_test): null_platform_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(null_platform_test): LDLIBS += $(DRM_LIBS)
+
+CC_BINARY(vgem_test): vgem_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(mmap_test): mmap_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+
+CC_BINARY(linear_bo_test): linear_bo_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(linear_bo_test): LDLIBS += -lGLESv2
+
+CC_BINARY(swrast_test): swrast_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(swrast_test): LDLIBS += -lGLESv2
+
+CC_BINARY(atomictest): atomictest.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(atomictest): CFLAGS += -DUSE_ATOMIC_API
+CC_BINARY(atomictest): LDLIBS += $(DRM_LIBS)
+
+CC_BINARY(gamma_test): gamma_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(gamma_test): LDLIBS += -lm $(DRM_LIBS)
+
+CC_BINARY(plane_test): plane_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(plane_test): LDLIBS += -lm $(DRM_LIBS)
+
+CC_BINARY(mapped_texture_test): mapped_texture_test.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(mapped_texture_test): LDLIBS += -lGLESv2
+
+ifeq ($(USE_VULKAN),1)
+CC_BINARY(vk_glow): vk_glow.o CC_STATIC_LIBRARY(libbsdrm.pic.a)
+CC_BINARY(vk_glow): LDLIBS += -lm -lvulkan $(DRM_LIBS)
+endif
diff --git a/drm-tests/atomictest.c b/drm-tests/atomictest.c
new file mode 100644
index 0000000..19e6570
--- /dev/null
+++ b/drm-tests/atomictest.c
@@ -0,0 +1,1286 @@
+/*
+ * Copyright 2017 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/*
+ * This file performs some sanity checks on the DRM atomic API. To run a test, please run the
+ * following command:
+ *
+ * atomictest <testname>
+ *
+ * To get a list of possible tests, run:
+ *
+ * atomictest
+ */
+
+#include <getopt.h>
+
+#include "bs_drm.h"
+
+#define CHECK(cond)                                               \
+	do {                                                      \
+		if (!(cond)) {                                    \
+			bs_debug_error("check %s failed", #cond); \
+			return -1;                                \
+		}                                                 \
+	} while (0)
+
+#define CHECK_RESULT(ret)                                             \
+	do {                                                          \
+		if ((ret) < 0) {                                      \
+			bs_debug_error("failed with error: %d", ret); \
+			return -1;                                    \
+		}                                                     \
+	} while (0)
+
+#define CURSOR_SIZE 64
+
+// TODO(gsingh) This is defined in upstream libdrm -- remove when CROS libdrm is updated.
+#ifndef DRM_ROTATE_0
+#define DRM_ROTATE_0 (1UL << 0)
+#endif
+
+#ifndef DRM_REFLECT_Y
+#define DRM_REFLECT_Y (1UL << 5)
+#endif
+
+static const uint32_t yuv_formats[] = {
+	DRM_FORMAT_NV12, DRM_FORMAT_YVU420,
+};
+
+static struct gbm_device *gbm = NULL;
+
+static void page_flip_handler(int fd, unsigned int sequence, unsigned int tv_sec,
+			      unsigned int tv_usec, void *user_data)
+{
+	// Nothing to do.
+}
+
+struct atomictest_property {
+	uint32_t pid;
+	uint32_t value;
+};
+
+struct atomictest_plane {
+	drmModePlane drm_plane;
+	struct gbm_bo *bo;
+
+	uint32_t format_idx;
+
+	/* Properties. */
+	struct atomictest_property crtc_id;
+	struct atomictest_property crtc_x;
+	struct atomictest_property crtc_y;
+	struct atomictest_property crtc_w;
+	struct atomictest_property crtc_h;
+	struct atomictest_property fb_id;
+	struct atomictest_property src_x;
+	struct atomictest_property src_y;
+	struct atomictest_property src_w;
+	struct atomictest_property src_h;
+	struct atomictest_property type;
+	struct atomictest_property rotation;
+	struct atomictest_property ctm;
+};
+
+struct atomictest_connector {
+	uint32_t connector_id;
+	struct atomictest_property crtc_id;
+	struct atomictest_property edid;
+	struct atomictest_property dpms;
+};
+
+struct atomictest_crtc {
+	uint32_t crtc_id;
+	uint32_t width;
+	uint32_t height;
+	uint32_t *primary_idx;
+	uint32_t *cursor_idx;
+	uint32_t *overlay_idx;
+	uint32_t num_primary;
+	uint32_t num_cursor;
+	uint32_t num_overlay;
+
+	struct atomictest_plane *planes;
+	struct atomictest_property mode_id;
+	struct atomictest_property active;
+};
+
+struct atomictest_mode {
+	uint32_t height;
+	uint32_t width;
+	uint32_t id;
+};
+
+struct atomictest_context {
+	int fd;
+	uint32_t num_crtcs;
+	uint32_t num_connectors;
+	uint32_t num_modes;
+
+	struct atomictest_connector *connectors;
+	struct atomictest_crtc *crtcs;
+	struct atomictest_mode *modes;
+	drmModeAtomicReqPtr pset;
+	drmEventContext drm_event_ctx;
+
+	struct bs_mapper *mapper;
+};
+
+typedef int (*test_function)(struct atomictest_context *ctx, struct atomictest_crtc *crtc);
+
+struct atomictest_testcase {
+	const char *name;
+	test_function test_func;
+};
+
+// clang-format off
+enum draw_format_type {
+	DRAW_NONE = 0,
+	DRAW_STRIPE = 1,
+	DRAW_ELLIPSE = 2,
+	DRAW_CURSOR = 3,
+	DRAW_LINES = 4,
+};
+// clang-format on
+
+static int32_t get_format_idx(struct atomictest_plane *plane, uint32_t format)
+{
+	for (int32_t i = 0; i < plane->drm_plane.count_formats; i++)
+		if (plane->drm_plane.formats[i] == format)
+			return i;
+	return -1;
+}
+
+static void copy_drm_plane(drmModePlane *dest, drmModePlane *src)
+{
+	memcpy(dest, src, sizeof(drmModePlane));
+	dest->formats = calloc(src->count_formats, sizeof(uint32_t));
+	memcpy(dest->formats, src->formats, src->count_formats * sizeof(uint32_t));
+}
+
+static struct atomictest_plane *get_plane(struct atomictest_crtc *crtc, uint32_t idx, uint64_t type)
+{
+	uint32_t index;
+	switch (type) {
+		case DRM_PLANE_TYPE_OVERLAY:
+			index = crtc->overlay_idx[idx];
+			break;
+		case DRM_PLANE_TYPE_PRIMARY:
+			index = crtc->primary_idx[idx];
+			break;
+		case DRM_PLANE_TYPE_CURSOR:
+			index = crtc->cursor_idx[idx];
+			break;
+		default:
+			bs_debug_error("invalid plane type returned");
+			return NULL;
+	}
+
+	return &crtc->planes[index];
+}
+
+static int draw_to_plane(struct bs_mapper *mapper, struct atomictest_plane *plane,
+			 enum draw_format_type pattern)
+{
+	struct gbm_bo *bo = plane->bo;
+	uint32_t format = gbm_bo_get_format(bo);
+	const struct bs_draw_format *draw_format = bs_get_draw_format(format);
+
+	if (draw_format && pattern) {
+		switch (pattern) {
+			case DRAW_STRIPE:
+				CHECK(bs_draw_stripe(mapper, bo, draw_format));
+				break;
+			case DRAW_ELLIPSE:
+				CHECK(bs_draw_ellipse(mapper, bo, draw_format, 0));
+				break;
+			case DRAW_CURSOR:
+				CHECK(bs_draw_cursor(mapper, bo, draw_format));
+				break;
+			case DRAW_LINES:
+				CHECK(bs_draw_lines(mapper, bo, draw_format));
+				break;
+			default:
+				bs_debug_error("invalid draw type");
+				return -1;
+		}
+	} else {
+		// DRM_FORMAT_RGB565 --> red, DRM_FORMAT_BGR565 --> blue,
+		// everything else --> something
+		void *map_data;
+		uint16_t value = 0xF800;
+		void *addr = bs_mapper_map(mapper, bo, 0, &map_data);
+		uint32_t num_shorts = gbm_bo_get_plane_size(bo, 0) / sizeof(uint16_t);
+		uint16_t *pixel = (uint16_t *)addr;
+
+		CHECK(addr);
+		for (uint32_t i = 0; i < num_shorts; i++)
+			pixel[i] = value;
+
+		bs_mapper_unmap(mapper, bo, map_data);
+	}
+
+	return 0;
+}
+
+static int get_prop(int fd, drmModeObjectPropertiesPtr props, const char *name,
+		    struct atomictest_property *bs_prop)
+{
+	/* Property ID should always be > 0. */
+	bs_prop->pid = 0;
+	drmModePropertyPtr prop;
+	for (uint32_t i = 0; i < props->count_props; i++) {
+		if (bs_prop->pid)
+			break;
+
+		prop = drmModeGetProperty(fd, props->props[i]);
+		if (prop) {
+			if (!strcmp(prop->name, name)) {
+				bs_prop->pid = prop->prop_id;
+				bs_prop->value = props->prop_values[i];
+			}
+			drmModeFreeProperty(prop);
+		}
+	}
+
+	return (bs_prop->pid == 0) ? -1 : 0;
+}
+
+static int get_connector_props(int fd, struct atomictest_connector *connector,
+			       drmModeObjectPropertiesPtr props)
+{
+	CHECK_RESULT(get_prop(fd, props, "CRTC_ID", &connector->crtc_id));
+	CHECK_RESULT(get_prop(fd, props, "EDID", &connector->edid));
+	CHECK_RESULT(get_prop(fd, props, "DPMS", &connector->dpms));
+	return 0;
+}
+
+static int get_crtc_props(int fd, struct atomictest_crtc *crtc, drmModeObjectPropertiesPtr props)
+{
+	CHECK_RESULT(get_prop(fd, props, "MODE_ID", &crtc->mode_id));
+	CHECK_RESULT(get_prop(fd, props, "ACTIVE", &crtc->active));
+	return 0;
+}
+
+static int get_plane_props(int fd, struct atomictest_plane *plane, drmModeObjectPropertiesPtr props)
+{
+	CHECK_RESULT(get_prop(fd, props, "CRTC_ID", &plane->crtc_id));
+	CHECK_RESULT(get_prop(fd, props, "FB_ID", &plane->fb_id));
+	CHECK_RESULT(get_prop(fd, props, "CRTC_X", &plane->crtc_x));
+	CHECK_RESULT(get_prop(fd, props, "CRTC_Y", &plane->crtc_y));
+	CHECK_RESULT(get_prop(fd, props, "CRTC_W", &plane->crtc_w));
+	CHECK_RESULT(get_prop(fd, props, "CRTC_H", &plane->crtc_h));
+	CHECK_RESULT(get_prop(fd, props, "SRC_X", &plane->src_x));
+	CHECK_RESULT(get_prop(fd, props, "SRC_Y", &plane->src_y));
+	CHECK_RESULT(get_prop(fd, props, "SRC_W", &plane->src_w));
+	CHECK_RESULT(get_prop(fd, props, "SRC_H", &plane->src_h));
+	CHECK_RESULT(get_prop(fd, props, "type", &plane->type));
+
+	/*
+	 * The atomic API makes no guarantee a property is present in object. This test
+	 * requires the above common properties since a plane is undefined without them.
+	 * Other properties (i.e: rotation and ctm) are optional.
+	 */
+	get_prop(fd, props, "rotation", &plane->rotation);
+	get_prop(fd, props, "PLANE_CTM", &plane->ctm);
+	return 0;
+}
+
+int set_connector_props(struct atomictest_connector *conn, drmModeAtomicReqPtr pset)
+{
+	uint32_t id = conn->connector_id;
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, conn->crtc_id.pid, conn->crtc_id.value));
+	return 0;
+}
+
+int set_crtc_props(struct atomictest_crtc *crtc, drmModeAtomicReqPtr pset)
+{
+	uint32_t id = crtc->crtc_id;
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, crtc->mode_id.pid, crtc->mode_id.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, crtc->active.pid, crtc->active.value));
+	return 0;
+}
+
+int set_plane_props(struct atomictest_plane *plane, drmModeAtomicReqPtr pset)
+{
+	uint32_t id = plane->drm_plane.plane_id;
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->crtc_id.pid, plane->crtc_id.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->fb_id.pid, plane->fb_id.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->crtc_x.pid, plane->crtc_x.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->crtc_y.pid, plane->crtc_y.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->crtc_w.pid, plane->crtc_w.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->crtc_h.pid, plane->crtc_h.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->src_x.pid, plane->src_x.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->src_y.pid, plane->src_y.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->src_w.pid, plane->src_w.value));
+	CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->src_h.pid, plane->src_h.value));
+	if (plane->rotation.pid)
+		CHECK_RESULT(
+		    drmModeAtomicAddProperty(pset, id, plane->rotation.pid, plane->rotation.value));
+	if (plane->ctm.pid)
+		CHECK_RESULT(drmModeAtomicAddProperty(pset, id, plane->ctm.pid, plane->ctm.value));
+
+	return 0;
+}
+
+static int remove_plane_fb(struct atomictest_context *ctx, struct atomictest_plane *plane)
+{
+	if (plane->bo && plane->fb_id.value) {
+		CHECK_RESULT(drmModeRmFB(ctx->fd, plane->fb_id.value));
+		gbm_bo_destroy(plane->bo);
+		plane->bo = NULL;
+		plane->fb_id.value = 0;
+	}
+
+	return 0;
+}
+
+static int add_plane_fb(struct atomictest_context *ctx, struct atomictest_plane *plane)
+{
+	if (plane->format_idx < plane->drm_plane.count_formats) {
+		CHECK_RESULT(remove_plane_fb(ctx, plane));
+		uint32_t flags = (plane->type.value == DRM_PLANE_TYPE_CURSOR) ? GBM_BO_USE_CURSOR
+									      : GBM_BO_USE_SCANOUT;
+		flags |= GBM_BO_USE_SW_WRITE_RARELY;
+		/* TODO(gsingh): add create with modifiers option. */
+		plane->bo = gbm_bo_create(gbm, plane->crtc_w.value, plane->crtc_h.value,
+					  plane->drm_plane.formats[plane->format_idx], flags);
+
+		CHECK(plane->bo);
+		plane->fb_id.value = bs_drm_fb_create_gbm(plane->bo);
+		CHECK(plane->fb_id.value);
+		CHECK_RESULT(set_plane_props(plane, ctx->pset));
+	}
+
+	return 0;
+}
+
+static int init_plane(struct atomictest_context *ctx, struct atomictest_plane *plane,
+		      uint32_t format, uint32_t x, uint32_t y, uint32_t w, uint32_t h,
+		      uint32_t crtc_id)
+{
+	int32_t idx = get_format_idx(plane, format);
+	if (idx < 0)
+		return -EINVAL;
+
+	plane->format_idx = idx;
+	plane->crtc_x.value = x;
+	plane->crtc_y.value = y;
+	plane->crtc_w.value = w;
+	plane->crtc_h.value = h;
+	plane->src_w.value = plane->crtc_w.value << 16;
+	plane->src_h.value = plane->crtc_h.value << 16;
+	plane->crtc_id.value = crtc_id;
+
+	CHECK_RESULT(add_plane_fb(ctx, plane));
+	return 0;
+}
+
+static int init_yuv_plane(struct atomictest_context *ctx, struct atomictest_plane *plane,
+			  uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t crtc_id)
+{
+	uint32_t i;
+	for (i = 0; i < BS_ARRAY_LEN(yuv_formats); i++)
+		if (!init_plane(ctx, plane, yuv_formats[i], x, y, w, h, crtc_id))
+			return 0;
+
+	return -EINVAL;
+}
+
+static int disable_plane(struct atomictest_context *ctx, struct atomictest_plane *plane)
+{
+	plane->format_idx = 0;
+	plane->crtc_x.value = 0;
+	plane->crtc_y.value = 0;
+	plane->crtc_w.value = 0;
+	plane->crtc_h.value = 0;
+	plane->src_w.value = 0;
+	plane->src_h.value = 0;
+	plane->crtc_id.value = 0;
+
+	if (plane->rotation.pid)
+		plane->rotation.value = DRM_ROTATE_0;
+	if (plane->ctm.pid)
+		plane->ctm.value = 0;
+
+	CHECK_RESULT(remove_plane_fb(ctx, plane));
+	CHECK_RESULT(set_plane_props(plane, ctx->pset));
+	return 0;
+}
+
+static int move_plane(struct atomictest_context *ctx, struct atomictest_crtc *crtc,
+		      struct atomictest_plane *plane, uint32_t dx, uint32_t dy)
+{
+	if (plane->crtc_x.value < (crtc->width - plane->crtc_w.value) &&
+	    plane->crtc_y.value < (crtc->height - plane->crtc_h.value)) {
+		plane->crtc_x.value += dx;
+		plane->crtc_y.value += dy;
+		CHECK_RESULT(set_plane_props(plane, ctx->pset));
+		return 0;
+	}
+
+	return -1;
+}
+
+static int scale_plane(struct atomictest_context *ctx, struct atomictest_crtc *crtc,
+		       struct atomictest_plane *plane, float dw, float dh)
+{
+	int32_t plane_w = (int32_t)plane->crtc_w.value + dw * plane->crtc_w.value;
+	int32_t plane_h = (int32_t)plane->crtc_h.value + dh * plane->crtc_h.value;
+	if (plane_w > 0 && plane_h > 0 && (plane->crtc_x.value + plane_w < crtc->width) &&
+	    (plane->crtc_h.value + plane_h < crtc->height)) {
+		plane->crtc_w.value = BS_ALIGN((uint32_t)plane_w, 2);
+		plane->crtc_h.value = BS_ALIGN((uint32_t)plane_h, 2);
+		CHECK_RESULT(set_plane_props(plane, ctx->pset));
+		return 0;
+	}
+
+	return -1;
+}
+
+static void log(struct atomictest_context *ctx)
+{
+	printf("Committing the following configuration: \n");
+	for (uint32_t i = 0; i < ctx->num_crtcs; i++) {
+		struct atomictest_plane *plane;
+		struct atomictest_crtc *crtc = &ctx->crtcs[i];
+		uint32_t num_planes = crtc->num_primary + crtc->num_cursor + crtc->num_overlay;
+		if (!crtc->active.value)
+			continue;
+
+		printf("----- [CRTC: %u] -----\n", crtc->crtc_id);
+		for (uint32_t j = 0; j < num_planes; j++) {
+			plane = &crtc->planes[j];
+			if (plane->crtc_id.value == crtc->crtc_id && plane->fb_id.value) {
+				uint32_t format = gbm_bo_get_format(plane->bo);
+				char *fourcc = (char *)&format;
+				printf("\t{Plane ID: %u, ", plane->drm_plane.plane_id);
+				printf("Plane format: %c%c%c%c, ", fourcc[0], fourcc[1], fourcc[2],
+				       fourcc[3]);
+				printf("Plane type: ");
+				switch (plane->type.value) {
+					case DRM_PLANE_TYPE_OVERLAY:
+						printf("overlay, ");
+						break;
+					case DRM_PLANE_TYPE_PRIMARY:
+						printf("primary, ");
+						break;
+					case DRM_PLANE_TYPE_CURSOR:
+						printf("cursor, ");
+						break;
+				}
+
+				printf("CRTC_X: %u, CRTC_Y: %u, CRTC_W: %u, CRTC_H: %u}\n",
+				       plane->crtc_x.value, plane->crtc_y.value,
+				       plane->crtc_w.value, plane->crtc_h.value);
+			}
+		}
+	}
+}
+
+static int test_commit(struct atomictest_context *ctx)
+{
+	return drmModeAtomicCommit(ctx->fd, ctx->pset, DRM_MODE_ATOMIC_TEST_ONLY, NULL);
+}
+
+static int commit(struct atomictest_context *ctx)
+{
+	int ret;
+	fd_set fds;
+	FD_ZERO(&fds);
+	FD_SET(ctx->fd, &fds);
+
+	log(ctx);
+	ret = drmModeAtomicCommit(ctx->fd, ctx->pset,
+				  DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
+	CHECK_RESULT(ret);
+	do {
+		ret = select(ctx->fd + 1, &fds, NULL, NULL, NULL);
+	} while (ret == -1 && errno == EINTR);
+
+	CHECK_RESULT(ret);
+	if (FD_ISSET(ctx->fd, &fds))
+		drmHandleEvent(ctx->fd, &ctx->drm_event_ctx);
+
+	return 0;
+}
+
+static int pageflip_formats(struct atomictest_context *ctx, struct atomictest_crtc *crtc,
+			    struct atomictest_plane *plane)
+{
+	uint32_t flags;
+	for (uint32_t i = 0; i < plane->drm_plane.count_formats; i++) {
+		flags = (plane->type.value == DRM_PLANE_TYPE_CURSOR) ? GBM_BO_USE_CURSOR
+								     : GBM_BO_USE_SCANOUT;
+		if (!gbm_device_is_format_supported(gbm, plane->drm_plane.formats[i], flags))
+			continue;
+
+		CHECK_RESULT(init_plane(ctx, plane, plane->drm_plane.formats[i], 0, 0, crtc->width,
+					crtc->height, crtc->crtc_id));
+		CHECK_RESULT(draw_to_plane(ctx->mapper, plane, DRAW_ELLIPSE));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+
+		// disable, but don't commit, since we can't have an active CRTC without any planes.
+		CHECK_RESULT(disable_plane(ctx, plane));
+	}
+
+	return 0;
+}
+
+static uint32_t get_connection(struct atomictest_crtc *crtc, uint32_t crtc_index)
+{
+	uint32_t connector_id = 0;
+	uint32_t crtc_mask = 1u << crtc_index;
+	struct bs_drm_pipe pipe = { 0 };
+	struct bs_drm_pipe_plumber *plumber = bs_drm_pipe_plumber_new();
+	bs_drm_pipe_plumber_crtc_mask(plumber, crtc_mask);
+	if (bs_drm_pipe_plumber_make(plumber, &pipe))
+		connector_id = pipe.connector_id;
+
+	bs_drm_pipe_plumber_destroy(&plumber);
+	return connector_id;
+}
+
+static int enable_crtc(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	drmModeAtomicSetCursor(ctx->pset, 0);
+
+	for (uint32_t i = 0; i < ctx->num_connectors; i++) {
+		ctx->connectors[i].crtc_id.value = 0;
+		set_connector_props(&ctx->connectors[i], ctx->pset);
+	}
+
+	for (uint32_t i = 0; i < ctx->num_crtcs; i++) {
+		if (&ctx->crtcs[i] == crtc) {
+			uint32_t connector_id = get_connection(crtc, i);
+			CHECK(connector_id);
+			for (uint32_t j = 0; j < ctx->num_connectors; j++) {
+				if (connector_id == ctx->connectors[j].connector_id) {
+					ctx->connectors[j].crtc_id.value = crtc->crtc_id;
+					set_connector_props(&ctx->connectors[j], ctx->pset);
+					break;
+				}
+			}
+
+			break;
+		}
+	}
+
+	int ret = -EINVAL;
+	int cursor = drmModeAtomicGetCursor(ctx->pset);
+
+	for (uint32_t i = 0; i < ctx->num_modes; i++) {
+		struct atomictest_mode *mode = &ctx->modes[i];
+		drmModeAtomicSetCursor(ctx->pset, cursor);
+
+		crtc->mode_id.value = mode->id;
+		crtc->active.value = 1;
+		crtc->width = mode->width;
+		crtc->height = mode->height;
+
+		set_crtc_props(crtc, ctx->pset);
+		ret = drmModeAtomicCommit(ctx->fd, ctx->pset,
+					  DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET,
+					  NULL);
+		if (!ret)
+			return 0;
+	}
+
+	bs_debug_error("[CRTC:%d]: failed to find mode", crtc->crtc_id);
+	return ret;
+}
+
+static int disable_crtc(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	for (uint32_t i = 0; i < ctx->num_connectors; i++) {
+		ctx->connectors[i].crtc_id.value = 0;
+		set_connector_props(&ctx->connectors[i], ctx->pset);
+	}
+
+	crtc->mode_id.value = 0;
+	crtc->active.value = 0;
+	set_crtc_props(crtc, ctx->pset);
+	int ret = drmModeAtomicCommit(ctx->fd, ctx->pset, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
+	CHECK_RESULT(ret);
+	return ret;
+}
+
+static struct atomictest_context *new_context(uint32_t num_connectors, uint32_t num_crtcs,
+					      uint32_t num_planes)
+{
+	struct atomictest_context *ctx = calloc(1, sizeof(*ctx));
+
+	ctx->mapper = bs_mapper_gem_new();
+	if (ctx->mapper == NULL) {
+		bs_debug_error("failed to create mapper object");
+		free(ctx);
+		return NULL;
+	}
+
+	ctx->connectors = calloc(num_connectors, sizeof(*ctx->connectors));
+	ctx->crtcs = calloc(num_crtcs, sizeof(*ctx->crtcs));
+	for (uint32_t i = 0; i < num_crtcs; i++) {
+		ctx->crtcs[i].planes = calloc(num_planes, sizeof(*ctx->crtcs[i].planes));
+		ctx->crtcs[i].overlay_idx = calloc(num_planes, sizeof(uint32_t));
+		ctx->crtcs[i].primary_idx = calloc(num_planes, sizeof(uint32_t));
+		ctx->crtcs[i].cursor_idx = calloc(num_planes, sizeof(uint32_t));
+	}
+
+	ctx->num_connectors = num_connectors;
+	ctx->num_crtcs = num_crtcs;
+	ctx->num_modes = 0;
+	ctx->modes = NULL;
+	ctx->pset = drmModeAtomicAlloc();
+	ctx->drm_event_ctx.version = DRM_EVENT_CONTEXT_VERSION;
+	ctx->drm_event_ctx.page_flip_handler = page_flip_handler;
+
+	return ctx;
+}
+
+static void free_context(struct atomictest_context *ctx)
+{
+	for (uint32_t i = 0; i < ctx->num_crtcs; i++) {
+		uint32_t num_planes = ctx->crtcs[i].num_primary + ctx->crtcs[i].num_cursor +
+				      ctx->crtcs[i].num_overlay;
+
+		for (uint32_t j = 0; j < num_planes; j++) {
+			remove_plane_fb(ctx, &ctx->crtcs[i].planes[j]);
+			free(ctx->crtcs[i].planes[j].drm_plane.formats);
+		}
+
+		free(ctx->crtcs[i].planes);
+		free(ctx->crtcs[i].overlay_idx);
+		free(ctx->crtcs[i].cursor_idx);
+		free(ctx->crtcs[i].primary_idx);
+	}
+
+	drmModeAtomicFree(ctx->pset);
+	free(ctx->modes);
+	free(ctx->crtcs);
+	free(ctx->connectors);
+	bs_mapper_destroy(ctx->mapper);
+	free(ctx);
+}
+
+static struct atomictest_context *query_kms(int fd)
+{
+	drmModeRes *res = drmModeGetResources(fd);
+	if (res == NULL) {
+		bs_debug_error("failed to get drm resources");
+		return false;
+	}
+
+	drmModePlaneRes *plane_res = drmModeGetPlaneResources(fd);
+	if (plane_res == NULL) {
+		bs_debug_error("failed to get plane resources");
+		drmModeFreeResources(res);
+		return NULL;
+	}
+
+	struct atomictest_context *ctx =
+	    new_context(res->count_connectors, res->count_crtcs, plane_res->count_planes);
+	if (ctx == NULL) {
+		bs_debug_error("failed to allocate atomic context");
+		drmModeFreePlaneResources(plane_res);
+		drmModeFreeResources(res);
+		return NULL;
+	}
+
+	ctx->fd = fd;
+	drmModeObjectPropertiesPtr props = NULL;
+
+	for (uint32_t conn_index = 0; conn_index < res->count_connectors; conn_index++) {
+		uint32_t conn_id = res->connectors[conn_index];
+		ctx->connectors[conn_index].connector_id = conn_id;
+		props = drmModeObjectGetProperties(fd, conn_id, DRM_MODE_OBJECT_CONNECTOR);
+		get_connector_props(fd, &ctx->connectors[conn_index], props);
+
+		drmModeConnector *connector = drmModeGetConnector(fd, conn_id);
+		for (uint32_t mode_index = 0; mode_index < connector->count_modes; mode_index++) {
+			ctx->modes =
+			    realloc(ctx->modes, (ctx->num_modes + 1) * sizeof(*ctx->modes));
+			drmModeCreatePropertyBlob(fd, &connector->modes[mode_index],
+						  sizeof(drmModeModeInfo),
+						  &ctx->modes[ctx->num_modes].id);
+			ctx->modes[ctx->num_modes].width = connector->modes[mode_index].hdisplay;
+			ctx->modes[ctx->num_modes].height = connector->modes[mode_index].vdisplay;
+			ctx->num_modes++;
+		}
+
+		drmModeFreeConnector(connector);
+		drmModeFreeObjectProperties(props);
+		props = NULL;
+	}
+
+	uint32_t crtc_index;
+	for (crtc_index = 0; crtc_index < res->count_crtcs; crtc_index++) {
+		ctx->crtcs[crtc_index].crtc_id = res->crtcs[crtc_index];
+		props =
+		    drmModeObjectGetProperties(fd, res->crtcs[crtc_index], DRM_MODE_OBJECT_CRTC);
+		get_crtc_props(fd, &ctx->crtcs[crtc_index], props);
+
+		drmModeFreeObjectProperties(props);
+		props = NULL;
+	}
+
+	uint32_t overlay_idx, primary_idx, cursor_idx, idx;
+
+	for (uint32_t plane_index = 0; plane_index < plane_res->count_planes; plane_index++) {
+		drmModePlane *plane = drmModeGetPlane(fd, plane_res->planes[plane_index]);
+		if (plane == NULL) {
+			bs_debug_error("failed to get plane id %u", plane_res->planes[plane_index]);
+			continue;
+		}
+
+		uint32_t crtc_mask = 0;
+
+		drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(
+		    fd, plane_res->planes[plane_index], DRM_MODE_OBJECT_PLANE);
+
+		for (crtc_index = 0; crtc_index < res->count_crtcs; crtc_index++) {
+			crtc_mask = (1 << crtc_index);
+			if (plane->possible_crtcs & crtc_mask) {
+				struct atomictest_crtc *crtc = &ctx->crtcs[crtc_index];
+				cursor_idx = crtc->num_cursor;
+				primary_idx = crtc->num_primary;
+				overlay_idx = crtc->num_overlay;
+				idx = cursor_idx + primary_idx + overlay_idx;
+				copy_drm_plane(&crtc->planes[idx].drm_plane, plane);
+				get_plane_props(fd, &crtc->planes[idx], props);
+				switch (crtc->planes[idx].type.value) {
+					case DRM_PLANE_TYPE_OVERLAY:
+						crtc->overlay_idx[overlay_idx] = idx;
+						crtc->num_overlay++;
+						break;
+					case DRM_PLANE_TYPE_PRIMARY:
+						crtc->primary_idx[primary_idx] = idx;
+						crtc->num_primary++;
+						break;
+					case DRM_PLANE_TYPE_CURSOR:
+						crtc->cursor_idx[cursor_idx] = idx;
+						crtc->num_cursor++;
+						break;
+					default:
+						bs_debug_error("invalid plane type returned");
+						return NULL;
+				}
+
+				/*
+				 * The DRM UAPI states that cursor and overlay framebuffers may be
+				 * present after a CRTC disable, so zero this out so we can get a
+				 * clean slate.
+				 */
+				crtc->planes[idx].fb_id.value = 0;
+			}
+		}
+
+		drmModeFreePlane(plane);
+		drmModeFreeObjectProperties(props);
+		props = NULL;
+	}
+
+	drmModeFreePlaneResources(plane_res);
+	drmModeFreeResources(res);
+	return ctx;
+}
+
+static int test_multiple_planes(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *primary, *overlay, *cursor;
+	for (uint32_t i = 0; i < crtc->num_primary; i++) {
+		bool video = true;
+		uint32_t x, y;
+		for (uint32_t j = 0; j < crtc->num_overlay; j++) {
+			overlay = get_plane(crtc, j, DRM_PLANE_TYPE_OVERLAY);
+			x = crtc->width >> (j + 2);
+			y = crtc->height >> (j + 2);
+			// drmModeAddFB2 requires the height and width are even for sub-sampled YUV
+			// formats.
+			x = BS_ALIGN(x, 2);
+			y = BS_ALIGN(y, 2);
+			if (video && !init_yuv_plane(ctx, overlay, x, y, x, y, crtc->crtc_id)) {
+				CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_STRIPE));
+				video = false;
+			} else {
+				CHECK_RESULT(init_plane(ctx, overlay, DRM_FORMAT_XRGB8888, x, y, x,
+							y, crtc->crtc_id));
+				CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_LINES));
+			}
+		}
+
+		for (uint32_t j = 0; j < crtc->num_cursor; j++) {
+			x = crtc->width >> (j + 2);
+			y = crtc->height >> (j + 2);
+			cursor = get_plane(crtc, j, DRM_PLANE_TYPE_CURSOR);
+			CHECK_RESULT(init_plane(ctx, cursor, DRM_FORMAT_ARGB8888, x, y, CURSOR_SIZE,
+						CURSOR_SIZE, crtc->crtc_id));
+			CHECK_RESULT(draw_to_plane(ctx->mapper, cursor, DRAW_CURSOR));
+		}
+
+		primary = get_plane(crtc, i, DRM_PLANE_TYPE_PRIMARY);
+		CHECK_RESULT(init_plane(ctx, primary, DRM_FORMAT_XRGB8888, 0, 0, crtc->width,
+					crtc->height, crtc->crtc_id));
+		CHECK_RESULT(draw_to_plane(ctx->mapper, primary, DRAW_ELLIPSE));
+
+		uint32_t num_planes = crtc->num_primary + crtc->num_cursor + crtc->num_overlay;
+		int done = 0;
+		struct atomictest_plane *plane;
+		while (!done) {
+			done = 1;
+			for (uint32_t j = 0; j < num_planes; j++) {
+				plane = &crtc->planes[j];
+				if (plane->type.value != DRM_PLANE_TYPE_PRIMARY)
+					done &= move_plane(ctx, crtc, plane, 20, 20);
+			}
+
+			CHECK_RESULT(commit(ctx));
+			usleep(1e6 / 60);
+		}
+
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+
+		/* Disable primary plane and verify overlays show up. */
+		CHECK_RESULT(disable_plane(ctx, primary));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+	}
+
+	return 0;
+}
+
+static int test_video_overlay(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *overlay;
+	for (uint32_t i = 0; i < crtc->num_overlay; i++) {
+		overlay = get_plane(crtc, i, DRM_PLANE_TYPE_OVERLAY);
+		if (init_yuv_plane(ctx, overlay, 0, 0, 800, 800, crtc->crtc_id))
+			continue;
+
+		CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_STRIPE));
+		while (!move_plane(ctx, crtc, overlay, 20, 20)) {
+			CHECK_RESULT(commit(ctx));
+			usleep(1e6 / 60);
+		}
+	}
+
+	return 0;
+}
+
+static int test_orientation(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *primary, *overlay;
+	for (uint32_t i = 0; i < crtc->num_overlay; i++) {
+		overlay = get_plane(crtc, i, DRM_PLANE_TYPE_OVERLAY);
+		if (!overlay->rotation.pid)
+			continue;
+
+		CHECK_RESULT(init_plane(ctx, overlay, DRM_FORMAT_XRGB8888, 0, 0, crtc->width,
+					crtc->height, crtc->crtc_id));
+
+		overlay->rotation.value = DRM_ROTATE_0;
+		set_plane_props(overlay, ctx->pset);
+		CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_LINES));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+
+		overlay->rotation.value = DRM_REFLECT_Y;
+		set_plane_props(overlay, ctx->pset);
+		if (!test_commit(ctx)) {
+			CHECK_RESULT(commit(ctx));
+			usleep(1e6);
+		}
+
+		CHECK_RESULT(disable_plane(ctx, overlay));
+	}
+
+	for (uint32_t i = 0; i < crtc->num_primary; i++) {
+		primary = get_plane(crtc, i, DRM_PLANE_TYPE_PRIMARY);
+		if (!primary->rotation.pid)
+			continue;
+
+		CHECK_RESULT(init_plane(ctx, primary, DRM_FORMAT_XRGB8888, 0, 0, crtc->width,
+					crtc->height, crtc->crtc_id));
+
+		primary->rotation.value = DRM_ROTATE_0;
+		set_plane_props(primary, ctx->pset);
+		CHECK_RESULT(draw_to_plane(ctx->mapper, primary, DRAW_LINES));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+
+		primary->rotation.value = DRM_REFLECT_Y;
+		set_plane_props(primary, ctx->pset);
+		if (!test_commit(ctx)) {
+			CHECK_RESULT(commit(ctx));
+			usleep(1e6);
+		}
+
+		CHECK_RESULT(disable_plane(ctx, primary));
+	}
+
+	return 0;
+}
+
+static int test_plane_ctm(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *primary, *overlay;
+	/*
+	 * The blob for the PLANE_CTM propery is a drm_color_ctm.
+	 * drm_color_ctm contains a 3x3 u64 matrix, where every element
+	 * represents a S31.32 fixed point number.
+	 */
+	int64_t identity_ctm[9] = { 0x100000000, 0x0, 0x0, 0x0,	0x100000000,
+				    0x0,	 0x0, 0x0, 0x100000000 };
+	int64_t red_shift_ctm[9] = { 0x140000000, 0x0, 0x0, 0x0,       0xC0000000,
+				     0x0,	 0x0, 0x0, 0xC0000000 };
+
+	for (uint32_t i = 0; i < crtc->num_overlay; i++) {
+		overlay = get_plane(crtc, i, DRM_PLANE_TYPE_OVERLAY);
+		if (!overlay->ctm.pid)
+			continue;
+
+		CHECK_RESULT(init_plane(ctx, overlay, DRM_FORMAT_XRGB8888, 0, 0, crtc->width,
+					crtc->height, crtc->crtc_id));
+
+		CHECK_RESULT(drmModeCreatePropertyBlob(ctx->fd, identity_ctm, sizeof(identity_ctm),
+						       &overlay->ctm.value));
+		set_plane_props(overlay, ctx->pset);
+		CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_LINES));
+		CHECK_RESULT(commit(ctx));
+		CHECK_RESULT(drmModeDestroyPropertyBlob(ctx->fd, overlay->ctm.value));
+		usleep(1e6);
+
+		CHECK_RESULT(drmModeCreatePropertyBlob(ctx->fd, red_shift_ctm,
+						       sizeof(red_shift_ctm), &overlay->ctm.value));
+		set_plane_props(overlay, ctx->pset);
+		CHECK_RESULT(commit(ctx));
+		CHECK_RESULT(drmModeDestroyPropertyBlob(ctx->fd, overlay->ctm.value));
+		usleep(1e6);
+
+		CHECK_RESULT(disable_plane(ctx, overlay));
+	}
+
+	for (uint32_t i = 0; i < crtc->num_primary; i++) {
+		primary = get_plane(crtc, i, DRM_PLANE_TYPE_PRIMARY);
+		if (!primary->ctm.pid)
+			continue;
+
+		CHECK_RESULT(init_plane(ctx, primary, DRM_FORMAT_XRGB8888, 0, 0, crtc->width,
+					crtc->height, crtc->crtc_id));
+		CHECK_RESULT(drmModeCreatePropertyBlob(ctx->fd, identity_ctm, sizeof(identity_ctm),
+						       &primary->ctm.value));
+		set_plane_props(primary, ctx->pset);
+		CHECK_RESULT(draw_to_plane(ctx->mapper, primary, DRAW_LINES));
+		CHECK_RESULT(commit(ctx));
+		CHECK_RESULT(drmModeDestroyPropertyBlob(ctx->fd, primary->ctm.value));
+		usleep(1e6);
+
+		CHECK_RESULT(drmModeCreatePropertyBlob(ctx->fd, red_shift_ctm,
+						       sizeof(red_shift_ctm), &primary->ctm.value));
+		set_plane_props(primary, ctx->pset);
+		CHECK_RESULT(commit(ctx));
+		CHECK_RESULT(drmModeDestroyPropertyBlob(ctx->fd, primary->ctm.value));
+		usleep(1e6);
+
+		CHECK_RESULT(disable_plane(ctx, primary));
+	}
+
+	return 0;
+}
+
+static int test_fullscreen_video(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *primary;
+	for (uint32_t i = 0; i < crtc->num_primary; i++) {
+		primary = get_plane(crtc, i, DRM_PLANE_TYPE_PRIMARY);
+		if (init_yuv_plane(ctx, primary, 0, 0, crtc->width, crtc->height, crtc->crtc_id))
+			continue;
+
+		CHECK_RESULT(draw_to_plane(ctx->mapper, primary, DRAW_STRIPE));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+	}
+
+	return 0;
+}
+
+static int test_disable_primary(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *primary, *overlay;
+	for (uint32_t i = 0; i < crtc->num_primary; i++) {
+		for (uint32_t j = 0; j < crtc->num_overlay; j++) {
+			overlay = get_plane(crtc, j, DRM_PLANE_TYPE_OVERLAY);
+			uint32_t x = crtc->width >> (j + 2);
+			uint32_t y = crtc->height >> (j + 2);
+			CHECK_RESULT(init_plane(ctx, overlay, DRM_FORMAT_XRGB8888, x, y, x, y,
+						crtc->crtc_id));
+			CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_LINES));
+		}
+
+		primary = get_plane(crtc, i, DRM_PLANE_TYPE_PRIMARY);
+		CHECK_RESULT(init_plane(ctx, primary, DRM_FORMAT_XRGB8888, 0, 0, crtc->width,
+					crtc->height, crtc->crtc_id));
+		CHECK_RESULT(draw_to_plane(ctx->mapper, primary, DRAW_ELLIPSE));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+
+		/* Disable primary plane. */
+		disable_plane(ctx, primary);
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+	}
+
+	return 0;
+}
+
+static int test_overlay_pageflip(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *overlay;
+	for (uint32_t i = 0; i < crtc->num_overlay; i++) {
+		overlay = get_plane(crtc, i, DRM_PLANE_TYPE_OVERLAY);
+		CHECK_RESULT(pageflip_formats(ctx, crtc, overlay));
+	}
+
+	return 0;
+}
+
+static int test_overlay_downscaling(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *overlay;
+	uint32_t w = BS_ALIGN(crtc->width / 2, 2);
+	uint32_t h = BS_ALIGN(crtc->height / 2, 2);
+	for (uint32_t i = 0; i < crtc->num_overlay; i++) {
+		overlay = get_plane(crtc, i, DRM_PLANE_TYPE_OVERLAY);
+		if (init_yuv_plane(ctx, overlay, 0, 0, w, h, crtc->crtc_id)) {
+			CHECK_RESULT(init_plane(ctx, overlay, DRM_FORMAT_XRGB8888, 0, 0, w, h,
+						crtc->crtc_id));
+		}
+
+		CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_LINES));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+
+		while (!scale_plane(ctx, crtc, overlay, -.1f, -.1f) && !test_commit(ctx)) {
+			CHECK_RESULT(commit(ctx));
+			usleep(1e6);
+		}
+
+		disable_plane(ctx, overlay);
+	}
+
+	return 0;
+}
+
+static int test_overlay_upscaling(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *overlay;
+	uint32_t w = BS_ALIGN(crtc->width / 4, 2);
+	uint32_t h = BS_ALIGN(crtc->height / 4, 2);
+	for (uint32_t i = 0; i < crtc->num_overlay; i++) {
+		overlay = get_plane(crtc, i, DRM_PLANE_TYPE_OVERLAY);
+		if (init_yuv_plane(ctx, overlay, 0, 0, w, h, crtc->crtc_id)) {
+			CHECK_RESULT(init_plane(ctx, overlay, DRM_FORMAT_XRGB8888, 0, 0, w, h,
+						crtc->crtc_id));
+		}
+
+		CHECK_RESULT(draw_to_plane(ctx->mapper, overlay, DRAW_LINES));
+		CHECK_RESULT(commit(ctx));
+		usleep(1e6);
+
+		while (!scale_plane(ctx, crtc, overlay, .1f, .1f) && !test_commit(ctx)) {
+			CHECK_RESULT(commit(ctx));
+			usleep(1e6);
+		}
+
+		disable_plane(ctx, overlay);
+	}
+
+	return 0;
+}
+
+static int test_primary_pageflip(struct atomictest_context *ctx, struct atomictest_crtc *crtc)
+{
+	struct atomictest_plane *primary;
+	for (uint32_t i = 0; i < crtc->num_primary; i++) {
+		primary = get_plane(crtc, i, DRM_PLANE_TYPE_PRIMARY);
+		CHECK_RESULT(pageflip_formats(ctx, crtc, primary));
+	}
+
+	return 0;
+}
+
+static const struct atomictest_testcase cases[] = {
+	{ "disable_primary", test_disable_primary },
+	{ "fullscreen_video", test_fullscreen_video },
+	{ "multiple_planes", test_multiple_planes },
+	{ "overlay_pageflip", test_overlay_pageflip },
+	{ "overlay_downscaling", test_overlay_downscaling },
+	{ "overlay_upscaling", test_overlay_upscaling },
+	{ "primary_pageflip", test_primary_pageflip },
+	{ "video_overlay", test_video_overlay },
+	{ "orientation", test_orientation },
+	/* CTM stands for Color Transform Matrix. */
+	{ "plane_ctm", test_plane_ctm },
+};
+
+static int run_testcase(struct atomictest_context *ctx, struct atomictest_crtc *crtc,
+			test_function func)
+{
+	int cursor = drmModeAtomicGetCursor(ctx->pset);
+	uint32_t num_planes = crtc->num_primary + crtc->num_cursor + crtc->num_overlay;
+
+	int ret = func(ctx, crtc);
+
+	for (uint32_t i = 0; i < num_planes; i++)
+		disable_plane(ctx, &crtc->planes[i]);
+
+	drmModeAtomicSetCursor(ctx->pset, cursor);
+
+	CHECK_RESULT(commit(ctx));
+	usleep(1e6 / 60);
+
+	return ret;
+}
+
+static int run_atomictest(const char *name, uint32_t crtc_mask)
+{
+	int ret = 0;
+	uint32_t num_run = 0;
+	int fd = bs_drm_open_main_display();
+	CHECK_RESULT(fd);
+
+	gbm = gbm_create_device(fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm device");
+		ret = -1;
+		goto destroy_fd;
+	}
+
+	ret = drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
+	if (ret) {
+		bs_debug_error("failed to enable DRM_CLIENT_CAP_UNIVERSAL_PLANES");
+		ret = -1;
+		goto destroy_gbm_device;
+	}
+
+	ret = drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1);
+	if (ret) {
+		bs_debug_error("failed to enable DRM_CLIENT_CAP_ATOMIC");
+		ret = -1;
+		goto destroy_gbm_device;
+	}
+
+	struct atomictest_context *ctx = query_kms(fd);
+	if (!ctx) {
+		bs_debug_error("querying atomictest failed.");
+		ret = -1;
+		goto destroy_gbm_device;
+	}
+
+	struct atomictest_crtc *crtc;
+	for (uint32_t crtc_index = 0; crtc_index < ctx->num_crtcs; crtc_index++) {
+		crtc = &ctx->crtcs[crtc_index];
+		if (!((1 << crtc_index) & crtc_mask))
+			continue;
+
+		for (uint32_t i = 0; i < BS_ARRAY_LEN(cases); i++) {
+			if (strcmp(cases[i].name, name) && strcmp("all", name))
+				continue;
+
+			num_run++;
+			ret = enable_crtc(ctx, crtc);
+			if (ret)
+				goto out;
+
+			ret = run_testcase(ctx, crtc, cases[i].test_func);
+			if (ret)
+				goto out;
+
+			ret = disable_crtc(ctx, crtc);
+			if (ret)
+				goto out;
+		}
+	}
+
+	ret = (num_run == 0);
+
+out:
+	free_context(ctx);
+destroy_gbm_device:
+	gbm_device_destroy(gbm);
+destroy_fd:
+	close(fd);
+
+	return ret;
+}
+
+static const struct option longopts[] = {
+	{ "crtc", required_argument, NULL, 'c' },
+	{ "test_name", required_argument, NULL, 't' },
+	{ "help", no_argument, NULL, 'h' },
+	{ 0, 0, 0, 0 },
+};
+
+static void print_help(const char *argv0)
+{
+	printf("usage: %s -t <test_name> -c <crtc_index>\n", argv0);
+	printf("A valid name test is one the following:\n");
+	for (uint32_t i = 0; i < BS_ARRAY_LEN(cases); i++)
+		printf("%s\n", cases[i].name);
+	printf("all\n");
+}
+
+int main(int argc, char **argv)
+{
+	int c;
+	char *name = NULL;
+	int32_t crtc_idx = -1;
+	uint32_t crtc_mask = ~0;
+	while ((c = getopt_long(argc, argv, "c:t:h", longopts, NULL)) != -1) {
+		switch (c) {
+			case 'c':
+				if (sscanf(optarg, "%d", &crtc_idx) != 1)
+					goto print;
+				break;
+			case 't':
+				if (name) {
+					free(name);
+					name = NULL;
+				}
+
+				name = strdup(optarg);
+				break;
+			case 'h':
+				goto print;
+			default:
+				goto print;
+		}
+	}
+
+	if (!name)
+		goto print;
+
+	if (crtc_idx >= 0)
+		crtc_mask = 1 << crtc_idx;
+
+	int ret = run_atomictest(name, crtc_mask);
+	if (ret == 0)
+		printf("[  PASSED  ] atomictest.%s\n", name);
+	else if (ret < 0)
+		printf("[  FAILED  ] atomictest.%s\n", name);
+
+	free(name);
+
+	if (ret > 0)
+		goto print;
+
+	return ret;
+
+print:
+	print_help(argv[0]);
+	return 0;
+}
diff --git a/drm-tests/bsdrm/README.md b/drm-tests/bsdrm/README.md
new file mode 100644
index 0000000..887ca2a
--- /dev/null
+++ b/drm-tests/bsdrm/README.md
@@ -0,0 +1,88 @@
+BS DRM
+===
+
+BS DRM is a library to factor out most of the boilerplate found in programs
+written against DRM.
+
+Dependencies
+---
+- Clang or GCC with `-std=gnu99`
+- `libgbm`
+- `libdrm`
+
+Features
+---
+- Opens DRM devices
+- Creates display pipelines
+- Allocates buffer objects with framebuffers
+- Dumb maps buffer objects
+
+Goals
+---
+(in order of priority)
+
+- Useful
+- Consistent and guessable API
+- Easy to read
+- Code is documentation
+- 0 warnings from Clang or GCC
+- Make errors easy to debug
+- Fun
+
+Code Philosophy
+---
+- This library is meant to be consumed by applications, not other libraries
+- Applications usually can't recover from errors
+- Corollary: returning error codes to applications is a waste of programmer time
+- Badly behaving applications should fail fast
+- Asserts fail fast
+- Use asserts to make bad applications fail fast
+- Make code style consistent with clang-format
+
+Naming Rules
+---
+- File names: <module name>.c
+- Functions: start with bs\_<name of file with extension>\_
+- Constructor functions: suffix with "new"
+- Destructor functions: suffix with "destroy"
+- Conversion functions: suffix with "create"
+- Functions that overload: put overload name at the very end
+
+Build Example
+---
+Define `CC` `CFLAGS` and `LDFLAGS` for your device. Then run the following from the top level directory:
+```
+${CC} ${CFLAGS} -c src/drm_fb.c -o drm_fb.o || exit 1
+${CC} ${CFLAGS} -c src/drm_open.c -o drm_open.o || exit 1
+${CC} ${CFLAGS} -c src/drm_pipe.c -o drm_pipe.o || exit 1
+${CC} ${CFLAGS} -c src/pipe.c -o pipe.o || exit 1
+${CC} ${CFLAGS} -c example/stripe.c -o stripe.o || exit 1
+${CC} ${LDFLAGS} \
+    pipe.o \
+    drm_pipe.o \
+    drm_fb.o \
+    drm_open.o \
+    stripe.o -o bstest || exit 1
+```
+
+Clang Format Example
+---
+If you want to format `gamma_test.c`, first go to the drm-tests source directory and then run:
+
+```
+clang-format -style=file -i gamma_test.c
+```
+
+The `-style=file` argument will cause clang-format to search recursively upwards for the `.clang-format` style file, which is in drm-tests.
+
+Upcoming Features
+---
+- Vsync
+- Atomic Commit
+- Multi-card and multi-monitor
+- Keyboard/Mouse input for debugging
+- Dump buffer output for debugging
+
+What does BS stand for?
+---
+BS does't stand for anything.
diff --git a/drm-tests/bsdrm/example/module.mk b/drm-tests/bsdrm/example/module.mk
new file mode 100644
index 0000000..9d58d5d
--- /dev/null
+++ b/drm-tests/bsdrm/example/module.mk
@@ -0,0 +1,8 @@
+# Copyright 2016 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+include common.mk
+
+CC_BINARY(stripe): bsdrm/example/stripe.o \
+  CC_STATIC_LIBRARY(libbsdrm.pic.a)
diff --git a/drm-tests/bsdrm/example/stripe.c b/drm-tests/bsdrm/example/stripe.c
new file mode 100644
index 0000000..aefc53a
--- /dev/null
+++ b/drm-tests/bsdrm/example/stripe.c
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+int main()
+{
+	drmModeConnector *connector;
+	struct bs_drm_pipe pipe = { 0 };
+	struct bs_drm_pipe_plumber *plumber = bs_drm_pipe_plumber_new();
+	bs_drm_pipe_plumber_connector_ptr(plumber, &connector);
+	if (!bs_drm_pipe_plumber_make(plumber, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		return 1;
+	}
+	bs_drm_pipe_plumber_destroy(&plumber);
+
+	int fd = pipe.fd;
+	drmModeModeInfo *mode = &connector->modes[0];
+
+	struct gbm_device *gbm = gbm_create_device(fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm");
+		return 1;
+	}
+
+	struct gbm_bo *bos[2];
+	uint32_t ids[2];
+	for (size_t fb_index = 0; fb_index < 2; fb_index++) {
+		bos[fb_index] =
+		    gbm_bo_create(gbm, mode->hdisplay, mode->vdisplay, GBM_FORMAT_XRGB8888,
+				  GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+		if (bos[fb_index] == NULL) {
+			bs_debug_error("failed to allocate frame buffer");
+			return 1;
+		}
+		ids[fb_index] = bs_drm_fb_create_gbm(bos[fb_index]);
+		if (ids[fb_index] == 0) {
+			bs_debug_error("failed to create framebuffer id");
+			return 1;
+		}
+	}
+
+	struct bs_mapper *mapper = bs_mapper_dma_buf_new();
+	if (mapper == NULL) {
+		bs_debug_error("failed to create mapper object");
+		return 1;
+	}
+
+	for (size_t frame_index = 0; frame_index < 10000; frame_index++) {
+		size_t fb_index = frame_index % 2;
+		struct gbm_bo *bo = bos[fb_index];
+		size_t bo_size = gbm_bo_get_stride(bo) * mode->vdisplay;
+		void *map_data;
+		char *ptr = bs_mapper_map(mapper, bo, 0, &map_data);
+		if (ptr == MAP_FAILED) {
+			bs_debug_error("failed to mmap gbm buffer object");
+			return 1;
+		}
+
+		for (size_t i = 0; i < bo_size / 4; i++) {
+			ptr[i * 4 + 0] = (i + frame_index * 50) % 256;
+			ptr[i * 4 + 1] = (i + frame_index * 50 + 85) % 256;
+			ptr[i * 4 + 2] = (i + frame_index * 50 + 170) % 256;
+			ptr[i * 4 + 3] = 0;
+		}
+		bs_mapper_unmap(mapper, bo, map_data);
+
+		int ret = drmModeSetCrtc(fd, pipe.crtc_id, ids[fb_index], 0 /* x */, 0 /* y */,
+					 &pipe.connector_id, 1 /* connector count */, mode);
+		if (ret) {
+			bs_debug_error("failed to set crtc: %d", ret);
+			return 1;
+		}
+		usleep(16667);
+	}
+
+	bs_mapper_destroy(mapper);
+
+	for (size_t fb_index = 0; fb_index < 2; fb_index++) {
+		gbm_bo_destroy(bos[fb_index]);
+		drmModeRmFB(fd, ids[fb_index]);
+	}
+
+	gbm_device_destroy(gbm);
+
+	drmModeFreeConnector(connector);
+	close(fd);
+
+	return 0;
+}
diff --git a/drm-tests/bsdrm/include/bs_drm.h b/drm-tests/bsdrm/include/bs_drm.h
new file mode 100644
index 0000000..53cdbd0
--- /dev/null
+++ b/drm-tests/bsdrm/include/bs_drm.h
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef __BS_DRM_H__
+#define __BS_DRM_H__
+
+#define _GNU_SOURCE
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#include <drm_fourcc.h>
+#include <gbm.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+
+#define bs_rank_skip UINT32_MAX
+
+#define BS_ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
+
+#define BS_ALIGN(a, alignment) ((a + (alignment - 1)) & ~(alignment - 1))
+
+// debug.c
+__attribute__((format(printf, 5, 6))) void bs_debug_print(const char *prefix, const char *func,
+							  const char *file, int line,
+							  const char *format, ...);
+#define bs_debug_error(...)                                                         \
+	do {                                                                        \
+		bs_debug_print("ERROR", __func__, __FILE__, __LINE__, __VA_ARGS__); \
+	} while (0)
+
+int64_t bs_debug_gettime_ns();
+
+// pipe.c
+typedef bool (*bs_make_pipe_piece)(void *context, void *out);
+
+bool bs_pipe_make(void *context, bs_make_pipe_piece *pieces, size_t piece_count, void *out_pipe,
+		  size_t pipe_size);
+
+// open.c
+
+// A return value of true causes enumeration to end immediately. fd is always
+// closed after the callback.
+typedef bool (*bs_open_enumerate_func)(void *user, int fd);
+
+// A return value of true causes the filter to return the given fd.
+typedef bool (*bs_open_filter_func)(int fd);
+
+// The fd with the lowest (magnitude) rank is returned. A fd with rank UINT32_MAX is skipped. A fd
+// with rank 0 ends the enumeration early and is returned. On a tie, the fd returned will be
+// arbitrarily chosen from the set of lowest rank fds.
+typedef uint32_t (*bs_open_rank_func)(int fd);
+
+void bs_open_enumerate(const char *format, unsigned start, unsigned end,
+		       bs_open_enumerate_func body, void *user);
+int bs_open_filtered(const char *format, unsigned start, unsigned end, bs_open_filter_func filter);
+int bs_open_ranked(const char *format, unsigned start, unsigned end, bs_open_rank_func rank);
+
+// drm_connectors.c
+#define bs_drm_connectors_any UINT32_MAX
+
+// Interleaved arrays in the layout { DRM_MODE_CONNECTOR_*, rank, DRM_MODE_CONNECTOR_*, rank, ... },
+// terminated by { ... 0, 0 }. bs_drm_connectors_any can be used in place of DRM_MODE_CONNECTOR_* to
+// match any connector. bs_rank_skip can be used in place of a rank to indicate that that connector
+// should be skipped.
+
+// Use internal connectors and fallback to any connector
+extern const uint32_t bs_drm_connectors_main_rank[];
+// Use only internal connectors
+extern const uint32_t bs_drm_connectors_internal_rank[];
+// Use only external connectors
+extern const uint32_t bs_drm_connectors_external_rank[];
+
+uint32_t bs_drm_connectors_rank(const uint32_t *ranks, uint32_t connector_type);
+
+// drm_pipe.c
+struct bs_drm_pipe {
+	int fd;  // Always owned by the user of this library
+	uint32_t connector_id;
+	uint32_t encoder_id;
+	uint32_t crtc_id;
+};
+struct bs_drm_pipe_plumber;
+
+// A class that makes pipes with certain constraints.
+struct bs_drm_pipe_plumber *bs_drm_pipe_plumber_new();
+void bs_drm_pipe_plumber_destroy(struct bs_drm_pipe_plumber **);
+// Takes ranks in the rank array format from drm_connectors.c. Lifetime of connector_ranks must
+// exceed the plumber
+void bs_drm_pipe_plumber_connector_ranks(struct bs_drm_pipe_plumber *,
+					 const uint32_t *connector_ranks);
+// crtc_mask is in the same format as drmModeEncoder.possible_crtcs
+void bs_drm_pipe_plumber_crtc_mask(struct bs_drm_pipe_plumber *, uint32_t crtc_mask);
+// Sets which card fd the plumber should use. The fd remains owned by the caller. If left unset,
+// bs_drm_pipe_plumber_make will try all available cards.
+void bs_drm_pipe_plumber_fd(struct bs_drm_pipe_plumber *, int card_fd);
+// Sets a pointer to store the chosen connector in after a succesful call to
+// bs_drm_pipe_plumber_make. It's optional, but calling drmModeGetConnector yourself can be slow.
+void bs_drm_pipe_plumber_connector_ptr(struct bs_drm_pipe_plumber *, drmModeConnector **ptr);
+// Makes the pipe based on the constraints of the plumber. Returns false if no pipe worked.
+bool bs_drm_pipe_plumber_make(struct bs_drm_pipe_plumber *, struct bs_drm_pipe *pipe);
+
+// Makes any pipe that will work for the given card fd. Returns false if no pipe worked.
+bool bs_drm_pipe_make(int fd, struct bs_drm_pipe *pipe);
+
+// drm_fb.c
+struct bs_drm_fb_builder;
+
+// A builder class used to collect drm framebuffer parameters and then build it.
+struct bs_drm_fb_builder *bs_drm_fb_builder_new();
+void bs_drm_fb_builder_destroy(struct bs_drm_fb_builder **);
+// Copies all available framebuffer parameters from the given buffer object.
+void bs_drm_fb_builder_gbm_bo(struct bs_drm_fb_builder *, struct gbm_bo *bo);
+// Sets the drm format parameter of the resulting framebuffer.
+void bs_drm_fb_builder_format(struct bs_drm_fb_builder *, uint32_t format);
+// Creates the framebuffer ID from the previously set parameters and returns it or 0 if there was a
+// failure.
+uint32_t bs_drm_fb_builder_create_fb(struct bs_drm_fb_builder *);
+
+// Creates a drm framebuffer from the given buffer object and returns the framebuffer's ID on
+// success or 0 on failure.
+uint32_t bs_drm_fb_create_gbm(struct gbm_bo *bo);
+
+// drm_open.c
+// Opens an arbitrary display's card.
+int bs_drm_open_for_display();
+// Opens the main display's card. This falls back to bs_drm_open_for_display().
+int bs_drm_open_main_display();
+int bs_drm_open_vgem();
+
+// egl.c
+struct bs_egl;
+struct bs_egl_fb;
+
+struct bs_egl *bs_egl_new();
+void bs_egl_destroy(struct bs_egl **egl);
+bool bs_egl_setup(struct bs_egl *self);
+bool bs_egl_make_current(struct bs_egl *self);
+
+EGLImageKHR bs_egl_image_create_gbm(struct bs_egl *self, struct gbm_bo *bo);
+void bs_egl_image_destroy(struct bs_egl *self, EGLImageKHR *image);
+bool bs_egl_image_flush_external(struct bs_egl *self, EGLImageKHR image);
+
+EGLSyncKHR bs_egl_create_sync(struct bs_egl *self, EGLenum type, const EGLint *attrib_list);
+EGLint bs_egl_wait_sync(struct bs_egl *self, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
+EGLBoolean bs_egl_destroy_sync(struct bs_egl *self, EGLSyncKHR sync);
+
+struct bs_egl_fb *bs_egl_fb_new(struct bs_egl *self, EGLImageKHR image);
+bool bs_egl_target_texture2D(struct bs_egl *self, EGLImageKHR image);
+bool bs_egl_has_extension(const char *extension, const char *extensions);
+void bs_egl_fb_destroy(struct bs_egl_fb **fb);
+GLuint bs_egl_fb_name(struct bs_egl_fb *self);
+
+// gl.c
+// The entry after the last valid binding should have name == NULL. The binding array is terminated
+// by a NULL name.
+struct bs_gl_program_create_binding {
+	// These parameters are passed to glBindAttribLocation
+	GLuint index;
+	const GLchar *name;
+};
+
+GLuint bs_gl_shader_create(GLenum type, const GLchar *src);
+// bindings can be NULL.
+GLuint bs_gl_program_create_vert_frag_bind(const GLchar *vert_src, const GLchar *frag_src,
+					   struct bs_gl_program_create_binding *bindings);
+
+// app.c
+struct bs_app;
+
+struct bs_app *bs_app_new();
+void bs_app_destroy(struct bs_app **app);
+int bs_app_fd(struct bs_app *self);
+size_t bs_app_fb_count(struct bs_app *self);
+void bs_app_set_fb_count(struct bs_app *self, size_t fb_count);
+struct gbm_bo *bs_app_fb_bo(struct bs_app *self, size_t index);
+uint32_t bs_app_fb_id(struct bs_app *self, size_t index);
+bool bs_app_setup(struct bs_app *self);
+int bs_app_display_fb(struct bs_app *self, size_t index);
+
+// mmap.c
+struct bs_mapper;
+struct bs_mapper *bs_mapper_dma_buf_new();
+struct bs_mapper *bs_mapper_gem_new();
+struct bs_mapper *bs_mapper_dumb_new(int device_fd);
+void bs_mapper_destroy(struct bs_mapper *mapper);
+void *bs_mapper_map(struct bs_mapper *mapper, struct gbm_bo *bo, size_t plane, void **map_data);
+void bs_mapper_unmap(struct bs_mapper *mapper, struct gbm_bo *bo, void *map_data);
+
+// draw.c
+
+struct bs_draw_format;
+
+bool bs_draw_stripe(struct bs_mapper *mapper, struct gbm_bo *bo,
+		    const struct bs_draw_format *format);
+bool bs_draw_ellipse(struct bs_mapper *mapper, struct gbm_bo *bo,
+		     const struct bs_draw_format *format, float progress);
+bool bs_draw_cursor(struct bs_mapper *mapper, struct gbm_bo *bo,
+		    const struct bs_draw_format *format);
+bool bs_draw_lines(struct bs_mapper *mapper, struct gbm_bo *bo,
+		   const struct bs_draw_format *format);
+const struct bs_draw_format *bs_get_draw_format(uint32_t pixel_format);
+const struct bs_draw_format *bs_get_draw_format_from_name(const char *str);
+uint32_t bs_get_pixel_format(const struct bs_draw_format *format);
+const char *bs_get_format_name(const struct bs_draw_format *format);
+bool bs_parse_draw_format(const char *str, const struct bs_draw_format **format);
+
+#endif
diff --git a/drm-tests/bsdrm/module.mk b/drm-tests/bsdrm/module.mk
new file mode 100644
index 0000000..7e2fa4d
--- /dev/null
+++ b/drm-tests/bsdrm/module.mk
@@ -0,0 +1,5 @@
+# Copyright 2016 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+include common.mk
diff --git a/drm-tests/bsdrm/src/app.c b/drm-tests/bsdrm/src/app.c
new file mode 100644
index 0000000..c7f28bf
--- /dev/null
+++ b/drm-tests/bsdrm/src/app.c
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+struct bs_app_fb {
+	struct gbm_bo *bo;
+	uint32_t id;
+};
+
+struct bs_app {
+	bool setup;
+
+	int fd;
+	struct gbm_device *gbm;
+
+	uint32_t connector_id, crtc_id;
+	drmModeModeInfo mode;
+
+	uint32_t fb_format;
+	size_t fb_count;
+	struct bs_app_fb *fbs;
+};
+
+struct bs_app *bs_app_new()
+{
+	struct bs_app *self = calloc(1, sizeof(struct bs_app));
+	assert(self);
+	self->fd = -1;
+	self->fb_format = GBM_FORMAT_XRGB8888;
+	self->fb_count = 2;
+	return self;
+}
+
+void bs_app_destroy(struct bs_app **app)
+{
+	assert(app);
+	struct bs_app *self = *app;
+	assert(self);
+
+	if (self->setup) {
+		if (self->fbs) {
+			for (size_t fb_index = 0; fb_index < self->fb_count; fb_index++) {
+				struct bs_app_fb *fb = &self->fbs[fb_index];
+				gbm_bo_destroy(fb->bo);
+				if (fb->id)
+					drmModeRmFB(self->fd, fb->id);
+			}
+			free(self->fbs);
+		}
+
+		if (self->gbm) {
+			gbm_device_destroy(self->gbm);
+			self->gbm = NULL;
+		}
+
+		if (self->fd >= 0) {
+			close(self->fd);
+			self->fd = -1;
+		}
+	}
+
+	free(self);
+	*app = NULL;
+}
+
+int bs_app_fd(struct bs_app *self)
+{
+	assert(self);
+	assert(self->setup);
+	return self->fd;
+}
+size_t bs_app_fb_count(struct bs_app *self)
+{
+	assert(self);
+	return self->fb_count;
+}
+
+void bs_app_set_fb_count(struct bs_app *self, size_t fb_count)
+{
+	assert(self);
+	assert(!self->setup);
+	assert(fb_count > 0);
+	self->fb_count = fb_count;
+}
+
+struct gbm_bo *bs_app_fb_bo(struct bs_app *self, size_t index)
+{
+	assert(self);
+	assert(self->fbs);
+	assert(index < self->fb_count);
+	return self->fbs[index].bo;
+}
+
+uint32_t bs_app_fb_id(struct bs_app *self, size_t index)
+{
+	assert(self);
+	assert(self->fbs);
+	assert(index < self->fb_count);
+	return self->fbs[index].id;
+}
+
+bool bs_app_setup(struct bs_app *self)
+{
+	assert(self);
+	assert(!self->setup);
+	assert(self->fb_count > 0);
+
+	self->fd = bs_drm_open_main_display();
+	if (self->fd < 0) {
+		bs_debug_error("failed to open card for display");
+		return false;
+	}
+
+	self->gbm = gbm_create_device(self->fd);
+	if (!self->gbm) {
+		bs_debug_error("failed to create gbm");
+		goto close_fd;
+	}
+
+	struct bs_drm_pipe pipe = { 0 };
+	if (!bs_drm_pipe_make(self->fd, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		goto destroy_device;
+	}
+	self->crtc_id = pipe.crtc_id;
+	self->connector_id = pipe.connector_id;
+
+	drmModeConnector *connector = drmModeGetConnector(self->fd, pipe.connector_id);
+	if (!connector) {
+		bs_debug_error("failed to get connector %u", pipe.connector_id);
+		goto destroy_device;
+	}
+
+	self->mode = connector->modes[0];
+	drmModeFreeConnector(connector);
+
+	self->fbs = calloc(self->fb_count, sizeof(self->fbs[0]));
+	assert(self->fbs);
+	for (size_t fb_index = 0; fb_index < self->fb_count; fb_index++) {
+		struct bs_app_fb *fb = &self->fbs[fb_index];
+		fb->bo = gbm_bo_create(self->gbm, self->mode.hdisplay, self->mode.vdisplay,
+				       self->fb_format, GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+		if (fb->bo == NULL) {
+			bs_debug_error("failed to allocate framebuffer %zu of %zu", fb_index + 1,
+				       self->fb_count);
+			goto destroy_fbs;
+		}
+		fb->id = bs_drm_fb_create_gbm(fb->bo);
+		if (fb->id == 0) {
+			bs_debug_error("failed to create framebuffer id %zu of %zu", fb_index + 1,
+				       self->fb_count);
+			goto destroy_fbs;
+		}
+	}
+
+	self->setup = true;
+	return true;
+
+destroy_fbs:
+	for (size_t fb_index = 0; fb_index < self->fb_count; fb_index++) {
+		struct bs_app_fb *fb = &self->fbs[fb_index];
+		if (fb->id != 0)
+			drmModeRmFB(self->fd, fb->id);
+		if (fb->bo)
+			gbm_bo_destroy(fb->bo);
+	}
+	free(self->fbs);
+	self->fbs = NULL;
+
+destroy_device:
+	gbm_device_destroy(self->gbm);
+	self->gbm = NULL;
+
+close_fd:
+	close(self->fd);
+	self->fd = -1;
+	return false;
+}
+
+int bs_app_display_fb(struct bs_app *self, size_t index)
+{
+	assert(self);
+	assert(self->fd >= 0);
+	assert(self->fbs);
+	assert(index < self->fb_count);
+	return drmModeSetCrtc(self->fd, self->crtc_id, self->fbs[index].id, 0 /* x */, 0 /* y */,
+			      &self->connector_id, 1 /* connector count */, &self->mode);
+}
diff --git a/drm-tests/bsdrm/src/debug.c b/drm-tests/bsdrm/src/debug.c
new file mode 100644
index 0000000..4a2cd2d
--- /dev/null
+++ b/drm-tests/bsdrm/src/debug.c
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+void bs_debug_print(const char *prefix, const char *func, const char *file, int line,
+		    const char *format, ...)
+{
+	va_list args;
+	va_start(args, format);
+	fprintf(stderr, "%s:%s():%s:%d:", prefix, func, basename(file), line);
+	vfprintf(stderr, format, args);
+	fprintf(stderr, "\n");
+	va_end(args);
+}
+
+int64_t bs_debug_gettime_ns()
+{
+	struct timespec t;
+	int ret = clock_gettime(CLOCK_MONOTONIC, &t);
+	if (ret)
+		return -1;
+	const int64_t billion = 1000000000;
+	return (int64_t)t.tv_nsec + (int64_t)t.tv_sec * billion;
+}
diff --git a/drm-tests/bsdrm/src/draw.c b/drm-tests/bsdrm/src/draw.c
new file mode 100644
index 0000000..93589d2
--- /dev/null
+++ b/drm-tests/bsdrm/src/draw.c
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2017 The Chromium OS Authors. All rights reserved.
+
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+struct draw_format_component {
+	float rgb_coeffs[3];
+	float value_offset;
+	uint32_t horizontal_subsample_rate;
+	uint32_t vertical_subsample_rate;
+	uint32_t byte_skip;
+	uint32_t plane_index;
+	uint32_t plane_offset;
+};
+
+#define MAX_COMPONENTS 4
+struct bs_draw_format {
+	uint32_t pixel_format;
+	const char *name;
+	size_t component_count;
+	struct draw_format_component components[MAX_COMPONENTS];
+};
+
+struct draw_data {
+	uint32_t x;
+	uint32_t y;
+	uint32_t w;
+	uint32_t h;
+	float progress;
+	uint8_t out_color[MAX_COMPONENTS];
+};
+
+struct draw_data_lines {
+	struct draw_data base;
+	bool color_olive;
+	uint32_t interval;
+	uint32_t stop;
+};
+
+typedef void (*compute_color_t)(struct draw_data *data);
+
+#define PIXEL_FORMAT_AND_NAME(x) GBM_FORMAT_##x, #x
+static const struct bs_draw_format bs_draw_formats[] = {
+	{
+	    PIXEL_FORMAT_AND_NAME(ABGR8888),
+	    4,
+	    {
+		{ { 1.0f, 0.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 0 },
+		{ { 0.0f, 1.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 1 },
+		{ { 0.0f, 0.0f, 1.0f }, 0.0f, 1, 1, 4, 0, 2 },
+		{ { 0.0f, 0.0f, 0.0f }, 255.0f, 1, 1, 4, 0, 3 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(ARGB8888),
+	    4,
+	    {
+		{ { 0.0f, 0.0f, 1.0f }, 0.0f, 1, 1, 4, 0, 0 },
+		{ { 0.0f, 1.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 1 },
+		{ { 1.0f, 0.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 2 },
+		{ { 0.0f, 0.0f, 0.0f }, 255.0f, 1, 1, 4, 0, 3 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(BGR888),
+	    3,
+	    {
+		{ { 1.0f, 0.0f, 0.0f }, 0.0f, 1, 1, 3, 0, 0 },
+		{ { 0.0f, 1.0f, 0.0f }, 0.0f, 1, 1, 3, 0, 1 },
+		{ { 0.0f, 0.0f, 1.0f }, 0.0f, 1, 1, 3, 0, 2 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(NV12),
+	    3,
+	    {
+		{ { 0.2567890625f, 0.50412890625f, 0.09790625f }, 16.0f, 1, 1, 1, 0, 0 },
+		{ { -0.14822265625f, -0.2909921875f, 0.43921484375f }, 128.0f, 2, 2, 2, 1, 0 },
+		{ { 0.43921484375f, -0.3677890625f, -0.07142578125f }, 128.0f, 2, 2, 2, 1, 1 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(NV21),
+	    3,
+	    {
+		{ { 0.2567890625f, 0.50412890625f, 0.09790625f }, 16.0f, 1, 1, 1, 0, 0 },
+		{ { 0.43921484375f, -0.3677890625f, -0.07142578125f }, 128.0f, 2, 2, 2, 1, 0 },
+		{ { -0.14822265625f, -0.2909921875f, 0.43921484375f }, 128.0f, 2, 2, 2, 1, 1 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(RGB888),
+	    3,
+	    {
+		{ { 0.0f, 0.0f, 1.0f }, 0.0f, 1, 1, 3, 0, 0 },
+		{ { 0.0f, 1.0f, 0.0f }, 0.0f, 1, 1, 3, 0, 1 },
+		{ { 1.0f, 0.0f, 0.0f }, 0.0f, 1, 1, 3, 0, 2 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(XBGR8888),
+	    3,
+	    {
+		{ { 1.0f, 0.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 0 },
+		{ { 0.0f, 1.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 1 },
+		{ { 0.0f, 0.0f, 1.0f }, 0.0f, 1, 1, 4, 0, 2 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(XRGB8888),
+	    3,
+	    {
+		{ { 0.0f, 0.0f, 1.0f }, 0.0f, 1, 1, 4, 0, 0 },
+		{ { 0.0f, 1.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 1 },
+		{ { 1.0f, 0.0f, 0.0f }, 0.0f, 1, 1, 4, 0, 2 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(UYVY),
+	    3,
+	    {
+		{ { -0.14822265625f, -0.2909921875f, 0.43921484375f }, 128.0f, 2, 1, 4, 0, 0 },
+		{ { 0.2567890625f, 0.50412890625f, 0.09790625f }, 16.0f, 1, 1, 2, 0, 1 },
+		{ { 0.43921484375f, -0.3677890625f, -0.07142578125f }, 128.0f, 2, 1, 4, 0, 2 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(YUYV),
+	    3,
+	    {
+		{ { 0.2567890625f, 0.50412890625f, 0.09790625f }, 16.0f, 1, 1, 2, 0, 0 },
+		{ { -0.14822265625f, -0.2909921875f, 0.43921484375f }, 128.0f, 2, 1, 4, 0, 1 },
+		{ { 0.43921484375f, -0.3677890625f, -0.07142578125f }, 128.0f, 2, 1, 4, 0, 3 },
+	    },
+	},
+	{
+	    PIXEL_FORMAT_AND_NAME(YVU420),
+	    3,
+	    {
+		{ { 0.2567890625f, 0.50412890625f, 0.09790625f }, 16.0f, 1, 1, 1, 0, 0 },
+		{ { 0.43921484375f, -0.3677890625f, -0.07142578125f }, 128.0f, 2, 2, 1, 1, 0 },
+		{ { -0.14822265625f, -0.2909921875f, 0.43921484375f }, 128.0f, 2, 2, 1, 2, 0 },
+	    },
+	},
+};
+
+struct draw_plane {
+	uint32_t row_stride;
+	uint8_t *ptr;
+	void *map_data;
+};
+
+static uint8_t clampbyte(float f)
+{
+	if (f >= 255.0f)
+		return 255;
+	if (f <= 0.0f)
+		return 0;
+	return (uint8_t)f;
+}
+
+uint8_t static convert_color(const struct draw_format_component *comp, uint8_t r, uint8_t g,
+			     uint8_t b)
+{
+	return clampbyte(comp->value_offset + r * comp->rgb_coeffs[0] + g * comp->rgb_coeffs[1] +
+			 b * comp->rgb_coeffs[2]);
+}
+
+static void unmmap_planes(struct bs_mapper *mapper, struct gbm_bo *bo, size_t num_planes,
+			  struct draw_plane *planes)
+{
+	for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++)
+		bs_mapper_unmap(mapper, bo, planes[plane_index].map_data);
+}
+
+static size_t mmap_planes(struct bs_mapper *mapper, struct gbm_bo *bo,
+			  struct draw_plane planes[GBM_MAX_PLANES])
+{
+	size_t num_planes = gbm_bo_get_num_planes(bo);
+	for (size_t plane_index = 0; plane_index < num_planes; plane_index++) {
+		struct draw_plane *plane = &planes[plane_index];
+		plane->row_stride = gbm_bo_get_plane_stride(bo, plane_index);
+		plane->ptr = bs_mapper_map(mapper, bo, plane_index, &plane->map_data);
+		if (plane->ptr == MAP_FAILED) {
+			bs_debug_error("failed to mmap plane %zu of buffer object", plane_index);
+			unmmap_planes(mapper, bo, plane_index, planes);
+			return 0;
+		}
+	}
+
+	return num_planes;
+}
+
+static bool draw_color(struct bs_mapper *mapper, struct gbm_bo *bo,
+		       const struct bs_draw_format *format, struct draw_data *data,
+		       compute_color_t compute_color_fn)
+{
+	uint8_t *ptr, *converted_colors[MAX_COMPONENTS];
+	struct draw_plane planes[GBM_MAX_PLANES];
+	uint32_t height = data->h = gbm_bo_get_height(bo);
+	uint32_t width = data->w = gbm_bo_get_width(bo);
+
+	size_t num_planes = mmap_planes(mapper, bo, planes);
+	if (num_planes == 0) {
+		bs_debug_error("failed to prepare to draw pattern to buffer object");
+		return false;
+	}
+
+	for (size_t comp_index = 0; comp_index < format->component_count; comp_index++) {
+		converted_colors[comp_index] = calloc(width * height, sizeof(uint8_t));
+		assert(converted_colors[comp_index]);
+	}
+
+	for (uint32_t y = 0; y < height; y++) {
+		data->y = y;
+		for (uint32_t x = 0; x < width; x++) {
+			data->x = x;
+			compute_color_fn(data);
+			for (size_t comp_index = 0; comp_index < format->component_count;
+			     comp_index++) {
+				const struct draw_format_component *comp =
+				    &format->components[comp_index];
+				ptr = converted_colors[comp_index] + width * y + x;
+				*ptr = convert_color(comp, data->out_color[2], data->out_color[1],
+						     data->out_color[0]);
+			}
+		}
+	}
+
+	uint32_t color, samples, offset;
+	uint8_t *rows[MAX_COMPONENTS] = { 0 };
+	for (size_t comp_index = 0; comp_index < format->component_count; comp_index++) {
+		const struct draw_format_component *comp = &format->components[comp_index];
+		struct draw_plane *plane = &planes[comp->plane_index];
+		for (uint32_t y = 0; y < height / comp->vertical_subsample_rate; y++) {
+			rows[comp_index] = plane->ptr + comp->plane_offset + plane->row_stride * y;
+			for (uint32_t x = 0; x < width / comp->horizontal_subsample_rate; x++) {
+				offset = color = samples = 0;
+				for (uint32_t j = 0; j < comp->vertical_subsample_rate; j++) {
+					offset = (y * comp->vertical_subsample_rate + j) * width +
+						 x * comp->horizontal_subsample_rate;
+					for (uint32_t i = 0; i < comp->horizontal_subsample_rate;
+					     i++) {
+						color += converted_colors[comp_index][offset];
+						samples++;
+						offset++;
+					}
+				}
+
+				*(rows[comp_index] + x * comp->byte_skip) = color / samples;
+			}
+		}
+	}
+
+	unmmap_planes(mapper, bo, num_planes, planes);
+	for (size_t comp_index = 0; comp_index < format->component_count; comp_index++) {
+		free(converted_colors[comp_index]);
+	}
+
+	return true;
+}
+
+static void compute_stripe(struct draw_data *data)
+{
+	const uint32_t striph = data->h / 4;
+	const uint32_t s = data->y / striph;
+	uint8_t r = 0, g = 0, b = 0;
+	switch (s) {
+		case 0:
+			r = g = b = 1;
+			break;
+		case 1:
+			r = 1;
+			break;
+		case 2:
+			g = 1;
+			break;
+		case 3:
+			b = 1;
+			break;
+		default:
+			r = g = b = 0;
+			break;
+	}
+
+	const float i = (float)data->x / (float)data->w * 256.0f;
+	data->out_color[0] = b * i;
+	data->out_color[1] = g * i;
+	data->out_color[2] = r * i;
+	data->out_color[3] = 0;
+}
+
+static void compute_ellipse(struct draw_data *data)
+{
+	float xratio = ((int)data->x - (int)data->w / 2) / ((float)(data->w / 2));
+	float yratio = ((int)data->y - (int)data->h / 2) / ((float)(data->h / 2));
+
+	// If a point is on or inside an ellipse, num <= 1.
+	float num = xratio * xratio + yratio * yratio;
+	uint32_t g = 255 * num;
+
+	if (g < 256) {
+		memset(data->out_color, 0, 4);
+		data->out_color[2] = 0xFF;
+		data->out_color[1] = g;
+	} else {
+		memset(data->out_color, (uint8_t)(data->progress * 255), 4);
+	}
+}
+
+static void compute_cursor(struct draw_data *data)
+{
+	// A white triangle pointing right
+	if (data->y > data->x / 2 && data->y < (data->w - data->x / 2))
+		memset(data->out_color, 0xFF, 4);
+	else
+		memset(data->out_color, 0, 4);
+}
+
+static void compute_lines(struct draw_data *data)
+{
+	struct draw_data_lines *line_data = (struct draw_data_lines *)data;
+	// horizontal stripes on first vertical half, vertical stripes on next half
+	if (line_data->base.y < (line_data->base.h / 2)) {
+		if (line_data->base.x == 0) {
+			line_data->color_olive = false;
+			line_data->interval = 5;
+			line_data->stop = line_data->base.x + line_data->interval;
+		} else if (line_data->base.x >= line_data->stop) {
+			if (line_data->color_olive)
+				line_data->interval += 5;
+
+			line_data->stop += line_data->interval;
+			line_data->color_olive = !line_data->color_olive;
+		}
+	} else {
+		if (line_data->base.y == (line_data->base.h / 2)) {
+			line_data->color_olive = false;
+			line_data->interval = 10;
+			line_data->stop = line_data->base.y + line_data->interval;
+		} else if (line_data->base.y >= line_data->stop) {
+			if (line_data->color_olive)
+				line_data->interval += 5;
+
+			line_data->stop += line_data->interval;
+			line_data->color_olive = !line_data->color_olive;
+		}
+	}
+	if (line_data->color_olive) {
+		// yellowish green color
+		line_data->base.out_color[0] = 0;
+		line_data->base.out_color[1] = 128;
+		line_data->base.out_color[2] = 128;
+		line_data->base.out_color[3] = 0;
+	} else {
+		// fuchsia
+		line_data->base.out_color[0] = 255;
+		line_data->base.out_color[1] = 0;
+		line_data->base.out_color[2] = 255;
+		line_data->base.out_color[3] = 0;
+	}
+}
+
+bool bs_draw_stripe(struct bs_mapper *mapper, struct gbm_bo *bo,
+		    const struct bs_draw_format *format)
+{
+	struct draw_data data = { 0 };
+	return draw_color(mapper, bo, format, &data, compute_stripe);
+}
+
+bool bs_draw_ellipse(struct bs_mapper *mapper, struct gbm_bo *bo,
+		     const struct bs_draw_format *format, float progress)
+{
+	struct draw_data data = { 0 };
+	data.progress = progress;
+	return draw_color(mapper, bo, format, &data, compute_ellipse);
+}
+
+bool bs_draw_cursor(struct bs_mapper *mapper, struct gbm_bo *bo,
+		    const struct bs_draw_format *format)
+{
+	struct draw_data data = { 0 };
+	return draw_color(mapper, bo, format, &data, compute_cursor);
+}
+
+bool bs_draw_lines(struct bs_mapper *mapper, struct gbm_bo *bo, const struct bs_draw_format *format)
+{
+	struct draw_data_lines line_data = { { 0 } };
+	return draw_color(mapper, bo, format, &line_data.base, compute_lines);
+}
+
+const struct bs_draw_format *bs_get_draw_format(uint32_t pixel_format)
+{
+	for (size_t format_index = 0; format_index < BS_ARRAY_LEN(bs_draw_formats);
+	     format_index++) {
+		const struct bs_draw_format *format = &bs_draw_formats[format_index];
+		if (format->pixel_format == pixel_format)
+			return format;
+	}
+
+	return NULL;
+}
+
+const struct bs_draw_format *bs_get_draw_format_from_name(const char *str)
+{
+	for (size_t format_index = 0; format_index < BS_ARRAY_LEN(bs_draw_formats);
+	     format_index++) {
+		const struct bs_draw_format *format = &bs_draw_formats[format_index];
+		if (!strcmp(str, format->name))
+			return format;
+	}
+
+	return NULL;
+}
+
+uint32_t bs_get_pixel_format(const struct bs_draw_format *format)
+{
+	assert(format);
+	return format->pixel_format;
+}
+
+const char *bs_get_format_name(const struct bs_draw_format *format)
+{
+	assert(format);
+	return format->name;
+}
+
+bool bs_parse_draw_format(const char *str, const struct bs_draw_format **format)
+{
+	if (strlen(str) == 4) {
+		const struct bs_draw_format *bs_draw_format = bs_get_draw_format(*(uint32_t *)str);
+		if (bs_draw_format) {
+			*format = bs_draw_format;
+			return true;
+		}
+	} else {
+		const struct bs_draw_format *bs_draw_format = bs_get_draw_format_from_name(str);
+		if (bs_draw_format) {
+			*format = bs_draw_format;
+			return true;
+		}
+	}
+
+	bs_debug_error("format %s is not recognized\n", str);
+	return false;
+}
diff --git a/drm-tests/bsdrm/src/drm_connectors.c b/drm-tests/bsdrm/src/drm_connectors.c
new file mode 100644
index 0000000..a0f8ff2
--- /dev/null
+++ b/drm-tests/bsdrm/src/drm_connectors.c
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+const uint32_t bs_drm_connectors_main_rank[] = { DRM_MODE_CONNECTOR_LVDS,
+						 0x01,
+						 DRM_MODE_CONNECTOR_eDP,
+						 0x02,
+						 DRM_MODE_CONNECTOR_DSI,
+						 0x03,
+						 DRM_MODE_CONNECTOR_HDMIA,
+						 0x04,
+						 bs_drm_connectors_any,
+						 0xFF,
+						 0,
+						 0 };
+
+const uint32_t bs_drm_connectors_internal_rank[] = { DRM_MODE_CONNECTOR_LVDS,
+						     0x01,
+						     DRM_MODE_CONNECTOR_eDP,
+						     0x02,
+						     DRM_MODE_CONNECTOR_DSI,
+						     0x03,
+						     0,
+						     0 };
+
+const uint32_t bs_drm_connectors_external_rank[] = { DRM_MODE_CONNECTOR_LVDS,
+						     bs_rank_skip,
+						     DRM_MODE_CONNECTOR_eDP,
+						     bs_rank_skip,
+						     DRM_MODE_CONNECTOR_DSI,
+						     bs_rank_skip,
+						     DRM_MODE_CONNECTOR_HDMIA,
+						     0x01,
+						     bs_drm_connectors_any,
+						     0x02,
+						     0,
+						     0 };
+
+uint32_t bs_drm_connectors_rank(const uint32_t *ranks, uint32_t connector_type)
+{
+	for (size_t rank_index = 0; ranks[rank_index] != 0; rank_index += 2)
+		if (ranks[rank_index] == bs_drm_connectors_any ||
+		    connector_type == ranks[rank_index])
+			return ranks[rank_index + 1];
+	return bs_rank_skip;
+}
diff --git a/drm-tests/bsdrm/src/drm_fb.c b/drm-tests/bsdrm/src/drm_fb.c
new file mode 100644
index 0000000..93655ea
--- /dev/null
+++ b/drm-tests/bsdrm/src/drm_fb.c
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+#define MAX_PLANE_COUNT 4
+
+struct bs_drm_fb_builder {
+	int fd;
+	uint32_t width;
+	uint32_t height;
+	uint32_t format;
+	size_t plane_count;
+	uint32_t handles[MAX_PLANE_COUNT];
+	uint32_t strides[MAX_PLANE_COUNT];
+	uint32_t offsets[MAX_PLANE_COUNT];
+};
+
+void bs_drm_fb_builder_init(struct bs_drm_fb_builder *self)
+{
+	assert(self);
+	self->fd = -1;
+}
+
+struct bs_drm_fb_builder *bs_drm_fb_builder_new()
+{
+	struct bs_drm_fb_builder *self = calloc(1, sizeof(struct bs_drm_fb_builder));
+	assert(self);
+	bs_drm_fb_builder_init(self);
+	return self;
+}
+
+void bs_drm_fb_builder_destroy(struct bs_drm_fb_builder **self)
+{
+	assert(self);
+	assert(*self);
+	free(*self);
+	*self = NULL;
+}
+
+void bs_drm_fb_builder_gbm_bo(struct bs_drm_fb_builder *self, struct gbm_bo *bo)
+{
+	assert(self);
+	assert(bo);
+
+	struct gbm_device *gbm = gbm_bo_get_device(bo);
+	assert(gbm);
+
+	self->fd = gbm_device_get_fd(gbm);
+	self->width = gbm_bo_get_width(bo);
+	self->height = gbm_bo_get_height(bo);
+	self->format = gbm_bo_get_format(bo);
+	self->plane_count = gbm_bo_get_num_planes(bo);
+	if (self->plane_count > MAX_PLANE_COUNT) {
+		bs_debug_print("WARNING", __func__, __FILE__, __LINE__,
+			       "only using first %d planes out of buffer object's %zu planes",
+			       MAX_PLANE_COUNT, self->plane_count);
+		self->plane_count = MAX_PLANE_COUNT;
+	}
+
+	for (size_t plane_index = 0; plane_index < self->plane_count; plane_index++) {
+		self->handles[plane_index] = gbm_bo_get_plane_handle(bo, plane_index).u32;
+		self->strides[plane_index] = gbm_bo_get_plane_stride(bo, plane_index);
+		self->offsets[plane_index] = gbm_bo_get_plane_offset(bo, plane_index);
+	}
+}
+
+void bs_drm_fb_builder_format(struct bs_drm_fb_builder *self, uint32_t format)
+{
+	assert(self);
+	self->format = format;
+}
+
+uint32_t bs_drm_fb_builder_create_fb(struct bs_drm_fb_builder *self)
+{
+	assert(self);
+
+	if (self->fd < 0) {
+		bs_debug_error("failed to create drm fb: card fd %d is invalid", self->fd);
+		return 0;
+	}
+
+	if (self->width <= 0 || self->height <= 0) {
+		bs_debug_error("failed to create drm fb: dimensions %ux%u are invalid", self->width,
+			       self->height);
+		return 0;
+	}
+
+	if (self->format == 0) {
+		bs_debug_error("failed to create drm fb: 0 format is invalid");
+		return 0;
+	}
+
+	if (self->plane_count == 0 || self->plane_count > MAX_PLANE_COUNT) {
+		bs_debug_error("failed to create drm fb: plane count %zu is invalid\n",
+			       self->plane_count);
+		return 0;
+	}
+
+	for (size_t plane_index = self->plane_count; plane_index < MAX_PLANE_COUNT; plane_index++) {
+		self->handles[plane_index] = 0;
+		self->strides[plane_index] = 0;
+		self->offsets[plane_index] = 0;
+	}
+
+	uint32_t fb_id;
+
+	int ret = drmModeAddFB2(self->fd, self->width, self->height, self->format, self->handles,
+				self->strides, self->offsets, &fb_id, 0);
+
+	if (ret) {
+		bs_debug_error("failed to create drm fb: drmModeAddFB2 returned %d", ret);
+		return 0;
+	}
+
+	return fb_id;
+}
+
+uint32_t bs_drm_fb_create_gbm(struct gbm_bo *bo)
+{
+	assert(bo);
+
+	struct bs_drm_fb_builder builder;
+	bs_drm_fb_builder_init(&builder);
+	bs_drm_fb_builder_gbm_bo(&builder, bo);
+	uint32_t fb_id = bs_drm_fb_builder_create_fb(&builder);
+
+	if (!fb_id) {
+		bs_debug_error("failed to create framebuffer from buffer object");
+		return 0;
+	}
+
+	return fb_id;
+}
diff --git a/drm-tests/bsdrm/src/drm_open.c b/drm-tests/bsdrm/src/drm_open.c
new file mode 100644
index 0000000..c6dc2a7
--- /dev/null
+++ b/drm-tests/bsdrm/src/drm_open.c
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+static bool display_filter(int fd)
+{
+	bool has_connection = false;
+	drmModeRes *res = drmModeGetResources(fd);
+	if (!res)
+		return false;
+
+	if (res->count_crtcs == 0)
+		goto out;
+
+	for (int connector_index = 0; connector_index < res->count_connectors; connector_index++) {
+		drmModeConnector *connector =
+		    drmModeGetConnector(fd, res->connectors[connector_index]);
+		if (connector == NULL)
+			continue;
+
+		has_connection =
+		    connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0;
+		drmModeFreeConnector(connector);
+		if (has_connection)
+			break;
+	}
+
+out:
+	drmModeFreeResources(res);
+	return has_connection;
+}
+
+int bs_drm_open_for_display()
+{
+	return bs_open_filtered("/dev/dri/card%u", 0, DRM_MAX_MINOR, display_filter);
+}
+
+static bool connector_has_crtc(int fd, drmModeRes *res, drmModeConnector *connector)
+{
+	for (int encoder_index = 0; encoder_index < connector->count_encoders; encoder_index++) {
+		drmModeEncoder *encoder = drmModeGetEncoder(fd, connector->encoders[encoder_index]);
+		if (encoder == NULL)
+			continue;
+
+		uint32_t possible_crtcs = encoder->possible_crtcs;
+		drmModeFreeEncoder(encoder);
+
+		for (int crtc_index = 0; crtc_index < res->count_crtcs; crtc_index++)
+			if ((possible_crtcs & (1 << crtc_index)) != 0)
+				return true;
+	}
+
+	return false;
+}
+
+static uint32_t display_rank_connector_type(uint32_t connector_type)
+{
+	switch (connector_type) {
+		case DRM_MODE_CONNECTOR_LVDS:
+			return 0x01;
+		case DRM_MODE_CONNECTOR_eDP:
+			return 0x02;
+		case DRM_MODE_CONNECTOR_DSI:
+			return 0x03;
+	}
+	return 0xFF;
+}
+
+static uint32_t display_rank(int fd)
+{
+	drmModeRes *res = drmModeGetResources(fd);
+	if (!res)
+		return bs_rank_skip;
+
+	uint32_t best_rank = bs_rank_skip;
+	if (res->count_crtcs == 0)
+		goto out;
+
+	for (int connector_index = 0; connector_index < res->count_connectors; connector_index++) {
+		drmModeConnector *connector =
+		    drmModeGetConnector(fd, res->connectors[connector_index]);
+		if (connector == NULL)
+			continue;
+
+		bool has_connection = connector->connection == DRM_MODE_CONNECTED &&
+				      connector->count_modes > 0 &&
+				      connector_has_crtc(fd, res, connector);
+		if (!has_connection)
+			continue;
+
+		uint32_t rank = display_rank_connector_type(connector->connector_type);
+		if (best_rank > rank)
+			best_rank = rank;
+		drmModeFreeConnector(connector);
+	}
+
+out:
+	drmModeFreeResources(res);
+	return best_rank;
+}
+
+int bs_drm_open_main_display()
+{
+	return bs_open_ranked("/dev/dri/card%u", 0, DRM_MAX_MINOR, display_rank);
+}
+
+static bool vgem_filter(int fd)
+{
+	drmVersion *version = drmGetVersion(fd);
+	if (!version)
+		return false;
+
+	bool is_vgem = (strncmp("vgem", version->name, version->name_len) == 0);
+	drmFreeVersion(version);
+	return is_vgem;
+}
+
+int bs_drm_open_vgem()
+{
+	return bs_open_filtered("/dev/dri/card%u", 0, DRM_MAX_MINOR, vgem_filter);
+}
diff --git a/drm-tests/bsdrm/src/drm_pipe.c b/drm-tests/bsdrm/src/drm_pipe.c
new file mode 100644
index 0000000..653bddc
--- /dev/null
+++ b/drm-tests/bsdrm/src/drm_pipe.c
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+struct bs_drm_pipe_plumber {
+	int fd;
+	const uint32_t *connector_ranks;
+	uint32_t crtc_mask;
+	drmModeConnector **connector_ptr;
+};
+
+struct fd_connector {
+	int fd;
+	drmModeRes *res;
+	drmModeConnector *connector;
+};
+
+struct pipe_internal {
+	struct fd_connector *connector;
+	size_t next_connector_index;
+	uint32_t connector_rank;
+	uint32_t encoder_id;
+	int next_encoder_index;
+	uint32_t crtc_id;
+	int next_crtc_index;
+};
+
+struct pipe_ctx {
+	size_t connector_count;
+	struct fd_connector *connectors;
+
+	uint32_t crtc_mask;
+	const uint32_t *connector_ranks;
+
+	uint32_t best_rank;
+	size_t first_connector_index;
+};
+
+static bool pipe_piece_connector(void *c, void *p)
+{
+	struct pipe_ctx *ctx = c;
+	struct pipe_internal *pipe = p;
+
+	bool use_connector = false;
+	size_t connector_index;
+	for (connector_index = pipe->next_connector_index == 0 ? ctx->first_connector_index
+							       : pipe->next_connector_index;
+	     connector_index < ctx->connector_count; connector_index++) {
+		struct fd_connector *fd_connector = &ctx->connectors[connector_index];
+		drmModeConnector *connector = fd_connector->connector;
+		if (connector == NULL)
+			continue;
+
+		use_connector =
+		    connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0;
+		if (use_connector && ctx->connector_ranks) {
+			uint32_t rank =
+			    bs_drm_connectors_rank(ctx->connector_ranks, connector->connector_type);
+			if (rank >= ctx->best_rank)
+				use_connector = false;
+			pipe->connector_rank = rank;
+		}
+
+		if (use_connector) {
+			pipe->connector = fd_connector;
+			break;
+		}
+	}
+
+	pipe->next_connector_index = connector_index + 1;
+	return use_connector;
+}
+
+static bool pipe_piece_encoder(void *c, void *p)
+{
+	(void)c;
+	struct pipe_internal *pipe = p;
+	int fd = pipe->connector->fd;
+
+	drmModeConnector *connector = pipe->connector->connector;
+	if (connector == NULL)
+		return false;
+
+	int encoder_index = 0;
+	for (encoder_index = pipe->next_encoder_index; encoder_index < connector->count_encoders;
+	     encoder_index++) {
+		drmModeEncoder *encoder = drmModeGetEncoder(fd, connector->encoders[encoder_index]);
+		if (encoder == NULL)
+			continue;
+
+		drmModeFreeEncoder(encoder);
+
+		pipe->encoder_id = connector->encoders[encoder_index];
+
+		break;
+	}
+
+	pipe->next_encoder_index = encoder_index + 1;
+	return encoder_index < connector->count_encoders;
+}
+
+static bool pipe_piece_crtc(void *c, void *p)
+{
+	struct pipe_ctx *ctx = c;
+	struct pipe_internal *pipe = p;
+	int fd = pipe->connector->fd;
+	drmModeRes *res = pipe->connector->res;
+
+	drmModeEncoder *encoder = drmModeGetEncoder(fd, pipe->encoder_id);
+	if (encoder == NULL)
+		return false;
+
+	uint32_t possible_crtcs = encoder->possible_crtcs & ctx->crtc_mask;
+	drmModeFreeEncoder(encoder);
+
+	bool use_crtc = false;
+	int crtc_index;
+	for (crtc_index = pipe->next_crtc_index; crtc_index < res->count_crtcs; crtc_index++) {
+		use_crtc = (possible_crtcs & (1 << crtc_index));
+		if (use_crtc) {
+			pipe->crtc_id = res->crtcs[crtc_index];
+			break;
+		}
+	}
+
+	pipe->next_crtc_index = crtc_index;
+	return use_crtc;
+}
+
+static void bs_drm_pipe_plumber_init(struct bs_drm_pipe_plumber *self)
+{
+	assert(self);
+	self->fd = -1;
+	self->crtc_mask = 0xffffffff;
+}
+
+struct bs_drm_pipe_plumber *bs_drm_pipe_plumber_new()
+{
+	struct bs_drm_pipe_plumber *self = calloc(1, sizeof(struct bs_drm_pipe_plumber));
+	assert(self);
+	bs_drm_pipe_plumber_init(self);
+	return self;
+}
+
+void bs_drm_pipe_plumber_destroy(struct bs_drm_pipe_plumber **self)
+{
+	assert(self);
+	assert(*self);
+	free(*self);
+	*self = NULL;
+}
+
+void bs_drm_pipe_plumber_connector_ranks(struct bs_drm_pipe_plumber *self,
+					 const uint32_t *connector_ranks)
+{
+	assert(self);
+	self->connector_ranks = connector_ranks;
+}
+
+void bs_drm_pipe_plumber_crtc_mask(struct bs_drm_pipe_plumber *self, uint32_t crtc_mask)
+{
+	assert(self);
+	self->crtc_mask = crtc_mask;
+}
+
+void bs_drm_pipe_plumber_fd(struct bs_drm_pipe_plumber *self, int card_fd)
+{
+	assert(self);
+	self->fd = card_fd;
+}
+
+void bs_drm_pipe_plumber_connector_ptr(struct bs_drm_pipe_plumber *self, drmModeConnector **ptr)
+{
+	assert(self);
+	self->connector_ptr = ptr;
+}
+
+bool bs_drm_pipe_plumber_make(struct bs_drm_pipe_plumber *self, struct bs_drm_pipe *pipe)
+{
+	assert(self);
+	assert(pipe);
+
+	size_t connector_count = 0;
+	size_t fd_count = 0;
+	int fds[DRM_MAX_MINOR];
+	drmModeRes *fd_res[DRM_MAX_MINOR];
+	if (self->fd >= 0) {
+		fds[0] = self->fd;
+		fd_res[0] = drmModeGetResources(fds[0]);
+		if (!fd_res[0])
+			return false;
+		connector_count += fd_res[0]->count_connectors;
+		fd_count++;
+	} else {
+		for (int fd_index = 0; fd_index < DRM_MAX_MINOR; fd_index++) {
+			char *file_path = NULL;
+			int ret = asprintf(&file_path, "/dev/dri/card%d", fd_index);
+			assert(ret != -1);
+			assert(file_path);
+
+			int fd = open(file_path, O_RDWR);
+			free(file_path);
+			if (fd < 0)
+				continue;
+
+			drmModeRes *res = drmModeGetResources(fd);
+			if (!res) {
+				close(fd);
+				continue;
+			}
+			fds[fd_count] = fd;
+			fd_res[fd_count] = res;
+			connector_count += res->count_connectors;
+			fd_count++;
+		}
+	}
+
+	struct fd_connector *connectors = calloc(connector_count, sizeof(struct fd_connector));
+	assert(connectors);
+	size_t connector_index = 0;
+	for (size_t fd_index = 0; fd_index < fd_count; fd_index++) {
+		int fd = fds[fd_index];
+		drmModeRes *res = fd_res[fd_index];
+		for (int fd_conn_index = 0; fd_conn_index < res->count_connectors;
+		     fd_conn_index++) {
+			connectors[connector_index].fd = fd;
+			connectors[connector_index].res = res;
+			connectors[connector_index].connector =
+			    drmModeGetConnector(fd, res->connectors[fd_conn_index]);
+			connector_index++;
+		}
+	}
+
+	struct pipe_ctx ctx = { connector_count,       connectors,   self->crtc_mask,
+				self->connector_ranks, bs_rank_skip, 0 };
+	struct pipe_internal pipe_internal;
+	bs_make_pipe_piece pieces[] = { pipe_piece_connector, pipe_piece_encoder, pipe_piece_crtc };
+	bool success = false;
+	while (ctx.best_rank != 0) {
+		struct pipe_internal current_pipe_internal;
+		bool current_success =
+		    bs_pipe_make(&ctx, pieces, sizeof(pieces) / sizeof(pieces[0]),
+				 &current_pipe_internal, sizeof(struct pipe_internal));
+		if (!current_success)
+			break;
+
+		pipe_internal = current_pipe_internal;
+		success = true;
+		if (ctx.connector_ranks == NULL)
+			break;
+
+		ctx.best_rank = current_pipe_internal.connector_rank;
+		ctx.first_connector_index = current_pipe_internal.next_connector_index;
+	}
+
+	if (success) {
+		struct fd_connector *fd_connector = pipe_internal.connector;
+		pipe->fd = fd_connector->fd;
+		pipe->connector_id = fd_connector->connector->connector_id;
+		pipe->encoder_id = pipe_internal.encoder_id;
+		pipe->crtc_id = pipe_internal.crtc_id;
+		if (self->connector_ptr) {
+			*self->connector_ptr = fd_connector->connector;
+			fd_connector->connector = NULL;
+		}
+	}
+
+	for (size_t connector_index = 0; connector_index < connector_count; connector_index++)
+		drmModeFreeConnector(connectors[connector_index].connector);
+	free(connectors);
+
+	for (size_t fd_index = 0; fd_index < fd_count; fd_index++) {
+		int fd = fds[fd_index];
+		if (self->fd != fd && !(success && pipe->fd == fd))
+			close(fd);
+		drmModeFreeResources(fd_res[fd_index]);
+	}
+
+	return success;
+}
+
+bool bs_drm_pipe_make(int fd, struct bs_drm_pipe *pipe)
+{
+	assert(fd >= 0);
+	assert(pipe);
+
+	struct bs_drm_pipe_plumber plumber = { 0 };
+	bs_drm_pipe_plumber_init(&plumber);
+	bs_drm_pipe_plumber_fd(&plumber, fd);
+
+	return bs_drm_pipe_plumber_make(&plumber, pipe);
+}
diff --git a/drm-tests/bsdrm/src/egl.c b/drm-tests/bsdrm/src/egl.c
new file mode 100644
index 0000000..e5ce77d
--- /dev/null
+++ b/drm-tests/bsdrm/src/egl.c
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+static const char *get_egl_error();
+static const char *get_gl_framebuffer_error();
+
+struct bs_egl {
+	bool setup;
+	EGLDisplay display;
+	EGLContext ctx;
+	bool use_image_flush_external;
+	bool use_dma_buf_import_modifiers;
+
+	// Names are the original gl/egl function names with the prefix chopped off.
+	PFNEGLCREATEIMAGEKHRPROC CreateImageKHR;
+	PFNEGLDESTROYIMAGEKHRPROC DestroyImageKHR;
+	PFNEGLIMAGEFLUSHEXTERNALEXTPROC ImageFlushExternal;
+	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC EGLImageTargetTexture2DOES;
+	PFNEGLCREATESYNCKHRPROC CreateSyncKHR;
+	PFNEGLCLIENTWAITSYNCKHRPROC ClientWaitSyncKHR;
+	PFNEGLDESTROYSYNCKHRPROC DestroySyncKHR;
+};
+
+struct bs_egl_fb {
+	GLuint tex;
+	GLuint fb;
+};
+
+struct bs_egl *bs_egl_new()
+{
+	struct bs_egl *self = calloc(1, sizeof(struct bs_egl));
+	assert(self);
+	self->display = EGL_NO_DISPLAY;
+	self->ctx = EGL_NO_CONTEXT;
+	return self;
+}
+
+void bs_egl_destroy(struct bs_egl **egl)
+{
+	assert(egl);
+	struct bs_egl *self = *egl;
+	assert(self);
+
+	if (self->ctx != EGL_NO_CONTEXT) {
+		assert(self->display != EGL_NO_DISPLAY);
+		eglMakeCurrent(self->display, NULL, NULL, NULL);
+		eglDestroyContext(self->display, self->ctx);
+	}
+
+	if (self->display != EGL_NO_DISPLAY)
+		eglTerminate(self->display);
+
+	free(self);
+	*egl = NULL;
+}
+
+bool bs_egl_setup(struct bs_egl *self)
+{
+	assert(self);
+	assert(!self->setup);
+
+	self->CreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+	self->DestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+	self->ImageFlushExternal =
+	    (PFNEGLIMAGEFLUSHEXTERNALEXTPROC)eglGetProcAddress("eglImageFlushExternalEXT");
+	self->EGLImageTargetTexture2DOES =
+	    (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+	if (!self->CreateImageKHR || !self->DestroyImageKHR || !self->EGLImageTargetTexture2DOES) {
+		bs_debug_error(
+		    "eglGetProcAddress returned NULL for a required extension entry point.");
+		return false;
+	}
+
+	self->CreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
+	self->ClientWaitSyncKHR =
+	    (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
+	self->DestroySyncKHR = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
+
+	self->display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+	if (self->display == EGL_NO_DISPLAY) {
+		bs_debug_error("failed to get egl display");
+		return false;
+	}
+
+	if (!eglInitialize(self->display, NULL /* ignore version */, NULL /* ignore version */)) {
+		bs_debug_error("failed to initialize egl: %s\n", get_egl_error());
+		return false;
+	}
+
+	// Get any EGLConfig. We need one to create a context, but it isn't used to create any
+	// surfaces.
+	const EGLint config_attribs[] = { EGL_SURFACE_TYPE, EGL_DONT_CARE, EGL_RENDERABLE_TYPE,
+					  EGL_OPENGL_ES2_BIT, EGL_NONE };
+	EGLConfig egl_config;
+	EGLint num_configs;
+	if (!eglChooseConfig(self->display, config_attribs, &egl_config, 1,
+			     &num_configs /* unused but can't be null */)) {
+		bs_debug_error("eglChooseConfig() failed with error: %s", get_egl_error());
+		goto terminate_display;
+	}
+
+	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
+		bs_debug_error("failed to bind OpenGL ES: %s", get_egl_error());
+		goto terminate_display;
+	}
+
+	const EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
+
+	self->ctx = eglCreateContext(self->display, egl_config,
+				     EGL_NO_CONTEXT /* no shared context */, context_attribs);
+	if (self->ctx == EGL_NO_CONTEXT) {
+		bs_debug_error("failed to create OpenGL ES Context: %s", get_egl_error());
+		goto terminate_display;
+	}
+
+	if (!eglMakeCurrent(self->display, EGL_NO_SURFACE /* no default draw surface */,
+			    EGL_NO_SURFACE /* no default draw read */, self->ctx)) {
+		bs_debug_error("failed to make the OpenGL ES Context current: %s", get_egl_error());
+		goto destroy_context;
+	}
+
+	const char *egl_extensions = eglQueryString(self->display, EGL_EXTENSIONS);
+	if (!bs_egl_has_extension("EGL_KHR_image_base", egl_extensions)) {
+		bs_debug_error("EGL_KHR_image_base extension not supported");
+		goto destroy_context;
+	}
+	if (!bs_egl_has_extension("EGL_EXT_image_dma_buf_import", egl_extensions)) {
+		bs_debug_error("EGL_EXT_image_dma_buf_import extension not supported");
+		goto destroy_context;
+	}
+
+	if (!bs_egl_has_extension("EGL_KHR_fence_sync", egl_extensions) &&
+	    !bs_egl_has_extension("EGL_KHR_wait_sync", egl_extensions)) {
+		bs_debug_error("EGL_KHR_fence_sync and EGL_KHR_wait_sync extension not supported");
+		goto destroy_context;
+	}
+
+	if (bs_egl_has_extension("EGL_EXT_image_flush_external", egl_extensions)) {
+		if (!self->ImageFlushExternal) {
+			bs_debug_print("WARNING", __func__, __FILE__, __LINE__,
+				       "EGL_EXT_image_flush_external extension is supported, but "
+				       "eglGetProcAddress returned NULL.");
+		} else {
+			self->use_image_flush_external = true;
+		}
+	}
+	if (bs_egl_has_extension("EGL_EXT_image_dma_buf_import_modifiers", egl_extensions))
+		self->use_dma_buf_import_modifiers = true;
+
+	const char *gl_extensions = (const char *)glGetString(GL_EXTENSIONS);
+	if (!bs_egl_has_extension("GL_OES_EGL_image", gl_extensions)) {
+		bs_debug_error("GL_OES_EGL_image extension not supported");
+		goto destroy_context;
+	}
+
+	self->setup = true;
+
+	return true;
+
+destroy_context:
+	eglDestroyContext(self->display, self->ctx);
+terminate_display:
+	eglTerminate(self->display);
+	self->display = EGL_NO_DISPLAY;
+	return false;
+}
+
+bool bs_egl_make_current(struct bs_egl *self)
+{
+	assert(self);
+	assert(self->display != EGL_NO_DISPLAY);
+	assert(self->ctx != EGL_NO_CONTEXT);
+	return eglMakeCurrent(self->display, EGL_NO_SURFACE /* No default draw surface */,
+			      EGL_NO_SURFACE /* No default draw read */, self->ctx);
+}
+
+EGLImageKHR bs_egl_image_create_gbm(struct bs_egl *self, struct gbm_bo *bo)
+{
+	assert(self);
+	assert(self->CreateImageKHR);
+	assert(self->display != EGL_NO_DISPLAY);
+	assert(bo);
+
+	int fds[GBM_MAX_PLANES];
+	for (size_t plane = 0; plane < gbm_bo_get_num_planes(bo); plane++) {
+		fds[plane] = gbm_bo_get_plane_fd(bo, plane);
+		if (fds[plane] < 0) {
+			bs_debug_error("failed to get fb for bo: %d", fds[plane]);
+			return EGL_NO_IMAGE_KHR;
+		}
+	}
+
+	// When the bo has 3 planes with modifier support, it requires 39 components.
+	EGLint khr_image_attrs[39] = {
+		EGL_WIDTH,
+		gbm_bo_get_width(bo),
+		EGL_HEIGHT,
+		gbm_bo_get_height(bo),
+		EGL_LINUX_DRM_FOURCC_EXT,
+		(int)gbm_bo_get_format(bo),
+		EGL_NONE,
+	};
+
+	size_t attrs_index = 6;
+	for (size_t plane = 0; plane < gbm_bo_get_num_planes(bo); plane++) {
+		khr_image_attrs[attrs_index++] = EGL_DMA_BUF_PLANE0_FD_EXT + plane * 3;
+		khr_image_attrs[attrs_index++] = fds[plane];
+		khr_image_attrs[attrs_index++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT + plane * 3;
+		khr_image_attrs[attrs_index++] = gbm_bo_get_plane_offset(bo, plane);
+		khr_image_attrs[attrs_index++] = EGL_DMA_BUF_PLANE0_PITCH_EXT + plane * 3;
+		khr_image_attrs[attrs_index++] = gbm_bo_get_plane_stride(bo, plane);
+		if (self->use_dma_buf_import_modifiers) {
+			const uint64_t modifier = gbm_bo_get_format_modifier(bo);
+			khr_image_attrs[attrs_index++] =
+			    EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT + plane * 2;
+			khr_image_attrs[attrs_index++] = modifier & 0xfffffffful;
+			khr_image_attrs[attrs_index++] =
+			    EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT + plane * 2;
+			khr_image_attrs[attrs_index++] = modifier >> 32;
+		}
+	}
+
+	// Instead of disabling the extension completely, allow it to be used
+	// without creating EGLImages with correct attributes on Tegra.
+	// TODO(gsingh): Remove this once Tegra is end-of-life.
+	const char *egl_vendor = eglQueryString(self->display, EGL_VENDOR);
+	if (self->use_image_flush_external && strcmp(egl_vendor, "NVIDIA")) {
+		khr_image_attrs[attrs_index++] = EGL_IMAGE_EXTERNAL_FLUSH_EXT;
+		khr_image_attrs[attrs_index++] = EGL_TRUE;
+	}
+
+	khr_image_attrs[attrs_index++] = EGL_NONE;
+
+	EGLImageKHR image =
+	    self->CreateImageKHR(self->display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT,
+				 NULL /* no client buffer */, khr_image_attrs);
+
+	if (image == EGL_NO_IMAGE_KHR) {
+		bs_debug_error("failed to make image from target buffer: %s", get_egl_error());
+		return EGL_NO_IMAGE_KHR;
+	}
+
+	for (size_t plane = 0; plane < gbm_bo_get_num_planes(bo); plane++) {
+		close(fds[plane]);
+	}
+
+	return image;
+}
+
+void bs_egl_image_destroy(struct bs_egl *self, EGLImageKHR *image)
+{
+	assert(self);
+	assert(image);
+	assert(*image != EGL_NO_IMAGE_KHR);
+	assert(self->DestroyImageKHR);
+	self->DestroyImageKHR(self->display, *image);
+	*image = EGL_NO_IMAGE_KHR;
+}
+
+bool bs_egl_image_flush_external(struct bs_egl *self, EGLImageKHR image)
+{
+	assert(self);
+	assert(image != EGL_NO_IMAGE_KHR);
+	if (!self->use_image_flush_external)
+		return true;
+	const EGLAttrib attrs[] = { EGL_NONE };
+	return self->ImageFlushExternal(self->display, image, attrs);
+}
+
+struct bs_egl_fb *bs_egl_fb_new(struct bs_egl *self, EGLImageKHR image)
+{
+	assert(self);
+	assert(self->EGLImageTargetTexture2DOES);
+
+	struct bs_egl_fb *fb = calloc(1, sizeof(struct bs_egl_fb));
+	assert(fb);
+
+	glGenTextures(1, &fb->tex);
+	glBindTexture(GL_TEXTURE_2D, fb->tex);
+	self->EGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)image);
+	glBindTexture(GL_TEXTURE_2D, 0);
+
+	glGenFramebuffers(1, &fb->fb);
+	glBindFramebuffer(GL_FRAMEBUFFER, fb->fb);
+	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb->tex, 0);
+
+	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+		bs_debug_error("failed framebuffer check for created target buffer: %s",
+			       get_gl_framebuffer_error());
+		glDeleteFramebuffers(1, &fb->fb);
+		glDeleteTextures(1, &fb->tex);
+		free(fb);
+		return NULL;
+	}
+
+	return fb;
+}
+
+void bs_egl_fb_destroy(struct bs_egl_fb **fb)
+{
+	assert(fb);
+	struct bs_egl_fb *self = *fb;
+	assert(self);
+
+	glDeleteFramebuffers(1, &self->fb);
+	glDeleteTextures(1, &self->tex);
+
+	free(self);
+	*fb = NULL;
+}
+
+GLuint bs_egl_fb_name(struct bs_egl_fb *self)
+{
+	assert(self);
+	return self->fb;
+}
+
+bool bs_egl_target_texture2D(struct bs_egl *self, EGLImageKHR image)
+{
+	assert(self);
+	assert(self->EGLImageTargetTexture2DOES);
+	self->EGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)image);
+	GLint error = glGetError();
+	return (error == GL_NO_ERROR);
+}
+
+EGLSyncKHR bs_egl_create_sync(struct bs_egl *self, EGLenum type, const EGLint *attrib_list)
+{
+	return self->CreateSyncKHR(self->display, type, attrib_list);
+}
+
+EGLint bs_egl_wait_sync(struct bs_egl *self, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout)
+{
+	return self->ClientWaitSyncKHR(self->display, sync, flags, timeout);
+}
+
+EGLBoolean bs_egl_destroy_sync(struct bs_egl *self, EGLSyncKHR sync)
+{
+	return self->DestroySyncKHR(self->display, sync);
+}
+
+bool bs_egl_has_extension(const char *extension, const char *extensions)
+{
+	const char *start, *where, *terminator;
+	start = extensions;
+	for (;;) {
+		where = (char *)strstr((const char *)start, extension);
+		if (!where)
+			break;
+		terminator = where + strlen(extension);
+		if (where == start || *(where - 1) == ' ')
+			if (*terminator == ' ' || *terminator == '\0')
+				return true;
+		start = terminator;
+	}
+	return false;
+}
+
+static const char *get_egl_error()
+{
+	switch (eglGetError()) {
+		case EGL_SUCCESS:
+			return "EGL_SUCCESS";
+		case EGL_NOT_INITIALIZED:
+			return "EGL_NOT_INITIALIZED";
+		case EGL_BAD_ACCESS:
+			return "EGL_BAD_ACCESS";
+		case EGL_BAD_ALLOC:
+			return "EGL_BAD_ALLOC";
+		case EGL_BAD_ATTRIBUTE:
+			return "EGL_BAD_ATTRIBUTE";
+		case EGL_BAD_CONTEXT:
+			return "EGL_BAD_CONTEXT";
+		case EGL_BAD_CONFIG:
+			return "EGL_BAD_CONFIG";
+		case EGL_BAD_CURRENT_SURFACE:
+			return "EGL_BAD_CURRENT_SURFACE";
+		case EGL_BAD_DISPLAY:
+			return "EGL_BAD_DISPLAY";
+		case EGL_BAD_SURFACE:
+			return "EGL_BAD_SURFACE";
+		case EGL_BAD_MATCH:
+			return "EGL_BAD_MATCH";
+		case EGL_BAD_PARAMETER:
+			return "EGL_BAD_PARAMETER";
+		case EGL_BAD_NATIVE_PIXMAP:
+			return "EGL_BAD_NATIVE_PIXMAP";
+		case EGL_BAD_NATIVE_WINDOW:
+			return "EGL_BAD_NATIVE_WINDOW";
+		case EGL_CONTEXT_LOST:
+			return "EGL_CONTEXT_LOST";
+		default:
+			return "EGL_???";
+	}
+}
+
+static const char *get_gl_framebuffer_error()
+{
+	switch (glCheckFramebufferStatus(GL_FRAMEBUFFER)) {
+		case GL_FRAMEBUFFER_COMPLETE:
+			return "GL_FRAMEBUFFER_COMPLETE";
+		case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
+			return "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT";
+		case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
+			return "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT";
+		case GL_FRAMEBUFFER_UNSUPPORTED:
+			return "GL_FRAMEBUFFER_UNSUPPORTED";
+		case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
+			return "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
+		default:
+			return "GL_FRAMEBUFFER_???";
+	}
+}
diff --git a/drm-tests/bsdrm/src/gl.c b/drm-tests/bsdrm/src/gl.c
new file mode 100644
index 0000000..ebd71f8
--- /dev/null
+++ b/drm-tests/bsdrm/src/gl.c
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+GLuint bs_gl_shader_create(GLenum type, const GLchar *src)
+{
+	assert(src);
+	GLuint shader = glCreateShader(type);
+	if (!shader) {
+		bs_debug_error("failed call to glCreateShader(%d)", type);
+		return 0;
+	}
+
+	glShaderSource(shader, 1, &src, NULL);
+	glCompileShader(shader);
+
+	GLint status;
+	glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
+	if (status == GL_FALSE) {
+		GLint log_len = 0;
+		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_len);
+		char *shader_log = calloc(log_len, sizeof(char));
+		assert(shader_log);
+		glGetShaderInfoLog(shader, log_len, NULL, shader_log);
+		bs_debug_error("failed to compile shader: %s", shader_log);
+		free(shader_log);
+		glDeleteShader(shader);
+		return 0;
+	}
+
+	return shader;
+}
+
+GLuint bs_gl_program_create_vert_frag_bind(const GLchar *vert_src, const GLchar *frag_src,
+					   struct bs_gl_program_create_binding *bindings)
+{
+	assert(vert_src);
+	assert(frag_src);
+	GLuint program = glCreateProgram();
+	if (!program) {
+		bs_debug_error("failed to create program");
+		return 0;
+	}
+
+	GLuint vert_shader = bs_gl_shader_create(GL_VERTEX_SHADER, vert_src);
+	if (!vert_shader) {
+		bs_debug_error("failed to create vertex shader");
+		glDeleteProgram(program);
+		return 0;
+	}
+
+	GLuint frag_shader = bs_gl_shader_create(GL_FRAGMENT_SHADER, frag_src);
+	if (!frag_shader) {
+		bs_debug_error("failed to create fragment shader");
+		glDeleteShader(vert_shader);
+		glDeleteProgram(program);
+		return 0;
+	}
+
+	glAttachShader(program, vert_shader);
+	glAttachShader(program, frag_shader);
+	if (bindings) {
+		for (size_t binding_index = 0; bindings[binding_index].name != NULL;
+		     binding_index++) {
+			const struct bs_gl_program_create_binding *binding =
+			    &bindings[binding_index];
+			glBindAttribLocation(program, binding->index, binding->name);
+		}
+	}
+	glLinkProgram(program);
+	glDetachShader(program, vert_shader);
+	glDetachShader(program, frag_shader);
+	glDeleteShader(vert_shader);
+	glDeleteShader(frag_shader);
+
+	GLint status;
+	glGetProgramiv(program, GL_LINK_STATUS, &status);
+	if (!status) {
+		GLint log_len = 0;
+		glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_len);
+		char *program_log = calloc(log_len, sizeof(char));
+		assert(program_log);
+		glGetProgramInfoLog(program, log_len, NULL, program_log);
+		bs_debug_error("failed to link program: %s", program_log);
+		free(program_log);
+		glDeleteProgram(program);
+		return 0;
+	}
+
+	return program;
+}
diff --git a/drm-tests/bsdrm/src/mmap.c b/drm-tests/bsdrm/src/mmap.c
new file mode 100644
index 0000000..2955899
--- /dev/null
+++ b/drm-tests/bsdrm/src/mmap.c
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2017 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <linux/dma-buf.h>
+#include <sys/ioctl.h>
+
+#include "bs_drm.h"
+
+#define HANDLE_EINTR(x)                                                  \
+	({                                                               \
+		int eintr_wrapper_counter = 0;                           \
+		int eintr_wrapper_result;                                \
+		do {                                                     \
+			eintr_wrapper_result = (x);                      \
+		} while (eintr_wrapper_result == -1 && errno == EINTR && \
+			 eintr_wrapper_counter++ < 100);                 \
+		eintr_wrapper_result;                                    \
+	})
+
+struct bs_map_info {
+	size_t plane_index;
+	void *ptr;
+	void *map_data;
+};
+
+typedef void *(*bs_map_t)(struct bs_mapper *mapper, struct gbm_bo *bo, size_t plane,
+			  struct bs_map_info *info);
+typedef void (*bs_unmap_t)(struct gbm_bo *bo, struct bs_map_info *info);
+
+struct bs_mapper {
+	bs_map_t map_plane_fn;
+	bs_unmap_t unmap_plane_fn;
+	int device_fd;
+};
+
+static void *dma_buf_map(struct bs_mapper *mapper, struct gbm_bo *bo, size_t plane,
+			 struct bs_map_info *info)
+{
+	int drm_prime_fd = gbm_bo_get_plane_fd(bo, plane);
+	uint32_t handle = gbm_bo_get_plane_handle(bo, plane).u32;
+	size_t length = 0;
+
+	for (size_t p = 0; p <= plane; p++) {
+		if (gbm_bo_get_plane_handle(bo, p).u32 == handle)
+			length += gbm_bo_get_plane_size(bo, p);
+	}
+
+	void *ptr = mmap(NULL, length, (PROT_READ | PROT_WRITE), MAP_SHARED, drm_prime_fd, 0);
+	if (ptr == MAP_FAILED) {
+		bs_debug_error("dma-buf mmap returned MAP_FAILED: %d", errno);
+		ptr = MAP_FAILED;
+	} else {
+		ptr += gbm_bo_get_plane_offset(bo, plane);
+	}
+
+	struct dma_buf_sync sync_start = { 0 };
+	sync_start.flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_RW;
+	int ret = HANDLE_EINTR(ioctl(drm_prime_fd, DMA_BUF_IOCTL_SYNC, &sync_start));
+	if (ret)
+		bs_debug_error("DMA_BUF_IOCTL_SYNC failed");
+
+	close(drm_prime_fd);
+	return ptr;
+}
+
+static void dma_buf_unmap(struct gbm_bo *bo, struct bs_map_info *info)
+{
+	struct dma_buf_sync sync_end = { 0 };
+	sync_end.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_RW;
+	int drm_prime_fd = gbm_bo_get_plane_fd(bo, info->plane_index);
+	int ret = HANDLE_EINTR(ioctl(drm_prime_fd, DMA_BUF_IOCTL_SYNC, &sync_end));
+	close(drm_prime_fd);
+	if (ret)
+		bs_debug_error("DMA_BUF_IOCTL_SYNC failed");
+
+	void *addr = info->ptr;
+	addr -= gbm_bo_get_plane_offset(bo, info->plane_index);
+	ret = munmap(addr, gbm_bo_get_plane_size(bo, info->plane_index));
+	if (ret)
+		bs_debug_error("dma-buf unmap failed.");
+}
+
+static void *gem_map(struct bs_mapper *mapper, struct gbm_bo *bo, size_t plane,
+		     struct bs_map_info *info)
+{
+	uint32_t w = gbm_bo_get_width(bo);
+	uint32_t h = gbm_bo_get_height(bo);
+	uint32_t stride;
+	void *ptr =
+	    gbm_bo_map(bo, 0, 0, w, h, GBM_BO_TRANSFER_READ_WRITE, &stride, &info->map_data, plane);
+	return ptr;
+}
+
+static void gem_unmap(struct gbm_bo *bo, struct bs_map_info *info)
+{
+	gbm_bo_unmap(bo, info->map_data);
+}
+
+static void *dumb_map(struct bs_mapper *mapper, struct gbm_bo *bo, size_t plane,
+		      struct bs_map_info *info)
+{
+	if (plane) {
+		bs_debug_error("dump map supports only single plane buffer.");
+		return MAP_FAILED;
+	}
+
+	int prime_fd = gbm_bo_get_fd(bo);
+	uint32_t size = gbm_bo_get_plane_size(bo, plane);
+
+	uint32_t bo_handle;
+	int ret = drmPrimeFDToHandle(mapper->device_fd, prime_fd, &bo_handle);
+	if (ret) {
+		bs_debug_error("dump map failed.");
+		return MAP_FAILED;
+	}
+
+	struct drm_mode_map_dumb mmap_arg = { 0 };
+	mmap_arg.handle = bo_handle;
+
+	ret = drmIoctl(mapper->device_fd, DRM_IOCTL_MODE_MAP_DUMB, &mmap_arg);
+	if (ret) {
+		bs_debug_error("failed DRM_IOCTL_MODE_MAP_DUMB: %d", ret);
+		return MAP_FAILED;
+	}
+
+	if (mmap_arg.offset == 0) {
+		bs_debug_error("DRM_IOCTL_MODE_MAP_DUMB returned 0 offset");
+		return MAP_FAILED;
+	}
+
+	void *ptr = mmap(NULL, size, (PROT_READ | PROT_WRITE), MAP_SHARED, mapper->device_fd,
+			 mmap_arg.offset);
+
+	if (ptr == MAP_FAILED) {
+		bs_debug_error("mmap returned MAP_FAILED: %d", errno);
+		return MAP_FAILED;
+	}
+
+	close(prime_fd);
+	return ptr;
+}
+
+static void dumb_unmap(struct gbm_bo *bo, struct bs_map_info *info)
+{
+	int ret = munmap(info->ptr, gbm_bo_get_plane_size(bo, info->plane_index));
+	if (ret)
+		bs_debug_error("dump unmap failed.");
+}
+
+struct bs_mapper *bs_mapper_dma_buf_new()
+{
+	struct bs_mapper *mapper = calloc(1, sizeof(struct bs_mapper));
+	assert(mapper);
+	mapper->map_plane_fn = dma_buf_map;
+	mapper->unmap_plane_fn = dma_buf_unmap;
+	mapper->device_fd = -1;
+	return mapper;
+}
+
+struct bs_mapper *bs_mapper_gem_new()
+{
+	struct bs_mapper *mapper = calloc(1, sizeof(struct bs_mapper));
+	assert(mapper);
+	mapper->map_plane_fn = gem_map;
+	mapper->unmap_plane_fn = gem_unmap;
+	mapper->device_fd = -1;
+	return mapper;
+}
+
+struct bs_mapper *bs_mapper_dumb_new(int device_fd)
+{
+	assert(device_fd >= 0);
+	struct bs_mapper *mapper = calloc(1, sizeof(struct bs_mapper));
+	assert(mapper);
+	mapper->map_plane_fn = dumb_map;
+	mapper->unmap_plane_fn = dumb_unmap;
+	mapper->device_fd = dup(device_fd);
+	assert(mapper->device_fd >= 0);
+	return mapper;
+}
+
+void bs_mapper_destroy(struct bs_mapper *mapper)
+{
+	assert(mapper);
+	if (mapper->device_fd >= 0)
+		close(mapper->device_fd);
+
+	free(mapper);
+}
+
+void *bs_mapper_map(struct bs_mapper *mapper, struct gbm_bo *bo, size_t plane, void **map_data)
+{
+	assert(mapper);
+	assert(bo);
+	assert(map_data);
+	struct bs_map_info *info = calloc(1, sizeof(struct bs_map_info));
+	info->plane_index = plane;
+	void *ptr = mapper->map_plane_fn(mapper, bo, plane, info);
+	if (ptr == MAP_FAILED) {
+		free(info);
+		return MAP_FAILED;
+	}
+
+	info->ptr = ptr;
+	*map_data = info;
+	return info->ptr;
+}
+
+void bs_mapper_unmap(struct bs_mapper *mapper, struct gbm_bo *bo, void *map_data)
+{
+	struct bs_map_info *info = map_data;
+	assert(info);
+	mapper->unmap_plane_fn(bo, info);
+	free(info);
+}
diff --git a/drm-tests/bsdrm/src/module.mk b/drm-tests/bsdrm/src/module.mk
new file mode 100644
index 0000000..abdbc39
--- /dev/null
+++ b/drm-tests/bsdrm/src/module.mk
@@ -0,0 +1,21 @@
+# Copyright 2016 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+include common.mk
+
+CFLAGS += -std=gnu99 -I$(SRC)/bsdrm/include
+
+CC_STATIC_LIBRARY(libbsdrm.pic.a): \
+  bsdrm/src/app.o \
+  bsdrm/src/debug.o \
+  bsdrm/src/draw.o \
+  bsdrm/src/drm_connectors.o \
+  bsdrm/src/drm_fb.o \
+  bsdrm/src/drm_open.o \
+  bsdrm/src/drm_pipe.o \
+  bsdrm/src/egl.o \
+  bsdrm/src/gl.o \
+  bsdrm/src/mmap.o \
+  bsdrm/src/open.o \
+  bsdrm/src/pipe.o
diff --git a/drm-tests/bsdrm/src/open.c b/drm-tests/bsdrm/src/open.c
new file mode 100644
index 0000000..a7cc582
--- /dev/null
+++ b/drm-tests/bsdrm/src/open.c
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+// Suppresses warnings for our usage of asprintf with one of the parameters.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+void bs_open_enumerate(const char *format, unsigned start, unsigned end,
+		       bs_open_enumerate_func body, void *user)
+{
+	assert(end >= start);
+	for (unsigned dev_index = start; dev_index < end; dev_index++) {
+		char *file_path = NULL;
+		int ret = asprintf(&file_path, format, dev_index);
+		if (ret == -1)
+			continue;
+		assert(file_path);
+
+		int fd = open(file_path, O_RDWR);
+		free(file_path);
+		if (fd < 0)
+			continue;
+
+		bool end = body(user, fd);
+		close(fd);
+
+		if (end)
+			return;
+	}
+}
+#pragma GCC diagnostic pop
+
+struct bs_open_filtered_user {
+	bs_open_filter_func filter;
+	int fd;
+};
+
+static bool bs_open_filtered_body(void *user, int fd)
+{
+	struct bs_open_filtered_user *data = (struct bs_open_filtered_user *)user;
+	if (data->filter(fd)) {
+		data->fd = dup(fd);
+		return true;
+	}
+
+	return false;
+}
+
+int bs_open_filtered(const char *format, unsigned start, unsigned end, bs_open_filter_func filter)
+{
+	struct bs_open_filtered_user data = { filter, -1 };
+	bs_open_enumerate(format, start, end, bs_open_filtered_body, &data);
+	return data.fd;
+}
+
+struct bs_open_ranked_user {
+	bs_open_rank_func rank;
+	uint32_t rank_index;
+	int fd;
+};
+
+static bool bs_open_ranked_body(void *user, int fd)
+{
+	struct bs_open_ranked_user *data = (struct bs_open_ranked_user *)user;
+	uint32_t rank_index = data->rank(fd);
+
+	if (data->rank_index > rank_index) {
+		data->rank_index = rank_index;
+		if (data->fd >= 0)
+			close(data->fd);
+		data->fd = dup(fd);
+	}
+
+	return rank_index == 0;
+}
+
+int bs_open_ranked(const char *format, unsigned start, unsigned end, bs_open_rank_func rank)
+{
+	struct bs_open_ranked_user data = { rank, UINT32_MAX, -1 };
+	bs_open_enumerate(format, start, end, bs_open_ranked_body, &data);
+	return data.fd;
+}
diff --git a/drm-tests/bsdrm/src/pipe.c b/drm-tests/bsdrm/src/pipe.c
new file mode 100644
index 0000000..047eac6
--- /dev/null
+++ b/drm-tests/bsdrm/src/pipe.c
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+bool bs_pipe_make(void *context, bs_make_pipe_piece *pieces, size_t piece_count, void *out_pipe,
+		  size_t pipe_size)
+{
+	bool success = false;
+	char *pipe_stack = calloc(pipe_size, piece_count + 1);
+	for (size_t i = 0; i < piece_count;) {
+		char *pipe_ptr = pipe_stack + i * pipe_size;
+		if (pieces[i](context, pipe_ptr)) {
+			i++;
+			memcpy(pipe_ptr + pipe_size, pipe_ptr, pipe_size);
+			continue;
+		}
+
+		if (i == 0)
+			goto out;
+
+		i--;
+	}
+
+	memcpy(out_pipe, pipe_stack + (piece_count - 1) * pipe_size, pipe_size);
+	success = true;
+
+out:
+	free(pipe_stack);
+	return success;
+}
diff --git a/drm-tests/common.mk b/drm-tests/common.mk
new file mode 100644
index 0000000..b8aade5
--- /dev/null
+++ b/drm-tests/common.mk
@@ -0,0 +1,844 @@
+# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# If this file is part of another source distribution, it's license may be
+# stored in LICENSE.makefile or LICENSE.common.mk.
+#
+# NOTE NOTE NOTE
+#  The authoritative common.mk is located in:
+#    https://chromium.googlesource.com/chromiumos/platform/common-mk.git
+#  Please make all changes there, then copy into place in other repos.
+# NOTE NOTE NOTE
+#
+# This file provides a common architecture for building C/C++ source trees.
+# It uses recursive makefile inclusion to create a single make process which
+# can be built in the source tree or with the build artifacts placed elsewhere.
+#
+# It is fully parallelizable for all targets, including static archives.
+#
+# To use:
+# 1. Place common.mk in your top source level
+# 2. In your top-level Makefile, place "include common.mk" at the top
+# 3. In all subdirectories, create a 'module.mk' file that starts with:
+#      include common.mk
+#    And then contains the remainder of your targets.
+# 4. All build targets should look like:
+#    relative/path/target: relative/path/obj.o
+#
+# See existing makefiles for rule examples.
+#
+# Exported macros:
+#   - cc_binary, cxx_binary provide standard compilation steps for binaries
+#   - cxx_library, cc_library provide standard compilation steps for
+#     shared objects.
+#   All of the above optionally take an argument for extra flags.
+#   - update_archive creates/updates a given .a target
+#
+# Instead of using the build macros, most users can just use wrapped targets:
+#   - CXX_BINARY, CC_BINARY, CC_STATIC_BINARY, CXX_STATIC_BINARY
+#   - CXX_LIBRARY, CC_LIBRARY, CC_STATIC_LIBRARY, CXX_STATIC_LIBRARY
+#   - E.g., CXX_BINARY(mahbinary): foo.o
+#   - object.depends targets may be used when a prerequisite is required for an
+#     object file. Because object files result in multiple build artifacts to
+#     handle PIC and PIE weirdness. E.g.
+#       foo.o.depends: generated/dbus.h
+#   - TEST(binary) or TEST(CXX_BINARY(binary)) may be used as a prerequisite
+#     for the tests target to trigger an automated test run.
+#   - CLEAN(file_or_dir) dependency can be added to 'clean'.
+#
+# If source code is being generated, rules will need to be registered for
+# compiling the objects.  This can be done by adding one of the following
+# to the Makefile:
+#   - For C source files
+#     $(eval $(call add_object_rules,sub/dir/gen_a.o sub/dir/gen_b.o,CC,c))
+#   - For C++ source files
+#     $(eval $(call add_object_rules,sub/dir/gen_a.o sub/dir/gen_b.o,CXX,cc))
+#
+# Exported targets meant to have prerequisites added to:
+#  - all - Your desired targets should be given
+#  - tests - Any TEST(test_binary) targets should be given
+#  - FORCE - force the given target to run regardless of changes
+#            In most cases, using .PHONY is preferred.
+#
+# Possible command line variables:
+#   - COLOR=[0|1] to set ANSI color output (default: 1)
+#   - VERBOSE=[0|1] to hide/show commands (default: 0)
+#   - MODE=[opt|dbg|profiling] (default: opt)
+#          opt - Enable optimizations for release builds
+#          dbg - Turn down optimization for debugging
+#          profiling - Turn off optimization and turn on profiling/coverage
+#                      support.
+#   - ARCH=[x86|arm|supported qemu name] (default: from portage or uname -m)
+#   - SPLITDEBUG=[0|1] splits debug info in target.debug (default: 0)
+#        If NOSTRIP=1, SPLITDEBUG will never strip the final emitted objects.
+#   - NOSTRIP=[0|1] determines if binaries are stripped. (default: 1)
+#        NOSTRIP=0 and MODE=opt will also drop -g from the CFLAGS.
+#   - VALGRIND=[0|1] runs tests under valgrind (default: 0)
+#   - OUT=/path/to/builddir puts all output in given path (default: $PWD)
+#   - VALGRIND_ARGS="" supplies extra memcheck arguments
+#
+# Per-target(-ish) variable:
+#   - NEEDS_ROOT=[0|1] allows a TEST() target to run with root.
+#     Default is 0 unless it is running under QEmu.
+#   - NEEDS_MOUNTS=[0|1] allows a TEST() target running on QEmu to get
+#     setup mounts in the $(SYSROOT)
+#
+# Caveats:
+# - Directories or files with spaces in them DO NOT get along with GNU Make.
+#   If you need them, all uses of dir/notdir/etc will need to have magic
+#   wrappers.  Proceed at risk to your own sanity.
+# - External CXXFLAGS and CFLAGS should be passed via the environment since
+#   this file does not use 'override' to control them.
+# - Our version of GNU Make doesn't seem to support the 'private' variable
+#   annotation, so you can't tag a variable private on a wrapping target.
+
+# Behavior configuration variables
+SPLITDEBUG ?= 0
+NOSTRIP ?= 1
+VALGRIND ?= 0
+COLOR ?= 1
+VERBOSE ?= 0
+MODE ?= opt
+ARCH ?= $(shell uname -m)
+NEEDS_ROOT = 0
+NEEDS_MOUNTS = 0
+
+# Put objects in a separate tree based on makefile locations
+# This means you can build a tree without touching it:
+#   make -C $SRCDIR  # will create ./build-$(MODE)
+# Or
+#   make -C $SRCDIR OUT=$PWD
+# This variable is extended on subdir calls and doesn't need to be re-called.
+OUT ?= $(PWD)/
+
+# Make OUT now so we can use realpath.
+$(shell mkdir -p "$(OUT)")
+
+# TODO(wad) Relative paths are resolved against SRC and not the calling dir.
+# Ensure a command-line supplied OUT has a slash
+override OUT := $(realpath $(OUT))/
+
+# SRC is not meant to be set by the end user, but during make call relocation.
+# $(PWD) != $(CURDIR) all the time.
+export SRC ?= $(CURDIR)
+
+# Re-start in the $(OUT) directory if we're not there.
+# We may be invoked using -C or bare and we need to ensure behavior
+# is consistent so we check both PWD vs OUT and PWD vs CURDIR.
+override RELOCATE_BUILD := 0
+ifneq (${PWD}/,${OUT})
+override RELOCATE_BUILD := 1
+endif
+# Make sure we're running with no builtin targets. They cause
+# leakage and mayhem!
+ifneq (${PWD},${CURDIR})
+override RELOCATE_BUILD := 1
+# If we're run from the build dir, don't let it get cleaned up later.
+ifeq (${PWD}/,${OUT})
+$(shell touch "$(PWD)/.dont_delete_on_clean")
+endif
+endif  # ifneq (${PWD},${CURDIR}
+
+# "Relocate" if we need to restart without implicit rules.
+ifeq ($(subst r,,$(MAKEFLAGS)),$(MAKEFLAGS))
+override RELOCATE_BUILD := 1
+endif
+
+ifeq (${RELOCATE_BUILD},1)
+# By default, silence build output. Reused below as well.
+QUIET = @
+ifeq ($(VERBOSE),1)
+  QUIET=
+endif
+
+# This target will override all targets, including prerequisites. To avoid
+# calling $(MAKE) once per prereq on the given CMDGOAL, we guard it with a local
+# variable.
+RUN_ONCE := 0
+MAKECMDGOALS ?= all
+# Keep the rules split as newer make does not allow them to be declared
+# on the same line.  But the way :: rules work, the _all here will also
+# invoke the %:: rule while retaining "_all" as the default.
+_all::
+%::
+	$(if $(filter 0,$(RUN_ONCE)), \
+	  cd "$(OUT)" && \
+	  $(MAKE) -r -I "$(SRC)" -f "$(CURDIR)/Makefile" \
+	    SRC="$(CURDIR)" OUT="$(OUT)" $(foreach g,$(MAKECMDGOALS),"$(g)"),)
+	$(eval RUN_ONCE := 1)
+pass-to-subcall := 1
+endif
+
+ifeq ($(pass-to-subcall),)
+
+# Only call MODULE if we're in a submodule
+MODULES_LIST := $(filter-out Makefile %.d,$(MAKEFILE_LIST))
+ifeq ($(words $(filter-out Makefile common.mk %.d $(SRC)/Makefile \
+                           $(SRC)/common.mk,$(MAKEFILE_LIST))),0)
+
+# All the top-level defines outside of module.mk.
+
+#
+# Helper macros
+#
+
+# Create the directory if it doesn't yet exist.
+define auto_mkdir
+  $(if $(wildcard $(dir $1)),$2,$(QUIET)mkdir -p "$(dir $1)")
+endef
+
+# Creates the actual archive with an index.
+# The target $@ must end with .pic.a or .pie.a.
+define update_archive
+  $(call auto_mkdir,$(TARGET_OR_MEMBER))
+  $(QUIET)# Create the archive in one step to avoid parallel use accessing it
+  $(QUIET)# before all the symbols are present.
+  @$(ECHO) "AR		$(subst \
+$(SRC)/,,$(^:.o=$(suffix $(basename $(TARGET_OR_MEMBER))).o)) \
+-> $(subst $(SRC)/,,$(TARGET_OR_MEMBER))"
+  $(QUIET)$(AR) rcs $(TARGET_OR_MEMBER) \
+          $(subst $(SRC)/,,$(^:.o=$(suffix $(basename $(TARGET_OR_MEMBER))).o))
+endef
+
+# Default compile from objects using pre-requisites but filters out
+# subdirs and .d files.
+define cc_binary
+  $(call COMPILE_BINARY_implementation,CC,$(CFLAGS) $(1),$(EXTRA_FLAGS))
+endef
+
+define cxx_binary
+  $(call COMPILE_BINARY_implementation,CXX,$(CXXFLAGS) $(1),$(EXTRA_FLAGS))
+endef
+
+# Default compile from objects using pre-requisites but filters out
+# subdirs and .d files.
+define cc_library
+  $(call COMPILE_LIBRARY_implementation,CC,$(CFLAGS) $(1),$(EXTRA_FLAGS))
+endef
+define cxx_library
+  $(call COMPILE_LIBRARY_implementation,CXX,$(CXXFLAGS) $(1),$(EXTRA_FLAGS))
+endef
+
+# Deletes files silently if they exist. Meant for use in any local
+# clean targets.
+define silent_rm
+  $(if $(wildcard $(1)),
+  $(QUIET)($(ECHO) -n '$(COLOR_RED)CLEANFILE$(COLOR_RESET)		' && \
+    $(ECHO) '$(subst $(OUT)/,,$(wildcard $(1)))' && \
+    $(RM) $(1) 2>/dev/null) || true,)
+endef
+define silent_rmdir
+  $(if $(wildcard $(1)),
+    $(if $(wildcard $(1)/*),
+  $(QUIET)# $(1) not empty [$(wildcard $(1)/*)]. Not deleting.,
+  $(QUIET)($(ECHO) -n '$(COLOR_RED)CLEANDIR$(COLOR_RESET)		' && \
+    $(ECHO) '$(subst $(OUT)/,,$(wildcard $(1)))' && \
+    $(RMDIR) $(1) 2>/dev/null) || true),)
+endef
+
+#
+# Default variable values
+#
+
+# Only override toolchain vars if they are from make.
+CROSS_COMPILE ?=
+define override_var
+ifneq ($(filter undefined default,$(origin $1)),)
+$1 = $(CROSS_COMPILE)$2
+endif
+endef
+$(eval $(call override_var,AR,ar))
+$(eval $(call override_var,CC,gcc))
+$(eval $(call override_var,CXX,g++))
+$(eval $(call override_var,OBJCOPY,objcopy))
+$(eval $(call override_var,PKG_CONFIG,pkg-config))
+$(eval $(call override_var,RANLIB,ranlib))
+$(eval $(call override_var,STRIP,strip))
+
+RMDIR ?= rmdir
+ECHO = /bin/echo -e
+
+ifeq ($(lastword $(subst /, ,$(CC))),clang)
+CDRIVER = clang
+else
+CDRIVER = gcc
+endif
+
+ifeq ($(lastword $(subst /, ,$(CXX))),clang++)
+CXXDRIVER = clang
+else
+CXXDRIVER = gcc
+endif
+
+# To update these from an including Makefile:
+#  CXXFLAGS += -mahflag  # Append to the list
+#  CXXFLAGS := -mahflag $(CXXFLAGS) # Prepend to the list
+#  CXXFLAGS := $(filter-out badflag,$(CXXFLAGS)) # Filter out a value
+# The same goes for CFLAGS.
+COMMON_CFLAGS-gcc := -fstack-protector-strong -fvisibility=internal -ggdb3 \
+  -Wa,--noexecstack
+COMMON_CFLAGS-clang := -fstack-protector-all -fvisibility=hidden -ggdb
+COMMON_CFLAGS := -Wall -Werror -fno-strict-aliasing -O1 -Wformat=2
+CXXFLAGS += $(COMMON_CFLAGS) $(COMMON_CFLAGS-$(CXXDRIVER))
+CFLAGS += $(COMMON_CFLAGS) $(COMMON_CFLAGS-$(CDRIVER))
+CPPFLAGS += -D_FORTIFY_SOURCE=2
+
+
+ifeq ($(MODE),opt)
+  # Up the optimizations.
+  CFLAGS := $(filter-out -O1,$(CFLAGS)) -O2
+  CXXFLAGS := $(filter-out -O1,$(CXXFLAGS)) -O2
+  # Only drop -g* if symbols aren't desired.
+  ifeq ($(NOSTRIP),0)
+    # TODO: do we want -fomit-frame-pointer on x86?
+    CFLAGS := $(filter-out -ggdb3,$(CFLAGS))
+    CXXFLAGS := $(filter-out -ggdb3,$(CXXFLAGS))
+  endif
+endif
+
+ifeq ($(MODE),profiling)
+  CFLAGS := $(CFLAGS) -O0 -g  --coverage
+  CXXFLAGS := $(CXXFLAGS) -O0 -g  --coverage
+  LDFLAGS := $(LDFLAGS) --coverage
+endif
+
+LDFLAGS := $(LDFLAGS) -Wl,-z,relro -Wl,-z,noexecstack -Wl,-z,now
+
+# Fancy helpers for color if a prompt is defined
+ifeq ($(COLOR),1)
+COLOR_RESET = \x1b[0m
+COLOR_GREEN = \x1b[32;01m
+COLOR_RED = \x1b[31;01m
+COLOR_YELLOW = \x1b[33;01m
+endif
+
+# By default, silence build output.
+QUIET = @
+ifeq ($(VERBOSE),1)
+  QUIET=
+endif
+
+#
+# Implementation macros for compile helpers above
+#
+
+# Useful for dealing with pie-broken toolchains.
+# Call make with PIE=0 to disable default PIE use.
+OBJ_PIE_FLAG = -fPIE
+COMPILE_PIE_FLAG = -pie
+ifeq ($(PIE),0)
+  OBJ_PIE_FLAG =
+  COMPILE_PIE_FLAG =
+endif
+
+# Favor member targets first for CXX_BINARY(%) magic.
+# And strip out nested members if possible.
+LP := (
+RP := )
+TARGET_OR_MEMBER = $(lastword $(subst $(LP), ,$(subst $(RP),,$(or $%,$@))))
+
+# Default compile from objects using pre-requisites but filters out
+# all non-.o files.
+define COMPILE_BINARY_implementation
+  @$(ECHO) "LD$(1)		$(subst $(PWD)/,,$(TARGET_OR_MEMBER))"
+  $(call auto_mkdir,$(TARGET_OR_MEMBER))
+  $(QUIET)$($(1)) $(COMPILE_PIE_FLAGS) -o $(TARGET_OR_MEMBER) \
+    $(2) $(LDFLAGS) \
+    $(filter %.o %.a,$(^:.o=.pie.o)) \
+    $(foreach so,$(filter %.so,$^),-L$(dir $(so)) \
+                            -l$(patsubst lib%,%,$(basename $(notdir $(so))))) \
+    $(LDLIBS)
+  $(call conditional_strip)
+  @$(ECHO) -n "BIN		"
+  @$(ECHO) "$(COLOR_GREEN)$(subst $(PWD)/,,$(TARGET_OR_MEMBER))$(COLOR_RESET)"
+  @$(ECHO) "	$(COLOR_YELLOW)-----$(COLOR_RESET)"
+endef
+
+# TODO: add version support extracted from PV environment variable
+#ifeq ($(PV),9999)
+#$(warning PV=$(PV). If shared object versions matter, please force PV=.)
+#endif
+# Then add -Wl,-soname,$@.$(PV) ?
+
+# Default compile from objects using pre-requisites but filters out
+# all non-.o values. (Remember to add -L$(OUT) -llib)
+COMMA := ,
+define COMPILE_LIBRARY_implementation
+  @$(ECHO) "SHARED$(1)	$(subst $(PWD)/,,$(TARGET_OR_MEMBER))"
+  $(call auto_mkdir,$(TARGET_OR_MEMBER))
+  $(QUIET)$($(1)) -shared -Wl,-E -o $(TARGET_OR_MEMBER) \
+    $(2) $(LDFLAGS) \
+    $(if $(filter %.a,$^),-Wl$(COMMA)--whole-archive,) \
+    $(filter %.o ,$(^:.o=.pic.o)) \
+    $(foreach a,$(filter %.a,$^),-L$(dir $(a)) \
+                            -l$(patsubst lib%,%,$(basename $(notdir $(a))))) \
+    $(foreach so,$(filter %.so,$^),-L$(dir $(so)) \
+                            -l$(patsubst lib%,%,$(basename $(notdir $(so))))) \
+    $(LDLIBS)
+  $(call conditional_strip)
+  @$(ECHO) -n "LIB		$(COLOR_GREEN)"
+  @$(ECHO) "$(subst $(PWD)/,,$(TARGET_OR_MEMBER))$(COLOR_RESET)"
+  @$(ECHO) "	$(COLOR_YELLOW)-----$(COLOR_RESET)"
+endef
+
+define conditional_strip
+  $(if $(filter 0,$(NOSTRIP)),$(call strip_artifact))
+endef
+
+define strip_artifact
+  @$(ECHO) "STRIP		$(subst $(OUT)/,,$(TARGET_OR_MEMBER))"
+  $(if $(filter 1,$(SPLITDEBUG)), @$(ECHO) -n "DEBUG	"; \
+    $(ECHO) "$(COLOR_YELLOW)\
+$(subst $(OUT)/,,$(TARGET_OR_MEMBER)).debug$(COLOR_RESET)")
+  $(if $(filter 1,$(SPLITDEBUG)), \
+    $(QUIET)$(OBJCOPY) --only-keep-debug "$(TARGET_OR_MEMBER)" \
+      "$(TARGET_OR_MEMBER).debug")
+  $(if $(filter-out dbg,$(MODE)),$(QUIET)$(STRIP) --strip-unneeded \
+    "$(TARGET_OR_MEMBER)",)
+endef
+
+#
+# Global pattern rules
+#
+
+# Below, the archive member syntax is abused to create fancier
+# syntactic sugar for recipe authors that avoids needed to know
+# subcall options.  The downside is that make attempts to look
+# into the phony archives for timestamps. This will cause the final
+# target to be rebuilt/linked on _every_ call to make even when nothing
+# has changed.  Until a better way presents itself, we have helpers that
+# do the stat check on make's behalf.  Dodgy but simple.
+define old_or_no_timestamp
+  $(if $(realpath $%),,$(1))
+  $(if $(shell find $^ -cnewer "$%" 2>/dev/null),$(1))
+endef
+
+define check_deps
+  $(if $(filter 0,$(words $^)),\
+    $(error Missing dependencies or declaration of $@($%)),)
+endef
+
+# Build a cxx target magically
+CXX_BINARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call cxx_binary))
+clean: CLEAN(CXX_BINARY*)
+
+CC_BINARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call cc_binary))
+clean: CLEAN(CC_BINARY*)
+
+CXX_STATIC_BINARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call cxx_binary,-static))
+clean: CLEAN(CXX_STATIC_BINARY*)
+
+CC_STATIC_BINARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call cc_binary,-static))
+clean: CLEAN(CC_STATIC_BINARY*)
+
+CXX_LIBRARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call cxx_library))
+clean: CLEAN(CXX_LIBRARY*)
+
+CXX_LIBARY(%):
+	$(error Typo alert! LIBARY != LIBRARY)
+
+CC_LIBRARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call cc_library))
+clean: CLEAN(CC_LIBRARY*)
+
+CC_LIBARY(%):
+	$(error Typo alert! LIBARY != LIBRARY)
+
+CXX_STATIC_LIBRARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call update_archive))
+clean: CLEAN(CXX_STATIC_LIBRARY*)
+
+CXX_STATIC_LIBARY(%):
+	$(error Typo alert! LIBARY != LIBRARY)
+
+CC_STATIC_LIBRARY(%):
+	$(call check_deps)
+	$(call old_or_no_timestamp,$(call update_archive))
+clean: CLEAN(CC_STATIC_LIBRARY*)
+
+CC_STATIC_LIBARY(%):
+	$(error Typo alert! LIBARY != LIBRARY)
+
+
+TEST(%): % qemu_chroot_install
+	$(call TEST_implementation)
+.PHONY: TEST
+
+# multiple targets with a wildcard need to share an directory.
+# Don't use this directly it just makes sure the directory is removed _after_
+# the files are.
+CLEANFILE(%):
+	$(call silent_rm,$(TARGET_OR_MEMBER))
+.PHONY: CLEANFILE
+
+CLEAN(%): CLEANFILE(%)
+	$(QUIET)# CLEAN($%) meta-target called
+	$(if $(filter-out $(PWD)/,$(dir $(abspath $(TARGET_OR_MEMBER)))), \
+	  $(call silent_rmdir,$(dir $(abspath $(TARGET_OR_MEMBER)))),\
+	  $(QUIET)# Not deleting $(dir $(abspath $(TARGET_OR_MEMBER))) yet.)
+.PHONY: CLEAN
+
+#
+# Top-level objects and pattern rules
+#
+
+# All objects for .c files at the top level
+C_OBJECTS = $(patsubst $(SRC)/%.c,%.o,$(wildcard $(SRC)/*.c))
+
+
+# All objects for .cxx files at the top level
+CXX_OBJECTS = $(patsubst $(SRC)/%.cc,%.o,$(wildcard $(SRC)/*.cc))
+
+# Note, the catch-all pattern rules don't work in subdirectories because
+# we're building from the $(OUT) directory. At the top-level (here) they will
+# work, but we go ahead and match using the module form.  Then we can place a
+# generic pattern rule to capture leakage from the main Makefile. (Later in the
+# file.)
+#
+# The reason target specific pattern rules work well for modules,
+# MODULE_C_OBJECTS, is because it scopes the behavior to the given target which
+# ensures we get a relative directory offset from $(OUT) which otherwise would
+# not match without further magic on a per-subdirectory basis.
+
+# Creates object file rules. Call with eval.
+# $(1) list of .o files
+# $(2) source type (CC or CXX)
+# $(3) source suffix (cc or c)
+# $(4) compiler flag name (CFLAGS or CXXFLAGS)
+# $(5) source dir: _only_ if $(SRC). Leave blank for obj tree.
+define add_object_rules
+$(patsubst %.o,%.pie.o,$(1)): %.pie.o: $(5)%.$(3) %.o.depends
+	$$(call auto_mkdir,$$@)
+	$$(call OBJECT_PATTERN_implementation,$(2),\
+          $$(basename $$@),$$($(4)) $$(CPPFLAGS) $$(OBJ_PIE_FLAG))
+
+$(patsubst %.o,%.pic.o,$(1)): %.pic.o: $(5)%.$(3) %.o.depends
+	$$(call auto_mkdir,$$@)
+	$$(call OBJECT_PATTERN_implementation,$(2),\
+          $$(basename $$@),$$($(4)) $$(CPPFLAGS) -fPIC)
+
+# Placeholder for depends
+$(patsubst %.o,%.o.depends,$(1)):
+	$$(call auto_mkdir,$$@)
+	$$(QUIET)touch "$$@"
+
+$(1): %.o: %.pic.o %.pie.o
+	$$(call auto_mkdir,$$@)
+	$$(QUIET)touch "$$@"
+endef
+
+define OBJECT_PATTERN_implementation
+  @$(ECHO) "$(1)		$(subst $(SRC)/,,$<) -> $(2).o"
+  $(call auto_mkdir,$@)
+  $(QUIET)$($(1)) -c -MD -MF $(2).d $(3) -o $(2).o $<
+  $(QUIET)# Wrap all the deps in $$(wildcard) so a missing header
+  $(QUIET)# won't cause weirdness.  First we remove newlines and \,
+  $(QUIET)# then wrap it.
+  $(QUIET)sed -i -e :j -e '$$!N;s|\\\s*\n| |;tj' \
+    -e 's|^\(.*\s*:\s*\)\(.*\)$$|\1 $$\(wildcard \2\)|' $(2).d
+endef
+
+# Now actually register handlers for C(XX)_OBJECTS.
+$(eval $(call add_object_rules,$(C_OBJECTS),CC,c,CFLAGS,$(SRC)/))
+$(eval $(call add_object_rules,$(CXX_OBJECTS),CXX,cc,CXXFLAGS,$(SRC)/))
+
+# Disable default pattern rules to help avoid leakage.
+# These may already be handled by '-r', but let's keep it to be safe.
+%: %.o ;
+%.a: %.o ;
+%.o: %.c ;
+%.o: %.cc ;
+
+# NOTE: A specific rule for archive objects is avoided because parallel
+#       update of the archive causes build flakiness.
+# Instead, just make the objects the prerequisites and use update_archive
+# To use the foo.a(obj.o) functionality, targets would need to specify the
+# explicit object they expect on the prerequisite line.
+
+#
+# Architecture detection and QEMU wrapping
+#
+
+HOST_ARCH ?= $(shell uname -m)
+override ARCH := $(strip $(ARCH))
+override HOST_ARCH := $(strip $(HOST_ARCH))
+# emake will supply "x86" or "arm" for ARCH, but
+# if uname -m runs and you get x86_64, then this subst
+# will break.
+ifeq ($(subst x86,i386,$(ARCH)),i386)
+  QEMU_ARCH := $(subst x86,i386,$(ARCH))  # x86 -> i386
+else ifeq ($(subst amd64,x86_64,$(ARCH)),x86_64)
+  QEMU_ARCH := $(subst amd64,x86_64,$(ARCH))  # amd64 -> x86_64
+else
+  QEMU_ARCH = $(ARCH)
+endif
+override QEMU_ARCH := $(strip $(QEMU_ARCH))
+
+# If we're cross-compiling, try to use qemu for running the tests.
+ifneq ($(QEMU_ARCH),$(HOST_ARCH))
+  ifeq ($(SYSROOT),)
+    $(info SYSROOT not defined. qemu-based testing disabled)
+  else
+    # A SYSROOT is assumed for QEmu use.
+    USE_QEMU ?= 1
+
+    # Allow 64-bit hosts to run 32-bit without qemu.
+    ifeq ($(HOST_ARCH),x86_64)
+      ifeq ($(QEMU_ARCH),i386)
+        USE_QEMU = 0
+      endif
+    endif
+  endif
+else
+  USE_QEMU ?= 0
+endif
+
+SYSROOT_OUT = $(OUT)
+ifneq ($(SYSROOT),)
+  SYSROOT_OUT = $(subst $(SYSROOT),,$(OUT))
+else
+  # Default to / when all the empty-sysroot logic is done.
+  SYSROOT = /
+endif
+
+
+#
+# Output full configuration at top level
+#
+
+# Don't show on clean
+ifneq ($(MAKECMDGOALS),clean)
+  $(info build configuration:)
+  $(info - OUT=$(OUT))
+  $(info - SRC=$(SRC))
+  $(info - MODE=$(MODE))
+  $(info - SPLITDEBUG=$(SPLITDEBUG))
+  $(info - NOSTRIP=$(NOSTRIP))
+  $(info - VALGRIND=$(VALGRIND))
+  $(info - COLOR=$(COLOR))
+  $(info - ARCH=$(ARCH))
+  $(info - QEMU_ARCH=$(QEMU_ARCH))
+  $(info - SYSROOT=$(SYSROOT))
+  $(info )
+endif
+
+#
+# Standard targets with detection for when they are improperly configured.
+#
+
+# all does not include tests by default
+all:
+	$(QUIET)(test -z "$^" && \
+	$(ECHO) "You must add your targets as 'all' prerequisites") || true
+	$(QUIET)test -n "$^"
+
+# Builds and runs tests for the target arch
+# Run them in parallel
+# After the test have completed, if profiling, run coverage analysis
+tests:
+ifeq ($(MODE),profiling)
+	@$(ECHO) "COVERAGE [$(COLOR_YELLOW)STARTED$(COLOR_RESET)]"
+	$(QUIET)FILES="";						\
+		for GCNO in `find . -name "*.gcno"`; do			\
+			GCDA="$${GCNO%.gcno}.gcda";			\
+			if [ -e $${GCDA} ]; then			\
+				FILES="$${FILES} $${GCDA}";		\
+			fi						\
+		done;							\
+		if [ -n "$${FILES}" ]; then				\
+			gcov -l $${FILES};				\
+			lcov --capture --directory .			\
+				--output-file=lcov-coverage.info;	\
+			genhtml lcov-coverage.info			\
+				--output-directory lcov-html;		\
+		fi
+	@$(ECHO) "COVERAGE [$(COLOR_YELLOW)FINISHED$(COLOR_RESET)]"
+endif
+.PHONY: tests
+
+qemu_clean:
+	$(call if_qemu,$(call silent_rm,$(OUT)/qemu-$(QEMU_ARCH)))
+
+qemu_chroot_install:
+ifeq ($(USE_QEMU),1)
+	$(QUIET)$(ECHO) "QEMU   Preparing qemu-$(QEMU_ARCH)"
+	$(QUIET)cp -fu /usr/bin/qemu-$(QEMU_ARCH) $(OUT)/qemu-$(QEMU_ARCH)
+	$(QUIET)chmod a+rx $(OUT)/qemu-$(QEMU_ARCH)
+endif
+.PHONY: qemu_clean qemu_chroot_install
+
+# TODO(wad) Move to -L $(SYSROOT) and fakechroot when qemu-user
+#           doesn't hang traversing /proc from SYSROOT.
+QEMU_CMD =
+ROOT_CMD = $(if $(filter 1,$(NEEDS_ROOT)),sudo , )
+MOUNT_CMD = $(if $(filter 1,$(NEEDS_MOUNTS)),$(ROOT_CMD) mount, \#)
+UMOUNT_CMD = $(if $(filter 1,$(NEEDS_MOUNTS)),$(ROOT_CMD) umount, \#)
+QEMU_LDPATH = $(SYSROOT_LDPATH):/lib64:/lib:/usr/lib64:/usr/lib
+ROOT_CMD_LDPATH = $(SYSROOT_LDPATH):$(SYSROOT)/lib64:
+ROOT_CMD_LDPATH := $(ROOT_CMD_LDPATH):$(SYSROOT)/lib:$(SYSROOT)/usr/lib64:
+ROOT_CMD_LDPATH := $(ROOT_CMD_LDPATH):$(SYSROOT)/usr/lib
+ifeq ($(USE_QEMU),1)
+  export QEMU_CMD = \
+   sudo chroot $(SYSROOT) $(SYSROOT_OUT)qemu-$(QEMU_ARCH) \
+   -drop-ld-preload \
+   -E LD_LIBRARY_PATH="$(QEMU_LDPATH):$(patsubst $(OUT),,$(LD_DIRS))" \
+   -E HOME="$(HOME)" --
+  # USE_QEMU conditional function
+  define if_qemu
+    $(1)
+  endef
+else
+  ROOT_CMD = $(if $(filter 1,$(NEEDS_ROOT)),sudo, ) \
+    LD_LIBRARY_PATH="$(ROOT_CMD_LDPATH):$(LD_DIRS)"
+  define if_qemu
+    $(2)
+  endef
+endif
+
+VALGRIND_CMD =
+ifeq ($(VALGRIND),1)
+  VALGRIND_CMD = /usr/bin/valgrind --tool=memcheck $(VALGRIND_ARGS) --
+endif
+
+define TEST_implementation
+  $(QUIET)$(call TEST_setup)
+  $(QUIET)$(call TEST_run)
+  $(QUIET)$(call TEST_teardown)
+  $(QUIET)exit $$(cat $(OUT)$(TARGET_OR_MEMBER).status.test)
+endef
+
+define TEST_setup
+  @$(ECHO) -n "TEST		$(TARGET_OR_MEMBER) "
+  @$(ECHO) "[$(COLOR_YELLOW)SETUP$(COLOR_RESET)]"
+  $(QUIET)# Setup a target-specific results file
+  $(QUIET)(echo 1 > $(OUT)$(TARGET_OR_MEMBER).status.test)
+  $(QUIET)(echo > $(OUT)$(TARGET_OR_MEMBER).cleanup.test)
+  $(QUIET)# No setup if we are not using QEMU
+  $(QUIET)# TODO(wad) this is racy until we use a vfs namespace
+  $(call if_qemu,\
+    $(QUIET)sudo mkdir -p "$(SYSROOT)/proc" "$(SYSROOT)/dev")
+  $(call if_qemu,\
+    $(QUIET)$(MOUNT_CMD) --bind /proc "$(SYSROOT)/proc")
+  $(call if_qemu,\
+    $(QUIET)$(MOUNT_CMD) --bind /dev "$(SYSROOT)/dev")
+  $(call if_qemu,\
+    $(QUIET)(echo "$(UMOUNT_CMD) -l '$(SYSROOT)/proc'" \
+             >> "$(OUT)$(TARGET_OR_MEMBER).cleanup.test"))
+  $(call if_qemu,\
+    $(QUIET)(echo "$(UMOUNT_CMD) -l '$(SYSROOT)/dev'" \
+             >> "$(OUT)$(TARGET_OR_MEMBER).cleanup.test"))
+endef
+
+define TEST_teardown
+  @$(ECHO) -n "TEST		$(TARGET_OR_MEMBER) "
+  @$(ECHO) "[$(COLOR_YELLOW)TEARDOWN$(COLOR_RESET)]"
+  $(call if_qemu, $(QUIET)$(SHELL) "$(OUT)$(TARGET_OR_MEMBER).cleanup.test")
+endef
+
+# Use GTEST_ARGS.[arch] if defined.
+override GTEST_ARGS.real = \
+ $(call if_qemu,$(GTEST_ARGS.qemu.$(QEMU_ARCH)),$(GTEST_ARGS.host.$(HOST_ARCH)))
+
+define TEST_run
+  @$(ECHO) -n "TEST		$(TARGET_OR_MEMBER) "
+  @$(ECHO) "[$(COLOR_GREEN)RUN$(COLOR_RESET)]"
+  $(QUIET)(echo 1 > "$(OUT)$(TARGET_OR_MEMBER).status.test")
+  -($(ROOT_CMD) $(QEMU_CMD) $(VALGRIND_CMD) \
+    "$(strip $(call if_qemu, $(SYSROOT_OUT),$(OUT))$(TARGET_OR_MEMBER))" \
+      $(if $(filter-out 0,$(words $(GTEST_ARGS.real))),$(GTEST_ARGS.real),\
+           $(GTEST_ARGS)) && \
+    echo 0 > "$(OUT)$(TARGET_OR_MEMBER).status.test")
+endef
+
+# Recursive list reversal so that we get RMDIR_ON_CLEAN in reverse order.
+define reverse
+$(if $(1),$(call reverse,$(wordlist 2,$(words $(1)),$(1)))) $(firstword $(1))
+endef
+
+clean: qemu_clean
+clean: CLEAN($(OUT)*.d) CLEAN($(OUT)*.o) CLEAN($(OUT)*.debug)
+clean: CLEAN($(OUT)*.test) CLEAN($(OUT)*.depends)
+clean: CLEAN($(OUT)*.gcno) CLEAN($(OUT)*.gcda) CLEAN($(OUT)*.gcov)
+clean: CLEAN($(OUT)lcov-coverage.info) CLEAN($(OUT)lcov-html)
+
+clean:
+	$(QUIET)# Always delete the containing directory last.
+	$(call silent_rmdir,$(OUT))
+
+FORCE: ;
+# Empty rule for use when no special targets are needed, like large_tests
+NONE:
+
+.PHONY: clean NONE valgrind NONE
+.DEFAULT_GOAL  :=  all
+# Don't let make blow away "intermediates"
+.PRECIOUS: %.pic.o %.pie.o %.a %.pic.a %.pie.a %.test
+
+# Start accruing build info
+OUT_DIRS = $(OUT)
+LD_DIRS = $(OUT)
+SRC_DIRS = $(SRC)
+
+include $(wildcard $(OUT)*.d)
+SUBMODULE_DIRS = $(wildcard $(SRC)/*/module.mk)
+include $(SUBMODULE_DIRS)
+
+
+else  ## In duplicate inclusions of common.mk
+
+# Get the current inclusion directory without a trailing slash
+MODULE := $(patsubst %/,%, \
+           $(dir $(lastword $(filter-out %common.mk,$(MAKEFILE_LIST)))))
+MODULE := $(subst $(SRC)/,,$(MODULE))
+MODULE_NAME := $(subst /,_,$(MODULE))
+#VPATH := $(MODULE):$(VPATH)
+
+
+# Depth first
+$(eval OUT_DIRS += $(OUT)$(MODULE))
+$(eval SRC_DIRS += $(OUT)$(MODULE))
+$(eval LD_DIRS := $(LD_DIRS):$(OUT)$(MODULE))
+
+# Add the defaults from this dir to rm_clean
+clean: CLEAN($(OUT)$(MODULE)/*.d) CLEAN($(OUT)$(MODULE)/*.o)
+clean: CLEAN($(OUT)$(MODULE)/*.debug) CLEAN($(OUT)$(MODULE)/*.test)
+clean: CLEAN($(OUT)$(MODULE)/*.depends)
+clean: CLEAN($(OUT)$(MODULE)/*.gcno) CLEAN($(OUT)$(MODULE)/*.gcda)
+clean: CLEAN($(OUT)$(MODULE)/*.gcov) CLEAN($(OUT)lcov-coverage.info)
+clean: CLEAN($(OUT)lcov-html)
+
+$(info + submodule: $(MODULE_NAME))
+# We must eval otherwise they may be dropped.
+MODULE_C_OBJECTS = $(patsubst $(SRC)/$(MODULE)/%.c,$(MODULE)/%.o,\
+  $(wildcard $(SRC)/$(MODULE)/*.c))
+$(eval $(MODULE_NAME)_C_OBJECTS ?= $(MODULE_C_OBJECTS))
+MODULE_CXX_OBJECTS = $(patsubst $(SRC)/$(MODULE)/%.cc,$(MODULE)/%.o,\
+  $(wildcard $(SRC)/$(MODULE)/*.cc))
+$(eval $(MODULE_NAME)_CXX_OBJECTS ?= $(MODULE_CXX_OBJECTS))
+
+# Note, $(MODULE) is implicit in the path to the %.c.
+# See $(C_OBJECTS) for more details.
+# Register rules for the module objects.
+$(eval $(call add_object_rules,$(MODULE_C_OBJECTS),CC,c,CFLAGS,$(SRC)/))
+$(eval $(call add_object_rules,$(MODULE_CXX_OBJECTS),CXX,cc,CXXFLAGS,$(SRC)/))
+
+# Continue recursive inclusion of module.mk files
+SUBMODULE_DIRS = $(wildcard $(SRC)/$(MODULE)/*/module.mk)
+include $(wildcard $(OUT)$(MODULE)/*.d)
+include $(SUBMODULE_DIRS)
+
+endif
+endif  ## pass-to-subcall wrapper for relocating the call directory
diff --git a/drm-tests/drm_cursor_test.c b/drm-tests/drm_cursor_test.c
new file mode 100644
index 0000000..ea8204c
--- /dev/null
+++ b/drm-tests/drm_cursor_test.c
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+int main(int argc, char **argv)
+{
+	uint32_t cursor_size = 64;
+	if (argc >= 2) {
+		char *end_str;
+		unsigned long new_cursor_size = strtoul(argv[1], &end_str, 0);
+		if (end_str == argv[1] || new_cursor_size == 0 || new_cursor_size > UINT32_MAX) {
+			printf(
+			    "usage:\n  drm_cursor_test [cursor size]\n\nCursor size defaults to "
+			    "%u\n",
+			    cursor_size);
+			return 1;
+		}
+		cursor_size = (uint32_t)new_cursor_size;
+	}
+
+	int fd = bs_drm_open_main_display();
+	if (fd < 0) {
+		bs_debug_error("failed to open card for display");
+		return 1;
+	}
+
+	struct gbm_device *gbm = gbm_create_device(fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm");
+		return 1;
+	}
+
+	struct bs_drm_pipe pipe = { 0 };
+	if (!bs_drm_pipe_make(fd, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		return 1;
+	}
+
+	drmModeConnector *connector = drmModeGetConnector(fd, pipe.connector_id);
+	drmModeModeInfo *mode = &connector->modes[0];
+	uint32_t crtc_id = pipe.crtc_id;
+
+	// Restart the cursor position before binding the crtc so that the old cursor position isn't
+	// displayed briefly when the display is turned activated.
+	int ret = drmModeMoveCursor(fd, crtc_id, 0, 0);
+	if (ret) {
+		bs_debug_error("failed to move cursor: %d", ret);
+		return 1;
+	}
+
+	struct gbm_bo *fb_bo =
+	    gbm_bo_create(gbm, mode->hdisplay, mode->vdisplay, GBM_FORMAT_XRGB8888,
+			  GBM_BO_USE_SCANOUT | GBM_BO_USE_SW_WRITE_RARELY);
+	if (!fb_bo) {
+		bs_debug_error("failed to create buffer object for frame buffer");
+		return 1;
+	}
+
+	struct bs_mapper *mapper = bs_mapper_gem_new();
+	if (mapper == NULL) {
+		bs_debug_error("failed to create mapper object");
+		return 1;
+	}
+
+	void *map_data;
+	uint8_t *fb_ptr = bs_mapper_map(mapper, fb_bo, 0, &map_data);
+	if (fb_ptr == MAP_FAILED) {
+		bs_debug_error("failed to mmap frame buffer object");
+		return 1;
+	}
+	uint32_t stride = gbm_bo_get_stride(fb_bo);
+	for (size_t y = 0; y < mode->vdisplay; y++) {
+		for (size_t x = 0; x < mode->hdisplay; x++) {
+			// Solid blue fill
+			fb_ptr[y * stride + x * 4 + 0] = 0xff;
+			fb_ptr[y * stride + x * 4 + 1] = 0;
+			fb_ptr[y * stride + x * 4 + 2] = 0;
+			fb_ptr[y * stride + x * 4 + 3] = 0;
+		}
+	}
+	bs_mapper_unmap(mapper, fb_bo, map_data);
+
+	uint32_t fb_id = bs_drm_fb_create_gbm(fb_bo);
+	if (!fb_id) {
+		bs_debug_error("failed to create frame buffer from buffer object");
+		return 1;
+	}
+
+	ret = drmModeSetCrtc(fd, crtc_id, fb_id, 0 /* x */, 0 /* y */, &pipe.connector_id,
+			     1 /* connector count */, mode);
+	if (ret) {
+		bs_debug_error("failed to set crtc: %d", ret);
+		return 1;
+	}
+
+	struct gbm_bo *cursor_bo = gbm_bo_create(gbm, cursor_size, cursor_size, GBM_FORMAT_ARGB8888,
+						 GBM_BO_USE_CURSOR | GBM_BO_USE_SW_WRITE_RARELY);
+	if (!cursor_bo) {
+		bs_debug_error("failed to create cursor buffer object");
+		return 1;
+	}
+
+	const struct bs_draw_format *draw_format = bs_get_draw_format(GBM_FORMAT_ARGB8888);
+	if (!draw_format) {
+		bs_debug_error("failed to get draw format");
+		return 1;
+	}
+
+	if (!bs_draw_cursor(mapper, cursor_bo, draw_format)) {
+		bs_debug_error("failed to draw cursor");
+		return 1;
+	}
+
+	ret = drmModeSetCursor(fd, crtc_id, gbm_bo_get_handle(cursor_bo).u32, cursor_size,
+			       cursor_size);
+	if (ret) {
+		bs_debug_error("failed to set cursor: %d", ret);
+		return 1;
+	}
+
+	// Divisor chosen so that the test would last about 10 seconds @ 60fps.
+	const uint32_t divisor = 25;
+	const uint32_t xinc = mode->hdisplay / divisor;
+	const uint32_t yinc = mode->vdisplay / divisor;
+	for (uint32_t x = 0; x < divisor; x++) {
+		for (uint32_t y = 0; y < divisor; y++) {
+			ret = drmModeMoveCursor(fd, crtc_id, x * xinc + y, y * yinc + x);
+			if (ret) {
+				bs_debug_error("failed to move cursor: %d", ret);
+				return 1;
+			}
+			usleep(16667);
+		}
+	}
+
+	bs_mapper_destroy(mapper);
+	drmModeRmFB(fd, fb_id);
+	gbm_bo_destroy(fb_bo);
+	gbm_bo_destroy(cursor_bo);
+	gbm_device_destroy(gbm);
+	close(fd);
+
+	return 0;
+}
diff --git a/drm-tests/gamma_test.c b/drm-tests/gamma_test.c
new file mode 100644
index 0000000..80c1907
--- /dev/null
+++ b/drm-tests/gamma_test.c
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2015 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+#include <getopt.h>
+#include <math.h>
+
+#define TABLE_LINEAR 0
+#define TABLE_NEGATIVE 1
+#define TABLE_POW 2
+#define TABLE_STEP 3
+
+#define FLAG_INTERNAL 'i'
+#define FLAG_EXTERNAL 'e'
+#define FLAG_GAMMA 'g'
+#define FLAG_LINEAR 'l'
+#define FLAG_NEGATIVE 'n'
+#define FLAG_TIME 't'
+#define FLAG_CRTCS 'c'
+#define FLAG_PERSIST 'p'
+#define FLAG_STEP 's'
+#define FLAG_HELP 'h'
+
+static struct option command_options[] = { { "internal", no_argument, NULL, FLAG_INTERNAL },
+					   { "external", no_argument, NULL, FLAG_EXTERNAL },
+					   { "gamma", required_argument, NULL, FLAG_GAMMA },
+					   { "linear", no_argument, NULL, FLAG_LINEAR },
+					   { "negative", no_argument, NULL, FLAG_NEGATIVE },
+					   { "time", required_argument, NULL, FLAG_TIME },
+					   { "crtcs", required_argument, NULL, FLAG_CRTCS },
+					   { "persist", no_argument, NULL, FLAG_PERSIST },
+					   { "step", no_argument, NULL, FLAG_STEP },
+					   { "help", no_argument, NULL, FLAG_HELP },
+					   { NULL, 0, NULL, 0 } };
+
+static void gamma_linear(uint16_t *table, int size)
+{
+	int i;
+	for (i = 0; i < size; i++) {
+		float v = (float)(i) / (float)(size - 1);
+		v *= 65535.0f;
+		table[i] = (uint16_t)v;
+	}
+}
+
+static void gamma_inv(uint16_t *table, int size)
+{
+	int i;
+	for (i = 0; i < size; i++) {
+		float v = (float)(size - 1 - i) / (float)(size - 1);
+		v *= 65535.0f;
+		table[i] = (uint16_t)v;
+	}
+}
+
+static void gamma_pow(uint16_t *table, int size, float p)
+{
+	int i;
+	for (i = 0; i < size; i++) {
+		float v = (float)(i) / (float)(size - 1);
+		v = pow(v, p);
+		v *= 65535.0f;
+		table[i] = (uint16_t)v;
+	}
+}
+
+static void gamma_step(uint16_t *table, int size)
+{
+	int i;
+	for (i = 0; i < size; i++) {
+		table[i] = (i < size / 2) ? 0 : 65535;
+	}
+}
+
+static void fsleep(double secs)
+{
+	usleep((useconds_t)(1000000.0f * secs));
+}
+
+static drmModeModeInfoPtr find_best_mode(int mode_count, drmModeModeInfoPtr modes)
+{
+	assert(mode_count >= 0);
+	if (mode_count == 0)
+		return NULL;
+
+	assert(modes);
+
+	for (int m = 0; m < mode_count; m++)
+		if (modes[m].type & DRM_MODE_TYPE_PREFERRED)
+			return &modes[m];
+
+	return &modes[0];
+}
+
+static bool draw_pattern(struct bs_mapper *mapper, struct gbm_bo *bo)
+{
+	const uint32_t stride = gbm_bo_get_stride(bo);
+	const uint32_t height = gbm_bo_get_height(bo);
+	const uint32_t bo_size = stride * height;
+	const uint32_t stripw = gbm_bo_get_width(bo) / 256;
+	const uint32_t striph = height / 4;
+
+	void *map_data;
+	uint8_t *bo_ptr = bs_mapper_map(mapper, bo, 0, &map_data);
+	if (bo_ptr == MAP_FAILED) {
+		bs_debug_error("failed to mmap buffer while drawing pattern");
+		return false;
+	}
+
+	bool success = true;
+
+	memset(bo_ptr, 0, bo_size);
+	for (uint32_t s = 0; s < 4; s++) {
+		uint8_t r = 0, g = 0, b = 0;
+		switch (s) {
+			case 0:
+				r = g = b = 1;
+				break;
+			case 1:
+				r = 1;
+				break;
+			case 2:
+				g = 1;
+				break;
+			case 3:
+				b = 1;
+				break;
+			default:
+				assert("invalid strip" && false);
+				success = false;
+				goto out;
+		}
+		for (uint32_t y = s * striph; y < (s + 1) * striph; y++) {
+			uint8_t *row_ptr = &bo_ptr[y * stride];
+			for (uint32_t i = 0; i < 256; i++) {
+				for (uint32_t x = i * stripw; x < (i + 1) * stripw; x++) {
+					row_ptr[x * 4 + 0] = b * i;
+					row_ptr[x * 4 + 1] = g * i;
+					row_ptr[x * 4 + 2] = r * i;
+					row_ptr[x * 4 + 3] = 0;
+				}
+			}
+		}
+	}
+
+out:
+	bs_mapper_unmap(mapper, bo, map_data);
+	return success;
+}
+
+static int set_gamma(int fd, uint32_t crtc_id, int gamma_size, int gamma_table, float gamma)
+{
+	int res;
+	uint16_t *r, *g, *b;
+	r = calloc(gamma_size, sizeof(*r));
+	g = calloc(gamma_size, sizeof(*g));
+	b = calloc(gamma_size, sizeof(*b));
+
+	printf("Setting gamma table %d\n", gamma_table);
+	switch (gamma_table) {
+		case TABLE_LINEAR:
+			gamma_linear(r, gamma_size);
+			gamma_linear(g, gamma_size);
+			gamma_linear(b, gamma_size);
+			break;
+		case TABLE_NEGATIVE:
+			gamma_inv(r, gamma_size);
+			gamma_inv(g, gamma_size);
+			gamma_inv(b, gamma_size);
+			break;
+		case TABLE_POW:
+			gamma_pow(r, gamma_size, gamma);
+			gamma_pow(g, gamma_size, gamma);
+			gamma_pow(b, gamma_size, gamma);
+			break;
+		case TABLE_STEP:
+			gamma_step(r, gamma_size);
+			gamma_step(g, gamma_size);
+			gamma_step(b, gamma_size);
+			break;
+	}
+
+	res = drmModeCrtcSetGamma(fd, crtc_id, gamma_size, r, g, b);
+	if (res)
+		bs_debug_error("drmModeCrtcSetGamma(%d) failed: %s", crtc_id, strerror(errno));
+	free(r);
+	free(g);
+	free(b);
+	return res;
+}
+
+void help(void)
+{
+	printf(
+	    "\
+gamma test\n\
+command line options:\
+\n\
+--help - this\n\
+--linear - set linear gamma table\n\
+--negative - set negative linear gamma table\n\
+--step - set step gamma table\n\
+--gamma=f - set pow(gamma) gamma table with gamma=f\n\
+--time=f - set test time\n\
+--crtcs=n - set mask of crtcs to test\n\
+--persist - do not reset gamma table at the end of the test\n\
+--internal - display tests on internal display\n\
+--external - display tests on external display\n\
+");
+}
+
+int main(int argc, char **argv)
+{
+	int internal = 1;
+	int persist = 0;
+	float time = 5.0;
+	float gamma = 2.2f;
+	float table = TABLE_LINEAR;
+	uint32_t crtcs = 0xFFFF;
+
+	for (;;) {
+		int c = getopt_long(argc, argv, "", command_options, NULL);
+
+		if (c == -1)
+			break;
+
+		switch (c) {
+			case FLAG_HELP:
+				help();
+				return 0;
+
+			case FLAG_INTERNAL:
+				internal = 1;
+				break;
+
+			case FLAG_EXTERNAL:
+				internal = 0;
+				break;
+
+			case FLAG_GAMMA:
+				gamma = strtof(optarg, NULL);
+				table = TABLE_POW;
+				break;
+
+			case FLAG_LINEAR:
+				table = TABLE_LINEAR;
+				break;
+
+			case FLAG_NEGATIVE:
+				table = TABLE_NEGATIVE;
+				break;
+
+			case FLAG_STEP:
+				table = TABLE_STEP;
+				break;
+
+			case FLAG_TIME:
+				time = strtof(optarg, NULL);
+				break;
+
+			case FLAG_CRTCS:
+				crtcs = strtoul(optarg, NULL, 0);
+				break;
+
+			case FLAG_PERSIST:
+				persist = 1;
+				break;
+		}
+	}
+
+	drmModeConnector *connector = NULL;
+	struct bs_drm_pipe pipe = { 0 };
+	struct bs_drm_pipe_plumber *plumber = bs_drm_pipe_plumber_new();
+	bs_drm_pipe_plumber_connector_ptr(plumber, &connector);
+	bs_drm_pipe_plumber_crtc_mask(plumber, crtcs);
+	if (!internal)
+		bs_drm_pipe_plumber_connector_ranks(plumber, bs_drm_connectors_external_rank);
+	if (!bs_drm_pipe_plumber_make(plumber, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		return 1;
+	}
+
+	int fd = pipe.fd;
+	bs_drm_pipe_plumber_fd(plumber, fd);
+	drmModeRes *resources = drmModeGetResources(fd);
+	if (!resources) {
+		bs_debug_error("failed to get drm resources");
+		return 1;
+	}
+
+	struct gbm_device *gbm = gbm_create_device(fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm");
+		return 1;
+	}
+
+	struct bs_mapper *mapper = bs_mapper_gem_new();
+	if (mapper == NULL) {
+		bs_debug_error("failed to create mapper object");
+		return 1;
+	}
+
+	uint32_t num_success = 0;
+	for (int c = 0; c < resources->count_crtcs && (crtcs >> c); c++) {
+		int ret;
+		drmModeCrtc *crtc;
+		uint32_t crtc_mask = 1u << c;
+
+		if (!(crtcs & crtc_mask))
+			continue;
+
+		if (connector != NULL) {
+			drmModeFreeConnector(connector);
+			connector = NULL;
+		}
+
+		bs_drm_pipe_plumber_crtc_mask(plumber, crtc_mask);
+		if (!bs_drm_pipe_plumber_make(plumber, &pipe)) {
+			printf("unable to make pipe with crtc mask: %x\n", crtc_mask);
+			continue;
+		}
+
+		crtc = drmModeGetCrtc(fd, pipe.crtc_id);
+		if (!crtc) {
+			bs_debug_error("drmModeGetCrtc(%d) failed: %s\n", pipe.crtc_id,
+				       strerror(errno));
+			return 1;
+		}
+		int gamma_size = crtc->gamma_size;
+		drmModeFreeCrtc(crtc);
+
+		if (!gamma_size) {
+			bs_debug_error("CRTC %d has no gamma table", crtc->crtc_id);
+			continue;
+		}
+
+		printf("CRTC:%d gamma size:%d\n", pipe.crtc_id, gamma_size);
+
+		printf("Using CRTC:%u ENCODER:%u CONNECTOR:%u\n", pipe.crtc_id, pipe.encoder_id,
+		       pipe.connector_id);
+
+		drmModeModeInfoPtr mode = find_best_mode(connector->count_modes, connector->modes);
+		if (!mode) {
+			bs_debug_error("Could not find mode for CRTC %d", pipe.crtc_id);
+			continue;
+		}
+
+		printf("Using mode %s\n", mode->name);
+
+		printf("Creating buffer %ux%u\n", mode->hdisplay, mode->vdisplay);
+		struct gbm_bo *bo =
+		    gbm_bo_create(gbm, mode->hdisplay, mode->vdisplay, GBM_FORMAT_XRGB8888,
+				  GBM_BO_USE_SCANOUT | GBM_BO_USE_SW_WRITE_RARELY);
+		if (!bo) {
+			bs_debug_error("failed to create buffer object");
+			return 1;
+		}
+
+		uint32_t fb_id = bs_drm_fb_create_gbm(bo);
+		if (!fb_id) {
+			bs_debug_error("failed to create frame buffer for buffer object");
+			return 1;
+		}
+
+		if (!draw_pattern(mapper, bo)) {
+			bs_debug_error("failed to draw pattern on buffer object");
+			return 1;
+		}
+
+		ret = drmModeSetCrtc(fd, pipe.crtc_id, fb_id, 0, 0, &pipe.connector_id, 1, mode);
+		if (ret < 0) {
+			bs_debug_error("Could not set mode on CRTC %d %s", pipe.crtc_id,
+				       strerror(errno));
+			return 1;
+		}
+
+		ret = set_gamma(fd, pipe.crtc_id, gamma_size, table, gamma);
+		if (ret)
+			return ret;
+
+		fsleep(time);
+
+		if (!persist) {
+			ret = set_gamma(fd, pipe.crtc_id, gamma_size, TABLE_LINEAR, 0.0f);
+			if (ret)
+				return ret;
+		}
+
+		ret = drmModeSetCrtc(fd, pipe.crtc_id, 0, 0, 0, NULL, 0, NULL);
+		if (ret < 0) {
+			bs_debug_error("Could disable CRTC %d %s\n", pipe.crtc_id, strerror(errno));
+			return 1;
+		}
+
+		drmModeRmFB(fd, fb_id);
+		gbm_bo_destroy(bo);
+		num_success++;
+	}
+	bs_mapper_destroy(mapper);
+
+	if (connector != NULL) {
+		drmModeFreeConnector(connector);
+		connector = NULL;
+	}
+
+	drmModeFreeResources(resources);
+	bs_drm_pipe_plumber_destroy(&plumber);
+
+	if (!num_success) {
+		bs_debug_error("unable to set gamma table on any CRTC");
+		return 1;
+	}
+
+	return 0;
+}
diff --git a/drm-tests/linear_bo_test.c b/drm-tests/linear_bo_test.c
new file mode 100644
index 0000000..6955a01
--- /dev/null
+++ b/drm-tests/linear_bo_test.c
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+static const useconds_t test_case_display_usec = 2000000;
+
+struct test_case {
+	uint32_t format;    /* format for allocating buffer object from GBM*/
+	uint32_t fb_format; /* format used to create DRM framebuffer, 0 indicates same as format */
+	enum gbm_bo_flags usage;
+};
+
+const char *format_to_string(uint32_t format)
+{
+	switch (format) {
+		case GBM_FORMAT_XRGB8888:
+			return "GBM_FORMAT_XRGB8888";
+		case GBM_FORMAT_ARGB8888:
+			return "GBM_FORMAT_ARGB8888";
+		default:
+			return "GBM_FORMAT_????????";
+	}
+}
+
+static void test_case_print(FILE *out, const struct test_case *tcase)
+{
+	fprintf(out, "format=%s usage=", format_to_string(tcase->format));
+	bool first = true;
+	if (tcase->usage & GBM_BO_USE_SCANOUT) {
+		fprintf(out, "GBM_BO_USE_SCANOUT");
+		first = false;
+	}
+	if (tcase->usage & GBM_BO_USE_RENDERING) {
+		fprintf(out, "%sGBM_BO_USE_RENDERING", first ? "" : " | ");
+		first = false;
+	}
+	if (tcase->usage & GBM_BO_USE_LINEAR) {
+		fprintf(out, "%sGBM_BO_USE_LINEAR", first ? "" : " | ");
+		first = false;
+	}
+	if (tcase->fb_format)
+		fprintf(out, " fb_format=%s", format_to_string(tcase->fb_format));
+}
+
+static void test_case_colors(const struct test_case *tcase,
+			     float *colors /* sizeof(colors) / sizeof(colors[0]) == 9 */)
+{
+	colors[0] = 0.0f;
+	colors[1] = 1.0f;
+	colors[2] = 0.0f;
+
+	colors[3] = tcase->usage & GBM_BO_USE_SCANOUT ? 1.0f : 0.0f;
+	colors[4] = tcase->usage & GBM_BO_USE_RENDERING ? 0.66f : 0.0f;
+	colors[5] = tcase->usage & GBM_BO_USE_LINEAR ? 1.0f : 0.0f;
+
+	switch (tcase->format) {
+		case GBM_FORMAT_XRGB8888:
+			colors[6] = 1.0f;
+			colors[7] = 1.0f;
+			break;
+		case GBM_FORMAT_ARGB8888:
+			colors[7] = 1.0f;
+			colors[8] = 1.0f;
+			break;
+		default:
+			colors[6] = 0.33f;
+			colors[7] = 0.33f;
+			colors[8] = 0.33f;
+			break;
+	}
+}
+
+static void bo_lines(uint32_t height, float *lines /* sizeof(lines) / sizeof(lines[0]) == 9 */)
+{
+	/*
+	The screen is divided into sections using 3 lines as shown.
+	*----------*
+	|\ | /     |
+	| \|/      |
+	|  X       |
+	| /|\      |
+	|/ | \     |
+	*----------*
+
+	Lines are evaluated as positive or negative in the linear equation:
+	  Ax + By - C
+
+	Where the coffecicents A, B, and C appear in the array in the following order:
+	[ A,   B,   C ]
+	*/
+	// negative left of the following lines' intersection
+	lines[0] = 1;
+	lines[1] = 0;
+	lines[2] = height / 2;
+
+	// negative on lower-right triangle section
+	lines[3] = 1;
+	lines[4] = -1;
+	lines[5] = 0;
+
+	// negative on upper-left triangle section
+	lines[6] = 1;
+	lines[7] = 1;
+	lines[8] = height;
+}
+
+static bool test_case_draw_gl(struct bs_egl *egl, const struct test_case *tcase, struct gbm_bo *bo)
+{
+	bool success = true;
+	uint32_t width = gbm_bo_get_width(bo);
+	uint32_t height = gbm_bo_get_height(bo);
+
+	EGLImageKHR image = bs_egl_image_create_gbm(egl, bo);
+	if (image == EGL_NO_IMAGE_KHR) {
+		success = false;
+		bs_debug_error("failed to make image from buffer object");
+		goto out;
+	}
+
+	struct bs_egl_fb *fb = bs_egl_fb_new(egl, image);
+	if (!fb) {
+		success = false;
+		bs_debug_error("failed to make rednering framebuffer for buffer object");
+		bs_egl_image_destroy(egl, &image);
+		goto image_destroy;
+	}
+
+	const GLchar *vert =
+	    "attribute vec2 vPosition;\n"
+	    "void main() {\n"
+	    "  gl_Position = vec4(vPosition, 0, 1);\n"
+	    "}\n";
+
+	const GLchar *frag =
+	    "precision mediump float;\n"
+	    "uniform vec3 uColors[3];\n"
+	    "uniform vec3 uLines[3];\n"
+	    "void main() {\n"
+	    "  bool left = dot(uLines[0].xy, gl_FragCoord.xy) < uLines[0].z;\n"
+	    "  bool lower_right = dot(uLines[1].xy, gl_FragCoord.xy) < uLines[1].z;\n"
+	    "  bool upper_left = dot(uLines[2].xy, gl_FragCoord.xy) < uLines[2].z;\n"
+	    "  if (left && upper_left)\n"
+	    "    gl_FragColor = vec4(uColors[0], 1.0);\n"
+	    "  else if ((left && !upper_left) || (!left && lower_right))\n"
+	    "    gl_FragColor = vec4(uColors[1], 1.0);\n"
+	    "  else\n"
+	    "    gl_FragColor = vec4(uColors[2], 1.0);\n"
+	    "}\n";
+
+	struct bs_gl_program_create_binding bindings[] = {
+		{ 0, "vPosition" }, { 0, NULL },
+	};
+
+	GLuint program = bs_gl_program_create_vert_frag_bind(vert, frag, bindings);
+	if (!program) {
+		success = false;
+		bs_debug_error("failed to compile test case shader program");
+		goto fb_destroy;
+	}
+	GLint colors_location = glGetUniformLocation(program, "uColors");
+	GLint lines_location = glGetUniformLocation(program, "uLines");
+	if (colors_location == -1 || lines_location == -1) {
+		success = false;
+		bs_debug_error("failed to retrieve uniform location");
+		goto delete_program;
+	}
+
+	GLfloat colors[9];
+	test_case_colors(tcase, colors);
+
+	float lines[9];
+	bo_lines(height, lines);
+
+	// clang-format off
+	const GLfloat verts[] = {
+		-1.0f, -1.0f,
+		2.0f, -1.0f,
+		-1.0f, 2.0f,
+		2.0f, 2.0f,
+	};
+	// clang-format on
+
+	glBindFramebuffer(GL_FRAMEBUFFER, bs_egl_fb_name(fb));
+	glViewport(0, 0, (GLint)width, (GLint)height);
+
+	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT);
+
+	glUseProgram(program);
+	glUniform3fv(colors_location, 3, colors);
+	glUniform3fv(lines_location, 3, lines);
+
+	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, verts);
+	glEnableVertexAttribArray(0);
+	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+	glFinish();
+
+	glUseProgram(0);
+	glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+delete_program:
+	glDeleteProgram(program);
+fb_destroy:
+	bs_egl_fb_destroy(&fb);
+image_destroy:
+	bs_egl_image_destroy(egl, &image);
+out:
+	return success;
+}
+
+static bool test_case_draw_dma_buf(const struct test_case *tcase, struct bs_mapper *mapper,
+				   struct gbm_bo *bo)
+{
+	int bo_fd = gbm_bo_get_fd(bo);
+	if (bo_fd < 0) {
+		bs_debug_error("failed to get fd of bo");
+		return false;
+	}
+	uint32_t width = gbm_bo_get_width(bo);
+	uint32_t height = gbm_bo_get_height(bo);
+	uint32_t stride = gbm_bo_get_stride(bo);
+	void *map_data;
+	uint8_t *ptr = bs_mapper_map(mapper, bo, 0, &map_data);
+	if (ptr == MAP_FAILED) {
+		bs_debug_error("failed to mmap gbm bo");
+		return false;
+	}
+
+	float colors_float[9];
+	test_case_colors(tcase, colors_float);
+	uint8_t colors[9];
+	for (size_t i = 0; i < 9; i++)
+		colors[i] = (uint8_t)(colors_float[i] * 255.0f);
+
+	float lines[9];
+	bo_lines(height, lines);
+
+	for (uint32_t y = 0; y < height; y++) {
+		uint8_t *row_ptr = &ptr[y * stride];
+		for (uint32_t x = 0; x < width; x++) {
+			bool left = lines[0] * (float)x + lines[1] * (float)y < lines[2];
+			bool lower_right = lines[3] * (float)x + lines[4] * (float)y < lines[5];
+			bool upper_left = lines[6] * (float)x + lines[7] * (float)y < lines[8];
+
+			int color_index = 0;
+			if (left && upper_left)
+				color_index = 0;
+			else if ((left && !upper_left) || (!left && lower_right))
+				color_index = 1;
+			else
+				color_index = 2;
+
+			row_ptr[x * 4 + 0] = colors[color_index * 3 + 2];
+			row_ptr[x * 4 + 1] = colors[color_index * 3 + 1];
+			row_ptr[x * 4 + 2] = colors[color_index * 3 + 0];
+			row_ptr[x * 4 + 3] = 0;
+		}
+	}
+
+	bs_mapper_unmap(mapper, bo, map_data);
+
+	return true;
+}
+
+int main(int argc, char **argv)
+{
+	const struct test_case tcases[] = {
+		{ GBM_FORMAT_XRGB8888, 0, GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING },
+		{ GBM_FORMAT_XRGB8888, 0, GBM_BO_USE_SCANOUT | GBM_BO_USE_LINEAR },
+		{ GBM_FORMAT_ARGB8888, GBM_FORMAT_XRGB8888,
+		  GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING },
+		{ GBM_FORMAT_ARGB8888, GBM_FORMAT_XRGB8888,
+		  GBM_BO_USE_SCANOUT | GBM_BO_USE_LINEAR },
+	};
+	const size_t tcase_count = BS_ARRAY_LEN(tcases);
+
+	int display_fd = bs_drm_open_main_display();
+	if (display_fd < 0) {
+		bs_debug_error("failed to open card for display");
+		return 1;
+	}
+
+	struct gbm_device *gbm = gbm_create_device(display_fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm device");
+		return 1;
+	}
+
+	struct bs_drm_pipe pipe = { 0 };
+	if (!bs_drm_pipe_make(display_fd, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		return 1;
+	}
+
+	drmModeConnector *connector = drmModeGetConnector(display_fd, pipe.connector_id);
+	drmModeModeInfo *mode = &connector->modes[0];
+	uint32_t width = mode->hdisplay;
+	uint32_t height = mode->vdisplay;
+
+	struct bs_egl *egl = bs_egl_new();
+	if (!bs_egl_setup(egl)) {
+		bs_debug_error("failed to setup egl context");
+		return 1;
+	}
+
+	uint32_t fbs[BS_ARRAY_LEN(tcases)] = { 0 };
+	struct bs_mapper *mapper = bs_mapper_dma_buf_new();
+	if (mapper == NULL) {
+		bs_debug_error("failed to create mapper object");
+		return 1;
+	}
+
+	bool all_pass = true;
+	for (size_t tcase_index = 0; tcase_index < tcase_count; tcase_index++) {
+		const struct test_case *tcase = &tcases[tcase_index];
+
+		struct gbm_bo *bo = gbm_bo_create(gbm, width, height, tcase->format, tcase->usage);
+
+		if (!bo) {
+			all_pass = false;
+			printf("failed test case: ");
+			test_case_print(stdout, tcase);
+			printf("\n");
+			continue;
+		}
+
+		struct bs_drm_fb_builder *fb_builder = bs_drm_fb_builder_new();
+		bs_drm_fb_builder_gbm_bo(fb_builder, bo);
+		if (tcase->fb_format)
+			bs_drm_fb_builder_format(fb_builder, tcase->fb_format);
+		fbs[tcase_index] = bs_drm_fb_builder_create_fb(fb_builder);
+		bs_drm_fb_builder_destroy(&fb_builder);
+		if (!fbs[tcase_index]) {
+			bs_debug_error("failed to create framebuffer from buffer object");
+			return 1;
+		}
+
+		if (tcase->usage & GBM_BO_USE_LINEAR) {
+			if (!test_case_draw_dma_buf(tcase, mapper, bo)) {
+				bs_debug_error("failed to draw to buffer using vgem");
+				return 1;
+			}
+		} else if (tcase->usage & GBM_BO_USE_RENDERING) {
+			if (!test_case_draw_gl(egl, tcase, bo)) {
+				bs_debug_error("failed to draw to buffer using GL");
+				return 1;
+			}
+		}
+
+		// Reference held in kernel by the frame buffer.
+		gbm_bo_destroy(bo);
+	}
+
+	bs_mapper_destroy(mapper);
+
+	for (size_t tcase_index = 0; tcase_index < tcase_count; tcase_index++) {
+		const struct test_case *tcase = &tcases[tcase_index];
+		uint32_t fb_id = fbs[tcase_index];
+
+		if (fb_id == 0)
+			continue;
+
+		printf("displaying test case: ");
+		test_case_print(stdout, tcase);
+		printf("\n");
+
+		int ret = drmModeSetCrtc(display_fd, pipe.crtc_id, fb_id, 0 /* x */, 0 /* y */,
+					 &pipe.connector_id, 1 /* connector count */, mode);
+		if (ret) {
+			bs_debug_error("failed to set crtc: %d", ret);
+			return 1;
+		}
+		usleep(test_case_display_usec);
+	}
+
+	for (size_t tcase_index = 0; tcase_index < tcase_count; tcase_index++)
+		if (fbs[tcase_index] != 0)
+			drmModeRmFB(display_fd, fbs[tcase_index]);
+
+	bs_egl_destroy(&egl);
+
+	return all_pass ? 0 : 2;
+}
diff --git a/drm-tests/mapped_texture_test.c b/drm-tests/mapped_texture_test.c
new file mode 100644
index 0000000..e1cbac3
--- /dev/null
+++ b/drm-tests/mapped_texture_test.c
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2017 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/*
+  The mapped_texture_test test consists of:
+    * Importing external buffers as EGL Images
+    * Drawing an ellipse using the CPU
+    * Binding CPU drawn buffer as a texture and sampling from it
+    * Using KMS to scanout the resultant framebuffer
+ */
+
+#include <getopt.h>
+
+#include "bs_drm.h"
+
+// double buffering
+#define NUM_BUFFERS 2
+
+struct offscreen_buffer {
+	struct gbm_bo *bo;
+	GLuint tex;
+	EGLImageKHR image;
+	const struct bs_draw_format *draw_format;
+};
+
+struct framebuffer {
+	struct gbm_bo *bo;
+	uint32_t fb_id;
+	EGLImageKHR image;
+	struct bs_egl_fb *egl_fb;
+};
+
+struct gl_resources {
+	GLuint program;
+	GLuint vbo;
+};
+
+// clang-format off
+static const GLfloat vertices[] = {
+	// x       y     u     v
+	-0.25f, -0.25f, 0.0f, 0.0f, // Bottom left
+	-0.25f,  0.25f, 0.0f, 1.0f, // Top left
+	 0.25f,  0.25f, 1.0f, 1.0f, // Top right
+	 0.25f, -0.25f, 1.0f, 0.0f, // Bottom Right
+};
+
+static const int binding_xy = 0;
+static const int binding_uv = 1;
+
+static const GLubyte indices[] = {
+	0, 1, 2,
+	0, 2, 3
+};
+
+// clang-format on
+
+static const GLchar *vert =
+    "attribute vec2 xy;\n"
+    "attribute vec2 uv;\n"
+    "varying vec2 tex_coordinate;\n"
+    "void main() {\n"
+    "    gl_Position = vec4(xy, 0, 1);\n"
+    "    tex_coordinate = uv;\n"
+    "}\n";
+
+static const GLchar *frag =
+    "precision mediump float;\n"
+    "uniform sampler2D ellipse;\n"
+    "varying vec2 tex_coordinate;\n"
+    "void main() {\n"
+    "    gl_FragColor = texture2D(ellipse, tex_coordinate);\n"
+    "}\n";
+
+static bool create_framebuffer(int display_fd, struct gbm_device *gbm, struct bs_egl *egl,
+			       uint32_t width, uint32_t height, struct framebuffer *fb)
+{
+	fb->bo = gbm_bo_create(gbm, width, height, GBM_FORMAT_XRGB8888,
+			       GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+	if (!fb->bo) {
+		bs_debug_error("failed to create a gbm buffer.");
+		goto delete_gl_fb;
+	}
+
+	fb->fb_id = bs_drm_fb_create_gbm(fb->bo);
+	if (!fb->fb_id) {
+		bs_debug_error("failed to create framebuffer from buffer object");
+		goto delete_gl_image;
+	}
+
+	fb->image = bs_egl_image_create_gbm(egl, fb->bo);
+	if (fb->image == EGL_NO_IMAGE_KHR) {
+		bs_debug_error("failed to make image from buffer object");
+		goto delete_fb;
+	}
+
+	fb->egl_fb = bs_egl_fb_new(egl, fb->image);
+	if (!fb->egl_fb) {
+		bs_debug_error("failed to make rednering framebuffer for buffer object");
+		goto delete_gbm_bo;
+	}
+
+	return true;
+
+delete_gl_fb:
+	bs_egl_fb_destroy(&fb->egl_fb);
+delete_gl_image:
+	bs_egl_image_destroy(egl, &fb->image);
+delete_fb:
+	drmModeRmFB(display_fd, fb->fb_id);
+delete_gbm_bo:
+	gbm_bo_destroy(fb->bo);
+	return false;
+}
+
+static bool add_offscreen_texture(struct gbm_device *gbm, struct bs_egl *egl,
+				  struct offscreen_buffer *buffer, uint32_t width, uint32_t height,
+				  uint32_t flags)
+{
+	buffer->bo =
+	    gbm_bo_create(gbm, width, height, bs_get_pixel_format(buffer->draw_format), flags);
+	if (!buffer->bo) {
+		bs_debug_error("failed to allocate offscreen buffer object: format=%s \n",
+			       bs_get_format_name(buffer->draw_format));
+		goto destroy_offscreen_buffer;
+	}
+
+	buffer->image = bs_egl_image_create_gbm(egl, buffer->bo);
+	if (buffer->image == EGL_NO_IMAGE_KHR) {
+		bs_debug_error("failed to create offscreen egl image");
+		goto destroy_offscreen_bo;
+	}
+
+	glActiveTexture(GL_TEXTURE1);
+	glGenTextures(1, &buffer->tex);
+	glBindTexture(GL_TEXTURE_2D, buffer->tex);
+
+	if (!bs_egl_target_texture2D(egl, buffer->image)) {
+		bs_debug_error("failed to import egl image as texture");
+		goto destroy_offscreen_image;
+	}
+
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+	return true;
+
+destroy_offscreen_image:
+	glDeleteTextures(1, &buffer->tex);
+	bs_egl_image_destroy(egl, &buffer->image);
+destroy_offscreen_bo:
+	gbm_bo_destroy(buffer->bo);
+destroy_offscreen_buffer:
+	return false;
+}
+
+static bool init_gl(struct bs_egl_fb *fb, uint32_t width, uint32_t height,
+		    struct gl_resources *resources)
+{
+	struct bs_gl_program_create_binding bindings[] = {
+		{ binding_xy, "xy" }, { binding_uv, "uv" }, { 2, NULL },
+	};
+
+	resources->program = bs_gl_program_create_vert_frag_bind(vert, frag, bindings);
+	if (!resources->program) {
+		bs_debug_error("failed to compile shader program");
+		return false;
+	}
+
+	glGenBuffers(1, &resources->vbo);
+	glBindBuffer(GL_ARRAY_BUFFER, resources->vbo);
+	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+
+	glBindFramebuffer(GL_FRAMEBUFFER, bs_egl_fb_name(fb));
+	glViewport(0, 0, (GLint)width, (GLint)height);
+
+	glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
+	glClear(GL_COLOR_BUFFER_BIT);
+
+	glUseProgram(resources->program);
+
+	glUniform1i(glGetUniformLocation(resources->program, "ellipse"), 1);
+	glEnableVertexAttribArray(binding_xy);
+	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
+
+	glEnableVertexAttribArray(binding_uv);
+	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat),
+			      (void *)(2 * sizeof(GLfloat)));
+	return true;
+}
+
+static void draw_textured_quad(GLuint tex)
+{
+	glClear(GL_COLOR_BUFFER_BIT);
+	glActiveTexture(GL_TEXTURE1);
+	glBindTexture(GL_TEXTURE_2D, tex);
+	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, indices);
+}
+
+static const struct option longopts[] = {
+	{ "help", no_argument, NULL, 'h' },
+	{ "format", required_argument, NULL, 'f' },
+	{ "dma-buf", no_argument, NULL, 'b' },
+	{ "gem", no_argument, NULL, 'g' },
+	{ "dumb", no_argument, NULL, 'd' },
+	{ "tiled", no_argument, NULL, 't' },
+	{ 0, 0, 0, 0 },
+};
+
+static void print_help(const char *argv0)
+{
+	printf("Usage: %s [OPTIONS]\n", argv0);
+	printf(" -h, --help             Print help.\n");
+	printf(" -f, --format FOURCC    format of texture (defaults to ARGB8888)\n");
+	printf(" -b, --dma-buf  Use dma-buf mmap.\n");
+	printf(" -g, --gem      Use GEM map(by default).\n");
+	printf(" -d, --dumb     Use dump map.\n");
+}
+
+static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec,
+			      void *data)
+{
+	int *waiting_for_flip = data;
+	*waiting_for_flip = 0;
+}
+
+void flush_egl(struct bs_egl *egl, EGLImageKHR image)
+{
+	bs_egl_image_flush_external(egl, image);
+	EGLSyncKHR sync = bs_egl_create_sync(egl, EGL_SYNC_FENCE_KHR, NULL);
+	bs_egl_wait_sync(egl, sync, 0, EGL_FOREVER_KHR);
+	bs_egl_destroy_sync(egl, sync);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = 1;
+	int display_fd = bs_drm_open_main_display();
+	if (display_fd < 0) {
+		bs_debug_error("failed to open card for display");
+		goto out;
+	}
+
+	struct offscreen_buffer buffer;
+	buffer.draw_format = bs_get_draw_format_from_name("ARGB8888");
+	struct bs_mapper *mapper = NULL;
+	uint32_t flags = GBM_BO_USE_TEXTURING;
+
+	int c;
+	while ((c = getopt_long(argc, argv, "f:bgdh", longopts, NULL)) != -1) {
+		switch (c) {
+			case 'f':
+				if (!bs_parse_draw_format(optarg, &buffer.draw_format)) {
+					printf("choose the default format ARGB8888\n");
+				}
+				printf("format=%s\n", bs_get_format_name(buffer.draw_format));
+				break;
+			case 'b':
+				mapper = bs_mapper_dma_buf_new();
+				flags |= GBM_BO_USE_LINEAR;
+				printf("using dma-buf mmap\n");
+				break;
+			case 'g':
+				mapper = bs_mapper_gem_new();
+				flags |= GBM_BO_USE_SW_WRITE_OFTEN;
+				printf("using GEM map\n");
+				break;
+			case 'd':
+				mapper = bs_mapper_dumb_new(display_fd);
+				flags |= GBM_BO_USE_LINEAR;
+				printf("using dumb map\n");
+				break;
+			case 'h':
+			default:
+				print_help(argv[0]);
+				goto destroy_display_fd;
+		}
+	}
+
+	// Use gem map mapper by default, in case any arguments aren't selected.
+	if (!mapper) {
+		mapper = bs_mapper_gem_new();
+		flags |= GBM_BO_USE_SW_WRITE_OFTEN;
+		printf("using GEM map\n");
+	}
+
+	if (!mapper) {
+		bs_debug_error("failed to create mapper object");
+		goto destroy_display_fd;
+	}
+
+	uint32_t width;
+	uint32_t height;
+
+	struct gbm_device *gbm = gbm_create_device(display_fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm device");
+		goto destroy_mapper;
+	}
+
+	struct bs_drm_pipe pipe = { 0 };
+	if (!bs_drm_pipe_make(display_fd, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		goto destroy_gbm_device;
+	}
+
+	drmModeConnector *connector = drmModeGetConnector(display_fd, pipe.connector_id);
+	drmModeModeInfo *mode = &connector->modes[0];
+	width = mode->hdisplay;
+	height = mode->vdisplay;
+
+	struct bs_egl *egl = bs_egl_new();
+	if (!bs_egl_setup(egl)) {
+		bs_debug_error("failed to setup egl context");
+		goto destroy_gbm_device;
+	}
+
+	struct framebuffer fbs[NUM_BUFFERS] = {};
+	uint32_t front_buffer = 0;
+	for (size_t i = 0; i < NUM_BUFFERS; i++) {
+		if (!create_framebuffer(display_fd, gbm, egl, width, height, &fbs[i])) {
+			bs_debug_error("failed to create framebuffer");
+			goto delete_framebuffers;
+		}
+	}
+
+	if (!add_offscreen_texture(gbm, egl, &buffer, width / 4, height / 4, flags)) {
+		bs_debug_error("failed to create offscreen texture");
+		goto destroy_offscreen_buffer;
+	}
+
+	const struct framebuffer *back_fb = &fbs[front_buffer ^ 1];
+	struct gl_resources resources;
+	if (!init_gl(back_fb->egl_fb, width, height, &resources)) {
+		bs_debug_error("failed to initialize GL resources.\n");
+		goto destroy_gl_resources;
+	}
+
+	flush_egl(egl, back_fb->image);
+
+	ret = drmModeSetCrtc(display_fd, pipe.crtc_id, fbs[front_buffer].fb_id, 0 /* x */,
+			     0 /* y */, &pipe.connector_id, 1 /* connector count */, mode);
+	if (ret) {
+		bs_debug_error("failed to set crtc: %d", ret);
+		goto destroy_gl_resources;
+	}
+
+	drmEventContext evctx = {
+		.version = DRM_EVENT_CONTEXT_VERSION, .page_flip_handler = page_flip_handler,
+	};
+	fd_set fds;
+
+	// The test takes about 2 seconds to complete.
+	const size_t test_frames = 120;
+	for (size_t i = 0; i < test_frames; i++) {
+		int waiting_for_flip = 1;
+
+		const struct framebuffer *back_fb = &fbs[front_buffer ^ 1];
+		glBindFramebuffer(GL_FRAMEBUFFER, bs_egl_fb_name(back_fb->egl_fb));
+		if (!bs_draw_ellipse(mapper, buffer.bo, buffer.draw_format,
+				     (float)i / test_frames)) {
+			bs_debug_error("failed to draw to buffer");
+			goto destroy_gl_resources;
+		}
+
+		draw_textured_quad(buffer.tex);
+
+		flush_egl(egl, back_fb->image);
+
+		ret = drmModePageFlip(display_fd, pipe.crtc_id, back_fb->fb_id,
+				      DRM_MODE_PAGE_FLIP_EVENT, &waiting_for_flip);
+		if (ret) {
+			bs_debug_error("failed to queue page flip");
+			goto destroy_gl_resources;
+		}
+
+		while (waiting_for_flip) {
+			FD_ZERO(&fds);
+			FD_SET(0, &fds);
+			FD_SET(display_fd, &fds);
+			int ret = select(display_fd + 1, &fds, NULL, NULL, NULL);
+			if (ret < 0) {
+				bs_debug_error("select err: %s", strerror(errno));
+				goto destroy_gl_resources;
+			} else if (FD_ISSET(0, &fds)) {
+				bs_debug_error("exit due to user-input");
+				goto destroy_gl_resources;
+			} else if (FD_ISSET(display_fd, &fds)) {
+				drmHandleEvent(display_fd, &evctx);
+			}
+		}
+
+		front_buffer ^= 1;
+	}
+
+destroy_gl_resources:
+	glBindBuffer(GL_ARRAY_BUFFER, 0);
+	glUseProgram(0);
+	glBindFramebuffer(GL_FRAMEBUFFER, 0);
+	glBindTexture(GL_TEXTURE_2D, 0);
+	glDeleteProgram(resources.program);
+	glDeleteBuffers(1, &resources.vbo);
+destroy_offscreen_buffer:
+	glDeleteTextures(1, &buffer.tex);
+	bs_egl_image_destroy(egl, &buffer.image);
+	gbm_bo_destroy(buffer.bo);
+delete_framebuffers:
+	for (size_t i = 0; i < NUM_BUFFERS; i++) {
+		bs_egl_fb_destroy(&fbs[i].egl_fb);
+		bs_egl_image_destroy(egl, &fbs[i].image);
+		drmModeRmFB(display_fd, fbs[i].fb_id);
+		gbm_bo_destroy(fbs[i].bo);
+	}
+	bs_egl_destroy(&egl);
+destroy_gbm_device:
+	gbm_device_destroy(gbm);
+destroy_mapper:
+	bs_mapper_destroy(mapper);
+destroy_display_fd:
+	close(display_fd);
+out:
+	return ret;
+}
diff --git a/drm-tests/mmap_test.c b/drm-tests/mmap_test.c
new file mode 100644
index 0000000..321e33e
--- /dev/null
+++ b/drm-tests/mmap_test.c
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2017 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <getopt.h>
+
+#include "bs_drm.h"
+
+#define BUFFERS 2
+#define NUM_FRAMES 0x40
+
+struct framebuffer {
+	struct gbm_bo *bo;
+	uint32_t id;
+};
+
+struct context {
+	int display_fd;
+	uint32_t crtc_id;
+
+	struct framebuffer fbs[BUFFERS];
+	struct bs_mapper *mapper;
+
+	int vgem_device_fd;
+};
+
+static void disable_psr()
+{
+	const char psr_path[] = "/sys/module/i915/parameters/enable_psr";
+	int psr_fd = open(psr_path, O_WRONLY);
+
+	if (psr_fd < 0)
+		return;
+
+	if (write(psr_fd, "0", 1) == -1) {
+		bs_debug_error("failed to disable psr");
+	} else {
+		printf("disabled psr");
+	}
+
+	close(psr_fd);
+}
+
+static void do_fixes()
+{
+	disable_psr();
+}
+
+#define STEP_SKIP 0
+#define STEP_MMAP 1
+#define STEP_FAULT 2
+#define STEP_FLIP 3
+#define STEP_DRAW 4
+
+static void show_sequence(const int *sequence)
+{
+	int sequence_subindex;
+	printf("starting sequence: ");
+	for (sequence_subindex = 0; sequence_subindex < 4; sequence_subindex++) {
+		switch (sequence[sequence_subindex]) {
+			case STEP_SKIP:
+				break;
+			case STEP_MMAP:
+				printf("mmap ");
+				break;
+			case STEP_FAULT:
+				printf("fault ");
+				break;
+			case STEP_FLIP:
+				printf("flip ");
+				break;
+			case STEP_DRAW:
+				printf("draw ");
+				break;
+			default:
+				bs_debug_error("<unknown step %d> (aborting!)",
+					       sequence[sequence_subindex]);
+				abort();
+				break;
+		}
+	}
+	printf("\n");
+}
+
+static void draw(struct context *ctx)
+{
+	// Run the drawing routine with the key driver events in different
+	// sequences.
+	const int sequences[4][4] = {
+		{ STEP_MMAP, STEP_FAULT, STEP_FLIP, STEP_DRAW },
+		{ STEP_MMAP, STEP_FLIP, STEP_DRAW, STEP_SKIP },
+		{ STEP_MMAP, STEP_DRAW, STEP_FLIP, STEP_SKIP },
+		{ STEP_FLIP, STEP_MMAP, STEP_DRAW, STEP_SKIP },
+	};
+
+	int sequence_index = 0;
+	int sequence_subindex = 0;
+
+	int fb_idx = 1;
+
+	for (sequence_index = 0; sequence_index < 4; sequence_index++) {
+		show_sequence(sequences[sequence_index]);
+		for (int frame_index = 0; frame_index < NUM_FRAMES; frame_index++) {
+			struct framebuffer *fb = &ctx->fbs[fb_idx];
+			size_t bo_stride = gbm_bo_get_plane_stride(fb->bo, 0);
+			size_t bo_size = gbm_bo_get_plane_size(fb->bo, 0);
+			const uint32_t width = gbm_bo_get_width(fb->bo);
+			const uint32_t height = gbm_bo_get_height(fb->bo);
+			uint32_t *bo_ptr;
+			volatile uint32_t *ptr;
+			void *map_data;
+
+			for (sequence_subindex = 0; sequence_subindex < 4; sequence_subindex++) {
+				switch (sequences[sequence_index][sequence_subindex]) {
+					case STEP_MMAP:
+						bo_ptr = bs_mapper_map(ctx->mapper, fb->bo, 0,
+								       &map_data);
+						if (bo_ptr == MAP_FAILED)
+							bs_debug_error("failed to mmap gbm bo");
+
+						ptr = bo_ptr;
+						break;
+
+					case STEP_FAULT:
+						*ptr = 1234567;
+						break;
+
+					case STEP_FLIP:
+						drmModePageFlip(ctx->display_fd, ctx->crtc_id,
+								ctx->fbs[fb_idx].id, 0, NULL);
+						break;
+
+					case STEP_DRAW:
+						for (ptr = bo_ptr;
+						     ptr < bo_ptr + (bo_size / sizeof(*bo_ptr));
+						     ptr++) {
+							int y = ((void *)ptr - (void *)bo_ptr) /
+								bo_stride;
+							int x = ((void *)ptr - (void *)bo_ptr -
+								 bo_stride * y) /
+								sizeof(*ptr);
+							x -= frame_index * (width / NUM_FRAMES);
+							y -= frame_index * (height / NUM_FRAMES);
+							*ptr = 0xff000000;
+							if (x * x + y * y <
+							    frame_index * frame_index)
+								*ptr |= (frame_index % 0x100) << 8;
+							else
+								*ptr |= 0xff |
+									(sequence_index * 64 << 16);
+						}
+						break;
+
+					case STEP_SKIP:
+					default:
+						break;
+				}
+			}
+
+			bs_mapper_unmap(ctx->mapper, fb->bo, map_data);
+
+			usleep(1e6 / 120); /* 120 Hz */
+
+			fb_idx = fb_idx ^ 1;
+		}
+	}
+}
+
+static const struct option longopts[] = {
+	{ "help", no_argument, NULL, 'h' },
+	{ "dma-buf", no_argument, NULL, 'b' },
+	{ "gem", no_argument, NULL, 'g' },
+	{ "dumb", no_argument, NULL, 'd' },
+	{ "vgem", no_argument, NULL, 'v' },
+	{ "scanout", no_argument, NULL, 's' },
+	{ 0, 0, 0, 0 },
+};
+
+static void print_help(const char *argv0)
+{
+	printf("Usage: %s [OPTIONS]\n", argv0);
+	printf(" -h, --help     Print help.\n");
+	printf(" -b, --dma-buf  Use dma-buf mmap (by default).\n");
+	printf(" -g, --gem      Use GEM map.\n");
+	printf(" -d, --dumb     Use dump map.\n");
+	printf(" -v, --vgem     Use vgem dump map.\n");
+	printf(" -s, --scanout  Use buffer optimized for scanout.\n");
+}
+
+int main(int argc, char **argv)
+{
+	struct context ctx = { 0 };
+
+	do_fixes();
+
+	ctx.display_fd = bs_drm_open_main_display();
+	if (ctx.display_fd < 0) {
+		bs_debug_error("failed to open card for display");
+		return 1;
+	}
+
+	struct gbm_device *gbm = gbm_create_device(ctx.display_fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm device");
+		return 1;
+	}
+
+	int c;
+	uint32_t flags = GBM_BO_USE_SCANOUT;
+	while ((c = getopt_long(argc, argv, "bgdvsh", longopts, NULL)) != -1) {
+		switch (c) {
+			case 'b':
+				ctx.mapper = bs_mapper_dma_buf_new();
+				flags |= GBM_BO_USE_LINEAR;
+				printf("started dma-buf mmap.\n");
+				break;
+			case 'g':
+				ctx.mapper = bs_mapper_gem_new();
+				flags |= GBM_BO_USE_SW_READ_OFTEN | GBM_BO_USE_SW_WRITE_OFTEN;
+				printf("started GEM map.\n");
+				break;
+			case 'd':
+				ctx.mapper = bs_mapper_dumb_new(gbm_device_get_fd(gbm));
+				flags |= GBM_BO_USE_LINEAR;
+				printf("started dumb map.\n");
+				break;
+			case 'v':
+				ctx.vgem_device_fd = bs_drm_open_vgem();
+				ctx.mapper = bs_mapper_dumb_new(ctx.vgem_device_fd);
+				printf("started vgem map.\n");
+				break;
+			case 's':
+				flags = GBM_BO_USE_SCANOUT;
+				break;
+			case 'h':
+			default:
+				print_help(argv[0]);
+				return 1;
+		}
+	}
+
+	// Use dma-buf mmap by default, in case any arguments aren't selected.
+	if (!ctx.mapper) {
+		ctx.mapper = bs_mapper_dma_buf_new();
+		printf("started dma-buf mmap.\n");
+	}
+
+	if (ctx.mapper == NULL) {
+		bs_debug_error("failed to create mapper object");
+		return 1;
+	}
+
+	struct bs_drm_pipe pipe = { 0 };
+	if (!bs_drm_pipe_make(ctx.display_fd, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		return 1;
+	}
+
+	drmModeConnector *connector = drmModeGetConnector(ctx.display_fd, pipe.connector_id);
+	drmModeModeInfo *mode = &connector->modes[0];
+	ctx.crtc_id = pipe.crtc_id;
+
+	printf("display size: %ux%u\n", mode->hdisplay, mode->vdisplay);
+
+	for (size_t fb_index = 0; fb_index < BUFFERS; ++fb_index) {
+		struct framebuffer *fb = &ctx.fbs[fb_index];
+		fb->bo =
+		    gbm_bo_create(gbm, mode->hdisplay, mode->vdisplay, GBM_FORMAT_XRGB8888, flags);
+
+		if (!fb->bo) {
+			bs_debug_error("failed to create buffer object");
+			return 1;
+		}
+
+		fb->id = bs_drm_fb_create_gbm(fb->bo);
+		if (fb->id == 0) {
+			bs_debug_error("failed to create fb");
+			return 1;
+		}
+	}
+
+	if (drmModeSetCrtc(ctx.display_fd, pipe.crtc_id, ctx.fbs[0].id, 0, 0, &pipe.connector_id, 1,
+			   mode)) {
+		bs_debug_error("failed to set CRTC");
+		return 1;
+	}
+
+	draw(&ctx);
+
+	return 0;
+}
diff --git a/drm-tests/null_platform_test.c b/drm-tests/null_platform_test.c
new file mode 100644
index 0000000..9eebfd0
--- /dev/null
+++ b/drm-tests/null_platform_test.c
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2014 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+#include <getopt.h>
+
+#define NUM_BUFFERS 2
+
+static uint32_t allowed_formats[] = {
+	GBM_FORMAT_XRGB8888, GBM_FORMAT_XBGR8888, GBM_FORMAT_XRGB2101010, GBM_FORMAT_XBGR2101010,
+};
+
+const size_t allowed_formats_length = BS_ARRAY_LEN(allowed_formats);
+
+static GLuint solid_shader_create()
+{
+	const GLchar *vert =
+	    "attribute vec4 vPosition;\n"
+	    "attribute vec4 vColor;\n"
+	    "varying vec4 vFillColor;\n"
+	    "void main() {\n"
+	    "  gl_Position = vPosition;\n"
+	    "  vFillColor = vColor;\n"
+	    "}\n";
+
+	const GLchar *frag =
+	    "precision mediump float;\n"
+	    "varying vec4 vFillColor;\n"
+	    "void main() {\n"
+	    "  gl_FragColor = vFillColor;\n"
+	    "}\n";
+
+	struct bs_gl_program_create_binding bindings[] = {
+		{ 0, "vPosition" }, { 1, "vColor" }, { 0, NULL },
+	};
+
+	return bs_gl_program_create_vert_frag_bind(vert, frag, bindings);
+}
+
+static float f(int i)
+{
+	int a = i % 40;
+	int b = (i / 40) % 6;
+	switch (b) {
+		case 0:
+		case 1:
+			return 0.0f;
+		case 3:
+		case 4:
+			return 1.0f;
+		case 2:
+			return (a / 40.0f);
+		case 5:
+			return 1.0f - (a / 40.0f);
+		default:
+			return 0.0f;
+	}
+}
+
+static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec,
+			      void *data)
+{
+	int *waiting_for_flip = data;
+	*waiting_for_flip = 0;
+}
+
+static uint32_t find_format(char *fourcc)
+{
+	if (!fourcc || strlen(fourcc) < 4)
+		return 0;
+
+	uint32_t format = fourcc_code(fourcc[0], fourcc[1], fourcc[2], fourcc[3]);
+	for (int i = 0; i < allowed_formats_length; i++) {
+		if (allowed_formats[i] == format)
+			return format;
+	}
+
+	return 0;
+}
+
+static const struct option longopts[] = {
+	{ "format", required_argument, NULL, 'f' },
+	{ "test-page-flip-format-change", required_argument, NULL, 'p' },
+	{ "help", no_argument, NULL, 'h' },
+	{ 0, 0, 0, 0 },
+};
+
+static void print_help(const char *argv0)
+{
+	char allowed_formats_string[allowed_formats_length * 6];
+	int i;
+	for (i = 0; i < allowed_formats_length; i++) {
+		uint32_t format = allowed_formats[i];
+		sprintf(allowed_formats_string + i * 6, "%.4s, ", (char *)&format);
+	}
+
+	allowed_formats_string[i * 6 - 2] = 0;
+
+	// clang-format off
+	printf("usage: %s [OPTIONS] [drm_device_path]\n", argv0);
+	printf("  -f, --format <format>			    defines the fb format.\n");
+	printf("  -p, --test-page-flip-format-change <format>	test page flips alternating formats.\n");
+	printf("  -h, --help		    show help\n");
+	printf("\n");
+        printf(" <format> must be one of [ %s ].\n", allowed_formats_string);
+	printf("\n");
+	printf("\n");
+	// clang-format on
+}
+
+int main(int argc, char **argv)
+{
+	int fd = -1;
+	bool help_flag = false;
+	uint32_t format = GBM_FORMAT_XRGB8888;
+	uint32_t test_page_flip_format_change = 0;
+
+	int c = -1;
+	while ((c = getopt_long(argc, argv, "hp:f:", longopts, NULL)) != -1) {
+		switch (c) {
+			case 'p':
+				test_page_flip_format_change = find_format(optarg);
+				if (!test_page_flip_format_change)
+					help_flag = true;
+				break;
+			case 'f':
+				format = find_format(optarg);
+				if (!format)
+					help_flag = true;
+				break;
+			case 'h':
+				help_flag = true;
+				break;
+		}
+	}
+
+	if (help_flag) {
+		print_help(*argv);
+	}
+
+	if (optind < argc) {
+		fd = open(argv[optind], O_RDWR);
+		if (fd < 0) {
+			bs_debug_error("failed to open card %s", argv[optind]);
+			return 1;
+		}
+	} else {
+		fd = bs_drm_open_main_display();
+		if (fd < 0) {
+			bs_debug_error("failed to open card for display");
+			return 1;
+		}
+	}
+
+	struct gbm_device *gbm = gbm_create_device(fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm");
+		return 1;
+	}
+
+	struct bs_drm_pipe pipe = { 0 };
+	if (!bs_drm_pipe_make(fd, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		return 1;
+	}
+
+	drmModeConnector *connector = drmModeGetConnector(fd, pipe.connector_id);
+	assert(connector);
+	drmModeModeInfo *mode = &connector->modes[0];
+
+	struct bs_egl *egl = bs_egl_new();
+	if (!bs_egl_setup(egl)) {
+		bs_debug_error("failed to setup egl context");
+		return 1;
+	}
+
+	struct gbm_bo *bos[NUM_BUFFERS];
+	uint32_t ids[NUM_BUFFERS];
+	struct bs_egl_fb *egl_fbs[NUM_BUFFERS];
+	EGLImageKHR egl_images[NUM_BUFFERS];
+	for (size_t fb_index = 0; fb_index < NUM_BUFFERS; fb_index++) {
+		if (test_page_flip_format_change && fb_index) {
+			format = test_page_flip_format_change;
+		}
+
+		bos[fb_index] = gbm_bo_create(gbm, mode->hdisplay, mode->vdisplay, format,
+					      GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+		if (bos[fb_index] == NULL) {
+			bs_debug_error("failed to allocate framebuffer");
+			return 1;
+		}
+
+		ids[fb_index] = bs_drm_fb_create_gbm(bos[fb_index]);
+		if (ids[fb_index] == 0) {
+			bs_debug_error("failed to create framebuffer id");
+			return 1;
+		}
+
+		EGLImageKHR egl_image = bs_egl_image_create_gbm(egl, bos[fb_index]);
+		if (egl_image == EGL_NO_IMAGE_KHR) {
+			bs_debug_error("failed to create EGLImageKHR from framebuffer");
+			return 1;
+		}
+
+		egl_fbs[fb_index] = bs_egl_fb_new(egl, egl_image);
+		if (!egl_fbs[fb_index]) {
+			bs_debug_error("failed to create framebuffer from EGLImageKHR");
+			return 1;
+		}
+		egl_images[fb_index] = egl_image;
+	}
+
+	int ret = drmModeSetCrtc(fd, pipe.crtc_id, ids[0], 0 /* x */, 0 /* y */, &pipe.connector_id,
+				 1 /* connector count */, mode);
+	if (ret) {
+		bs_debug_error("failed to set CRTC");
+		return 1;
+	}
+
+	GLuint program = solid_shader_create();
+	if (!program) {
+		bs_debug_error("failed to create solid shader");
+		return 1;
+	}
+
+	int fb_idx = 1;
+	for (int i = 0; i <= 500; i++) {
+		int waiting_for_flip = 1;
+		// clang-format off
+		GLfloat verts[] = {
+			0.0f, -0.5f, 0.0f,
+			-0.5f, 0.5f, 0.0f,
+			0.5f, 0.5f, 0.0f
+		};
+		GLfloat colors[] = {
+			1.0f, 0.0f, 0.0f, 1.0f,
+			0.0f, 1.0f, 0.0f, 1.0f,
+			0.0f, 0.0f, 1.0f, 1.0f
+		};
+		// clang-format on
+
+		glBindFramebuffer(GL_FRAMEBUFFER, bs_egl_fb_name(egl_fbs[fb_idx]));
+		glViewport(0, 0, (GLint)mode->hdisplay, (GLint)mode->vdisplay);
+
+		glClearColor(f(i), f(i + 80), f(i + 160), 0.0f);
+		glClear(GL_COLOR_BUFFER_BIT);
+
+		glUseProgram(program);
+		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, verts);
+		glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, colors);
+		glEnableVertexAttribArray(0);
+		glEnableVertexAttribArray(1);
+		glDrawArrays(GL_TRIANGLES, 0, 3);
+
+		usleep(1e6 / 120); /* 120 Hz */
+		glFinish();
+
+		if (!bs_egl_image_flush_external(egl, egl_images[fb_idx])) {
+			bs_debug_error("failed to call image_flush_external");
+			return 1;
+		}
+
+		ret = drmModePageFlip(fd, pipe.crtc_id, ids[fb_idx], DRM_MODE_PAGE_FLIP_EVENT,
+				      &waiting_for_flip);
+		if (ret) {
+			bs_debug_error("failed page flip: %d", ret);
+			return 1;
+		}
+
+		while (waiting_for_flip) {
+			drmEventContext evctx = {
+				.version = DRM_EVENT_CONTEXT_VERSION,
+				.page_flip_handler = page_flip_handler,
+			};
+
+			fd_set fds;
+			FD_ZERO(&fds);
+			FD_SET(fd, &fds);
+
+			ret = select(fd + 1, &fds, NULL, NULL, NULL);
+			if (ret < 0) {
+				bs_debug_error("select err: %s", strerror(errno));
+				return 1;
+			} else if (ret == 0) {
+				bs_debug_error("select timeout");
+				return 1;
+			}
+			ret = drmHandleEvent(fd, &evctx);
+			if (ret) {
+				bs_debug_error("failed to wait for page flip: %d", ret);
+				return 1;
+			}
+		}
+		fb_idx = fb_idx ^ 1;
+	}
+
+	for (size_t fb_index = 0; fb_index < NUM_BUFFERS; fb_index++) {
+		bs_egl_fb_destroy(&egl_fbs[fb_index]);
+		bs_egl_image_destroy(egl, &egl_images[fb_index]);
+		drmModeRmFB(fd, ids[fb_idx]);
+		gbm_bo_destroy(bos[fb_index]);
+	}
+
+	bs_egl_destroy(&egl);
+	gbm_device_destroy(gbm);
+	close(fd);
+	return 0;
+}
diff --git a/drm-tests/plane_test.c b/drm-tests/plane_test.c
new file mode 100644
index 0000000..bb54f52
--- /dev/null
+++ b/drm-tests/plane_test.c
@@ -0,0 +1,568 @@
+/*
+ * Copyright 2015 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <ctype.h>
+#include <getopt.h>
+#include <math.h>
+
+#include "bs_drm.h"
+
+#define MAX_TEST_PLANES 4
+
+static int64_t ns_since(struct timespec *since)
+{
+	struct timespec now;
+	clock_gettime(CLOCK_MONOTONIC, &now);
+
+	return (int64_t)(now.tv_sec - since->tv_sec) * 1000000000 + (int64_t)now.tv_nsec -
+	       (int64_t)since->tv_nsec;
+}
+
+static drmModeModeInfoPtr find_best_mode(int mode_count, drmModeModeInfoPtr modes)
+{
+	if (mode_count <= 0 || modes == NULL)
+		return NULL;
+
+	for (int m = 0; m < mode_count; m++)
+		if (modes[m].type & DRM_MODE_TYPE_PREFERRED)
+			return &modes[m];
+
+	return &modes[0];
+}
+
+static bool is_format_supported(uint32_t format_count, uint32_t *formats, uint32_t format)
+{
+	for (uint32_t i = 0; i < format_count; i++)
+		if (formats[i] == format)
+			return true;
+	return false;
+}
+
+static bool find_overlay_planes(int fd, uint32_t crtc_id, size_t plane_count, uint32_t *formats,
+				uint32_t *plane_ids)
+{
+	drmModeRes *res = drmModeGetResources(fd);
+	if (res == NULL) {
+		bs_debug_error("failed to get drm resources");
+		return false;
+	}
+
+	uint32_t crtc_mask = 0;
+	for (int crtc_index = 0; crtc_index < res->count_crtcs; crtc_index++) {
+		if (res->crtcs[crtc_index] == crtc_id) {
+			crtc_mask = (1 << crtc_index);
+			break;
+		}
+	}
+	if (crtc_mask == 0) {
+		bs_debug_error("invalid crtc id %u", crtc_id);
+		drmModeFreeResources(res);
+		return false;
+	}
+
+	drmModePlaneRes *plane_res = drmModeGetPlaneResources(fd);
+	if (plane_res == NULL) {
+		bs_debug_error("failed to get plane resources");
+		drmModeFreeResources(res);
+		return false;
+	}
+
+	for (size_t out_index = 0; out_index < plane_count; out_index++)
+		plane_ids[out_index] = 0;
+
+	size_t remaining_out = plane_count;
+	for (uint32_t plane_index = 0; remaining_out > 0 && plane_index < plane_res->count_planes;
+	     plane_index++) {
+		drmModePlane *plane = drmModeGetPlane(fd, plane_res->planes[plane_index]);
+		if (plane == NULL) {
+			bs_debug_error("failed to get plane id %u", plane_res->planes[plane_index]);
+			continue;
+		}
+
+		size_t out_index;
+		for (out_index = 0; out_index < plane_count; out_index++) {
+			if (plane_ids[out_index] == 0 &&
+			    is_format_supported(plane->count_formats, plane->formats,
+						formats[out_index]))
+				break;
+		}
+
+		if ((plane->possible_crtcs & crtc_mask) == 0 || out_index == plane_count) {
+			drmModeFreePlane(plane);
+			continue;
+		}
+
+		drmModeObjectPropertiesPtr props =
+		    drmModeObjectGetProperties(fd, plane->plane_id, DRM_MODE_OBJECT_PLANE);
+		for (uint32_t prop_index = 0; prop_index < props->count_props; ++prop_index) {
+			drmModePropertyPtr prop = drmModeGetProperty(fd, props->props[prop_index]);
+			if (strcmp(prop->name, "type")) {
+				drmModeFreeProperty(prop);
+				continue;
+			}
+
+			uint64_t desired_value = 0;
+			bool has_value = false;
+			for (int enum_index = 0; enum_index < prop->count_enums; enum_index++) {
+				struct drm_mode_property_enum *penum = &prop->enums[enum_index];
+				if (!strcmp(penum->name, "Overlay")) {
+					desired_value = penum->value;
+					has_value = true;
+					break;
+				}
+			}
+			drmModeFreeProperty(prop);
+
+			if (has_value && desired_value == props->prop_values[prop_index]) {
+				plane_ids[out_index] = plane->plane_id;
+				remaining_out--;
+			}
+
+			break;
+		}
+
+		drmModeFreeObjectProperties(props);
+		drmModeFreePlane(plane);
+	}
+
+	drmModeFreePlaneResources(plane_res);
+	drmModeFreeResources(res);
+
+	return remaining_out == 0;
+}
+
+struct test_plane {
+	const struct bs_draw_format *format;
+
+	bool has_bo_size;
+	uint32_t bo_w;
+	uint32_t bo_h;
+
+	bool has_dst_position;
+	int32_t dst_x;
+	int32_t dst_y;
+
+	int32_t dst_center_x;
+	int32_t dst_center_y;
+
+	int32_t dst_vx;
+	int32_t dst_vy;
+
+	bool has_dst_size;
+	uint32_t dst_w;
+	uint32_t dst_h;
+
+	uint32_t dst_min_w;
+	uint32_t dst_max_w;
+	uint32_t dst_min_h;
+	uint32_t dst_max_h;
+
+	bool has_dst_scale;
+	uint32_t dst_downscale_factor;
+	uint32_t dst_upscale_factor;
+
+	int32_t src_x;
+	int32_t src_y;
+
+	int32_t src_vx;
+	int32_t src_vy;
+
+	bool has_src_size;
+	uint32_t src_w;
+	uint32_t src_h;
+
+	struct gbm_bo *bo;
+	uint32_t fb_id;
+};
+
+static bool update_test_plane(int step, int32_t max_x, int32_t max_y, struct test_plane *tp)
+{
+	bool needs_set = (step == 0);
+	if (tp->has_dst_scale) {
+		float scale_factor = (sinf(step / 120.0f) / 2.0f + 0.5f);
+		scale_factor = powf(scale_factor, 4);
+		tp->dst_w = tp->dst_min_w + scale_factor * (tp->dst_max_w - tp->dst_min_w);
+		tp->dst_h = tp->dst_min_h + scale_factor * (tp->dst_max_h - tp->dst_min_h);
+		needs_set = true;
+	}
+
+	if (tp->dst_vx != 0) {
+		tp->dst_center_x += tp->dst_vx;
+		if (tp->dst_center_x > max_x || tp->dst_center_x < 0) {
+			tp->dst_vx = -tp->dst_vx;
+			tp->dst_x += tp->dst_vx * 2;
+		}
+		needs_set = true;
+	}
+	tp->dst_x = tp->dst_center_x - tp->dst_w / 2;
+
+	if (tp->dst_vy != 0) {
+		tp->dst_center_y += tp->dst_vy;
+		if (tp->dst_center_y > max_y || tp->dst_center_y < 0) {
+			tp->dst_vy = -tp->dst_vy;
+			tp->dst_y += tp->dst_vy * 2;
+		}
+		needs_set = true;
+	}
+	tp->dst_y = tp->dst_center_y - tp->dst_h / 2;
+
+	if (tp->src_vx != 0) {
+		tp->src_x += tp->src_vx;
+		if (tp->src_x + tp->src_w > gbm_bo_get_width(tp->bo) || tp->src_x < 0) {
+			tp->src_vx = -tp->src_vx;
+			tp->src_x += tp->src_vx * 2;
+		}
+		needs_set = true;
+	}
+
+	if (tp->src_vy != 0) {
+		tp->src_y += tp->src_vy;
+		if (tp->src_y + tp->src_h > gbm_bo_get_height(tp->bo) || tp->src_y < 0) {
+			tp->src_vy = -tp->src_vy;
+			tp->src_y += tp->src_vy * 2;
+		}
+		needs_set = true;
+	}
+	return needs_set;
+}
+
+static bool parse_size(const char *str, uint32_t *w, uint32_t *h)
+{
+	if (sscanf(str, "%ux%u", w, h) == 2)
+		return true;
+
+	printf("unrecognized size format \"%s\"\n", str);
+
+	return false;
+}
+
+static bool parse_scale(const char *str, uint32_t *down, uint32_t *up)
+{
+	if (sscanf(str, "%u/%u", down, up) == 2)
+		return true;
+
+	printf("unrecognized scale format \"%s\"\n", str);
+
+	return false;
+}
+
+static bool parse_rect(const char *str, int32_t *x, int32_t *y, uint32_t *w, uint32_t *h,
+		       bool *has_position, bool *has_size)
+{
+	if (sscanf(str, "%d,%d,%u,%u", x, y, w, h) == 4) {
+		if (has_position)
+			*has_position = true;
+		if (has_size)
+			*has_size = true;
+		return true;
+	}
+
+	if (sscanf(str, "%d,%d", x, y) == 2) {
+		if (has_position)
+			*has_position = true;
+		if (has_size)
+			*has_size = false;
+		return true;
+	}
+
+	if (sscanf(str, "%ux%u", w, h) == 2) {
+		if (has_position)
+			*has_position = false;
+		if (has_size)
+			*has_size = true;
+		return true;
+	}
+
+	printf("unrecognized rectangle format \"%s\"\n", str);
+
+	return false;
+}
+
+static const struct option longopts[] = {
+	{ "plane", no_argument, NULL, 'p' },
+	{ "format", required_argument, NULL, 'f' },
+	{ "size", required_argument, NULL, 'z' },
+	{ "scale", required_argument, NULL, 'c' },
+	{ "translate", no_argument, NULL, 't' },
+	{ "src", required_argument, NULL, 's' },
+	{ "dst", required_argument, NULL, 'd' },
+	{ "help", no_argument, NULL, 'h' },
+	{ 0, 0, 0, 0 },
+};
+
+static void print_help(const char *argv0)
+{
+	// clang-format off
+	printf("usage: %s [OPTIONS]\n", argv0);
+	printf("  -p, --plane               indicates that subsequent parameters are for a new plane\n");
+	printf("  -f, --format FOURCC       format of source buffer (defaults to NV12)\n");
+	printf("  -z, --size WIDTHxHEIGHT   size of the source buffer (defaults to screen size)\n");
+	printf("  -c, --scale DOWN/UP       scale plane over time between (1/DOWN)x and UPx\n");
+	printf("  -t, --translate           translate plane over time\n");
+	printf("  -s, --src RECT            source rectangle (defaults to full buffer)\n");
+	printf("  -d, --dst RECT            destination rectangle (defaults to buffer size centered in screen)\n");
+	printf("  -h, --help                show help\n");
+	printf("\n");
+	printf("The format of RECT arguments is X,Y or X,Y,WIDTH,HEIGHT or WIDTHxHEIGHT.\n");
+	printf("To test more than one plane, separate plane arguments with -p. For example:\n");
+	printf("  %s --format NV12 --size 400x400 -p --format XR24 --size 100x100 --translate\n", argv0);
+	printf("\n");
+	// clang-format on
+}
+
+int main(int argc, char **argv)
+{
+	int ret = 0;
+	bool help_flag = false;
+	size_t test_planes_count = 1;
+	struct test_plane test_planes[MAX_TEST_PLANES] = { { 0 } };
+	uint32_t test_planes_formats[MAX_TEST_PLANES] = { 0 };
+	uint32_t test_planes_ids[MAX_TEST_PLANES] = { 0 };
+
+	int c;
+	while ((c = getopt_long(argc, argv, "pf:z:c:ts:d:h", longopts, NULL)) != -1) {
+		struct test_plane *current_plane = &test_planes[test_planes_count - 1];
+		switch (c) {
+			case 'p':
+				test_planes_count++;
+				if (test_planes_count > MAX_TEST_PLANES) {
+					printf("only %d planes are allowed\n", MAX_TEST_PLANES);
+					return 1;
+				}
+				break;
+			case 'f':
+				if (!bs_parse_draw_format(optarg, &current_plane->format))
+					return 1;
+				break;
+			case 'z':
+				if (!parse_size(optarg, &current_plane->bo_w, &current_plane->bo_h))
+					return 1;
+				current_plane->has_bo_size = true;
+				break;
+			case 'c':
+				if (!parse_scale(optarg, &current_plane->dst_downscale_factor,
+						 &current_plane->dst_upscale_factor))
+					return 1;
+				current_plane->has_dst_scale = true;
+				break;
+			case 't':
+				current_plane->dst_vx = 7;
+				current_plane->dst_vy = 7;
+				break;
+			case 's':
+				if (!parse_rect(optarg, &current_plane->src_x,
+						&current_plane->src_y, &current_plane->src_w,
+						&current_plane->src_h, NULL,
+						&current_plane->has_src_size))
+					return 1;
+				break;
+			case 'd':
+				if (!parse_rect(optarg, &current_plane->dst_x,
+						&current_plane->dst_y, &current_plane->dst_w,
+						&current_plane->dst_h,
+						&current_plane->has_dst_position,
+						&current_plane->has_dst_size))
+					return 1;
+				break;
+			case '?':
+				ret = 1;
+			case 'h':
+				help_flag = true;
+				break;
+		}
+	}
+
+	if (help_flag)
+		print_help(argv[0]);
+
+	if (ret)
+		return ret;
+
+	drmModeConnector *connector;
+	struct bs_drm_pipe pipe = { 0 };
+	struct bs_drm_pipe_plumber *plumber = bs_drm_pipe_plumber_new();
+	bs_drm_pipe_plumber_connector_ranks(plumber, bs_drm_connectors_internal_rank);
+	bs_drm_pipe_plumber_connector_ptr(plumber, &connector);
+	if (!bs_drm_pipe_plumber_make(plumber, &pipe)) {
+		bs_debug_error("failed to make pipe");
+		return 1;
+	}
+	bs_drm_pipe_plumber_destroy(&plumber);
+
+	drmModeModeInfo *mode_ptr = find_best_mode(connector->count_modes, connector->modes);
+	if (!mode_ptr) {
+		bs_debug_error("failed to find preferred mode");
+		return 1;
+	}
+	drmModeModeInfo mode = *mode_ptr;
+	drmModeFreeConnector(connector);
+	printf("Using mode %s\n", mode.name);
+
+	printf("Using CRTC:%u ENCODER:%u CONNECTOR:%u\n", pipe.crtc_id, pipe.encoder_id,
+	       pipe.connector_id);
+
+	struct gbm_device *gbm = gbm_create_device(pipe.fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm");
+		return 1;
+	}
+
+	struct gbm_bo *bg_bo = gbm_bo_create(gbm, mode.hdisplay, mode.vdisplay, GBM_FORMAT_XRGB8888,
+					     GBM_BO_USE_SCANOUT | GBM_BO_USE_SW_WRITE_RARELY);
+	if (!bg_bo) {
+		bs_debug_error("failed to create background buffer ojbect");
+		return 1;
+	}
+
+	struct bs_mapper *mapper = bs_mapper_gem_new();
+	if (mapper == NULL) {
+		bs_debug_error("failed to create mapper object");
+		return 1;
+	}
+
+	void *map_data;
+	void *bo_ptr = bs_mapper_map(mapper, bg_bo, 0, &map_data);
+	if (bo_ptr == MAP_FAILED) {
+		bs_debug_error("failed to mmap background buffer object");
+		return 1;
+	}
+	memset(bo_ptr, 0, gbm_bo_get_height(bg_bo) * gbm_bo_get_stride(bg_bo));
+	bs_mapper_unmap(mapper, bg_bo, map_data);
+
+	uint32_t crtc_fb_id = bs_drm_fb_create_gbm(bg_bo);
+	if (!crtc_fb_id) {
+		bs_debug_error("failed to create frame buffer for buffer object");
+		return 1;
+	}
+
+	for (size_t test_plane_index = 0; test_plane_index < test_planes_count;
+	     test_plane_index++) {
+		struct test_plane *tp = &test_planes[test_plane_index];
+
+		if (!tp->format)
+			tp->format = bs_get_draw_format(GBM_FORMAT_NV12);
+
+		test_planes_formats[test_plane_index] = bs_get_pixel_format(tp->format);
+
+		if (!tp->has_bo_size) {
+			tp->bo_w = mode.hdisplay;
+			tp->bo_h = mode.vdisplay;
+		}
+
+		if (!tp->has_src_size) {
+			tp->src_w = tp->bo_w;
+			tp->src_h = tp->bo_h;
+		}
+
+		if (!tp->has_dst_size) {
+			tp->dst_w = tp->bo_w;
+			tp->dst_h = tp->bo_h;
+		}
+
+		if (tp->has_dst_position) {
+			tp->dst_center_x = tp->dst_x + mode.hdisplay / 2;
+			tp->dst_center_y = tp->dst_y + mode.vdisplay / 2;
+		} else {
+			tp->dst_center_x = mode.hdisplay / 2;
+			tp->dst_center_y = mode.vdisplay / 2;
+			tp->dst_x = tp->dst_center_x - tp->dst_w / 2;
+			tp->dst_y = tp->dst_center_y - tp->dst_h / 2;
+		}
+
+		if (tp->has_dst_scale) {
+			tp->dst_min_w = tp->dst_w / tp->dst_downscale_factor;
+			tp->dst_max_w = tp->dst_w * tp->dst_upscale_factor;
+			tp->dst_min_h = tp->dst_h / tp->dst_downscale_factor;
+			tp->dst_max_h = tp->dst_h * tp->dst_upscale_factor;
+		}
+
+		printf("Creating buffer %ux%u %s\n", tp->bo_w, tp->bo_h,
+		       bs_get_format_name(tp->format));
+		tp->bo = gbm_bo_create(gbm, tp->bo_w, tp->bo_h, bs_get_pixel_format(tp->format),
+				       GBM_BO_USE_SCANOUT | GBM_BO_USE_SW_WRITE_RARELY);
+		if (!tp->bo) {
+			bs_debug_error("failed to create buffer object");
+			return 1;
+		}
+
+		tp->fb_id = bs_drm_fb_create_gbm(tp->bo);
+		if (!tp->fb_id) {
+			bs_debug_error("failed to create plane frame buffer for buffer object");
+			return 1;
+		}
+
+		if (!bs_draw_stripe(mapper, tp->bo, tp->format)) {
+			bs_debug_error("failed to draw pattern to buffer object");
+			return 1;
+		}
+	}
+
+	if (!find_overlay_planes(pipe.fd, pipe.crtc_id, test_planes_count, test_planes_formats,
+				 test_planes_ids)) {
+		bs_debug_error("failed to find overlay planes for given formats");
+		return 1;
+	}
+
+	ret = drmModeSetCrtc(pipe.fd, pipe.crtc_id, crtc_fb_id, 0, 0, &pipe.connector_id, 1, &mode);
+	if (ret < 0) {
+		bs_debug_error("Could not set mode on CRTC %d %s", pipe.crtc_id, strerror(errno));
+		return 1;
+	}
+
+	struct timespec start;
+	clock_gettime(CLOCK_MONOTONIC, &start);
+	const int64_t ten_seconds_in_ns = 10000000000;
+	for (int i = 0; ns_since(&start) < ten_seconds_in_ns; i++) {
+		for (size_t test_plane_index = 0; test_plane_index < test_planes_count;
+		     test_plane_index++) {
+			struct test_plane *current_plane = &test_planes[test_plane_index];
+
+			bool needs_set =
+			    update_test_plane(i, mode.hdisplay, mode.vdisplay, current_plane);
+			if (!needs_set)
+				continue;
+
+			ret = drmModeSetPlane(
+			    pipe.fd, test_planes_ids[test_plane_index], pipe.crtc_id,
+			    current_plane->fb_id, 0 /* flags */, current_plane->dst_x,
+			    current_plane->dst_y, current_plane->dst_w, current_plane->dst_h,
+			    current_plane->src_x << 16, current_plane->src_y << 16,
+			    current_plane->src_w << 16, current_plane->src_h << 16);
+
+			if (ret) {
+				bs_debug_error(
+				    "failed to set plane "
+				    "%d:\ndst[x,y,w,h]=%d,%d,%u,%u\nsrc[x,y,w,h]=%d,%d,%u,%u",
+				    ret, current_plane->dst_x, current_plane->dst_y,
+				    current_plane->dst_w, current_plane->dst_h,
+				    current_plane->src_x, current_plane->src_y,
+				    current_plane->src_w, current_plane->src_h);
+				return 1;
+			}
+		}
+		usleep(1000000 / 60);
+	}
+
+	ret = drmModeSetCrtc(pipe.fd, pipe.crtc_id, 0, 0, 0, NULL, 0, NULL);
+	if (ret < 0) {
+		bs_debug_error("Could not disable CRTC %d %s", pipe.crtc_id, strerror(errno));
+		return 1;
+	}
+
+	bs_mapper_destroy(mapper);
+	for (size_t test_plane_index = 0; test_plane_index < test_planes_count;
+	     test_plane_index++) {
+		struct test_plane *current_plane = &test_planes[test_plane_index];
+		drmModeRmFB(pipe.fd, current_plane->fb_id);
+		gbm_bo_destroy(current_plane->bo);
+	}
+	drmModeRmFB(pipe.fd, crtc_fb_id);
+	gbm_bo_destroy(bg_bo);
+	gbm_device_destroy(gbm);
+	close(pipe.fd);
+
+	return 0;
+}
diff --git a/drm-tests/rk3288_hdmitables.py b/drm-tests/rk3288_hdmitables.py
new file mode 100755
index 0000000..99d0f23
--- /dev/null
+++ b/drm-tests/rk3288_hdmitables.py
@@ -0,0 +1,454 @@
+#!/usr/bin/env python2
+# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import sys
+
+# Things seem to magically change in the tables at these TMDS rates.
+# Specifically looking at NO pixel repetition in the table:
+#
+#   0       - 44.9    - output divider is 0b11
+#   49.5    - 90.0    - output divider is 0b10
+#   94.5    - 182.75  - output divider is 0b01
+#   185.625 -         - output divider is 0b00
+#
+# You can also notice that MPLL charge pump settings change at similar times.
+
+RATE1 = 46000000
+RATE2 = 92000000
+RATE3 = 184000000
+
+
+def make_mpll(rate, depth, pixel_rep=0):
+  assert pixel_rep == 0, "Untested with non-zero pixel rep and probably wrong"
+
+  tmds = (rate * depth) / 8. * (pixel_rep + 1)
+
+  if depth == 8:
+    prep_div = 0
+  elif depth == 10:
+    prep_div = 1
+  elif depth == 12:
+    prep_div = 2
+  elif depth == 16:
+    prep_div = 3
+
+  # Rates higher than 340MHz are HDMI 2.0
+  # From tables, tmdsmhl_cntrl is 100% correlated with HDMI 1.4 vs 2.0
+  if tmds <= 340000000:
+    opmode = 0 # HDMI 1.4
+    tmdsmhl_cntrl = 0x0
+  else:
+    opmode = 1 # HDMI 2.0
+    tmdsmhl_cntrl = 0x3
+
+  # Keep the rate within the proper range with the output divider control
+  if tmds <= RATE1:
+    n_cntrl       = 0x3  # output divider: 0b11
+  elif tmds <= RATE2:
+    n_cntrl       = 0x2  # output divider: 0b10
+  elif tmds <= RATE3:
+    n_cntrl       = 0x1  # output divider: 0b01
+  else:
+    n_cntrl       = 0x0  # output divider: 0b00
+
+  # Need to make the dividers work out
+  #
+  # This could be done algorithmically, but let's not for now.  We show the
+  # math to make this work out below as an assert.
+  if n_cntrl == 0x3:
+    if depth == 8:
+      fbdiv2_cntrl  = 0x2  # feedback div1: / 2 (no +1)
+      fbdiv1_cntrl  = 0x3  # feedback div2: / 4
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 10:
+      fbdiv2_cntrl  = 0x5  # feedback div1: / 5 (no +1)
+      fbdiv1_cntrl  = 0x1  # feedback div2: / 2
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 12:
+      fbdiv2_cntrl  = 0x3  # feedback div1: / 3 (no +1)
+      fbdiv1_cntrl  = 0x3  # feedback div2: / 4
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 16:
+      # Guess:
+      fbdiv2_cntrl  = 0x4  # feedback div1: / 4 (no +1)
+      fbdiv1_cntrl  = 0x3  # feedback div2: / 4
+      ref_cntrl     = 0x0  # input divider: / 1
+
+  elif n_cntrl == 0x2:
+    if depth == 8:
+      fbdiv2_cntrl  = 0x1  # feedback div1: / 1 (no +1)
+      fbdiv1_cntrl  = 0x3  # feedback div2: / 4
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 10:
+      fbdiv2_cntrl  = 0x5  # feedback div1: / 5 (no +1)
+      fbdiv1_cntrl  = 0x0  # feedback div2: / 1
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 12:
+      fbdiv2_cntrl  = 0x2  # feedback div1: / 2 (no +1)
+      fbdiv1_cntrl  = 0x2  # feedback div2: / 3
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 16:
+      fbdiv2_cntrl  = 0x2  # feedback div1: / 2 (no +1)
+      fbdiv1_cntrl  = 0x3  # feedback div2: / 4
+      ref_cntrl     = 0x0  # input divider: / 1
+
+  elif n_cntrl == 0x1:
+    if depth == 8:
+      fbdiv2_cntrl  = 0x1  # feedback div1: / 1 (no +1)
+      fbdiv1_cntrl  = 0x1  # feedback div2: / 2
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 10:
+      fbdiv2_cntrl  = 0x5  # feedback div1: / 5 (no +1)
+      fbdiv1_cntrl  = 0x0  # feedback div2: / 1
+      ref_cntrl     = 0x1  # input divider: / 2
+    elif depth == 12:
+      fbdiv2_cntrl  = 0x1  # feedback div1: / 1 (no +1)
+      fbdiv1_cntrl  = 0x2  # feedback div2: / 3
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 16:
+      fbdiv2_cntrl  = 0x1  # feedback div1: / 1 (no +1)
+      fbdiv1_cntrl  = 0x3  # feedback div2: / 4
+      ref_cntrl     = 0x0  # input divider: / 1
+
+  elif n_cntrl == 0x0:
+    if depth == 8:
+      fbdiv2_cntrl  = 0x1  # feedback div1: / 1 (no +1)
+      fbdiv1_cntrl  = 0x0  # feedback div2: / 1
+      ref_cntrl     = 0x0  # input divider: / 1
+    elif depth == 10:
+      fbdiv2_cntrl  = 0x5  # feedback div1: / 5 (no +1)
+      fbdiv1_cntrl  = 0x0  # feedback div2: / 1
+      ref_cntrl     = 0x3  # input divider: / 4
+    elif depth == 12:
+      fbdiv2_cntrl  = 0x1  # feedback div1: / 1 (no +1)
+      fbdiv1_cntrl  = 0x2  # feedback div2: / 3
+      ref_cntrl     = 0x1  # input divider: / 2
+    elif depth == 16:
+      fbdiv2_cntrl  = 0x1  # feedback div1: / 1 (no +1)
+      fbdiv1_cntrl  = 0x1  # feedback div2: / 2
+      ref_cntrl     = 0x0  # input divider: / 1
+
+  # Double check with math; this formula derived from the table.
+  total_div = (fbdiv2_cntrl * (fbdiv1_cntrl + 1) * (1 << (3 - n_cntrl)) /
+	       (ref_cntrl + 1))
+  assert depth == total_div, \
+	"Error with rate=%d, tmds=%d, depth=%d, n_cntrl=%d, pixel_rep=%d" % (
+	  rate, tmds, depth, n_cntrl, pixel_rep)
+
+  # Could be done by math, but this makes it more obvious I think...
+  if n_cntrl == 3:
+    gmp_cntrl = 0
+  elif n_cntrl == 2:
+    gmp_cntrl = 1
+  elif n_cntrl == 1:
+    gmp_cntrl = 2
+  elif n_cntrl == 0:
+    gmp_cntrl = 3
+
+  return ((n_cntrl << 0) |
+	  (ref_cntrl << 2) |
+	  (fbdiv1_cntrl << 4) |
+	  (fbdiv2_cntrl << 6) |
+	  (opmode << 9) |
+	  (tmdsmhl_cntrl << 11) |
+	  (prep_div << 13),
+	  gmp_cntrl)
+
+def do_mpll_loop():
+  mpll_cfg_table = {}
+  last_mpll_cfg = None
+  last_rate = None
+
+  for rate in xrange(13500000, 600001000, 1000):
+    for8bpp = make_mpll(rate, 8)
+    for10bpp = make_mpll(rate, 10)
+    for12bpp = make_mpll(rate, 12)
+
+    mpll_cfg = (for8bpp, for10bpp, for12bpp)
+    if (mpll_cfg != last_mpll_cfg) and (last_rate is not None):
+      mpll_cfg_table[last_rate] = last_mpll_cfg
+
+    last_rate = rate
+    last_mpll_cfg = mpll_cfg
+
+  mpll_cfg_table[last_rate] = last_mpll_cfg
+
+  print "\t",
+  for rate in sorted(mpll_cfg_table.keys()):
+    print ("{\n"
+	   "\t\t%d, {\n"
+	   "\t\t\t{ %#06x, %#06x },\n"
+	   "\t\t\t{ %#06x, %#06x },\n"
+	   "\t\t\t{ %#06x, %#06x },\n"
+	   "\t\t},\n"
+	   "\t}, ") % (
+	   rate,
+	   mpll_cfg_table[rate][0][0], mpll_cfg_table[rate][0][1],
+	   mpll_cfg_table[rate][1][0], mpll_cfg_table[rate][1][1],
+	   mpll_cfg_table[rate][2][0], mpll_cfg_table[rate][2][1]),
+  print
+
+def CLK_SLOP(clk): return ((clk) / 1000)
+def CLK_PLUS_SLOP(clk): return ((clk) + CLK_SLOP(clk))
+def CLK_MINUS_SLOP(clk): return ((clk) - CLK_SLOP(clk))
+
+def make_cur_ctr(rate, depth, pixel_rep=0):
+  assert pixel_rep == 0, "Untested with non-zero pixel rep and probably wrong"
+
+  tmds = (rate * depth) / 8. * (pixel_rep + 1)
+
+  adjust_for_jittery_pll = True
+
+  # If the PIXEL clock (not the TMDS rate) is using the special 594 PLL
+  # and is slow enough, we can use normal rates...
+  if ((CLK_MINUS_SLOP(74250000) <= rate <= CLK_PLUS_SLOP(74250000)) or
+      (CLK_MINUS_SLOP(148500000) <= rate <= CLK_PLUS_SLOP(148500000))):
+    adjust_for_jittery_pll = False
+
+  # If rate is slow enough then our jitter isn't a huge issue.
+  # ...allowable clock jitter is 362.3 or higher and we're OK there w/ plenty of
+  # margin as long as we're careful about our PLL settings.
+  if rate <= 79000000:
+    adjust_for_jittery_pll = False
+
+  if not adjust_for_jittery_pll:
+    # This is as documented
+    if tmds <= RATE1:   # 46000000
+      return 0x18
+    elif tmds <= RATE2: # 92000000
+      return 0x28
+
+    # I have no idea why the below is true, but it is the simplest rule I could
+    # come up with that matched the tables...
+    if depth == 8:
+      if tmds <= 340000000:
+	# HDMI 1.4
+	return 0x38
+      # HDMI 2.0
+      return 0x18
+    elif depth == 16:
+      if tmds < 576000000:
+	return 0x38
+      return 0x28
+    else:
+      return 0x38
+
+  # The output of rk3288 PLL is the source of the HDMI's MPLL.  Apparently
+  # the rk3288 PLL is too jittery.  We can lower the PLL bandwidth of MPLL
+  # to compensate.
+  #
+  # Where possible, we try to use the MPLL bandwidth suggested by Synopsis
+  # and we just use lower bandwidth when testing has shown that it's needed.
+  # We try to stick to 0x28 and 0x18 since those numbers are present in
+  # Synopsis tables.  We go down to 0x08 if needed and finally to 0x00.
+
+  if rate <= 79000000:
+    # Supposed to be 0x28 here, but we'll do 0x18 to reduce jitter
+    return 0x18
+  elif rate <= 118000000:
+    # Supposed to be 0x28/0x38 here, but we'll do 0x08 to reduce jitter
+    return 0x08
+  # Any higher clock rates go to bandwidth = 0
+  return 0
+
+def do_curr_ctrl_loop():
+  cur_ctrl_table = {}
+  last_cur_ctrl = None
+  last_rate = None
+
+  for rate in xrange(13500000, 600001000, 1000):
+    for8bpp = make_cur_ctr(rate, 8)
+    for10bpp = make_cur_ctr(rate, 10)
+    for12bpp = make_cur_ctr(rate, 12)
+
+    cur_ctrl = (for8bpp, for10bpp, for12bpp)
+    if (cur_ctrl != last_cur_ctrl) and (last_rate is not None):
+      cur_ctrl_table[last_rate] = last_cur_ctrl
+
+    last_rate = rate
+    last_cur_ctrl = cur_ctrl
+
+  cur_ctrl_table[last_rate] = last_cur_ctrl
+
+  print "\t",
+  for rate in sorted(cur_ctrl_table.keys()):
+    print ("{\n"
+	   "\t\t%d, { %#06x, %#06x, %#06x },\n"
+	   "\t}, ") % (
+	   rate,
+	   cur_ctrl_table[rate][0],
+	   cur_ctrl_table[rate][1],
+	   cur_ctrl_table[rate][2]),
+  print
+
+
+
+# From HDMI spec
+VPH_RXTERM = 3.3
+RXTERM = 50
+
+def get_phy_preemphasis(symon, traon, trbon):
+  if (symon, traon, trbon) == (0, 0, 0):
+    assert False, "Not valid?"
+  elif (symon, traon, trbon) == (1, 0, 0):
+    preemph = 0.00
+  elif (symon, traon, trbon) == (1, 0, 1):
+    # Numbers match examples better if I assume .25 / 3 rather than .08
+    preemph = 0.25 / 3
+  elif (symon, traon, trbon) == (1, 1, 0):
+    # Numbers match examples better if I assume .50 / 3 rather than .17
+    preemph = 0.50 / 3
+  elif (symon, traon, trbon) == (1, 1, 1):
+    preemph = 0.25
+  else:
+    assert False, "Not valid"
+
+  return preemph
+
+def phy_lvl_to_voltages(lvl, preemph, rterm):
+    v_lo = VPH_RXTERM - (.772 - 0.01405 * lvl)
+    v_swing = ((VPH_RXTERM - v_lo) * (1 - preemph) /
+	      (1 + (RXTERM * (1 + preemph)) / (2 * rterm)))
+    v_hi = v_lo + v_swing
+
+    return v_lo, v_swing, v_hi
+
+def print_phy_config(symbol, term, vlev):
+  ck_symon = bool(symbol & (1 << 0))
+  tx_trbon = bool(symbol & (1 << 1))
+  tx_traon = bool(symbol & (1 << 2))
+  tx_symon = bool(symbol & (1 << 3))
+
+  slopeboost = {
+    0: "no slope boost",
+    1: " 5-10% decrease on TMDS rise/fall times",
+    2: "10-20% decrease on TMDS rise/fall times",
+    3: "20-35% decrease on TMDS rise/fall times",
+  }[(symbol >> 4) & 0x3]
+
+  override = bool(symbol & (1 << 15))
+
+  rterm = (50, 57.14, 66.67, 80, 100, 133, 200)[term]
+
+  sup_ck_lvl = (vlev >> 0) & 0x1f
+  sup_tx_lvl = (vlev >> 5) & 0x1f
+
+  preemph = get_phy_preemphasis(tx_symon, tx_traon, tx_trbon)
+
+  print "symbol=%#06x, term=%#06x, vlev=%#06x" % (symbol, term, vlev)
+  for name, lvl in [("ck", sup_ck_lvl), ("tx", sup_tx_lvl)]:
+    v_lo, v_swing, v_hi = phy_lvl_to_voltages(lvl, preemph, rterm)
+    print " %s: lvl = %2d, term=%3d, vlo = %.2f, vhi=%.2f, vswing = %.2f, %s" % (
+      name, lvl, rterm, v_lo, v_hi, v_swing, slopeboost)
+
+#def calc_ideal_phy_lvl(swing_mv, preemph, rterm):
+  #"""Get the ideal "lvl" for the given swing, preemph, and termination.
+
+  #This might not be integral, but and might not fit the 0-31 range.
+  #"""
+  #v_lo = (VPH_RXTERM -
+	  #v_swing / (1 - preemph) -
+	  #(v_swing * RXTERM * (1 + preemph)) / (2 * rterm * (1 - preemph)))
+  #lvl = (.772 - (VPH_RXTERM - v_lo)) / 0.01405
+
+  #return lvl
+
+def do_phy_config_list(rate=16500000):
+  # From HDMI spec
+  VPH_RXTERM = 3.3
+  RXTERM = 50
+
+  # Set to True to print even things that don't meet requirements.
+  print_invalid = False
+
+  # Totally a guess based on what's in IMX6DQRM
+  if rate <= 165000000:
+    symon = 1 # tx_symon
+    traon = 0 # tx_traon
+    trbon = 0 # tx_trbon
+  else:
+    symon = 1 # tx_symon
+    traon = 0 # tx_traon
+    trbon = 1 # tx_trbon
+
+  print "Guessing symon, traon, trbon based on rate: (%d, %d, %d)" % (
+    symon, traon, trbon)
+
+  preemph = get_phy_preemphasis(symon, traon, trbon)
+
+  # Notes:
+  # - swing needs to be between .4 and .6
+  # - If <= 165MHz, vhi is (VPH_RXTERM + .01) thru (VPH_RXTERM - .01)
+  # - If >  165MHz, vhi is (VPH_RXTERM + .01) thru (VPH_RXTERM - .2)
+  # - If <= 165MHz, vlo is (VPH_RXTERM - .4)  thru (VPH_RXTERM - .6)
+  # - If >  165MHz, vlo is (VPH_RXTERM - .4)  thru (VPH_RXTERM - .7)
+  #
+  # TODO: I'm not sure we can actually reach vhi of 3.3 +/- .01
+  # TODO: How do we pick amongst all of these?
+
+  if rate <= 165000000:
+    v_hi_min = 3.19  # Should be (VPH_RXTERM - .01), but not possible?
+    v_hi_max = (VPH_RXTERM + .01)
+    v_lo_min = (VPH_RXTERM - .6)
+    v_lo_max = (VPH_RXTERM - .4)
+  else:
+    v_hi_min = (VPH_RXTERM - .2)
+    v_hi_max = (VPH_RXTERM + .01)
+    v_lo_min = (VPH_RXTERM - .7)
+    v_lo_max = (VPH_RXTERM - .4)
+
+  for lvl in xrange(0, 31):
+    for rterm in (50, 57.14, 66.67, 80, 100, 133, 200):
+      v_lo, v_swing, v_hi = phy_lvl_to_voltages(lvl, preemph, rterm)
+
+      if (print_invalid or
+	  ((.4 <= v_swing <= .6) and
+	   (v_hi_min <= v_hi <= v_hi_max) and
+	   (v_lo_min <= v_lo <= v_lo_max))):
+	print "lvl = %2d, term=%3d, vlo = %.2f, vhi=%.2f, vswing = %.2f" % (
+	  lvl, rterm, v_lo, v_hi, v_swing)
+
+# Examples:
+#
+# $ ./rk3288_hdmitables.py mpll
+# $ ./rk3288_hdmitables.py curr
+# $ ./rk3288_hdmitables.py phy_list 165000000
+# $ ./rk3288_hdmitables.py phy_list 600000000
+# $ ./rk3288_hdmitables.py phy_print 0x8009, 0x0005, 0x01ad
+
+def main(todo, *args):
+  if todo == "mpll":
+    do_mpll_loop()
+  elif todo == "curr":
+    do_curr_ctrl_loop()
+  elif todo == "phy_list":
+    (rate,) = args
+    rate = int(rate, 0)
+    do_phy_config_list(rate)
+  elif todo == "phy_print":
+    (symbol, term, vlev) = args
+    symbol = int(symbol.rstrip(","), 0)
+    term = int(term.rstrip(","), 0)
+    vlev = int(vlev.rstrip(","), 0)
+
+    print_phy_config(symbol, term, vlev)
+
+  # These ought to match the tables in the docs.  They are close, but not
+  # perfect.  ...but my math is definitely right since v_lo doesn't match
+  # and v_lo should be very simple.  Even if we try to find more significant
+  # digits for 0.772 and 0.01405 we still can't make it match, so I'm assuming
+  # that they rounded somewhere in their math...
+
+  #print "%.3f, %.3f, %.3f" % phy_lvl_to_voltages(19, get_phy_preemphasis(1, 0, 0), 100)
+  #print "%.3f, %.3f, %.3f" % phy_lvl_to_voltages(10, get_phy_preemphasis(1, 0, 0), 100)
+  #print "%.3f, %.3f, %.3f" % phy_lvl_to_voltages( 6, get_phy_preemphasis(1, 0, 1), 100)
+
+  #print "%.3f, %.3f, %.3f" % phy_lvl_to_voltages(21, get_phy_preemphasis(1, 0, 0), 133)
+  #print "%.3f, %.3f, %.3f" % phy_lvl_to_voltages(13, get_phy_preemphasis(1, 0, 0), 133)
+  #print "%.3f, %.3f, %.3f" % phy_lvl_to_voltages( 8, get_phy_preemphasis(1, 0, 1), 133)
+
+
+if __name__ == '__main__':
+  main(*sys.argv[1:])
diff --git a/drm-tests/swrast_test.c b/drm-tests/swrast_test.c
new file mode 100644
index 0000000..9cce1ad
--- /dev/null
+++ b/drm-tests/swrast_test.c
@@ -0,0 +1,323 @@
+/* Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bs_drm.h"
+
+const char *get_gl_error()
+{
+	switch (glGetError()) {
+		case GL_NO_ERROR:
+			return "No error has been recorded.";
+		case GL_INVALID_ENUM:
+			return "An unacceptable value is specified for an enumerated argument. The "
+			       "offending command is ignored and has no other side effect than to "
+			       "set the error flag.";
+		case GL_INVALID_VALUE:
+			return "A numeric argument is out of range. The offending command is "
+			       "ignored and has no other side effect than to set the error flag.";
+		case GL_INVALID_OPERATION:
+			return "The specified operation is not allowed in the current state. The "
+			       "offending command is ignored and has no other side effect than to "
+			       "set the error flag.";
+		case GL_INVALID_FRAMEBUFFER_OPERATION:
+			return "The command is trying to render to or read from the framebuffer "
+			       "while the currently bound framebuffer is not framebuffer complete "
+			       "(i.e. the return value from glCheckFramebufferStatus is not "
+			       "GL_FRAMEBUFFER_COMPLETE). The offending command is ignored and has "
+			       "no other side effect than to set the error flag.";
+		case GL_OUT_OF_MEMORY:
+			return "There is not enough memory left to execute the command. The state "
+			       "of the GL is undefined, except for the state of the error flags, "
+			       "after this error is recorded.";
+		default:
+			return "Unknown error";
+	}
+}
+
+const char *get_egl_error()
+{
+	switch (eglGetError()) {
+		case EGL_SUCCESS:
+			return "The last function succeeded without error.";
+		case EGL_NOT_INITIALIZED:
+			return "EGL is not initialized, or could not be initialized, for the "
+			       "specified EGL display connection.";
+		case EGL_BAD_ACCESS:
+			return "EGL cannot access a requested resource (for example a context is "
+			       "bound in another thread).";
+		case EGL_BAD_ALLOC:
+			return "EGL failed to allocate resources for the requested operation.";
+		case EGL_BAD_ATTRIBUTE:
+			return "An unrecognized attribute or attribute value was passed in the "
+			       "attribute list.";
+		case EGL_BAD_CONTEXT:
+			return "An EGLContext argument does not name a valid EGL rendering "
+			       "context.";
+		case EGL_BAD_CONFIG:
+			return "An EGLConfig argument does not name a valid EGL frame buffer "
+			       "configuration.";
+		case EGL_BAD_CURRENT_SURFACE:
+			return "The current surface of the calling thread is a window, pixel "
+			       "buffer or pixmap that is no longer valid.";
+		case EGL_BAD_DISPLAY:
+			return "An EGLDisplay argument does not name a valid EGL display "
+			       "connection.";
+		case EGL_BAD_SURFACE:
+			return "An EGLSurface argument does not name a valid surface (window, "
+			       "pixel buffer or pixmap) configured for GL rendering.";
+		case EGL_BAD_MATCH:
+			return "Arguments are inconsistent (for example, a valid context requires "
+			       "buffers not supplied by a valid surface).";
+		case EGL_BAD_PARAMETER:
+			return "One or more argument values are invalid.";
+		case EGL_BAD_NATIVE_PIXMAP:
+			return "A NativePixmapType argument does not refer to a valid native "
+			       "pixmap.";
+		case EGL_BAD_NATIVE_WINDOW:
+			return "A NativeWindowType argument does not refer to a valid native "
+			       "window.";
+		case EGL_CONTEXT_LOST:
+			return "A power management event has occurred. The application must "
+			       "destroy all contexts and reinitialise OpenGL ES state and objects "
+			       "to continue rendering.";
+		default:
+			return "Unknown error";
+	}
+}
+
+struct context {
+	unsigned width;
+	unsigned height;
+	EGLDisplay egl_display;
+	EGLContext egl_ctx;
+
+	unsigned gl_fb;
+	unsigned gl_rb;
+};
+
+float f(int i)
+{
+	int a = i % 40;
+	int b = (i / 40) % 6;
+	switch (b) {
+		case 0:
+		case 1:
+			return 0.0f;
+		case 3:
+		case 4:
+			return 1.0f;
+		case 2:
+			return (a / 40.0f);
+		case 5:
+			return 1.0f - (a / 40.0f);
+		default:
+			return 0.0f;
+	}
+}
+
+void draw(struct context *ctx)
+{
+	int i;
+	const GLchar *vertexShaderStr =
+	    "attribute vec4 vPosition;\n"
+	    "attribute vec4 vColor;\n"
+	    "varying vec4 vFillColor;\n"
+	    "void main() {\n"
+	    "  gl_Position = vPosition;\n"
+	    "  vFillColor = vColor;\n"
+	    "}\n";
+	const GLchar *fragmentShaderStr =
+	    "precision mediump float;\n"
+	    "varying vec4 vFillColor;\n"
+	    "void main() {\n"
+	    "  gl_FragColor = vFillColor;\n"
+	    "}\n";
+	GLint vertexShader, fragmentShader, program, status;
+
+	vertexShader = glCreateShader(GL_VERTEX_SHADER);
+	if (!vertexShader) {
+		fprintf(stderr, "Failed to create vertex shader. Error=0x%x\n", glGetError());
+		return;
+	}
+	glShaderSource(vertexShader, 1, &vertexShaderStr, NULL);
+	glCompileShader(vertexShader);
+	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &status);
+	if (!status) {
+		fprintf(stderr, "Failed to compile vertex shader. Error=0x%x\n", glGetError());
+		return;
+	}
+
+	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
+	if (!fragmentShader) {
+		fprintf(stderr, "Failed to create fragment shader. Error=0x%x\n", glGetError());
+		return;
+	}
+	glShaderSource(fragmentShader, 1, &fragmentShaderStr, NULL);
+	glCompileShader(fragmentShader);
+	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &status);
+	if (!status) {
+		fprintf(stderr, "Failed to compile fragment shader. Error=0x%x\n", glGetError());
+		return;
+	}
+
+	program = glCreateProgram();
+	if (!program) {
+		fprintf(stderr, "Failed to create program.\n");
+		return;
+	}
+	glAttachShader(program, vertexShader);
+	glAttachShader(program, fragmentShader);
+	glBindAttribLocation(program, 0, "vPosition");
+	glBindAttribLocation(program, 1, "vColor");
+	glLinkProgram(program);
+	glGetShaderiv(program, GL_LINK_STATUS, &status);
+	if (!status) {
+		fprintf(stderr, "Failed to link program.\n");
+		return;
+	}
+
+	glViewport(0, 0, (GLint)ctx->width, (GLint)ctx->height);
+
+	for (i = 0; i <= 500; i++) {
+		GLfloat verts[] = { 0.0f, -0.5f, 0.0f, -0.5f, 0.5f, 0.0f, 0.5f, 0.5f, 0.0f };
+		GLfloat colors[] = { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
+				     0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f };
+
+		glClearColor(f(i), f(i + 80), f(i + 160), 0.0f);
+		glClear(GL_COLOR_BUFFER_BIT);
+
+		glUseProgram(program);
+		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, verts);
+		glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, colors);
+		glEnableVertexAttribArray(0);
+		glEnableVertexAttribArray(1);
+		glDrawArrays(GL_TRIANGLES, 0, 3);
+
+		usleep(1e6 / 120); /* 120 Hz */
+		glFinish();
+
+		unsigned char pixels[4];
+		glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
+		printf("color = %hhu %hhu %hhu %hhu\n", pixels[0], pixels[1], pixels[2], pixels[3]);
+	}
+
+	glDeleteProgram(program);
+}
+
+int main(int argc, char **argv)
+{
+	int ret = 0;
+	struct context ctx;
+	EGLint egl_major, egl_minor;
+	const char *extensions;
+	EGLint num_configs;
+	EGLConfig egl_config;
+
+	ctx.width = 800;
+	ctx.height = 600;
+
+	const EGLint config_attribs[] = { EGL_RED_SIZE,
+					  1,
+					  EGL_GREEN_SIZE,
+					  1,
+					  EGL_BLUE_SIZE,
+					  1,
+					  EGL_DEPTH_SIZE,
+					  1,
+					  EGL_RENDERABLE_TYPE,
+					  EGL_OPENGL_ES2_BIT,
+					  EGL_SURFACE_TYPE,
+					  EGL_DONT_CARE,
+					  EGL_NONE };
+	const EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
+
+	ctx.egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+	if (ctx.egl_display == EGL_NO_DISPLAY) {
+		fprintf(stderr, "failed to get egl display\n");
+		ret = 1;
+		goto fail;
+	}
+
+	if (!eglInitialize(ctx.egl_display, &egl_major, &egl_minor)) {
+		fprintf(stderr, "failed to initialize egl: %s\n", get_egl_error());
+		ret = 1;
+		goto terminate_display;
+	}
+
+	printf("EGL %d.%d\n", egl_major, egl_minor);
+	printf("EGL %s\n", eglQueryString(ctx.egl_display, EGL_VERSION));
+
+	extensions = eglQueryString(ctx.egl_display, EGL_EXTENSIONS);
+	printf("EGL Extensions: %s\n", extensions);
+
+	if (!eglChooseConfig(ctx.egl_display, config_attribs, NULL, 0, &num_configs)) {
+		fprintf(stderr, "eglChooseConfig() failed with error: %x\n", eglGetError());
+		goto terminate_display;
+	}
+	if (!eglChooseConfig(ctx.egl_display, config_attribs, &egl_config, 1, &num_configs)) {
+		fprintf(stderr, "eglChooseConfig() failed with error: %x\n", eglGetError());
+		goto terminate_display;
+	}
+
+	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
+		fprintf(stderr, "failed to bind OpenGL ES: %s\n", get_egl_error());
+		ret = 1;
+		goto terminate_display;
+	}
+
+	if (bs_egl_has_extension("EGL_KHR_no_config_context", extensions)) {
+		ctx.egl_ctx =
+		    eglCreateContext(ctx.egl_display, NULL /* No Config */,
+				     EGL_NO_CONTEXT /* No shared context */, context_attribs);
+	} else {
+		ctx.egl_ctx =
+		    eglCreateContext(ctx.egl_display, egl_config,
+				     EGL_NO_CONTEXT /* No shared context */, context_attribs);
+	}
+
+	if (ctx.egl_ctx == EGL_NO_CONTEXT) {
+		fprintf(stderr, "failed to create OpenGL ES Context: %s\n", get_egl_error());
+		ret = 1;
+		goto terminate_display;
+	}
+
+	if (!eglMakeCurrent(ctx.egl_display, EGL_NO_SURFACE /* No default draw surface */,
+			    EGL_NO_SURFACE /* No default draw read */, ctx.egl_ctx)) {
+		fprintf(stderr, "failed to make the OpenGL ES Context current: %s\n",
+			get_egl_error());
+		ret = 1;
+		goto destroy_context;
+	}
+
+	printf("GL extensions: %s\n", glGetString(GL_EXTENSIONS));
+
+	glGenFramebuffers(1, &ctx.gl_fb);
+	glBindFramebuffer(GL_FRAMEBUFFER, ctx.gl_fb);
+	glGenRenderbuffers(1, &ctx.gl_rb);
+	glBindRenderbuffer(GL_RENDERBUFFER, ctx.gl_rb);
+	glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8_OES, ctx.width, ctx.height);
+	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ctx.gl_rb);
+
+	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+		fprintf(stderr, "failed to create framebuffer: %s\n", get_gl_error());
+		ret = 1;
+		goto delete_gl_buffers;
+	}
+
+	draw(&ctx);
+
+delete_gl_buffers:
+	glBindRenderbuffer(GL_RENDERBUFFER, 0);
+	glBindFramebuffer(GL_FRAMEBUFFER, 0);
+	glDeleteFramebuffers(1, &ctx.gl_fb);
+	glDeleteRenderbuffers(1, &ctx.gl_rb);
+destroy_context:
+	eglMakeCurrent(ctx.egl_display, NULL, NULL, NULL);
+	eglDestroyContext(ctx.egl_display, ctx.egl_ctx);
+terminate_display:
+	eglTerminate(ctx.egl_display);
+fail:
+	return ret;
+}
diff --git a/drm-tests/vgem_test.c b/drm-tests/vgem_test.c
new file mode 100644
index 0000000..5e85751
--- /dev/null
+++ b/drm-tests/vgem_test.c
@@ -0,0 +1,381 @@
+/* Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * This is a test meant to exercise the VGEM DRM kernel module's PRIME
+ * import/export functions. It will create a gem buffer object, mmap, write, and
+ * then verify it. Then the test will repeat that with the same gem buffer, but
+ * exported and then imported. Finally, a new gem buffer object is made in a
+ * different driver which exports into VGEM and the mmap, write, verify sequence
+ * is repeated on that.
+ */
+
+#define _GNU_SOURCE
+#include <assert.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <xf86drm.h>
+
+#include "bs_drm.h"
+
+#define WITH_COLOR 1
+
+#if WITH_COLOR
+#define ANSI_COLOR_RED "\x1b[31m"
+#define ANSI_COLOR_GREEN "\x1b[32m"
+#define ANSI_COLOR_YELLOW "\x1b[33m"
+#define ANSI_COLOR_BLUE "\x1b[34m"
+#define ANSI_COLOR_MAGENTA "\x1b[35m"
+#define ANSI_COLOR_CYAN "\x1b[36m"
+#define ANSI_COLOR_RESET "\x1b[0m"
+#else
+#define ANSI_COLOR_RED ""
+#define ANSI_COLOR_GREEN ""
+#define ANSI_COLOR_YELLOW ""
+#define ANSI_COLOR_BLUE ""
+#define ANSI_COLOR_MAGENTA ""
+#define ANSI_COLOR_CYAN ""
+#define ANSI_COLOR_RESET ""
+#endif
+
+#define FAIL_COLOR ANSI_COLOR_RED "failed" ANSI_COLOR_RESET
+#define SUCCESS_COLOR(x) ANSI_COLOR_GREEN x ANSI_COLOR_RESET
+
+const uint32_t g_bo_pattern = 0xdeadbeef;
+const char g_dev_card_path_format[] = "/dev/dri/card%d";
+
+int create_vgem_bo(int fd, size_t size, uint32_t *handle)
+{
+	struct drm_mode_create_dumb create;
+	int ret;
+
+	memset(&create, 0, sizeof(create));
+	create.height = size;
+	create.width = 1;
+	create.bpp = 8;
+
+	ret = drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
+	if (ret)
+		return ret;
+
+	assert(create.size >= size);
+
+	*handle = create.handle;
+
+	return 0;
+}
+
+void *mmap_dumb_bo(int fd, int handle, size_t size)
+{
+	struct drm_mode_map_dumb mmap_arg;
+	void *ptr;
+	int ret;
+
+	memset(&mmap_arg, 0, sizeof(mmap_arg));
+
+	mmap_arg.handle = handle;
+
+	ret = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mmap_arg);
+	assert(ret == 0);
+	assert(mmap_arg.offset != 0);
+
+	ptr = mmap(NULL, size, (PROT_READ | PROT_WRITE), MAP_SHARED, fd, mmap_arg.offset);
+
+	return ptr;
+}
+
+void write_pattern(volatile uint32_t *bo_ptr, size_t bo_size)
+{
+	volatile uint32_t *ptr;
+	for (ptr = bo_ptr; ptr < bo_ptr + (bo_size / sizeof(*bo_ptr)); ptr++) {
+		*ptr = g_bo_pattern;
+	}
+}
+
+bool verify_pattern(volatile uint32_t *bo_ptr, size_t bo_size)
+{
+	volatile uint32_t *ptr;
+	for (ptr = bo_ptr; ptr < bo_ptr + (bo_size / sizeof(*bo_ptr)); ptr++) {
+		if (*ptr != g_bo_pattern) {
+			fprintf(stderr,
+				"buffer object verify " FAIL_COLOR " at offset %td = 0x%X\n",
+				(void *)ptr - (void *)bo_ptr, *ptr);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static const char help_text[] =
+    "Usage: %s [OPTIONS]\n"
+    " -h          Print this help.\n"
+    " -d [DEVICE] Open the given vgem device file (defaults to trying all cards under "
+    "/dev/dri/).\n"
+    " -c [SIZE]   Create a buffer objects of the given size in bytes.\n";
+
+void print_help(const char *argv0)
+{
+	printf(help_text, argv0);
+}
+
+static const char optstr[] = "hd:c:";
+
+int main(int argc, char *argv[])
+{
+	int ret = 0;
+	const char *device_file = NULL;
+	long bo_size = 65536;
+	bool test_pattern = true;
+	bool export_to_fd = true;
+	bool import_to_handle = true;
+	bool import_foreign = true;
+
+	int c;
+	while ((c = getopt(argc, argv, optstr)) != -1) {
+		switch (c) {
+			case 'h':
+				print_help(argv[0]);
+				exit(0);
+				break;
+			case 'd':
+				device_file = optarg;
+				break;
+			case 'c':
+				bo_size = atol(optarg);
+				break;
+			default:
+				print_help(argv[0]);
+				return 1;
+		}
+	}
+
+	int vgem_fd = -1;
+	if (device_file)
+		vgem_fd = open(device_file, O_RDWR);
+	else
+		vgem_fd = bs_drm_open_vgem();
+
+	if (vgem_fd < 0) {
+		perror(FAIL_COLOR " to open vgem device");
+		return 1;
+	}
+
+	printf(SUCCESS_COLOR("opened") " vgem device\n");
+
+	uint32_t bo_handle;
+	if (bo_size > 0) {
+		ret = create_vgem_bo(vgem_fd, bo_size, &bo_handle);
+		if (ret) {
+			fprintf(stderr, FAIL_COLOR " to create bo: %d\n", ret);
+			ret = 1;
+			goto close_vgem_fd;
+		}
+		printf(SUCCESS_COLOR("created") " vgem buffer object, handle = %u\n", bo_handle);
+	}
+
+	if (test_pattern) {
+		if (bo_size <= 0) {
+			fprintf(stderr,
+				"buffer object must be created before it can be written to\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		volatile uint32_t *bo_ptr = mmap_dumb_bo(vgem_fd, bo_handle, bo_size);
+		if (bo_ptr == MAP_FAILED) {
+			fprintf(stderr, FAIL_COLOR " to map buffer object\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("mapped") " vgem buffer object\n");
+
+		write_pattern(bo_ptr, bo_size);
+
+		assert(!munmap((uint32_t *)bo_ptr, bo_size));
+
+		printf(SUCCESS_COLOR("wrote") " to vgem buffer object\n");
+
+		bo_ptr = mmap_dumb_bo(vgem_fd, bo_handle, bo_size);
+
+		if (!verify_pattern(bo_ptr, bo_size)) {
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		assert(!munmap((uint32_t *)bo_ptr, bo_size));
+
+		printf(SUCCESS_COLOR("verified") " vgem buffer object writes\n");
+	}
+
+	int prime_fd;
+	if (export_to_fd) {
+		if (bo_size <= 0) {
+			fprintf(stderr, "created buffer object required to perform export\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		ret = drmPrimeHandleToFD(vgem_fd, bo_handle, O_CLOEXEC, &prime_fd);
+		if (ret) {
+			fprintf(stderr, FAIL_COLOR " to export buffer object: %d\n", ret);
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("exported") " vgem buffer object, fd = %d\n", prime_fd);
+	}
+
+	uint32_t imported_handle;
+	if (import_to_handle) {
+		if (bo_size <= 0 || !export_to_fd) {
+			fprintf(stderr, "exported buffer object required to perform import\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		ret = drmPrimeFDToHandle(vgem_fd, prime_fd, &imported_handle);
+		if (ret) {
+			fprintf(stderr, FAIL_COLOR " to import buffer object: %d\n", ret);
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("imported") " to vgem buffer object, handle = %d\n",
+		       imported_handle);
+
+		volatile uint32_t *bo_ptr = mmap_dumb_bo(vgem_fd, imported_handle, bo_size);
+		if (bo_ptr == MAP_FAILED) {
+			fprintf(stderr, FAIL_COLOR " to map imported buffer object\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("mapped") " imported vgem buffer object\n");
+
+		if (!verify_pattern(bo_ptr, bo_size)) {
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		assert(!munmap((uint32_t *)bo_ptr, bo_size));
+
+		printf(SUCCESS_COLOR("verified") " imported vgem buffer object writes\n");
+	}
+
+	if (import_foreign) {
+		int dumb_fd = open("/dev/dri/card0", O_RDWR);
+		if (dumb_fd == -1) {
+			perror(FAIL_COLOR " to open non-vgem card\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("opened") " non-vgem device\n");
+
+		struct drm_mode_create_dumb create;
+
+		memset(&create, 0, sizeof(create));
+		create.width = 640;
+		create.height = 480;
+		create.bpp = 32;
+
+		ret = drmIoctl(dumb_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
+		if (ret) {
+			fprintf(stderr, FAIL_COLOR " to create non-vgem buffer object: %d\n", ret);
+			ret = 1;
+			goto close_dumb_fd;
+		}
+
+		printf(SUCCESS_COLOR("created") " non-vgem buffer object, handle = %u\n",
+		       create.handle);
+
+		assert(create.size == create.width * create.height * 4);
+
+		int foreign_prime_fd;
+		ret = drmPrimeHandleToFD(dumb_fd, create.handle, O_CLOEXEC, &foreign_prime_fd);
+		if (ret) {
+			fprintf(stderr, FAIL_COLOR " to export non-vgem buffer object: %d\n", ret);
+			ret = 1;
+			goto close_dumb_fd;
+		}
+
+		printf(SUCCESS_COLOR("exported") " non-vgem buffer object, fd = %d\n",
+		       foreign_prime_fd);
+
+		uint32_t foreign_imported_handle;
+		ret = drmPrimeFDToHandle(vgem_fd, foreign_prime_fd, &foreign_imported_handle);
+		if (ret) {
+			fprintf(stderr, FAIL_COLOR " to import non-vgem buffer object: %d\n", ret);
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("imported") " to vgem buffer object, handle = %d\n",
+		       foreign_imported_handle);
+
+		volatile uint32_t *bo_ptr =
+		    mmap_dumb_bo(vgem_fd, foreign_imported_handle, create.size);
+		if (bo_ptr == MAP_FAILED) {
+			fprintf(stderr, FAIL_COLOR " to map imported non-vgem buffer object\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("mapped") " imported non-vgem vgem buffer object\n");
+
+		write_pattern(bo_ptr, create.size);
+
+		assert(!munmap((uint32_t *)bo_ptr, create.size));
+
+		printf(SUCCESS_COLOR("wrote") " to non-vgem buffer object\n");
+
+		bo_ptr = mmap_dumb_bo(vgem_fd, foreign_imported_handle, create.size);
+		if (bo_ptr == MAP_FAILED) {
+			fprintf(stderr, FAIL_COLOR " to map imported non-vgem buffer object\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		if (!verify_pattern(bo_ptr, create.size)) {
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		assert(!munmap((uint32_t *)bo_ptr, create.size));
+
+		printf(SUCCESS_COLOR("verified") " imported non-vgem buffer object writes\n");
+
+		bo_ptr = mmap_dumb_bo(dumb_fd, create.handle, create.size);
+		if (bo_ptr == MAP_FAILED) {
+			fprintf(stderr, FAIL_COLOR " to map non-vgem buffer object\n");
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		if (!verify_pattern(bo_ptr, create.size)) {
+			ret = 1;
+			goto close_vgem_fd;
+		}
+
+		printf(SUCCESS_COLOR("verified") " non-vgem buffer object writes\n");
+
+		assert(!munmap((uint32_t *)bo_ptr, create.size));
+
+	close_dumb_fd:
+		close(dumb_fd);
+	}
+
+close_vgem_fd:
+	close(vgem_fd);
+	return ret;
+}
diff --git a/drm-tests/vk_glow.c b/drm-tests/vk_glow.c
new file mode 100644
index 0000000..d2e8a35
--- /dev/null
+++ b/drm-tests/vk_glow.c
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2016 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+// Intel's Vulkan driver exposes an unregistered extension function,
+// vkCreateDmaBufImageINTEL.  There is no extension associated with the
+// function (as of 2016-09-21). The Intel developers added the function during
+// Vulkan's early early days, in Mesa's first Vulkan commit on 2015-05-08, to
+// provide a convenient way to test the driver. The Vulkan API had no extension
+// mechanism yet in that early pre-1.0 timeframe.
+//
+// We use vkCreateDmaBufImageINTEL, despite its ambiguous status, because there
+// does not yet exist a good alternative for importing a dma_buf as a VkImage.
+//
+// The Vulkan validation layer (VK_LAYER_LUNARG_standard_validation) does not
+// understand vkCreateDmaBufImageINTEL. To run with the validation layer,
+// #undef USE_vkCreateDmaBufImageINTEL and rebuild.
+#undef USE_vkCreateDmaBufImageINTEL
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <vulkan/vulkan.h>
+
+#ifdef USE_vkCreateDmaBufImageINTEL
+#include <vulkan/vulkan_intel.h>
+#endif
+
+#include "bs_drm.h"
+
+// Used for double-buffering.
+struct frame {
+	struct gbm_bo *bo;
+	int bo_prime_fd;
+	uint32_t drm_fb_id;
+	VkDeviceMemory vk_memory;
+	VkImage vk_image;
+	VkImageView vk_image_view;
+	VkFramebuffer vk_framebuffer;
+	VkCommandBuffer vk_cmd_buf;
+};
+
+#define check_vk_success(result, vk_func) \
+	__check_vk_success(__FILE__, __LINE__, __func__, (result), (vk_func))
+
+static void __check_vk_success(const char *file, int line, const char *func, VkResult result,
+			       const char *vk_func)
+{
+	if (result == VK_SUCCESS)
+		return;
+
+	bs_debug_print("ERROR", func, file, line, "%s failed with VkResult(%d)", vk_func, result);
+	exit(EXIT_FAILURE);
+}
+
+static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec,
+			      void *data)
+{
+	bool *waiting_for_flip = data;
+	*waiting_for_flip = false;
+}
+
+// Choose the first physical device. Exit on failure.
+VkPhysicalDevice choose_physical_device(VkInstance inst)
+{
+	uint32_t n_phys_devs;
+	VkResult res;
+
+	res = vkEnumeratePhysicalDevices(inst, &n_phys_devs, NULL);
+	check_vk_success(res, "vkEnumeratePhysicalDevices");
+
+	if (n_phys_devs == 0) {
+		fprintf(stderr, "No available VkPhysicalDevices\n");
+		exit(EXIT_FAILURE);
+	}
+
+	VkPhysicalDevice phys_devs[n_phys_devs];
+	res = vkEnumeratePhysicalDevices(inst, &n_phys_devs, phys_devs);
+	check_vk_success(res, "vkEnumeratePhysicalDevices");
+
+	// Print information about all available devices. This helps debugging
+	// when bringing up Vulkan on a new system.
+	printf("Available VkPhysicalDevices:\n");
+
+	for (uint32_t i = 0; i < n_phys_devs; ++i) {
+		VkPhysicalDeviceProperties props;
+
+		vkGetPhysicalDeviceProperties(phys_devs[i], &props);
+
+		printf("    VkPhysicalDevice %u:\n", i);
+		printf("	apiVersion: %u.%u.%u\n", VK_VERSION_MAJOR(props.apiVersion),
+		       VK_VERSION_MINOR(props.apiVersion), VK_VERSION_PATCH(props.apiVersion));
+		printf("	driverVersion: %u\n", props.driverVersion);
+		printf("	vendorID: 0x%x\n", props.vendorID);
+		printf("	deviceID: 0x%x\n", props.deviceID);
+		printf("	deviceName: %s\n", props.deviceName);
+		printf("	pipelineCacheUUID: %x%x%x%x-%x%x-%x%x-%x%x-%x%x%x%x%x%x\n",
+		       props.pipelineCacheUUID[0], props.pipelineCacheUUID[1],
+		       props.pipelineCacheUUID[2], props.pipelineCacheUUID[3],
+		       props.pipelineCacheUUID[4], props.pipelineCacheUUID[5],
+		       props.pipelineCacheUUID[6], props.pipelineCacheUUID[7],
+		       props.pipelineCacheUUID[8], props.pipelineCacheUUID[9],
+		       props.pipelineCacheUUID[10], props.pipelineCacheUUID[11],
+		       props.pipelineCacheUUID[12], props.pipelineCacheUUID[13],
+		       props.pipelineCacheUUID[14], props.pipelineCacheUUID[15]);
+	}
+
+	printf("Chose VkPhysicalDevice 0\n");
+	fflush(stdout);
+
+	return phys_devs[0];
+}
+
+// Return the index of a graphics-enabled queue family. Return UINT32_MAX on
+// failure.
+uint32_t choose_gfx_queue_family(VkPhysicalDevice phys_dev)
+{
+	uint32_t family_idx = UINT32_MAX;
+	VkQueueFamilyProperties *props = NULL;
+	uint32_t n_props = 0;
+
+	vkGetPhysicalDeviceQueueFamilyProperties(phys_dev, &n_props, NULL);
+
+	props = calloc(sizeof(props[0]), n_props);
+	if (!props) {
+		bs_debug_error("out of memory");
+		exit(EXIT_FAILURE);
+	}
+
+	vkGetPhysicalDeviceQueueFamilyProperties(phys_dev, &n_props, props);
+
+	// Choose the first graphics queue.
+	for (uint32_t i = 0; i < n_props; ++i) {
+		if ((props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && props[i].queueCount > 0) {
+			family_idx = i;
+			break;
+		}
+	}
+
+	free(props);
+	return family_idx;
+}
+
+int main(int argc, char **argv)
+{
+	VkInstance inst;
+	VkPhysicalDevice phys_dev;
+	uint32_t gfx_queue_family_idx;
+	VkDevice dev;
+	VkQueue gfx_queue;
+	VkRenderPass pass;
+	VkCommandPool cmd_pool;
+	VkResult res;
+	struct frame frames[2];
+	int err;
+
+	int dev_fd = bs_drm_open_main_display();
+	if (dev_fd < 0) {
+		bs_debug_error("failed to open display device");
+		exit(EXIT_FAILURE);
+	}
+
+	struct gbm_device *gbm = gbm_create_device(dev_fd);
+	if (!gbm) {
+		bs_debug_error("failed to create gbm_device");
+		exit(EXIT_FAILURE);
+	}
+
+	struct bs_drm_pipe pipe = { 0 };
+	if (!bs_drm_pipe_make(dev_fd, &pipe)) {
+		bs_debug_error("failed to make drm pipe");
+		exit(EXIT_FAILURE);
+	}
+
+	drmModeConnector *connector = drmModeGetConnector(dev_fd, pipe.connector_id);
+	if (!connector) {
+		bs_debug_error("drmModeGetConnector failed");
+		exit(EXIT_FAILURE);
+	}
+
+	drmModeModeInfo *mode = &connector->modes[0];
+
+	res = vkCreateInstance(
+	    &(VkInstanceCreateInfo){
+		.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+		.pApplicationInfo =
+		    &(VkApplicationInfo){
+			.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+			.apiVersion = VK_MAKE_VERSION(1, 0, 0),
+		    },
+	    },
+	    /*pAllocator*/ NULL, &inst);
+	check_vk_success(res, "vkCreateInstance");
+
+	phys_dev = choose_physical_device(inst);
+
+	gfx_queue_family_idx = choose_gfx_queue_family(phys_dev);
+	if (gfx_queue_family_idx == UINT32_MAX) {
+		bs_debug_error(
+		    "VkPhysicalDevice exposes no VkQueueFamilyProperties "
+		    "with graphics");
+		exit(EXIT_FAILURE);
+	}
+
+	res = vkCreateDevice(phys_dev,
+			     &(VkDeviceCreateInfo){
+				 .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+				 .queueCreateInfoCount = 1,
+				 .pQueueCreateInfos =
+				     (VkDeviceQueueCreateInfo[]){
+					 {
+					     .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
+					     .queueFamilyIndex = gfx_queue_family_idx,
+					     .queueCount = 1,
+					     .pQueuePriorities = (float[]){ 1.0f },
+
+					 },
+				     },
+			     },
+			     /*pAllocator*/ NULL, &dev);
+	check_vk_success(res, "vkCreateDevice");
+
+#if USE_vkCreateDmaBufImageINTEL
+	PFN_vkCreateDmaBufImageINTEL bs_vkCreateDmaBufImageINTEL =
+	    (void *)vkGetDeviceProcAddr(dev, "vkCreateDmaBufImageINTEL");
+	if (bs_vkCreateDmaBufImageINTEL == NULL) {
+		bs_debug_error("vkGetDeviceProcAddr(\"vkCreateDmaBufImageINTEL\') failed");
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	vkGetDeviceQueue(dev, gfx_queue_family_idx, /*queueIndex*/ 0, &gfx_queue);
+
+	res = vkCreateCommandPool(dev,
+				  &(VkCommandPoolCreateInfo){
+				      .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+				      .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT |
+					       VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
+				      .queueFamilyIndex = gfx_queue_family_idx,
+				  },
+				  /*pAllocator*/ NULL, &cmd_pool);
+	check_vk_success(res, "vkCreateCommandPool");
+
+	res = vkCreateRenderPass(
+	    dev,
+	    &(VkRenderPassCreateInfo){
+		.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
+		.attachmentCount = 1,
+		.pAttachments =
+		    (VkAttachmentDescription[]){
+			{
+			    .format = VK_FORMAT_A8B8G8R8_UNORM_PACK32,
+			    .samples = 1,
+			    .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
+			    .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
+			    .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+			    .finalLayout = VK_IMAGE_LAYOUT_GENERAL,
+			},
+		    },
+		.subpassCount = 1,
+		.pSubpasses =
+		    (VkSubpassDescription[]){
+			{
+			    .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
+			    .colorAttachmentCount = 1,
+			    .pColorAttachments =
+				(VkAttachmentReference[]){
+				    {
+					.attachment = 0,
+					.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+				    },
+				},
+			},
+		    },
+	    },
+	    /*pAllocator*/ NULL, &pass);
+	check_vk_success(res, "vkCreateRenderPass");
+
+	for (int i = 0; i < BS_ARRAY_LEN(frames); ++i) {
+		struct frame *fr = &frames[i];
+
+		fr->bo = gbm_bo_create(gbm, mode->hdisplay, mode->vdisplay, GBM_FORMAT_XBGR8888,
+				       GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+		if (fr->bo == NULL) {
+			bs_debug_error("failed to create framebuffer's gbm_bo");
+			return 1;
+		}
+
+		fr->drm_fb_id = bs_drm_fb_create_gbm(fr->bo);
+		if (fr->drm_fb_id == 0) {
+			bs_debug_error("failed to create drm framebuffer id");
+			return 1;
+		}
+
+		fr->bo_prime_fd = gbm_bo_get_fd(fr->bo);
+		if (fr->bo_prime_fd < 0) {
+			bs_debug_error("failed to get prime fd for gbm_bo");
+			return 1;
+		}
+
+#if USE_vkCreateDmaBufImageINTEL
+		res = bs_vkCreateDmaBufImageINTEL(
+		    dev,
+		    &(VkDmaBufImageCreateInfo){
+			.sType = VK_STRUCTURE_TYPE_DMA_BUF_IMAGE_CREATE_INFO_INTEL,
+			.fd = fr->bo_prime_fd,
+			.format = VK_FORMAT_A8B8G8R8_UNORM_PACK32,
+			.extent = (VkExtent3D){ mode->hdisplay, mode->hdisplay, 1 },
+			.strideInBytes = gbm_bo_get_stride(fr->bo),
+		    },
+		    /*pAllocator*/ NULL, &fr->vk_memory, &fr->vk_image);
+		check_vk_success(res, "vkCreateDmaBufImageINTEL");
+#else
+		res = vkCreateImage(dev,
+				    &(VkImageCreateInfo){
+					.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+					.imageType = VK_IMAGE_TYPE_2D,
+					.format = VK_FORMAT_A8B8G8R8_UNORM_PACK32,
+					.extent = (VkExtent3D){ mode->hdisplay, mode->hdisplay, 1 },
+					.mipLevels = 1,
+					.arrayLayers = 1,
+					.samples = 1,
+					.tiling = VK_IMAGE_TILING_LINEAR,
+					.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
+					.queueFamilyIndexCount = 1,
+					.pQueueFamilyIndices = (uint32_t[]){ gfx_queue_family_idx },
+					.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+				    },
+				    /*pAllocator*/ NULL, &fr->vk_image);
+		check_vk_success(res, "vkCreateImage");
+
+		VkMemoryRequirements mem_reqs;
+		vkGetImageMemoryRequirements(dev, fr->vk_image, &mem_reqs);
+
+		res = vkAllocateMemory(dev,
+				       &(VkMemoryAllocateInfo){
+					   .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+					   .allocationSize = mem_reqs.size,
+
+					   // Simply choose the first available memory type.
+					   // We need neither performance nor mmap, so all
+					   // memory types are equally good.
+					   .memoryTypeIndex = ffs(mem_reqs.memoryTypeBits) - 1,
+				       },
+				       /*pAllocator*/ NULL, &fr->vk_memory);
+		check_vk_success(res, "vkAllocateMemory");
+
+		res = vkBindImageMemory(dev, fr->vk_image, fr->vk_memory,
+					/*memoryOffset*/ 0);
+		check_vk_success(res, "vkBindImageMemory");
+#endif
+
+		res = vkCreateImageView(dev,
+					&(VkImageViewCreateInfo){
+					    .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+					    .image = fr->vk_image,
+					    .viewType = VK_IMAGE_VIEW_TYPE_2D,
+					    .format = VK_FORMAT_A8B8G8R8_UNORM_PACK32,
+					    .components =
+						(VkComponentMapping){
+						    .r = VK_COMPONENT_SWIZZLE_IDENTITY,
+						    .b = VK_COMPONENT_SWIZZLE_IDENTITY,
+						    .g = VK_COMPONENT_SWIZZLE_IDENTITY,
+						    .a = VK_COMPONENT_SWIZZLE_IDENTITY,
+						},
+					    .subresourceRange =
+						(VkImageSubresourceRange){
+						    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+						    .baseMipLevel = 0,
+						    .levelCount = 1,
+						    .baseArrayLayer = 0,
+						    .layerCount = 1,
+						},
+					},
+					/*pAllocator*/ NULL, &fr->vk_image_view);
+		check_vk_success(res, "vkCreateImageView");
+
+		res = vkCreateFramebuffer(dev,
+					  &(VkFramebufferCreateInfo){
+					      .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
+					      .renderPass = pass,
+					      .attachmentCount = 1,
+					      .pAttachments = (VkImageView[]){ fr->vk_image_view },
+					      .width = mode->hdisplay,
+					      .height = mode->vdisplay,
+					      .layers = 1,
+					  },
+					  /*pAllocator*/ NULL, &fr->vk_framebuffer);
+		check_vk_success(res, "vkCreateFramebuffer");
+
+		res = vkAllocateCommandBuffers(
+		    dev,
+		    &(VkCommandBufferAllocateInfo){
+			.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+			.commandPool = cmd_pool,
+			.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+			.commandBufferCount = 1,
+		    },
+		    &fr->vk_cmd_buf);
+		check_vk_success(res, "vkAllocateCommandBuffers");
+	}
+
+	// We set the screen mode using framebuffer 0. Then the first page flip
+	// waits on framebuffer 1.
+	err = drmModeSetCrtc(dev_fd, pipe.crtc_id, frames[0].drm_fb_id,
+			     /*x*/ 0, /*y*/ 0, &pipe.connector_id, /*connector_count*/ 1, mode);
+	if (err) {
+		bs_debug_error("drmModeSetCrtc failed: %d", err);
+		exit(EXIT_FAILURE);
+	}
+
+	// We set an upper bound on the render loop so we can run this in
+	// from a testsuite.
+	for (int i = 1; i < 500; ++i) {
+		struct frame *fr = &frames[i % BS_ARRAY_LEN(frames)];
+
+		// vkBeginCommandBuffer implicity resets the command buffer due
+		// to VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT.
+		res = vkBeginCommandBuffer(fr->vk_cmd_buf,
+					   &(VkCommandBufferBeginInfo){
+					       .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+					       .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
+					   });
+		check_vk_success(res, "vkBeginCommandBuffer");
+
+		// Cycle along the circumference of the RGB color wheel.
+		VkClearValue clear_color = {
+			.color =
+			    {
+				.float32 =
+				    {
+					0.5f + 0.5f * sinf(2 * M_PI * i / 240.0f),
+					0.5f + 0.5f * sinf(2 * M_PI * i / 240.0f +
+							   (2.0f / 3.0f * M_PI)),
+					0.5f + 0.5f * sinf(2 * M_PI * i / 240.0f +
+							   (4.0f / 3.0f * M_PI)),
+					1.0f,
+				    },
+			    },
+		};
+
+		vkCmdBeginRenderPass(fr->vk_cmd_buf,
+				     &(VkRenderPassBeginInfo){
+					 .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
+					 .renderPass = pass,
+					 .framebuffer = fr->vk_framebuffer,
+					 .renderArea =
+					     (VkRect2D){
+						 .offset = { 0, 0 },
+						 .extent = { mode->hdisplay, mode->vdisplay },
+					     },
+					 .clearValueCount = 1,
+					 .pClearValues = (VkClearValue[]){ clear_color },
+				     },
+				     VK_SUBPASS_CONTENTS_INLINE);
+		vkCmdEndRenderPass(fr->vk_cmd_buf);
+
+		res = vkEndCommandBuffer(fr->vk_cmd_buf);
+		check_vk_success(res, "vkEndCommandBuffer");
+
+		res =
+		    vkQueueSubmit(gfx_queue,
+				  /*submitCount*/ 1,
+				  (VkSubmitInfo[]){
+				      {
+					  .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+					  .commandBufferCount = 1,
+					  .pCommandBuffers = (VkCommandBuffer[]){ fr->vk_cmd_buf },
+				      },
+				  },
+				  VK_NULL_HANDLE);
+		check_vk_success(res, "vkQueueSubmit");
+
+		res = vkQueueWaitIdle(gfx_queue);
+		check_vk_success(res, "vkQueueWaitIdle");
+
+		bool waiting_for_flip = true;
+		err = drmModePageFlip(dev_fd, pipe.crtc_id, fr->drm_fb_id, DRM_MODE_PAGE_FLIP_EVENT,
+				      &waiting_for_flip);
+		if (err) {
+			bs_debug_error("failed page flip: error=%d", err);
+			exit(EXIT_FAILURE);
+		}
+
+		while (waiting_for_flip) {
+			drmEventContext ev_ctx = {
+				.version = DRM_EVENT_CONTEXT_VERSION,
+				.page_flip_handler = page_flip_handler,
+			};
+
+			fd_set fds;
+			FD_ZERO(&fds);
+			FD_SET(dev_fd, &fds);
+
+			int n_fds = select(dev_fd + 1, &fds, NULL, NULL, NULL);
+			if (n_fds < 0) {
+				bs_debug_error("select() failed on page flip: %s", strerror(errno));
+				exit(EXIT_FAILURE);
+			} else if (n_fds == 0) {
+				bs_debug_error("select() timeout on page flip");
+				exit(EXIT_FAILURE);
+			}
+
+			err = drmHandleEvent(dev_fd, &ev_ctx);
+			if (err) {
+				bs_debug_error(
+				    "drmHandleEvent failed while "
+				    "waiting for page flip: error=%d",
+				    err);
+				exit(EXIT_FAILURE);
+			}
+		}
+	}
+
+	return 0;
+}