From 83976d7d2cb79365f20d4f312be05afe7f897268 Mon Sep 17 00:00:00 2001
From: Alex Badics <admin@stickman.hu>
Date: Mon, 14 Aug 2023 14:59:20 +0200
Subject: [PATCH] d/rokid: add Rokid Air and Max driver

---
 CMakeLists.txt                          |   3 +
 src/xrt/drivers/CMakeLists.txt          |  15 +
 src/xrt/drivers/rokid/rokid_hmd.c       | 599 ++++++++++++++++++++++++
 src/xrt/drivers/rokid/rokid_interface.h |  43 ++
 src/xrt/targets/common/CMakeLists.txt   |   4 +
 src/xrt/targets/common/target_lists.c   |   8 +
 6 files changed, 672 insertions(+)
 create mode 100644 src/xrt/drivers/rokid/rokid_hmd.c
 create mode 100644 src/xrt/drivers/rokid/rokid_interface.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 32979e2ce..4308d73da 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -341,6 +341,7 @@ option_with_deps(XRT_BUILD_DRIVER_QWERTY "Enable Qwerty driver" DEPENDS XRT_HAVE
 option_with_deps(XRT_BUILD_DRIVER_REALSENSE "Enable RealSense device driver" DEPENDS XRT_HAVE_REALSENSE)
 option_with_deps(XRT_BUILD_DRIVER_REMOTE "Enable remote debugging driver" DEPENDS "XRT_HAVE_LINUX OR ANDROID OR WIN32")
 option_with_deps(XRT_BUILD_DRIVER_RIFT_S "Enable Oculus Rift S device driver" DEPENDS XRT_HAVE_HIDAPI XRT_HAVE_V4L2)
+option_with_deps(XRT_BUILD_DRIVER_ROKID "Enable Rokid driver" DEPENDS "XRT_HAVE_LIBUSB")
 option_with_deps(XRT_BUILD_DRIVER_STEAMVR_LIGHTHOUSE "Enable SteamVR Lighthouse driver" DEPENDS XRT_HAVE_LINUX XRT_HAVE_STEAM XRT_MODULE_AUX_VIVE)
 option_with_deps(XRT_BUILD_DRIVER_SURVIVE "Enable libsurvive driver" DEPENDS SURVIVE_FOUND XRT_MODULE_AUX_VIVE)
 option_with_deps(XRT_BUILD_DRIVER_ULV2 "Enable Ultraleap v2 driver" DEPENDS LeapV2_FOUND)
@@ -399,6 +400,7 @@ list(
 	"REALSENSE"
 	"REMOTE"
 	"RIFT_S"
+	"ROKID"
 	"SURVIVE"
 	"V4L2"
 	"ULV2"
@@ -592,6 +594,7 @@ message(STATUS "#    DRIVER_QWERTY:               ${XRT_BUILD_DRIVER_QWERTY}")
 message(STATUS "#    DRIVER_REALSENSE:            ${XRT_BUILD_DRIVER_REALSENSE}")
 message(STATUS "#    DRIVER_REMOTE:               ${XRT_BUILD_DRIVER_REMOTE}")
 message(STATUS "#    DRIVER_RIFT_S:               ${XRT_BUILD_DRIVER_RIFT_S}")
+message(STATUS "#    DRIVER_ROKID:                ${XRT_BUILD_DRIVER_ROKID}")
 message(STATUS "#    DRIVER_SIMULATED:            ${XRT_BUILD_DRIVER_SIMULATED}")
 message(STATUS "#    DRIVER_SIMULAVR:             ${XRT_BUILD_DRIVER_SIMULAVR}")
 message(STATUS "#    DRIVER_SURVIVE:              ${XRT_BUILD_DRIVER_SURVIVE}")
diff --git a/src/xrt/drivers/CMakeLists.txt b/src/xrt/drivers/CMakeLists.txt
index 19ebb3e2c..7fd48a6d4 100644
--- a/src/xrt/drivers/CMakeLists.txt
+++ b/src/xrt/drivers/CMakeLists.txt
@@ -205,6 +205,21 @@ if(XRT_BUILD_DRIVER_REMOTE)
 	list(APPEND ENABLED_HEADSET_DRIVERS remote)
 endif()
 
+if(XRT_BUILD_DRIVER_ROKID)
+	add_library(drv_rokid STATIC rokid/rokid_hmd.c rokid/rokid_interface.h)
+	target_link_libraries(
+		drv_rokid
+		PRIVATE
+			xrt-interfaces
+			aux_util
+			aux_os
+			aux_math
+			${LIBUSB1_LIBRARIES}
+		)
+	target_include_directories(drv_rokid PUBLIC ${LIBUSB1_INCLUDE_DIRS})
+	list(APPEND ENABLED_HEADSET_DRIVERS rokid)
+endif()
+
 if(XRT_BUILD_DRIVER_RIFT_S)
 	add_library(
 		drv_rift_s STATIC
diff --git a/src/xrt/drivers/rokid/rokid_hmd.c b/src/xrt/drivers/rokid/rokid_hmd.c
new file mode 100644
index 000000000..20f552d09
--- /dev/null
+++ b/src/xrt/drivers/rokid/rokid_hmd.c
@@ -0,0 +1,599 @@
+// Copyright 2023, Alex Badics
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief  Driver for the Rokid Air and Max devices.
+ *
+ *
+ * @author Alex Badics <admin@stickman.hu>
+ * @ingroup drv_rokid
+ */
+
+#include "rokid_interface.h"
+
+#include "xrt/xrt_defines.h"
+#include "xrt/xrt_device.h"
+#include "xrt/xrt_prober.h"
+
+#include "os/os_threading.h"
+#include "os/os_time.h"
+
+#include "math/m_api.h"
+#include "math/m_imu_3dof.h"
+#include "math/m_mathinclude.h"
+#include "math/m_predict.h"
+
+#include "util/u_debug.h"
+#include "util/u_device.h"
+#include "util/u_distortion_mesh.h"
+#include "util/u_trace_marker.h"
+#include "util/u_var.h"
+
+#ifdef XRT_OS_LINUX
+#include "util/u_linux.h"
+#endif
+
+
+
+#include <libusb.h>
+
+
+/*
+ *
+ * Defines and "normal" structs
+ *
+ */
+
+#define ROKID_USB_INTERFACE_NUM 2
+#define ROKID_INTERRUPT_IN_ENDPOINT 0x82
+#define ROKID_USB_BUFFER_LEN 0x40
+#define ROKID_USB_TRANSFER_TIMEOUT_MS 1000
+
+struct rokid_fusion
+{
+	struct os_mutex mutex;
+	struct m_imu_3dof i3dof;
+	struct xrt_space_relation last_relation;
+	uint64_t last_update;
+	struct xrt_vec3 last_gyro;
+	struct xrt_vec3 last_accel;
+	uint64_t gyro_ts_device;
+	uint64_t accel_ts_device;
+
+	bool initialized;
+};
+
+struct rokid_hmd
+{
+	struct xrt_device base;
+	enum u_logging_level log_level;
+
+	struct os_thread_helper usb_thread;
+
+	libusb_context *usb_ctx;
+	libusb_device_handle *usb_dev;
+
+	struct rokid_fusion fusion;
+};
+
+
+/*
+ *
+ * Packed structs for USB communication
+ *
+ */
+
+#if defined(__GNUC__)
+#define ROKID_PACKED __attribute__((packed))
+#else
+#define ROKID_PACKED
+#endif /* __GNUC__ */
+
+#if defined(_MSC_VER)
+#pragma pack(push, 1)
+#endif
+
+struct rokid_usb_packed_vec
+{
+	float x;
+	float y;
+	float z;
+} ROKID_PACKED;
+
+struct rokid_usb_pkt_combined
+{
+	uint8_t packet_type;
+	uint64_t timestamp;
+	struct rokid_usb_packed_vec accel;
+	struct rokid_usb_packed_vec gyro;
+	struct rokid_usb_packed_vec magnetometer;
+	uint8_t keys_pressed;
+	uint8_t proxy_sensor;
+	uint8_t _unknown_0;
+	uint64_t vsync_timestamp;
+	uint8_t _unknown_1[3];
+	uint8_t display_brightness;
+	uint8_t volume;
+	uint8_t _unknown_2[3];
+} ROKID_PACKED;
+
+
+struct rokid_usb_pkt_sensor
+{
+	uint8_t packet_type;
+	uint8_t sensor_type;
+	uint32_t seq;
+	uint8_t _unknown_0[3];
+	uint64_t timestamp;
+	uint8_t _unknown_1[4];
+	struct rokid_usb_packed_vec vector;
+	uint8_t _unknown_2[31];
+} ROKID_PACKED;
+
+#if defined(_MSC_VER)
+#pragma pack(pop)
+#endif
+
+/*
+ *
+ * Helper functions
+ *
+ */
+
+DEBUG_GET_ONCE_LOG_OPTION(rokid_log, "ROKID_LOG", U_LOGGING_WARN)
+
+#define ROKID_TRACE(p, ...) U_LOG_XDEV_IFL_T(&rokid->base, rokid->log_level, __VA_ARGS__)
+#define ROKID_DEBUG(p, ...) U_LOG_XDEV_IFL_D(&rokid->base, rokid->log_level, __VA_ARGS__)
+#define ROKID_INFO(p, ...) U_LOG_XDEV_IFL_I(&rokid->base, rokid->log_level, __VA_ARGS__)
+#define ROKID_ERROR(p, ...) U_LOG_XDEV_IFL_E(&rokid->base, rokid->log_level, __VA_ARGS__)
+
+
+static struct xrt_vec3
+rokid_convert_vector(struct rokid_usb_packed_vec *v)
+{
+	struct xrt_vec3 result = {
+	    .x = v->x,
+	    .y = v->y,
+	    .z = v->z,
+	};
+	return result;
+}
+
+// Casting helper function
+static inline struct rokid_hmd *
+rokid_hmd(struct xrt_device *xdev)
+{
+	return (struct rokid_hmd *)xdev;
+}
+
+/*
+ *
+ * Fusion-related functions
+ *
+ */
+
+static void
+rokid_fusion_create(struct rokid_fusion *fusion)
+{
+	os_mutex_init(&fusion->mutex);
+	m_imu_3dof_init(&fusion->i3dof, M_IMU_3DOF_USE_GRAVITY_DUR_300MS);
+	struct xrt_space_relation zero_relation = XRT_SPACE_RELATION_ZERO;
+	fusion->last_relation = zero_relation;
+	fusion->initialized = true;
+}
+
+static void
+rokid_fusion_parse_usb_packet(struct rokid_fusion *fusion, unsigned char usb_buffer[ROKID_USB_BUFFER_LEN])
+{
+	switch (usb_buffer[0]) {
+	case 4: {
+		// Old-style packet, where we get one packet for each sensor
+		// Order is usually the same, but not guaranteed, because packet losses.
+		struct rokid_usb_pkt_sensor *packet = (struct rokid_usb_pkt_sensor *)usb_buffer;
+		switch (packet->sensor_type) {
+		case 1: {
+			fusion->last_accel = rokid_convert_vector(&packet->vector);
+			fusion->accel_ts_device = packet->timestamp;
+			break;
+		}
+		case 2: {
+			fusion->last_gyro = rokid_convert_vector(&packet->vector);
+			fusion->gyro_ts_device = packet->timestamp;
+			break;
+		}
+		}
+		break;
+	}
+	case 17: {
+		// New-style combined packet
+		struct rokid_usb_pkt_combined *packet = (struct rokid_usb_pkt_combined *)usb_buffer;
+		fusion->last_gyro = rokid_convert_vector(&packet->gyro);
+		fusion->last_accel = rokid_convert_vector(&packet->accel);
+		fusion->gyro_ts_device = packet->timestamp;
+		fusion->accel_ts_device = packet->timestamp;
+		break;
+	}
+	}
+
+	// Only update fusion once we have data from both sensors for this timestamp
+	if (fusion->gyro_ts_device == fusion->accel_ts_device) {
+		uint64_t now = os_monotonic_get_ns();
+		m_imu_3dof_update(&fusion->i3dof, now, &fusion->last_accel, &fusion->last_gyro);
+		struct xrt_vec3 angular_velocity_ws;
+		math_quat_rotate_vec3(&fusion->i3dof.rot, &fusion->last_gyro, &angular_velocity_ws);
+		fusion->last_relation.relation_flags = XRT_SPACE_RELATION_ORIENTATION_VALID_BIT |
+		                                       XRT_SPACE_RELATION_ANGULAR_VELOCITY_VALID_BIT |
+		                                       XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT;
+		fusion->last_relation.pose.orientation = fusion->i3dof.rot;
+		fusion->last_relation.angular_velocity = angular_velocity_ws;
+		fusion->last_update = now;
+	}
+}
+
+
+static void
+rokid_fusion_get_pose(struct rokid_fusion *fusion, uint64_t at_timestamp_ns, struct xrt_space_relation *out_relation)
+{
+	if (at_timestamp_ns > fusion->last_update) {
+		double time_diff = time_ns_to_s(at_timestamp_ns - fusion->last_update);
+		if (time_diff > 0.1) {
+			time_diff = 0.1;
+		}
+		m_predict_relation(&fusion->last_relation, time_diff, out_relation);
+	} else {
+		*out_relation = fusion->last_relation;
+	}
+}
+
+static void
+rokid_fusion_destroy(struct rokid_fusion *fusion)
+{
+	os_mutex_destroy(&fusion->mutex);
+	m_imu_3dof_close(&fusion->i3dof);
+
+	fusion->initialized = false;
+}
+
+static void
+rokid_fusion_add_vars(struct rokid_fusion *fusion, void *root)
+{
+	m_imu_3dof_add_vars(&fusion->i3dof, root, "fusion.");
+	u_var_add_pose(root, &fusion->last_relation.pose, "last_pose");
+	u_var_add_ro_vec3_f32(root, &fusion->last_gyro, "gyro");
+	u_var_add_ro_vec3_f32(root, &fusion->last_accel, "accel");
+	u_var_add_ro_u64(root, &fusion->last_update, "timestamp");
+}
+
+
+/*
+ *
+ * USB handling boilerplate
+ *
+ */
+
+static void *
+rokid_usb_thread(void *ptr)
+{
+
+	U_TRACE_SET_THREAD_NAME("Rokid USB thread");
+	struct rokid_hmd *rokid = ptr;
+
+#ifdef XRT_OS_LINUX
+	// Try to raise priority of this thread, so we don't miss packets under load
+	u_linux_try_to_set_realtime_priority_on_thread(U_LOGGING_INFO, "Rokid USB thread");
+#endif
+
+	int last_libusb_result = LIBUSB_SUCCESS;
+
+	os_thread_helper_lock(&rokid->usb_thread);
+	while (os_thread_helper_is_running_locked(&rokid->usb_thread) && last_libusb_result == LIBUSB_SUCCESS) {
+		os_thread_helper_unlock(&rokid->usb_thread);
+
+		unsigned char usb_buffer[ROKID_USB_BUFFER_LEN] = {0};
+		int read_length = 0;
+
+		int last_libusb_result =
+		    libusb_interrupt_transfer(rokid->usb_dev, ROKID_INTERRUPT_IN_ENDPOINT, usb_buffer,
+		                              sizeof(usb_buffer), &read_length, ROKID_USB_TRANSFER_TIMEOUT_MS);
+		if (last_libusb_result == LIBUSB_SUCCESS) {
+			os_mutex_lock(&rokid->fusion.mutex);
+			rokid_fusion_parse_usb_packet(&rokid->fusion, usb_buffer);
+			os_mutex_unlock(&rokid->fusion.mutex);
+		}
+
+		os_thread_helper_lock(&rokid->usb_thread);
+	}
+	os_thread_helper_unlock(&rokid->usb_thread);
+	if (last_libusb_result == LIBUSB_SUCCESS) {
+		ROKID_INFO(rokid, "Usb thread exiting normally");
+	} else {
+		ROKID_ERROR(rokid, "Exiting on libusb error %s", libusb_strerror(last_libusb_result));
+	}
+
+	return NULL;
+}
+
+// Returns 0-255 for valid display modes (0 is 2D, 1 is 3D, others are more special modes),
+// and -1 in case of error
+static int
+rokid_hmd_get_display_mode(struct rokid_hmd *rokid)
+{
+	uint8_t data[ROKID_USB_BUFFER_LEN] = {0};
+	int res = libusb_control_transfer(rokid->usb_dev,
+	                                  LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE,
+	                                  0x81, // request type = get display mode,
+	                                  0,    // wValue
+	                                  0x1,  // wIndex
+	                                  data, // data is mandatory for some reason,
+	                                  sizeof(data), ROKID_USB_TRANSFER_TIMEOUT_MS);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to set glasses to SBS mode");
+		return -1;
+	}
+	return data[1];
+}
+
+static bool
+rokid_hmd_set_display_mode(struct rokid_hmd *rokid, uint16_t mode)
+{
+	uint8_t data[1] = {1};
+	int res = libusb_control_transfer(rokid->usb_dev,
+	                                  LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE,
+	                                  0x1,  // request type = set display mode,
+	                                  mode, // display type
+	                                  0x1,  // wIndex is fixed
+	                                  data, // data is mandatory for some reason,
+	                                  sizeof(data), ROKID_USB_TRANSFER_TIMEOUT_MS);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to set glasses to SBS mode");
+		return false;
+	}
+	return true;
+}
+
+static bool
+rokid_hmd_usb_init(struct rokid_hmd *rokid, struct xrt_prober_device *prober_device)
+{
+	int res;
+
+	res = libusb_init(&rokid->usb_ctx);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to init USB");
+		return false;
+	}
+
+	rokid->usb_dev =
+	    libusb_open_device_with_vid_pid(rokid->usb_ctx, prober_device->vendor_id, prober_device->product_id);
+	if (rokid->usb_dev == NULL) {
+		ROKID_ERROR(rokid, "Failed to open USB device");
+		return false;
+	}
+
+	struct libusb_device_descriptor usb_desc;
+	res = libusb_get_device_descriptor(libusb_get_device(rokid->usb_dev), &usb_desc);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to get descriptor");
+		return false;
+	}
+
+	res = libusb_get_string_descriptor_ascii(rokid->usb_dev, usb_desc.iProduct, (unsigned char *)rokid->base.str,
+	                                         XRT_DEVICE_NAME_LEN - 1);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to get product name");
+		return false;
+	}
+	res = libusb_get_string_descriptor_ascii(rokid->usb_dev, usb_desc.iSerialNumber,
+	                                         (unsigned char *)rokid->base.serial, XRT_DEVICE_NAME_LEN - 1);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to get serial");
+		return false;
+	}
+
+	res = libusb_set_auto_detach_kernel_driver(rokid->usb_dev, 1);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to set autodetach on USB device");
+		return false;
+	}
+
+	res = libusb_claim_interface(rokid->usb_dev, ROKID_USB_INTERFACE_NUM);
+	if (res < 0) {
+		ROKID_ERROR(rokid, "Failed to claim USB status interface");
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ *
+ * HMD entry points
+ *
+ */
+
+static void
+rokid_hmd_destroy(struct xrt_device *xdev)
+{
+	// This function has to handle partial initializations,
+	// as it can be called from the middle of the constructor.
+
+	struct rokid_hmd *rokid = rokid_hmd(xdev);
+	if (rokid->usb_thread.initialized) {
+		os_thread_helper_destroy(&rokid->usb_thread);
+	}
+
+	if (rokid->fusion.initialized) {
+		rokid_fusion_destroy(&rokid->fusion);
+	}
+	// Remove the variable tracking.
+	u_var_remove_root(rokid);
+
+	u_device_free(&rokid->base);
+}
+
+static void
+rokid_hmd_update_inputs(struct xrt_device *xdev)
+{
+	// Empty, everything is done in the USB thread
+}
+
+static void
+rokid_hmd_get_tracked_pose(struct xrt_device *xdev,
+                           enum xrt_input_name name,
+                           uint64_t at_timestamp_ns,
+                           struct xrt_space_relation *out_relation)
+{
+	struct rokid_hmd *rokid = rokid_hmd(xdev);
+
+	if (name != XRT_INPUT_GENERIC_HEAD_POSE) {
+		ROKID_ERROR(rokid, "unknown input name");
+		return;
+	}
+	os_mutex_lock(&rokid->fusion.mutex);
+	rokid_fusion_get_pose(&rokid->fusion, at_timestamp_ns, out_relation);
+	os_mutex_unlock(&rokid->fusion.mutex);
+}
+
+static void
+rokid_hmd_get_view_poses(struct xrt_device *xdev,
+                         const struct xrt_vec3 *default_eye_relation,
+                         uint64_t at_timestamp_ns,
+                         uint32_t view_count,
+                         struct xrt_space_relation *out_head_relation,
+                         struct xrt_fov *out_fovs,
+                         struct xrt_pose *out_poses)
+{
+	u_device_get_view_poses(xdev, default_eye_relation, at_timestamp_ns, view_count, out_head_relation, out_fovs,
+	                        out_poses);
+}
+
+static struct xrt_device *
+rokid_hmd_create(struct xrt_prober_device *prober_device)
+{
+	// This indicates you won't be using Monado's built-in tracking algorithms.
+	enum u_device_alloc_flags flags =
+	    (enum u_device_alloc_flags)(U_DEVICE_ALLOC_HMD | U_DEVICE_ALLOC_TRACKING_NONE);
+
+	struct rokid_hmd *rokid = U_DEVICE_ALLOCATE(struct rokid_hmd, flags, 1, 0);
+	rokid->log_level = debug_get_log_option_rokid_log();
+
+	ROKID_DEBUG(rokid, "Starting Rokid driver instance");
+
+	rokid_fusion_create(&rokid->fusion);
+
+	if (os_thread_helper_init(&rokid->usb_thread) != 0) {
+		ROKID_ERROR(hmd, "Failed to init USB thread");
+		goto cleanup;
+	}
+	os_thread_helper_name(&rokid->usb_thread, "Rokid USB thread");
+
+	// This also sets base.str used below.
+	if (!rokid_hmd_usb_init(rokid, prober_device)) {
+		goto cleanup;
+	}
+	bool is_rokid_max = strstr(rokid->base.str, "Max");
+	ROKID_INFO(rokid, "Rokid model: %s", (is_rokid_max ? "Max" : "Air"));
+
+	// This list should be ordered, most preferred first.
+	size_t idx = 0;
+	rokid->base.hmd->blend_modes[idx++] = XRT_BLEND_MODE_OPAQUE;
+	rokid->base.hmd->blend_mode_count = idx;
+
+	rokid->base.update_inputs = rokid_hmd_update_inputs;
+	rokid->base.get_tracked_pose = rokid_hmd_get_tracked_pose;
+	rokid->base.get_view_poses = rokid_hmd_get_view_poses;
+	rokid->base.destroy = rokid_hmd_destroy;
+
+	// Setup input.
+	rokid->base.name = XRT_DEVICE_GENERIC_HMD;
+	rokid->base.device_type = XRT_DEVICE_TYPE_HMD;
+	rokid->base.inputs[0].name = XRT_INPUT_GENERIC_HEAD_POSE;
+	rokid->base.orientation_tracking_supported = true;
+	rokid->base.position_tracking_supported = false;
+
+	// Set up display details
+	// refresh rate
+	rokid->base.hmd->screens[0].nominal_frame_interval_ns = time_s_to_ns(1.0f / 60.0f);
+
+	const float quarter_vFOV = 0.25f * (is_rokid_max ? 46.0f : 40.0f) * ((float)M_PI / 180.0f);
+	const float quarter_hFOV = quarter_vFOV * 16.0f / 9.0f;
+	const float display_tilt = (is_rokid_max ? 0.035f : 0.011f);
+	struct xrt_fov fov = {
+	    .angle_left = -quarter_hFOV,
+	    .angle_right = quarter_hFOV,
+	    .angle_up = quarter_vFOV + display_tilt,
+	    .angle_down = -quarter_vFOV + display_tilt,
+	};
+	rokid->base.hmd->distortion.fov[1] = rokid->base.hmd->distortion.fov[0] = fov;
+
+	const int panel_w = 1920;
+	const int panel_h = 1080;
+
+	// Single "screen" (always the case)
+	rokid->base.hmd->screens[0].w_pixels = panel_w * 2;
+	rokid->base.hmd->screens[0].h_pixels = panel_h;
+
+	// Left, Right
+	for (uint8_t eye = 0; eye < 2; ++eye) {
+		rokid->base.hmd->views[eye].display.w_pixels = panel_w;
+		rokid->base.hmd->views[eye].display.h_pixels = panel_h;
+		rokid->base.hmd->views[eye].viewport.y_pixels = 0;
+		rokid->base.hmd->views[eye].viewport.w_pixels = panel_w;
+		rokid->base.hmd->views[eye].viewport.h_pixels = panel_h;
+		rokid->base.hmd->views[eye].rot = u_device_rotation_ident;
+	}
+	// left eye starts at x=0, right eye starts at x=panel_width
+	rokid->base.hmd->views[0].viewport.x_pixels = 0;
+	rokid->base.hmd->views[1].viewport.x_pixels = panel_w;
+
+	// Distortion information, fills in xdev->compute_distortion().
+	u_distortion_mesh_set_none(&rokid->base);
+
+	// Setup variable tracker: Optional but useful for debugging
+	u_var_add_root(rokid, "Rokid", true);
+	u_var_add_log_level(rokid, &rokid->log_level, "log_level");
+	rokid_fusion_add_vars(&rokid->fusion, rokid);
+
+	if (os_thread_helper_start(&rokid->usb_thread, rokid_usb_thread, rokid) != 0) {
+		ROKID_ERROR(hmd, "Failed to start USB thread");
+		goto cleanup;
+	}
+
+	int display_mode = rokid_hmd_get_display_mode(rokid);
+	if (display_mode < 0) {
+		ROKID_ERROR(hmd, "Failed to get display mode");
+		goto cleanup;
+	}
+	if (display_mode != 1) {
+		ROKID_INFO(rokid, "Setting Rokid display to SBS mode");
+		if (!rokid_hmd_set_display_mode(rokid, 1)) {
+			ROKID_ERROR(hmd, "Failed to get display mode");
+			goto cleanup;
+		}
+		os_nanosleep((int64_t)3 * (int64_t)U_TIME_1S_IN_NS);
+	}
+
+	ROKID_INFO(rokid, "Started Rokid driver instance");
+
+	return &rokid->base;
+
+cleanup:
+	rokid_hmd_destroy(&rokid->base);
+	return NULL;
+}
+
+
+int
+rokid_found(struct xrt_prober *xp,
+            struct xrt_prober_device **devices,
+            size_t device_count,
+            size_t index,
+            cJSON *attached_data,
+            struct xrt_device **out_xdev)
+{
+	struct xrt_device *device = rokid_hmd_create(devices[index]);
+	if (!device) {
+		return 0;
+	}
+	out_xdev[0] = device;
+	return 1;
+}
diff --git a/src/xrt/drivers/rokid/rokid_interface.h b/src/xrt/drivers/rokid/rokid_interface.h
new file mode 100644
index 000000000..a1b76cae1
--- /dev/null
+++ b/src/xrt/drivers/rokid/rokid_interface.h
@@ -0,0 +1,43 @@
+// Copyright 2020-2021, Collabora, Ltd.
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief  Interface to rokid driver.
+ * @author Jakob Bornecrantz <jakob@collabora.com>
+ * @author Ryan Pavlik <ryan.pavlik@collabora.com>
+ * @ingroup drv_rokid
+ */
+
+#pragma once
+
+#include <stddef.h>
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// NOTE: this is 1234:5679
+#define ROKID_VID 0x04d2
+#define ROKID_PID 0x162f
+
+typedef struct cJSON cJSON;
+struct xrt_prober;
+struct xrt_prober_device;
+struct xrt_device;
+
+/*!
+ * Probing function for Rokid devices.
+ *
+ * @ingroup drv_hdk
+ * @see xrt_prober_found_func_t
+ */
+int
+rokid_found(struct xrt_prober *xp,
+            struct xrt_prober_device **devices,
+            size_t device_count,
+            size_t index,
+            cJSON *attached_data,
+            struct xrt_device **out_xdev);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/xrt/targets/common/CMakeLists.txt b/src/xrt/targets/common/CMakeLists.txt
index b5c0de274..82d69c7b4 100644
--- a/src/xrt/targets/common/CMakeLists.txt
+++ b/src/xrt/targets/common/CMakeLists.txt
@@ -136,6 +136,10 @@ if(XRT_BUILD_DRIVER_RIFT_S)
 	target_link_libraries(target_lists PRIVATE drv_rift_s)
 endif()
 
+if(XRT_BUILD_DRIVER_ROKID)
+	target_link_libraries(target_lists PRIVATE drv_rokid)
+endif()
+
 if(XRT_HAVE_V4L2)
 	target_link_libraries(target_lists PRIVATE drv_v4l2)
 endif()
diff --git a/src/xrt/targets/common/target_lists.c b/src/xrt/targets/common/target_lists.c
index 31d35c104..b115e4edc 100644
--- a/src/xrt/targets/common/target_lists.c
+++ b/src/xrt/targets/common/target_lists.c
@@ -44,6 +44,10 @@
 #include "rift_s/rift_s_interface.h"
 #endif
 
+#ifdef XRT_BUILD_DRIVER_ROKID
+#include "rokid/rokid_interface.h"
+#endif
+
 #ifdef XRT_BUILD_DRIVER_HYDRA
 #include "hydra/hydra_interface.h"
 #endif
@@ -164,6 +168,10 @@ struct xrt_prober_entry target_entry_list[] = {
     {PSSENSE_VID, PSSENSE_PID_RIGHT, pssense_found, "PlayStation VR2 Sense Controller (R)", "pssense"},
 #endif // XRT_BUILD_DRIVER_PSSENSE
 
+#ifdef XRT_BUILD_DRIVER_ROKID
+    {ROKID_VID, ROKID_PID, rokid_found, "Rokid Air or Max", "rokid"},
+#endif // XRT_BUILD_DRIVER_ROKID
+
 #ifdef XRT_BUILD_DRIVER_HYDRA
     {HYDRA_VID, HYDRA_PID, hydra_found, "Razer Hydra", "hydra"},
 #endif // XRT_BUILD_DRIVER_HYDRA