diff --git a/driver/slimevr/resources/icons/slimevr_tracker_icon.png b/driver/slimevr/resources/icons/slimevr_tracker_icon.png new file mode 100644 index 00000000..e3e50fbc Binary files /dev/null and b/driver/slimevr/resources/icons/slimevr_tracker_icon.png differ diff --git a/driver/slimevr/resources/input/actions.json b/driver/slimevr/resources/input/actions.json new file mode 100644 index 00000000..6c62a384 --- /dev/null +++ b/driver/slimevr/resources/input/actions.json @@ -0,0 +1,41 @@ +{ + "action_manifest_version": 1, + "controller_type": "slimevr_virtual_controller", + "interaction_profile": "/interaction_profiles/valve/index_controller", + "description": "SlimeVR virtual controller", + "app_key": "openvr.component.slimevr", + "actions": { + "pose": [ + { "name": "/actions/main/in/PoseRaw" }, + { "name": "/actions/main/in/PoseAim" } + ], + "haptic": [ + { "name": "/actions/main/out/Haptic" } + ], + "boolean": [ + { "name": "/actions/main/in/DoubleTap" }, + { "name": "/actions/main/in/TripleTap" }, + { "name": "/actions/main/in/A" }, + { "name": "/actions/main/in/B" }, + { "name": "/actions/main/in/System" }, + { "name": "/actions/main/in/TriggerClick" }, + { "name": "/actions/main/in/GripTouch" }, + { "name": "/actions/main/in/TrackpadClick" }, + { "name": "/actions/main/in/TrackpadTouch" }, + { "name": "/actions/main/in/ThumbstickClick" }, + { "name": "/actions/main/in/ThumbstickTouch" } + ], + "scalar": [ + { "name": "/actions/main/in/TriggerValue" }, + { "name": "/actions/main/in/GripValue" }, + { "name": "/actions/main/in/TrackpadX" }, + { "name": "/actions/main/in/TrackpadY" }, + { "name": "/actions/main/in/ThumbstickX" }, + { "name": "/actions/main/in/ThumbstickY" } + ], + "skeleton": [ + { "name": "/actions/main/in/SkeletonLeft" }, + { "name": "/actions/main/in/SkeletonRight" } + ] + } +} diff --git a/driver/slimevr/resources/input/slimevr_controller_bindings.json b/driver/slimevr/resources/input/slimevr_controller_bindings.json new file mode 100644 index 00000000..aa9f7555 --- /dev/null +++ b/driver/slimevr/resources/input/slimevr_controller_bindings.json @@ -0,0 +1,109 @@ +{ + "jsonid": "input_profile", + "controller_type": "slimevr_virtual_controller", + "device_class": "TrackedDeviceClass_Controller", + "resource_root": "slimevr", + "driver_name": "slimevr", + "input_bindingui_mode": "controller_handed", + "should_show_binding_errors": true, + "compatibility_mode_controller_type": "knuckles", + "input_bindingui_left": { + "image": "{slimevr}/icons/slimevr_tracker_icon.png" + }, + "input_bindingui_right": { + "image": "{slimevr}/icons/slimevr_tracker_icon.png" + }, + "input_source": { + "/input/system": { + "type": "button", + "binding_image_point": [ 34, 45 ], + "order": 1 + }, + "/input/a": { + "type": "button", + "binding_image_point": [ 26, 42 ], + "order": 5 + }, + "/input/b": { + "type": "button", + "binding_image_point": [ 18, 37 ], + "order": 6 + }, + "/input/trigger": { + "type": "trigger", + "binding_image_point": [ 11, 60 ], + "order": 2 + }, + "/input/trackpad": { + "type": "trackpad", + "binding_image_point": [ 27, 37 ], + "order": 3 + }, + "/input/grip": { + "type": "trigger", + "binding_image_point": [ 47, 86 ], + "order": 7 + }, + "/input/thumbstick": { + "type": "joystick", + "binding_image_point": [ 31, 26 ], + "order": 4 + }, + "/input/pinch": { + "type": "pinch", + "binding_image_point": [ 27, 37 ], + "value_source": "/input/trigger", + "capsense_source": "/input/finger/index", + "force_source": "/input/trackpad", + "order": 7 + }, + + "/input/finger/index": { + "type": "trigger", + "visibility": "InputValueVisibility_AvailableButHidden", + "binding_image_point": [ 56, 86 ], + "order": 7 + }, + "/input/finger/middle": { + "type": "trigger", + "visibility": "InputValueVisibility_AvailableButHidden", + "binding_image_point": [ 56, 86 ], + "order": 8 + }, + "/input/finger/ring": { + "type": "trigger", + "visibility": "InputValueVisibility_AvailableButHidden", + "binding_image_point": [ 56, 86 ], + "order": 9 + }, + "/input/finger/pinky": { + "type": "trigger", + "visibility": "InputValueVisibility_AvailableButHidden", + "binding_image_point": [ 56, 86 ], + "order": 10 + }, + + "/pose/raw": { + "type": "pose", + "binding_image_point": [ 5, 35 ] + }, + "/input/skeleton/left": { + "type": "skeleton", + "skeleton": "/skeleton/hand/left", + "side": "left", + "binding_image_point": [ 5, 35 ] + + }, + "/input/skeleton/right": { + "type": "skeleton", + "skeleton": "/skeleton/hand/right", + "side": "right", + "binding_image_point": [ 5, 35 ] + }, + + "/output/haptic": { + "type": "vibration", + "binding_image_point": [ 5, 35 ] + } + } +} \ No newline at end of file diff --git a/driver/slimevr/resources/input/slimevr_controller_bindings_legacy.json b/driver/slimevr/resources/input/slimevr_controller_bindings_legacy.json new file mode 100644 index 00000000..487766bf --- /dev/null +++ b/driver/slimevr/resources/input/slimevr_controller_bindings_legacy.json @@ -0,0 +1,24 @@ +{ + "bindings": { + "/actions/legacy": { + "haptics": [ + ], + "poses": [ + { + "output": "/actions/legacy/in/Left_Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/legacy/in/Right_Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "sources": [ + ] + } + }, + "controller_type": "slimevr_virtual_controller", + "description": "Default binding values for legacy apps using the soft knuckles controller", + "name": "SlimeVR Legacy Defaults", + "simulated_actions": [] +} \ No newline at end of file diff --git a/driver/slimevr/resources/input/vrcompositor_bindings_slimevr.json b/driver/slimevr/resources/input/vrcompositor_bindings_slimevr.json new file mode 100644 index 00000000..ef9e0bf9 --- /dev/null +++ b/driver/slimevr/resources/input/vrcompositor_bindings_slimevr.json @@ -0,0 +1,303 @@ +{ + "action_manifest_version": 1, + "controller_type": "slimevr_virtual_controller", + "interaction_profile": "/interaction_profiles/valve/index_controller", + "description": "SlimeVR virtual controller", + "app_key": "openvr.component.vrcompositor", + "bindings": { + "/actions/lasermouse": { + "poses": [ + { + "output": "/actions/lasermouse/in/Pointer", + "path": "/user/hand/left/pose/tip" + }, + { + "output": "/actions/lasermouse/in/Pointer", + "path": "/user/hand/right/pose/tip" + } + ], + "haptics": [ + { + "output": "/actions/lasermouse/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/lasermouse/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ], + "sources": [ + { + "inputs": { "click": { "output": "/actions/lasermouse/in/LeftClick" } }, + "mode": "button", + "parameters": { + "click_activate_threshold": "0.65", + "click_deactivate_threshold": "0.6" + }, + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse/in/LeftClick" } }, + "mode": "button", + "parameters": { + "click_activate_threshold": "0.65", + "click_deactivate_threshold": "0.6" + }, + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse/in/Back" } }, + "mode": "button", + "path": "/user/hand/left/input/b" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse/in/Back" } }, + "mode": "button", + "path": "/user/hand/right/input/b" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse/in/Home" } }, + "mode": "button", + "path": "/user/hand/left/input/a" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse/in/Home" } }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "position": { "output": "/actions/lasermouse/in/TrackpadValue" }, + "touch": { "output": "/actions/lasermouse/in/TrackpadTouch" } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { "output": "/actions/lasermouse/in/TrackpadValue" }, + "touch": { "output": "/actions/lasermouse/in/TrackpadTouch" } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse/in/MiddleClick" } }, + "mode": "joystick", + "path": "/user/hand/left/input/joystick" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse/in/MiddleClick" } }, + "mode": "joystick", + "path": "/user/hand/right/input/joystick" + } + ] + }, + "/actions/lasermouse_secondary": { + "sources": [ + { + "inputs": { "click": { "output": "/actions/lasermouse_secondary/in/SwitchLaserHand" } }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { "click": { "output": "/actions/lasermouse_secondary/in/SwitchLaserHand" } }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + } + ] + }, + "/actions/scroll_discrete": { + "sources": [ + { + "inputs": { "scroll": { "output": "/actions/scroll_discrete/in/Scroll" } }, + "mode": "scroll", + "parameters": { "scroll_mode": "discrete" }, + "path": "/user/hand/left/input/joystick" + }, + { + "inputs": { "scroll": { "output": "/actions/scroll_discrete/in/Scroll" } }, + "mode": "scroll", + "parameters": { "scroll_mode": "discrete" }, + "path": "/user/hand/right/input/joystick" + }, + { + "inputs": { "scroll": { "output": "/actions/scroll_discrete/in/Scroll" } }, + "mode": "scroll", + "parameters": { + "scroll_mode": "discrete", + "discrete_scroll_trackpad_accumthreshold_onmove": "0.28", + "discrete_scroll_trackpad_accumthreshold_onreversal": "0.84", + "discrete_scroll_trackpad_accumthreshold_ontouch": "0.78", + "discrete_scroll_trackpad_noisethreshold_onmove": "0.01", + "discrete_scroll_trackpad_noisethreshold_onreversal": "0.045", + "discrete_scroll_trackpad_noisethreshold_ontouch": "0.15", + "discrete_scroll_trackpad_slideandhold_borderbottom": "-0.65", + "discrete_scroll_trackpad_slideandhold_bordertop": "0.55" + }, + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { "scroll": { "output": "/actions/scroll_discrete/in/Scroll" } }, + "mode": "scroll", + "parameters": { + "scroll_mode": "discrete", + "discrete_scroll_trackpad_accumthreshold_onmove": "0.28", + "discrete_scroll_trackpad_accumthreshold_onreversal": "0.84", + "discrete_scroll_trackpad_accumthreshold_ontouch": "0.78", + "discrete_scroll_trackpad_noisethreshold_onmove": "0.01", + "discrete_scroll_trackpad_noisethreshold_onreversal": "0.045", + "discrete_scroll_trackpad_noisethreshold_ontouch": "0.15", + "discrete_scroll_trackpad_slideandhold_borderbottom": "-0.65", + "discrete_scroll_trackpad_slideandhold_bordertop": "0.55" + }, + "path": "/user/hand/right/input/trackpad" + } + ] + }, + "/actions/scroll_smooth": { + "sources": [ + { + "inputs": { "scroll": { "output": "/actions/scroll_smooth/in/Scroll" } }, + "mode": "scroll", + "parameters": { "scroll_mode": "smooth" }, + "path": "/user/hand/left/input/joystick" + }, + { + "inputs": { "scroll": { "output": "/actions/scroll_smooth/in/Scroll" } }, + "mode": "scroll", + "parameters": { "scroll_mode": "smooth" }, + "path": "/user/hand/right/input/joystick" + }, + { + "inputs": { "scroll": { "output": "/actions/scroll_smooth/in/Scroll" } }, + "mode": "scroll", + "parameters": { "scroll_mode": "smooth" }, + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { "scroll": { "output": "/actions/scroll_smooth/in/Scroll" } }, + "mode": "scroll", + "parameters": { "scroll_mode": "smooth" }, + "path": "/user/hand/right/input/trackpad" + } + ] + }, + "/actions/system": { + "chords": [ + { + "inputs": [ + [ "/user/hand/left/input/system", "click" ], + [ "/user/hand/left/input/trigger", "click" ] + ], + "output": "/actions/system/in/TakeScreenshot" + }, + { + "inputs": [ + [ "/user/hand/right/input/system", "click" ], + [ "/user/hand/right/input/trigger", "click" ] + ], + "output": "/actions/system/in/TakeScreenshot" + } + ], + "sources": [ + { + "inputs": { + "click": { "output": "/actions/system/in/ToggleDashboard" }, + "double": { "output": "/actions/system/in/ToggleRoomView" }, + "held": { "output": "/actions/system/in/SystemButtonChord" } + }, + "mode": "button", + "path": "/user/hand/left/input/system" + }, + { + "inputs": { + "click": { "output": "/actions/system/in/ToggleDashboard" }, + "double": { "output": "/actions/system/in/ToggleRoomView" }, + "held": { "output": "/actions/system/in/SystemButtonChord" } + }, + "mode": "button", + "path": "/user/hand/right/input/system" + } + ] + }, + "/actions/quickrecenter": { + "sources": [ + { + "inputs": { "long": { "output": "/actions/quickrecenter/in/Recenter" } }, + "parameters": { + "long_press_delay": 1.0, + "long_press_expiry": 3.0 + }, + "mode": "button", + "path": "/user/hand/left/input/system" + }, + { + "inputs": { "long": { "output": "/actions/quickrecenter/in/Recenter" } }, + "parameters": { + "long_press_delay": 1.0, + "long_press_expiry": 3.0 + }, + "mode": "button", + "path": "/user/hand/right/input/system" + } + ] + }, + "/actions/roomsetup_floor": { + "sources": [ + { + "inputs": { "position": { "output": "/actions/roomsetup_floor/in/FloorFineAdjust" } }, + "mode": "joystick", + "path": "/user/hand/left/input/joystick" + }, + { + "inputs": { "position": { "output": "/actions/roomsetup_floor/in/FloorFineAdjust" } }, + "mode": "joystick", + "path": "/user/hand/right/input/joystick" + } + ] + }, + "/actions/dualanalog": { + "sources": [ + { + "inputs": { "click": { "output": "/actions/dualanalog/in/ModeSwitch1" } }, + "mode": "button", + "path": "/user/hand/left/input/a" + }, + { + "inputs": { "click": { "output": "/actions/dualanalog/in/ModeSwitch2" } }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "click": { "output": "/actions/dualanalog/in/LeftClick" }, + "position": { "output": "/actions/dualanalog/in/LeftValue" }, + "touch": { "output": "/actions/dualanalog/in/LeftTouch" } + }, + "mode": "joystick", + "path": "/user/hand/left/input/joystick" + }, + { + "inputs": { + "click": { "output": "/actions/dualanalog/in/RightClick" }, + "position": { "output": "/actions/dualanalog/in/RightValue" }, + "touch": { "output": "/actions/dualanalog/in/RightTouch" } + }, + "mode": "joystick", + "path": "/user/hand/right/input/joystick" + }, + { + "inputs": { "click": { "output": "/actions/dualanalog/in/LeftClick" } }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { "click": { "output": "/actions/dualanalog/in/RightClick" } }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + } + ] + } + } +} diff --git a/driver/slimevr/resources/localization/localization.json b/driver/slimevr/resources/localization/localization.json index d85396f3..8e628252 100644 --- a/driver/slimevr/resources/localization/localization.json +++ b/driver/slimevr/resources/localization/localization.json @@ -1,6 +1,26 @@ [ { "language_tag": "en_US", + "/input/a": "A Button", + "/input/b": "B Button", + "/input/system": "System Button", + "/input/trackpad": "Trackpad", + "/input/joystick": "Joystick", + "/input/skeleton": "Skeleton", + "/input/trigger": "Trigger", + "/output/haptic": "Haptic", + "slimevr": "SlimeVR", + "slimevr_virtual_controller": "SlimeVR Virtual Controller", + "/input/double_tap": "Double tap", + "/input/triple_tap": "Triple tap", + "/input/a": "A Button", + "/input/b": "B Button", + "/input/pinch": "Pinch Gesture", + "/input/finger/index": "Index Finger", + "/input/finger/middle": "Middle Finger", + "/input/finger/ring": "Ring Finger", + "/input/finger/pinky": "Pinky Finger", + "/input/thumbstick": "Thumb Stick", "slimevr_tracker" : "SlimeVR Tracker", "slimevr_tracker_left_foot" : "SlimeVR Tracker on Left Foot", "slimevr_tracker_right_foot" : "SlimeVR Tracker on Right Foot", diff --git a/driver/slimevr/resources/slimevr_driver_config.json b/driver/slimevr/resources/slimevr_driver_config.json new file mode 100644 index 00000000..ef2a8040 --- /dev/null +++ b/driver/slimevr/resources/slimevr_driver_config.json @@ -0,0 +1,4 @@ +{ + "external_hand_max_radius_m": 1.7, + "pose_lerp_speed": 0.8 +} diff --git a/src/IVRDevice.hpp b/src/IVRDevice.hpp index b2681abc..6fafa0a4 100644 --- a/src/IVRDevice.hpp +++ b/src/IVRDevice.hpp @@ -68,6 +68,11 @@ namespace SlimeVRDriver { * Updates device position from a received message. */ virtual void PositionMessage(messages::Position& position) = 0; + + /** + * Updates device position from a received message. + */ + virtual void ControllerInputMessage(messages::ControllerInput& position) = 0; /** * Updates device status from a received message. diff --git a/src/IVRDriver.hpp b/src/IVRDriver.hpp index 240cc54f..90a72952 100644 --- a/src/IVRDriver.hpp +++ b/src/IVRDriver.hpp @@ -1,33 +1,35 @@ #pragma once -#include -#include +#include "IVRDevice.hpp" #include -#include -#include +#include #include -#include "IVRDevice.hpp" +#include #include +#include +#include + namespace SlimeVRDriver { - class UniverseTranslation { - public: - // TODO: do we want to store this differently? - vr::HmdVector3_t translation; - float yaw; +class UniverseTranslation { +public: + // TODO: do we want to store this differently? + vr::HmdVector3_t translation; + float yaw; - static UniverseTranslation parse(simdjson::ondemand::object &obj); - }; + static UniverseTranslation parse(simdjson::ondemand::object &obj); +}; - typedef std::variant SettingsValue; +typedef std::variant + SettingsValue; - class IVRDriver : protected vr::IServerTrackedDeviceProvider { - public: - /** - * Returns all devices being managed by this driver. - * - * @return A vector of shared pointers to all managed devices. - */ - virtual std::vector> GetDevices() = 0; +class IVRDriver : protected vr::IServerTrackedDeviceProvider { +public: + /** + * Returns all devices being managed by this driver. + * + * @return A vector of shared pointers to all managed devices. + */ + virtual std::vector> GetDevices() = 0; /** * Returns all OpenVR events that happened on the current frame. @@ -36,59 +38,82 @@ namespace SlimeVRDriver { */ virtual const std::vector& GetOpenVREvents() = 0; - /** - * Returns the milliseconds between last frame and this frame. - * - * @return Milliseconds between last frame and this frame. - */ - virtual std::chrono::milliseconds GetLastFrameTime() = 0; + /** + * Returns the milliseconds between last frame and this frame. + * + * @return Milliseconds between last frame and this frame. + */ + virtual std::chrono::milliseconds GetLastFrameTime() = 0; - /** - * Adds a device to the driver. - * - * @param device A shared pointer to the device to be added. - * @return True on success, false on failure. - */ - virtual bool AddDevice(std::shared_ptr device) = 0; + /** + * Adds a device to the driver. + * + * @param device A shared pointer to the device to be added. + * @return True on success, false on failure. + */ + virtual bool AddDevice(std::shared_ptr device) = 0; - /** - * Returns the value of a settings key. - * - * @param key The settings key - * @return Value of the key, std::monostate if the value is malformed or missing. - */ - virtual SettingsValue GetSettingsValue(std::string key) = 0; + /** + * Returns the value of a settings key. + * + * @param key The settings key + * @return Value of the key, std::monostate if the value is malformed or + * missing. + */ + virtual SettingsValue GetSettingsValue(std::string key) = 0; - /** - * Gets the OpenVR VRDriverInput pointer. - * - * @return OpenVR VRDriverInput pointer. - */ - virtual vr::IVRDriverInput* GetInput() = 0; + /** + * Gets the OpenVR VRDriverInput pointer. + * + * @return OpenVR VRDriverInput pointer. + */ + virtual vr::IVRDriverInput *GetInput() = 0; - /** - * Gets the OpenVR VRDriverProperties pointer. - * - * @return OpenVR VRDriverProperties pointer. - */ - virtual vr::CVRPropertyHelpers* GetProperties() = 0; + /** + * Gets the OpenVR VRDriverProperties pointer. + * + * @return OpenVR VRDriverProperties pointer. + */ + virtual vr::CVRPropertyHelpers *GetProperties() = 0; - /** - * Gets the OpenVR VRServerDriverHost pointer. - * - * @return OpenVR VRServerDriverHost pointer. - */ - virtual vr::IVRServerDriverHost* GetDriverHost() = 0; + /** + * Gets the OpenVR VRServerDriverHost pointer. + * + * @return OpenVR VRServerDriverHost pointer. + */ + virtual vr::IVRServerDriverHost *GetDriverHost() = 0; - /** - * Gets the current UniverseTranslation. - */ - virtual std::optional GetCurrentUniverse() = 0; + /** + * Gets the current UniverseTranslation. + */ + virtual std::optional GetCurrentUniverse() = 0; + + /** + * Gets the current pose from an external controller (e.g. Virtual Desktop / + * Steam Link on Quest) for the given hand, if one is connected and tracked. + * Used to prefer external hand position when in view, falling back to SlimeVR + * when not. + */ + virtual std::optional + GetExternalPoseForHand(bool left_hand) = 0; + + /** Pose lerp speed (0–1) for smoothing. From driver config file. */ + virtual float GetPoseLerpSpeed() = 0; + /** Slower lerp speed used when swapping VD ↔ SlimeVR to smooth the transition. */ + virtual float GetPoseLerpSpeedOnSwap() = 0; + + /** Priority for the controller hand role. */ + virtual int GetControllerPriority() = 0; + /** Whether to avoid overwriting input when no data is received. */ + virtual bool GetInputPassthrough() = 0; + + /** Gets external controller buttons for the hand. */ + virtual uint64_t GetExternalButtonsForHand(bool left_hand) = 0; - virtual inline const char* const* GetInterfaceVersions() override { - return vr::k_InterfaceVersions; - }; + virtual inline const char *const *GetInterfaceVersions() override { + return vr::k_InterfaceVersions; + }; - virtual ~IVRDriver() {} - }; -} \ No newline at end of file + virtual ~IVRDriver() {} +}; +} // namespace SlimeVRDriver \ No newline at end of file diff --git a/src/TrackerDevice.cpp b/src/TrackerDevice.cpp index 5efa6911..5ccfd73e 100644 --- a/src/TrackerDevice.cpp +++ b/src/TrackerDevice.cpp @@ -1,59 +1,168 @@ #include "TrackerDevice.hpp" - -SlimeVRDriver::TrackerDevice::TrackerDevice(std::string serial, int device_id, TrackerRole tracker_role): - serial_(serial), - tracker_role_(tracker_role), - device_id_(device_id), - last_pose_(MakeDefaultPose()), - last_pose_atomic_(MakeDefaultPose()) -{ } - -std::string SlimeVRDriver::TrackerDevice::GetSerial() { - return serial_; +#include +#include + +namespace fs = std::filesystem; + +SlimeVRDriver::TrackerDevice::TrackerDevice(std::string serial, int device_id, + TrackerRole tracker_role) + : serial_(serial), tracker_role_(tracker_role), device_id_(device_id), + is_left_hand_(tracker_role_ == TrackerRole::LEFT_CONTROLLER || + tracker_role_ == TrackerRole::LEFT_HAND), + is_right_hand_(tracker_role_ == TrackerRole::RIGHT_CONTROLLER || + tracker_role_ == TrackerRole::RIGHT_HAND), + fingertracking_enabled_(is_left_hand_ || is_right_hand_), + is_controller_(tracker_role_ == TrackerRole::LEFT_CONTROLLER || + tracker_role_ == TrackerRole::RIGHT_CONTROLLER || + tracker_role_ == TrackerRole::LEFT_HAND || + tracker_role_ == TrackerRole::RIGHT_HAND), + last_pose_(MakeDefaultPose()), last_pose_atomic_(MakeDefaultPose()) {} + +std::string SlimeVRDriver::TrackerDevice::GetSerial() { return serial_; } + +vr::DriverPose_t +SlimeVRDriver::TrackerDevice::LerpPose(const vr::DriverPose_t &from, + const vr::DriverPose_t &to, float t) { + vr::DriverPose_t out = to; + for (int i = 0; i < 3; i++) + out.vecPosition[i] = + from.vecPosition[i] + (to.vecPosition[i] - from.vecPosition[i]) * t; + // Nlerp rotation (pose quaternions are double) + double w = (1.0 - static_cast(t)) * from.qRotation.w + static_cast(t) * to.qRotation.w; + double x = (1.0 - static_cast(t)) * from.qRotation.x + static_cast(t) * to.qRotation.x; + double y = (1.0 - static_cast(t)) * from.qRotation.y + static_cast(t) * to.qRotation.y; + double z = (1.0 - static_cast(t)) * from.qRotation.z + static_cast(t) * to.qRotation.z; + double len = std::sqrt(w * w + x * x + y * y + z * z); + if (len > 1e-6) { + out.qRotation.w = w / len; + out.qRotation.x = x / len; + out.qRotation.y = y / len; + out.qRotation.z = z / len; + } + return out; } void SlimeVRDriver::TrackerDevice::Update() { - if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) return; - - // Check if this device was asked to be identified - auto& events = GetDriver()->GetOpenVREvents(); - for (const auto& event : events) { - // Note here, event.trackedDeviceIndex does not necessarily equal device_index_, not sure why, but the component handle will match so we can just use that instead - //if (event.trackedDeviceIndex == device_index_) { - if (event.eventType == vr::EVREventType::VREvent_Input_HapticVibration) { - if (event.data.hapticVibration.componentHandle == haptic_component_) { - did_vibrate_ = true; - } - } - //} + if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) + return; + + // Check if this device was asked to be identified + auto events = GetDriver()->GetOpenVREvents(); + for (auto event : events) { + // Note here, event.trackedDeviceIndex does not necessarily equal device_index_, not sure why, but the component handle will match so we can just use that instead + // if (event.trackedDeviceIndex == device_index_) { + if (event.eventType == vr::EVREventType::VREvent_Input_HapticVibration) { + if (event.data.hapticVibration.componentHandle == haptic_component_) { + did_vibrate_ = true; + } } - - // Check if we need to keep vibrating - if (did_vibrate_) { - vibrate_anim_state_ += GetDriver()->GetLastFrameTime().count() / 1000.f; - if (vibrate_anim_state_ > 1.0f) { - did_vibrate_ = false; - vibrate_anim_state_ = 0.0f; - } + //} + } + + // Check if we need to keep vibrating + if (did_vibrate_) { + vibrate_anim_state_ += GetDriver()->GetLastFrameTime().count() / 1000.f; + if (vibrate_anim_state_ > 1.0f) { + did_vibrate_ = false; + vibrate_anim_state_ = 0.0f; + } + } + + bool have_external = false; + if (is_controller_) { + auto external = GetDriver()->GetExternalPoseForHand(is_left_hand_); + have_external = external.has_value(); + } + + if (was_activated_ && is_controller_) { + bool send_input = true; + if (GetDriver()->GetInputPassthrough() && have_external) { + send_input = false; } -} - -void SlimeVRDriver::TrackerDevice::PositionMessage(messages::Position &position) { - if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) return; - // Setup pose for this frame - auto pose = last_pose_; - //send the new position and rotation from the pipe to the tracker object - if (position.has_x()) { - pose.vecPosition[0] = position.x(); - pose.vecPosition[1] = position.y(); - pose.vecPosition[2] = position.z(); + // Merge external button state (from VD/Steam Link) with SlimeVR's own state. + // For digital buttons we OR them together so either source can trigger. + uint64_t ext_buttons = GetDriver()->GetExternalButtonsForHand(is_left_hand_); + bool ext_a = (ext_buttons & (1ULL << vr::k_EButton_A)) != 0; + bool ext_b = (ext_buttons & (1ULL << vr::k_EButton_ApplicationMenu)) != 0; + bool ext_stick = (ext_buttons & (1ULL << vr::k_EButton_SteamVR_Touchpad)) != 0; + bool ext_system = (ext_buttons & (1ULL << vr::k_EButton_System)) != 0; + bool ext_grip = (ext_buttons & (1ULL << vr::k_EButton_Grip)) != 0; + bool ext_trigger= (ext_buttons & (1ULL << vr::k_EButton_SteamVR_Trigger)) != 0; + + // Merged digital states: local SlimeVR OR external VD + bool merged_a = button_1_value || ext_a; + bool merged_b = button_2_value || ext_b; + bool merged_stick = stick_click_value || ext_stick; + bool merged_system = system_click_value|| ext_system; + bool merged_trigger_click = trigger_value_click || ext_trigger; + + if (send_input) { + // Analog inputs from SlimeVR (no external analog source available) + vr::VRDriverInput()->UpdateScalarComponent(this->trigger_component_, + trigger_value_, 0); + vr::VRDriverInput()->UpdateScalarComponent(this->grip_value_component_, + grip_value, 0); + vr::VRDriverInput()->UpdateScalarComponent(this->stick_x_component_, + thumbstick_x_value, 0); + vr::VRDriverInput()->UpdateScalarComponent(this->stick_y_component_, + thumbstick_y_value, 0); } - pose.qRotation.w = position.qw(); - pose.qRotation.x = position.qx(); - pose.qRotation.y = position.qy(); - pose.qRotation.z = position.qz(); + // Digital buttons are always merged regardless of send_input, + // so VD button presses always come through. + vr::VRDriverInput()->UpdateBooleanComponent(this->button_a_component_, + merged_a, 0); + vr::VRDriverInput()->UpdateBooleanComponent(this->button_b_component_, + merged_b, 0); + vr::VRDriverInput()->UpdateBooleanComponent(this->stick_click_component_, + merged_stick, 0); + vr::VRDriverInput()->UpdateBooleanComponent(this->system_component, + merged_system, 0); + vr::VRDriverInput()->UpdateBooleanComponent(this->system_component_touch, + merged_system, 0); + vr::VRDriverInput()->UpdateBooleanComponent(this->trigger_component_touch_, + merged_trigger_click, 0); + } + + // Target pose: controllers use external (VD/Steam Link) when available else SlimeVR; trackers use last SlimeVR pose. + vr::DriverPose_t target = last_pose_atomic_.load(); + if (is_controller_) { + auto external = GetDriver()->GetExternalPoseForHand(is_left_hand_); + if (external.has_value()) + target = *external; + } + // Use slower lerp when swapping VD ↔ SlimeVR to smooth the transition. + float lerp_t = GetDriver()->GetPoseLerpSpeed(); + if (is_controller_ && (have_external != last_frame_had_external_)) + lerp_t = GetDriver()->GetPoseLerpSpeedOnSwap(); + last_frame_had_external_ = have_external; + if (!smoothed_pose_.has_value()) + smoothed_pose_ = target; + else + smoothed_pose_ = LerpPose(*smoothed_pose_, target, lerp_t); + GetDriver()->GetDriverHost()->TrackedDevicePoseUpdated( + device_index_, *smoothed_pose_, sizeof(vr::DriverPose_t)); +} + +void SlimeVRDriver::TrackerDevice::PositionMessage( + messages::Position &position) { + if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) + return; + + // Setup pose for this frame + auto pose = last_pose_; + // send the new position and rotation from the pipe to the tracker object + if (position.has_x()) { + pose.vecPosition[0] = position.x(); + pose.vecPosition[1] = position.y(); + pose.vecPosition[2] = position.z(); + } + + pose.qRotation.w = position.qw(); + pose.qRotation.x = position.qx(); + pose.qRotation.y = position.qy(); + pose.qRotation.z = position.qz(); if (position.has_vx()) { pose.vecVelocity[0] = position.vx(); @@ -68,136 +177,538 @@ void SlimeVRDriver::TrackerDevice::PositionMessage(messages::Position &position) if (current_universe.has_value()) { auto trans = current_universe.value(); - // TODO: set this once, somewhere? - pose.vecWorldFromDriverTranslation[0] = -trans.translation.v[0]; - pose.vecWorldFromDriverTranslation[1] = -trans.translation.v[1]; - pose.vecWorldFromDriverTranslation[2] = -trans.translation.v[2]; - - pose.qWorldFromDriverRotation.w = cos(trans.yaw / 2); - pose.qWorldFromDriverRotation.x = 0; - pose.qWorldFromDriverRotation.y = sin(trans.yaw / 2); - pose.qWorldFromDriverRotation.z = 0; + // TODO: set this once, somewhere? + pose.vecWorldFromDriverTranslation[0] = -trans.translation.v[0]; + pose.vecWorldFromDriverTranslation[1] = -trans.translation.v[1]; + pose.vecWorldFromDriverTranslation[2] = -trans.translation.v[2]; + + pose.qWorldFromDriverRotation.w = cos(trans.yaw / 2); + pose.qWorldFromDriverRotation.x = 0; + pose.qWorldFromDriverRotation.y = sin(trans.yaw / 2); + pose.qWorldFromDriverRotation.z = 0; + } + + bool double_tap = false; + bool triple_tap = false; + + // Only push SlimeVR finger data when SlimeVR is actually reporting it; when it isn't, we skip so we don't overwrite finger data from Virtual Desktop/Steam Link. + if (fingertracking_enabled_ && position.finger_bone_rotations_size() > 0) { + vr::VRBoneTransform_t finger_skeleton_[31]{}; + for (int i = 0; i < position.finger_bone_rotations_size(); i++) { + auto fingerData = position.finger_bone_rotations(i); + int fingerBoneName = static_cast(fingerData.name()); + int boneIndex = protobuf_fingers_to_openvr[fingerBoneName]; + finger_skeleton_[boneIndex].orientation = { + fingerData.w(), fingerData.x(), fingerData.y(), fingerData.z()}; } - - pose.deviceIsConnected = true; - pose.poseIsValid = true; - pose.result = vr::ETrackingResult::TrackingResult_Running_OK; - - // Notify SteamVR that pose was updated - last_pose_atomic_ = (last_pose_ = pose); - GetDriver()->GetDriverHost()->TrackedDevicePoseUpdated(device_index_, pose, sizeof(vr::DriverPose_t)); + // Update the finger skeleton once with the full hand. With and without + // controller have the same pose. + vr::VRDriverInput()->UpdateSkeletonComponent( + skeletal_component_handle_, vr::VRSkeletalMotionRange_WithController, + finger_skeleton_, 31); + vr::VRDriverInput()->UpdateSkeletonComponent( + skeletal_component_handle_, vr::VRSkeletalMotionRange_WithoutController, + finger_skeleton_, 31); + } + + pose.deviceIsConnected = true; + pose.poseIsValid = true; + pose.result = vr::ETrackingResult::TrackingResult_Running_OK; + + if (is_controller_) { + // Set inputs + vr::VRDriverInput()->UpdateBooleanComponent(this->double_tap_component_, + double_tap, 0); + vr::VRDriverInput()->UpdateBooleanComponent(this->triple_tap_component_, + triple_tap, 0); + } + + // Store for Update(); pose is submitted only there. + last_pose_atomic_ = (last_pose_ = pose); +} +void SlimeVRDriver::TrackerDevice::ControllerInputMessage( + messages::ControllerInput &controllerInput) { + if (was_activated_ && is_controller_) { + // Get inputs from protobuf, store them for Update which is called during RunFrame + trigger_value_ = controllerInput.trigger(); + trigger_value_click = controllerInput.trigger() > 0.5f; + grip_value = controllerInput.grip(); + thumbstick_x_value = controllerInput.thumbstick_x(); + thumbstick_y_value = controllerInput.thumbstick_y(); + button_1_value = controllerInput.button_1(); + button_2_value = controllerInput.button_2(); + stick_click_value = controllerInput.stick_click(); + system_value = controllerInput.menu_recenter(); + system_click_value = controllerInput.menu_recenter(); + } } - void SlimeVRDriver::TrackerDevice::BatteryMessage(messages::Battery &battery) { - if (this->device_index_ == vr::k_unTrackedDeviceIndexInvalid) - return; - - // Get the properties handle - auto props = GetDriver()->GetProperties()->TrackedDeviceToPropertyContainer(this->device_index_); + if (this->device_index_ == vr::k_unTrackedDeviceIndexInvalid) + return; + + // Get the properties handle + auto containerHandle_ = + GetDriver()->GetProperties()->TrackedDeviceToPropertyContainer( + this->device_index_); + + vr::ETrackedPropertyError err; + + // Set that the tracker reports battery level in case it has not already been + // set to true It's a given that the tracker supports reporting battery life + // because otherwise a BatteryMessage would not be received + if (vr::VRProperties()->GetBoolProperty( + containerHandle_, vr::Prop_DeviceProvidesBatteryStatus_Bool, &err) != + true) { + vr::VRProperties()->SetBoolProperty( + containerHandle_, vr::Prop_DeviceProvidesBatteryStatus_Bool, true); + } + + if (battery.is_charging()) { + vr::VRProperties()->SetBoolProperty(containerHandle_, + vr::Prop_DeviceIsCharging_Bool, true); + } else { + vr::VRProperties()->SetBoolProperty(containerHandle_, + vr::Prop_DeviceIsCharging_Bool, false); + } + + // Set the battery Level; 0 = 0%, 1 = 100% + vr::VRProperties()->SetFloatProperty(containerHandle_, + vr::Prop_DeviceBatteryPercentage_Float, + battery.battery_level()); +} - vr::ETrackedPropertyError err; +void SlimeVRDriver::TrackerDevice::StatusMessage( + messages::TrackerStatus &status) { + if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) + return; - // Set that the tracker reports battery level in case it has not already been set to true - // It's a given that the tracker supports reporting battery life because otherwise a BatteryMessage would not be received - if (vr::VRProperties()->GetBoolProperty(props, vr::Prop_DeviceProvidesBatteryStatus_Bool, &err) != true) { - vr::VRProperties()->SetBoolProperty(props, vr::Prop_DeviceProvidesBatteryStatus_Bool, true); - } + vr::DriverPose_t pose = last_pose_; + switch (status.status()) { + case messages::TrackerStatus_Status_OK: + pose.deviceIsConnected = true; + pose.poseIsValid = true; + break; + case messages::TrackerStatus_Status_DISCONNECTED: + pose.deviceIsConnected = false; + pose.poseIsValid = false; + break; + case messages::TrackerStatus_Status_ERROR: + case messages::TrackerStatus_Status_BUSY: + default: + pose.deviceIsConnected = true; + pose.poseIsValid = false; + break; + } - if (battery.is_charging()) { - vr::VRProperties()->SetBoolProperty(props, vr::Prop_DeviceIsCharging_Bool, true); - } else { - vr::VRProperties()->SetBoolProperty(props, vr::Prop_DeviceIsCharging_Bool, false); - } - - // Set the battery Level; 0 = 0%, 1 = 100% - vr::VRProperties()->SetFloatProperty(props, vr::Prop_DeviceBatteryPercentage_Float, battery.battery_level()); -} - -void SlimeVRDriver::TrackerDevice::StatusMessage(messages::TrackerStatus &status) { - if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) return; - - vr::DriverPose_t pose = last_pose_; - switch (status.status()) { - case messages::TrackerStatus_Status_OK: - pose.deviceIsConnected = true; - pose.poseIsValid = true; - break; - case messages::TrackerStatus_Status_DISCONNECTED: - pose.deviceIsConnected = false; - pose.poseIsValid = false; - break; - case messages::TrackerStatus_Status_ERROR: - case messages::TrackerStatus_Status_BUSY: - default: - pose.deviceIsConnected = true; - pose.poseIsValid = false; - break; - } + // TODO: send position/rotation of 0 instead of last pose? - // TODO: send position/rotation of 0 instead of last pose? - - last_pose_atomic_ = (last_pose_ = pose); - GetDriver()->GetDriverHost()->TrackedDevicePoseUpdated(device_index_, pose, sizeof(vr::DriverPose_t)); + // Store for Update(); pose is submitted only there. + last_pose_atomic_ = (last_pose_ = pose); } DeviceType SlimeVRDriver::TrackerDevice::GetDeviceType() { - return DeviceType::TRACKER; + if (is_controller_) { + return DeviceType::CONTROLLER; + } + return DeviceType::TRACKER; } vr::TrackedDeviceIndex_t SlimeVRDriver::TrackerDevice::GetDeviceIndex() { - return device_index_; + return device_index_; } vr::EVRInitError SlimeVRDriver::TrackerDevice::Activate(uint32_t unObjectId) { - device_index_ = unObjectId; - - logger_->Log("Activating tracker {}", serial_); - - auto props = GetDriver()->GetProperties()->TrackedDeviceToPropertyContainer(device_index_); - - GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_ManufacturerName_String, "SlimeVR"); - GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_ModelNumber_String, "SlimeVR Virtual Tracker"); - - GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_RenderModelName_String, "{htc}/rendermodels/vr_tracker_vive_1_0"); - - // Some device properties will be derived at runtime by SteamVR - // using the profile, such as the device class and controller type - GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_InputProfilePath_String, "{slimevr}/input/slimevr_tracker_profile.json"); + device_index_ = unObjectId; + + logger_->Log("Activating tracker %s", serial_.c_str()); + + const std::string log_dir = "C:\\Temp\\SlimeVRLogs\\"; + + // Create directory if it doesn't exist + try { + fs::create_directories(log_dir); + } catch (...) { + // If this fails, we silently continue (driver must not crash) + } + + // One log file per tracker + const std::string log_path = log_dir + "input.log"; + + input_log_.open(log_path, std::ios::out | std::ios::app); + if (input_log_.is_open()) { + input_log_ << "=== Activating tracker " << serial_ << " ===" << std::endl; + } + + // Get the properties handle + containerHandle_ = + GetDriver()->GetProperties()->TrackedDeviceToPropertyContainer( + device_index_); + + // Set some universe ID (Must be 2 or higher) + GetDriver()->GetProperties()->SetUint64Property( + containerHandle_, vr::Prop_CurrentUniverseId_Uint64, 4); + + // Set up a model "number" (not needed but good to have) + if (is_controller_) { + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_ModelNumber_String, + "SlimeVR Virtual Controller"); + } else { + GetDriver()->GetProperties()->SetStringProperty(containerHandle_, + vr::Prop_ModelNumber_String, + "SlimeVR Virtual Tracker"); + } + + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_ManufacturerName_String, "SlimeVR"); + + //// Hand selection + if (is_left_hand_) { + GetDriver()->GetProperties()->SetInt32Property( + containerHandle_, vr::Prop_ControllerRoleHint_Int32, + vr::ETrackedControllerRole::TrackedControllerRole_LeftHand); + } else if (is_right_hand_) { + GetDriver()->GetProperties()->SetInt32Property( + containerHandle_, vr::Prop_ControllerRoleHint_Int32, + vr::ETrackedControllerRole::TrackedControllerRole_RightHand); + } else { + GetDriver()->GetProperties()->SetInt32Property( + containerHandle_, vr::Prop_ControllerRoleHint_Int32, + vr::ETrackedControllerRole::TrackedControllerRole_OptOut); + } + + // Should be treated as controller or as tracker? (Hand = Tracker and + // Controller = Controller) + if (is_controller_) { + vr::VRProperties()->SetInt32Property(containerHandle_, + vr::Prop_DeviceClass_Int32, + vr::TrackedDeviceClass_Controller); + vr::VRProperties()->SetStringProperty(containerHandle_, + vr::Prop_ControllerType_String, + "slimevr_virtual_controller"); + vr::VRProperties()->SetInt32Property( + containerHandle_, vr::Prop_ControllerHandSelectionPriority_Int32, + GetDriver()->GetControllerPriority()); + } else { + vr::VRProperties()->SetInt32Property(containerHandle_, + vr::Prop_DeviceClass_Int32, + vr::TrackedDeviceClass_GenericTracker); + } + + // Set up a render model path (index controllers for controllers and vive + // trackers 1.0 for trackers) + std::string model_path; + if (is_controller_) { + vr::VRProperties()->SetStringProperty( + containerHandle_, vr::Prop_RenderModelName_String, + is_right_hand_ ? "{indexcontroller}valve_controller_knu_1_0_right" + : "{indexcontroller}valve_controller_knu_1_0_left"); + } else { + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_RenderModelName_String, + "{htc}/rendermodels/vr_tracker_vive_1_0"); + } + + // Set the icons + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceReady_String, + "{slimevr}/icons/tracker_status_ready.png"); + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceOff_String, + "{slimevr}/icons/tracker_status_off.png"); + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceSearching_String, + "{slimevr}/icons/tracker_status_ready.png"); + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceSearchingAlert_String, + "{slimevr}/icons/tracker_status_ready_alert.png"); + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceReadyAlert_String, + "{slimevr}/icons/tracker_status_ready_alert.png"); + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceNotReady_String, + "{slimevr}/icons/tracker_status_error.png"); + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceStandby_String, + "{slimevr}/icons/tracker_status_standby.png"); + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_NamedIconPathDeviceAlertLow_String, + "{slimevr}/icons/tracker_status_ready_low.png"); + + // Set inputs + if (is_controller_) { + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_InputProfilePath_String, + "{slimevr}/input/slimevr_controller_bindings.json"); + uint64_t supportedButtons = 0xFFFFFFFFFFFFFFFFULL; + vr::VRProperties()->SetUint64Property( + containerHandle_, vr::Prop_SupportedButtons_Uint64, supportedButtons); + + LogInfo("Creating /pose/raw component"); + vr::EVRInputError input_error = vr::VRDriverInput()->CreatePoseComponent( + containerHandle_, "/pose/raw", &this->raw_pose_component_handle_); + LogInputError(input_error, "/pose/raw", this->raw_pose_component_handle_); + + LogInfo("Creating /pose/tip component"); + input_error = vr::VRDriverInput()->CreatePoseComponent( + containerHandle_, "/pose/tip", &this->aim_pose_component_handle_); + LogInputError(input_error, "/pose/tip", this->aim_pose_component_handle_); + + LogInfo("Creating /input/double_tap/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/double_tap/click", + &this->double_tap_component_); + LogInputError(input_error, "/input/double_tap/click", + this->double_tap_component_); + + LogInfo("Creating /input/triple_tap/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/triple_tap/click", + &this->triple_tap_component_); + LogInputError(input_error, "/input/triple_tap/click", + this->triple_tap_component_); + + LogInfo("Creating /input/a/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/a/click", &this->button_a_component_); + LogInputError(input_error, "/input/a/click", this->button_a_component_); + + LogInfo("Creating /input/a/touch component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/a/touch", &this->button_a_component_touch_); + LogInputError(input_error, "/input/a/touch", + this->button_a_component_touch_); + + LogInfo("Creating /input/b/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/b/click", &this->button_b_component_); + LogInputError(input_error, "/input/b/click", this->button_b_component_); + + LogInfo("Creating /input/b/touch component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/b/touch", &this->button_b_component_touch_); + LogInputError(input_error, "/input/b/touch", + this->button_b_component_touch_); + + LogInfo("Creating /input/system/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/system/click", &this->system_component); + LogInputError(input_error, "/input/system/click", this->system_component); + + LogInfo("Creating /input/system/touch component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/system/touch", &this->system_component_touch); + LogInputError(input_error, "/input/system/touch", + this->system_component_touch); + + LogInfo("Creating /input/trackpad/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/trackpad/click", + &this->trackpad_click_component_); + LogInputError(input_error, "/input/trackpad/click", + this->trackpad_click_component_); + + LogInfo("Creating /input/trackpad/touch component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/trackpad/touch", + &this->trackpad_touch_component_); + LogInputError(input_error, "/input/trackpad/touch", + this->trackpad_touch_component_); + + LogInfo("Creating /input/joystick/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/joystick/click", + &this->stick_click_component_); + LogInputError(input_error, "/input/joystick/click", + this->stick_click_component_); + + LogInfo("Creating /input/thumbstick/touch component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/thumbstick/touch", + &this->stick_click_component_touch_); + LogInputError(input_error, "/input/thumbstick/touch", + this->stick_click_component_touch_); + + // Scalar components + + LogInfo("Creating /input/trigger/value component"); + input_error = vr::VRDriverInput()->CreateScalarComponent( + containerHandle_, "/input/trigger/value", &this->trigger_component_, + vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedOneSided); + LogInputError(input_error, "/input/trigger/value", + this->trigger_component_); + + LogInfo("Creating /input/trigger/touch component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/trigger/touch", + &this->trigger_component_touch_); + LogInputError(input_error, "/input/trigger/touch", + this->trigger_component_touch_); + + LogInfo("Creating /input/trigger/click component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/trigger/click", + &this->trigger_component_click_); + LogInputError(input_error, "/input/trigger/click", + this->trigger_component_click_); + + LogInfo("Creating /input/grip/value component"); + input_error = vr::VRDriverInput()->CreateScalarComponent( + containerHandle_, "/input/grip/value", &this->grip_value_component_, + vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedOneSided); + LogInputError(input_error, "/input/grip/value", + this->grip_value_component_); + + LogInfo("Creating /input/grip/touch component"); + input_error = vr::VRDriverInput()->CreateBooleanComponent( + containerHandle_, "/input/grip/touch", + &this->grip_value_component_touch_); + LogInputError(input_error, "/input/grip/touch", + this->grip_value_component_touch_); + + LogInfo("Creating /input/trackpad/x component"); + input_error = vr::VRDriverInput()->CreateScalarComponent( + containerHandle_, "/input/trackpad/x", &this->trackpad_x_component_, + vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided); + LogInputError(input_error, "/input/trackpad/x", + this->trackpad_x_component_); + + LogInfo("Creating /input/trackpad/y component"); + input_error = vr::VRDriverInput()->CreateScalarComponent( + containerHandle_, "/input/trackpad/y", &this->trackpad_y_component_, + vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided); + LogInputError(input_error, "/input/trackpad/y", + this->trackpad_y_component_); + + LogInfo("Creating /input/thumbstick/x component"); + input_error = vr::VRDriverInput()->CreateScalarComponent( + containerHandle_, "/input/thumbstick/x", &this->stick_x_component_, + vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided); + LogInputError(input_error, "/input/thumbstick/x", this->stick_x_component_); + + LogInfo("Creating /input/joystick/y component"); + input_error = vr::VRDriverInput()->CreateScalarComponent( + containerHandle_, "/input/thumbstick/y", &this->stick_y_component_, + vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided); + LogInputError(input_error, "/input/thumbstick/y", this->stick_y_component_); + + LogInfo("Creating /output/haptic component"); + input_error = vr::VRDriverInput()->CreateHapticComponent( + containerHandle_, "/output/haptic", &this->haptic_component_); + LogInputError(input_error, "/output/haptic", this->haptic_component_); + } + + // Automatically select vive tracker roles and set hints for games that need + // it (Beat Saber avatar mod, for example) + if (!is_controller_) { + auto role_hint = GetViveRoleHint(tracker_role_); + if (role_hint != "") { + GetDriver()->GetProperties()->SetStringProperty( + containerHandle_, vr::Prop_ControllerType_String, role_hint.c_str()); + } - // Doesn't apply until restart of SteamVR auto role = GetViveRole(tracker_role_); if (role != "") { - vr::VRSettings()->SetString(vr::k_pch_Trackers_Section, ("/devices/slimevr/" + serial_).c_str(), role.c_str()); + vr::VRSettings()->SetString(vr::k_pch_Trackers_Section, + ("/devices/slimevr/" + serial_).c_str(), + role.c_str()); } + } + + // Setup skeletal input for fingertracking + if (fingertracking_enabled_) { + vr::VRDriverInput()->CreateSkeletonComponent( + containerHandle_, + is_right_hand_ ? "/input/skeleton/right" : "/input/skeleton/left", + is_right_hand_ ? "/skeleton/hand/right" : "/skeleton/hand/left", + "/pose/raw", vr::EVRSkeletalTrackingLevel::VRSkeletalTracking_Full, + NULL, // Fist + 31, &skeletal_component_handle_); + + // NOTE: We intentionally do NOT send a zeroed-out skeleton here. + // Sending an empty skeleton on activation would override finger data + // from other sources (e.g. Virtual Desktop hand tracking). + // The first real skeleton update will come via PositionMessage when + // SlimeVR Server actually has finger tracking data to send. + } + was_activated_ = true; + return vr::EVRInitError::VRInitError_None; +} - return vr::EVRInitError::VRInitError_None; +void SlimeVRDriver::TrackerDevice::LogInfo(const char *message) { + if (input_log_.is_open()) { + input_log_ << "[Info] " << message << std::endl; + input_log_.flush(); + } } -void SlimeVRDriver::TrackerDevice::Deactivate() { - device_index_ = vr::k_unTrackedDeviceIndexInvalid; +void SlimeVRDriver::TrackerDevice::LogInputError( + vr::EVRInputError err, const char *path, + vr::VRInputComponentHandle_t componentHandle) { + if (!input_log_.is_open()) + return; + + bool validHandle = componentHandle != vr::k_ulInvalidInputComponentHandle; + input_log_ << "[" << (err == vr::VRInputError_None ? "Info" : "InputError") + << "] " << path << "\r\n Handle: " << componentHandle + << "\r\n Handle Is Valid: " << (validHandle ? "true" : "false") + << "\r\n Failure Result: " << GetInputErrorName(err) << " (" << err + << ")" << std::endl; + input_log_.flush(); // force write immediately +} +void SlimeVRDriver::TrackerDevice::LogInput( + const char *path, vr::VRInputComponentHandle_t componentHandle) { + if (!input_log_.is_open()) + return; + + bool validHandle = componentHandle != vr::k_ulInvalidInputComponentHandle; + input_log_ << "[" + << "Info" + << "] " << path << "\r\n Handle: " << componentHandle + << "\r\n Handle Is Valid: " << (validHandle ? "true" : "false") + << std::endl; + input_log_.flush(); // force write immediately } -void SlimeVRDriver::TrackerDevice::EnterStandby() { +const char * +SlimeVRDriver::TrackerDevice::GetInputErrorName(vr::EVRInputError err) { + switch (err) { + case vr::VRInputError_None: + return "None"; + case vr::VRInputError_NameNotFound: + return "NameNotFound"; + case vr::VRInputError_WrongType: + return "WrongType"; + // Add others as needed + default: + return "Unknown"; + } +} +void SlimeVRDriver::TrackerDevice::Deactivate() { + device_index_ = vr::k_unTrackedDeviceIndexInvalid; } -void* SlimeVRDriver::TrackerDevice::GetComponent(const char* pchComponentNameAndVersion) { - return nullptr; +void SlimeVRDriver::TrackerDevice::EnterStandby() {} + +void *SlimeVRDriver::TrackerDevice::GetComponent( + const char *pchComponentNameAndVersion) { + return nullptr; } -void SlimeVRDriver::TrackerDevice::DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) { - if (unResponseBufferSize >= 1) { - pchResponseBuffer[0] = 0; - } +void SlimeVRDriver::TrackerDevice::DebugRequest(const char *pchRequest, + char *pchResponseBuffer, + uint32_t unResponseBufferSize) { + if (unResponseBufferSize >= 1) { + pchResponseBuffer[0] = 0; + } } vr::DriverPose_t SlimeVRDriver::TrackerDevice::GetPose() { - return last_pose_atomic_; + return last_pose_atomic_; } -int SlimeVRDriver::TrackerDevice::GetDeviceId() { - return device_id_; -} +int SlimeVRDriver::TrackerDevice::GetDeviceId() { return device_id_; } void SlimeVRDriver::TrackerDevice::SetDeviceId(int device_id) { - device_id_ = device_id; + device_id_ = device_id; } diff --git a/src/TrackerDevice.hpp b/src/TrackerDevice.hpp index 76536553..7b4d2ab1 100644 --- a/src/TrackerDevice.hpp +++ b/src/TrackerDevice.hpp @@ -1,63 +1,150 @@ #pragma once +#include #include #include -#include +#include #include -#include #include +#include -#include -#include +#include +#include #include +#include #include -#include "TrackerRole.hpp" +#include + #include "Logger.hpp" +#include "TrackerRole.hpp" namespace SlimeVRDriver { - class TrackerDevice : public IVRDevice { - public: - TrackerDevice(std::string serial, int device_id, TrackerRole tracker_role); - ~TrackerDevice() = default; - - // Inherited via IVRDevice - virtual std::string GetSerial() override; - virtual void Update() override; - virtual vr::TrackedDeviceIndex_t GetDeviceIndex() override; - virtual DeviceType GetDeviceType() override; - virtual int GetDeviceId() override; - virtual void SetDeviceId(int device_id) override; - virtual void PositionMessage(messages::Position &position) override; - virtual void StatusMessage(messages::TrackerStatus &status) override; - virtual void BatteryMessage(messages::Battery &battery) override; - - // Inherited via ITrackedDeviceServerDriver - virtual vr::EVRInitError Activate(uint32_t unObjectId) override; - virtual void Deactivate() override; - virtual void EnterStandby() override; - virtual void* GetComponent(const char* pchComponentNameAndVersion) override; - virtual void DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) override; - virtual vr::DriverPose_t GetPose() override; - - private: - std::shared_ptr logger_ = std::make_shared(); - - std::atomic device_index_ = vr::k_unTrackedDeviceIndexInvalid; - std::string serial_; - - int device_id_; - TrackerRole tracker_role_; - - vr::DriverPose_t last_pose_ = IVRDevice::MakeDefaultPose(); - std::atomic last_pose_atomic_ = IVRDevice::MakeDefaultPose(); - - bool did_vibrate_ = false; - float vibrate_anim_state_ = 0.f; - - vr::VRInputComponentHandle_t haptic_component_ = 0; - vr::VRInputComponentHandle_t system_click_component_ = 0; - vr::VRInputComponentHandle_t system_touch_component_ = 0; - }; -}; \ No newline at end of file +class TrackerDevice : public IVRDevice { +public: + TrackerDevice(std::string serial, int device_id, TrackerRole tracker_role); + ~TrackerDevice() = default; + void LogInput(const char *path, vr::VRInputComponentHandle_t componentHandle); + void LogInputError(vr::EVRInputError err, const char *path, + vr::VRInputComponentHandle_t componentHandle); + void LogInfo(const char *message); + // Inherited via IVRDevice + virtual std::string GetSerial() override; + virtual void Update() override; + virtual vr::TrackedDeviceIndex_t GetDeviceIndex() override; + virtual DeviceType GetDeviceType() override; + virtual int GetDeviceId() override; + virtual void SetDeviceId(int device_id) override; + virtual void PositionMessage(messages::Position &position) override; + virtual void + ControllerInputMessage(messages::ControllerInput &position) override; + virtual void StatusMessage(messages::TrackerStatus &status) override; + virtual void BatteryMessage(messages::Battery &battery) override; + const char *GetInputErrorName(vr::EVRInputError err); + + // Inherited via ITrackedDeviceServerDriver + virtual vr::EVRInitError Activate(uint32_t unObjectId) override; + virtual void Deactivate() override; + virtual void EnterStandby() override; + virtual void *GetComponent(const char *pchComponentNameAndVersion) override; + virtual void DebugRequest(const char *pchRequest, char *pchResponseBuffer, + uint32_t unResponseBufferSize) override; + virtual vr::DriverPose_t GetPose() override; + vr::HmdMatrix34_t ToHmdMatrix(const vr::DriverPose_t &pose); + +private: + std::ofstream input_log_; + std::shared_ptr logger_ = std::make_shared(); + + std::atomic device_index_ = + vr::k_unTrackedDeviceIndexInvalid; + std::string serial_; + vr::PropertyContainerHandle_t containerHandle_; + + int device_id_; + TrackerRole tracker_role_; + bool fingertracking_enabled_; + + vr::DriverPose_t last_pose_ = IVRDevice::MakeDefaultPose(); + std::atomic last_pose_atomic_ = + IVRDevice::MakeDefaultPose(); + std::optional smoothed_pose_; + bool last_frame_had_external_ = false; + static vr::DriverPose_t LerpPose(const vr::DriverPose_t &from, + const vr::DriverPose_t &to, float t); + + bool did_vibrate_ = false; + float vibrate_anim_state_ = 0.f; + bool was_activated_ = false; + vr::VRInputComponentHandle_t haptic_component_ = 0; + vr::VRInputComponentHandle_t double_tap_component_ = 0; + vr::VRInputComponentHandle_t triple_tap_component_ = 0; + + vr::VRInputComponentHandle_t ignored = 0; + vr::VRInputComponentHandle_t pose_component_handle_ = 0; + + vr::VRInputComponentHandle_t raw_pose_component_handle_ = 0; + vr::VRInputComponentHandle_t aim_pose_component_handle_ = 0; + + vr::VRInputComponentHandle_t trigger_component_ = 0; + vr::VRInputComponentHandle_t grip_value_component_ = 0; + vr::VRInputComponentHandle_t stick_x_component_ = 0; + vr::VRInputComponentHandle_t stick_y_component_ = 0; + vr::VRInputComponentHandle_t button_a_component_ = 0; + vr::VRInputComponentHandle_t button_b_component_ = 0; + + vr::VRInputComponentHandle_t trackpad_x_component_ = 0; + vr::VRInputComponentHandle_t trackpad_y_component_ = 0; + vr::VRInputComponentHandle_t trackpad_click_component_ = 0; + vr::VRInputComponentHandle_t trackpad_touch_component_ = 0; + + vr::VRInputComponentHandle_t stick_click_component_ = 0; + vr::VRInputComponentHandle_t system_component = 0; + vr::VRInputComponentHandle_t system_component_touch = 0; + + vr::VRInputComponentHandle_t trigger_component_click_ = 0; + vr::VRInputComponentHandle_t trigger_component_touch_ = 0; + + vr::VRInputComponentHandle_t grip_value_component_touch_ = 0; + vr::VRInputComponentHandle_t stick_x_component_touch_ = 0; + vr::VRInputComponentHandle_t stick_y_component_touch_ = 0; + vr::VRInputComponentHandle_t button_a_component_touch_ = 0; + vr::VRInputComponentHandle_t button_b_component_touch_ = 0; + vr::VRInputComponentHandle_t stick_click_component_touch_ = 0; + + bool is_controller_; + bool is_left_hand_; + bool is_right_hand_; + + vr::VRInputComponentHandle_t skeletal_component_handle_; + + float trigger_value_ = 0; + bool trigger_value_click = false; + float grip_value = 0; + float thumbstick_x_value = 0; + float thumbstick_y_value = 0; + bool button_1_value = false; + bool button_2_value = false; + bool stick_click_value = false; + bool system_value = false; + bool system_click_value = false; + const int protobuf_fingers_to_openvr[15] = { + 2, // THUMB_METACARPAL → eBone_Thumb1 + 3, // THUMB_PROXIMAL → eBone_Thumb2 + 4, // THUMB_DISTAL → eBone_Thumb3 + 6, // INDEX_PROXIMAL → eBone_IndexFinger1 + 7, // INDEX_INTERMEDIATE → eBone_IndexFinger2 + 8, // INDEX_DISTAL → eBone_IndexFinger3 + 11, // MIDDLE_PROXIMAL → eBone_MiddleFinger1 + 12, // MIDDLE_INTERMEDIATE → eBone_MiddleFinger2 + 13, // MIDDLE_DISTAL → eBone_MiddleFinger3 + 16, // RING_PROXIMAL → eBone_RingFinger1 + 17, // RING_INTERMEDIATE → eBone_RingFinger2 + 18, // RING_DISTAL → eBone_RingFinger3 + 21, // LITTLE_PROXIMAL → eBone_PinkyFinger1 + 22, // LITTLE_INTERMEDIATE → eBone_PinkyFinger2 + 23 // LITTLE_DISTAL → eBone_PinkyFinger3 + }; +}; +}; // namespace SlimeVRDriver \ No newline at end of file diff --git a/src/VRDriver.cpp b/src/VRDriver.cpp index 9d8dcd2b..e98f5e94 100644 --- a/src/VRDriver.cpp +++ b/src/VRDriver.cpp @@ -1,47 +1,57 @@ #include "VRDriver.hpp" -#include #include "TrackerRole.hpp" +#include "VRPaths_openvr.hpp" +#include +#include +#include #include #include -#include "VRPaths_openvr.hpp" +#include -vr::EVRInitError SlimeVRDriver::VRDriver::Init(vr::IVRDriverContext* pDriverContext) { - // Perform driver context initialisation - if (vr::EVRInitError init_error = vr::InitServerDriverContext(pDriverContext); init_error != vr::EVRInitError::VRInitError_None) { - return init_error; - } +vr::EVRInitError +SlimeVRDriver::VRDriver::Init(vr::IVRDriverContext *pDriverContext) { + // Perform driver context initialisation + if (vr::EVRInitError init_error = vr::InitServerDriverContext(pDriverContext); + init_error != vr::EVRInitError::VRInitError_None) { + return init_error; + } - logger_->Log("Activating SlimeVR Driver..."); + logger_->Log("Activating SlimeVR Driver..."); - try { - auto json = simdjson::padded_string::load(GetVRPathRegistryFilename()); // load VR Path Registry - simdjson::ondemand::document doc = json_parser_.iterate(json); - auto path = std::string { doc.get_object()["config"].at(0).get_string().value() }; - default_chap_path_ = GetDefaultChaperoneFromConfigPath(path); - } catch (simdjson::simdjson_error& e) { - logger_->Log("Error getting VR Config path, continuing (error code {})", std::to_string(e.error())); - } + try { + auto json = simdjson::padded_string::load( + GetVRPathRegistryFilename()); // load VR Path Registry + simdjson::ondemand::document doc = json_parser_.iterate(json); + auto path = + std::string{doc.get_object()["config"].at(0).get_string().value()}; + default_chap_path_ = GetDefaultChaperoneFromConfigPath(path); + } catch (simdjson::simdjson_error &e) { + logger_->Log("Error getting VR Config path, continuing (error code {})", + std::to_string(e.error())); + } + + logger_->Log("SlimeVR Driver Loaded Successfully"); - logger_->Log("SlimeVR Driver Loaded Successfully"); + LoadDriverConfig(); - bridge_ = std::make_shared( - std::static_pointer_cast(std::make_shared("Bridge")), - std::bind(&SlimeVRDriver::VRDriver::OnBridgeMessage, this, std::placeholders::_1) - ); - bridge_->Start(); + bridge_ = std::make_shared( + std::static_pointer_cast(std::make_shared("Bridge")), + std::bind(&SlimeVRDriver::VRDriver::OnBridgeMessage, this, + std::placeholders::_1)); + bridge_->Start(); - exiting_pose_request_thread_ = false; - pose_request_thread_ = - std::make_unique(&SlimeVRDriver::VRDriver::RunPoseRequestThread, this); + exiting_pose_request_thread_ = false; + pose_request_thread_ = std::make_unique( + &SlimeVRDriver::VRDriver::RunPoseRequestThread, this); - return vr::VRInitError_None; + return vr::VRInitError_None; } void SlimeVRDriver::VRDriver::Cleanup() { - exiting_pose_request_thread_ = true; - pose_request_thread_->join(); - pose_request_thread_.reset(); - bridge_->Stop(); + exiting_pose_request_thread_ = true; + pose_request_thread_->join(); + pose_request_thread_.reset(); + bridge_->Stop(); } void SlimeVRDriver::VRDriver::RunPoseRequestThread() { @@ -54,7 +64,7 @@ void SlimeVRDriver::VRDriver::RunPoseRequestThread() { continue; } - messages::ProtobufMessage* message = google::protobuf::Arena::CreateMessage(&arena_); + messages::ProtobufMessage* message = google::protobuf::Arena::Create(&arena_); vr::TrackedDevicePose_t hmd_pose; vr::VRServerDriverHost()->GetRawTrackedDevicePoses(0.0f, &hmd_pose, 1); @@ -82,7 +92,7 @@ void SlimeVRDriver::VRDriver::RunPoseRequestThread() { logger_->Log("HMD props: serial='{}', model='{}', manufacturer='{}'", serial, name, manufacturer); // Send add message for HMD - messages::TrackerAdded* tracker_added = google::protobuf::Arena::CreateMessage(&arena_); + messages::TrackerAdded* tracker_added = google::protobuf::Arena::Create(&arena_); message->set_allocated_tracker_added(tracker_added); tracker_added->set_tracker_id(0); tracker_added->set_tracker_role(TrackerRole::HMD); @@ -91,7 +101,7 @@ void SlimeVRDriver::VRDriver::RunPoseRequestThread() { tracker_added->set_manufacturer(manufacturer.empty() ? "OpenVR" : manufacturer); bridge_->SendBridgeMessage(*message); - messages::TrackerStatus* tracker_status = google::protobuf::Arena::CreateMessage(&arena_); + messages::TrackerStatus* tracker_status = google::protobuf::Arena::Create(&arena_); message->set_allocated_tracker_status(tracker_status); tracker_status->set_tracker_id(0); tracker_status->set_status(messages::TrackerStatus_Status::TrackerStatus_Status_OK); @@ -151,7 +161,7 @@ void SlimeVRDriver::VRDriver::RunPoseRequestThread() { pos.v[2] = pos_z; } - messages::Position* hmd_position = google::protobuf::Arena::CreateMessage(&arena_); + messages::Position* hmd_position = google::protobuf::Arena::Create(&arena_); message->set_allocated_position(hmd_position); hmd_position->set_tracker_id(0); hmd_position->set_data_source(messages::Position_DataSource_FULL); @@ -168,7 +178,7 @@ void SlimeVRDriver::VRDriver::RunPoseRequestThread() { if (std::chrono::duration_cast(now - battery_sent_at_).count() > 100) { vr::ETrackedPropertyError err; if (vr::VRProperties()->GetBoolProperty(hmd_prop_container, vr::Prop_DeviceProvidesBatteryStatus_Bool, &err)) { - messages::Battery* hmdBattery = google::protobuf::Arena::CreateMessage(&arena_); + messages::Battery* hmdBattery = google::protobuf::Arena::Create(&arena_); message->set_allocated_battery(hmdBattery); hmdBattery->set_tracker_id(0); hmdBattery->set_battery_level(vr::VRProperties()->GetFloatProperty(hmd_prop_container, vr::Prop_DeviceBatteryPercentage_Float, &err) * 100); @@ -186,83 +196,108 @@ void SlimeVRDriver::VRDriver::RunPoseRequestThread() { } void SlimeVRDriver::VRDriver::RunFrame() { - // Collect events - vr::VREvent_t event; - std::vector events; - while (vr::VRServerDriverHost()->PollNextEvent(&event, sizeof(event))) { - events.push_back(event); - } - openvr_events_ = std::move(events); - - // Update frame timing - std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); - frame_timing_ = std::chrono::duration_cast(now - last_frame_time_); - last_frame_time_ = now; - - // Update devices - { - std::lock_guard lock(devices_mutex_); - for (auto& device : devices_) { - device->Update(); + // Collect events + vr::VREvent_t event; + std::vector events; + while (vr::VRServerDriverHost()->PollNextEvent(&event, sizeof(event))) { + events.push_back(event); + + if (event.eventType == vr::EVREventType::VREvent_ButtonPress || + event.eventType == vr::EVREventType::VREvent_ButtonUnpress) { + uint64_t mask = 1ULL << event.data.controller.button; + if (event.trackedDeviceIndex != vr::k_unTrackedDeviceIndexInvalid) { + if (event.trackedDeviceIndex == external_left_index_) { + if (event.eventType == vr::EVREventType::VREvent_ButtonPress) external_left_buttons_ |= mask; + else external_left_buttons_ &= ~mask; + } else if (event.trackedDeviceIndex == external_right_index_) { + if (event.eventType == vr::EVREventType::VREvent_ButtonPress) external_right_buttons_ |= mask; + else external_right_buttons_ &= ~mask; } + } } -} + } + openvr_events_ = std::move(events); + + // Update frame timing + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + frame_timing_ = std::chrono::duration_cast( + now - last_frame_time_); + last_frame_time_ = now; -void SlimeVRDriver::VRDriver::OnBridgeMessage(const messages::ProtobufMessage& message) { + // Update external controller poses (e.g. Virtual Desktop / Steam Link on Quest) so we can prefer their hand position when in view and fall back to SlimeVR when not. + { std::lock_guard lock(devices_mutex_); - if (message.has_tracker_added()) { - messages::TrackerAdded ta = message.tracker_added(); - switch(GetDeviceType(static_cast(ta.tracker_role()))) { - case DeviceType::TRACKER: - case DeviceType::CONTROLLER: - AddDevice(std::make_shared(ta.tracker_serial(), ta.tracker_id(), static_cast(ta.tracker_role()))); - break; - } - } else if (message.has_position()) { - messages::Position pos = message.position(); - auto device = devices_by_id_.find(pos.tracker_id()); - if (device != devices_by_id_.end()) { - device->second->PositionMessage(pos); - } - } else if (message.has_tracker_status()) { - messages::TrackerStatus status = message.tracker_status(); - auto device = devices_by_id_.find(status.tracker_id()); - if (device != devices_by_id_.end()) { - device->second->StatusMessage(status); - static const std::unordered_map status_map = { - { messages::TrackerStatus_Status_OK, "OK" }, - { messages::TrackerStatus_Status_DISCONNECTED, "DISCONNECTED" }, - { messages::TrackerStatus_Status_ERROR, "ERROR" }, - { messages::TrackerStatus_Status_BUSY, "BUSY" }, - }; - if (status_map.count(status.status())) { - logger_->Log("Tracker status id {} status {}", status.tracker_id(), status_map.at(status.status())); - } - } - } else if (message.has_battery()) { - messages::Battery bat = message.battery(); - auto device = this->devices_by_id_.find(bat.tracker_id()); - if (device != this->devices_by_id_.end()) { - device->second->BatteryMessage(bat); - } + UpdateExternalControllerPoses(); + } + + // Update devices + { + std::lock_guard lock(devices_mutex_); + for (auto &device : devices_) { + device->Update(); } + } } -bool SlimeVRDriver::VRDriver::ShouldBlockStandbyMode() { - return false; +void SlimeVRDriver::VRDriver::OnBridgeMessage( + const messages::ProtobufMessage &message) { + std::lock_guard lock(devices_mutex_); + if (message.has_tracker_added()) { + messages::TrackerAdded ta = message.tracker_added(); + AddDevice(std::make_shared( + ta.tracker_serial(), ta.tracker_id(), + static_cast(ta.tracker_role()))); + } else if (message.has_position()) { + messages::Position pos = message.position(); + auto device = devices_by_id_.find(pos.tracker_id()); + if (device != devices_by_id_.end()) { + device->second->PositionMessage(pos); + } + } else if (message.has_controller_input()) { + messages::ControllerInput controllerInput = message.controller_input(); + auto device = devices_by_id_.find(controllerInput.tracker_id()); + if (device != devices_by_id_.end()) { + device->second->ControllerInputMessage(controllerInput); + } + } else if (message.has_tracker_status()) { + messages::TrackerStatus status = message.tracker_status(); + auto device = devices_by_id_.find(status.tracker_id()); + if (device != devices_by_id_.end()) { + device->second->StatusMessage(status); + static const std::unordered_map + status_map = { + {messages::TrackerStatus_Status_OK, "OK"}, + {messages::TrackerStatus_Status_DISCONNECTED, "DISCONNECTED"}, + {messages::TrackerStatus_Status_ERROR, "ERROR"}, + {messages::TrackerStatus_Status_BUSY, "BUSY"}, + }; + if (status_map.count(status.status())) { + logger_->Log("Tracker status id {} status {}", status.tracker_id(), + status_map.at(status.status())); + } + } + } else if (message.has_battery()) { + messages::Battery bat = message.battery(); + auto device = this->devices_by_id_.find(bat.tracker_id()); + if (device != this->devices_by_id_.end()) { + device->second->BatteryMessage(bat); + } + } } -void SlimeVRDriver::VRDriver::EnterStandby() { -} +bool SlimeVRDriver::VRDriver::ShouldBlockStandbyMode() { return false; } -void SlimeVRDriver::VRDriver::LeaveStandby() { -} +void SlimeVRDriver::VRDriver::EnterStandby() {} -std::vector> SlimeVRDriver::VRDriver::GetDevices() { - std::lock_guard lock(devices_mutex_); - std::vector> devices; - devices.assign(devices.begin(), devices.end()); - return devices; +void SlimeVRDriver::VRDriver::LeaveStandby() {} + +std::vector> +SlimeVRDriver::VRDriver::GetDevices() { + std::lock_guard lock(devices_mutex_); + std::vector> devices; + devices.assign(devices.begin(), devices.end()); + return devices; } const std::vector& SlimeVRDriver::VRDriver::GetOpenVREvents() { @@ -270,157 +305,230 @@ const std::vector& SlimeVRDriver::VRDriver::GetOpenVREvents() { } std::chrono::milliseconds SlimeVRDriver::VRDriver::GetLastFrameTime() { - return frame_timing_; + return frame_timing_; } bool SlimeVRDriver::VRDriver::AddDevice(std::shared_ptr device) { - vr::ETrackedDeviceClass openvr_device_class; - // Remember to update this switch when new device types are added - switch (device->GetDeviceType()) { - case DeviceType::CONTROLLER: - openvr_device_class = vr::ETrackedDeviceClass::TrackedDeviceClass_Controller; - break; - case DeviceType::HMD: - openvr_device_class = vr::ETrackedDeviceClass::TrackedDeviceClass_HMD; - break; - case DeviceType::TRACKER: - openvr_device_class = vr::ETrackedDeviceClass::TrackedDeviceClass_GenericTracker; - break; - case DeviceType::TRACKING_REFERENCE: - openvr_device_class = vr::ETrackedDeviceClass::TrackedDeviceClass_TrackingReference; - break; - default: - return false; + vr::ETrackedDeviceClass openvr_device_class; + // Remember to update this switch when new device types are added + switch (device->GetDeviceType()) { + case DeviceType::CONTROLLER: + openvr_device_class = + vr::ETrackedDeviceClass::TrackedDeviceClass_Controller; + break; + case DeviceType::HMD: + openvr_device_class = vr::ETrackedDeviceClass::TrackedDeviceClass_HMD; + break; + case DeviceType::TRACKER: + openvr_device_class = + vr::ETrackedDeviceClass::TrackedDeviceClass_GenericTracker; + break; + case DeviceType::TRACKING_REFERENCE: + openvr_device_class = + vr::ETrackedDeviceClass::TrackedDeviceClass_TrackingReference; + break; + default: + return false; + } + if (!devices_by_serial_.count(device->GetSerial())) { + bool result = vr::VRServerDriverHost()->TrackedDeviceAdded( + device->GetSerial().c_str(), openvr_device_class, device.get()); + if (result) { + devices_.push_back(device); + devices_by_id_[device->GetDeviceId()] = device; + devices_by_serial_[device->GetSerial()] = device; + logger_->Log("New tracker device added {} (id {})", device->GetSerial(), + device->GetDeviceId()); + } else { + logger_->Log("Failed to add tracker device {} (id {})", + device->GetSerial(), device->GetDeviceId()); + return false; } - if (!devices_by_serial_.count(device->GetSerial())) { - bool result = vr::VRServerDriverHost()->TrackedDeviceAdded(device->GetSerial().c_str(), openvr_device_class, device.get()); - if (result) { - devices_.push_back(device); - devices_by_id_[device->GetDeviceId()] = device; - devices_by_serial_[device->GetSerial()] = device; - logger_->Log("New tracker device added {} (id {})", device->GetSerial(), device->GetDeviceId()); - } else { - logger_->Log("Failed to add tracker device {} (id {})", device->GetSerial(), device->GetDeviceId()); - return false; - } + } else { + std::shared_ptr oldDevice = + devices_by_serial_[device->GetSerial()]; + if (oldDevice->GetDeviceId() != device->GetDeviceId()) { + devices_by_id_[device->GetDeviceId()] = oldDevice; + oldDevice->SetDeviceId(device->GetDeviceId()); + logger_->Log("Device overridden from id {} to {} for serial {}", + oldDevice->GetDeviceId(), device->GetDeviceId(), + device->GetSerial()); } else { - std::shared_ptr oldDevice = devices_by_serial_[device->GetSerial()]; - if (oldDevice->GetDeviceId() != device->GetDeviceId()) { - devices_by_id_[device->GetDeviceId()] = oldDevice; - oldDevice->SetDeviceId(device->GetDeviceId()); - logger_->Log("Device overridden from id {} to {} for serial {}", oldDevice->GetDeviceId(), device->GetDeviceId(), device->GetSerial()); - } else { - logger_->Log("Device readded id {}, serial {}", device->GetDeviceId(), device->GetSerial()); - } + logger_->Log("Device readded id {}, serial {}", device->GetDeviceId(), + device->GetSerial()); } - return true; + } + return true; } -SlimeVRDriver::SettingsValue SlimeVRDriver::VRDriver::GetSettingsValue(std::string key) { - vr::EVRSettingsError err = vr::EVRSettingsError::VRSettingsError_None; - int int_value = vr::VRSettings()->GetInt32(settings_key_.c_str(), key.c_str(), &err); - if (err == vr::EVRSettingsError::VRSettingsError_None) { - return int_value; - } - err = vr::EVRSettingsError::VRSettingsError_None; - float float_value = vr::VRSettings()->GetFloat(settings_key_.c_str(), key.c_str(), &err); - if (err == vr::EVRSettingsError::VRSettingsError_None) { - return float_value; - } - err = vr::EVRSettingsError::VRSettingsError_None; - bool bool_value = vr::VRSettings()->GetBool(settings_key_.c_str(), key.c_str(), &err); - if (err == vr::EVRSettingsError::VRSettingsError_None) { - return bool_value; - } - std::string str_value; - str_value.reserve(1024); - vr::VRSettings()->GetString(settings_key_.c_str(), key.c_str(), str_value.data(), 1024, &err); - if (err == vr::EVRSettingsError::VRSettingsError_None) { - return str_value; - } - err = vr::EVRSettingsError::VRSettingsError_None; +void SlimeVRDriver::VRDriver::LoadDriverConfig() { + vr::IVRResources *res = vr::VRResources(); + if (!res) return; + char path[1024]; + uint32_t len = res->GetResourceFullPath( + "slimevr_driver_config.json", "", path, sizeof(path)); + if (len == 0 || len >= sizeof(path)) return; + path[len] = '\0'; + try { + auto json = simdjson::padded_string::load(path); + simdjson::ondemand::document doc = json_parser_.iterate(json); + auto obj = doc.get_object(); + if (auto v = obj["external_hand_max_radius_m"]; !v.error()) config_external_hand_max_radius_m_ = static_cast(v.get_double()); + if (auto v = obj["pose_lerp_speed"]; !v.error()) config_pose_lerp_speed_ = static_cast(v.get_double()); + if (auto v = obj["pose_lerp_speed_on_swap"]; !v.error()) config_pose_lerp_speed_on_swap_ = static_cast(v.get_double()); + if (auto v = obj["controller_priority"]; !v.error()) config_controller_priority_ = static_cast(v.get_int64()); + if (auto v = obj["input_passthrough"]; !v.error()) { + bool val = false; + if (v.get_bool().get(val) == simdjson::SUCCESS) { + config_input_passthrough_ = val; + } + } + logger_->Log("Loaded driver config from {}", path); + } catch (const simdjson::simdjson_error &) { + // Use defaults; config file missing or invalid + } +} + +float SlimeVRDriver::VRDriver::GetPoseLerpSpeed() { + return config_pose_lerp_speed_; +} + +float SlimeVRDriver::VRDriver::GetPoseLerpSpeedOnSwap() { + return config_pose_lerp_speed_on_swap_; +} - return SettingsValue(); +int SlimeVRDriver::VRDriver::GetControllerPriority() { + return config_controller_priority_; } -vr::IVRDriverInput* SlimeVRDriver::VRDriver::GetInput() { - return vr::VRDriverInput(); +bool SlimeVRDriver::VRDriver::GetInputPassthrough() { + return config_input_passthrough_; } -vr::CVRPropertyHelpers* SlimeVRDriver::VRDriver::GetProperties() { - return vr::VRProperties(); +SlimeVRDriver::SettingsValue +SlimeVRDriver::VRDriver::GetSettingsValue(std::string key) { + vr::EVRSettingsError err = vr::EVRSettingsError::VRSettingsError_None; + int int_value = + vr::VRSettings()->GetInt32(settings_key_.c_str(), key.c_str(), &err); + if (err == vr::EVRSettingsError::VRSettingsError_None) { + return int_value; + } + err = vr::EVRSettingsError::VRSettingsError_None; + float float_value = + vr::VRSettings()->GetFloat(settings_key_.c_str(), key.c_str(), &err); + if (err == vr::EVRSettingsError::VRSettingsError_None) { + return float_value; + } + err = vr::EVRSettingsError::VRSettingsError_None; + bool bool_value = + vr::VRSettings()->GetBool(settings_key_.c_str(), key.c_str(), &err); + if (err == vr::EVRSettingsError::VRSettingsError_None) { + return bool_value; + } + std::string str_value; + str_value.reserve(1024); + vr::VRSettings()->GetString(settings_key_.c_str(), key.c_str(), + str_value.data(), 1024, &err); + if (err == vr::EVRSettingsError::VRSettingsError_None) { + return str_value; + } + err = vr::EVRSettingsError::VRSettingsError_None; + + return SettingsValue(); } -vr::IVRServerDriverHost* SlimeVRDriver::VRDriver::GetDriverHost() { - return vr::VRServerDriverHost(); +vr::IVRDriverInput *SlimeVRDriver::VRDriver::GetInput() { + return vr::VRDriverInput(); +} + +vr::CVRPropertyHelpers *SlimeVRDriver::VRDriver::GetProperties() { + return vr::VRProperties(); +} + +vr::IVRServerDriverHost *SlimeVRDriver::VRDriver::GetDriverHost() { + return vr::VRServerDriverHost(); } //----------------------------------------------------------------------------- -// Purpose: Calculates quaternion (qw,qx,qy,qz) representing the rotation -// from: https://github.com/Omnifinity/OpenVR-Tracking-Example/blob/master/HTC%20Lighthouse%20Tracking%20Example/LighthouseTracking.cpp +// Purpose: Calculates quaternion (qw,qx,qy,qz) representing the rotation. From: https://github.com/Omnifinity/OpenVR-Tracking-Example/blob/master/HTC%20Lighthouse%20Tracking%20Example/LighthouseTracking.cpp //----------------------------------------------------------------------------- -vr::HmdQuaternion_t SlimeVRDriver::VRDriver::GetRotation(vr::HmdMatrix34_t &matrix) { - vr::HmdQuaternion_t q; - - q.w = sqrt(fmax(0, 1 + matrix.m[0][0] + matrix.m[1][1] + matrix.m[2][2])) / 2; - q.x = sqrt(fmax(0, 1 + matrix.m[0][0] - matrix.m[1][1] - matrix.m[2][2])) / 2; - q.y = sqrt(fmax(0, 1 - matrix.m[0][0] + matrix.m[1][1] - matrix.m[2][2])) / 2; - q.z = sqrt(fmax(0, 1 - matrix.m[0][0] - matrix.m[1][1] + matrix.m[2][2])) / 2; - q.x = copysign(q.x, matrix.m[2][1] - matrix.m[1][2]); - q.y = copysign(q.y, matrix.m[0][2] - matrix.m[2][0]); - q.z = copysign(q.z, matrix.m[1][0] - matrix.m[0][1]); - return q; +vr::HmdQuaternion_t +SlimeVRDriver::VRDriver::GetRotation(vr::HmdMatrix34_t &matrix) { + vr::HmdQuaternion_t q; + + q.w = sqrt(fmax(0, 1 + matrix.m[0][0] + matrix.m[1][1] + matrix.m[2][2])) / 2; + q.x = sqrt(fmax(0, 1 + matrix.m[0][0] - matrix.m[1][1] - matrix.m[2][2])) / 2; + q.y = sqrt(fmax(0, 1 - matrix.m[0][0] + matrix.m[1][1] - matrix.m[2][2])) / 2; + q.z = sqrt(fmax(0, 1 - matrix.m[0][0] - matrix.m[1][1] + matrix.m[2][2])) / 2; + q.x = copysign(q.x, matrix.m[2][1] - matrix.m[1][2]); + q.y = copysign(q.y, matrix.m[0][2] - matrix.m[2][0]); + q.z = copysign(q.z, matrix.m[1][0] - matrix.m[0][1]); + return q; } //----------------------------------------------------------------------------- // Purpose: Extracts position (x,y,z). -// from: https://github.com/Omnifinity/OpenVR-Tracking-Example/blob/master/HTC%20Lighthouse%20Tracking%20Example/LighthouseTracking.cpp +// from: +// https://github.com/Omnifinity/OpenVR-Tracking-Example/blob/master/HTC%20Lighthouse%20Tracking%20Example/LighthouseTracking.cpp //----------------------------------------------------------------------------- -vr::HmdVector3_t SlimeVRDriver::VRDriver::GetPosition(vr::HmdMatrix34_t &matrix) { - vr::HmdVector3_t vector; +vr::HmdVector3_t +SlimeVRDriver::VRDriver::GetPosition(vr::HmdMatrix34_t &matrix) { + vr::HmdVector3_t vector; - vector.v[0] = matrix.m[0][3]; - vector.v[1] = matrix.m[1][3]; - vector.v[2] = matrix.m[2][3]; + vector.v[0] = matrix.m[0][3]; + vector.v[1] = matrix.m[1][3]; + vector.v[2] = matrix.m[2][3]; - return vector; + return vector; } -SlimeVRDriver::UniverseTranslation SlimeVRDriver::UniverseTranslation::parse(simdjson::ondemand::object &obj) { - SlimeVRDriver::UniverseTranslation res; - int iii = 0; - for (auto component: obj["translation"]) { - if (iii > 2) { - break; // TODO: 4 components in a translation vector? should this be an error? - } - res.translation.v[iii] = static_cast(component.get_double()); - iii += 1; +SlimeVRDriver::UniverseTranslation +SlimeVRDriver::UniverseTranslation::parse(simdjson::ondemand::object &obj) { + SlimeVRDriver::UniverseTranslation res; + int iii = 0; + for (auto component : obj["translation"]) { + if (iii > 2) { + break; // TODO: 4 components in a translation vector? should this be an error? } - res.yaw = static_cast(obj["yaw"].get_double()); + res.translation.v[iii] = static_cast(component.get_double()); + iii += 1; + } + res.yaw = static_cast(obj["yaw"].get_double()); - return res; + return res; } -std::optional SlimeVRDriver::VRDriver::SearchUniverse(const simdjson::padded_string &json, uint64_t target) { - simdjson::ondemand::document doc = json_parser_.iterate(json); - - for (simdjson::ondemand::object uni: doc["universes"]) { - // TODO: universeID comes after the translation, would it be faster to unconditionally parse the translation? - auto elem = uni["universeID"]; - uint64_t parsed_universe; - - auto is_integer = elem.is_integer(); - if (!is_integer.error() && is_integer.value_unsafe()) { - parsed_universe = elem.get_uint64(); +std::optional SlimeVRDriver::VRDriver::SearchUniverse(std::string path_or_json, uint64_t target) { + try { + simdjson::padded_string json; + if (path_or_json.find('{') != std::string::npos) { + json = simdjson::padded_string(path_or_json); } else { - parsed_universe = elem.get_uint64_in_string(); + json = simdjson::padded_string::load(path_or_json); } + simdjson::ondemand::document doc = json_parser_.iterate(json); - if (parsed_universe == target) { - auto standing_uni = uni["standing"].get_object(); - return SlimeVRDriver::UniverseTranslation::parse(standing_uni.value()); + for (simdjson::ondemand::object uni : doc["universes"]) { + // TODO: universeID comes after the translation, would it be faster to unconditionally parse the translation? + auto elem = uni["universeID"]; + uint64_t parsed_universe; + + auto is_integer = elem.is_integer(); + if (!is_integer.error() && is_integer.value_unsafe()) { + parsed_universe = elem.get_uint64(); + } else { + parsed_universe = elem.get_uint64_in_string(); + } + + if (parsed_universe == target) { + auto standing_uni = uni["standing"].get_object(); + return SlimeVRDriver::UniverseTranslation::parse(standing_uni.value()); + } } + } catch (simdjson::simdjson_error &e) { + logger_->Log("Error getting universes from {}: {}", path_or_json, e.what()); + return std::nullopt; } return std::nullopt; @@ -444,7 +552,7 @@ std::optional SlimeVRDriver::VRDriver::Searc auto driver_chap_path = vr::VRProperties()->GetStringProperty(hmd_prop_container, vr::Prop_DriverProvidedChaperonePath_String); if (driver_chap_path != "") { try { - auto driver_res = SearchUniverse(simdjson::padded_string::load(driver_chap_path).take_value(), target); + auto driver_res = SearchUniverse(driver_chap_path, target); if (driver_res.has_value()) { return driver_res.value(); } @@ -456,7 +564,7 @@ std::optional SlimeVRDriver::VRDriver::Searc if (default_chap_path_.has_value() && std::filesystem::exists(default_chap_path_.value())) { try { - return SearchUniverse(simdjson::padded_string::load(default_chap_path_.value()).take_value(), target); + return SearchUniverse(default_chap_path_.value(), target); } catch (simdjson::simdjson_error &e) { logger_->Log("Error loading chaperone from default path {}: {}", default_chap_path_.value(), e.what()); @@ -466,10 +574,152 @@ std::optional SlimeVRDriver::VRDriver::Searc return std::nullopt; } -std::optional SlimeVRDriver::VRDriver::GetCurrentUniverse() { - if (current_universe_.has_value()) { - return current_universe_.value().second; +std::optional +SlimeVRDriver::VRDriver::GetCurrentUniverse() { + if (current_universe_.has_value()) { + return current_universe_.value().second; + } + + return std::nullopt; +} + +std::optional +SlimeVRDriver::VRDriver::GetExternalPoseForHand(bool left_hand) { + if (left_hand && external_left_pose_.has_value()) { + return external_left_pose_; + } + if (!left_hand && external_right_pose_.has_value()) { + return external_right_pose_; + } + return std::nullopt; +} + +vr::DriverPose_t SlimeVRDriver::VRDriver::DriverPoseFromTrackedDevicePose( + const vr::TrackedDevicePose_t &raw) { + vr::DriverPose_t pose = + IVRDevice::MakeDefaultPose(raw.bDeviceIsConnected, raw.bPoseIsValid); + pose.poseTimeOffset = 0.0; + pose.result = raw.eTrackingResult; + + vr::HmdMatrix34_t m = raw.mDeviceToAbsoluteTracking; + pose.qRotation = GetRotation(m); + vr::HmdVector3_t pos = GetPosition(m); + pose.vecPosition[0] = pos.v[0]; + pose.vecPosition[1] = pos.v[1]; + pose.vecPosition[2] = pos.v[2]; + + pose.vecVelocity[0] = raw.vVelocity.v[0]; + pose.vecVelocity[1] = raw.vVelocity.v[1]; + pose.vecVelocity[2] = raw.vVelocity.v[2]; + pose.vecAngularVelocity[0] = raw.vAngularVelocity.v[0]; + pose.vecAngularVelocity[1] = raw.vAngularVelocity.v[1]; + pose.vecAngularVelocity[2] = raw.vAngularVelocity.v[2]; + + // External poses are already in tracking space; use identity + // world-from-driver + pose.qWorldFromDriverRotation.w = 1.0; + pose.qWorldFromDriverRotation.x = 0.0; + pose.qWorldFromDriverRotation.y = 0.0; + pose.qWorldFromDriverRotation.z = 0.0; + pose.vecWorldFromDriverTranslation[0] = 0.0; + pose.vecWorldFromDriverTranslation[1] = 0.0; + pose.vecWorldFromDriverTranslation[2] = 0.0; + pose.qDriverFromHeadRotation.w = 1.0; + pose.qDriverFromHeadRotation.x = 0.0; + pose.qDriverFromHeadRotation.y = 0.0; + pose.qDriverFromHeadRotation.z = 0.0; + pose.vecDriverFromHeadTranslation[0] = 0.0; + pose.vecDriverFromHeadTranslation[1] = 0.0; + pose.vecDriverFromHeadTranslation[2] = 0.0; + + return pose; +} + +bool SlimeVRDriver::VRDriver::ExternalHandInFrontAndInRadius( + const double hand_pos[3], const vr::TrackedDevicePose_t &hmd_pose) const { + if (!hmd_pose.bPoseIsValid) + return false; + double r = static_cast(config_external_hand_max_radius_m_); + const auto &m = hmd_pose.mDeviceToAbsoluteTracking.m; + double hx = m[0][3], hy = m[1][3], hz = m[2][3]; + double dx = hand_pos[0] - hx, dy = hand_pos[1] - hy, dz = hand_pos[2] - hz; + double dist_sq = dx * dx + dy * dy + dz * dz; + if (dist_sq > r * r) + return false; + // Forward = -Z in OpenVR (third column of rotation) + double fx = -m[0][2], fy = -m[1][2], fz = -m[2][2]; + if (dx * fx + dy * fy + dz * fz < 0.0) + return false; // behind HMD + return true; +} + +void SlimeVRDriver::VRDriver::UpdateExternalControllerPoses() { + external_left_index_ = vr::k_unTrackedDeviceIndexInvalid; + external_right_index_ = vr::k_unTrackedDeviceIndexInvalid; + + vr::TrackedDevicePose_t raw_poses[vr::k_unMaxTrackedDeviceCount]; + vr::VRServerDriverHost()->GetRawTrackedDevicePoses( + 0.0f, raw_poses, vr::k_unMaxTrackedDeviceCount); + + std::unordered_set our_indices; + for (const auto &device : devices_) { + vr::TrackedDeviceIndex_t idx = device->GetDeviceIndex(); + if (idx != vr::k_unTrackedDeviceIndexInvalid) { + our_indices.insert(idx); + } + } + + const vr::TrackedDevicePose_t &hmd_pose = raw_poses[0]; + auto *props = GetProperties(); + for (uint32_t i = 1; i < vr::k_unMaxTrackedDeviceCount; i++) { + if (our_indices.count(i)) + continue; + const vr::TrackedDevicePose_t &p = raw_poses[i]; + + vr::PropertyContainerHandle_t container = + props->TrackedDeviceToPropertyContainer(i); + vr::ETrackedPropertyError err = + vr::ETrackedPropertyError::TrackedProp_Success; + int32_t device_class = + props->GetInt32Property(container, vr::Prop_DeviceClass_Int32, &err); + if (err != vr::ETrackedPropertyError::TrackedProp_Success || + device_class != vr::TrackedDeviceClass_Controller) { + continue; + } + int32_t role = props->GetInt32Property( + container, vr::Prop_ControllerRoleHint_Int32, &err); + if (err != vr::ETrackedPropertyError::TrackedProp_Success) + continue; + + // Never use our own driver's controllers as "external". + char manufacturer[256] = {}; + props->GetStringProperty(container, vr::Prop_ManufacturerName_String, + manufacturer, sizeof(manufacturer), &err); + if (err == vr::ETrackedPropertyError::TrackedProp_Success && + std::strcmp(manufacturer, "SlimeVR") == 0) { + continue; } - return std::nullopt; + vr::DriverPose_t driver_pose = DriverPoseFromTrackedDevicePose(p); + // Only use external hand when it's in front of HMD and within 170 cm radius; else SlimeVR. + if (!ExternalHandInFrontAndInRadius(driver_pose.vecPosition, hmd_pose)) { + if (role == vr::TrackedControllerRole_LeftHand) { + external_left_pose_ = std::nullopt; + } else if (role == vr::TrackedControllerRole_RightHand) { + external_right_pose_ = std::nullopt; + } + continue; + } + if (role == vr::TrackedControllerRole_LeftHand) { + external_left_index_ = i; + external_left_pose_ = driver_pose; + } else if (role == vr::TrackedControllerRole_RightHand) { + external_right_index_ = i; + external_right_pose_ = driver_pose; + } + } +} + +uint64_t SlimeVRDriver::VRDriver::GetExternalButtonsForHand(bool left_hand) { + return left_hand ? external_left_buttons_ : external_right_buttons_; } diff --git a/src/VRDriver.hpp b/src/VRDriver.hpp index a0a6d824..eb184fa0 100644 --- a/src/VRDriver.hpp +++ b/src/VRDriver.hpp @@ -1,19 +1,19 @@ #pragma once #define NOMINMAX -#include #include #include +#include #include -#include #include +#include #include -#include "bridge/BridgeClient.hpp" #include "Logger.hpp" +#include "bridge/BridgeClient.hpp" namespace SlimeVRDriver { class VRDriver : public IVRDriver { @@ -25,53 +25,86 @@ namespace SlimeVRDriver { virtual bool AddDevice(std::shared_ptr device) override; virtual SettingsValue GetSettingsValue(std::string key) override; - virtual vr::IVRDriverInput* GetInput() override; - virtual vr::CVRPropertyHelpers* GetProperties() override; - virtual vr::IVRServerDriverHost* GetDriverHost() override; - - // Inherited via IServerTrackedDeviceProvider - virtual vr::EVRInitError Init(vr::IVRDriverContext* pDriverContext) override; - virtual void Cleanup() override; - virtual void RunFrame() override; - virtual bool ShouldBlockStandbyMode() override; - virtual void EnterStandby() override; - virtual void LeaveStandby() override; - virtual ~VRDriver() = default; - - virtual std::optional GetCurrentUniverse() override; - - void OnBridgeMessage(const messages::ProtobufMessage& message); - void RunPoseRequestThread(); - - private: - std::unique_ptr pose_request_thread_ = nullptr; - std::atomic exiting_pose_request_thread_ = false; - - std::shared_ptr bridge_ = nullptr; - google::protobuf::Arena arena_; - std::shared_ptr logger_ = std::make_shared(); - std::mutex devices_mutex_; - std::vector> devices_; - std::vector openvr_events_; - std::map> devices_by_id_; - std::map> devices_by_serial_; - std::chrono::milliseconds frame_timing_ = std::chrono::milliseconds(16); - std::chrono::steady_clock::time_point last_frame_time_ = std::chrono::steady_clock::now(); - std::chrono::steady_clock::time_point battery_sent_at_ = std::chrono::steady_clock::now(); - std::string settings_key_ = "driver_slimevr"; - - vr::HmdQuaternion_t GetRotation(vr::HmdMatrix34_t &matrix); - vr::HmdVector3_t GetPosition(vr::HmdMatrix34_t &matrix); - - bool sent_hmd_add_message_ = false; - - simdjson::ondemand::parser json_parser_; - std::optional default_chap_path_ = std::nullopt; - //std::map universes; - - vr::ETrackedPropertyError last_universe_error_; - std::optional> current_universe_ = std::nullopt; - std::optional SearchUniverse(const simdjson::padded_string &json, uint64_t target); - std::optional SearchUniverses(uint64_t target); - }; -}; \ No newline at end of file + virtual vr::IVRDriverInput *GetInput() override; + virtual vr::CVRPropertyHelpers *GetProperties() override; + virtual vr::IVRServerDriverHost *GetDriverHost() override; + + // Inherited via IServerTrackedDeviceProvider + virtual vr::EVRInitError Init(vr::IVRDriverContext *pDriverContext) override; + virtual void Cleanup() override; + virtual void RunFrame() override; + virtual bool ShouldBlockStandbyMode() override; + virtual void EnterStandby() override; + virtual void LeaveStandby() override; + virtual ~VRDriver() = default; + + virtual std::optional GetCurrentUniverse() override; + + virtual std::optional + GetExternalPoseForHand(bool left_hand) override; + + void OnBridgeMessage(const messages::ProtobufMessage &message); + void RunPoseRequestThread(); + +private: + std::unique_ptr pose_request_thread_ = nullptr; + std::atomic exiting_pose_request_thread_ = false; + + std::shared_ptr bridge_ = nullptr; + google::protobuf::Arena arena_; + std::shared_ptr logger_ = std::make_shared(); + std::mutex devices_mutex_; + std::vector> devices_; + std::vector openvr_events_; + std::map> devices_by_id_; + std::map> devices_by_serial_; + std::chrono::milliseconds frame_timing_ = std::chrono::milliseconds(16); + std::chrono::steady_clock::time_point last_frame_time_ = + std::chrono::steady_clock::now(); + std::chrono::steady_clock::time_point battery_sent_at_ = + std::chrono::steady_clock::now(); + std::string settings_key_ = "driver_slimevr"; + + static vr::HmdQuaternion_t GetRotation(vr::HmdMatrix34_t &matrix); + static vr::HmdVector3_t GetPosition(vr::HmdMatrix34_t &matrix); + + bool sent_hmd_add_message_ = false; + + simdjson::ondemand::parser json_parser_; + std::optional default_chap_path_ = std::nullopt; + // std::map universes; + + vr::ETrackedPropertyError last_universe_error_; + std::optional> current_universe_ = + std::nullopt; + std::optional SearchUniverse(std::string path, + uint64_t target); + std::optional SearchUniverses(uint64_t target); + + std::optional external_left_pose_; + std::optional external_right_pose_; + // Values from slimevr_driver_config.json (fallbacks if file missing) + float config_external_hand_max_radius_m_ = 1.7f; + float config_pose_lerp_speed_ = 0.8f; + float config_pose_lerp_speed_on_swap_ = 0.25f; + int config_controller_priority_ = 2147483647; + bool config_input_passthrough_ = false; + void LoadDriverConfig(); + void UpdateExternalControllerPoses(); + static vr::DriverPose_t + DriverPoseFromTrackedDevicePose(const vr::TrackedDevicePose_t &raw); + bool ExternalHandInFrontAndInRadius(const double hand_pos[3], + const vr::TrackedDevicePose_t &hmd_pose) const; + virtual float GetPoseLerpSpeed() override; + virtual float GetPoseLerpSpeedOnSwap() override; + virtual int GetControllerPriority() override; + virtual bool GetInputPassthrough() override; + virtual uint64_t GetExternalButtonsForHand(bool left_hand) override; + + // External controller event spying + vr::TrackedDeviceIndex_t external_left_index_ = vr::k_unTrackedDeviceIndexInvalid; + vr::TrackedDeviceIndex_t external_right_index_ = vr::k_unTrackedDeviceIndexInvalid; + uint64_t external_left_buttons_ = 0; + uint64_t external_right_buttons_ = 0; +}; +}; // namespace SlimeVRDriver diff --git a/src/bridge/BridgeClient.cpp b/src/bridge/BridgeClient.cpp index 716bcda1..fa141a71 100644 --- a/src/bridge/BridgeClient.cpp +++ b/src/bridge/BridgeClient.cpp @@ -82,8 +82,8 @@ void BridgeClient::CloseConnectionHandles() { } void BridgeClient::SendVersion() { - messages::ProtobufMessage* message = google::protobuf::Arena::CreateMessage(&arena_); - messages::Version* version = google::protobuf::Arena::CreateMessage(&arena_); + messages::ProtobufMessage* message = google::protobuf::Arena::Create(&arena_); + messages::Version* version = google::protobuf::Arena::Create(&arena_); message->set_allocated_version(version); version->set_protocol_version(PROTOCOL_VERSION); SendBridgeMessage(*message); diff --git a/src/bridge/ProtobufMessages.proto b/src/bridge/ProtobufMessages.proto index 30d07d04..ad67b782 100644 --- a/src/bridge/ProtobufMessages.proto +++ b/src/bridge/ProtobufMessages.proto @@ -58,6 +58,47 @@ message Version { int32 protocol_version = 1; } +message FingerBoneRotation { + enum FingerBoneName { + THUMB_METACARPAL = 0; + THUMB_PROXIMAL = 1; + THUMB_DISTAL = 2; + INDEX_PROXIMAL = 3; + INDEX_INTERMEDIATE = 4; + INDEX_DISTAL = 5; + MIDDLE_PROXIMAL = 6; + MIDDLE_INTERMEDIATE = 7; + MIDDLE_DISTAL = 8; + RING_PROXIMAL = 9; + RING_INTERMEDIATE = 10; + RING_DISTAL = 11; + LITTLE_PROXIMAL = 12; + LITTLE_INTERMEDIATE = 13; + LITTLE_DISTAL = 14; + } + FingerBoneName name = 1; + float x = 2; + float y = 3; + float z = 4; + float w = 5; +} + +message Input { + enum InputType { + DOUBLE_TAP = 0; + TRIPLE_TAP = 1; + BUTTON_1_HELD = 2; + BUTTON_1_UNHELD = 3; + BUTTON_2_HELD = 4; + BUTTON_2_UNHELD = 5; + MENU_RECENTER_HELD = 6; + MENU_RECENTER_UNHELD = 7; + STICK_CLICK_HELD = 9; + STICK_CLICK_UNHELD = 10; + } + InputType type = 1; +} + message Position { int32 tracker_id = 1; optional float x = 2; @@ -74,9 +115,28 @@ message Position { FULL = 3; } optional DataSource data_source = 9; - optional float vx = 10; - optional float vy = 11; - optional float vz = 12; + + repeated FingerBoneRotation finger_bone_rotations = 10; + + repeated Input input = 11; + optional float vx = 12; + optional float vy = 13; + optional float vz = 14; +} + +message ControllerInput { + int32 tracker_id = 1; + float thumbstick_x = 2; + float thumbstick_y = 3; + float trackpad_x = 4; + float trackpad_y = 5; + float trigger = 6; + float grip = 7; + bool button_1 = 8; + bool button_2 = 9; + bool menu_recenter = 10; + bool stick_click = 11; + bool trackpad_click = 12; } message UserAction { @@ -126,5 +186,6 @@ message ProtobufMessage { TrackerStatus tracker_status = 4; Battery battery = 5; Version version = 6; + ControllerInput controller_input = 7; } } \ No newline at end of file