├── .gitmodules ├── cmake ├── version.h.in └── gitversion.cmake ├── src ├── setup.hpp ├── bridge.hpp ├── matrix_utils.cpp ├── matrix_utils.h ├── pathtools_excerpt.h ├── setup.cpp ├── pathtools_excerpt.cpp ├── bridge.cpp ├── unix_sockets.hpp └── main.cpp ├── vcpkg.json ├── bindings ├── generic_hmd.json ├── vive_tracker_chest.json ├── vive_tracker_waist.json ├── vive_tracker_left_foot.json ├── vive_tracker_left_knee.json ├── vive_tracker_left_elbow.json ├── vive_tracker_right_foot.json ├── vive_tracker_right_knee.json ├── vive_tracker_right_elbow.json ├── vive_tracker_left_shoulder.json ├── vive_tracker_right_shoulder.json ├── knuckles.json ├── generic.json ├── oculus_touch.json ├── vive_controller.json ├── hpmotioncontroller.json ├── holographic_controller.json └── actions.json ├── manifest.vrmanifest ├── LICENSE ├── README.md ├── .github └── workflows │ └── build.yml ├── .gitattributes ├── CMakeLists.txt ├── ProtobufMessages.proto └── .gitignore /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vcpkg"] 2 | path = vcpkg 3 | url = https://github.com/Microsoft/vcpkg.git -------------------------------------------------------------------------------- /cmake/version.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | static constexpr const char *version = "@_build_version@"; -------------------------------------------------------------------------------- /src/setup.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // little wrapper for unique_ptr 5 | void shutdown_vr(vr::IVRSystem* _system); 6 | 7 | int handle_setup(bool install_manifest); -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slimevr-feeder", 3 | "version-string": "0.1.4", 4 | "dependencies": [ 5 | "fmt", 6 | "openvr", 7 | "protobuf", 8 | "args", 9 | "simdjson" 10 | ] 11 | } -------------------------------------------------------------------------------- /bindings/generic_hmd.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Bindings for Generic Head", 3 | "controller_type": "generic_hmd", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/head", 12 | "path": "/user/head/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /manifest.vrmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "source" : "builtin", 3 | "applications": [{ 4 | "app_key": "slimevr.steamvr.feeder", 5 | "launch_type": "binary", 6 | "binary_path_windows": "SlimeVR-Feeder-App.exe", 7 | "is_dashboard_overlay": true, 8 | "action_manifest_path": "bindings/actions.json", 9 | 10 | "strings": { 11 | "en_us": { 12 | "name": "SlimeVR Feeder App", 13 | "description": "Feeds controller and tracker data to SlimeVR Server" 14 | } 15 | } 16 | }] 17 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_chest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Chest)", 3 | "controller_type": "vive_tracker_chest", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/chest", 12 | "path": "/user/chest/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_waist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Waist)", 3 | "controller_type": "vive_tracker_waist", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/waist", 12 | "path": "/user/waist/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_left_foot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Left Foot)", 3 | "controller_type": "vive_tracker_left_foot", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_foot", 12 | "path": "/user/foot/left/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_left_knee.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Left Knee)", 3 | "controller_type": "vive_tracker_left_knee", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_knee", 12 | "path": "/user/knee/left/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_left_elbow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Left Elbow)", 3 | "controller_type": "vive_tracker_left_elbow", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_elbow", 12 | "path": "/user/elbow/left/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_right_foot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Right Foot)", 3 | "controller_type": "vive_tracker_right_foot", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/right_foot", 12 | "path": "/user/foot/right/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_right_knee.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Right Knee)", 3 | "controller_type": "vive_tracker_right_knee", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/right_knee", 12 | "path": "/user/knee/right/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_right_elbow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Right Elbow)", 3 | "controller_type": "vive_tracker_right_elbow", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/right_elbow", 12 | "path": "/user/elbow/right/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_left_shoulder.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Left Shoulder)", 3 | "controller_type": "vive_tracker_left_shoulder", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_shoulder", 12 | "path": "/user/shoulder/left/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bindings/vive_tracker_right_shoulder.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default bindings for Vive Tracker (Right Shoulder)", 3 | "controller_type": "vive_tracker_right_shoulder", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/right_shoulder", 12 | "path": "/user/shoulder/right/pose/raw", 13 | "requirement": "optional" 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /cmake/gitversion.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | set(_build_version "unknown") 4 | 5 | find_package(Git) 6 | if (GIT_FOUND) 7 | execute_process( 8 | COMMAND ${GIT_EXECUTABLE} describe --tags 9 | WORKING_DIRECTORY "${local_dir}" 10 | OUTPUT_VARIABLE _build_version 11 | ERROR_QUIET 12 | OUTPUT_STRIP_TRAILING_WHITESPACE 13 | ) 14 | message(STATUS "version in git: ${_build_version}") 15 | else() 16 | message(STATUS "git not found") 17 | endif() 18 | 19 | configure_file("${local_dir}/cmake/version.h.in" "${output_dir}/version.h" @ONLY) 20 | -------------------------------------------------------------------------------- /bindings/knuckles.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Bindings for Knuckles", 3 | "controller_type": "knuckles", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_hand", 12 | "path": "/user/hand/left/pose/raw", 13 | "requirement": "optional" 14 | }, 15 | { 16 | "output": "/actions/main/in/right_hand", 17 | "path": "/user/hand/right/pose/raw", 18 | "requirement": "optional" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /bindings/generic.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Bindings for Generic Hands", 3 | "controller_type": "generic", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_hand", 12 | "path": "/user/hand/left/pose/raw", 13 | "requirement": "optional" 14 | }, 15 | { 16 | "output": "/actions/main/in/right_hand", 17 | "path": "/user/hand/right/pose/raw", 18 | "requirement": "optional" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /bindings/oculus_touch.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Bindings for Oculus Touch", 3 | "controller_type": "oculus_touch", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_hand", 12 | "path": "/user/hand/left/pose/raw", 13 | "requirement": "optional" 14 | }, 15 | { 16 | "output": "/actions/main/in/right_hand", 17 | "path": "/user/hand/right/pose/raw", 18 | "requirement": "optional" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /bindings/vive_controller.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Bindings for Vive Wand", 3 | "controller_type": "vive_controller", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_hand", 12 | "path": "/user/hand/left/pose/raw", 13 | "requirement": "optional" 14 | }, 15 | { 16 | "output": "/actions/main/in/right_hand", 17 | "path": "/user/hand/right/pose/raw", 18 | "requirement": "optional" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /bindings/hpmotioncontroller.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Bindings for HP WMR", 3 | "controller_type": "hpmotioncontroller", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_hand", 12 | "path": "/user/hand/left/pose/raw", 13 | "requirement": "optional" 14 | }, 15 | { 16 | "output": "/actions/main/in/right_hand", 17 | "path": "/user/hand/right/pose/raw", 18 | "requirement": "optional" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /bindings/holographic_controller.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Bindings for WMR", 3 | "controller_type": "holographic_controller", 4 | "bindings": { 5 | "/actions/main": { 6 | "sources": [], 7 | "skeleton": [], 8 | "haptics": [], 9 | "poses": [ 10 | { 11 | "output": "/actions/main/in/left_hand", 12 | "path": "/user/hand/left/pose/raw", 13 | "requirement": "optional" 14 | }, 15 | { 16 | "output": "/actions/main/in/right_hand", 17 | "path": "/user/hand/right/pose/raw", 18 | "requirement": "optional" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/bridge.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | enum BridgeStatus { 5 | BRIDGE_DISCONNECTED = 0, 6 | BRIDGE_CONNECTED = 1, 7 | BRIDGE_ERROR = 2, 8 | }; 9 | 10 | class SlimeVRBridge { 11 | public: 12 | SlimeVRBridge() {} 13 | 14 | virtual ~SlimeVRBridge() {}; 15 | 16 | BridgeStatus status; 17 | 18 | // returns true if the pipe has *just* (re-)connected 19 | bool runFrame(); 20 | 21 | virtual bool getNextMessage(messages::ProtobufMessage &msg) = 0; 22 | virtual bool sendMessage(messages::ProtobufMessage &msg) = 0; 23 | 24 | static std::unique_ptr factory(); 25 | 26 | private: 27 | virtual void connect() = 0; 28 | virtual void reset() = 0; 29 | virtual void update() = 0; 30 | }; -------------------------------------------------------------------------------- /src/matrix_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "matrix_utils.h" 2 | 3 | #include 4 | 5 | using namespace vr; 6 | 7 | HmdQuaternion_t GetRotation(HmdMatrix34_t matrix) { 8 | vr::HmdQuaternion_t q; 9 | 10 | q.w = sqrt(fmax(0, 1 + matrix.m[0][0] + matrix.m[1][1] + matrix.m[2][2])) / 2; 11 | q.x = sqrt(fmax(0, 1 + matrix.m[0][0] - matrix.m[1][1] - matrix.m[2][2])) / 2; 12 | q.y = sqrt(fmax(0, 1 - matrix.m[0][0] + matrix.m[1][1] - matrix.m[2][2])) / 2; 13 | q.z = sqrt(fmax(0, 1 - matrix.m[0][0] - matrix.m[1][1] + matrix.m[2][2])) / 2; 14 | q.x = copysign(q.x, matrix.m[2][1] - matrix.m[1][2]); 15 | q.y = copysign(q.y, matrix.m[0][2] - matrix.m[2][0]); 16 | q.z = copysign(q.z, matrix.m[1][0] - matrix.m[0][1]); 17 | return q; 18 | } 19 | 20 | HmdVector3_t GetPosition(HmdMatrix34_t matrix) { 21 | vr::HmdVector3_t vector; 22 | 23 | vector.v[0] = matrix.m[0][3]; 24 | vector.v[1] = matrix.m[1][3]; 25 | vector.v[2] = matrix.m[2][3]; 26 | 27 | return vector; 28 | } -------------------------------------------------------------------------------- /src/matrix_utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef MATRIX_UTILS 3 | #define MATRIX_UTILS 4 | 5 | #include 6 | 7 | //----------------------------------------------------------------------------- 8 | // Purpose: Calculates quaternion (qw,qx,qy,qz) representing the rotation 9 | // from: https://github.com/Omnifinity/OpenVR-Tracking-Example/blob/master/HTC%20Lighthouse%20Tracking%20Example/LighthouseTracking.cpp 10 | //----------------------------------------------------------------------------- 11 | vr::HmdQuaternion_t GetRotation(vr::HmdMatrix34_t matrix); 12 | 13 | //----------------------------------------------------------------------------- 14 | // Purpose: Extracts position (x,y,z). 15 | // from: https://github.com/Omnifinity/OpenVR-Tracking-Example/blob/master/HTC%20Lighthouse%20Tracking%20Example/LighthouseTracking.cpp 16 | //----------------------------------------------------------------------------- 17 | vr::HmdVector3_t GetPosition(vr::HmdMatrix34_t matrix); 18 | 19 | #endif // MATRIX_UTILS -------------------------------------------------------------------------------- /src/pathtools_excerpt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef PATHTOOLS_H 3 | 4 | #include 5 | 6 | /** Makes an absolute path from a relative path and a base path */ 7 | std::string Path_MakeAbsolute(const std::string& sRelativePath, const std::string& sBasePath); 8 | 9 | /** Returns the path (including filename) to the current executable */ 10 | std::string Path_GetExecutablePath(); 11 | 12 | /** Returns the specified path without its filename */ 13 | std::string Path_StripFilename(const std::string& sPath, char slash = 0); 14 | 15 | bool Path_IsAbsolute(const std::string& sPath); 16 | std::string Path_Compact(const std::string& sRawPath, char slash = 0); 17 | std::string Path_FixSlashes(const std::string& sPath, char slash = 0); 18 | std::string Path_Join(const std::string& first, const std::string& second, char slash = 0); 19 | char Path_GetSlash(); 20 | 21 | #ifndef MAX_UNICODE_PATH 22 | #define MAX_UNICODE_PATH 32767 23 | #endif 24 | 25 | #ifndef MAX_UNICODE_PATH_IN_UTF8 26 | #define MAX_UNICODE_PATH_IN_UTF8 (MAX_UNICODE_PATH * 4) 27 | #endif 28 | 29 | #endif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Omnifinity, Kitlith 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SlimeVR Feeder Application 2 | 3 | TODO: 4 | 5 | * It might be worth switching away from C++ because build systems/library management is a pain! 6 | I'd use rust but rust's openvr stuff is... out of date and unmaintained. 7 | maybe c#? 8 | 9 | * Create default bindings for the boolean actions (possibly involving chording?) 10 | 11 | * I think the choices made here regarding how to find/sort controllers is somewhat dubious, and out-of-date with slimevr. 12 | The logic could use a re-do. 13 | 14 | ## How to use 15 | 16 | You can download the feeder app by running the SlimeVR installer here: https://github.com/SlimeVR/SlimeVR-Installer/releases/latest/download/slimevr_web_installer.exe. This will make it launch automatically along with SteamVR. 17 | 18 | Alternatively, you can download the feeder app here: https://github.com/SlimeVR/SlimeVR-Feeder-App/releases/latest/download/SlimeVR-Feeder-App-win64.zip and manually launch the .exe. 19 | 20 | ## Building from source 21 | 22 | We assume that you already have Git and CMake installed. Run: 23 | 24 | ``` 25 | git clone --recursive https://github.com/SlimeVR/SlimeVR-Feeder-App.git 26 | cd SlimeVR-Feeder-App 27 | cmake -B build 28 | cmake --build build --target package --config Release 29 | ``` 30 | 31 | You can then execute the newly built binary: 32 | 33 | ``` 34 | ./build/_CPack_Packages/win64/ZIP/SlimeVR-Feeder-App-win64/SlimeVR-Feeder-App.exe 35 | ``` 36 | 37 | ## Thanks 38 | This was largely based off of https://github.com/Omnifinity/OpenVR-Tracking-Example , even if the structure is different. Thanks, @Omnifinity. 39 | 40 | Rust setup was basically copy-pasted from https://github.com/XiangpengHao/cxx-cmake-example. 41 | Thanks, @XiangpengHao. 42 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Feeder App 2 | on: [push, workflow_dispatch] 3 | 4 | jobs: 5 | job: 6 | name: ${{ matrix.os }}-${{ github.workflow }} 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [windows-latest, ubuntu-latest] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | submodules: true 17 | # shallow clone doesn't pull tags, unfortunately. 18 | fetch-depth: 0 19 | 20 | - uses: lukka/get-cmake@latest 21 | 22 | - name: vcpkg setup/restore artifacts 23 | uses: lukka/run-vcpkg@v11 24 | id: runvcpkg 25 | 26 | - name: Prints output of run-vcpkg's action. 27 | run: echo "root='${{ steps.runvcpkg.outputs.RUNVCPKG_VCPKG_ROOT_OUT }}', triplet='${{ steps.runvcpkg.outputs.RUNVCPKG_VCPKG_DEFAULT_TRIPLET_OUT }}' " 28 | 29 | - name: Setup vcpkg env 30 | run: vcpkg env 31 | shell: cmd 32 | if: matrix.os == 'windows-latest' 33 | 34 | - name: CMake+vcpkg configure 35 | run: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo 36 | 37 | - name: Build 38 | run: cmake --build build --config RelWithDebInfo 39 | 40 | - name: Package 41 | run: cmake --build build --target package --config RelWithDebInfo 42 | 43 | - name: Get package path for Artifact 44 | run: echo "artifactPath=$(realpath build/_CPack_Packages/*/ZIP/SlimeVR-Feeder-App-*/ --relative-to .)" >> $GITHUB_ENV 45 | shell: bash 46 | 47 | - name: Get name for Artifact 48 | run: echo "artifactName=$(basename -- ${{ env.artifactPath }} .zip )" >> $GITHUB_ENV 49 | shell: bash 50 | 51 | - name: Upload Artifact 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: ${{ env.artifactName }} 55 | path: ${{ env.artifactPath }} 56 | 57 | - name: Release 58 | uses: softprops/action-gh-release@v1 59 | if: startsWith(github.ref, 'refs/tags/') 60 | with: 61 | files: build/SlimeVR-Feeder-App-*.zip -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/setup.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "pathtools_excerpt.h" 5 | 6 | static constexpr const char *rel_manifest_path = "./manifest.vrmanifest"; 7 | static constexpr const char *application_key = "slimevr.steamvr.feeder"; 8 | 9 | void shutdown_vr(vr::IVRSystem* _system) { 10 | vr::VR_Shutdown(); 11 | } 12 | 13 | int handle_setup(bool install) { 14 | vr::EVRInitError init_error = vr::VRInitError_None; 15 | std::unique_ptr system(vr::VR_Init(&init_error, vr::VRApplication_Utility), &shutdown_vr); 16 | 17 | if (init_error != vr::VRInitError_None) { 18 | fmt::print("Unable to init VR runtime as utility: {}\n", VR_GetVRInitErrorAsEnglishDescription(init_error)); 19 | return EXIT_FAILURE; 20 | } 21 | 22 | vr::IVRApplications* apps = vr::VRApplications(); 23 | vr::EVRApplicationError app_error = vr::VRApplicationError_None; 24 | 25 | bool currently_installed = apps->IsApplicationInstalled(application_key); 26 | std::string manifest_path = Path_MakeAbsolute(rel_manifest_path, Path_StripFilename(Path_GetExecutablePath())); 27 | if (install) { 28 | if (currently_installed) { 29 | if(apps->GetApplicationAutoLaunch(application_key) == false){ 30 | fmt::print("Enabling auto-start.\n"); 31 | apps->SetApplicationAutoLaunch(application_key, true); 32 | }else{ 33 | fmt::print("Manifest is already installed and auto-start is enabled.\n"); 34 | } 35 | 36 | return 0; 37 | } 38 | 39 | fmt::print("Attempting to install manifest.\n"); 40 | 41 | app_error = apps->AddApplicationManifest(manifest_path.c_str()); 42 | if (app_error != vr::VRApplicationError_None) { 43 | fmt::print("Could not install manifest: {}\n", apps->GetApplicationsErrorNameFromEnum(app_error)); 44 | return EXIT_FAILURE; 45 | } 46 | 47 | app_error = apps->SetApplicationAutoLaunch(application_key, true); 48 | if (app_error != vr::VRApplicationError_None) { 49 | fmt::print("Could not set auto start: {}\n", apps->GetApplicationsErrorNameFromEnum(app_error)); 50 | return EXIT_FAILURE; 51 | } 52 | } else if (currently_installed) { // disable 53 | if (apps->GetApplicationAutoLaunch(application_key) == true){ 54 | fmt::print("Disabling auto-start\n"); 55 | apps->SetApplicationAutoLaunch(application_key, false); 56 | }else{ 57 | fmt::print("Auto-start is already disabled\n"); 58 | } 59 | 60 | return 0; 61 | } else { 62 | fmt::print("Manifest is not installed.\n"); 63 | return 0; 64 | } 65 | 66 | return 0; 67 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | #set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake") 3 | 4 | project(SlimeVR-Feeder-App LANGUAGES CXX) 5 | 6 | include("${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake") 7 | 8 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 9 | 10 | find_package(fmt CONFIG REQUIRED) 11 | find_library(OPENVR_LIB openvr_api) 12 | find_package(Protobuf CONFIG REQUIRED) 13 | find_package(simdjson CONFIG REQUIRED) 14 | 15 | set(protos_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/protos) 16 | file(MAKE_DIRECTORY "${protos_OUTPUT_DIR}") 17 | 18 | include_directories(include) 19 | 20 | set(CMAKE_SKIP_BUILD_RPATH FALSE) 21 | set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) 22 | set(CMAKE_INSTALL_RPATH $ORIGIN) 23 | 24 | # Project 25 | add_executable("${PROJECT_NAME}" "src/main.cpp" "src/pathtools_excerpt.cpp" "src/pathtools_excerpt.h" "src/matrix_utils.cpp" "src/matrix_utils.h" "src/bridge.cpp" "src/bridge.hpp" "src/setup.cpp" "src/setup.hpp" "ProtobufMessages.proto") 26 | target_link_libraries("${PROJECT_NAME}" PRIVATE "${OPENVR_LIB}" fmt::fmt protobuf::libprotobuf simdjson::simdjson) 27 | protobuf_generate(TARGET "${PROJECT_NAME}" LANGUAGE cpp PROTOC_OUT_DIR ${protos_OUTPUT_DIR}) 28 | target_include_directories("${PROJECT_NAME}" PUBLIC ${protos_OUTPUT_DIR} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) 29 | target_compile_features("${PROJECT_NAME}" PRIVATE cxx_std_17) 30 | 31 | # IDE Config 32 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX "Header Files" FILES ${HEADERS}) 33 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX "Source Files" FILES ${SOURCES}) 34 | 35 | add_custom_target( 36 | version 37 | COMMAND ${CMAKE_COMMAND} 38 | -Dlocal_dir="${CMAKE_CURRENT_SOURCE_DIR}" 39 | -Doutput_dir="${CMAKE_CURRENT_BINARY_DIR}" 40 | -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/gitversion.cmake" 41 | ) 42 | add_dependencies("${PROJECT_NAME}" version) 43 | 44 | install(DIRECTORY "${PROJECT_SOURCE_DIR}/bindings" 45 | DESTINATION "." 46 | ) 47 | install(FILES "${PROJECT_SOURCE_DIR}/manifest.vrmanifest" 48 | DESTINATION "." 49 | ) 50 | 51 | if (WIN32) 52 | set(VCPKG_BINARIES "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-windows/bin") 53 | install(FILES "${VCPKG_BINARIES}/openvr_api.dll" "${VCPKG_BINARIES}/fmt.dll" "${VCPKG_BINARIES}/libprotobuf.dll" "${VCPKG_BINARIES}/simdjson.dll" "${VCPKG_BINARIES}/abseil_dll.dll" DESTINATION ".") 54 | elseif(UNIX) 55 | # TODO: MacOS 56 | set(VCPKG_BINARIES "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-linux/bin") 57 | install(FILES "${VCPKG_BINARIES}/libopenvr_api.so" DESTINATION ".") 58 | endif() 59 | 60 | install(TARGETS "${PROJECT_NAME}" 61 | RUNTIME 62 | DESTINATION . 63 | ) 64 | 65 | set(CPACK_SYSTEM_NAME ${CMAKE_SYSTEM_NAME}) 66 | if(${CPACK_SYSTEM_NAME} MATCHES Windows) 67 | if(CMAKE_CL_64) 68 | set(CPACK_SYSTEM_NAME win64) 69 | set(CPACK_IFW_TARGET_DIRECTORY "@RootDir@/Program Files/${CMAKE_PROJECT_NAME}") 70 | else() 71 | set(CPACK_SYSTEM_NAME win32) 72 | endif() 73 | endif() 74 | 75 | set(CPACK_PROJECT_NAME ${PROJECT_NAME}) 76 | set(CPACK_PACKAGE_FILE_NAME ${CPACK_PROJECT_NAME}-${CPACK_SYSTEM_NAME}) 77 | set(CPACK_GENERATOR "ZIP") 78 | set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY ON) 79 | set(CPACK_VERBATIM_VARIABLES YES) 80 | include(CPack) 81 | -------------------------------------------------------------------------------- /ProtobufMessages.proto: -------------------------------------------------------------------------------- 1 | /* 2 | SlimeVR Code is placed under the MIT license 3 | Copyright (c) 2021 Eiren Rain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | /* 24 | Define all Proto Buffer messages that server and driver/app can exchange 25 | */ 26 | syntax = "proto3"; 27 | 28 | package messages; 29 | 30 | /** 31 | Tracker designations clarifications 32 | 33 | tracker_id field contains internal tracker id of a tracker on the SENDING side. Recieving 34 | side can have their own ids, names, serials, etc. If two sides exchange tracker information 35 | in both ways, they're allowed to reuse tracker ids for the trackers they create themselves. 36 | 37 | tracker_name is user-readable descriptive name of a tracker, it is allowed to be non-unique. 38 | 39 | tracker_serial is unique identificator of a tracker on the sender side, can be used to remap 40 | trackers from one id to another during reconnect or session restart, or to save persistent 41 | data or configuration between sessions 42 | */ 43 | 44 | /** 45 | * Can be send each frame if there is nothing else to send, 46 | * signaling to the other side that we're alive and ready to 47 | * recieve messages this frame. 48 | */ 49 | 50 | option java_package = "dev.slimevr.bridge"; 51 | option java_outer_classname = "ProtobufMessages"; 52 | 53 | message PingPong { 54 | } 55 | 56 | message Position { 57 | int32 tracker_id = 1; 58 | optional float x = 2; 59 | optional float y = 3; 60 | optional float z = 4; 61 | float qx = 5; 62 | float qy = 6; 63 | float qz = 7; 64 | float qw = 8; 65 | enum DataSource { 66 | NONE = 0; 67 | IMU = 1; 68 | PRECISION = 2; 69 | FULL = 3; 70 | } 71 | optional DataSource data_source = 9; 72 | } 73 | 74 | message UserAction { 75 | string name = 1; 76 | map action_arguments = 2; 77 | } 78 | 79 | message TrackerAdded { 80 | int32 tracker_id = 1; 81 | string tracker_serial = 2; 82 | string tracker_name = 3; 83 | int32 tracker_role = 4; 84 | } 85 | 86 | message TrackerStatus { 87 | int32 tracker_id = 1; 88 | enum Status { 89 | DISCONNECTED = 0; 90 | OK = 1; 91 | BUSY = 2; 92 | ERROR = 3; 93 | OCCLUDED = 4; 94 | } 95 | Status status = 2; 96 | map extra = 3; 97 | enum Confidence { 98 | NO = 0; 99 | LOW = 1; 100 | MEDIUM = 5; 101 | HIGH = 10; 102 | } 103 | optional Confidence confidence = 4; 104 | } 105 | 106 | message ProtobufMessage { 107 | oneof message { 108 | Position position = 1; 109 | UserAction user_action = 2; 110 | TrackerAdded tracker_added = 3; 111 | TrackerStatus tracker_status = 4; 112 | } 113 | } -------------------------------------------------------------------------------- /bindings/actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_bindings": [ 3 | { 4 | "controller_type": "generic", 5 | "binding_url": "generic.json" 6 | }, 7 | { 8 | "controller_type": "oculus_touch", 9 | "binding_url": "oculus_touch.json" 10 | }, 11 | { 12 | "controller_type": "hpmotioncontroller", 13 | "binding_url": "hpmotioncontroller.json" 14 | }, 15 | { 16 | "controller_type": "knuckles", 17 | "binding_url": "knuckles.json" 18 | }, 19 | { 20 | "controller_type": "vive_controller", 21 | "binding_url": "vive_controller.json" 22 | }, 23 | { 24 | "controller_type": "holographic_controller", 25 | "binding_url": "holographic_controller.json" 26 | }, 27 | { 28 | "controller_type": "vive_tracker_chest", 29 | "binding_url": "vive_tracker_chest.json" 30 | }, 31 | { 32 | "controller_type": "vive_tracker_left_elbow", 33 | "binding_url": "vive_tracker_left_elbow.json" 34 | }, 35 | { 36 | "controller_type": "vive_tracker_left_foot", 37 | "binding_url": "vive_tracker_left_foot.json" 38 | }, 39 | { 40 | "controller_type": "vive_tracker_left_knee", 41 | "binding_url": "vive_tracker_left_knee.json" 42 | }, 43 | { 44 | "controller_type": "vive_tracker_left_shoulder", 45 | "binding_url": "vive_tracker_left_shoulder.json" 46 | }, 47 | { 48 | "controller_type": "vive_tracker_right_elbow", 49 | "binding_url": "vive_tracker_right_elbow.json" 50 | }, 51 | { 52 | "controller_type": "vive_tracker_right_foot", 53 | "binding_url": "vive_tracker_right_foot.json" 54 | }, 55 | { 56 | "controller_type": "vive_tracker_right_knee", 57 | "binding_url": "vive_tracker_right_knee.json" 58 | }, 59 | { 60 | "controller_type": "vive_tracker_right_shoulder", 61 | "binding_url": "vive_tracker_right_shoulder.json" 62 | }, 63 | { 64 | "controller_type": "vive_tracker_waist", 65 | "binding_url": "vive_tracker_waist.json" 66 | }, 67 | { 68 | "controller_type": "generic_hmd", 69 | "binding_url": "generic_hmd.json" 70 | } 71 | ], 72 | "actions": [ 73 | { 74 | "name": "/actions/main/in/head", 75 | "type": "pose", 76 | "requirement": "optional" 77 | }, 78 | { 79 | "name": "/actions/main/in/left_hand", 80 | "type": "pose", 81 | "requirement": "optional" 82 | }, 83 | { 84 | "name": "/actions/main/in/right_hand", 85 | "type": "pose", 86 | "requirement": "optional" 87 | }, 88 | { 89 | "name": "/actions/main/in/chest", 90 | "type": "pose", 91 | "requirement": "optional" 92 | }, 93 | { 94 | "name": "/actions/main/in/left_elbow", 95 | "type": "pose", 96 | "requirement": "optional" 97 | }, 98 | { 99 | "name": "/actions/main/in/left_foot", 100 | "type": "pose", 101 | "requirement": "optional" 102 | }, 103 | { 104 | "name": "/actions/main/in/left_knee", 105 | "type": "pose", 106 | "requirement": "optional" 107 | }, 108 | { 109 | "name": "/actions/main/in/left_shoulder", 110 | "type": "pose", 111 | "requirement": "optional" 112 | }, 113 | { 114 | "name": "/actions/main/in/right_elbow", 115 | "type": "pose", 116 | "requirement": "optional" 117 | }, 118 | { 119 | "name": "/actions/main/in/right_foot", 120 | "type": "pose", 121 | "requirement": "optional" 122 | }, 123 | { 124 | "name": "/actions/main/in/right_knee", 125 | "type": "pose", 126 | "requirement": "optional" 127 | }, 128 | { 129 | "name": "/actions/main/in/right_shoulder", 130 | "type": "pose", 131 | "requirement": "optional" 132 | }, 133 | { 134 | "name": "/actions/main/in/waist", 135 | "type": "pose", 136 | "requirement": "optional" 137 | }, 138 | { 139 | "name": "/actions/main/in/request_calibration", 140 | "type": "boolean", 141 | "requirement": "optional" 142 | }, 143 | { 144 | "name": "/actions/main/in/fast_reset", 145 | "type": "boolean", 146 | "requirement": "optional" 147 | }, 148 | { 149 | "name": "/actions/main/in/mounting_reset", 150 | "type": "boolean", 151 | "requirement": "optional" 152 | }, 153 | { 154 | "name": "/actions/main/in/pause_tracking", 155 | "type": "boolean", 156 | "requirement": "optional" 157 | } 158 | ], 159 | "action_sets": [ 160 | { 161 | "name": "/actions/main", 162 | "usage": "leftright" 163 | } 164 | ], 165 | "localization": [ 166 | { 167 | "language_tag": "en_us", 168 | "/actions/main/in/head": "Head", 169 | "/actions/main/in/left_hand": "Left Hand", 170 | "/actions/main/in/right_hand": "Right Hand", 171 | "/actions/main/in/chest": "Chest", 172 | "/actions/main/in/left_shoulder": "Left Shoulder", 173 | "/actions/main/in/right_shoulder": "Right Shoulder", 174 | "/actions/main/in/left_elbow": "Left Elbow", 175 | "/actions/main/in/right_elbow": "Right Elbow", 176 | "/actions/main/in/left_knee": "Left Knee", 177 | "/actions/main/in/right_knee": "Right Knee", 178 | "/actions/main/in/waist": "Waist", 179 | "/actions/main/in/foot_left": "Left Foot", 180 | "/actions/main/in/foot_right": "Right Foot", 181 | "/actions/main/in/request_calibration": "Full Reset", 182 | "/actions/main/in/fast_reset": "Yaw Reset", 183 | "/actions/main/in/mounting_reset": "Mounting Reset", 184 | "/actions/main/in/pause_tracking": "Pause Tracking" 185 | } 186 | ] 187 | } 188 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | 365 | # Cmake (when using VS Code or other) 366 | build/ 367 | .vscode/ -------------------------------------------------------------------------------- /src/pathtools_excerpt.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | pathtools_excerpt.h - Excerpt of the full shared pathtools.c from valve: 3 | 4 | Copyright(c) 2015, Valve Corporation 5 | All rights reserved. 6 | 7 | Redistributionand use in sourceand binary forms, with or without modification, 8 | are permitted provided that the following conditions are met : 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditionsand the following disclaimer in the documentationand /or 15 | other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its contributors 18 | may be used to endorse or promote products derived from this software without 19 | specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | 33 | #if defined(WIN32) 34 | 35 | #define WIN32_LEAN_AND_MEAN 36 | #include 37 | 38 | #elif defined(__unix__) 39 | 40 | #include 41 | 42 | #endif 43 | #include 44 | #include "pathtools_excerpt.h" 45 | 46 | bool Path_IsAbsolute(const std::string& sPath) 47 | { 48 | if (sPath.empty()) 49 | return false; 50 | 51 | #if defined( WIN32 ) 52 | if (sPath.size() < 3) // must be c:\x or \\x at least 53 | return false; 54 | 55 | if (sPath[1] == ':') // drive letter plus slash, but must test both slash cases 56 | { 57 | if (sPath[2] == '\\' || sPath[2] == '/') 58 | return true; 59 | } 60 | else if (sPath[0] == '\\' && sPath[1] == '\\') // UNC path 61 | return true; 62 | #else 63 | if (sPath[0] == '\\' || sPath[0] == '/') // any leading slash 64 | return true; 65 | #endif 66 | 67 | return false; 68 | } 69 | 70 | 71 | /** Removes redundant /.. elements in the path. Returns an empty path if the 72 | * specified path has a broken number of directories for its number of ..s */ 73 | std::string Path_Compact(const std::string& sRawPath, char slash) 74 | { 75 | if (slash == 0) 76 | slash = Path_GetSlash(); 77 | 78 | std::string sPath = Path_FixSlashes(sRawPath, slash); 79 | std::string sSlashString(1, slash); 80 | 81 | // strip out all /./ 82 | for (std::string::size_type i = 0; (i + 3) < sPath.length(); ) 83 | { 84 | if (sPath[i] == slash && sPath[i + 1] == '.' && sPath[i + 2] == slash) 85 | { 86 | sPath.replace(i, 3, sSlashString); 87 | } 88 | else 89 | { 90 | ++i; 91 | } 92 | } 93 | 94 | 95 | // get rid of trailing /. but leave the path separator 96 | if (sPath.length() > 2) 97 | { 98 | std::string::size_type len = sPath.length(); 99 | if (sPath[len - 1] == '.' && sPath[len - 2] == slash) 100 | { 101 | // sPath.pop_back(); 102 | sPath[len - 1] = 0; // for now, at least 103 | } 104 | } 105 | 106 | // get rid of leading ./ 107 | if (sPath.length() > 2) 108 | { 109 | if (sPath[0] == '.' && sPath[1] == slash) 110 | { 111 | sPath.replace(0, 2, ""); 112 | } 113 | } 114 | 115 | // each time we encounter .. back up until we've found the previous directory name 116 | // then get rid of both 117 | std::string::size_type i = 0; 118 | while (i < sPath.length()) 119 | { 120 | if (i > 0 && sPath.length() - i >= 2 121 | && sPath[i] == '.' 122 | && sPath[i + 1] == '.' 123 | && (i + 2 == sPath.length() || sPath[i + 2] == slash) 124 | && sPath[i - 1] == slash) 125 | { 126 | // check if we've hit the start of the string and have a bogus path 127 | if (i == 1) 128 | return ""; 129 | 130 | // find the separator before i-1 131 | std::string::size_type iDirStart = i - 2; 132 | while (iDirStart > 0 && sPath[iDirStart - 1] != slash) 133 | --iDirStart; 134 | 135 | // remove everything from iDirStart to i+2 136 | sPath.replace(iDirStart, (i - iDirStart) + 3, ""); 137 | 138 | // start over 139 | i = 0; 140 | } 141 | else 142 | { 143 | ++i; 144 | } 145 | } 146 | 147 | return sPath; 148 | } 149 | 150 | /** Fixes the directory separators for the current platform */ 151 | std::string Path_FixSlashes(const std::string& sPath, char slash) 152 | { 153 | if (slash == 0) 154 | slash = Path_GetSlash(); 155 | 156 | std::string sFixed = sPath; 157 | for (std::string::iterator i = sFixed.begin(); i != sFixed.end(); i++) 158 | { 159 | if (*i == '/' || *i == '\\') 160 | *i = slash; 161 | } 162 | 163 | return sFixed; 164 | } 165 | 166 | /** Jams two paths together with the right kind of slash */ 167 | std::string Path_Join(const std::string& first, const std::string& second, char slash) 168 | { 169 | if (slash == 0) 170 | slash = Path_GetSlash(); 171 | 172 | // only insert a slash if we don't already have one 173 | std::string::size_type nLen = first.length(); 174 | if (!nLen) 175 | return second; 176 | #if defined(_WIN32) 177 | if (first.back() == '\\' || first.back() == '/') 178 | nLen--; 179 | #else 180 | char last_char = first[first.length() - 1]; 181 | if (last_char == '\\' || last_char == '/') 182 | nLen--; 183 | #endif 184 | 185 | return first.substr(0, nLen) + std::string(1, slash) + second; 186 | } 187 | 188 | 189 | /** Makes an absolute path from a relative path and a base path */ 190 | std::string Path_MakeAbsolute(const std::string& sRelativePath, const std::string& sBasePath) 191 | { 192 | if (Path_IsAbsolute(sRelativePath)) 193 | return sRelativePath; 194 | else 195 | { 196 | if (!Path_IsAbsolute(sBasePath)) 197 | return ""; 198 | 199 | std::string sCompacted = Path_Compact(Path_Join(sBasePath, sRelativePath)); 200 | if (Path_IsAbsolute(sCompacted)) 201 | return sCompacted; 202 | else 203 | return ""; 204 | } 205 | } 206 | 207 | /** Returns the path (including filename) to the current executable */ 208 | std::string Path_GetExecutablePath() 209 | { 210 | #if defined( _WIN32 ) 211 | wchar_t *pwchPath = new wchar_t[MAX_UNICODE_PATH]; 212 | char *pchPath = new char[MAX_UNICODE_PATH_IN_UTF8]; 213 | ::GetModuleFileNameW( NULL, pwchPath, MAX_UNICODE_PATH ); 214 | WideCharToMultiByte( CP_UTF8, 0, pwchPath, -1, pchPath, MAX_UNICODE_PATH_IN_UTF8, NULL, NULL ); 215 | delete[] pwchPath; 216 | 217 | std::string sPath = pchPath; 218 | delete[] pchPath; 219 | return sPath; 220 | #elif defined( __unix__ ) 221 | char rchPath[1024]; 222 | size_t nBuff = sizeof( rchPath ); 223 | ssize_t nRead = readlink("/proc/self/exe", rchPath, nBuff-1 ); 224 | if ( nRead != -1 ) 225 | { 226 | rchPath[ nRead ] = 0; 227 | return rchPath; 228 | } 229 | else 230 | { 231 | return ""; 232 | } 233 | #else 234 | #error Implement Plat_GetExecutablePath 235 | // AssertMsg( false, "Implement Plat_GetExecutablePath" ); 236 | // return ""; 237 | #endif 238 | } 239 | 240 | /** Returns the specified path without its filename */ 241 | std::string Path_StripFilename(const std::string& sPath, char slash) 242 | { 243 | if (slash == 0) 244 | slash = Path_GetSlash(); 245 | 246 | std::string::size_type n = sPath.find_last_of(slash); 247 | if (n == std::string::npos) 248 | return sPath; 249 | else 250 | return std::string(sPath.begin(), sPath.begin() + n); 251 | } 252 | 253 | char Path_GetSlash() 254 | { 255 | #if defined(_WIN32) 256 | return '\\'; 257 | #else 258 | return '/'; 259 | #endif 260 | } -------------------------------------------------------------------------------- /src/bridge.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "bridge.hpp" 7 | 8 | #if defined(_WIN32) 9 | #include 10 | 11 | class NamedPipeBridge final: public SlimeVRBridge { 12 | private: 13 | static constexpr char* pipe_name = "\\\\.\\pipe\\SlimeVRInput"; 14 | HANDLE pipe = INVALID_HANDLE_VALUE; 15 | // our buffer "size" will probably always be 0, we're just making use of the capacity. 16 | std::vector buffer; 17 | 18 | void pipe_error() { 19 | status = BRIDGE_ERROR; 20 | fmt::print("Bridge error: 0x{:x}\n", GetLastError()); 21 | } 22 | public: 23 | bool getNextMessage(messages::ProtobufMessage &msg) final override { 24 | if (status != BRIDGE_CONNECTED) { 25 | return false; 26 | } 27 | 28 | DWORD read_bytes = 0; 29 | uint8_t size_bytes[4]; 30 | if (!PeekNamedPipe(pipe, size_bytes, 4, &read_bytes, NULL, NULL)) { 31 | pipe_error(); 32 | return false; 33 | } else if (read_bytes != 4) { 34 | return false; 35 | } 36 | 37 | DWORD size = size_bytes[0] | size_bytes[1] << 8 | size_bytes[2] << 16 | size_bytes[3] << 24; 38 | buffer.resize(size); 39 | if (!ReadFile(pipe, buffer.data(), size, &read_bytes, NULL)) { 40 | pipe_error(); 41 | return false; 42 | } 43 | 44 | return msg.ParseFromArray(buffer.data() + 4, size - 4); 45 | } 46 | 47 | bool sendMessage(messages::ProtobufMessage &msg) final override { 48 | if (status != BRIDGE_CONNECTED) { 49 | return false; 50 | } 51 | 52 | DWORD size = msg.ByteSizeLong() + 4; // wire size includes 4 bytes for size 53 | buffer.resize(size); 54 | 55 | buffer[0] = size & 0xFF; 56 | buffer[1] = (size >> 8) & 0xFF; 57 | buffer[2] = (size >> 16) & 0xFF; 58 | buffer[3] = (size >> 24) & 0xFF; 59 | if (!msg.SerializeToArray(buffer.data() + 4, size - 4)) { 60 | return false; 61 | } 62 | 63 | DWORD _written = 0; 64 | if (!WriteFile(pipe, buffer.data(), size, &_written, NULL)) { 65 | pipe_error(); 66 | return false; 67 | } 68 | 69 | return true; 70 | } 71 | private: 72 | void connect() final override { 73 | pipe = CreateFileA(pipe_name, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); 74 | if (pipe != INVALID_HANDLE_VALUE) { 75 | status = BRIDGE_CONNECTED; 76 | fmt::print("Pipe was connected!\n"); 77 | } 78 | } 79 | virtual void reset() final override { 80 | if (pipe != INVALID_HANDLE_VALUE) { 81 | CloseHandle(pipe); 82 | pipe = INVALID_HANDLE_VALUE; 83 | status = BRIDGE_DISCONNECTED; 84 | fmt::print("Pipe was reset.\n"); 85 | } 86 | } 87 | virtual void update() final override {} 88 | }; 89 | 90 | #else 91 | #include "unix_sockets.hpp" 92 | 93 | #include 94 | #include 95 | 96 | namespace fs = std::filesystem; 97 | 98 | class UnixSocketBridge final : public SlimeVRBridge { 99 | private: 100 | static constexpr std::string_view TMP_DIR = "/tmp"; 101 | static constexpr std::string_view XDG_DATA_DIR_DEFAULT = ".local/share"; 102 | static constexpr std::string_view SLIMEVR_DATA_DIR = "slimevr"; 103 | static constexpr std::string_view SOCKET_NAME = "SlimeVRInput"; 104 | inline static constexpr int HEADER_SIZE = 4; 105 | inline static constexpr int BUFFER_SIZE = 1024; 106 | using ByteBuffer = std::array; 107 | 108 | ByteBuffer byteBuffer; 109 | BasicLocalClient client; 110 | 111 | /// @return iterator after header 112 | template 113 | std::optional WriteHeader(TBufIt bufBegin, int bufSize, int msgSize) { 114 | const int totalSize = msgSize + HEADER_SIZE; // include header bytes in total size 115 | if (bufSize < totalSize) return std::nullopt; // header won't fit 116 | 117 | const auto size = static_cast(totalSize); 118 | TBufIt it = bufBegin; 119 | *(it++) = static_cast(size); 120 | *(it++) = static_cast(size >> 8U); 121 | *(it++) = static_cast(size >> 16U); 122 | *(it++) = static_cast(size >> 24U); 123 | return it; 124 | } 125 | /// @return iterator after header 126 | template 127 | std::optional ReadHeader(TBufIt bufBegin, int numBytesRecv, int& outMsgSize) { 128 | if (numBytesRecv < HEADER_SIZE) return std::nullopt; // header won't fit 129 | 130 | uint32_t size = 0; 131 | TBufIt it = bufBegin; 132 | size = static_cast(*(it++)); 133 | size |= static_cast(*(it++)) << 8U; 134 | size |= static_cast(*(it++)) << 16U; 135 | size |= static_cast(*(it++)) << 24U; 136 | 137 | const auto totalSize = static_cast(size); 138 | if (totalSize < HEADER_SIZE) return std::nullopt; 139 | outMsgSize = totalSize - HEADER_SIZE; 140 | return it; 141 | } 142 | 143 | void connect() final { 144 | if (!client.IsOpen()) { 145 | fs::path socket; 146 | // TODO: do this once in the constructor or something 147 | if(const char* ptr = std::getenv("XDG_RUNTIME_DIR")) { 148 | const fs::path xdg_runtime = ptr; 149 | socket = (xdg_runtime / SOCKET_NAME); 150 | } 151 | if(!fs::exists(socket)) { 152 | socket = (fs::path(TMP_DIR) / SOCKET_NAME); 153 | } 154 | // try using home dir if the vrserver is run in a chroot like 155 | if(!fs::exists(socket)) { 156 | if (const char* ptr = std::getenv("XDG_DATA_DIR")) { 157 | const fs::path data_dir = ptr; 158 | socket = (data_dir / SLIMEVR_DATA_DIR / SOCKET_NAME); 159 | } else if (const char* ptr = std::getenv("HOME")) { 160 | const fs::path home = ptr; 161 | socket = (home / XDG_DATA_DIR_DEFAULT / SLIMEVR_DATA_DIR / SOCKET_NAME); 162 | } 163 | } 164 | if(fs::exists(socket)) { 165 | fmt::print("bridge socket: {}", std::string(socket)); 166 | client.Open(socket.native()); 167 | } 168 | } 169 | } 170 | void reset() final { 171 | client.Close(); 172 | } 173 | void update() final { 174 | client.UpdateOnce(); 175 | } 176 | 177 | public: 178 | bool getNextMessage(messages::ProtobufMessage &msg) final { 179 | if (!client.IsOpen()) return false; 180 | 181 | int bytesRecv = 0; 182 | try { 183 | bytesRecv = client.RecvOnce(byteBuffer.begin(), HEADER_SIZE); 184 | } catch (const std::exception& e) { 185 | client.Close(); 186 | fmt::print("bridge send error: {}\n", e.what()); 187 | return false; 188 | } 189 | if (bytesRecv == 0) return false; // no message waiting 190 | 191 | int msgSize = 0; 192 | const std::optional msgBeginIt = ReadHeader(byteBuffer.begin(), bytesRecv, msgSize); 193 | if (!msgBeginIt) { 194 | fmt::print("bridge recv error: invalid message header or size\n"); 195 | return false; 196 | } 197 | if (msgSize <= 0) { 198 | fmt::print("bridge recv error: empty message\n"); 199 | return false; 200 | } 201 | try { 202 | if (!client.RecvAll(*msgBeginIt, msgSize)) { 203 | fmt::print("bridge recv error: client closed\n"); 204 | return false; 205 | } 206 | } catch (const std::exception& e) { 207 | client.Close(); 208 | fmt::print("bridge send error: {}\n", e.what()); 209 | return false; 210 | } 211 | if (!msg.ParseFromArray(&(**msgBeginIt), msgSize)) { 212 | fmt::print("bridge recv error: failed to parse\n"); 213 | return false; 214 | } 215 | 216 | return true; 217 | } 218 | bool sendMessage(messages::ProtobufMessage &msg) final { 219 | if (!client.IsOpen()) return false; 220 | const auto bufBegin = byteBuffer.begin(); 221 | const auto bufferSize = static_cast(std::distance(bufBegin, byteBuffer.end())); 222 | const auto msgSize = static_cast(msg.ByteSizeLong()); 223 | const std::optional msgBeginIt = WriteHeader(bufBegin, bufferSize, msgSize); 224 | if (!msgBeginIt) { 225 | fmt::print("bridge send error: failed to write header\n"); 226 | return false; 227 | } 228 | if (!msg.SerializeToArray(&(**msgBeginIt), msgSize)) { 229 | fmt::print("bridge send error: failed to serialize\n"); 230 | return false; 231 | } 232 | int bytesToSend = static_cast(std::distance(bufBegin, *msgBeginIt + msgSize)); 233 | if (bytesToSend <= 0) { 234 | fmt::print("bridge send error: empty message\n"); 235 | return false; 236 | } 237 | if (bytesToSend > bufferSize) { 238 | fmt::print("bridge send error: message too big\n"); 239 | return false; 240 | } 241 | try { 242 | return client.Send(bufBegin, bytesToSend); 243 | } catch (const std::exception& e) { 244 | client.Close(); 245 | fmt::print("bridge send error: {}\n", e.what()); 246 | return false; 247 | } 248 | } 249 | }; 250 | 251 | #endif 252 | 253 | bool SlimeVRBridge::runFrame() { 254 | switch (status) { 255 | case BRIDGE_DISCONNECTED: 256 | connect(); 257 | return status == BRIDGE_CONNECTED; 258 | case BRIDGE_ERROR: 259 | reset(); 260 | return false; 261 | case BRIDGE_CONNECTED: 262 | update(); 263 | return false; 264 | default: 265 | // uhhh, what? 266 | reset(); 267 | status = BRIDGE_DISCONNECTED; 268 | return false; 269 | } 270 | } 271 | 272 | // TODO: take some kind of configuration input for switching between named pipes, unix sockets, websockets? 273 | std::unique_ptr SlimeVRBridge::factory() { 274 | #if defined(_WIN32) 275 | return std::make_unique(); 276 | #elif defined(__linux__) 277 | return std::make_unique(); 278 | #else 279 | #error Unsupported platform 280 | #endif 281 | } 282 | 283 | -------------------------------------------------------------------------------- /src/unix_sockets.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | /// AF_UNIX / local socket specific address 24 | using sockaddr_un_t = struct sockaddr_un; 25 | /// generic address, usually pointer argument 26 | using sockaddr_t = struct sockaddr; 27 | /// used as list for poll() 28 | using pollfd_t = struct pollfd; 29 | /// file descriptor 30 | using Descriptor = int; 31 | 32 | /// std::errc or int 33 | /// Unwrap will either return the int, or throw the errc as a system_error 34 | class SysReturn { 35 | static constexpr std::errc sNotAnError = std::errc(); 36 | public: 37 | constexpr explicit SysReturn(int value) noexcept : mCode(sNotAnError), mValue(value) {} 38 | constexpr explicit SysReturn(std::errc code) noexcept : mCode(code), mValue() {} 39 | constexpr bool IsError() const { return mCode != sNotAnError; } 40 | [[noreturn]] void ThrowCode() const { throw std::system_error(std::make_error_code(mCode)); } 41 | constexpr int Unwrap() const { if (IsError()) ThrowCode(); return mValue; } 42 | constexpr std::errc GetCode() const { return mCode; } 43 | private: 44 | std::errc mCode; 45 | int mValue; 46 | }; 47 | 48 | /// call a system function and wrap the errno or int result in a SysReturn 49 | template 50 | [[nodiscard]] inline SysReturn SysCall(Fn&& func, Args&&... args) noexcept { 51 | const int result = static_cast(func(std::forward(args)...)); 52 | if (result != -1) return SysReturn(result); 53 | return SysReturn(std::errc(errno)); 54 | } 55 | 56 | /// wrap a blocking syscall and return nullopt if it would block 57 | template 58 | [[nodiscard]] inline std::optional SysCallBlocking(Fn&& func, Args&&... args) noexcept { 59 | const int result = static_cast(func(std::forward(args)...)); 60 | if (result != -1) return std::optional(result); 61 | const auto code = static_cast(errno); 62 | if (code == std::errc::operation_would_block || code == std::errc::resource_unavailable_try_again) { 63 | return std::nullopt; 64 | } 65 | return std::optional(code); 66 | } 67 | 68 | namespace event { 69 | 70 | enum class SockMode { 71 | Acceptor, 72 | Connector 73 | }; 74 | 75 | /// bitmask for which events to return 76 | using Mask = short; 77 | inline constexpr Mask Readable = POLLIN; /// enable Readable events 78 | inline constexpr Mask Priority = POLLPRI; /// enable Priority events 79 | inline constexpr Mask Writable = POLLOUT; /// enable Writable events 80 | 81 | class Result { 82 | public: 83 | explicit Result(short events) : v(events) {} 84 | bool IsReadable() const { return (v & POLLIN) != 0; } /// without blocking, connector can call read or acceptor can call accept 85 | bool IsPriority() const { return (v & POLLPRI) != 0; } /// some exceptional condition, for tcp this is OOB data 86 | bool IsWritable() const { return (v & POLLOUT) != 0; } /// can call write without blocking 87 | bool IsErrored() const { return (v & POLLERR) != 0; } /// error to be checked with Socket::GetError(), or write pipe's target read pipe was closed 88 | bool IsClosed() const { return (v & POLLHUP) != 0; } /// socket closed, however for connector, subsequent reads must be called until returns 0 89 | bool IsInvalid() const { return (v & POLLNVAL) != 0; } /// not an open descriptor and shouldn't be polled 90 | private: 91 | short v; 92 | }; 93 | /// poll an acceptor and its connections 94 | class Poller { 95 | static constexpr Mask mConnectorMask = Readable | Writable; 96 | static constexpr Mask mAcceptorMask = Readable; 97 | public: 98 | void Poll(int timeoutMs) { 99 | SysCall(::poll, mPollList.data(), mPollList.size(), timeoutMs).Unwrap(); 100 | } 101 | /// @tparam TPred (Descriptor, event::Result, event::SockMode) -> void 102 | template 103 | void Poll(int timeoutMs, TPred&& pred) { 104 | Poll(timeoutMs); 105 | for (const pollfd_t& elem : mPollList) { 106 | SockMode mode = (elem.events == mAcceptorMask) ? SockMode::Acceptor : SockMode::Connector; 107 | pred(elem.fd, Result(elem.revents), mode); 108 | } 109 | } 110 | void AddConnector(Descriptor descriptor) { 111 | mPollList.push_back({descriptor, mConnectorMask, 0}); 112 | } 113 | void AddAcceptor(Descriptor descriptor) { 114 | mPollList.push_back({descriptor, mAcceptorMask, 0}); 115 | } 116 | Result At(int idx) const { 117 | return Result(mPollList.at(idx).revents); 118 | } 119 | bool Remove(Descriptor descriptor) { 120 | auto it = std::find_if(mPollList.begin(), mPollList.end(), 121 | [&](const pollfd_t& elem){ return elem.fd == descriptor; }); 122 | if (it == mPollList.end()) return false; 123 | mPollList.erase(it); 124 | return true; 125 | } 126 | void Clear() { mPollList.clear(); } 127 | int GetSize() const { return static_cast(mPollList.size()); } 128 | private: 129 | std::vector mPollList{}; 130 | }; 131 | 132 | } 133 | 134 | /// owned socket file descriptor 135 | class Socket { 136 | static constexpr Descriptor sInvalidSocket = -1; 137 | public: 138 | /// open a new socket 139 | Socket(int domain, int type, int protocol) : mDescriptor(SysCall(::socket, domain, type, protocol).Unwrap()) { 140 | SetNonBlocking(); 141 | } 142 | /// using file descriptor returned from system call 143 | explicit Socket(Descriptor descriptor) : mDescriptor(descriptor) { 144 | if (descriptor == sInvalidSocket) throw std::invalid_argument("invalid socket descriptor"); 145 | SetNonBlocking(); // accepted from non-blocking socket still needs to be set 146 | } 147 | ~Socket() { 148 | // owns resource and must close it, descriptor will be set invalid if moved from 149 | // discard any errors thrown by close, most mean it never owned it or didn't exist 150 | if (mDescriptor != sInvalidSocket) (void)SysCall(::close, mDescriptor); 151 | } 152 | // manage descriptor like never null unique_ptr 153 | Socket(Socket&& other) noexcept : 154 | mDescriptor(other.mDescriptor), 155 | mIsReadable(other.mIsReadable), 156 | mIsWritable(other.mIsWritable), 157 | mIsNonBlocking(other.mIsNonBlocking) { 158 | other.mDescriptor = sInvalidSocket; 159 | } 160 | Socket& operator=(Socket&& rhs) noexcept { 161 | std::swap(mDescriptor, rhs.mDescriptor); 162 | mIsReadable = rhs.mIsReadable; 163 | mIsWritable = rhs.mIsWritable; 164 | mIsNonBlocking = rhs.mIsNonBlocking; 165 | return *this; 166 | } 167 | Socket(const Socket&) = delete; 168 | Socket& operator=(const Socket&) = delete; 169 | /// get underlying file descriptor 170 | Descriptor GetDescriptor() const { return mDescriptor; } 171 | /// get an error on the socket, indicated by errored poll event 172 | std::errc GetError() const { 173 | return static_cast(GetSockOpt(SOL_SOCKET, SO_ERROR).first); 174 | } 175 | void SetBlocking() { mIsNonBlocking = false; SetStatusFlags(GetStatusFlags() & ~(O_NONBLOCK)); } 176 | void SetNonBlocking() { mIsNonBlocking = true; SetStatusFlags(GetStatusFlags() | O_NONBLOCK); } 177 | // only applies to non blocking, and set from Update (poll), always return true if blocking 178 | bool GetAndResetIsReadable() { const bool temp = mIsReadable; mIsReadable = false; return temp || !mIsNonBlocking; } 179 | bool GetAndResetIsWritable() { const bool temp = mIsWritable; mIsWritable = false; return temp || !mIsNonBlocking; } 180 | /// @return false if socket should close 181 | bool Update(event::Result res) { 182 | if (res.IsErrored()) { 183 | throw std::system_error(std::make_error_code(GetError())); 184 | } 185 | if (res.IsInvalid() || res.IsClosed()) { 186 | // TODO: technically could still have bytes waiting to be read on the close event 187 | return false; 188 | } 189 | if (res.IsReadable()) { 190 | mIsReadable = true; 191 | } 192 | if (res.IsWritable()) { 193 | mIsWritable = true; 194 | } 195 | return true; 196 | } 197 | 198 | private: 199 | int GetStatusFlags() const { return SysCall(::fcntl, mDescriptor, F_GETFL, 0).Unwrap(); } 200 | void SetStatusFlags(int flags) { SysCall(::fcntl, mDescriptor, F_SETFL, flags).Unwrap(); } 201 | 202 | /// get or set socket option, most are ints, non default length is only for strings 203 | template 204 | std::pair GetSockOpt(int level, int optname, T inputValue = T(), socklen_t inputSize = sizeof(T)) const { 205 | T outValue = inputValue; 206 | socklen_t outSize = inputSize; 207 | SysCall(::getsockopt, mDescriptor, level, optname, &outValue, &outSize).Unwrap(); 208 | return std::make_pair(outValue, outSize); 209 | } 210 | template 211 | void SetSockOpt(int level, int optname, const T& inputValue, socklen_t inputSize = sizeof(T)) { 212 | SysCall(::setsockopt, level, optname, &inputValue, inputSize).Unwrap(); 213 | } 214 | 215 | Descriptor mDescriptor; 216 | bool mIsReadable = false; 217 | bool mIsWritable = false; 218 | bool mIsNonBlocking = false; 219 | }; 220 | 221 | /// address for unix sockets 222 | class LocalAddress { 223 | static constexpr sa_family_t sFamily = AF_UNIX; // always AF_UNIX 224 | /// max returned by Size() 225 | static constexpr socklen_t sMaxSize = sizeof(sockaddr_un_t); 226 | /// offset of sun_path within the address object, sun_path is a char array 227 | static constexpr socklen_t sPathOffset = sMaxSize - sizeof(sockaddr_un_t::sun_path); 228 | public: 229 | /// empty address 230 | LocalAddress() noexcept : mSize(sMaxSize) {} 231 | /// almost always bind before use 232 | explicit LocalAddress(std::string_view path) 233 | : mSize(sPathOffset + path.size() + 1) { // beginning of object + length of path + null terminator 234 | if (path.empty()) throw std::invalid_argument("path empty"); 235 | if (mSize > sMaxSize) throw std::length_error("path too long"); 236 | // copy and null terminate path 237 | std::strncpy(&mAddress.sun_path[0], path.data(), path.size()); 238 | mAddress.sun_path[path.size()] = '\0'; 239 | mAddress.sun_family = sFamily; 240 | } 241 | 242 | /// deletes the path on the filesystem, usually called before bind 243 | void Unlink() const { 244 | // TODO: keep important errors (permissions, logic, ...) 245 | (void)SysCall(::unlink, GetPath()); 246 | } 247 | 248 | /// system calls with address 249 | const sockaddr_t* GetPtr() const { return reinterpret_cast(&mAddress); } 250 | /// system calls with address out 251 | sockaddr_t* GetPtr() { return reinterpret_cast(&mAddress); } 252 | /// system calls with addrLen 253 | socklen_t GetSize() const { return mSize; } 254 | /// system calls with addrLen out 255 | socklen_t* GetSizePtr() { 256 | mSize = sMaxSize; 257 | return &mSize; 258 | } 259 | const char* GetPath() const { return &mAddress.sun_path[0]; } 260 | bool IsValid() const { 261 | return mAddress.sun_family == sFamily; // not used with wrong socket type 262 | } 263 | private: 264 | socklen_t mSize; 265 | sockaddr_un_t mAddress{}; 266 | }; 267 | 268 | class LocalSocket : public Socket { 269 | static constexpr int sDomain = AF_UNIX, // unix domain socket 270 | sType = SOCK_STREAM, // connection oriented, no message boundaries 271 | sProtocol = 0; // auto selected 272 | public: 273 | explicit LocalSocket(std::string_view path) : Socket(sDomain, sType, sProtocol), mAddress(path) {} 274 | LocalSocket(Descriptor descriptor, LocalAddress address) : Socket(descriptor), mAddress(address) { 275 | if (!mAddress.IsValid()) throw std::invalid_argument("invalid local socket address"); 276 | } 277 | 278 | protected: 279 | void UnlinkAddress() const { mAddress.Unlink(); } 280 | void Bind() const { SysCall(::bind, GetDescriptor(), mAddress.GetPtr(), mAddress.GetSize()).Unwrap(); } 281 | void Listen(int backlog) const { SysCall(::listen, GetDescriptor(), backlog).Unwrap(); } 282 | void Connect() const { SysCall(::connect, GetDescriptor(), mAddress.GetPtr(), mAddress.GetSize()).Unwrap(); } 283 | 284 | private: 285 | LocalAddress mAddress; 286 | }; 287 | 288 | /// connector manages a connection, can send/recv with 289 | class LocalConnectorSocket : public LocalSocket { 290 | public: 291 | /// open as outbound connector to path 292 | explicit LocalConnectorSocket(std::string_view path) : LocalSocket(path) { 293 | Connect(); 294 | } 295 | /// open as inbound connector from accept 296 | LocalConnectorSocket(Descriptor descriptor, LocalAddress address) : LocalSocket(descriptor, address) {} 297 | /// send a byte buffer 298 | /// @tparam TBufIt iterator to contiguous memory 299 | /// @return number of bytes sent or nullopt if blocking 300 | template 301 | std::optional TrySend(TBufIt bufBegin, int bytesToSend) { 302 | if (!GetAndResetIsWritable()) return std::nullopt; 303 | constexpr int flags = 0; 304 | if (auto bytesSent = SysCallBlocking(::send, GetDescriptor(), &(*bufBegin), bytesToSend, flags)) { 305 | return (*bytesSent).Unwrap(); 306 | } 307 | return std::nullopt; 308 | } 309 | /// receive a byte buffer 310 | /// @tparam TBufIt iterator to contiguous memory 311 | /// @return number of bytes written to buffer or nullopt if blocking 312 | template 313 | std::optional TryRecv(TBufIt bufBegin, int bufSize) { 314 | if (!GetAndResetIsReadable()) return std::nullopt; 315 | constexpr int flags = 0; 316 | if (auto bytesRecv = SysCallBlocking(::recv, GetDescriptor(), &(*bufBegin), bufSize, flags)) { 317 | return (*bytesRecv).Unwrap(); 318 | } 319 | return std::nullopt; 320 | } 321 | }; 322 | 323 | /// aka listener/passive socket, accepts connectors 324 | class LocalAcceptorSocket : public LocalSocket { 325 | public: 326 | /// open as acceptor on path, backlog is accept queue size 327 | LocalAcceptorSocket(std::string_view path, int backlog) : LocalSocket(path) { 328 | UnlinkAddress(); 329 | Bind(); 330 | Listen(backlog); 331 | } 332 | /// accept an inbound connector or nullopt if blocking 333 | std::optional Accept() { 334 | if (!GetAndResetIsReadable()) return std::nullopt; 335 | LocalAddress address; 336 | if (auto desc = SysCallBlocking(::accept, GetDescriptor(), address.GetPtr(), address.GetSizePtr())) { 337 | return LocalConnectorSocket((*desc).Unwrap(), address); 338 | } 339 | return std::nullopt; 340 | } 341 | }; 342 | 343 | /// manage a single outbound connector 344 | class BasicLocalClient { 345 | public: 346 | void Open(std::string_view path) { 347 | if (IsOpen()) throw std::runtime_error("connection already open"); 348 | mConnector = LocalConnectorSocket(path); 349 | mPoller.AddConnector(mConnector->GetDescriptor()); 350 | assert(mPoller.GetSize() == 1); 351 | } 352 | void Close() { 353 | mConnector.reset(); 354 | mPoller.Clear(); 355 | } 356 | 357 | /// default timeout returns immediately 358 | void UpdateOnce(int timeoutMs = 0) { 359 | if (!IsOpen()) throw std::runtime_error("connection not open"); 360 | mPoller.Poll(timeoutMs); 361 | 362 | if (!mConnector->Update(mPoller.At(0))) { 363 | Close(); 364 | } 365 | } 366 | 367 | /// send a byte buffer, continuously updates until entire message is sent 368 | /// @tparam TBufIt iterator to contiguous memory 369 | /// @return false if the send fails (connection closed) 370 | template 371 | bool Send(TBufIt bufBegin, int bufSize) { 372 | TBufIt msgIt = bufBegin; 373 | int bytesToSend = bufSize; 374 | int maxIter = 100; 375 | while (--maxIter && bytesToSend > 0) { 376 | if (!IsOpen()) return false; 377 | std::optional bytesSent = mConnector->TrySend(msgIt, bytesToSend); 378 | if (!bytesSent) { 379 | // blocking, poll and try again 380 | UpdateOnce(); 381 | } else if (*bytesSent <= 0) { 382 | // returning 0 is very unlikely given the small amount of data, something about filling up the internal buffer? 383 | // handle it the same as a would block error, and hope eventually it'll resolve itself 384 | UpdateOnce(20); // 20ms timeout to give the buffer time to be emptied 385 | } else if (*bytesSent > bytesToSend) { 386 | // probably guaranteed to not happen, but just in case 387 | throw std::runtime_error("bytes sent > bytes to send"); 388 | } else { 389 | // SOCK_STREAM allows partial sends, but almost guaranteed to not happen on local sockets 390 | bytesToSend -= *bytesSent; 391 | msgIt += *bytesSent; 392 | } 393 | } 394 | if (maxIter == 0) { 395 | throw std::runtime_error("send stuck in infinite loop"); 396 | } 397 | return true; 398 | } 399 | 400 | /// receive into byte buffer 401 | /// @tparam TBufIt iterator to contiguous memory 402 | /// @return number of bytes written to buffer, 0 indicating there is no message waiting 403 | template 404 | int RecvOnce(TBufIt bufBegin, int bytesToRead) { 405 | std::optional bytesRecv = mConnector->TryRecv(bufBegin, bytesToRead); 406 | // if the user is doing while(messageReceived) { } to empty the message queue 407 | // then need to poll once before the next iteration, but only if there were bytes received 408 | if (bytesRecv && *bytesRecv > 0) UpdateOnce(); 409 | return bytesRecv.value_or(0); 410 | } 411 | 412 | /// receive into byte buffer, continously updates until all bytes are read 413 | /// @tparam TBufIt iterator to contiguous memory 414 | /// @return true if bytesToRead bytes were written to buffer 415 | template 416 | bool RecvAll(const TBufIt bufBegin, int bytesToRead) { 417 | int maxIter = 100; 418 | auto bufIt = bufBegin; 419 | while (--maxIter && bytesToRead > 0) { 420 | if (!IsOpen()) return false; 421 | std::optional bytesRecv = mConnector->TryRecv(bufIt, bytesToRead); 422 | if (!bytesRecv || *bytesRecv == 0) { 423 | // try again 424 | } else if (*bytesRecv < 0 || *bytesRecv > bytesToRead) { 425 | // should not be possible 426 | throw std::length_error("bytesRecv"); 427 | } else { 428 | // read some or all of the message 429 | bytesToRead -= *bytesRecv; 430 | bufIt += *bytesRecv; 431 | } 432 | // set readable for next bytes, or a future call to Recv 433 | UpdateOnce(); 434 | } 435 | if (maxIter == 0) { 436 | throw std::runtime_error("recv stuck in infinite loop"); 437 | } 438 | return true; 439 | } 440 | 441 | bool IsOpen() const { return mConnector.has_value(); } 442 | 443 | private: 444 | std::optional mConnector{}; 445 | event::Poller mPoller{}; // index 0 is connector if open 446 | }; 447 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "pathtools_excerpt.h" 18 | #include "matrix_utils.h" 19 | #include "bridge.hpp" 20 | #include "setup.hpp" 21 | #include "version.h" 22 | #include 23 | 24 | #if defined(WIN32) 25 | #include 26 | #endif 27 | 28 | using namespace vr; 29 | 30 | // TODO: Temp Path 31 | static constexpr const char* actions_path = "./bindings/actions.json"; 32 | static constexpr const char* config_path = "./config.txt"; 33 | 34 | enum class BodyPosition { 35 | Head = 0, 36 | LeftHand, 37 | RightHand, 38 | LeftFoot, 39 | RightFoot, 40 | LeftShoulder, 41 | RightShoulder, 42 | LeftElbow, 43 | RightElbow, 44 | LeftKnee, 45 | RightKnee, 46 | Waist, 47 | Chest, 48 | BodyPosition_Count 49 | }; 50 | 51 | // TODO: keep track of things as SlimeVRPosition in the first place. 52 | enum class SlimeVRPosition { 53 | None = 0, 54 | Waist, 55 | LeftFoot, 56 | RightFoot, 57 | Chest, 58 | LeftKnee, 59 | RightKnee, 60 | LeftElbow, 61 | RightElbow, 62 | LeftShoulder, 63 | RightShoulder, 64 | LeftHand, 65 | RightHand, 66 | LeftController, 67 | RightController, 68 | Head, 69 | Neck, 70 | Camera, 71 | Keyboard, 72 | HMD, 73 | Beacon, 74 | GenericController 75 | }; 76 | 77 | static constexpr SlimeVRPosition positionIDs[(int)BodyPosition::BodyPosition_Count] = { 78 | SlimeVRPosition::Head, 79 | SlimeVRPosition::LeftController, 80 | SlimeVRPosition::RightController, 81 | SlimeVRPosition::LeftFoot, 82 | SlimeVRPosition::RightFoot, 83 | SlimeVRPosition::LeftShoulder, 84 | SlimeVRPosition::RightShoulder, 85 | SlimeVRPosition::LeftElbow, 86 | SlimeVRPosition::RightElbow, 87 | SlimeVRPosition::LeftKnee, 88 | SlimeVRPosition::RightKnee, 89 | SlimeVRPosition::Waist, 90 | SlimeVRPosition::Chest 91 | }; 92 | 93 | static constexpr const char* positionNames[(int)SlimeVRPosition::GenericController + 1] = { 94 | "None", 95 | "Waist", 96 | "LeftFoot", 97 | "RightFoot", 98 | "Chest", 99 | "LeftKnee", 100 | "RightKnee", 101 | "LeftElbow", 102 | "RightElbow", 103 | "LeftShoulder", 104 | "RightShoulder", 105 | "LeftHand", 106 | "RightHand", 107 | "LeftController", 108 | "RightController", 109 | "Head", 110 | "Neck", 111 | "Camera", 112 | "Keyboard", 113 | "HMD", 114 | "Beacon", 115 | "GenericController" 116 | }; 117 | 118 | static constexpr const char* actions[(int)BodyPosition::BodyPosition_Count] = { 119 | "/actions/main/in/head", 120 | "/actions/main/in/left_hand", 121 | "/actions/main/in/right_hand", 122 | "/actions/main/in/left_foot", 123 | "/actions/main/in/right_foot", 124 | "/actions/main/in/left_shoulder", 125 | "/actions/main/in/right_shoulder", 126 | "/actions/main/in/left_elbow", 127 | "/actions/main/in/right_elbow", 128 | "/actions/main/in/left_knee", 129 | "/actions/main/in/right_knee", 130 | "/actions/main/in/waist", 131 | "/actions/main/in/chest" 132 | }; 133 | 134 | template 135 | std::optional GetOpenVRString(F &&openvr_closure) { 136 | uint32_t size = openvr_closure(nullptr, 0); 137 | 138 | if (size == 0) { 139 | return std::nullopt; 140 | } 141 | 142 | std::string prop_value = std::string(size, '\0'); 143 | uint32_t error = openvr_closure(prop_value.data(), size); 144 | prop_value.resize(size-1); 145 | 146 | if (error == 0) { 147 | return std::nullopt; 148 | } 149 | 150 | return prop_value; 151 | } 152 | 153 | VRActionHandle_t GetAction(const char* action_path) { 154 | VRActionHandle_t handle = k_ulInvalidInputValueHandle; 155 | EVRInputError error = (EVRInputError)VRInput()->GetActionHandle(action_path, &handle); 156 | if (error != VRInputError_None) { 157 | fmt::print("Error: Unable to get action handle '{}': {}", action_path, (int)error); 158 | std::exit(1); 159 | } 160 | 161 | return handle; 162 | } 163 | 164 | class UniverseTranslation { 165 | public: 166 | // TODO: do we want to store this differently? 167 | vr::HmdVector3_t translation; 168 | float yaw; 169 | 170 | static UniverseTranslation parse(simdjson::ondemand::object &&obj) { 171 | UniverseTranslation res; 172 | int iii = 0; 173 | for (auto component: obj["translation"]) { 174 | if (iii > 2) { 175 | break; // TODO: 4 components in a translation vector? should this be an error? 176 | } 177 | res.translation.v[iii] = component.get_double(); 178 | iii += 1; 179 | } 180 | res.yaw = obj["yaw"].get_double(); 181 | 182 | return res; 183 | } 184 | }; 185 | 186 | enum class TrackerState { 187 | DISCONNECTED, 188 | WAITING, 189 | RUNNING 190 | }; 191 | 192 | struct TrackerInfo { 193 | std::string name = ""; 194 | std::optional serial = std::nullopt; 195 | SlimeVRPosition position = SlimeVRPosition::None; 196 | messages::TrackerStatus_Status status = messages::TrackerStatus_Status_DISCONNECTED; 197 | 198 | TrackerState state = TrackerState::DISCONNECTED; 199 | /// number of ticks since last detect or valid pose 200 | uint8_t connection_timeout = 0; 201 | /// number of ticks since NONE position was first detected 202 | uint8_t detect_timeout = 0; 203 | 204 | bool blacklisted = false; 205 | }; 206 | 207 | class Trackers { 208 | private: 209 | TrackerInfo tracker_info[k_unMaxTrackedDeviceCount] = {}; 210 | TrackedDevicePose_t poses[k_unMaxTrackedDeviceCount]; 211 | 212 | std::set current_trackers = {}; 213 | //TrackedDeviceIndex_t current_trackers[k_unMaxTrackedDeviceCount]; 214 | //uint32_t current_trackers_size = 0; 215 | 216 | SlimeVRBridge &bridge; 217 | public: 218 | VRActiveActionSet_t actionSet; 219 | std::optional> current_universe = std::nullopt; 220 | private: 221 | ETrackingUniverseOrigin universe; 222 | VRActionHandle_t action_handles[(int)BodyPosition::BodyPosition_Count]; 223 | 224 | Trackers(SlimeVRBridge &bridge, ETrackingUniverseOrigin universe): bridge(bridge), universe(universe) {} 225 | 226 | std::optional GetStringProp(TrackedDeviceIndex_t index, ETrackedDeviceProperty prop) { 227 | if (index >= k_unMaxTrackedDeviceCount) { 228 | fmt::print("GetStringProp: Got invalid index {}!\n", index); 229 | return std::nullopt; 230 | } 231 | 232 | auto get_prop = [index, prop](char *prop_str, uint32_t passed_size) { 233 | auto system = VRSystem(); 234 | vr::ETrackedPropertyError prop_error = TrackedProp_Success; 235 | uint32_t size = system->GetStringTrackedDeviceProperty(index, prop, prop_str, passed_size, &prop_error); 236 | 237 | if (size == 0 || (prop_error != TrackedProp_Success && prop_error != TrackedProp_BufferTooSmall)) { 238 | if (prop_error != TrackedProp_Success) { 239 | fmt::print("Error getting {}: IVRSystem::GetStringTrackedDeviceProperty({}): {}\n", size ? "data" : "size", (int)prop, system->GetPropErrorNameFromEnum(prop_error)); 240 | } 241 | 242 | return (uint32_t)0; 243 | } 244 | 245 | return size; 246 | }; 247 | 248 | return GetOpenVRString(get_prop); 249 | } 250 | 251 | std::optional GetLocalizedName(VRInputValueHandle_t handle, EVRInputStringBits flags) { 252 | std::string name = std::string(100, '\0'); 253 | EVRInputError input_error = VRInput()->GetOriginLocalizedName(handle, name.data(), 100, flags | EVRInputStringBits::VRInputString_ControllerType); 254 | 255 | if (input_error != VRInputError_None && input_error != VRInputError_BufferTooSmall) { 256 | if (input_error != VRInputError_None) { 257 | fmt::print("Error getting data: IVRInput::GetOriginLocalizedName(): {}\n", (int)input_error); 258 | } 259 | 260 | return std::nullopt; 261 | } 262 | 263 | name.resize(strlen(name.data())); 264 | 265 | return name; 266 | } 267 | 268 | std::optional GetIndex(VRInputValueHandle_t value_handle) { 269 | InputOriginInfo_t info; 270 | EVRInputError error = VRInput()->GetOriginTrackedDeviceInfo(value_handle, &info, sizeof(info)); 271 | if (error != EVRInputError::VRInputError_None) { 272 | fmt::print("Error: IVRInput::GetOriginTrackedDeviceInfo: {}\n", (int)error); 273 | return std::nullopt; 274 | } 275 | 276 | if (info.trackedDeviceIndex >= k_unMaxTrackedDeviceCount) { 277 | fmt::print("GetIndex: Got invalid index {}!\n", info.trackedDeviceIndex); 278 | return std::nullopt; 279 | } 280 | 281 | return std::make_optional(info.trackedDeviceIndex); 282 | } 283 | 284 | void SetStatus(TrackedDeviceIndex_t index, messages::TrackerStatus_Status status_val, bool send_anyway) { 285 | if (index >= k_unMaxTrackedDeviceCount) { 286 | fmt::print("SetStatus: Got invalid index {}!\n", index); 287 | return; 288 | } 289 | 290 | auto info = tracker_info + index; 291 | 292 | if (info->blacklisted) { 293 | return; // don't send information on blacklisted trackers 294 | } 295 | 296 | if (info->status == status_val && !send_anyway) { 297 | return; // already up to date; 298 | } 299 | 300 | info->status = status_val; 301 | 302 | messages::ProtobufMessage message; 303 | messages::TrackerStatus *status = message.mutable_tracker_status(); 304 | 305 | status->set_status(status_val); 306 | status->set_tracker_id(index); 307 | 308 | bridge.sendMessage(message); 309 | 310 | fmt::print("Device (Index {}) status: {} ({})\n", index, messages::TrackerStatus_Status_Name(status_val), (int)status_val); 311 | } 312 | 313 | void Update(TrackedDeviceIndex_t index, bool just_connected) { 314 | if (index >= k_unMaxTrackedDeviceCount) { 315 | fmt::print("Update: Got invalid index {}!\n", index); 316 | return; 317 | } 318 | auto pose = poses[index]; 319 | auto info = tracker_info + index; 320 | 321 | if (info->state != TrackerState::RUNNING) { 322 | return; 323 | } 324 | 325 | // if(pose.bDeviceIsConnected) { 326 | // info->connection_timeout = 0; 327 | // } 328 | 329 | if (info->blacklisted) { 330 | return; // don't bother with blacklisted trackers 331 | } 332 | 333 | if (pose.bPoseIsValid || pose.eTrackingResult == ETrackingResult::TrackingResult_Fallback_RotationOnly) { 334 | if (pose.eTrackingResult == ETrackingResult::TrackingResult_Fallback_RotationOnly) { 335 | SetStatus(index, messages::TrackerStatus_Status_OCCLUDED, just_connected); 336 | } else { 337 | SetStatus(index, messages::TrackerStatus_Status_OK, just_connected); 338 | } 339 | 340 | HmdQuaternion_t new_rotation = GetRotation(pose.mDeviceToAbsoluteTracking); 341 | HmdVector3_t new_position = GetPosition(pose.mDeviceToAbsoluteTracking); 342 | 343 | if (current_universe.has_value()) { 344 | auto trans = current_universe.value().second; 345 | new_position.v[0] += trans.translation.v[0]; 346 | new_position.v[1] += trans.translation.v[1]; 347 | new_position.v[2] += trans.translation.v[2]; 348 | 349 | // rotate by quaternion w = cos(-trans.yaw / 2), x = 0, y = sin(-trans.yaw / 2), z = 0 350 | auto tmp_w = cos(-trans.yaw / 2); 351 | auto tmp_y = sin(-trans.yaw / 2); 352 | auto new_w = tmp_w * new_rotation.w - tmp_y * new_rotation.y; 353 | auto new_x = tmp_w * new_rotation.x + tmp_y * new_rotation.z; 354 | auto new_y = tmp_w * new_rotation.y + tmp_y * new_rotation.w; 355 | auto new_z = tmp_w * new_rotation.z - tmp_y * new_rotation.x; 356 | 357 | new_rotation.w = new_w; 358 | new_rotation.x = new_x; 359 | new_rotation.y = new_y; 360 | new_rotation.z = new_z; 361 | 362 | // rotate point on the xz plane by -trans.yaw radians 363 | // this is equivilant to the quaternion multiplication, after applying the double angle formula. 364 | float tmp_sin = sin(-trans.yaw); 365 | float tmp_cos = cos(-trans.yaw); 366 | auto pos_x = new_position.v[0] * tmp_cos + new_position.v[2] * tmp_sin; 367 | auto pos_z = new_position.v[0] * -tmp_sin + new_position.v[2] * tmp_cos; 368 | 369 | new_position.v[0] = pos_x; 370 | new_position.v[2] = pos_z; 371 | } 372 | 373 | // send our position message 374 | messages::ProtobufMessage message; 375 | messages::Position *position = message.mutable_position(); 376 | position->set_x(new_position.v[0]); 377 | position->set_y(new_position.v[1]); 378 | position->set_z(new_position.v[2]); 379 | position->set_qw(new_rotation.w); 380 | position->set_qx(new_rotation.x); 381 | position->set_qy(new_rotation.y); 382 | position->set_qz(new_rotation.z); 383 | position->set_tracker_id(index); 384 | position->set_data_source( 385 | pose.eTrackingResult == ETrackingResult::TrackingResult_Fallback_RotationOnly 386 | ? messages::Position_DataSource_IMU 387 | : messages::Position_DataSource_FULL 388 | ); 389 | 390 | bridge.sendMessage(message); 391 | } 392 | 393 | // send status update on change, or if we just connected. 394 | if (!pose.bDeviceIsConnected) { 395 | SetStatus(index, messages::TrackerStatus_Status_DISCONNECTED, just_connected); 396 | } else if (!pose.bPoseIsValid) { 397 | if (pose.eTrackingResult == ETrackingResult::TrackingResult_Calibrating_OutOfRange) { 398 | SetStatus(index, messages::TrackerStatus_Status_OCCLUDED, just_connected); 399 | } else { 400 | SetStatus(index, messages::TrackerStatus_Status_ERROR, just_connected); 401 | } 402 | } 403 | } 404 | 405 | void SetPosition(TrackedDeviceIndex_t index, SlimeVRPosition position, bool send_anyway) { 406 | if (index >= k_unMaxTrackedDeviceCount) { 407 | fmt::print("SetPosition: Got invalid index {}!\n", index); 408 | return; 409 | } 410 | auto info = tracker_info + index; 411 | 412 | info->connection_timeout = 0; 413 | 414 | if (info->blacklisted) { 415 | return; // don't send information on blacklisted trackers 416 | } 417 | 418 | bool should_send = false; 419 | 420 | switch (info->state) { 421 | case TrackerState::DISCONNECTED: 422 | info->position = position; 423 | if (position == SlimeVRPosition::None) { 424 | info->state = TrackerState::WAITING; 425 | info->detect_timeout = 0; 426 | fmt::print("Waiting for role for \"{}\" with index {}\n", info->name, index); 427 | } else { 428 | should_send = true; 429 | info->state = TrackerState::RUNNING; 430 | } 431 | break; 432 | 433 | case TrackerState::WAITING: 434 | if (position != SlimeVRPosition::None || info->detect_timeout >= 100) { 435 | if (info->detect_timeout >= 100) { 436 | fmt::print("Role timeout reached for index {}.\n", index); 437 | } 438 | info->position = position; 439 | info->state = TrackerState::RUNNING; 440 | should_send = true; 441 | } else { 442 | // increment timeout 443 | info->detect_timeout += 1; 444 | } 445 | break; 446 | 447 | case TrackerState::RUNNING: 448 | if (position != SlimeVRPosition::None && position != info->position) { 449 | info->position = position; 450 | should_send = true; 451 | } 452 | break; 453 | } 454 | 455 | if (should_send || (send_anyway && info->state == TrackerState::RUNNING)) { 456 | messages::ProtobufMessage message; 457 | messages::TrackerAdded *added = message.mutable_tracker_added(); 458 | added->set_tracker_id(index); 459 | added->set_tracker_role((int)info->position); 460 | added->set_tracker_name(info->name); 461 | if (info->serial.has_value()) { 462 | added->set_tracker_serial(info->serial.value()); 463 | } 464 | 465 | bridge.sendMessage(message); 466 | 467 | // log it. 468 | fmt::print("Found device \"{}\" at {} ({}) with index {}\n", info->name, positionNames[(int)info->position], (int)info->position, index); 469 | } 470 | } 471 | 472 | public: 473 | static std::optional Create(SlimeVRBridge &bridge, ETrackingUniverseOrigin universe); 474 | 475 | void Detect(bool just_connected, bool enable_hmd) { 476 | current_trackers.clear(); 477 | uint32_t all_trackers_size = 0; 478 | TrackedDeviceIndex_t all_trackers[k_unMaxTrackedDeviceCount]; 479 | 480 | // only detect the HMD if the user requests it. 481 | if (enable_hmd) { 482 | all_trackers_size += VRSystem()->GetSortedTrackedDeviceIndicesOfClass(TrackedDeviceClass_HMD, all_trackers, k_unMaxTrackedDeviceCount); 483 | } 484 | // detect controllers and trackers, regardless of role. 485 | all_trackers_size += VRSystem()->GetSortedTrackedDeviceIndicesOfClass(TrackedDeviceClass_Controller, all_trackers + all_trackers_size, k_unMaxTrackedDeviceCount - all_trackers_size); 486 | all_trackers_size += VRSystem()->GetSortedTrackedDeviceIndicesOfClass(TrackedDeviceClass_GenericTracker, all_trackers + all_trackers_size, k_unMaxTrackedDeviceCount - all_trackers_size); 487 | 488 | if (just_connected) { 489 | fmt::print("number of trackers: {}\n", all_trackers_size); 490 | } 491 | 492 | for (auto iii = 0; iii < all_trackers_size; ++iii) { 493 | auto index = all_trackers[iii]; 494 | auto driver = this->GetStringProp(index, ETrackedDeviceProperty::Prop_TrackingSystemName_String); 495 | auto info = tracker_info + index; 496 | 497 | info->blacklisted = (driver == "SlimeVR" || driver == "slimevr" || driver == "standable"); 498 | 499 | // only write values once, to avoid overwriting good values later. 500 | if (info->name == "") { 501 | auto controller_type = this->GetStringProp(index, ETrackedDeviceProperty::Prop_ControllerType_String); 502 | if (controller_type.has_value()) { 503 | info->name = controller_type.value(); 504 | } else { 505 | // uhhhhhhhhhhhhhhh 506 | info->name = fmt::format("Index{}", index); 507 | } 508 | } 509 | 510 | info->serial = this->GetStringProp(index, ETrackedDeviceProperty::Prop_SerialNumber_String); 511 | 512 | current_trackers.insert(index); 513 | 514 | SetPosition(index, SlimeVRPosition::None, just_connected); 515 | } 516 | 517 | // detect roles, more specific names 518 | auto input = VRInput(); 519 | EVRInputError input_error = input->UpdateActionState(&actionSet, sizeof(VRActiveActionSet_t), 1); 520 | if (input_error != EVRInputError::VRInputError_None) { 521 | fmt::print("Error: IVRInput::UpdateActionState: {}\n", (int)input_error); 522 | return; 523 | } 524 | 525 | for (unsigned int jjj = 0; jjj < (int)BodyPosition::BodyPosition_Count; ++jjj) { 526 | if (!enable_hmd && jjj == (int)BodyPosition::Head) { 527 | continue; // don't query the head if we aren't reporting it. 528 | } 529 | 530 | InputPoseActionData_t pose; 531 | input_error = input->GetPoseActionDataRelativeToNow(action_handles[jjj], universe, 0, &pose, sizeof(pose), 0); 532 | if (input_error != EVRInputError::VRInputError_None) { 533 | fmt::print("Error: IVRInput::GetPoseActionDataRelativeToNow: {}\n", (int)input_error); 534 | continue; 535 | } 536 | 537 | if (pose.bActive) { 538 | std::optional trackedDeviceIndex = GetIndex(pose.activeOrigin); 539 | if (!trackedDeviceIndex.has_value()) { 540 | // already printed a message about this in GetIndex, just continue. 541 | continue; 542 | } 543 | 544 | auto index = trackedDeviceIndex.value(); 545 | 546 | // TODO: I feel like this 'only left+right hand' thing is going to bite us in the ass later with things that aren't index/vive trackers. 547 | // oh well. 548 | auto name = GetLocalizedName( 549 | pose.activeOrigin, 550 | (jjj == (int)BodyPosition::LeftHand || jjj == (int)BodyPosition::RightHand) 551 | ? EVRInputStringBits::VRInputString_Hand 552 | : (EVRInputStringBits)0 553 | ); 554 | if (name.has_value()) { 555 | tracker_info[index].name = name.value(); 556 | } 557 | 558 | current_trackers.insert(index); 559 | 560 | SetPosition(index, positionIDs[jjj], just_connected); 561 | } 562 | } 563 | 564 | for (auto iii = 0; iii < k_unMaxTrackedDeviceCount; ++iii) { 565 | auto info = tracker_info + iii; 566 | 567 | if (info->state == TrackerState::DISCONNECTED) { 568 | continue; 569 | } 570 | 571 | if (info->connection_timeout >= 100) { 572 | fmt::print("Tracker connection timeout.\n"); 573 | info->state = TrackerState::DISCONNECTED; 574 | SetStatus(iii, messages::TrackerStatus_Status_DISCONNECTED, just_connected); 575 | info->name = ""; 576 | info->connection_timeout = 0; 577 | } else { 578 | info->connection_timeout += 1; 579 | } 580 | } 581 | } 582 | 583 | void Tick(bool just_connected) { 584 | VRSystem()->GetDeviceToAbsoluteTrackingPose(universe, 0, poses, k_unMaxTrackedDeviceCount); 585 | for (TrackedDeviceIndex_t index: current_trackers) { 586 | Update(index, just_connected); 587 | } 588 | } 589 | 590 | std::optional HandleDigitalActionBool(VRActionHandle_t action_handle, std::optional server_name = std::nullopt) { 591 | InputDigitalActionData_t action_data; 592 | EVRInputError input_error = VRInputError_None; 593 | 594 | input_error = VRInput()->GetDigitalActionData(action_handle, &action_data, sizeof(InputDigitalActionData_t), 0); 595 | if (input_error == EVRInputError::VRInputError_None) { 596 | constexpr bool falling_edge = false; // rising edge for now, making it easy to switch for now just in case. 597 | if (action_data.bChanged && (action_data.bState ^ falling_edge) && server_name.has_value()) { 598 | messages::ProtobufMessage message; 599 | messages::UserAction *userAction = message.mutable_user_action(); 600 | userAction->set_name(server_name.value()); 601 | 602 | fmt::print("Sending {} action\n", server_name.value()); 603 | 604 | bridge.sendMessage(message); 605 | } 606 | 607 | return action_data; 608 | } else { 609 | fmt::print("Error: VRInput::GetDigitalActionData(\"{}\"): {}\n", server_name.value_or(""), (int)input_error); 610 | return {}; 611 | } 612 | } 613 | }; 614 | 615 | std::optional Trackers::Create(SlimeVRBridge &bridge, ETrackingUniverseOrigin universe){ 616 | VRActionSetHandle_t action_set_handle; 617 | EVRInputError input_error; 618 | Trackers result(bridge, universe); 619 | 620 | std::string actionsFileName = Path_MakeAbsolute(actions_path, Path_StripFilename(Path_GetExecutablePath())); 621 | 622 | if ((input_error = VRInput()->SetActionManifestPath(actionsFileName.c_str())) != EVRInputError::VRInputError_None) { 623 | fmt::print("Error: IVRInput::SetActionManifectPath: {}\n", (int)input_error); 624 | return std::nullopt; 625 | } 626 | 627 | if ((input_error = VRInput()->GetActionSetHandle("/actions/main", &action_set_handle)) != EVRInputError::VRInputError_None) { 628 | fmt::print("Error: VRInput::GetActionSetHandle: {}\n", (int)input_error); 629 | return std::nullopt; 630 | } 631 | 632 | result.actionSet = { 633 | action_set_handle, 634 | k_ulInvalidInputValueHandle, 635 | k_ulInvalidActionSetHandle, 636 | 0, 637 | 0 638 | }; 639 | 640 | for (unsigned int iii = 0; iii < (int)BodyPosition::BodyPosition_Count; ++iii) { 641 | result.action_handles[iii] = GetAction(actions[iii]); 642 | } 643 | 644 | return result; 645 | } 646 | 647 | volatile static sig_atomic_t should_exit = 0; 648 | 649 | void handle_signal(int num) { 650 | // reinstall, in case it goes back to default. 651 | //signal(num, handle_signal); 652 | switch (num) { 653 | case SIGINT: 654 | should_exit = 1; 655 | break; 656 | } 657 | } 658 | 659 | static const std::unordered_map> universe_map { 660 | {"seated", {ETrackingUniverseOrigin::TrackingUniverseSeated, false}}, 661 | {"standing", {ETrackingUniverseOrigin::TrackingUniverseStanding, false}}, 662 | {"raw", {ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated, false}}, 663 | {"static_standing", {ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated, true}} 664 | }; 665 | // default is static_standing 666 | static constexpr std::pair universe_default = {ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated, true}; 667 | 668 | // TEMP, cba to setup a proper header file. 669 | void test_lto(); 670 | 671 | std::optional search_universe(simdjson::ondemand::parser &json_parser, uint64_t target) { 672 | uint32_t length = 0; 673 | VRChaperoneSetup()->ExportLiveToBuffer(nullptr, &length); 674 | 675 | // compile time check to ensure we're being sane, otherwise this would be a buffer overrun! 676 | static_assert(simdjson::SIMDJSON_PADDING >= 1, "simdjson doesn't specify enough padding for a trailing null byte!"); 677 | // i'd say something about caching a padded string to avoid allocation 678 | // but if it's not *exactly* the same size we'd need to allocate anyway 679 | // because simdjson doesn't allow modifying the valid length. 680 | auto json = simdjson::padded_string(length - 1); 681 | 682 | if (!VRChaperoneSetup()->ExportLiveToBuffer(json.data(), &length)) { 683 | return std::nullopt; 684 | } 685 | 686 | simdjson::ondemand::document doc; 687 | try { 688 | doc = json_parser.iterate(json); 689 | 690 | for (simdjson::ondemand::object uni: doc["universes"]) { 691 | // TODO: universeID comes after the translation, would it be faster to unconditionally parse the translation? 692 | auto res = uni.find_field_unordered("universeID"); 693 | if (res.error()) { 694 | static bool missingId = false; 695 | if (!missingId) { 696 | missingId = true; 697 | fmt::print("Warning: 'universes' are present that don't have a universeID, skipping."); 698 | } 699 | continue; 700 | } 701 | simdjson::ondemand::value elem = res.value_unsafe(); // uni["universeID"]; 702 | 703 | uint64_t parsed_universe; 704 | auto is_integer = elem.is_integer(); 705 | if (!is_integer.error() && is_integer.value_unsafe()) { 706 | parsed_universe = elem.get_uint64(); 707 | } else { 708 | parsed_universe = elem.get_uint64_in_string(); 709 | } 710 | if (parsed_universe == target) { 711 | return UniverseTranslation::parse(uni["standing"].get_object().value()); 712 | } 713 | } 714 | } catch (simdjson::simdjson_error& e) { 715 | std::string_view raw_token_view; 716 | 717 | static bool parse_error = false; 718 | if (parse_error) { 719 | return std::nullopt; 720 | } 721 | 722 | if (!doc.raw_json_token().get(raw_token_view)) { 723 | fmt::print("Error while parsing steamvr universes: {}\nraw_token: |{}|\n", e.what(), raw_token_view); 724 | } else { 725 | fmt::print("Error while parsing steamvr universes: {}\n", e.what()); 726 | } 727 | 728 | parse_error = true; 729 | 730 | return std::nullopt; 731 | } 732 | 733 | return std::nullopt; 734 | } 735 | 736 | int main(int argc, char* argv[]) { 737 | GOOGLE_PROTOBUF_VERIFY_VERSION; 738 | 739 | // test_lto(); 740 | 741 | args::ArgumentParser parser("Feeds controller/tracker data to SlimeVR Server.", "This program also parses arguments from a config file \"config.txt\" in the same directory as the executable. It is formatted as one line per option, and ignores characters on a line after a '#' character. Options passed on the command line are parsed after those read from the config file, and thus override options read from the config file."); 742 | args::HelpFlag help(parser, "help", "Display this help menu", {'h', "help"}); 743 | args::CompletionFlag completion(parser, {"complete"}); 744 | 745 | args::MapFlag> universe( 746 | parser, 747 | "universe", 748 | "Tracking Universe. Possible values:\n" 749 | " raw: raw/uncalibrated space\n" 750 | " seated: seated universe\n" 751 | " standing: standing universe\n" 752 | " static_standing: standing universe unaffected by playspace movement (this matches slimevr driver, default)", 753 | {"universe"}, 754 | universe_map, 755 | universe_default 756 | ); 757 | args::ValueFlag tps(parser, "tps", "Ticks per second. i.e. the number of times per second to send tracking information to slimevr server. Default is 100.", {"tps"}, 100); 758 | args::Flag enable_hmd(parser, "hmd", "Enabled sending the HMD position along with controller/tracker information.", {"hmd"}); 759 | 760 | args::Group setup_group(parser, "Setup options", args::Group::Validators::AtMostOne); 761 | args::Flag install(setup_group, "install", "Installs the manifest and enables autostart. Used by the installer.", {"install"}); 762 | args::Flag uninstall(setup_group, "uninstall", "Removes the manifest file.", {"uninstall"}); 763 | 764 | args::Flag show_console(parser, "console", "Show the command line output which is hidden by default on Windows.", {"console"}); 765 | 766 | std::string configFileName = Path_MakeAbsolute(config_path, Path_StripFilename(Path_GetExecutablePath())); 767 | std::ifstream configFile(configFileName); 768 | 769 | std::vector args; 770 | 771 | for (std::string line; std::getline(configFile, line); ) { 772 | const auto comment_pos = line.find("#"); 773 | if (comment_pos == 0) { 774 | continue; // line immediately starts with a comment. 775 | } 776 | const auto end = line.find_last_not_of(" \t", comment_pos - 1); 777 | if (end == std::string::npos) { 778 | continue; // line consists of only blank characters or comments. 779 | } 780 | const auto begin = line.find_first_not_of(" \t"); 781 | if (begin == std::string::npos) { 782 | continue; // line consists of only blank characters. should be caught by the previous if statement, though. 783 | } 784 | 785 | line = line.substr(begin, end - begin + 1); // perform the actual trimming 786 | 787 | args.push_back(line); 788 | } 789 | 790 | // place command line arguments on the end, to override any arguments in the config file. 791 | for (int iii = 1; iii < argc; ++iii) { 792 | args.push_back(argv[iii]); 793 | } 794 | 795 | try { 796 | parser.Prog(argv[0]); 797 | parser.ParseArgs(args); 798 | } catch (args::Help) { 799 | std::cout << parser; 800 | return 0; 801 | } catch (args::ParseError e) { 802 | std::cerr << e.what() << std::endl; 803 | std::cerr << parser; 804 | return 1; 805 | } catch (args::ValidationError e) { 806 | std::cerr << e.what() << std::endl; 807 | std::cerr << parser; 808 | return 1; 809 | } 810 | 811 | if (install || uninstall) { 812 | return handle_setup(install); 813 | } 814 | 815 | fmt::print("SlimeVR-Feeder-App version {}\n\n", version); 816 | 817 | EVRInitError init_error = VRInitError_None; 818 | EVRInputError input_error = VRInputError_None; 819 | 820 | signal(SIGINT, handle_signal); 821 | 822 | std::unique_ptr system(VR_Init(&init_error, VRApplication_Overlay), &shutdown_vr); 823 | if (init_error != VRInitError_None) { 824 | system = nullptr; 825 | fmt::print("Unable to init VR runtime: {}\n", VR_GetVRInitErrorAsEnglishDescription(init_error)); 826 | return EXIT_FAILURE; 827 | } 828 | 829 | // Ensure VR Compositor is available, otherwise getting poses causes a crash (openvr v1.3.22) 830 | if (!VRCompositor()) { 831 | std::cout << "Failed to initialize VR compositor!" << std::endl; 832 | return EXIT_FAILURE; 833 | } 834 | 835 | auto bridge = SlimeVRBridge::factory(); 836 | auto tracking_universe = universe.Get().first; 837 | bool use_vrchaperone = universe.Get().second; 838 | std::optional maybe_trackers = Trackers::Create(*bridge, tracking_universe); 839 | if (!maybe_trackers.has_value()) { 840 | return EXIT_FAILURE; 841 | } 842 | 843 | #if defined(WIN32) 844 | // Hide command line output after any potential error 845 | if (!show_console) { 846 | // Thanks https://stackoverflow.com/a/78943791 847 | HWND console = GetConsoleWindow(); 848 | HWND console_owner = GetWindow(console, GW_OWNER); 849 | if (console_owner == NULL) { 850 | ShowWindow(console, SW_HIDE); 851 | } 852 | else { 853 | ShowWindow(console_owner, SW_HIDE); // Windows Terminal 854 | } 855 | } 856 | #endif 857 | 858 | Trackers trackers = maybe_trackers.value(); 859 | 860 | VRActionHandle_t calibration_action = GetAction("/actions/main/in/request_calibration"); 861 | VRActionHandle_t fast_reset_action = GetAction("/actions/main/in/fast_reset"); 862 | VRActionHandle_t mounting_reset_action = GetAction("/actions/main/in/mounting_reset"); 863 | VRActionHandle_t pause_tracking_action = GetAction("/actions/main/in/pause_tracking"); 864 | 865 | //trackers.Detect(false); 866 | 867 | auto tick_ns = std::chrono::nanoseconds(1'000'000'000 / tps.Get()); 868 | auto next_tick = std::chrono::duration_cast( 869 | std::chrono::high_resolution_clock::now().time_since_epoch() 870 | ); 871 | 872 | bool overlay_was_open = false; 873 | 874 | auto json_parser = simdjson::ondemand::parser(); 875 | 876 | // event loop 877 | while (!should_exit) { 878 | bool just_connected = bridge->runFrame(); 879 | 880 | VREvent_t event; 881 | // each loop is now spaced apart, so let's process all events right now. 882 | while (system->PollNextEvent(&event, sizeof(event))) { 883 | switch (event.eventType) { 884 | case VREvent_Quit: 885 | return 0; 886 | 887 | // TODO: add more events, or remove some events? 888 | // case VREvent_TrackedDeviceActivated: 889 | // case VREvent_TrackedDeviceDeactivated: 890 | // case VREvent_TrackedDeviceRoleChanged: 891 | // case VREvent_TrackedDeviceUpdated: 892 | // case VREvent_DashboardDeactivated: 893 | // trackers.Detect(just_connected); 894 | // break; 895 | 896 | default: 897 | //fmt::print("Unhandled event: {}({})\n", system->GetEventTypeNameFromEnum((EVREventType)event.eventType), event.eventType); 898 | // I'm not relying on events to actually trigger anything right now, so don't bother printing anything. 899 | break; 900 | } 901 | } 902 | 903 | messages::ProtobufMessage recievedMessage; 904 | // TODO: I don't think there are any messages from the server that we care about at the moment, but let's make sure to not let the pipe fill up. 905 | bridge->getNextMessage(recievedMessage); 906 | 907 | // TODO: are there events we should be listening to in order to fire this? 908 | uint64_t universe = VRSystem()->GetUint64TrackedDeviceProperty(0, Prop_CurrentUniverseId_Uint64); 909 | if (use_vrchaperone && (!trackers.current_universe.has_value() || trackers.current_universe.value().first != universe)) { 910 | auto res = search_universe(json_parser, universe); 911 | if (res.has_value()) { 912 | trackers.current_universe.emplace(universe, res.value()); 913 | } 914 | } 915 | 916 | // TODO: don't do this every loop, we really shouldn't need to. 917 | if (VROverlay()->IsDashboardVisible()) { 918 | if (!overlay_was_open) { 919 | fmt::print("Dashboard open, pausing detection.\n"); 920 | } 921 | overlay_was_open = true; 922 | 923 | // we should still be updating the action state, to get DigitalActions while the dashboard is open. 924 | VRInput()->UpdateActionState(&trackers.actionSet, sizeof(VRActiveActionSet_t), 1); 925 | } else { 926 | if (overlay_was_open) { 927 | fmt::print("Dashboard closed, re-enabling tracker detection.\n"); 928 | } 929 | overlay_was_open = false; 930 | trackers.Detect(just_connected, enable_hmd); 931 | } 932 | 933 | // TODO: rename these actions as appropriate, perhaps log them? 934 | trackers.HandleDigitalActionBool(calibration_action, { "reset" }); 935 | trackers.HandleDigitalActionBool(fast_reset_action, { "fast_reset" }); 936 | trackers.HandleDigitalActionBool(mounting_reset_action, { "mounting_reset" }); 937 | trackers.HandleDigitalActionBool(pause_tracking_action, { "pause_tracking" }); 938 | 939 | trackers.Tick(just_connected); 940 | 941 | next_tick += tick_ns; 942 | 943 | auto wait_ns = next_tick - std::chrono::duration_cast( 944 | std::chrono::high_resolution_clock::now().time_since_epoch() 945 | ); 946 | 947 | if (wait_ns.count() > 0) { 948 | std::this_thread::sleep_for(wait_ns); 949 | } else { 950 | // I don't care if you want more TPS than the feeder can provide, I'm yielding to the OS anyway. 951 | // if this is really an issue for someone, they can open an issue. 952 | std::this_thread::yield(); 953 | } 954 | } 955 | 956 | fmt::print("Exiting cleanly!\n"); 957 | 958 | return 0; 959 | } 960 | --------------------------------------------------------------------------------