diff --git a/src/xrt/state_trackers/gui/CMakeLists.txt b/src/xrt/state_trackers/gui/CMakeLists.txt
index ad28fcf72..e1efa36d1 100644
--- a/src/xrt/state_trackers/gui/CMakeLists.txt
+++ b/src/xrt/state_trackers/gui/CMakeLists.txt
@@ -13,6 +13,7 @@ set(GUI_SOURCE_FILES
 	gui_scene_calibrate.c
 	gui_scene_debug.c
 	gui_scene_main_menu.c
+	gui_scene_remote.c
 	gui_scene_video.c
 	../../../external/imgui/imgui/cimgui.cpp
 	../../../external/imgui/imgui/cimgui.h
@@ -49,8 +50,15 @@ target_link_libraries(st_gui PRIVATE
 target_include_directories(st_gui PUBLIC
 	${CMAKE_CURRENT_SOURCE_DIR}/..
 	${CMAKE_CURRENT_SOURCE_DIR}/../../../external/imgui
+	${CMAKE_CURRENT_SOURCE_DIR}/../../drivers
 	)
 
+if(XRT_BUILD_DRIVER_REMOTE)
+	target_link_libraries(st_gui PRIVATE
+		drv_remote
+		)
+endif()
+
 if(XRT_HAVE_SDL2)
 	add_library(imgui_impl_sdl STATIC
 		../../../external/imgui/imgui/cimgui_sdl.cpp
diff --git a/src/xrt/state_trackers/gui/gui_common.h b/src/xrt/state_trackers/gui/gui_common.h
index 31bfd2a8e..51436b19b 100644
--- a/src/xrt/state_trackers/gui/gui_common.h
+++ b/src/xrt/state_trackers/gui/gui_common.h
@@ -207,6 +207,14 @@ gui_scene_select_video_calibrate(struct gui_program *p);
 void
 gui_scene_debug(struct gui_program *p);
 
+/*!
+ * Remote control debugging UI.
+ *
+ * @ingroup gui
+ */
+void
+gui_scene_remote(struct gui_program *p);
+
 /*!
  * Given the frameserver runs the calibration code on it.
  * Claims ownership of @p s.
diff --git a/src/xrt/state_trackers/gui/gui_scene_main_menu.c b/src/xrt/state_trackers/gui/gui_scene_main_menu.c
index da881012f..2f0d70b8e 100644
--- a/src/xrt/state_trackers/gui/gui_scene_main_menu.c
+++ b/src/xrt/state_trackers/gui/gui_scene_main_menu.c
@@ -45,6 +45,12 @@ scene_render(struct gui_scene *scene, struct gui_program *p)
 		gui_scene_debug(p);
 	}
 
+	if (igButton("Remote", button_dims)) {
+		gui_scene_delete_me(p, scene);
+
+		gui_scene_remote(p);
+	}
+
 	igSeparator();
 
 	if (igButton("Exit", button_dims)) {
diff --git a/src/xrt/state_trackers/gui/gui_scene_remote.c b/src/xrt/state_trackers/gui/gui_scene_remote.c
new file mode 100644
index 000000000..2f93b09d3
--- /dev/null
+++ b/src/xrt/state_trackers/gui/gui_scene_remote.c
@@ -0,0 +1,341 @@
+// Copyright 2020, Collabora, Ltd.
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief  Remote debugging UI.
+ * @author Jakob Bornecrantz <jakob@collabora.com>
+ * @ingroup gui
+ */
+
+#include "xrt/xrt_config_drivers.h"
+
+#include "util/u_misc.h"
+
+#include "math/m_api.h"
+
+#include "gui_common.h"
+#include "gui_imgui.h"
+
+#include "remote/r_interface.h"
+
+
+/*
+ *
+ * Structs and defines.
+ *
+ */
+
+/*!
+ * A GUI scene that lets the user select a user device.
+ * @implements gui_scene
+ */
+struct gui_remote
+{
+	struct gui_scene base;
+
+	struct r_remote_connection rc;
+
+	struct r_remote_data reset;
+	struct r_remote_data data;
+
+	bool cheat_menu;
+};
+
+const ImVec2 zero_dims = {0, 0};
+
+
+/*
+ *
+ * Functions.
+ *
+ */
+
+#ifdef XRT_BUILD_DRIVER_REMOTE
+static void
+handle_draggable_vec3_f32(const char *name,
+                          struct xrt_vec3 *v,
+                          const struct xrt_vec3 *reset)
+{
+	float min = -256.0f;
+	float max = 256.0f;
+	char tmp[256];
+
+	snprintf(tmp, sizeof(tmp), "%s.reset", name);
+
+	if (igArrowButton(tmp, ImGuiDir_Left)) {
+		*v = *reset;
+	}
+
+	igSameLine(0, 3);
+	igDragFloat3(name, (float *)v, 0.005f, min, max, "%+f", 1.0f);
+}
+
+static void
+handle_draggable_quat(const char *name,
+                      struct xrt_quat *q,
+                      const struct xrt_quat *reset)
+{
+	float min = -1.0f;
+	float max = 1.0f;
+
+	char tmp[256];
+
+	snprintf(tmp, sizeof(tmp), "%s.reset", name);
+
+	if (igArrowButton(tmp, ImGuiDir_Left)) {
+		*q = *reset;
+	}
+
+	igSameLine(0, 3);
+	igDragFloat4(name, (float *)q, 0.005f, min, max, "%+f", 1.0f);
+
+	// Avoid invalid
+	if (q->x == 0.0f && q->y == 0.0f && q->z == 0.0f && q->w == 0.0f) {
+		q->w = 1.0f;
+	}
+
+	// And make sure it's a unit rotation.
+	math_quat_normalize(q);
+}
+
+static bool
+handle_downable_button(const char *name)
+{
+	igButton(name, zero_dims);
+	return igIsItemHovered(ImGuiHoveredFlags_RectOnly) &&
+	       igIsMouseDown(ImGuiMouseButton_Left);
+}
+
+static void
+render_cheat_menu(struct gui_remote *gr, struct gui_program *p)
+{
+	struct r_remote_data *d = &gr->data;
+
+	if (igButton("Reset all", zero_dims)) {
+		*d = gr->reset;
+	}
+
+	if (igButton("Interactive Throw #1", zero_dims)) {
+#if 0
+		d->left.pose.position.x = -1.0f;
+		d->left.pose.position.y = 1.0f;
+		d->left.pose.position.z = -3.0f;
+#else
+		d->left.pose.position.x = -0.200000f;
+		d->left.pose.position.y = 1.300000f;
+		d->left.pose.position.z = -0.500000f;
+		d->left.pose.orientation.x = 0.000000f;
+		d->left.pose.orientation.y = 0.000000f;
+		d->left.pose.orientation.z = 0.000000f;
+		d->left.pose.orientation.w = 1.000000f;
+		d->left.linear_velocity.x = -0.770000f;
+		d->left.linear_velocity.y = 3.255000f;
+		d->left.linear_velocity.z = -2.620000f;
+		d->left.angular_velocity.x = 0.000000f;
+		d->left.angular_velocity.y = 0.000000f;
+		d->left.angular_velocity.z = 0.000000f;
+#endif
+	}
+
+	if (igButton("Interactive Throw #2", zero_dims)) {
+#if 0
+		d->left.pose.position.x = 1.0f;
+		d->left.pose.position.y = 1.1f;
+		d->left.pose.position.z = -4.0f;
+#else
+		d->left.pose.position.x = -0.200000f;
+		d->left.pose.position.y = 1.300000f;
+		d->left.pose.position.z = -0.500000f;
+		d->left.pose.orientation.x = 0.858999f;
+		d->left.pose.orientation.y = -0.163382f;
+		d->left.pose.orientation.z = -0.000065f;
+		d->left.pose.orientation.w = 0.485209f;
+		d->left.linear_velocity.x = 0.000000f;
+		d->left.linear_velocity.y = 0.000000f;
+		d->left.linear_velocity.z = 0.000000f;
+		d->left.angular_velocity.x = -10.625000f;
+		d->left.angular_velocity.y = 0.000000f;
+		d->left.angular_velocity.z = 0.000000f;
+#endif
+	}
+
+	if (igButton("Interactive Throw #3", zero_dims)) {
+#if 0
+		d->left.pose.position.x = 0.0f;
+		d->left.pose.position.y = 3.0f;
+		d->left.pose.position.z = -5.0f;
+#else
+		d->left.pose.position.x = -0.200000f;
+		d->left.pose.position.y = 1.300000f;
+		d->left.pose.position.z = -0.500000f;
+		d->left.pose.orientation.x = 0.862432f;
+		d->left.pose.orientation.y = 0.000000f;
+		d->left.pose.orientation.z = 0.000000f;
+		d->left.pose.orientation.w = 0.506174f;
+		d->left.linear_velocity.x = 0.000000f;
+		d->left.linear_velocity.y = 0.000000f;
+		d->left.linear_velocity.z = -1.830000f;
+		d->left.angular_velocity.x = -16.900000f;
+		d->left.angular_velocity.y = 0.000000f;
+		d->left.angular_velocity.z = 0.000000f;
+#endif
+	}
+
+	if (igButton("Dump left", zero_dims)) {
+		fprintf(stderr,
+		        "d->left.pose.position.x = %ff;\n"
+		        "d->left.pose.position.y = %ff;\n"
+		        "d->left.pose.position.z = %ff;\n"
+		        "d->left.pose.orientation.x = %ff;\n"
+		        "d->left.pose.orientation.y = %ff;\n"
+		        "d->left.pose.orientation.z = %ff;\n"
+		        "d->left.pose.orientation.w = %ff;\n"
+		        "d->left.linear_velocity.x = %ff;\n"
+		        "d->left.linear_velocity.y = %ff;\n"
+		        "d->left.linear_velocity.z = %ff;\n"
+		        "d->left.angular_velocity.x = %ff;\n"
+		        "d->left.angular_velocity.y = %ff;\n"
+		        "d->left.angular_velocity.z = %ff;\n",
+		        d->left.pose.position.x, d->left.pose.position.y,
+		        d->left.pose.position.z, d->left.pose.orientation.x,
+		        d->left.pose.orientation.y, d->left.pose.orientation.z,
+		        d->left.pose.orientation.w, d->left.linear_velocity.x,
+		        d->left.linear_velocity.y, d->left.linear_velocity.z,
+		        d->left.angular_velocity.x, d->left.angular_velocity.y,
+		        d->left.angular_velocity.z);
+	}
+}
+
+#define POSE(prefix)                                                           \
+	do {                                                                   \
+		handle_draggable_vec3_f32(#prefix ".pose.position",            \
+		                          &d->prefix.pose.position,            \
+		                          &r->prefix.pose.position);           \
+		handle_draggable_quat(#prefix ".pose.orientation",             \
+		                      &d->prefix.pose.orientation,             \
+		                      &r->prefix.pose.orientation);            \
+	} while (false)
+
+#define LIN_ANG(prefix)                                                        \
+	do {                                                                   \
+		handle_draggable_vec3_f32(#prefix ".linear_velocity",          \
+		                          &d->prefix.linear_velocity,          \
+		                          &r->prefix.linear_velocity);         \
+		handle_draggable_vec3_f32(#prefix ".angular_velocity",         \
+		                          &d->prefix.angular_velocity,         \
+		                          &r->prefix.angular_velocity);        \
+	} while (false)
+
+#define BUTTONS(prefix)                                                        \
+	do {                                                                   \
+		d->prefix.select = handle_downable_button("Select");           \
+		igSameLine(0, 3);                                              \
+		d->prefix.menu = handle_downable_button("Menu");               \
+		igSameLine(0, 3);                                              \
+		igCheckbox("Active", &d->prefix.active);                       \
+	} while (false)
+
+static void
+on_connected(struct gui_remote *gr, struct gui_program *p)
+{
+	const struct r_remote_data *r = &gr->reset;
+	struct r_remote_data *d = &gr->data;
+
+	const ImVec2 hmd_size = {0, 42 + 4};
+	const ImVec2 ctrl_size = {0, 64 + 24 + 24 + 4};
+
+	igBeginChildStr("hmd", hmd_size, false, 0);
+	POSE(hmd);
+	igEndChild();
+
+	igBeginChildStr("left", ctrl_size, false, 0);
+	POSE(left);
+	LIN_ANG(left);
+	BUTTONS(left);
+	igEndChild();
+
+	igBeginChildStr("right", ctrl_size, false, 0);
+	POSE(right);
+	LIN_ANG(right);
+	BUTTONS(right);
+	igEndChild();
+
+	igCheckbox("Predefined poses", &gr->cheat_menu);
+	if (gr->cheat_menu) {
+		render_cheat_menu(gr, p);
+	}
+
+	r_remote_connection_write_one(&gr->rc, &gr->data);
+}
+
+static void
+on_not_connected(struct gui_remote *gr, struct gui_program *p)
+{
+	if (!igButton("Connect", zero_dims)) {
+		return;
+	}
+
+	r_remote_connection_init(&gr->rc, "127.0.0.1", 4242);
+	r_remote_connection_read_one(&gr->rc, &gr->reset);
+	r_remote_connection_read_one(&gr->rc, &gr->data);
+}
+#endif
+
+
+/*
+ *
+ * Scene functions.
+ *
+ */
+
+static void
+scene_render(struct gui_scene *scene, struct gui_program *p)
+{
+	struct gui_remote *gr = (struct gui_remote *)scene;
+	(void)gr;
+
+	igBegin("Remote control", NULL, 0);
+
+#ifdef XRT_BUILD_DRIVER_REMOTE
+	if (gr->rc.fd < 0) {
+		on_not_connected(gr, p);
+	} else {
+		on_connected(gr, p);
+	}
+#else
+	igText("Not compiled with the remote driver");
+	if (igButton("Exit", zero_dims)) {
+		gui_scene_delete_me(p, &gr->base);
+	}
+#endif
+
+	igEnd();
+}
+
+static void
+scene_destroy(struct gui_scene *scene, struct gui_program *p)
+{
+	struct gui_remote *gr = (struct gui_remote *)scene;
+	(void)gr;
+
+	free(scene);
+}
+
+
+/*
+ *
+ * 'Exported' functions.
+ *
+ */
+
+void
+gui_scene_remote(struct gui_program *p)
+{
+	struct gui_remote *gr = U_TYPED_CALLOC(struct gui_remote);
+
+	gr->base.render = scene_render;
+	gr->base.destroy = scene_destroy;
+	gr->rc.fd = -1;
+
+	gui_scene_push_front(p, &gr->base);
+}
diff --git a/src/xrt/state_trackers/gui/meson.build b/src/xrt/state_trackers/gui/meson.build
index 219f35240..65cb8367a 100644
--- a/src/xrt/state_trackers/gui/meson.build
+++ b/src/xrt/state_trackers/gui/meson.build
@@ -10,6 +10,7 @@ gui_sources = [
 	'gui_scene_calibrate.c',
 	'gui_scene_debug.c',
 	'gui_scene_main_menu.c',
+	'gui_scene_remote.c',
 	'gui_scene_video.c',
 	'../../../external/imgui/imgui/cimgui.cpp',
 	'../../../external/imgui/imgui/cimgui.h',
@@ -42,6 +43,7 @@ lib_st_gui = static_library(
 	files(gui_sources),
 	include_directories: [
 		xrt_include,
+		drv_include,
 		glad_include,
 		cjson_include,
 		imgui_include,
diff --git a/src/xrt/targets/gui/gui_sdl2_main.c b/src/xrt/targets/gui/gui_sdl2_main.c
index a177298e6..7f0f96dc0 100644
--- a/src/xrt/targets/gui/gui_sdl2_main.c
+++ b/src/xrt/targets/gui/gui_sdl2_main.c
@@ -39,6 +39,8 @@ main(int argc, char **argv)
 		gui_scene_debug(&p.base);
 	} else if (argc >= 2 && strcmp("calibrate", argv[1]) == 0) {
 		gui_scene_select_video_calibrate(&p.base);
+	} else if (argc >= 2 && strcmp("remote", argv[1]) == 0) {
+		gui_scene_remote(&p.base);
 	} else {
 		gui_scene_main_menu(&p.base);
 	}