diff --git a/CMakeLists.txt b/CMakeLists.txt
index cfe823a3c..dd4397f00 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -215,6 +215,11 @@ set(ILLIXR_PATH
 # This one is named differently because that's what CTest uses
 option(BUILD_TESTING "Enable building of the test suite?" ON)
 
+# Check if Steam's root folder exists
+if(EXISTS "$ENV{HOME}/.steam/root")
+	set(XRT_HAVE_STEAM YES)
+endif()
+
 if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
 	set(XRT_HAVE_INTERNAL_HID ON)
 endif()
@@ -336,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_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)
 option_with_deps(XRT_BUILD_DRIVER_VF "Build video frame driver (for video file support, uses gstreamer)" DEPENDS XRT_HAVE_GST)
@@ -404,6 +410,7 @@ list(
 	"EUROC"
 	"SIMULAVR"
 	"TWRAP"
+	"STEAMVR_LIGHTHOUSE"
 	)
 
 # Package name needs to be known by the native code itself.
@@ -566,33 +573,34 @@ message(STATUS "#    FEATURE_STEAMVR_PLUGIN:               ${XRT_FEATURE_STEAMVR
 message(STATUS "#    FEATURE_TRACING:                      ${XRT_FEATURE_TRACING}")
 message(STATUS "#    FEATURE_WINDOW_PEEK:                  ${XRT_FEATURE_WINDOW_PEEK}")
 message(STATUS "#")
-message(STATUS "#    DRIVER_ANDROID:      ${XRT_BUILD_DRIVER_ANDROID}")
-message(STATUS "#    DRIVER_ARDUINO:      ${XRT_BUILD_DRIVER_ARDUINO}")
-message(STATUS "#    DRIVER_DAYDREAM:     ${XRT_BUILD_DRIVER_DAYDREAM}")
-message(STATUS "#    DRIVER_DEPTHAI:      ${XRT_BUILD_DRIVER_DEPTHAI}")
-message(STATUS "#    DRIVER_EUROC:        ${XRT_BUILD_DRIVER_EUROC}")
-message(STATUS "#    DRIVER_HANDTRACKING: ${XRT_BUILD_DRIVER_HANDTRACKING}")
-message(STATUS "#    DRIVER_HDK:          ${XRT_BUILD_DRIVER_HDK}")
-message(STATUS "#    DRIVER_HYDRA:        ${XRT_BUILD_DRIVER_HYDRA}")
-message(STATUS "#    DRIVER_ILLIXR:       ${XRT_BUILD_DRIVER_ILLIXR}")
-message(STATUS "#    DRIVER_NS:           ${XRT_BUILD_DRIVER_NS}")
-message(STATUS "#    DRIVER_OHMD:         ${XRT_BUILD_DRIVER_OHMD}")
-message(STATUS "#    DRIVER_OPENGLOVES:   ${XRT_BUILD_DRIVER_OPENGLOVES}")
-message(STATUS "#    DRIVER_PSMV:         ${XRT_BUILD_DRIVER_PSMV}")
-message(STATUS "#    DRIVER_PSSENSE:      ${XRT_BUILD_DRIVER_PSSENSE}")
-message(STATUS "#    DRIVER_PSVR:         ${XRT_BUILD_DRIVER_PSVR}")
-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_SIMULATED:    ${XRT_BUILD_DRIVER_SIMULATED}")
-message(STATUS "#    DRIVER_SIMULAVR:     ${XRT_BUILD_DRIVER_SIMULAVR}")
-message(STATUS "#    DRIVER_SURVIVE:      ${XRT_BUILD_DRIVER_SURVIVE}")
-message(STATUS "#    DRIVER_TWRAP:        ${XRT_BUILD_DRIVER_TWRAP}")
-message(STATUS "#    DRIVER_ULV2:         ${XRT_BUILD_DRIVER_ULV2}")
-message(STATUS "#    DRIVER_VF:           ${XRT_BUILD_DRIVER_VF}")
-message(STATUS "#    DRIVER_VIVE:         ${XRT_BUILD_DRIVER_VIVE}")
-message(STATUS "#    DRIVER_WMR:          ${XRT_BUILD_DRIVER_WMR}")
+message(STATUS "#    DRIVER_ANDROID:              ${XRT_BUILD_DRIVER_ANDROID}")
+message(STATUS "#    DRIVER_ARDUINO:              ${XRT_BUILD_DRIVER_ARDUINO}")
+message(STATUS "#    DRIVER_DAYDREAM:             ${XRT_BUILD_DRIVER_DAYDREAM}")
+message(STATUS "#    DRIVER_DEPTHAI:              ${XRT_BUILD_DRIVER_DEPTHAI}")
+message(STATUS "#    DRIVER_EUROC:                ${XRT_BUILD_DRIVER_EUROC}")
+message(STATUS "#    DRIVER_HANDTRACKING:         ${XRT_BUILD_DRIVER_HANDTRACKING}")
+message(STATUS "#    DRIVER_HDK:                  ${XRT_BUILD_DRIVER_HDK}")
+message(STATUS "#    DRIVER_HYDRA:                ${XRT_BUILD_DRIVER_HYDRA}")
+message(STATUS "#    DRIVER_ILLIXR:               ${XRT_BUILD_DRIVER_ILLIXR}")
+message(STATUS "#    DRIVER_NS:                   ${XRT_BUILD_DRIVER_NS}")
+message(STATUS "#    DRIVER_OHMD:                 ${XRT_BUILD_DRIVER_OHMD}")
+message(STATUS "#    DRIVER_OPENGLOVES:           ${XRT_BUILD_DRIVER_OPENGLOVES}")
+message(STATUS "#    DRIVER_PSMV:                 ${XRT_BUILD_DRIVER_PSMV}")
+message(STATUS "#    DRIVER_PSSENSE:              ${XRT_BUILD_DRIVER_PSSENSE}")
+message(STATUS "#    DRIVER_PSVR:                 ${XRT_BUILD_DRIVER_PSVR}")
+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_SIMULATED:            ${XRT_BUILD_DRIVER_SIMULATED}")
+message(STATUS "#    DRIVER_SIMULAVR:             ${XRT_BUILD_DRIVER_SIMULAVR}")
+message(STATUS "#    DRIVER_SURVIVE:              ${XRT_BUILD_DRIVER_SURVIVE}")
+message(STATUS "#    DRIVER_TWRAP:                ${XRT_BUILD_DRIVER_TWRAP}")
+message(STATUS "#    DRIVER_ULV2:                 ${XRT_BUILD_DRIVER_ULV2}")
+message(STATUS "#    DRIVER_VF:                   ${XRT_BUILD_DRIVER_VF}")
+message(STATUS "#    DRIVER_VIVE:                 ${XRT_BUILD_DRIVER_VIVE}")
+message(STATUS "#    DRIVER_WMR:                  ${XRT_BUILD_DRIVER_WMR}")
+message(STATUS "#    DRIVER_STEAMVR_LIGHTHOUSE:   ${XRT_BUILD_DRIVER_STEAMVR_LIGHTHOUSE}")
 message(STATUS "#####----- Config -----#####")
 
 if(XRT_FEATURE_SERVICE AND NOT XRT_FEATURE_OPENXR)
diff --git a/src/xrt/drivers/CMakeLists.txt b/src/xrt/drivers/CMakeLists.txt
index d199ca11d..fa551acfd 100644
--- a/src/xrt/drivers/CMakeLists.txt
+++ b/src/xrt/drivers/CMakeLists.txt
@@ -432,6 +432,32 @@ if(XRT_BUILD_DRIVER_SIMULAVR)
 	list(APPEND ENABLED_HEADSET_DRIVERS svr)
 endif()
 
+if(XRT_BUILD_DRIVER_STEAMVR_LIGHTHOUSE)
+	add_library(
+		drv_steamvr_lh STATIC
+		steamvr_lh/steamvr_lh.cpp
+		steamvr_lh/device.cpp
+		steamvr_lh/interfaces/driver_manager.cpp
+		steamvr_lh/interfaces/iobuffer.cpp
+		steamvr_lh/interfaces/resources.cpp
+		steamvr_lh/interfaces/settings.cpp
+		steamvr_lh/interfaces/blockqueue.cpp
+		steamvr_lh/interfaces/paths.cpp
+		)
+	target_link_libraries(
+		drv_steamvr_lh
+		PRIVATE
+			aux_util
+			aux_math
+			xrt-interfaces
+			xrt-external-openvr
+			xrt-external-cjson
+			xrt-external-vdf
+			${CMAKE_DL_LIBS}
+		)
+	list(APPEND ENABLED_HEADSET_DRIVERS steamvr_lh)
+endif()
+
 if(XRT_BUILD_SAMPLES)
 	# We build the sample driver to make sure it stays valid,
 	# but it never gets linked into a final target.
diff --git a/src/xrt/drivers/steamvr_lh/device.cpp b/src/xrt/drivers/steamvr_lh/device.cpp
new file mode 100644
index 000000000..a8eeae07b
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/device.cpp
@@ -0,0 +1,435 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief SteamVR driver device implementation.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include <functional>
+#include <cstring>
+#include <thread>
+#include <algorithm>
+
+#include "math/m_api.h"
+#include "device.hpp"
+#include "interfaces/context.hpp"
+#include "util/u_device.h"
+#include "util/u_logging.h"
+#include "util/u_json.hpp"
+
+#define DEV_ERR(...) U_LOG_IFL_E(ctx->log_level, __VA_ARGS__)
+#define DEV_WARN(...) U_LOG_IFL_W(ctx->log_level, __VA_ARGS__)
+#define DEV_INFO(...) U_LOG_IFL_I(ctx->log_level, __VA_ARGS__)
+#define DEV_DEBUG(...) U_LOG_IFL_D(ctx->log_level, __VA_ARGS__)
+
+// Each device will have its own input class.
+struct InputClass
+{
+	xrt_device_name name;
+	std::string description;
+	const std::vector<xrt_input_name> poses;
+	const std::unordered_map<std::string_view, xrt_input_name> non_poses;
+};
+
+namespace {
+InputClass hmd_class{{}, "Generic HMD", {XRT_INPUT_GENERIC_HEAD_POSE}, {}};
+
+// Adding support for a new controller is a simple as adding it here.
+// The key for the map needs to be the name of input profile as indicated by the lighthouse driver.
+const std::unordered_map<std::string_view, InputClass> controller_classes{
+    {
+        "vive_controller",
+        InputClass{
+            XRT_DEVICE_VIVE_WAND,
+            "Vive Wand",
+            {
+                XRT_INPUT_VIVE_GRIP_POSE,
+                XRT_INPUT_VIVE_AIM_POSE,
+            },
+            {
+                {"/input/application_menu/click", XRT_INPUT_VIVE_MENU_CLICK},
+                {"/input/trackpad/click", XRT_INPUT_VIVE_TRACKPAD_CLICK},
+                {"/input/trackpad/touch", XRT_INPUT_VIVE_TRACKPAD_TOUCH},
+                {"/input/system/click", XRT_INPUT_VIVE_SYSTEM_CLICK},
+                {"/input/trigger/click", XRT_INPUT_VIVE_TRIGGER_CLICK},
+                {"/input/trigger/value", XRT_INPUT_VIVE_TRIGGER_VALUE},
+                {"/input/grip/click", XRT_INPUT_VIVE_SQUEEZE_CLICK},
+                {"/input/trackpad", XRT_INPUT_VIVE_TRACKPAD},
+            },
+        },
+    },
+};
+
+// Template for calling a member function of Device from a free function
+template <typename DeviceType, auto Func, typename Ret, typename... Args>
+inline Ret
+device_bouncer(struct xrt_device *xdev, Args... args)
+{
+	auto *dev = static_cast<DeviceType *>(xdev);
+	return std::invoke(Func, dev, args...);
+}
+} // namespace
+
+HmdDevice::HmdDevice(const DeviceBuilder &builder) : Device(builder)
+{
+	this->name = XRT_DEVICE_GENERIC_HMD;
+	this->device_type = XRT_DEVICE_TYPE_HMD;
+	this->container_handle = 0;
+
+	set_input_class(&hmd_class);
+
+#define SETUP_MEMBER_FUNC(name) this->xrt_device::name = &device_bouncer<HmdDevice, &HmdDevice::name>
+	SETUP_MEMBER_FUNC(get_view_poses);
+	SETUP_MEMBER_FUNC(compute_distortion);
+#undef SETUP_MEMBER_FUNC
+}
+
+ControllerDevice::ControllerDevice(vr::PropertyContainerHandle_t handle, const DeviceBuilder &builder) : Device(builder)
+{
+	this->device_type = XRT_DEVICE_TYPE_ANY_HAND_CONTROLLER;
+	this->container_handle = handle;
+	this->xrt_device::set_output = &device_bouncer<ControllerDevice, &ControllerDevice::set_output>;
+}
+
+Device::Device(const DeviceBuilder &builder) : xrt_device({}), ctx(builder.ctx), driver(builder.driver)
+{
+	std::strncpy(this->serial, builder.serial, XRT_DEVICE_NAME_LEN);
+	this->tracking_origin = ctx.get();
+	this->orientation_tracking_supported = true;
+	this->position_tracking_supported = true;
+	this->hand_tracking_supported = false;
+	this->force_feedback_supported = false;
+	this->form_factor_check_supported = false;
+
+#define SETUP_MEMBER_FUNC(name) this->xrt_device::name = &device_bouncer<Device, &Device::name>
+	SETUP_MEMBER_FUNC(update_inputs);
+	SETUP_MEMBER_FUNC(get_tracked_pose);
+#undef SETUP_MEMBER_FUNC
+
+	this->xrt_device::destroy = [](xrt_device *xdev) {
+		auto *dev = static_cast<Device *>(xdev);
+		dev->driver->Deactivate();
+		delete dev;
+	};
+
+	init_chaperone(builder.steam_install);
+}
+
+void
+Device::set_input_class(const InputClass *input_class)
+{
+	// this should only be called once
+	assert(inputs_vec.empty());
+	this->input_class = input_class;
+
+	// reserve to ensure our pointers don't get invalidated
+	inputs_vec.reserve(input_class->poses.size() + input_class->non_poses.size());
+	for (xrt_input_name input : input_class->poses) {
+		inputs_vec.push_back({true, 0, input, {}});
+	}
+	for (const auto &[path, input] : input_class->non_poses) {
+		assert(inputs_vec.capacity() >= inputs_vec.size() + 1);
+		inputs_vec.push_back({true, 0, input, {}});
+		inputs_map.insert({path, &inputs_vec.back()});
+	}
+
+	this->inputs = inputs_vec.data();
+	this->input_count = inputs_vec.size();
+}
+
+xrt_input *
+Device::get_input_from_name(const std::string_view name)
+{
+	auto input = inputs_map.find(name);
+	if (input == inputs_map.end()) {
+		DEV_WARN("requested unknown input name %s for device %s", std::string(name).c_str(), serial);
+		return nullptr;
+	}
+	return input->second;
+}
+
+void
+ControllerDevice::set_haptic_handle(vr::VRInputComponentHandle_t handle)
+{
+	// this should only be set once
+	assert(output == nullptr);
+	DEV_DEBUG("setting haptic handle for %lu", handle);
+	haptic_handle = handle;
+	xrt_output_name name;
+	switch (this->name) {
+	case XRT_DEVICE_VIVE_WAND: {
+		name = XRT_OUTPUT_NAME_VIVE_HAPTIC;
+		break;
+	}
+	default: {
+		DEV_WARN("Unknown device name (%u), haptics will not work", this->name);
+		return;
+	}
+	}
+	output = std::make_unique<xrt_output>(xrt_output{name});
+	this->output_count = 1;
+	this->outputs = output.get();
+}
+
+void
+Device::update_inputs()
+{
+	ctx->maybe_run_frame(++current_frame);
+}
+
+void
+Device::get_tracked_pose(xrt_input_name name, uint64_t at_timestamp_ns, xrt_space_relation *out_relation)
+{
+	*out_relation = this->relation;
+}
+
+void
+ControllerDevice::set_output(xrt_output_name name, const xrt_output_value *value)
+
+{
+	const auto &vib = value->vibration;
+	if (vib.amplitude == 0.0)
+		return;
+	vr::VREvent_HapticVibration_t event;
+	event.containerHandle = container_handle;
+	event.componentHandle = haptic_handle;
+	event.fDurationSeconds = (float)vib.duration_ns / 1e9f;
+	// 0.0f in OpenXR means let the driver determine a frequency, but
+	// in OpenVR means no haptic.
+	event.fFrequency = std::max(vib.frequency, 1.0f);
+	event.fAmplitude = vib.amplitude;
+
+	ctx->add_haptic_event(event);
+}
+
+void
+HmdDevice::get_view_poses(const xrt_vec3 *default_eye_relation,
+                          uint64_t at_timestamp_ns,
+                          uint32_t view_count,
+                          xrt_space_relation *out_head_relation,
+                          xrt_fov *out_fovs,
+                          xrt_pose *out_poses)
+{
+	u_device_get_view_poses(this, default_eye_relation, at_timestamp_ns, view_count, out_head_relation, out_fovs,
+	                        out_poses);
+}
+
+bool
+HmdDevice::compute_distortion(uint32_t view, float u, float v, xrt_uv_triplet *out_result)
+{
+	vr::EVREye eye = (view == 0) ? vr::Eye_Left : vr::Eye_Right;
+	vr::DistortionCoordinates_t coords = this->hmd_parts->display->ComputeDistortion(eye, u, v);
+	out_result->r = {coords.rfRed[0], coords.rfRed[1]};
+	out_result->g = {coords.rfGreen[0], coords.rfGreen[1]};
+	out_result->b = {coords.rfBlue[0], coords.rfBlue[1]};
+	return true;
+}
+
+void
+HmdDevice::set_hmd_parts(std::unique_ptr<Parts> parts)
+{
+	{
+		std::lock_guard lk(hmd_parts_mut);
+		hmd_parts = std::move(parts);
+		this->hmd = &hmd_parts->base;
+	}
+	hmd_parts_cv.notify_all();
+}
+
+namespace {
+xrt_quat
+copy_quat(const vr::HmdQuaternion_t &quat)
+{
+	return xrt_quat{(float)quat.x, (float)quat.y, (float)quat.z, (float)quat.w};
+}
+
+xrt_vec3
+copy_vec3(const double (&vec)[3])
+{
+	return xrt_vec3{(float)vec[0], (float)vec[1], (float)vec[2]};
+}
+} // namespace
+
+void
+Device::init_chaperone(const std::string &steam_install)
+{
+	static bool initialized = false;
+	if (initialized)
+		return;
+
+	initialized = true;
+
+	// Lighthouse driver seems to create a lighthousedb.json and a chaperone_info.vrchap (which is json)
+	// We will use the known_universes from the lighthousedb.json to match to a universe from chaperone_info.vrchap
+
+	using xrt::auxiliary::util::json::JSONNode;
+	auto lighthousedb = JSONNode::loadFromFile(steam_install + "/config/lighthouse/lighthousedb.json");
+	if (lighthousedb.isInvalid()) {
+		DEV_ERR("Couldn't load lighthousedb file, playspace center will be off - was Room Setup run?");
+		return;
+	}
+	auto chap_info = JSONNode::loadFromFile(steam_install + "/config/chaperone_info.vrchap");
+	if (chap_info.isInvalid()) {
+		DEV_ERR("Couldn't load chaperone info, playspace center will be off - was Room Setup run?");
+		return;
+	}
+
+	// XXX: This may be broken if there are multiple known universes - how do we determine which to use then?
+	JSONNode universe = lighthousedb["known_universes"][0];
+	std::string id = universe["id"].asString();
+	JSONNode info;
+	for (const JSONNode &u : chap_info["universes"].asArray()) {
+		if (u["universeID"].asString() == id) {
+			DEV_INFO("Found info for universe %s", id.c_str());
+			info = u;
+			break;
+		}
+	}
+
+	if (info.isInvalid()) {
+		DEV_ERR("Couldn't find chaperone info for universe %s, playspace center will be off", id.c_str());
+		return;
+	}
+
+	std::vector<JSONNode> translation_arr = info["standing"]["translation"].asArray();
+	double yaw = info["standing"]["yaw"].asDouble();
+	chaperone_center = {static_cast<float>(translation_arr[0].asDouble()),
+	                    static_cast<float>(translation_arr[1].asDouble()),
+	                    static_cast<float>(translation_arr[2].asDouble())};
+	const xrt_vec3 yaw_axis{0.0, -1.0, 0.0};
+	math_quat_from_angle_vector(yaw, &yaw_axis, &chaperone_yaw);
+	DEV_INFO("Initialized chaperone data.");
+}
+
+void
+Device::update_pose(const vr::DriverPose_t &newPose)
+{
+	xrt_space_relation relation;
+	if (newPose.poseIsValid) {
+		relation.relation_flags = XRT_SPACE_RELATION_BITMASK_ALL;
+
+		const xrt_vec3 to_local_pos = copy_vec3(newPose.vecDriverFromHeadTranslation);
+		const xrt_quat to_local_rot = copy_quat(newPose.qDriverFromHeadRotation);
+		const xrt_vec3 to_world_pos = copy_vec3(newPose.vecWorldFromDriverTranslation);
+		const xrt_quat to_world_rot = copy_quat(newPose.qWorldFromDriverRotation);
+
+		xrt_pose &pose = relation.pose;
+		pose.position = copy_vec3(newPose.vecPosition);
+		pose.orientation = copy_quat(newPose.qRotation);
+		relation.linear_velocity = copy_vec3(newPose.vecVelocity);
+		relation.angular_velocity = copy_vec3(newPose.vecAngularVelocity);
+
+		// apply world transform
+		auto world_transform = [&](xrt_vec3 &vec) {
+			math_quat_rotate_vec3(&to_world_rot, &vec, &vec);
+			math_vec3_accum(&to_world_pos, &vec);
+		};
+		world_transform(pose.position);
+		world_transform(relation.linear_velocity);
+		math_quat_rotate(&to_world_rot, &pose.orientation, &pose.orientation);
+		math_quat_rotate_vec3(&pose.orientation, &relation.angular_velocity, &relation.angular_velocity);
+
+		// apply local transform
+		xrt_vec3 local_rotated;
+		math_quat_rotate_vec3(&pose.orientation, &to_local_pos, &local_rotated);
+		math_vec3_accum(&local_rotated, &pose.position);
+		math_vec3_accum(&local_rotated, &relation.linear_velocity);
+		math_quat_rotate(&pose.orientation, &to_local_rot, &pose.orientation);
+
+		// apply chaperone transform
+		auto chap_transform = [&](xrt_vec3 &vec) {
+			math_vec3_accum(&chaperone_center, &vec);
+			math_quat_rotate_vec3(&chaperone_yaw, &vec, &vec);
+		};
+		chap_transform(pose.position);
+		chap_transform(relation.linear_velocity);
+		math_quat_rotate(&chaperone_yaw, &pose.orientation, &pose.orientation);
+	} else {
+		relation.relation_flags = XRT_SPACE_RELATION_BITMASK_NONE;
+	}
+	this->relation = relation;
+}
+
+void
+Device::handle_properties(const vr::PropertyWrite_t *batch, uint32_t count)
+{
+	for (uint32_t i = 0; i < count; ++i) {
+		handle_property_write(batch[i]);
+	}
+}
+
+void
+HmdDevice::set_nominal_frame_interval(uint64_t interval_ns)
+{
+	auto set = [this, interval_ns] { hmd_parts->base.screens[0].nominal_frame_interval_ns = interval_ns; };
+
+	if (hmd_parts) {
+		set();
+	} else {
+		std::thread t([this, set] {
+			std::unique_lock lk(hmd_parts_mut);
+			hmd_parts_cv.wait(lk, [this] { return hmd_parts != nullptr; });
+			set();
+		});
+		t.detach();
+	}
+}
+
+namespace {
+// From openvr driver documentation
+// (https://github.com/ValveSoftware/openvr/blob/master/docs/Driver_API_Documentation.md#Input-Profiles):
+// "Input profiles are expected to be a valid JSON file,
+// and should be located: <driver_name>/resources/input/<device_name>_profile.json"
+// So we will just parse the file name to get the device name.
+std::string_view
+parse_profile(std::string_view path)
+{
+	size_t name_start_idx = path.find_last_of('/') + 1;
+	size_t name_end_idx = path.find_last_of('_');
+	return path.substr(name_start_idx, name_end_idx - name_start_idx);
+}
+} // namespace
+
+void
+HmdDevice::handle_property_write(const vr::PropertyWrite_t &prop)
+{
+	switch (prop.prop) {
+	case vr::Prop_DisplayFrequency_Float: {
+		assert(prop.unBufferSize == sizeof(float));
+		float freq = *static_cast<float *>(prop.pvBuffer);
+		set_nominal_frame_interval((1.f / freq) * 1e9f);
+		break;
+	}
+	case vr::Prop_InputProfilePath_String: {
+		std::string_view profile =
+		    parse_profile(std::string_view(static_cast<char *>(prop.pvBuffer), prop.unBufferSize));
+		if (profile == "vive") {
+			std::strcpy(this->str, "Vive HMD");
+		}
+	}
+	default: break;
+	}
+}
+
+void
+ControllerDevice::handle_property_write(const vr::PropertyWrite_t &prop)
+{
+	switch (prop.prop) {
+	case vr::Prop_InputProfilePath_String: {
+		std::string_view profile =
+		    parse_profile(std::string_view(static_cast<char *>(prop.pvBuffer), prop.unBufferSize));
+		auto input_class = controller_classes.find(profile);
+		if (input_class == controller_classes.end()) {
+			DEV_ERR("Could not find input class for controller profile %s", std::string(profile).c_str());
+		} else {
+			std::strcpy(this->str, input_class->second.description.c_str());
+			this->name = input_class->second.name;
+			set_input_class(&input_class->second);
+		}
+		break;
+	}
+	default: break;
+	}
+}
diff --git a/src/xrt/drivers/steamvr_lh/device.hpp b/src/xrt/drivers/steamvr_lh/device.hpp
new file mode 100644
index 000000000..1aa56b5da
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/device.hpp
@@ -0,0 +1,140 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief SteamVR driver device header - inherits xrt_device.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include <string>
+#include <vector>
+#include <memory>
+#include <unordered_map>
+
+#include <condition_variable>
+#include <mutex>
+
+#include "xrt/xrt_device.h"
+#include "openvr_driver.h"
+
+class Context;
+struct InputClass;
+
+struct DeviceBuilder
+{
+	std::shared_ptr<Context> ctx;
+	vr::ITrackedDeviceServerDriver *driver;
+	const char *serial;
+	const std::string &steam_install;
+
+	// no copies!
+	DeviceBuilder(const DeviceBuilder &) = delete;
+	DeviceBuilder
+	operator=(const DeviceBuilder &) = delete;
+};
+
+class Device : public xrt_device
+{
+
+public:
+	xrt_space_relation relation = XRT_SPACE_RELATION_ZERO;
+
+	virtual ~Device() = default;
+
+	xrt_input *
+	get_input_from_name(std::string_view name);
+
+	void
+	update_inputs();
+
+	void
+	update_pose(const vr::DriverPose_t &newPose);
+
+	void
+	get_tracked_pose(xrt_input_name name, uint64_t at_timestamp_ns, xrt_space_relation *out_relation);
+
+	void
+	handle_properties(const vr::PropertyWrite_t *batch, uint32_t count);
+
+protected:
+	Device(const DeviceBuilder &builder);
+	std::shared_ptr<Context> ctx;
+	vr::PropertyContainerHandle_t container_handle{0};
+
+	virtual void
+	handle_property_write(const vr::PropertyWrite_t &prop) = 0;
+
+	void
+	set_input_class(const InputClass *input_class);
+
+private:
+	vr::ITrackedDeviceServerDriver *driver;
+	const InputClass *input_class;
+	std::vector<xrt_binding_profile> binding_profiles_vec;
+	std::unordered_map<std::string_view, xrt_input *> inputs_map;
+	std::vector<xrt_input> inputs_vec;
+	uint64_t current_frame{0};
+
+	void
+	init_chaperone(const std::string &steam_install);
+	inline static xrt_vec3 chaperone_center{};
+	inline static xrt_quat chaperone_yaw = XRT_QUAT_IDENTITY;
+};
+
+class HmdDevice : public Device
+{
+public:
+	struct Parts
+	{
+		xrt_hmd_parts base;
+		vr::IVRDisplayComponent *display;
+	};
+
+	HmdDevice(const DeviceBuilder &builder);
+
+	void
+	get_view_poses(const xrt_vec3 *default_eye_relation,
+	               uint64_t at_timestamp_ns,
+	               uint32_t view_count,
+	               xrt_space_relation *out_head_relation,
+	               xrt_fov *out_fovs,
+	               xrt_pose *out_poses);
+
+	bool
+	compute_distortion(uint32_t view, float u, float v, xrt_uv_triplet *out_result);
+
+	void
+	set_hmd_parts(std::unique_ptr<Parts> parts);
+
+private:
+	std::unique_ptr<Parts> hmd_parts{nullptr};
+
+	void
+	handle_property_write(const vr::PropertyWrite_t &prop) override;
+
+	void
+	set_nominal_frame_interval(uint64_t interval_ns);
+
+	std::condition_variable hmd_parts_cv;
+	std::mutex hmd_parts_mut;
+};
+
+class ControllerDevice : public Device
+{
+public:
+	ControllerDevice(vr::PropertyContainerHandle_t container_handle, const DeviceBuilder &builder);
+
+	void
+	set_output(xrt_output_name name, const xrt_output_value *value);
+
+	void
+	set_haptic_handle(vr::VRInputComponentHandle_t handle);
+
+private:
+	vr::VRInputComponentHandle_t haptic_handle{0};
+	std::unique_ptr<xrt_output> output{nullptr};
+
+	void
+	handle_property_write(const vr::PropertyWrite_t &prop) override;
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/blockqueue.cpp b/src/xrt/drivers/steamvr_lh/interfaces/blockqueue.cpp
new file mode 100644
index 000000000..d9cf0f3cc
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/blockqueue.cpp
@@ -0,0 +1,84 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRBlockQueue interface implementation.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include "blockqueue.hpp"
+
+
+// NOLINTBEGIN(bugprone-easily-swappable-parameters)
+vr::EBlockQueueError
+BlockQueue::Create(vr::PropertyContainerHandle_t *pulQueueHandle,
+                   char *pchPath,
+                   uint32_t unBlockDataSize,
+                   uint32_t unBlockHeaderSize,
+                   uint32_t unBlockCount,
+                   uint32_t unFlags)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::Connect(vr::PropertyContainerHandle_t *pulQueueHandle, char *pchPath)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::Destroy(vr::PropertyContainerHandle_t ulQueueHandle)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::AcquireWriteOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+                                  vr::PropertyContainerHandle_t *pulBlockHandle,
+                                  void **ppvBuffer)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::ReleaseWriteOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+                                  vr::PropertyContainerHandle_t ulBlockHandle)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::WaitAndAcquireReadOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+                                        vr::PropertyContainerHandle_t *pulBlockHandle,
+                                        void **ppvBuffer,
+                                        vr::EBlockQueueReadType eReadType,
+                                        uint32_t unTimeoutMs)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::AcquireReadOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+                                 vr::PropertyContainerHandle_t *pulBlockHandle,
+                                 void **ppvBuffer,
+                                 vr::EBlockQueueReadType eReadType)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::ReleaseReadOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+                                 vr::PropertyContainerHandle_t ulBlockHandle)
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+
+vr::EBlockQueueError
+BlockQueue::QueueHasReader(vr::PropertyContainerHandle_t ulQueueHandle, bool *pbHasReaders)
+
+{
+	return vr::EBlockQueueError_BlockQueueError_None;
+}
+// NOLINTEND(bugprone-easily-swappable-parameters)
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/blockqueue.hpp b/src/xrt/drivers/steamvr_lh/interfaces/blockqueue.hpp
new file mode 100644
index 000000000..f1c1c866e
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/blockqueue.hpp
@@ -0,0 +1,77 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRBlockQueue interface header.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+#include "openvr_driver.h"
+
+/** Definitions missing from C++ header, present in C */
+namespace vr {
+inline const char *IVRBlockQueue_Version = "IVRBlockQueue_005";
+
+typedef enum EBlockQueueError
+{
+	EBlockQueueError_BlockQueueError_None = 0,
+	EBlockQueueError_BlockQueueError_QueueAlreadyExists = 1,
+	EBlockQueueError_BlockQueueError_QueueNotFound = 2,
+	EBlockQueueError_BlockQueueError_BlockNotAvailable = 3,
+	EBlockQueueError_BlockQueueError_InvalidHandle = 4,
+	EBlockQueueError_BlockQueueError_InvalidParam = 5,
+	EBlockQueueError_BlockQueueError_ParamMismatch = 6,
+	EBlockQueueError_BlockQueueError_InternalError = 7,
+	EBlockQueueError_BlockQueueError_AlreadyInitialized = 8,
+	EBlockQueueError_BlockQueueError_OperationIsServerOnly = 9,
+	EBlockQueueError_BlockQueueError_TooManyConnections = 10,
+} EBlockQueueError;
+
+typedef enum EBlockQueueReadType
+{
+	EBlockQueueReadType_BlockQueueRead_Latest = 0,
+	EBlockQueueReadType_BlockQueueRead_New = 1,
+	EBlockQueueReadType_BlockQueueRead_Next = 2,
+} EBlockQueueReadType;
+} // namespace vr
+
+/** This interface is missing in the C++ header but present in the C one, and the lighthouse driver requires it. */
+class BlockQueue
+{
+public:
+	virtual vr::EBlockQueueError
+	Create(vr::PropertyContainerHandle_t *pulQueueHandle,
+	       char *pchPath,
+	       uint32_t unBlockDataSize,
+	       uint32_t unBlockHeaderSize,
+	       uint32_t unBlockCount,
+	       uint32_t unFlags);
+	virtual vr::EBlockQueueError
+	Connect(vr::PropertyContainerHandle_t *pulQueueHandle, char *pchPath);
+	virtual vr::EBlockQueueError
+	Destroy(vr::PropertyContainerHandle_t ulQueueHandle);
+	virtual vr::EBlockQueueError
+	AcquireWriteOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+	                      vr::PropertyContainerHandle_t *pulBlockHandle,
+	                      void **ppvBuffer);
+	virtual vr::EBlockQueueError
+	ReleaseWriteOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle, vr::PropertyContainerHandle_t ulBlockHandle);
+	virtual vr::EBlockQueueError
+	WaitAndAcquireReadOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+	                            vr::PropertyContainerHandle_t *pulBlockHandle,
+	                            void **ppvBuffer,
+	                            vr::EBlockQueueReadType eReadType,
+	                            uint32_t unTimeoutMs);
+	virtual vr::EBlockQueueError
+	AcquireReadOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle,
+	                     vr::PropertyContainerHandle_t *pulBlockHandle,
+	                     void **ppvBuffer,
+	                     vr::EBlockQueueReadType eReadType);
+	virtual vr::EBlockQueueError
+	ReleaseReadOnlyBlock(vr::PropertyContainerHandle_t ulQueueHandle, vr::PropertyContainerHandle_t ulBlockHandle);
+	virtual vr::EBlockQueueError
+	QueueHasReader(vr::PropertyContainerHandle_t ulQueueHandle, bool *pbHasReaders);
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/context.hpp b/src/xrt/drivers/steamvr_lh/interfaces/context.hpp
new file mode 100644
index 000000000..830dafb4d
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/context.hpp
@@ -0,0 +1,233 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief SteamVR driver context - implements xrt_tracking_origin and IVRDriverContext.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+#include <unordered_map>
+#include <memory>
+#include <optional>
+#include <chrono>
+#include <deque>
+#include <mutex>
+
+#include "openvr_driver.h"
+
+#include "settings.hpp"
+#include "resources.hpp"
+#include "iobuffer.hpp"
+#include "driver_manager.hpp"
+#include "server.hpp"
+#include "blockqueue.hpp"
+#include "paths.hpp"
+
+#include "xrt/xrt_tracking.h"
+
+struct xrt_input;
+class Device;
+class Context final : public xrt_tracking_origin,
+                      public vr::IVRDriverContext,
+                      public vr::IVRServerDriverHost,
+                      public vr::IVRDriverInput,
+                      public vr::IVRProperties,
+                      public vr::IVRDriverLog,
+                      public std::enable_shared_from_this<Context>
+
+{
+	Settings settings;
+	Resources resources;
+	IOBuffer iobuf;
+	DriverManager man;
+	Server server;
+	BlockQueue blockqueue;
+	Paths paths;
+
+	uint64_t current_frame{0};
+
+	std::unordered_map<vr::VRInputComponentHandle_t, xrt_input *> handle_to_input;
+	struct Vec2Components
+	{
+		vr::VRInputComponentHandle_t x;
+		vr::VRInputComponentHandle_t y;
+	};
+	std::unordered_map<vr::VRInputComponentHandle_t, Vec2Components *> vec2_inputs;
+	std::unordered_map<xrt_input *, std::unique_ptr<Vec2Components>> vec2_input_to_components;
+
+	struct Event
+	{
+		std::chrono::steady_clock::time_point insert_time;
+		vr::VREvent_t inner;
+	};
+	std::deque<Event> events;
+	std::mutex event_queue_mut;
+
+	Device *
+	prop_container_to_device(vr::PropertyContainerHandle_t handle);
+
+	vr::EVRInputError
+	create_component_common(vr::PropertyContainerHandle_t container,
+	                        const char *name,
+	                        vr::VRInputComponentHandle_t *handle);
+
+	xrt_input *
+	update_component_common(vr::VRInputComponentHandle_t handle,
+	                        double offset,
+	                        std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now());
+
+	bool
+	setup_hmd(const char *serial, vr::ITrackedDeviceServerDriver *driver);
+
+	bool
+	setup_controller(const char *serial, vr::ITrackedDeviceServerDriver *driver);
+	vr::IServerTrackedDeviceProvider *provider;
+
+protected:
+	Context(const std::string &steam_install, const std::string &steamvr_install, u_logging_level level);
+
+public:
+	// These are owned by monado, context is destroyed when these are destroyed
+	class HmdDevice *hmd{nullptr};
+	class ControllerDevice *controller[2]{nullptr, nullptr};
+	const u_logging_level log_level;
+
+	~Context();
+
+	[[nodiscard]] static std::shared_ptr<Context>
+	create(const std::string &steam_install,
+	       const std::string &steamvr_install,
+	       vr::IServerTrackedDeviceProvider *p);
+
+	void
+	maybe_run_frame(uint64_t new_frame);
+
+	void
+	add_haptic_event(vr::VREvent_HapticVibration_t event);
+
+	void
+	Log(const char *pchLogMessage) override;
+
+	/***** IVRDriverContext methods *****/
+
+	void *
+	GetGenericInterface(const char *pchInterfaceVersion, vr::EVRInitError *peError) override;
+
+	vr::DriverHandle_t
+	GetDriverHandle() override;
+
+	/***** IVRServerDriverHost methods *****/
+	bool
+	TrackedDeviceAdded(const char *pchDeviceSerialNumber,
+	                   vr::ETrackedDeviceClass eDeviceClass,
+	                   vr::ITrackedDeviceServerDriver *pDriver) override;
+
+	void
+	TrackedDevicePoseUpdated(uint32_t unWhichDevice,
+	                         const vr::DriverPose_t &newPose,
+	                         uint32_t unPoseStructSize) override;
+
+	void
+	VsyncEvent(double vsyncTimeOffsetSeconds) override;
+
+	void
+	VendorSpecificEvent(uint32_t unWhichDevice,
+	                    vr::EVREventType eventType,
+	                    const vr::VREvent_Data_t &eventData,
+	                    double eventTimeOffset) override;
+
+	bool
+	IsExiting() override;
+
+	bool
+	PollNextEvent(vr::VREvent_t *pEvent, uint32_t uncbVREvent) override;
+
+	void
+	GetRawTrackedDevicePoses(float fPredictedSecondsFromNow,
+	                         vr::TrackedDevicePose_t *pTrackedDevicePoseArray,
+	                         uint32_t unTrackedDevicePoseArrayCount) override;
+
+	void
+	RequestRestart(const char *pchLocalizedReason,
+	               const char *pchExecutableToStart,
+	               const char *pchArguments,
+	               const char *pchWorkingDirectory) override;
+
+	uint32_t
+	GetFrameTimings(vr::Compositor_FrameTiming *pTiming, uint32_t nFrames) override;
+
+	void
+	SetDisplayEyeToHead(uint32_t unWhichDevice,
+	                    const vr::HmdMatrix34_t &eyeToHeadLeft,
+	                    const vr::HmdMatrix34_t &eyeToHeadRight) override;
+
+	void
+	SetDisplayProjectionRaw(uint32_t unWhichDevice,
+	                        const vr::HmdRect2_t &eyeLeft,
+	                        const vr::HmdRect2_t &eyeRight) override;
+
+	void
+	SetRecommendedRenderTargetSize(uint32_t unWhichDevice, uint32_t nWidth, uint32_t nHeight) override;
+
+	/***** IVRDriverInput methods *****/
+
+	vr::EVRInputError
+	CreateBooleanComponent(vr::PropertyContainerHandle_t ulContainer,
+	                       const char *pchName,
+	                       vr::VRInputComponentHandle_t *pHandle) override;
+
+	vr::EVRInputError
+	UpdateBooleanComponent(vr::VRInputComponentHandle_t ulComponent, bool bNewValue, double fTimeOffset) override;
+
+	vr::EVRInputError
+	CreateScalarComponent(vr::PropertyContainerHandle_t ulContainer,
+	                      const char *pchName,
+	                      vr::VRInputComponentHandle_t *pHandle,
+	                      vr::EVRScalarType eType,
+	                      vr::EVRScalarUnits eUnits) override;
+
+	vr::EVRInputError
+	UpdateScalarComponent(vr::VRInputComponentHandle_t ulComponent, float fNewValue, double fTimeOffset) override;
+
+	vr::EVRInputError
+	CreateHapticComponent(vr::PropertyContainerHandle_t ulContainer,
+	                      const char *pchName,
+	                      vr::VRInputComponentHandle_t *pHandle) override;
+
+	vr::EVRInputError
+	CreateSkeletonComponent(vr::PropertyContainerHandle_t ulContainer,
+	                        const char *pchName,
+	                        const char *pchSkeletonPath,
+	                        const char *pchBasePosePath,
+	                        vr::EVRSkeletalTrackingLevel eSkeletalTrackingLevel,
+	                        const vr::VRBoneTransform_t *pGripLimitTransforms,
+	                        uint32_t unGripLimitTransformCount,
+	                        vr::VRInputComponentHandle_t *pHandle) override;
+
+	vr::EVRInputError
+	UpdateSkeletonComponent(vr::VRInputComponentHandle_t ulComponent,
+	                        vr::EVRSkeletalMotionRange eMotionRange,
+	                        const vr::VRBoneTransform_t *pTransforms,
+	                        uint32_t unTransformCount) override;
+
+	/***** IVRProperties methods *****/
+
+	vr::ETrackedPropertyError
+	ReadPropertyBatch(vr::PropertyContainerHandle_t ulContainerHandle,
+	                  vr::PropertyRead_t *pBatch,
+	                  uint32_t unBatchEntryCount) override;
+
+	vr::ETrackedPropertyError
+	WritePropertyBatch(vr::PropertyContainerHandle_t ulContainerHandle,
+	                   vr::PropertyWrite_t *pBatch,
+	                   uint32_t unBatchEntryCount) override;
+
+	const char *
+	GetPropErrorNameFromEnum(vr::ETrackedPropertyError error) override;
+
+	vr::PropertyContainerHandle_t
+	TrackedDeviceToPropertyContainer(vr::TrackedDeviceIndex_t nDevice) override;
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/driver_manager.cpp b/src/xrt/drivers/steamvr_lh/interfaces/driver_manager.cpp
new file mode 100644
index 000000000..959c19ac6
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/driver_manager.cpp
@@ -0,0 +1,34 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRDriverManager interface implementation.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include "driver_manager.hpp"
+
+uint32_t
+DriverManager::GetDriverCount() const
+{
+	return 1;
+}
+
+uint32_t
+DriverManager::GetDriverName(vr::DriverId_t nDriver, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize)
+{
+	return 0;
+}
+
+vr::DriverHandle_t
+DriverManager::GetDriverHandle(const char *pchDriverName)
+{
+	return 1;
+}
+
+bool
+DriverManager::IsEnabled(vr::DriverId_t nDriver) const
+{
+	return nDriver == 1;
+}
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/driver_manager.hpp b/src/xrt/drivers/steamvr_lh/interfaces/driver_manager.hpp
new file mode 100644
index 000000000..77797fcd1
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/driver_manager.hpp
@@ -0,0 +1,28 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRDriverManager interface header.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+#include "openvr_driver.h"
+
+class DriverManager : public vr::IVRDriverManager
+{
+public:
+	uint32_t
+	GetDriverCount() const override;
+
+	uint32_t
+	GetDriverName(vr::DriverId_t nDriver, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize) override;
+
+	vr::DriverHandle_t
+	GetDriverHandle(const char *pchDriverName) override;
+
+	bool
+	IsEnabled(vr::DriverId_t nDriver) const override;
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/iobuffer.cpp b/src/xrt/drivers/steamvr_lh/interfaces/iobuffer.cpp
new file mode 100644
index 000000000..eec7e0bfd
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/iobuffer.cpp
@@ -0,0 +1,52 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRIOBuffer interface implementation.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include "iobuffer.hpp"
+
+// NOLINTBEGIN(bugprone-easily-swappable-parameters)
+vr::EIOBufferError
+IOBuffer::Open(const char *pchPath,
+               vr::EIOBufferMode mode,
+               uint32_t unElementSize,
+               uint32_t unElements,
+               vr::IOBufferHandle_t *pulBuffer)
+{
+	return vr::IOBuffer_Success;
+}
+
+vr::EIOBufferError
+IOBuffer::Close(vr::IOBufferHandle_t ulBuffer)
+{
+	return vr::IOBuffer_Success;
+}
+
+vr::EIOBufferError
+IOBuffer::Read(vr::IOBufferHandle_t ulBuffer, void *pDst, uint32_t unBytes, uint32_t *punRead)
+{
+	return vr::IOBuffer_Success;
+}
+
+vr::EIOBufferError
+IOBuffer::Write(vr::IOBufferHandle_t ulBuffer, void *pSrc, uint32_t unBytes)
+{
+	return vr::IOBuffer_Success;
+}
+
+vr::PropertyContainerHandle_t
+IOBuffer::PropertyContainer(vr::IOBufferHandle_t ulBuffer)
+{
+	return 1;
+}
+
+bool
+IOBuffer::HasReaders(vr::IOBufferHandle_t ulBuffer)
+{
+	return false;
+}
+// NOLINTEND(bugprone-easily-swappable-parameters)
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/iobuffer.hpp b/src/xrt/drivers/steamvr_lh/interfaces/iobuffer.hpp
new file mode 100644
index 000000000..92c8cae19
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/iobuffer.hpp
@@ -0,0 +1,44 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRIOBuffer interface header.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+#include "openvr_driver.h"
+
+class IOBuffer : public vr::IVRIOBuffer
+{
+public:
+	/** opens an existing or creates a new IOBuffer of unSize bytes */
+	vr::EIOBufferError
+	Open(const char *pchPath,
+	     vr::EIOBufferMode mode,
+	     uint32_t unElementSize,
+	     uint32_t unElements,
+	     vr::IOBufferHandle_t *pulBuffer) override;
+
+	/** closes a previously opened or created buffer */
+	vr::EIOBufferError
+	Close(vr::IOBufferHandle_t ulBuffer) override;
+
+	/** reads up to unBytes from buffer into *pDst, returning number of bytes read in *punRead */
+	vr::EIOBufferError
+	Read(vr::IOBufferHandle_t ulBuffer, void *pDst, uint32_t unBytes, uint32_t *punRead) override;
+
+	/** writes unBytes of data from *pSrc into a buffer. */
+	vr::EIOBufferError
+	Write(vr::IOBufferHandle_t ulBuffer, void *pSrc, uint32_t unBytes) override;
+
+	/** retrieves the property container of an buffer. */
+	vr::PropertyContainerHandle_t
+	PropertyContainer(vr::IOBufferHandle_t ulBuffer) override;
+
+	/** inexpensively checks for readers to allow writers to fast-fail potentially expensive copies and writes. */
+	bool
+	HasReaders(vr::IOBufferHandle_t ulBuffer) override;
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/paths.cpp b/src/xrt/drivers/steamvr_lh/interfaces/paths.cpp
new file mode 100644
index 000000000..38dd4d315
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/paths.cpp
@@ -0,0 +1,36 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRPaths interface implementation.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include "paths.hpp"
+
+vr::ETrackedPropertyError
+Paths::ReadPathBatch(vr::PropertyContainerHandle_t ulRootHandle, struct PathRead_t *pBatch, uint32_t unBatchEntryCount)
+{
+	return vr::TrackedProp_Success;
+}
+
+vr::ETrackedPropertyError
+Paths::WritePathBatch(vr::PropertyContainerHandle_t ulRootHandle,
+                      struct PathWrite_t *pBatch,
+                      uint32_t unBatchEntryCount)
+{
+	return vr::TrackedProp_Success;
+}
+
+vr::ETrackedPropertyError
+Paths::StringToHandle(vr::PathHandle_t *pHandle, char *pchPath)
+{
+	return vr::TrackedProp_Success;
+}
+
+vr::ETrackedPropertyError
+Paths::HandleToString(vr::PathHandle_t pHandle, char *pchBuffer, uint32_t unBufferSize, uint32_t *punBufferSizeUsed)
+{
+	return vr::TrackedProp_Success;
+}
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/paths.hpp b/src/xrt/drivers/steamvr_lh/interfaces/paths.hpp
new file mode 100644
index 000000000..5ed1fb4c2
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/paths.hpp
@@ -0,0 +1,34 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRPaths interface header.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+#include "openvr_driver.h"
+
+namespace vr {
+inline const char *IVRPaths_Version = "IVRPaths_001";
+typedef uint64_t PathHandle_t;
+}; // namespace vr
+
+/** This interface is missing in the C++ header but present in the C one, and the lighthouse driver requires it. */
+class Paths
+{
+	virtual vr::ETrackedPropertyError
+	ReadPathBatch(vr::PropertyContainerHandle_t ulRootHandle,
+	              struct PathRead_t *pBatch,
+	              uint32_t unBatchEntryCount);
+	virtual vr::ETrackedPropertyError
+	WritePathBatch(vr::PropertyContainerHandle_t ulRootHandle,
+	               struct PathWrite_t *pBatch,
+	               uint32_t unBatchEntryCount);
+	virtual vr::ETrackedPropertyError
+	StringToHandle(vr::PathHandle_t *pHandle, char *pchPath);
+	virtual vr::ETrackedPropertyError
+	HandleToString(vr::PathHandle_t pHandle, char *pchBuffer, uint32_t unBufferSize, uint32_t *punBufferSizeUsed);
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/resources.cpp b/src/xrt/drivers/steamvr_lh/interfaces/resources.cpp
new file mode 100644
index 000000000..076d5d85f
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/resources.cpp
@@ -0,0 +1,62 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRResources interface implementation.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include "resources.hpp"
+#include "util/u_logging.h"
+#include <cstring>
+
+#define RES_ERR(...) U_LOG_IFL_E(log_level, __VA_ARGS__)
+// NOLINTBEGIN(bugprone-easily-swappable-parameters)
+uint32_t
+Resources::LoadSharedResource(const char *pchResourceName, char *pchBuffer, uint32_t unBufferLen)
+{
+	return 0;
+}
+
+uint32_t
+Resources::GetResourceFullPath(const char *pchResourceName,
+                               const char *pchResourceTypeDirectory,
+                               char *pchPathBuffer,
+                               uint32_t unBufferLen)
+{
+	std::string resource(pchResourceName);
+
+	auto return_str = [&](const std::string &str) {
+		const auto len = str.size() + 1;
+		if (unBufferLen >= len) {
+			std::strncpy(pchPathBuffer, str.c_str(), len);
+		}
+		return len;
+	};
+	// loading resource from driver folder (i.e. htc)
+	if (resource[0] == '{') {
+		const size_t idx = resource.find('}');
+		if (idx == std::string::npos) {
+			RES_ERR("malformed resource name: %s", resource.c_str());
+			return 0;
+		}
+		const std::string driver = resource.substr(1, idx - 1);
+		std::string path = steamvr_install + "/drivers/" + driver + "/resources/";
+		if (pchResourceTypeDirectory)
+			path += pchResourceTypeDirectory + std::string("/");
+
+		// for some reason sometimes it gives the paths like {driver}resource.file instead of
+		// {driver}/resource.file
+		path += resource.substr(idx + 1);
+		return return_str(path);
+	}
+
+	// loading from shared folder?
+	std::string path = steamvr_install + "/resources/";
+	path += pchResourceTypeDirectory;
+	path += "/";
+	path += pchResourceName;
+	return return_str(path);
+}
+// NOLINTEND(bugprone-easily-swappable-parameters)
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/resources.hpp b/src/xrt/drivers/steamvr_lh/interfaces/resources.hpp
new file mode 100644
index 000000000..a359068d1
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/resources.hpp
@@ -0,0 +1,40 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRResources interface header.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+#include "openvr_driver.h"
+#include "util/u_logging.h"
+
+class Resources : public vr::IVRResources
+{
+	const u_logging_level log_level;
+	const std::string steamvr_install;
+
+public:
+	Resources(u_logging_level l, const std::string &steamvr_install)
+	    : log_level(l), steamvr_install(steamvr_install){};
+	// ------------------------------------
+	// Shared Resource Methods
+	// ------------------------------------
+
+	/** Loads the specified resource into the provided buffer if large enough.
+	 * Returns the size in bytes of the buffer required to hold the specified resource. */
+	uint32_t
+	LoadSharedResource(const char *pchResourceName, char *pchBuffer, uint32_t unBufferLen) override;
+
+	/** Provides the full path to the specified resource. Resource names can include named directories for
+	 * drivers and other things, and this resolves all of those and returns the actual physical path.
+	 * pchResourceTypeDirectory is the subdirectory of resources to look in. */
+	uint32_t
+	GetResourceFullPath(const char *pchResourceName,
+	                    const char *pchResourceTypeDirectory,
+	                    VR_OUT_STRING() char *pchPathBuffer,
+	                    uint32_t unBufferLen) override;
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/server.hpp b/src/xrt/drivers/steamvr_lh/interfaces/server.hpp
new file mode 100644
index 000000000..7e1bbc28a
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/server.hpp
@@ -0,0 +1,118 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRServer internal interface header.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+/** An internal interface utilized by the lighthouse driver. Who knows what it does... */
+
+class Server
+{
+	virtual void
+	UnknownFunc001()
+	{}
+	virtual void
+	UnknownFunc002()
+	{}
+	virtual void
+	UnknownFunc003()
+	{}
+	virtual void
+	UnknownFunc004()
+	{}
+	virtual void
+	UnknownFunc005()
+	{}
+	virtual void
+	UnknownFunc006()
+	{}
+	virtual void
+	UnknownFunc007()
+	{}
+	virtual void
+	UnknownFunc008()
+	{}
+	virtual void
+	UnknownFunc009()
+	{}
+	virtual void
+	UnknownFunc010()
+	{}
+	virtual void
+	UnknownFunc011()
+	{}
+	virtual void
+	UnknownFunc012()
+	{}
+	virtual void
+	UnknownFunc013()
+	{}
+	virtual void
+	UnknownFunc014()
+	{}
+	virtual void
+	UnknownFunc015()
+	{}
+	virtual void
+	UnknownFunc016()
+	{}
+	virtual void
+	UnknownFunc017()
+	{}
+	virtual void
+	UnknownFunc018()
+	{}
+	virtual void
+	UnknownFunc019()
+	{}
+	virtual void
+	UnknownFunc020()
+	{}
+	virtual void
+	UnknownFunc021()
+	{}
+	virtual void
+	UnknownFunc022()
+	{}
+	virtual void
+	UnknownFunc023()
+	{}
+	virtual void
+	UnknownFunc024()
+	{}
+	virtual void
+	UnknownFunc025()
+	{}
+	virtual void
+	UnknownFunc026()
+	{}
+	virtual void
+	UnknownFunc027()
+	{}
+	virtual void
+	UnknownFunc028()
+	{}
+	virtual void
+	UnknownFunc029()
+	{}
+	virtual void
+	UnknownFunc030()
+	{}
+	virtual void
+	UnknownFunc031()
+	{}
+	virtual void
+	UnknownFunc032()
+	{}
+	virtual void
+	UnknownFunc033()
+	{}
+	virtual void
+	UnknownFunc034()
+	{}
+};
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/settings.cpp b/src/xrt/drivers/steamvr_lh/interfaces/settings.cpp
new file mode 100644
index 000000000..69918cc05
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/settings.cpp
@@ -0,0 +1,109 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRSettings interface implementation.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include <optional>
+#include <cstring>
+
+#include "settings.hpp"
+#include "util/u_json.hpp"
+
+using xrt::auxiliary::util::json::JSONNode;
+
+Settings::Settings(const std::string &steam_install, const std::string &steamvr_install)
+    : steamvr_settings(JSONNode::loadFromFile(steam_install + "/config/steamvr.vrsettings")),
+      driver_defaults(
+          JSONNode::loadFromFile(steamvr_install + "/drivers/lighthouse/resources/settings/default.vrsettings"))
+{}
+
+// NOLINTBEGIN(bugprone-easily-swappable-parameters)
+const char *
+Settings::GetSettingsErrorNameFromEnum(vr::EVRSettingsError eError)
+{
+	return nullptr;
+}
+
+void
+Settings::SetBool(const char *pchSection, const char *pchSettingsKey, bool bValue, vr::EVRSettingsError *peError)
+{}
+
+void
+Settings::SetInt32(const char *pchSection, const char *pchSettingsKey, int32_t nValue, vr::EVRSettingsError *peError)
+{}
+
+void
+Settings::SetFloat(const char *pchSection, const char *pchSettingsKey, float flValue, vr::EVRSettingsError *peError)
+{}
+
+void
+Settings::SetString(const char *pchSection,
+                    const char *pchSettingsKey,
+                    const char *pchValue,
+                    vr::EVRSettingsError *peError)
+{}
+
+bool
+Settings::GetBool(const char *pchSection, const char *pchSettingsKey, vr::EVRSettingsError *peError)
+{
+	return false;
+}
+
+int32_t
+Settings::GetInt32(const char *pchSection, const char *pchSettingsKey, vr::EVRSettingsError *peError)
+{
+	return 0;
+}
+
+float
+Settings::GetFloat(const char *pchSection, const char *pchSettingsKey, vr::EVRSettingsError *peError)
+{
+	return 0.0;
+}
+
+// Driver requires a few string settings to initialize properly
+void
+Settings::GetString(const char *pchSection,
+                    const char *pchSettingsKey,
+                    char *pchValue,
+                    uint32_t unValueLen,
+                    vr::EVRSettingsError *peError)
+{
+	if (peError)
+		*peError = vr::VRSettingsError_None;
+
+	auto get_string = [pchSection, pchSettingsKey](const JSONNode &root) -> std::optional<std::string> {
+		JSONNode section = root[pchSection];
+		if (!section.isValid())
+			return std::nullopt;
+
+		JSONNode value = section[pchSettingsKey];
+		if (!value.isValid() || !value.isString())
+			return std::nullopt;
+
+		return std::optional(value.asString());
+	};
+
+	std::optional value = get_string(driver_defaults);
+	if (!value.has_value())
+		value = get_string(steamvr_settings);
+
+	if (value.has_value()) {
+		if (unValueLen > value->size())
+			std::strncpy(pchValue, value->c_str(), value->size() + 1);
+	} else if (peError)
+		*peError = vr::VRSettingsError_ReadFailed;
+}
+
+void
+Settings::RemoveSection(const char *pchSection, vr::EVRSettingsError *peError)
+{}
+
+void
+Settings::RemoveKeyInSection(const char *pchSection, const char *pchSettingsKey, vr::EVRSettingsError *peError)
+{}
+// NOLINTEND(bugprone-easily-swappable-parameters)
diff --git a/src/xrt/drivers/steamvr_lh/interfaces/settings.hpp b/src/xrt/drivers/steamvr_lh/interfaces/settings.hpp
new file mode 100644
index 000000000..76cf331fa
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/interfaces/settings.hpp
@@ -0,0 +1,70 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief OpenVR IVRSettings interface header.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+#include "openvr_driver.h"
+#include "util/u_json.hpp"
+
+class Settings : public vr::IVRSettings
+{
+private:
+	const xrt::auxiliary::util::json::JSONNode steamvr_settings;
+	const xrt::auxiliary::util::json::JSONNode driver_defaults;
+
+public:
+	Settings(const std::string &steam_install, const std::string &steamvr_install);
+
+	const char *
+	GetSettingsErrorNameFromEnum(vr::EVRSettingsError eError) override;
+
+	void
+	SetBool(const char *pchSection,
+	        const char *pchSettingsKey,
+	        bool bValue,
+	        vr::EVRSettingsError *peError = nullptr) override;
+	void
+	SetInt32(const char *pchSection,
+	         const char *pchSettingsKey,
+	         int32_t nValue,
+	         vr::EVRSettingsError *peError = nullptr) override;
+	void
+	SetFloat(const char *pchSection,
+	         const char *pchSettingsKey,
+	         float flValue,
+	         vr::EVRSettingsError *peError = nullptr) override;
+	void
+	SetString(const char *pchSection,
+	          const char *pchSettingsKey,
+	          const char *pchValue,
+	          vr::EVRSettingsError *peError = nullptr) override;
+
+	// Users of the system need to provide a proper default in default.vrsettings in the resources/settings/
+	// directory of either the runtime or the driver_xxx directory. Otherwise the default will be false, 0, 0.0 or
+	// ""
+	bool
+	GetBool(const char *pchSection, const char *pchSettingsKey, vr::EVRSettingsError *peError = nullptr) override;
+	int32_t
+	GetInt32(const char *pchSection, const char *pchSettingsKey, vr::EVRSettingsError *peError = nullptr) override;
+	float
+	GetFloat(const char *pchSection, const char *pchSettingsKey, vr::EVRSettingsError *peError = nullptr) override;
+	void
+	GetString(const char *pchSection,
+	          const char *pchSettingsKey,
+	          VR_OUT_STRING() char *pchValue,
+	          uint32_t unValueLen,
+	          vr::EVRSettingsError *peError = nullptr) override;
+
+	void
+	RemoveSection(const char *pchSection, vr::EVRSettingsError *peError = nullptr) override;
+	void
+	RemoveKeyInSection(const char *pchSection,
+	                   const char *pchSettingsKey,
+	                   vr::EVRSettingsError *peError = nullptr) override;
+};
diff --git a/src/xrt/drivers/steamvr_lh/steamvr_lh.cpp b/src/xrt/drivers/steamvr_lh/steamvr_lh.cpp
new file mode 100644
index 000000000..175aef989
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/steamvr_lh.cpp
@@ -0,0 +1,644 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief SteamVR driver context implementation and entrypoint.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#include <cstring>
+#include <dlfcn.h>
+#include <memory>
+#include <cmath>
+#include <unordered_map>
+#include <string_view>
+#include <filesystem>
+#include <istream>
+
+#include "openvr_driver.h"
+#include "vdf_parser.hpp"
+#include "steamvr_lh_interface.h"
+#include "interfaces/context.hpp"
+#include "device.hpp"
+#include "util/u_device.h"
+
+namespace {
+DEBUG_GET_ONCE_LOG_OPTION(lh_log, "LIGHTHOUSE_LOG", U_LOGGING_INFO);
+
+// ~/.steam/root is a symlink to where the Steam root is
+const std::string STEAM_INSTALL_DIR = std::string(getenv("HOME")) + "/.steam/root";
+constexpr auto STEAMVR_APPID = "250820";
+
+// Parse libraryfolder.vdf to find where SteamVR is installed
+std::string
+find_steamvr_install()
+{
+	using namespace tyti;
+	std::ifstream file(STEAM_INSTALL_DIR + "/steamapps/libraryfolders.vdf");
+	auto root = vdf::read(file);
+	assert(root.name == "libraryfolders");
+	for (auto &[_, child] : root.children) {
+		U_LOG_D("Found library folder %s", child->attribs["path"].c_str());
+		std::shared_ptr<vdf::object> apps = child->children["apps"];
+		for (auto &[appid, _] : apps->attribs) {
+			if (appid == STEAMVR_APPID) {
+				return child->attribs["path"] + "/steamapps/common/SteamVR";
+			}
+		}
+	}
+	return std::string();
+}
+
+} // namespace
+
+#define CTX_ERR(...) U_LOG_IFL_E(log_level, __VA_ARGS__)
+#define CTX_WARN(...) U_LOG_IFL_E(log_level, __VA_ARGS__)
+#define CTX_INFO(...) U_LOG_IFL_I(log_level, __VA_ARGS__)
+#define CTX_TRACE(...) U_LOG_IFL_T(log_level, __VA_ARGS__)
+#define CTX_DEBUG(...) U_LOG_IFL_D(log_level, __VA_ARGS__)
+
+/**
+ * Since only the devices will live after our get_devices function is called, we make our Context
+ * a shared ptr that is owned by the devices that exist, so that it is also cleaned up by the
+ * devices that exist when they are all destroyed.
+ */
+std::shared_ptr<Context>
+Context::create(const std::string &steam_install,
+                const std::string &steamvr_install,
+                vr::IServerTrackedDeviceProvider *p)
+{
+	// xrt_tracking_origin initialization
+	Context *c = new Context(steam_install, steamvr_install, debug_get_log_option_lh_log());
+	c->provider = p;
+	std::strncpy(c->name, "SteamVR Lighthouse Tracking", XRT_TRACKING_NAME_LEN);
+	c->type = XRT_TRACKING_TYPE_LIGHTHOUSE;
+	c->offset = XRT_POSE_IDENTITY;
+	return std::shared_ptr<Context>(c);
+}
+
+Context::Context(const std::string &steam_install, const std::string &steamvr_install, u_logging_level level)
+    : settings(steam_install, steamvr_install), resources(level, steamvr_install), log_level(level)
+{}
+
+Context::~Context()
+{
+	provider->Cleanup();
+}
+
+/***** IVRDriverContext methods *****/
+
+void *
+Context::GetGenericInterface(const char *pchInterfaceVersion, vr::EVRInitError *peError)
+{
+#define MATCH_INTERFACE(version, interface)                                                                            \
+	if (std::strcmp(pchInterfaceVersion, version) == 0) {                                                          \
+		return interface;                                                                                      \
+	}
+#define MATCH_INTERFACE_THIS(interface) MATCH_INTERFACE(interface##_Version, static_cast<interface *>(this))
+
+	// Known interfaces
+	MATCH_INTERFACE_THIS(vr::IVRServerDriverHost);
+	MATCH_INTERFACE_THIS(vr::IVRDriverInput);
+	MATCH_INTERFACE_THIS(vr::IVRProperties);
+	MATCH_INTERFACE_THIS(vr::IVRDriverLog);
+	MATCH_INTERFACE(vr::IVRSettings_Version, &settings);
+	MATCH_INTERFACE(vr::IVRResources_Version, &resources);
+	MATCH_INTERFACE(vr::IVRIOBuffer_Version, &iobuf);
+	MATCH_INTERFACE(vr::IVRDriverManager_Version, &man);
+	MATCH_INTERFACE(vr::IVRBlockQueue_Version, &blockqueue);
+	MATCH_INTERFACE(vr::IVRPaths_Version, &paths);
+
+	// Internal interfaces
+	MATCH_INTERFACE("IVRServer_XXX", &server);
+	return nullptr;
+}
+
+vr::DriverHandle_t
+Context::GetDriverHandle()
+{
+	return 1;
+}
+
+
+/***** IVRServerDriverHost methods *****/
+
+bool
+Context::setup_hmd(const char *serial, vr::ITrackedDeviceServerDriver *driver)
+{
+	this->hmd = new HmdDevice(DeviceBuilder{this->shared_from_this(), driver, serial, STEAM_INSTALL_DIR});
+#define VERIFY(expr, msg)                                                                                              \
+	if (!(expr)) {                                                                                                 \
+		CTX_ERR("Activating HMD failed: %s", msg);                                                             \
+		delete this->hmd;                                                                                      \
+		this->hmd = nullptr;                                                                                   \
+		return false;                                                                                          \
+	}
+	vr::EVRInitError err = driver->Activate(0);
+	VERIFY(err == vr::VRInitError_None, std::to_string(err).c_str());
+
+	auto *display = static_cast<vr::IVRDisplayComponent *>(driver->GetComponent(vr::IVRDisplayComponent_Version));
+	VERIFY(display, "IVRDisplayComponent is null");
+#undef VERIFY
+
+	auto hmd_parts = std::make_unique<HmdDevice::Parts>();
+	for (size_t idx = 0; idx < 2; ++idx) {
+		vr::EVREye eye = (idx == 0) ? vr::Eye_Left : vr::Eye_Right;
+		xrt_view &view = hmd_parts->base.views[idx];
+
+		display->GetEyeOutputViewport(eye, &view.viewport.x_pixels, &view.viewport.y_pixels,
+		                              &view.viewport.w_pixels, &view.viewport.h_pixels);
+
+		view.display.w_pixels = view.viewport.w_pixels;
+		view.display.h_pixels = view.viewport.h_pixels;
+		view.rot = u_device_rotation_ident;
+	}
+
+	hmd_parts->base.screens[0].w_pixels =
+	    hmd_parts->base.views[0].display.w_pixels + hmd_parts->base.views[1].display.w_pixels;
+	hmd_parts->base.screens[0].h_pixels = hmd_parts->base.views[0].display.h_pixels;
+	// nominal frame interval will be set when lighthouse gives us the display frequency
+	// see HmdDevice::handle_property_write
+
+	hmd_parts->base.blend_modes[0] = XRT_BLEND_MODE_OPAQUE;
+	hmd_parts->base.blend_mode_count = 1;
+
+	auto &distortion = hmd_parts->base.distortion;
+	distortion.models = XRT_DISTORTION_MODEL_COMPUTE;
+	distortion.preferred = XRT_DISTORTION_MODEL_COMPUTE;
+	for (size_t idx = 0; idx < 2; ++idx) {
+		xrt_fov &fov = distortion.fov[idx];
+		float tan_left, tan_right, tan_top, tan_bottom;
+		display->GetProjectionRaw((vr::EVREye)idx, &tan_left, &tan_right, &tan_top, &tan_bottom);
+		fov.angle_left = atanf(tan_left);
+		fov.angle_right = atanf(tan_right);
+		fov.angle_up = atanf(tan_bottom);
+		fov.angle_down = atanf(tan_top);
+	}
+
+	hmd_parts->display = display;
+	hmd->set_hmd_parts(std::move(hmd_parts));
+	return true;
+}
+
+bool
+Context::setup_controller(const char *serial, vr::ITrackedDeviceServerDriver *driver)
+{
+	if (controller[0] && controller[1]) {
+		CTX_WARN("Attempted to activate more than two controllers - this is unsupported");
+		return false;
+	}
+	size_t device_idx = (controller[0]) ? 2 : 1;
+	auto &dev = controller[device_idx - 1];
+	dev = new ControllerDevice(device_idx + 1,
+	                           DeviceBuilder{this->shared_from_this(), driver, serial, STEAM_INSTALL_DIR});
+
+	vr::EVRInitError err = driver->Activate(device_idx);
+	if (err != vr::VRInitError_None) {
+		CTX_ERR("Activating controller failed: error %u", err);
+		return false;
+	}
+
+	return true;
+}
+
+void
+Context::maybe_run_frame(uint64_t new_frame)
+{
+	if (new_frame > current_frame) {
+		++current_frame;
+		provider->RunFrame();
+	}
+}
+// NOLINTBEGIN(bugprone-easily-swappable-parameters)
+bool
+Context::TrackedDeviceAdded(const char *pchDeviceSerialNumber,
+                            vr::ETrackedDeviceClass eDeviceClass,
+                            vr::ITrackedDeviceServerDriver *pDriver)
+{
+	CTX_INFO("New device added: %s", pchDeviceSerialNumber);
+	switch (eDeviceClass) {
+	case vr::TrackedDeviceClass_HMD: {
+		return setup_hmd(pchDeviceSerialNumber, pDriver);
+		break;
+	}
+	case vr::TrackedDeviceClass_Controller: {
+		return setup_controller(pchDeviceSerialNumber, pDriver);
+		break;
+	}
+	default: {
+		CTX_WARN("Attempted to add unsupported device class: %u", eDeviceClass);
+		return false;
+	}
+	}
+}
+
+void
+Context::TrackedDevicePoseUpdated(uint32_t unWhichDevice, const vr::DriverPose_t &newPose, uint32_t unPoseStructSize)
+{
+	assert(sizeof(newPose) == unPoseStructSize);
+	if (unWhichDevice > 2)
+		return;
+	Device *dev = (unWhichDevice == 0) ? static_cast<Device *>(this->hmd)
+	                                   : static_cast<Device *>(this->controller[unWhichDevice - 1]);
+	assert(dev);
+	dev->update_pose(newPose);
+}
+
+void
+Context::VsyncEvent(double vsyncTimeOffsetSeconds)
+{}
+
+void
+Context::VendorSpecificEvent(uint32_t unWhichDevice,
+                             vr::EVREventType eventType,
+                             const vr::VREvent_Data_t &eventData,
+                             double eventTimeOffset)
+{}
+
+bool
+Context::IsExiting()
+{
+	return false;
+}
+
+void
+Context::add_haptic_event(vr::VREvent_HapticVibration_t event)
+{
+	vr::VREvent_t e;
+	e.eventType = vr::EVREventType::VREvent_Input_HapticVibration;
+	e.trackedDeviceIndex = event.containerHandle - 1;
+	vr::VREvent_Data_t d;
+	d.hapticVibration = event;
+	e.data = d;
+
+	std::lock_guard lk(event_queue_mut);
+	events.push_back({std::chrono::steady_clock::now(), e});
+}
+
+bool
+Context::PollNextEvent(vr::VREvent_t *pEvent, uint32_t uncbVREvent)
+{
+	if (!events.empty()) {
+		assert(sizeof(vr::VREvent_t) == uncbVREvent);
+		Event e;
+		{
+			std::lock_guard lk(event_queue_mut);
+			e = events.front();
+			events.pop_front();
+		}
+		*pEvent = e.inner;
+		using float_sec = std::chrono::duration<float>;
+		float_sec event_age = std::chrono::steady_clock::now() - e.insert_time;
+		pEvent->eventAgeSeconds = event_age.count();
+		return true;
+	}
+	return false;
+}
+
+void
+Context::GetRawTrackedDevicePoses(float fPredictedSecondsFromNow,
+                                  vr::TrackedDevicePose_t *pTrackedDevicePoseArray,
+                                  uint32_t unTrackedDevicePoseArrayCount)
+{}
+
+void
+Context::RequestRestart(const char *pchLocalizedReason,
+                        const char *pchExecutableToStart,
+                        const char *pchArguments,
+                        const char *pchWorkingDirectory)
+{}
+
+uint32_t
+Context::GetFrameTimings(vr::Compositor_FrameTiming *pTiming, uint32_t nFrames)
+{
+	return 0;
+}
+
+void
+Context::SetDisplayEyeToHead(uint32_t unWhichDevice,
+                             const vr::HmdMatrix34_t &eyeToHeadLeft,
+                             const vr::HmdMatrix34_t &eyeToHeadRight)
+{}
+
+void
+Context::SetDisplayProjectionRaw(uint32_t unWhichDevice, const vr::HmdRect2_t &eyeLeft, const vr::HmdRect2_t &eyeRight)
+{}
+
+void
+Context::SetRecommendedRenderTargetSize(uint32_t unWhichDevice, uint32_t nWidth, uint32_t nHeight)
+{}
+
+/***** IVRDriverInput methods *****/
+
+
+vr::EVRInputError
+Context::create_component_common(vr::PropertyContainerHandle_t container,
+                                 const char *name,
+                                 vr::VRInputComponentHandle_t *pHandle)
+{
+	*pHandle = vr::k_ulInvalidInputComponentHandle;
+	Device *device = prop_container_to_device(container);
+	if (!device) {
+		return vr::VRInputError_InvalidHandle;
+	}
+	if (xrt_input *input = device->get_input_from_name(name); input) {
+		CTX_DEBUG("creating component %s", name);
+		vr::VRInputComponentHandle_t handle = handle_to_input.size() + 1;
+		handle_to_input[handle] = input;
+		*pHandle = handle;
+	}
+	return vr::VRInputError_None;
+}
+
+xrt_input *
+Context::update_component_common(vr::VRInputComponentHandle_t handle,
+                                 double offset,
+                                 std::chrono::steady_clock::time_point now)
+{
+	xrt_input *input{nullptr};
+	if (handle != vr::k_ulInvalidInputComponentHandle) {
+		input = handle_to_input[handle];
+		std::chrono::duration<double, std::chrono::seconds::period> offset_dur(offset);
+		std::chrono::duration offset = (now + offset_dur).time_since_epoch();
+		int64_t timestamp = std::chrono::duration_cast<std::chrono::nanoseconds>(offset).count();
+		input->active = true;
+		input->timestamp = timestamp;
+	}
+	return input;
+}
+
+vr::EVRInputError
+Context::CreateBooleanComponent(vr::PropertyContainerHandle_t ulContainer,
+                                const char *pchName,
+                                vr::VRInputComponentHandle_t *pHandle)
+{
+	return create_component_common(ulContainer, pchName, pHandle);
+}
+
+vr::EVRInputError
+Context::UpdateBooleanComponent(vr::VRInputComponentHandle_t ulComponent, bool bNewValue, double fTimeOffset)
+{
+	xrt_input *input = update_component_common(ulComponent, fTimeOffset);
+	if (input) {
+		input->value.boolean = bNewValue;
+	}
+	return vr::VRInputError_None;
+}
+
+vr::EVRInputError
+Context::CreateScalarComponent(vr::PropertyContainerHandle_t ulContainer,
+                               const char *pchName,
+                               vr::VRInputComponentHandle_t *pHandle,
+                               vr::EVRScalarType eType,
+                               vr::EVRScalarUnits eUnits)
+{
+	std::string_view name{pchName};
+	// Lighthouse gives thumbsticks/trackpads as x/y components,
+	// we need to combine them for Monado
+	auto end = name.back();
+	if (end == 'x' || end == 'y') {
+		Device *device = prop_container_to_device(ulContainer);
+		if (!device) {
+			return vr::VRInputError_InvalidHandle;
+		}
+		bool x = end == 'x';
+		name.remove_suffix(2);
+		std::string n(name);
+		xrt_input *input = device->get_input_from_name(n);
+		if (!input) {
+			return vr::VRInputError_None;
+		}
+
+		// Create the component mapping if it hasn't been created yet
+		Vec2Components *components =
+		    vec2_input_to_components.try_emplace(input, new Vec2Components).first->second.get();
+
+		vr::VRInputComponentHandle_t new_handle = handle_to_input.size() + 1;
+		if (x)
+			components->x = new_handle;
+		else
+			components->y = new_handle;
+
+		handle_to_input[new_handle] = input;
+		*pHandle = new_handle;
+		return vr::VRInputError_None;
+	}
+	return create_component_common(ulContainer, pchName, pHandle);
+}
+
+vr::EVRInputError
+Context::UpdateScalarComponent(vr::VRInputComponentHandle_t ulComponent, float fNewValue, double fTimeOffset)
+{
+	xrt_input *input = update_component_common(ulComponent, fTimeOffset);
+	if (input) {
+		if (XRT_GET_INPUT_TYPE(input->name) == XRT_INPUT_TYPE_VEC2_MINUS_ONE_TO_ONE) {
+			std::unique_ptr<Vec2Components> &components = vec2_input_to_components.at(input);
+			if (components->x == ulComponent) {
+				input->value.vec2.x = fNewValue;
+			} else if (components->y == ulComponent) {
+				input->value.vec2.y = fNewValue;
+			} else {
+				CTX_WARN(
+				    "Attempted to update component with handle %lu"
+				    " but it was neither the x nor y "
+				    "component of its associated input",
+				    ulComponent);
+			}
+
+		} else {
+			input->value.vec1.x = fNewValue;
+		}
+	}
+	return vr::VRInputError_None;
+}
+
+vr::EVRInputError
+Context::CreateHapticComponent(vr::PropertyContainerHandle_t ulContainer,
+                               const char *pchName,
+                               vr::VRInputComponentHandle_t *pHandle)
+{
+	*pHandle = vr::k_ulInvalidInputComponentHandle;
+	Device *d = prop_container_to_device(ulContainer);
+	if (!d) {
+		return vr::VRInputError_InvalidHandle;
+	}
+
+	// Assuming HMDs won't have haptics.
+	// Maybe a wrong assumption.
+	if (d == hmd) {
+		CTX_WARN("Didn't expect HMD with haptics.");
+		return vr::VRInputError_InvalidHandle;
+	}
+
+	auto *device = static_cast<ControllerDevice *>(d);
+	vr::VRInputComponentHandle_t handle = handle_to_input.size() + 1;
+	handle_to_input[handle] = nullptr;
+	device->set_haptic_handle(handle);
+	*pHandle = handle;
+
+	return vr::VRInputError_None;
+}
+
+vr::EVRInputError
+Context::CreateSkeletonComponent(vr::PropertyContainerHandle_t ulContainer,
+                                 const char *pchName,
+                                 const char *pchSkeletonPath,
+                                 const char *pchBasePosePath,
+                                 vr::EVRSkeletalTrackingLevel eSkeletalTrackingLevel,
+                                 const vr::VRBoneTransform_t *pGripLimitTransforms,
+                                 uint32_t unGripLimitTransformCount,
+                                 vr::VRInputComponentHandle_t *pHandle)
+{
+	return vr::VRInputError_None;
+}
+
+vr::EVRInputError
+Context::UpdateSkeletonComponent(vr::VRInputComponentHandle_t ulComponent,
+                                 vr::EVRSkeletalMotionRange eMotionRange,
+                                 const vr::VRBoneTransform_t *pTransforms,
+                                 uint32_t unTransformCount)
+{
+	return vr::VRInputError_None;
+}
+
+/***** IVRProperties methods *****/
+
+vr::ETrackedPropertyError
+Context::ReadPropertyBatch(vr::PropertyContainerHandle_t ulContainerHandle,
+                           vr::PropertyRead_t *pBatch,
+                           uint32_t unBatchEntryCount)
+{
+	return vr::TrackedProp_Success;
+}
+
+vr::ETrackedPropertyError
+Context::WritePropertyBatch(vr::PropertyContainerHandle_t ulContainerHandle,
+                            vr::PropertyWrite_t *pBatch,
+                            uint32_t unBatchEntryCount)
+{
+	Device *device = prop_container_to_device(ulContainerHandle);
+	if (!device)
+		return vr::TrackedProp_InvalidContainer;
+	if (!pBatch)
+		return vr::TrackedProp_InvalidOperation; // not verified vs steamvr
+	device->handle_properties(pBatch, unBatchEntryCount);
+	return vr::TrackedProp_Success;
+}
+
+const char *
+Context::GetPropErrorNameFromEnum(vr::ETrackedPropertyError error)
+{
+	return nullptr;
+}
+
+Device *
+Context::prop_container_to_device(vr::PropertyContainerHandle_t handle)
+{
+	switch (handle) {
+	case 1: {
+		return hmd;
+		break;
+	}
+	case 2:
+	case 3: {
+		return controller[handle - 2];
+		break;
+	}
+	default: {
+		return nullptr;
+	}
+	}
+}
+
+vr::PropertyContainerHandle_t
+Context::TrackedDeviceToPropertyContainer(vr::TrackedDeviceIndex_t nDevice)
+{
+	size_t container = nDevice + 1;
+	if (nDevice == 0 && this->hmd)
+		return container;
+	if ((nDevice == 1 || nDevice == 2) && this->controller[nDevice - 1]) {
+		return container;
+	}
+
+	return vr::k_ulInvalidPropertyContainer;
+}
+
+void
+Context::Log(const char *pchLogMessage)
+{
+	CTX_TRACE("[lighthouse]: %s", pchLogMessage);
+}
+// NOLINTEND(bugprone-easily-swappable-parameters)
+
+
+extern "C" int
+steamvr_lh_get_devices(struct xrt_device **out_xdevs)
+{
+	u_logging_level level = debug_get_log_option_lh_log();
+	// The driver likes to create a bunch of transient folder - lets make sure they're created where they normally
+	// are.
+	std::filesystem::current_path(STEAM_INSTALL_DIR + "/config/lighthouse");
+	std::string steamvr = find_steamvr_install();
+	if (steamvr.empty()) {
+		U_LOG_IFL_E(level, "Could not find where SteamVR is installed!");
+		return 0;
+	}
+
+	U_LOG_IFL_I(level, "Found SteamVR install: %s", steamvr.c_str());
+
+	// TODO: support windows?
+	auto driver_so = steamvr + "/drivers/lighthouse/bin/linux64/driver_lighthouse.so";
+
+	void *lighthouse_lib = dlopen(driver_so.c_str(), RTLD_LAZY);
+	if (!lighthouse_lib) {
+		U_LOG_IFL_E(level, "Couldn't open lighthouse lib: %s", dlerror());
+		return 0;
+	}
+
+	void *sym = dlsym(lighthouse_lib, "HmdDriverFactory");
+	if (!sym) {
+		U_LOG_IFL_E(level, "Couldn't find HmdDriverFactory in lighthouse lib: %s", dlerror());
+		return 0;
+	}
+	using HmdDriverFactory_t = void *(*)(const char *, int *);
+	auto factory = reinterpret_cast<HmdDriverFactory_t>(sym);
+
+	vr::EVRInitError err = vr::VRInitError_None;
+	auto *driver = static_cast<vr::IServerTrackedDeviceProvider *>(
+	    factory(vr::IServerTrackedDeviceProvider_Version, (int *)&err));
+	if (err != vr::VRInitError_None) {
+		U_LOG_IFL_E(level, "Couldn't get tracked device driver: error %u", err);
+		return 0;
+	}
+
+	std::shared_ptr ctx = Context::create(STEAM_INSTALL_DIR, steamvr, driver);
+
+	err = driver->Init(ctx.get());
+	if (err != vr::VRInitError_None) {
+		U_LOG_IFL_E(level, "Lighthouse driver initialization failed: error %u", err);
+		return 0;
+	}
+
+	U_LOG_IFL_I(level, "Lighthouse initialization complete, giving time to setup connected devices...");
+	// RunFrame needs to be called to detect controllers
+	using namespace std::chrono_literals;
+	auto start_time = std::chrono::steady_clock::now();
+	while (true) {
+		driver->RunFrame();
+		auto cur_time = std::chrono::steady_clock::now();
+		if (cur_time - start_time >= 1s) {
+			break;
+		}
+	}
+	U_LOG_IFL_I(level, "Device search time complete.");
+
+	int devices = 0;
+	Device *devs[] = {ctx->hmd, ctx->controller[0], ctx->controller[1]};
+	for (Device *dev : devs) {
+		if (dev) {
+			out_xdevs[devices++] = dev;
+		}
+	}
+	return devices;
+}
diff --git a/src/xrt/drivers/steamvr_lh/steamvr_lh_interface.h b/src/xrt/drivers/steamvr_lh/steamvr_lh_interface.h
new file mode 100644
index 000000000..4ef85b207
--- /dev/null
+++ b/src/xrt/drivers/steamvr_lh/steamvr_lh_interface.h
@@ -0,0 +1,44 @@
+// Copyright 2023, Shawn Wallace
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief SteamVR driver device interface.
+ * @author Shawn Wallace <yungwallace@live.com>
+ * @ingroup drv_steamvr_lh
+ */
+
+#pragma once
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+struct xrt_device;
+
+/*!
+ * @defgroup drv_steamvr_lh Wrapper for the SteamVR Lighthouse driver.
+ * @ingroup drv
+ *
+ * @brief Wrapper driver around the SteamVR Lighthouse driver.
+ */
+
+/*!
+ * @dir drivers/steamvr_lh
+ *
+ * @brief @ref drv_steamvr_lh files.
+ */
+
+/*!
+ * Create devices.
+ *
+ * @ingroup drv_steamvr_lh
+ */
+int
+steamvr_lh_get_devices(struct xrt_device **out_xdevs);
+
+
+#ifdef __cplusplus
+}
+#endif