The open source OpenXR runtime
1// Copyright 2023, Shawn Wallace
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief SteamVR driver context implementation and entrypoint.
6 * @author Shawn Wallace <yungwallace@live.com>
7 * @ingroup drv_steamvr_lh
8 */
9
10#include <cstring>
11#include <dlfcn.h>
12#include <memory>
13#include <cmath>
14#include <unordered_map>
15#include <string_view>
16#include <filesystem>
17#include <istream>
18#include <thread>
19
20#include "openvr_driver.h"
21#include "vdf_parser.hpp"
22#include "steamvr_lh_interface.h"
23#include "interfaces/context.hpp"
24#include "device.hpp"
25#include "util/u_device.h"
26#include "util/u_misc.h"
27#include "util/u_device.h"
28#include "util/u_system_helpers.h"
29#include "vive/vive_bindings.h"
30#include "util/u_device.h"
31
32#include "math/m_api.h"
33
34namespace {
35
36DEBUG_GET_ONCE_LOG_OPTION(lh_log, "LIGHTHOUSE_LOG", U_LOGGING_INFO)
37DEBUG_GET_ONCE_BOOL_OPTION(lh_load_slimevr, "LH_LOAD_SLIMEVR", false)
38DEBUG_GET_ONCE_NUM_OPTION(lh_discover_wait_ms, "LH_DISCOVER_WAIT_MS", 3000)
39
40static constexpr size_t MAX_CONTROLLERS = 16;
41
42
43struct steamvr_lh_system
44{
45 // System devices wrapper.
46 struct xrt_system_devices base;
47
48 //! Pointer to driver context
49 std::shared_ptr<Context> ctx;
50};
51
52struct steamvr_lh_system *svrs = U_TYPED_CALLOC(struct steamvr_lh_system);
53
54
55// ~/.steam/root is a symlink to where the Steam root is
56const std::string STEAM_INSTALL_DIR = std::string(getenv("HOME")) + "/.steam/root";
57constexpr auto STEAMVR_APPID = "250820";
58
59// Parse libraryfolder.vdf to find where SteamVR is installed
60std::string
61find_steamvr_install()
62{
63 using namespace tyti;
64 u_logging_level level = debug_get_log_option_lh_log();
65 std::ifstream file(STEAM_INSTALL_DIR + "/steamapps/libraryfolders.vdf");
66 if (!file.is_open()) {
67 U_LOG_IFL_E(level, "Failed to open libraryfolders.vdf");
68 return std::string();
69 }
70
71 vdf::basic_object<char> root;
72 try {
73 root = vdf::read(file);
74 } catch (std::exception &ex) {
75 U_LOG_IFL_E(level, "Failed to read libraryfolders.vdf: %s", ex.what());
76 return std::string();
77 }
78
79 assert(root.name == "libraryfolders");
80 for (auto &[_, child] : root.childs) {
81 U_LOG_D("Found library folder %s", child->attribs["path"].c_str());
82 std::shared_ptr<vdf::object> apps = child->childs["apps"];
83 for (auto &[appid, _] : apps->attribs) {
84 if (appid == STEAMVR_APPID) {
85 std::string path = child->attribs["path"] + "/steamapps/common/SteamVR";
86 if (std::filesystem::exists(path)) {
87 return path;
88 }
89 }
90 }
91 }
92 return std::string();
93}
94
95} // namespace
96
97#define CTX_ERR(...) U_LOG_IFL_E(log_level, __VA_ARGS__)
98#define CTX_WARN(...) U_LOG_IFL_W(log_level, __VA_ARGS__)
99#define CTX_INFO(...) U_LOG_IFL_I(log_level, __VA_ARGS__)
100#define CTX_TRACE(...) U_LOG_IFL_T(log_level, __VA_ARGS__)
101#define CTX_DEBUG(...) U_LOG_IFL_D(log_level, __VA_ARGS__)
102
103/**
104 * Since only the devices will live after our get_devices function is called, we make our Context
105 * a shared ptr that is owned by the devices that exist, so that it is also cleaned up by the
106 * devices that exist when they are all destroyed.
107 */
108std::shared_ptr<Context>
109Context::create(const std::string &steam_install,
110 const std::string &steamvr_install,
111 std::vector<vr::IServerTrackedDeviceProvider *> providers)
112{
113 // xrt_tracking_origin initialization
114 std::shared_ptr<Context> c =
115 std::make_shared<Context>(steam_install, steamvr_install, debug_get_log_option_lh_log());
116 c->providers = std::move(providers);
117 std::strncpy(c->name, "SteamVR Lighthouse Tracking", XRT_TRACKING_NAME_LEN);
118 c->type = XRT_TRACKING_TYPE_LIGHTHOUSE;
119 c->initial_offset = XRT_POSE_IDENTITY;
120 for (vr::IServerTrackedDeviceProvider *const &driver : c->providers) {
121 vr::EVRInitError err = driver->Init(c.get());
122 if (err != vr::VRInitError_None) {
123 U_LOG_IFL_E(c->log_level, "OpenVR driver initialization failed: error %u", err);
124 return nullptr;
125 }
126 }
127 return c;
128}
129
130Context::Context(const std::string &steam_install, const std::string &steamvr_install, u_logging_level level)
131 : settings(steam_install, steamvr_install, this), resources(level, steamvr_install), log_level(level)
132{}
133
134Context::~Context()
135{
136 for (vr::IServerTrackedDeviceProvider *const &provider : providers)
137 provider->Cleanup();
138}
139
140/***** IVRDriverContext methods *****/
141
142void *
143Context::GetGenericInterface(const char *pchInterfaceVersion, vr::EVRInitError *peError)
144{
145 CTX_DEBUG("Requested interface %s", pchInterfaceVersion);
146#define MATCH_INTERFACE(version, interface) \
147 if (std::strcmp(pchInterfaceVersion, version) == 0) { \
148 return interface; \
149 }
150#define MATCH_INTERFACE_THIS(interface) MATCH_INTERFACE(interface##_Version, static_cast<interface *>(this))
151
152 // Known interfaces
153 MATCH_INTERFACE_THIS(vr::IVRServerDriverHost);
154 MATCH_INTERFACE_THIS(vr::IVRDriverInput);
155 // This interface is not in a public header yet, but just passing IVRDriverInput_003 seems to work.
156 MATCH_INTERFACE("IVRDriverInput_004", static_cast<vr::IVRDriverInput *>(this));
157 MATCH_INTERFACE_THIS(vr::IVRProperties);
158 MATCH_INTERFACE_THIS(vr::IVRDriverLog);
159 MATCH_INTERFACE(vr::IVRSettings_Version, &settings);
160 MATCH_INTERFACE(vr::IVRResources_Version, &resources);
161 MATCH_INTERFACE(vr::IVRIOBuffer_Version, &iobuf);
162 MATCH_INTERFACE(vr::IVRDriverManager_Version, &man);
163 MATCH_INTERFACE(vr::IVRBlockQueue_Version, &blockqueue);
164 MATCH_INTERFACE(vr::IVRPaths_Version, &paths);
165 // This version of the interface is not in a public header.
166 // Luckily it seems to be compatible with the previous version.
167 MATCH_INTERFACE("IVRPaths_002", &paths);
168
169 // Internal interfaces
170 MATCH_INTERFACE("IVRServer_XXX", &server);
171 MATCH_INTERFACE("IVRServerInternal_XXX", &server);
172 return nullptr;
173}
174
175vr::DriverHandle_t
176Context::GetDriverHandle()
177{
178 return 1;
179}
180
181
182/***** IVRServerDriverHost methods *****/
183
184bool
185Context::setup_hmd(const char *serial, vr::ITrackedDeviceServerDriver *driver)
186{
187 this->hmd = new HmdDevice(DeviceBuilder{this->shared_from_this(), driver, serial, STEAM_INSTALL_DIR});
188#define VERIFY(expr, msg) \
189 if (!(expr)) { \
190 CTX_ERR("Activating HMD failed: %s", msg); \
191 delete this->hmd; \
192 this->hmd = nullptr; \
193 return false; \
194 }
195 vr::EVRInitError err = driver->Activate(0);
196 VERIFY(err == vr::VRInitError_None, std::to_string(err).c_str());
197
198 auto *display = static_cast<vr::IVRDisplayComponent *>(driver->GetComponent(vr::IVRDisplayComponent_Version3));
199 if (display == NULL) {
200 display = static_cast<vr::IVRDisplayComponent *>(driver->GetComponent(vr::IVRDisplayComponent_Version));
201 }
202 VERIFY(display, "IVRDisplayComponent is null");
203#undef VERIFY
204
205 auto hmd_parts = std::make_unique<HmdDevice::Parts>();
206 hmd_parts->base.view_count = 2;
207 for (size_t idx = 0; idx < 2; ++idx) {
208 vr::EVREye eye = (idx == 0) ? vr::Eye_Left : vr::Eye_Right;
209 xrt_view &view = hmd_parts->base.views[idx];
210
211 display->GetEyeOutputViewport(eye, &view.viewport.x_pixels, &view.viewport.y_pixels,
212 &view.viewport.w_pixels, &view.viewport.h_pixels);
213
214 view.display.w_pixels = view.viewport.w_pixels;
215 view.display.h_pixels = view.viewport.h_pixels;
216 view.rot = u_device_rotation_ident;
217 }
218
219 hmd_parts->base.screens[0].w_pixels =
220 hmd_parts->base.views[0].display.w_pixels + hmd_parts->base.views[1].display.w_pixels;
221 hmd_parts->base.screens[0].h_pixels = hmd_parts->base.views[0].display.h_pixels;
222 // nominal frame interval will be set when lighthouse gives us the display frequency
223 // see HmdDevice::handle_property_write
224
225 hmd_parts->base.blend_modes[0] = XRT_BLEND_MODE_OPAQUE;
226 hmd_parts->base.blend_mode_count = 1;
227
228 auto &distortion = hmd_parts->base.distortion;
229 distortion.models = XRT_DISTORTION_MODEL_COMPUTE;
230 distortion.preferred = XRT_DISTORTION_MODEL_COMPUTE;
231 for (size_t idx = 0; idx < 2; ++idx) {
232 xrt_fov &fov = distortion.fov[idx];
233 float tan_left, tan_right, tan_top, tan_bottom;
234 display->GetProjectionRaw((vr::EVREye)idx, &tan_left, &tan_right, &tan_top, &tan_bottom);
235 fov.angle_left = atanf(tan_left);
236 fov.angle_right = atanf(tan_right);
237 fov.angle_up = atanf(tan_bottom);
238 fov.angle_down = atanf(tan_top);
239 }
240
241 hmd_parts->display = display;
242 hmd->set_hmd_parts(std::move(hmd_parts));
243
244 return true;
245}
246
247bool
248Context::setup_controller(const char *serial, vr::ITrackedDeviceServerDriver *driver)
249{
250 // Find the first available slot for a new controller
251 size_t device_idx = 0;
252 for (; device_idx < MAX_CONTROLLERS; ++device_idx) {
253 if (!controller[device_idx])
254 break;
255 }
256
257 // Check if we've exceeded the maximum number of controllers
258 if (device_idx == MAX_CONTROLLERS) {
259 CTX_WARN("Attempted to activate more than %zu controllers - this is unsupported", MAX_CONTROLLERS);
260 return false;
261 }
262
263 // Create the new controller
264 controller[device_idx] = new ControllerDevice(
265 device_idx + 1, DeviceBuilder{this->shared_from_this(), driver, serial, STEAM_INSTALL_DIR});
266
267 vr::EVRInitError err = driver->Activate(device_idx + 1);
268 if (err != vr::VRInitError_None) {
269 CTX_ERR("Activating controller failed: error %u", err);
270 return false;
271 }
272
273 enum xrt_device_name name = controller[device_idx]->name;
274 switch (name) {
275 case XRT_DEVICE_VIVE_WAND:
276 controller[device_idx]->binding_profiles = vive_binding_profiles_wand;
277 controller[device_idx]->binding_profile_count = vive_binding_profiles_wand_count;
278 break;
279
280 break;
281 case XRT_DEVICE_INDEX_CONTROLLER:
282 controller[device_idx]->binding_profiles = vive_binding_profiles_index;
283 controller[device_idx]->binding_profile_count = vive_binding_profiles_index_count;
284 break;
285 default: break;
286 }
287
288 return true;
289}
290
291void
292Context::run_frame()
293{
294 for (vr::IServerTrackedDeviceProvider *const &provider : providers)
295 provider->RunFrame();
296}
297
298void
299Context::maybe_run_frame(uint64_t new_frame)
300{
301 if (new_frame > current_frame) {
302 ++current_frame;
303 run_frame();
304 }
305}
306// NOLINTBEGIN(bugprone-easily-swappable-parameters)
307bool
308Context::TrackedDeviceAdded(const char *pchDeviceSerialNumber,
309 vr::ETrackedDeviceClass eDeviceClass,
310 vr::ITrackedDeviceServerDriver *pDriver)
311{
312 CTX_INFO("New device added: %s", pchDeviceSerialNumber);
313 switch (eDeviceClass) {
314 case vr::TrackedDeviceClass_HMD: {
315 CTX_INFO("Found lighthouse HMD: %s", pchDeviceSerialNumber);
316 return setup_hmd(pchDeviceSerialNumber, pDriver);
317 }
318 case vr::TrackedDeviceClass_Controller: {
319 CTX_INFO("Found lighthouse controller: %s", pchDeviceSerialNumber);
320 return setup_controller(pchDeviceSerialNumber, pDriver);
321 }
322 case vr::TrackedDeviceClass_TrackingReference: {
323 CTX_INFO("Found lighthouse base station: %s", pchDeviceSerialNumber);
324 return false;
325 }
326 case vr::TrackedDeviceClass_GenericTracker: {
327 CTX_INFO("Found lighthouse tracker: %s", pchDeviceSerialNumber);
328 return setup_controller(pchDeviceSerialNumber, pDriver);
329 }
330 default: {
331 CTX_WARN("Attempted to add unsupported device class: %u", eDeviceClass);
332 return false;
333 }
334 }
335}
336
337void
338Context::TrackedDevicePoseUpdated(uint32_t unWhichDevice, const vr::DriverPose_t &newPose, uint32_t unPoseStructSize)
339{
340 assert(sizeof(newPose) == unPoseStructSize);
341
342 // Check for valid device index, allowing for the HMD plus up to MAX_CONTROLLERS controllers
343 if (unWhichDevice > MAX_CONTROLLERS)
344 return;
345
346 Device *dev = nullptr;
347
348 // If unWhichDevice is 0, it refers to the HMD; otherwise, it refers to one of the controllers
349 if (unWhichDevice == 0) {
350 dev = static_cast<Device *>(this->hmd);
351 } else {
352 // unWhichDevice - 1 will give the index into the controller array
353 dev = static_cast<Device *>(this->controller[unWhichDevice - 1]);
354 }
355
356 assert(dev);
357 dev->update_pose(newPose);
358}
359
360void
361Context::VsyncEvent(double vsyncTimeOffsetSeconds)
362{}
363
364void
365Context::VendorSpecificEvent(uint32_t unWhichDevice,
366 vr::EVREventType eventType,
367 const vr::VREvent_Data_t &eventData,
368 double eventTimeOffset)
369{
370 std::lock_guard lk(event_queue_mut);
371 events.push_back({std::chrono::steady_clock::now(),
372 {
373 .eventType = eventType,
374 .trackedDeviceIndex = unWhichDevice,
375 .eventAgeSeconds = {},
376 .data = eventData,
377 }});
378}
379
380bool
381Context::IsExiting()
382{
383 return false;
384}
385
386void
387Context::add_haptic_event(vr::VREvent_HapticVibration_t event)
388{
389 vr::VREvent_t e;
390 e.eventType = vr::EVREventType::VREvent_Input_HapticVibration;
391 e.trackedDeviceIndex = event.containerHandle - 1;
392 vr::VREvent_Data_t d;
393 d.hapticVibration = event;
394 e.data = d;
395
396 std::lock_guard lk(event_queue_mut);
397 events.push_back({std::chrono::steady_clock::now(), e});
398}
399
400bool
401Context::PollNextEvent(vr::VREvent_t *pEvent, uint32_t uncbVREvent)
402{
403 if (!events.empty()) {
404 assert(sizeof(vr::VREvent_t) == uncbVREvent);
405 Event e;
406 {
407 std::lock_guard lk(event_queue_mut);
408 e = events.front();
409 events.pop_front();
410 }
411 *pEvent = e.inner;
412 using float_sec = std::chrono::duration<float>;
413 float_sec event_age = std::chrono::steady_clock::now() - e.insert_time;
414 pEvent->eventAgeSeconds = event_age.count();
415 return true;
416 }
417 return false;
418}
419
420void
421Context::GetRawTrackedDevicePoses(float fPredictedSecondsFromNow,
422 vr::TrackedDevicePose_t *pTrackedDevicePoseArray,
423 uint32_t unTrackedDevicePoseArrayCount)
424{
425 // This is the bare minimum required for SlimeVR's HMD feedback to work
426 if (unTrackedDevicePoseArrayCount != 10 || this->hmd == nullptr)
427 return;
428 const uint64_t time =
429 std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch())
430 .count();
431 xrt_space_relation head = {};
432 xrt_device_get_tracked_pose(this->hmd, XRT_INPUT_GENERIC_HEAD_POSE, time, &head);
433 xrt_matrix_3x3 rot = {};
434 math_matrix_3x3_from_quat(&head.pose.orientation, &rot);
435 pTrackedDevicePoseArray[0].mDeviceToAbsoluteTracking = {{
436 {rot.v[0], rot.v[3], rot.v[6], head.pose.position.x},
437 {rot.v[1], rot.v[4], rot.v[7], head.pose.position.y},
438 {rot.v[2], rot.v[5], rot.v[8], head.pose.position.z},
439 }};
440}
441
442void
443Context::RequestRestart(const char *pchLocalizedReason,
444 const char *pchExecutableToStart,
445 const char *pchArguments,
446 const char *pchWorkingDirectory)
447{}
448
449uint32_t
450Context::GetFrameTimings(vr::Compositor_FrameTiming *pTiming, uint32_t nFrames)
451{
452 return 0;
453}
454
455void
456Context::SetDisplayEyeToHead(uint32_t unWhichDevice,
457 const vr::HmdMatrix34_t &eyeToHeadLeft,
458 const vr::HmdMatrix34_t &eyeToHeadRight)
459{
460 hmd->SetDisplayEyeToHead(unWhichDevice, eyeToHeadLeft, eyeToHeadRight);
461}
462
463void
464Context::SetDisplayProjectionRaw(uint32_t unWhichDevice, const vr::HmdRect2_t &eyeLeft, const vr::HmdRect2_t &eyeRight)
465{}
466
467void
468Context::SetRecommendedRenderTargetSize(uint32_t unWhichDevice, uint32_t nWidth, uint32_t nHeight)
469{}
470
471/***** IVRDriverInput methods *****/
472
473
474vr::EVRInputError
475Context::create_component_common(vr::PropertyContainerHandle_t container,
476 const char *name,
477 vr::VRInputComponentHandle_t *pHandle)
478{
479 *pHandle = vr::k_ulInvalidInputComponentHandle;
480 Device *device = prop_container_to_device(container);
481 if (!device) {
482 return vr::VRInputError_InvalidHandle;
483 }
484 if (xrt_input *input = device->get_input_from_name(name); input) {
485 CTX_DEBUG("creating component %s for %p", name, (void *)device);
486 vr::VRInputComponentHandle_t handle = new_handle();
487 handle_to_input[handle] = input;
488 *pHandle = handle;
489 }
490 return vr::VRInputError_None;
491}
492
493xrt_input *
494Context::update_component_common(vr::VRInputComponentHandle_t handle,
495 double offset,
496 std::chrono::steady_clock::time_point now)
497{
498 xrt_input *input{nullptr};
499 if (handle != vr::k_ulInvalidInputComponentHandle) {
500 input = handle_to_input[handle];
501 std::chrono::duration<double, std::chrono::seconds::period> offset_dur(offset);
502 std::chrono::duration offset = (now + offset_dur).time_since_epoch();
503 int64_t timestamp = std::chrono::duration_cast<std::chrono::nanoseconds>(offset).count();
504 input->active = true;
505 input->timestamp = timestamp;
506 }
507 return input;
508}
509
510vr::EVRInputError
511Context::CreateBooleanComponent(vr::PropertyContainerHandle_t ulContainer,
512 const char *pchName,
513 vr::VRInputComponentHandle_t *pHandle)
514{
515 return create_component_common(ulContainer, pchName, pHandle);
516}
517
518vr::EVRInputError
519Context::UpdateBooleanComponent(vr::VRInputComponentHandle_t ulComponent, bool bNewValue, double fTimeOffset)
520{
521 xrt_input *input = update_component_common(ulComponent, fTimeOffset);
522 if (input) {
523 input->value.boolean = bNewValue;
524 }
525 return vr::VRInputError_None;
526}
527
528vr::EVRInputError
529Context::CreateScalarComponent(vr::PropertyContainerHandle_t ulContainer,
530 const char *pchName,
531 vr::VRInputComponentHandle_t *pHandle,
532 vr::EVRScalarType eType,
533 vr::EVRScalarUnits eUnits)
534{
535 std::string_view name{pchName};
536 // Lighthouse gives thumbsticks/trackpads as x/y components,
537 // we need to combine them for Monado
538 auto end = name.back();
539 auto second_last = name.at(name.size() - 2);
540 if (second_last == '/' && (end == 'x' || end == 'y')) {
541 Device *device = prop_container_to_device(ulContainer);
542 if (!device) {
543 return vr::VRInputError_InvalidHandle;
544 }
545 bool x = end == 'x';
546 name.remove_suffix(2);
547 std::string n(name);
548 xrt_input *input = device->get_input_from_name(n);
549 if (!input) {
550 return vr::VRInputError_None;
551 }
552
553 // Create the component mapping if it hasn't been created yet
554 Vec2Components *components =
555 vec2_input_to_components.try_emplace(input, new Vec2Components).first->second.get();
556
557 vr::VRInputComponentHandle_t new_handle = this->new_handle();
558 if (x)
559 components->x = new_handle;
560 else
561 components->y = new_handle;
562
563 handle_to_input[new_handle] = input;
564 *pHandle = new_handle;
565 return vr::VRInputError_None;
566 }
567 return create_component_common(ulContainer, pchName, pHandle);
568}
569
570vr::EVRInputError
571Context::UpdateScalarComponent(vr::VRInputComponentHandle_t ulComponent, float fNewValue, double fTimeOffset)
572{
573 if (auto h = handle_to_input.find(ulComponent); h != handle_to_input.end() && h->second) {
574 xrt_input *input = update_component_common(ulComponent, fTimeOffset);
575 if (XRT_GET_INPUT_TYPE(input->name) == XRT_INPUT_TYPE_VEC2_MINUS_ONE_TO_ONE) {
576 std::unique_ptr<Vec2Components> &components = vec2_input_to_components.at(input);
577 if (components->x == ulComponent) {
578 input->value.vec2.x = fNewValue;
579 } else if (components->y == ulComponent) {
580 input->value.vec2.y = fNewValue;
581 } else {
582 CTX_WARN("Attempted to update component with handle %" PRIu64
583 " but it was neither the x nor y "
584 "component of its associated input",
585 ulComponent);
586 }
587
588 } else {
589 input->value.vec1.x = fNewValue;
590 }
591 }
592 return vr::VRInputError_None;
593}
594
595vr::EVRInputError
596Context::CreateHapticComponent(vr::PropertyContainerHandle_t ulContainer,
597 const char *pchName,
598 vr::VRInputComponentHandle_t *pHandle)
599{
600 *pHandle = vr::k_ulInvalidInputComponentHandle;
601 Device *d = prop_container_to_device(ulContainer);
602 if (!d) {
603 return vr::VRInputError_InvalidHandle;
604 }
605
606 // Assuming HMDs won't have haptics.
607 // Maybe a wrong assumption.
608 if (d == hmd) {
609 CTX_WARN("Didn't expect HMD with haptics.");
610 return vr::VRInputError_InvalidHandle;
611 }
612
613 auto *device = static_cast<ControllerDevice *>(d);
614 vr::VRInputComponentHandle_t handle = new_handle();
615 handle_to_input[handle] = nullptr;
616 device->set_haptic_handle(handle);
617 *pHandle = handle;
618
619 return vr::VRInputError_None;
620}
621
622vr::EVRInputError
623Context::CreateSkeletonComponent(vr::PropertyContainerHandle_t ulContainer,
624 const char *pchName,
625 const char *pchSkeletonPath,
626 const char *pchBasePosePath,
627 vr::EVRSkeletalTrackingLevel eSkeletalTrackingLevel,
628 const vr::VRBoneTransform_t *pGripLimitTransforms,
629 uint32_t unGripLimitTransformCount,
630 vr::VRInputComponentHandle_t *pHandle)
631{
632 std::string_view path(pchSkeletonPath); // should be /skeleton/hand/left or /skeleton/hand/right
633 std::string_view skeleton_pfx("/skeleton/hand/");
634 if (!path.starts_with(skeleton_pfx)) {
635 CTX_ERR("Got invalid skeleton path: %s", std::string(path).c_str());
636 return vr::VRInputError_InvalidSkeleton;
637 }
638
639 if (auto ret = create_component_common(ulContainer, pchSkeletonPath, pHandle); ret != vr::VRInputError_None) {
640 return ret;
641 }
642
643 auto *device = static_cast<ControllerDevice *>(prop_container_to_device(ulContainer));
644 path.remove_prefix(skeleton_pfx.size());
645 xrt_hand hand;
646 if (path == "left") {
647 hand = XRT_HAND_LEFT;
648 } else if (path == "right") {
649 hand = XRT_HAND_RIGHT;
650 } else {
651 CTX_ERR("Got invalid skeleton path suffix: %s", std::string(path).c_str());
652 return vr::VRInputError_InvalidSkeleton;
653 }
654
655 device->set_skeleton(std::span(pGripLimitTransforms, unGripLimitTransformCount), hand,
656 eSkeletalTrackingLevel == vr::VRSkeletalTracking_Estimated, pchSkeletonPath);
657 skeleton_to_controller[*pHandle] = device;
658
659 return vr::VRInputError_None;
660}
661
662vr::EVRInputError
663Context::UpdateSkeletonComponent(vr::VRInputComponentHandle_t ulComponent,
664 vr::EVRSkeletalMotionRange eMotionRange,
665 const vr::VRBoneTransform_t *pTransforms,
666 uint32_t unTransformCount)
667{
668 if (eMotionRange != vr::VRSkeletalMotionRange_WithoutController) {
669 return vr::VRInputError_None;
670 }
671
672 if (!update_component_common(ulComponent, 0)) {
673 return vr::VRInputError_InvalidHandle;
674 }
675
676 auto *device = skeleton_to_controller[ulComponent];
677 if (!device) {
678 CTX_ERR("Got unknown component handle %lu", ulComponent);
679 return vr::VRInputError_InvalidHandle;
680 }
681
682 device->update_skeleton_transforms(std::span(pTransforms, unTransformCount));
683
684 return vr::VRInputError_None;
685}
686
687/***** IVRProperties methods *****/
688
689vr::ETrackedPropertyError
690Context::ReadPropertyBatch(vr::PropertyContainerHandle_t ulContainerHandle,
691 vr::PropertyRead_t *pBatch,
692 uint32_t unBatchEntryCount)
693{
694 Device *device = prop_container_to_device(ulContainerHandle);
695 if (!device)
696 return vr::TrackedProp_InvalidContainer;
697 if (!pBatch)
698 return vr::TrackedProp_InvalidOperation; // not verified vs steamvr
699 return device->handle_read_properties(pBatch, unBatchEntryCount);
700}
701
702vr::ETrackedPropertyError
703Context::WritePropertyBatch(vr::PropertyContainerHandle_t ulContainerHandle,
704 vr::PropertyWrite_t *pBatch,
705 uint32_t unBatchEntryCount)
706{
707 Device *device = prop_container_to_device(ulContainerHandle);
708 if (!device)
709 return vr::TrackedProp_InvalidContainer;
710 if (!pBatch)
711 return vr::TrackedProp_InvalidOperation; // not verified vs steamvr
712 return device->handle_properties(pBatch, unBatchEntryCount);
713}
714
715const char *
716Context::GetPropErrorNameFromEnum(vr::ETrackedPropertyError error)
717{
718 return nullptr;
719}
720
721Device *
722Context::prop_container_to_device(vr::PropertyContainerHandle_t handle)
723{
724 switch (handle) {
725 case 1: {
726 return hmd;
727 break;
728 }
729 default: {
730 // If the handle corresponds to a controller
731 if (handle >= 2 && handle <= MAX_CONTROLLERS + 1) {
732 return controller[handle - 2];
733 } else {
734 return nullptr;
735 }
736 break;
737 }
738 }
739}
740
741vr::PropertyContainerHandle_t
742Context::TrackedDeviceToPropertyContainer(vr::TrackedDeviceIndex_t nDevice)
743{
744 size_t container = nDevice + 1;
745 if (nDevice == 0 && this->hmd) {
746 return container;
747 }
748 if (nDevice >= 1 && nDevice <= MAX_CONTROLLERS && this->controller[nDevice - 1]) {
749 return container;
750 }
751
752 return vr::k_ulInvalidPropertyContainer;
753}
754
755void
756Context::Log(const char *pchLogMessage)
757{
758 CTX_TRACE("[lighthouse]: %s", pchLogMessage);
759}
760// NOLINTEND(bugprone-easily-swappable-parameters)
761
762xrt_result_t
763get_roles(struct xrt_system_devices *xsysd, struct xrt_system_roles *out_roles)
764{
765 bool update_gen = false;
766 int head, left, right, gamepad;
767
768 u_device_assign_xdev_roles(xsysd->xdevs, xsysd->xdev_count, &head, &left, &right, &gamepad);
769
770 if (left != out_roles->left || right != out_roles->right || gamepad != out_roles->gamepad) {
771 update_gen = true;
772 }
773
774 if (update_gen) {
775 out_roles->generation_id++;
776
777 out_roles->left = left;
778 out_roles->right = right;
779 out_roles->gamepad = gamepad;
780
781 if (left != XRT_DEVICE_ROLE_UNASSIGNED) {
782 auto *left_dev = static_cast<ControllerDevice *>(xsysd->xdevs[left]);
783 left_dev->set_active_hand(XRT_HAND_LEFT);
784 }
785
786 if (right != XRT_DEVICE_ROLE_UNASSIGNED) {
787 auto *right_dev = static_cast<ControllerDevice *>(xsysd->xdevs[right]);
788 right_dev->set_active_hand(XRT_HAND_RIGHT);
789 }
790 }
791
792 return XRT_SUCCESS;
793}
794
795void
796destroy(struct xrt_system_devices *xsysd)
797{
798 for (uint32_t i = 0; i < ARRAY_SIZE(xsysd->xdevs); i++) {
799 xrt_device_destroy(&xsysd->xdevs[i]);
800 }
801
802 svrs->ctx.reset();
803 free(svrs);
804}
805
806extern "C" enum xrt_result
807steamvr_lh_create_devices(struct xrt_prober *xp, struct xrt_system_devices **out_xsysd)
808{
809 u_logging_level level = debug_get_log_option_lh_log();
810 // The driver likes to create a bunch of transient folders -
811 // let's try to make sure they're created where they normally are.
812 std::filesystem::path dir = STEAM_INSTALL_DIR + "/config/lighthouse";
813 if (!std::filesystem::exists(dir)) {
814 U_LOG_IFL_W(level,
815 "Couldn't find lighthouse config folder (%s)- transient folders will be created in current "
816 "working directory (%s)",
817 dir.c_str(), std::filesystem::current_path().c_str());
818 } else {
819 std::filesystem::current_path(dir);
820 }
821
822 std::string steamvr{};
823 if (getenv("STEAMVR_PATH") != nullptr) {
824 steamvr = getenv("STEAMVR_PATH");
825 } else {
826 steamvr = find_steamvr_install();
827 }
828
829 if (steamvr.empty()) {
830 U_LOG_IFL_E(level, "Could not find where SteamVR is installed!");
831 return xrt_result::XRT_ERROR_DEVICE_CREATION_FAILED;
832 }
833
834 U_LOG_IFL_I(level, "Found SteamVR install: %s", steamvr.c_str());
835
836 std::vector<vr::IServerTrackedDeviceProvider *> drivers = {};
837 const auto loadDriver = [&](std::string soPath, bool require) {
838 // TODO: support windows?
839 void *driver_lib = dlopen((steamvr + soPath).c_str(), RTLD_LAZY);
840 if (!driver_lib) {
841 U_LOG_IFL_E(level, "Couldn't open driver lib: %s", dlerror());
842 return !require;
843 }
844
845 void *sym = dlsym(driver_lib, "HmdDriverFactory");
846 if (!sym) {
847 U_LOG_IFL_E(level, "Couldn't find HmdDriverFactory in driver lib: %s", dlerror());
848 return false;
849 }
850 using HmdDriverFactory_t = void *(*)(const char *, int *);
851 auto factory = reinterpret_cast<HmdDriverFactory_t>(sym);
852
853 vr::EVRInitError err = vr::VRInitError_None;
854 drivers.push_back(static_cast<vr::IServerTrackedDeviceProvider *>(
855 factory(vr::IServerTrackedDeviceProvider_Version, (int *)&err)));
856 if (err != vr::VRInitError_None) {
857 U_LOG_IFL_E(level, "Couldn't get tracked device driver: error %u", err);
858 return false;
859 }
860 return true;
861 };
862 if (!loadDriver("/drivers/lighthouse/bin/linux64/driver_lighthouse.so", true))
863 return xrt_result::XRT_ERROR_DEVICE_CREATION_FAILED;
864 if (debug_get_bool_option_lh_load_slimevr() &&
865 !loadDriver("/drivers/slimevr/bin/linux64/driver_slimevr.so", false))
866 return xrt_result::XRT_ERROR_DEVICE_CREATION_FAILED;
867 svrs->ctx = Context::create(STEAM_INSTALL_DIR, steamvr, std::move(drivers));
868 if (svrs->ctx == nullptr)
869 return xrt_result::XRT_ERROR_DEVICE_CREATION_FAILED;
870
871 U_LOG_IFL_I(level, "Lighthouse initialization complete, giving time to setup connected devices...");
872 // RunFrame needs to be called to detect controllers
873 using namespace std::chrono_literals;
874 auto end_time = std::chrono::steady_clock::now() + 1ms * debug_get_num_option_lh_discover_wait_ms();
875 while (true) {
876 svrs->ctx->run_frame();
877 auto cur_time = std::chrono::steady_clock::now();
878 if (cur_time > end_time) {
879 break;
880 }
881 std::this_thread::sleep_for(20ms);
882 }
883 U_LOG_IFL_I(level, "Device search time complete.");
884
885 if (out_xsysd == NULL || *out_xsysd != NULL) {
886 U_LOG_IFL_E(level, "Invalid output system pointer");
887 return xrt_result::XRT_ERROR_DEVICE_CREATION_FAILED;
888 }
889
890 struct xrt_system_devices *xsysd = NULL;
891 xsysd = &svrs->base;
892
893 xsysd->destroy = destroy;
894 xsysd->get_roles = get_roles;
895
896 // Include the HMD
897 if (svrs->ctx->hmd) {
898 if (svrs->ctx->hmd->variant == VIVE_VARIANT_PRO2 && !svrs->ctx->hmd->init_vive_pro_2(xp)) {
899 U_LOG_IFL_W(level, "Found Vive Pro 2, but failed to initialize.");
900 }
901
902 // Always have a head at index 0 and iterate dev count.
903 xsysd->xdevs[xsysd->xdev_count] = svrs->ctx->hmd;
904 xsysd->static_roles.head = xsysd->xdevs[xsysd->xdev_count++];
905 }
906
907 // Include the controllers
908 for (size_t i = 0; i < MAX_CONTROLLERS; i++) {
909 if (svrs->ctx->controller[i]) {
910 xsysd->xdevs[xsysd->xdev_count++] = svrs->ctx->controller[i];
911 }
912 }
913
914 *out_xsysd = xsysd;
915
916 return xrt_result::XRT_SUCCESS;
917}