// Copyright 2023, Shawn Wallace // SPDX-License-Identifier: BSL-1.0 /*! * @file * @brief SteamVR driver context implementation and entrypoint. * @author Shawn Wallace * @ingroup drv_steamvr_lh */ #include #include #include #include #include #include #include #include #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 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::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(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(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(driver->GetComponent(vr::IVRDisplayComponent_Version)); VERIFY(display, "IVRDisplayComponent is null"); #undef VERIFY auto hmd_parts = std::make_unique(); 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(this->hmd) : static_cast(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_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 offset_dur(offset); std::chrono::duration offset = (now + offset_dur).time_since_epoch(); int64_t timestamp = std::chrono::duration_cast(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 &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(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(sym); vr::EVRInitError err = vr::VRInitError_None; auto *driver = static_cast( 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; }