From ab049ba699e660c106bf11b16e3f41b6e5c0d2dc Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:27:33 -0400 Subject: [PATCH] feat: implement libvirtualhid --- .gitmodules | 8 +- CMakeLists.txt | 3 +- cmake/compile_definitions/linux.cmake | 23 +- cmake/compile_definitions/windows.cmake | 7 + docs/configuration.md | 29 +- docs/getting_started.md | 3 +- docs/troubleshooting.md | 4 +- gh-pages-template/_data/features.yml | 1 - src/config.cpp | 4 +- src/config.h | 2 +- src/input.cpp | 8 +- src/platform/linux/input/inputtino.cpp | 163 ---- src/platform/linux/input/inputtino_common.h | 147 ---- .../linux/input/inputtino_gamepad.cpp | 337 -------- src/platform/linux/input/inputtino_gamepad.h | 88 -- .../linux/input/inputtino_keyboard.cpp | 215 ----- src/platform/linux/input/inputtino_keyboard.h | 36 - src/platform/linux/input/inputtino_mouse.cpp | 104 --- src/platform/linux/input/inputtino_mouse.h | 69 -- src/platform/linux/input/inputtino_pen.cpp | 72 -- src/platform/linux/input/inputtino_pen.h | 27 - src/platform/linux/input/inputtino_seat.cpp | 21 - src/platform/linux/input/inputtino_seat.h | 19 - src/platform/linux/input/inputtino_touch.cpp | 59 -- src/platform/linux/input/inputtino_touch.h | 27 - src/platform/linux/input/virtualhid.cpp | 155 ++++ src/platform/virtualhid_input.cpp | 786 ++++++++++++++++++ src/platform/virtualhid_input.h | 224 +++++ src/platform/windows/input.cpp | 134 ++- src_assets/common/assets/web/config.html | 2 +- .../common/assets/web/configs/tabs/Inputs.vue | 28 +- .../assets/web/public/assets/locale/en.json | 22 +- tests/unit/test_mouse.cpp | 4 +- third-party/inputtino | 1 - third-party/libvirtualhid | 1 + 35 files changed, 1351 insertions(+), 1482 deletions(-) delete mode 100644 src/platform/linux/input/inputtino.cpp delete mode 100644 src/platform/linux/input/inputtino_common.h delete mode 100644 src/platform/linux/input/inputtino_gamepad.cpp delete mode 100644 src/platform/linux/input/inputtino_gamepad.h delete mode 100644 src/platform/linux/input/inputtino_keyboard.cpp delete mode 100644 src/platform/linux/input/inputtino_keyboard.h delete mode 100644 src/platform/linux/input/inputtino_mouse.cpp delete mode 100644 src/platform/linux/input/inputtino_mouse.h delete mode 100644 src/platform/linux/input/inputtino_pen.cpp delete mode 100644 src/platform/linux/input/inputtino_pen.h delete mode 100644 src/platform/linux/input/inputtino_seat.cpp delete mode 100644 src/platform/linux/input/inputtino_seat.h delete mode 100644 src/platform/linux/input/inputtino_touch.cpp delete mode 100644 src/platform/linux/input/inputtino_touch.h create mode 100644 src/platform/linux/input/virtualhid.cpp create mode 100644 src/platform/virtualhid_input.cpp create mode 100644 src/platform/virtualhid_input.h delete mode 160000 third-party/inputtino create mode 160000 third-party/libvirtualhid diff --git a/.gitmodules b/.gitmodules index e1b90b92633..db5717cd2f8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,14 +13,14 @@ [submodule "third-party/glad"] path = third-party/glad url = https://github.com/Dav1dde/glad.git -[submodule "third-party/inputtino"] - path = third-party/inputtino - url = https://github.com/games-on-whales/inputtino.git - branch = stable [submodule "third-party/libdisplaydevice"] path = third-party/libdisplaydevice url = https://github.com/LizardByte/libdisplaydevice.git branch = master +[submodule "third-party/libvirtualhid"] + path = third-party/libvirtualhid + url = https://github.com/LizardByte/libvirtualhid.git + branch = master [submodule "third-party/lizardbyte-common"] path = third-party/lizardbyte-common url = https://github.com/LizardByte/lizardbyte-common.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c94c1ffa5b..7e544083d4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,8 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) # `CMAKE_CUDA_ARCHITECTURES` requires 3.18 # `set_source_files_properties` requires 3.18 # `cmake_path(CONVERT ... TO_NATIVE_PATH_LIST ...)` requires 3.20 +# `third-party/libvirtualhid` requires 3.24 # todo - set this conditionally project(Sunshine VERSION 0.0.0 diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 60c90349377..469adbafbe4 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -297,23 +297,20 @@ if(NOT ${CUDA_FOUND} message(FATAL_ERROR "Couldn't find either cuda, libdrm, libva, kwin, pipewire, portal, wayland or x11") endif() -# These need to be set before adding the inputtino subdirectory in order for them to be picked up +# These need to be set before adding the libvirtualhid subdirectory in order for them to be picked up set(LIBEVDEV_CUSTOM_INCLUDE_DIR "${EVDEV_INCLUDE_DIR}") set(LIBEVDEV_CUSTOM_LIBRARY "${EVDEV_LIBRARY}") -if(FREEBSD) - set(USE_UHID OFF) -endif() -add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/inputtino") -list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino) -file(GLOB_RECURSE INPUTTINO_SOURCES - ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.h - ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.cpp) -list(APPEND PLATFORM_TARGET_FILES ${INPUTTINO_SOURCES}) +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libvirtualhid") +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES libvirtualhid::libvirtualhid) +list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.h" + "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/input/virtualhid.cpp") -# build libevdev before the libinputtino target -if(EXTERNAL_PROJECT_LIBEVDEV_USED) - add_dependencies(libinputtino libevdev) +# build libevdev before the libvirtualhid target when using the ExternalProject fallback +if(EXTERNAL_PROJECT_LIBEVDEV_USED AND TARGET libvirtualhid) + add_dependencies(libvirtualhid libevdev) endif() # AppImage and Flatpak diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index e3e57339f18..257c61dba82 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -40,6 +40,9 @@ file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS # vigem include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include") +# libvirtualhid +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libvirtualhid") + # sunshine icon if(NOT DEFINED SUNSHINE_ICON_PATH) set(SUNSHINE_ICON_PATH "${CMAKE_SOURCE_DIR}/sunshine.ico") @@ -73,6 +76,8 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.h" + "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.h" + "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" @@ -105,3 +110,5 @@ list(PREPEND PLATFORM_LIBRARIES ws2_32 wsock32 ) + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES libvirtualhid::libvirtualhid) diff --git a/docs/configuration.md b/docs/configuration.md index e9fe004ddde..ad9bc91c36a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -332,30 +332,40 @@ editing the `conf` file in a text editor. Use the examples as reference. @endcode - Choices + Choices + generic + Generic HID gamepad + @note{This option applies to FreeBSD, Linux, and Windows.} + + ds4 DualShock 4 controller (PS4) - @note{This option applies to Windows only.} + @note{This option applies to FreeBSD, Linux, and Windows.} ds5 DualShock 5 controller (PS5) - @note{This option applies to FreeBSD and Linux only.} + @note{This option applies to FreeBSD, Linux, and Windows.} switch Switch Pro controller - @note{This option applies to FreeBSD and Linux only.} + @note{This option applies to FreeBSD, Linux, and Windows.} x360 Xbox 360 controller - @note{This option applies to Windows only.} + @note{This option applies to FreeBSD, Linux, and Windows.} xone Xbox One controller - @note{This option applies to FreeBSD and Linux only.} + @note{This option applies to FreeBSD, Linux, and Windows.} + + + xseries + Xbox Series controller + @note{This option applies to FreeBSD, Linux, and Windows.} @@ -440,14 +450,13 @@ editing the `conf` file in a text editor. Use the examples as reference. -### ds5_inputtino_randomize_mac +### virtualhid_randomize_mac @@ -459,7 +468,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
Description - Randomize the MAC-Address for the generated virtual controller. - @hint{Only applies on linux for gamepads created as PS5-style controllers} + Randomize the MAC address for PlayStation-style virtual controllers created by libvirtualhid.
Example @code{} - ds5_inputtino_randomize_mac = enabled + virtualhid_randomize_mac = enabled @endcode
diff --git a/docs/getting_started.md b/docs/getting_started.md index 1e3582c4d56..16de4081688 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -479,7 +479,8 @@ and enter its device name in the [audio_sink](configuration.md#audio_sink) field > Gamepads are not currently supported. ### Windows -In order for virtual gamepads to work, you must install ViGEmBus. You can do this from the troubleshooting tab +Sunshine uses libvirtualhid for virtual gamepads on Windows. ViGEmBus is only used as a fallback for Xbox 360 +and DualShock 4 gamepads when libvirtualhid is unavailable. You can install ViGEmBus from the troubleshooting tab in the web UI, as long as you are running Sunshine as a service or as an administrator. After installation, it is recommended to restart your computer. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f59b7ef213f..4ae6839e3db 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -255,7 +255,9 @@ launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist ## Windows ### No gamepad detected -You must install ViGEmBus to use virtual gamepads. You can install this from the troubleshooting tab of the web UI. +Sunshine uses libvirtualhid for virtual gamepads on Windows. ViGEmBus is only needed as a fallback for Xbox 360 +and DualShock 4 gamepads when libvirtualhid is unavailable. You can install ViGEmBus from the troubleshooting tab +of the web UI. Alternatively, you can manually install it from [ViGEmBus releases](https://github.com/nefarius/ViGEmBus/releases/latest). You must use version 1.17 or newer. diff --git a/gh-pages-template/_data/features.yml b/gh-pages-template/_data/features.yml index 595fa3288f2..ac66b9eabd8 100644 --- a/gh-pages-template/_data/features.yml +++ b/gh-pages-template/_data/features.yml @@ -30,7 +30,6 @@ Sunshine emulates an Xbox, PlayStation, or Nintendo Switch controller. Use nearly any controller on your Moonlight client!
diff --git a/src/config.cpp b/src/config.cpp index e1dab3516fb..b286c688d57 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -758,7 +758,7 @@ namespace config { true, // back as touchpad click enabled (manual DS4 only) true, // client gamepads with motion events are emulated as DS4 true, // client gamepads with touchpads are emulated as DS4 - true, // ds5_inputtino_randomize_mac + true, // virtualhid_randomize_mac true, // keyboard enabled true, // mouse enabled @@ -1690,7 +1690,7 @@ namespace config { bool_f(vars, "ds4_back_as_touchpad_click", input.ds4_back_as_touchpad_click); bool_f(vars, "motion_as_ds4", input.motion_as_ds4); bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4); - bool_f(vars, "ds5_inputtino_randomize_mac", input.ds5_inputtino_randomize_mac); + bool_f(vars, "virtualhid_randomize_mac", input.virtualhid_randomize_mac); bool_f(vars, "mouse", input.mouse); bool_f(vars, "keyboard", input.keyboard); diff --git a/src/config.h b/src/config.h index cbe0978ede4..0d8681dddf1 100644 --- a/src/config.h +++ b/src/config.h @@ -274,7 +274,7 @@ namespace config { bool ds4_back_as_touchpad_click; ///< Map the DS4 Back button to a touchpad click. bool motion_as_ds4; ///< Expose motion controls through the DS4 protocol. bool touchpad_as_ds4; ///< Expose touchpad input through the DS4 protocol. - bool ds5_inputtino_randomize_mac; ///< Randomize the inputtino DualSense MAC address. + bool virtualhid_randomize_mac; ///< Randomize the libvirtualhid virtual controller MAC address. bool keyboard; ///< Enable keyboard input from clients. bool key_rightalt_to_key_win; ///< Map the client Right Alt key to the Windows key. diff --git a/src/input.cpp b/src/input.cpp index 8108bcba378..0716a29381f 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -13,6 +13,7 @@ extern "C" { #include #include #include +#include #include #include @@ -610,8 +611,8 @@ namespace input { This final operation is a bit weird and has been brought about with lots of trial and error. A better way to do this may exist. - Basically, this is what makes the touchscreen map to the coordinates inputtino expects properly. - Since inputtino's dimensions are now logical (because scaling breaks everything otherwise), using the previous + Basically, this is what makes the touchscreen map to the logical virtual input coordinates properly. + Since the virtual input dimensions are logical (because scaling breaks everything otherwise), using the previous x and y coordinates would be incorrect when screens are scaled, because the touch port is smaller (or larger) by a factor (that factor is touch_port.scalar_tpcoords), and that factor must be used to account for that difference when moving the cursor. Otherwise, it will move either slower or faster than your finger proportionally to @@ -1880,8 +1881,7 @@ namespace input { * @brief Probe connected gamepads and update input capability state. */ bool probe_gamepads() { - auto input = static_cast(platf_input.get()); - const auto gamepads = platf::supported_gamepads(input); + const auto gamepads = platf::supported_gamepads(std::addressof(platf_input)); for (auto &gamepad : gamepads) { if (gamepad.is_enabled && gamepad.name != "auto") { return false; diff --git a/src/platform/linux/input/inputtino.cpp b/src/platform/linux/input/inputtino.cpp deleted file mode 100644 index c0e8a1dcde0..00000000000 --- a/src/platform/linux/input/inputtino.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino.cpp - * @brief Definitions for the inputtino Linux input handling. - */ -// lib includes -#include -#include - -// local includes -#include "inputtino_common.h" -#include "inputtino_gamepad.h" -#include "inputtino_keyboard.h" -#include "inputtino_mouse.h" -#include "inputtino_pen.h" -#include "inputtino_touch.h" -#include "src/config.h" -#include "src/platform/common.h" -#include "src/utility.h" - -using namespace std::literals; - -namespace platf { - - /** - * @brief Create the platform input backend for a stream. - */ - input_t input() { - return {new input_raw_t()}; - } - - std::unique_ptr allocate_client_input_context(input_t &input) { - return std::make_unique(input); - } - - /** - * @brief Release a platform input backend created by input(). - */ - void freeInput(void *p) { - auto *input = (input_raw_t *) p; - delete input; - } - - /** - * @brief Move mouse using the backend coordinate system. - */ - void move_mouse(input_t &input, int deltaX, int deltaY) { - auto raw = (input_raw_t *) input.get(); - platf::mouse::move(raw, deltaX, deltaY); - } - - /** - * @brief Move the pointer to an absolute client-provided touch coordinate. - */ - void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { - auto raw = (input_raw_t *) input.get(); - platf::mouse::move_abs(raw, touch_port, x, y); - } - - /** - * @brief Press or release a virtual mouse button. - */ - void button_mouse(input_t &input, int button, bool release) { - auto raw = (input_raw_t *) input.get(); - platf::mouse::button(raw, button, release); - } - - /** - * @brief Apply a vertical scroll event to the virtual mouse. - */ - void scroll(input_t &input, int high_res_distance) { - auto raw = (input_raw_t *) input.get(); - platf::mouse::scroll(raw, high_res_distance); - } - - /** - * @brief Apply a horizontal scroll event to the virtual mouse. - */ - void hscroll(input_t &input, int high_res_distance) { - auto raw = (input_raw_t *) input.get(); - platf::mouse::hscroll(raw, high_res_distance); - } - - /** - * @brief Press or release a virtual keyboard key. - */ - void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { - auto raw = (input_raw_t *) input.get(); - platf::keyboard::update(raw, modcode, release, flags); - } - - /** - * @brief Submit UTF-8 text input to the keyboard backend. - */ - void unicode(input_t &input, char *utf8, int size) { - auto raw = (input_raw_t *) input.get(); - platf::keyboard::unicode(raw, utf8, size); - } - - void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { - auto raw = (client_input_raw_t *) input; - platf::touch::update(raw, touch_port, touch); - } - - void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { - auto raw = (client_input_raw_t *) input; - platf::pen::update(raw, touch_port, pen); - } - - int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { - auto raw = (input_raw_t *) input.get(); - return platf::gamepad::alloc(raw, id, metadata, feedback_queue); - } - - /** - * @brief Release gamepad resources. - */ - void free_gamepad(input_t &input, int nr) { - auto raw = (input_raw_t *) input.get(); - platf::gamepad::free(raw, nr); - } - - void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - auto raw = (input_raw_t *) input.get(); - platf::gamepad::update(raw, nr, gamepad_state); - } - - void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { - auto raw = (input_raw_t *) input.get(); - platf::gamepad::touch(raw, touch); - } - - void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { - auto raw = (input_raw_t *) input.get(); - platf::gamepad::motion(raw, motion); - } - - void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { - auto raw = (input_raw_t *) input.get(); - platf::gamepad::battery(raw, battery); - } - - platform_caps::caps_t get_capabilities() { - platform_caps::caps_t caps = 0; - // TODO: if has_uinput - caps |= platform_caps::pen_touch; - - // We support controller touchpad input only when emulating the PS5 controller - if (config::input.gamepad == "ds5"sv || config::input.gamepad == "auto"sv) { - caps |= platform_caps::controller_touch; - } - - return caps; - } - - util::point_t get_mouse_loc(input_t &input) { - auto raw = (input_raw_t *) input.get(); - return platf::mouse::get_location(raw); - } - - std::vector &supported_gamepads(input_t *input) { - return platf::gamepad::supported_gamepads(input); - } -} // namespace platf diff --git a/src/platform/linux/input/inputtino_common.h b/src/platform/linux/input/inputtino_common.h deleted file mode 100644 index a6eba5e7292..00000000000 --- a/src/platform/linux/input/inputtino_common.h +++ /dev/null @@ -1,147 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_common.h - * @brief Declarations for inputtino common input handling. - */ -#pragma once - -// lib includes -#include -#include -#include - -// local includes -#include "src/config.h" -#include "src/logging.h" -#include "src/platform/common.h" -#include "src/platform/linux/input/inputtino_seat.h" -#include "src/utility.h" - -using namespace std::literals; - -namespace platf { - - /** - * @brief Append the target seat name to an inputtino device name when needed. - * - * @param base_name Base uinput device name. - * @return Device name scoped to the target seat. - */ - inline std::string inputtino_name_for_seat(std::string_view base_name) { - auto seat_id = inputtino_seat::get_target_seat(); - if (seat_id.empty() || seat_id == "seat0") { - return std::string(base_name); - } - - std::string name; - name.reserve(base_name.size() + seat_id.size() + 3); - name.append(base_name); - name.append(" ("); - name.append(seat_id); - name.push_back(')'); - return name; - } - - /** - * @brief Variant of inputtino virtual gamepad implementations Sunshine can create. - */ - using joypads_t = std::variant; - - /** - * @brief inputtino joypad collection and its ownership state. - */ - struct joypad_state { - std::unique_ptr joypad; ///< Active virtual gamepad object for one connected client slot. - gamepad_feedback_msg_t last_rumble; ///< Last rumble. - gamepad_feedback_msg_t last_rgb_led; ///< Last RGB led. - }; - - /** - * @brief Global inputtino device handles shared by clients. - */ - struct input_raw_t { - input_raw_t(): - mouse(inputtino::Mouse::create({ - .name = inputtino_name_for_seat("Mouse passthrough"sv), - .vendor_id = 0xBEEF, - .product_id = 0xDEAD, - .version = 0x111, - })), - keyboard(inputtino::Keyboard::create({ - .name = inputtino_name_for_seat("Keyboard passthrough"sv), - .vendor_id = 0xBEEF, - .product_id = 0xDEAD, - .version = 0x111, - })), - gamepads(MAX_GAMEPADS) { - if (!mouse) { - BOOST_LOG(warning) << "Unable to create virtual mouse: " << mouse.getErrorMessage(); - } - if (!keyboard) { - BOOST_LOG(warning) << "Unable to create virtual keyboard: " << keyboard.getErrorMessage(); - } - } - - ~input_raw_t() = default; - - // All devices are wrapped in Result because it might be that we aren't able to create them (ex: udev permission denied) - inputtino::Result mouse; ///< Shared inputtino virtual mouse device. - inputtino::Result keyboard; ///< inputtino virtual keyboard device. - - /** - * A list of gamepads that are currently connected. - * The pointer is shared because that state will be shared with background threads that deal with rumble and LED - */ - std::vector> gamepads; - }; - - /** - * @brief Per-client inputtino devices for touch and pen input. - */ - struct client_input_raw_t: public client_input_t { - /** - * @brief Create per-client inputtino devices for touch and pen input. - * - * @param input Platform input backend that receives the event. - */ - client_input_raw_t(input_t &input): - touch(inputtino::TouchScreen::create({ - .name = inputtino_name_for_seat("Touch passthrough"sv), - .vendor_id = 0xBEEF, - .product_id = 0xDEAD, - .version = 0x111, - })), - pen(inputtino::PenTablet::create({ - .name = inputtino_name_for_seat("Pen passthrough"sv), - .vendor_id = 0xBEEF, - .product_id = 0xDEAD, - .version = 0x111, - })) { - global = (input_raw_t *) input.get(); - if (!touch) { - BOOST_LOG(warning) << "Unable to create virtual touch screen: " << touch.getErrorMessage(); - } - if (!pen) { - BOOST_LOG(warning) << "Unable to create virtual pen tablet: " << pen.getErrorMessage(); - } - } - - input_raw_t *global; ///< Shared inputtino device set owned by the global input context. - - // Device state and handles for pen and touch input must be stored in the per-client - // input context, because each connected client may be sending their own independent - // pen/touch events. To maintain separation, we expose separate pen and touch devices - // for each client. - inputtino::Result touch; ///< Per-client virtual touchscreen device. - inputtino::Result pen; ///< Per-client virtual pen tablet device. - }; - - /** - * @brief Convert degrees to radians for controller motion data. - * - * @param degree Angle in degrees to convert. - * @return Angle in radians. - */ - inline float deg2rad(float degree) { - return degree * (M_PI / 180.f); - } -} // namespace platf diff --git a/src/platform/linux/input/inputtino_gamepad.cpp b/src/platform/linux/input/inputtino_gamepad.cpp deleted file mode 100644 index c5c9d3230bd..00000000000 --- a/src/platform/linux/input/inputtino_gamepad.cpp +++ /dev/null @@ -1,337 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_gamepad.cpp - * @brief Definitions for inputtino gamepad input handling. - */ -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "inputtino_gamepad.h" -#include "inputtino_seat.h" -#include "src/config.h" -#include "src/logging.h" -#include "src/platform/common.h" -#include "src/utility.h" - -using namespace std::literals; - -namespace platf::gamepad { - - /** - * @brief Enumerates supported gamepad status options. - */ - enum GamepadStatus { - UHID_NOT_AVAILABLE = 0, ///< UHID is not available - UINPUT_NOT_AVAILABLE, ///< UINPUT is not available - XINPUT_NOT_AVAILABLE, ///< XINPUT is not available - GAMEPAD_STATUS ///< Helper to indicate the number of status - }; - - /** - * @brief Create xbox one. - * - * @return Created xbox one object or status. - */ - auto create_xbox_one() { - return inputtino::XboxOneJoypad::create({.name = inputtino_name_for_seat("Sunshine X-Box One (virtual) pad"sv), - // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147 - .vendor_id = 0x045E, - .product_id = 0x02EA, - .version = 0x0408}); - } - - /** - * @brief Create an inputtino Nintendo Switch Pro controller. - * - * @return Created switch object or status. - */ - auto create_switch() { - return inputtino::SwitchJoypad::create({.name = inputtino_name_for_seat("Sunshine Nintendo (virtual) pad"sv), - // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 - .vendor_id = 0x057e, - .product_id = 0x2009, - .version = 0x8111}); - } - - /** - * @brief Create an inputtino DualSense controller. - * - * @param globalIndex Global index. - * @return Created DS5 object or status. - */ - auto create_ds5(int globalIndex) { - std::string device_mac = ""; // Inputtino checks empty() to generate a random MAC - - if (!config::input.ds5_inputtino_randomize_mac && globalIndex >= 0 && globalIndex <= 255) { - // Generate private virtual device MAC based on gamepad globalIndex between 0 (00) and 255 (ff) - device_mac = std::format("02:00:00:00:00:{:02x}", globalIndex); - } - - return inputtino::PS5Joypad::create({.name = inputtino_name_for_seat("Sunshine PS5 (virtual) pad"sv), .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111, .device_phys = device_mac, .device_uniq = device_mac}); - } - - /** - * @brief Allocate and initialize platform input state for a stream. - */ - int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { - ControllerType selectedGamepadType; - - if (config::input.gamepad == "xone"sv) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (manual selection)"sv; - selectedGamepadType = XboxOneWired; - } else if (config::input.gamepad == "ds5"sv) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense 5 controller (manual selection)"sv; - selectedGamepadType = DualSenseWired; - } else if (config::input.gamepad == "switch"sv) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (manual selection)"sv; - selectedGamepadType = SwitchProWired; - } else if (metadata.type == LI_CTYPE_XBOX) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (auto-selected by client-reported type)"sv; - selectedGamepadType = XboxOneWired; - } else if (metadata.type == LI_CTYPE_PS) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by client-reported type)"sv; - selectedGamepadType = DualSenseWired; - } else if (metadata.type == LI_CTYPE_NINTENDO) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (auto-selected by client-reported type)"sv; - selectedGamepadType = SwitchProWired; - } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by motion sensor presence)"sv; - selectedGamepadType = DualSenseWired; - } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by touchpad presence)"sv; - selectedGamepadType = DualSenseWired; - } else { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (default)"sv; - selectedGamepadType = XboxOneWired; - } - - if (selectedGamepadType == XboxOneWired || selectedGamepadType == SwitchProWired) { - if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) { - BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has motion sensors, but they are not usable when emulating a joypad different from DS5"sv; - } - if (metadata.capabilities & LI_CCAP_TOUCHPAD) { - BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has a touchpad, but it is not usable when emulating a joypad different from DS5"sv; - } - if (metadata.capabilities & LI_CCAP_RGB_LED) { - BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating a joypad different from DS5"sv; - } - } else if (selectedGamepadType == DualSenseWired) { - if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { - BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have motion sensors active"sv; - } - if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) { - BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have a touchpad"sv; - } - } - - auto gamepad = std::make_shared(joypad_state {}); - auto on_rumble_fn = [feedback_queue, idx = id.clientRelativeIndex, gamepad](int low_freq, int high_freq) { - // Don't resend duplicate rumble data - if (gamepad->last_rumble.type == platf::gamepad_feedback_e::rumble && gamepad->last_rumble.data.rumble.lowfreq == low_freq && gamepad->last_rumble.data.rumble.highfreq == high_freq) { - return; - } - - gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble(idx, low_freq, high_freq); - feedback_queue->raise(msg); - gamepad->last_rumble = msg; - }; - - switch (selectedGamepadType) { - case XboxOneWired: - { - auto xOne = create_xbox_one(); - if (xOne) { - (*xOne).set_on_rumble(on_rumble_fn); - gamepad->joypad = std::make_unique(std::move(*xOne)); - raw->gamepads[id.globalIndex] = std::move(gamepad); - return 0; - } else { - BOOST_LOG(warning) << "Unable to create virtual Xbox One controller: " << xOne.getErrorMessage(); - return -1; - } - } - case SwitchProWired: - { - auto switchPro = create_switch(); - if (switchPro) { - (*switchPro).set_on_rumble(on_rumble_fn); - gamepad->joypad = std::make_unique(std::move(*switchPro)); - raw->gamepads[id.globalIndex] = std::move(gamepad); - return 0; - } else { - BOOST_LOG(warning) << "Unable to create virtual Switch Pro controller: " << switchPro.getErrorMessage(); - return -1; - } - } - case DualSenseWired: - { - auto ds5 = create_ds5(id.globalIndex); - if (ds5) { - (*ds5).set_on_rumble(on_rumble_fn); - (*ds5).set_on_led([feedback_queue, idx = id.clientRelativeIndex, gamepad](int r, int g, int b) { - // Don't resend duplicate LED data - if (gamepad->last_rgb_led.type == platf::gamepad_feedback_e::set_rgb_led && gamepad->last_rgb_led.data.rgb_led.r == r && gamepad->last_rgb_led.data.rgb_led.g == g && gamepad->last_rgb_led.data.rgb_led.b == b) { - return; - } - - auto msg = gamepad_feedback_msg_t::make_rgb_led(idx, r, g, b); - feedback_queue->raise(msg); - gamepad->last_rgb_led = msg; - }); - - (*ds5).set_on_trigger_effect([feedback_queue, idx = id.clientRelativeIndex](const inputtino::PS5Joypad::TriggerEffect &trigger_effect) { - feedback_queue->raise(gamepad_feedback_msg_t::make_adaptive_triggers(idx, trigger_effect.event_flags, trigger_effect.type_left, trigger_effect.type_right, trigger_effect.left, trigger_effect.right)); - }); - - // Activate the motion sensors - feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100)); - feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100)); - - gamepad->joypad = std::make_unique(std::move(*ds5)); - raw->gamepads[id.globalIndex] = std::move(gamepad); - return 0; - } else { - BOOST_LOG(warning) << "Unable to create virtual DualShock 5 controller: " << ds5.getErrorMessage(); - return -1; - } - } - } - return -1; - } - - /** - * @brief Release backend resources for the indexed gamepad. - */ - void free(input_raw_t *raw, int nr) { - // This will call the destructor which in turn will stop the background threads for rumble and LED (and ultimately remove the joypad device) - raw->gamepads[nr]->joypad.reset(); - raw->gamepads[nr].reset(); - } - - /** - * @brief Apply the supplied state update to the platform backend. - */ - void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) { - auto gamepad = raw->gamepads[nr]; - if (!gamepad) { - return; - } - - std::visit([gamepad_state](inputtino::Joypad &gc) { - gc.set_pressed_buttons(gamepad_state.buttonFlags); - gc.set_stick(inputtino::Joypad::LS, gamepad_state.lsX, gamepad_state.lsY); - gc.set_stick(inputtino::Joypad::RS, gamepad_state.rsX, gamepad_state.rsY); - gc.set_triggers(gamepad_state.lt, gamepad_state.rt); - }, - *gamepad->joypad); - } - - /** - * @brief Apply controller touchpad data to the backend device. - */ - void touch(input_raw_t *raw, const gamepad_touch_t &touch) { - auto gamepad = raw->gamepads[touch.id.globalIndex]; - if (!gamepad) { - return; - } - // Only the PS5 controller supports touch input - if (std::holds_alternative(*gamepad->joypad)) { - if (touch.pressure > 0.5) { - std::get(*gamepad->joypad).place_finger(touch.pointerId, touch.x * inputtino::PS5Joypad::touchpad_width, touch.y * inputtino::PS5Joypad::touchpad_height); - } else { - std::get(*gamepad->joypad).release_finger(touch.pointerId); - } - } - } - - /** - * @brief Apply controller motion sensor data to the backend device. - */ - void motion(input_raw_t *raw, const gamepad_motion_t &motion) { - auto gamepad = raw->gamepads[motion.id.globalIndex]; - if (!gamepad) { - return; - } - // Only the PS5 controller supports motion - if (std::holds_alternative(*gamepad->joypad)) { - switch (motion.motionType) { - case LI_MOTION_TYPE_ACCEL: - std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::ACCELERATION, motion.x, motion.y, motion.z); - break; - case LI_MOTION_TYPE_GYRO: - std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::GYROSCOPE, deg2rad(motion.x), deg2rad(motion.y), deg2rad(motion.z)); - break; - } - } - } - - /** - * @brief Apply controller battery status to the backend device. - */ - void battery(input_raw_t *raw, const gamepad_battery_t &battery) { - auto gamepad = raw->gamepads[battery.id.globalIndex]; - if (!gamepad) { - return; - } - // Only the PS5 controller supports battery reports - if (std::holds_alternative(*gamepad->joypad)) { - inputtino::PS5Joypad::BATTERY_STATE state; - switch (battery.state) { - case LI_BATTERY_STATE_CHARGING: - state = inputtino::PS5Joypad::BATTERY_CHARGHING; - break; - case LI_BATTERY_STATE_DISCHARGING: - state = inputtino::PS5Joypad::BATTERY_DISCHARGING; - break; - case LI_BATTERY_STATE_FULL: - state = inputtino::PS5Joypad::BATTERY_FULL; - break; - case LI_BATTERY_STATE_UNKNOWN: - case LI_BATTERY_STATE_NOT_PRESENT: - default: - return; - } - if (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) { - std::get(*gamepad->joypad).set_battery(state, battery.percentage); - } - } - } - - /** - * @brief Return the virtual gamepad types supported by inputtino. - */ - std::vector &supported_gamepads(input_t *input) { - if (!input) { - static std::vector gps { - supported_gamepad_t {"auto", true, ""}, - supported_gamepad_t {"xone", false, ""}, - supported_gamepad_t {"ds5", false, ""}, - supported_gamepad_t {"switch", false, ""}, - }; - - return gps; - } - - auto ds5 = create_ds5(-1); // Index -1 will result in a random MAC virtual device, which is fine for probing - auto switchPro = create_switch(); - auto xOne = create_xbox_one(); - - static std::vector gps { - supported_gamepad_t {"auto", true, ""}, - supported_gamepad_t {"xone", static_cast(xOne), !xOne ? xOne.getErrorMessage() : ""}, - supported_gamepad_t {"ds5", static_cast(ds5), !ds5 ? ds5.getErrorMessage() : ""}, - supported_gamepad_t {"switch", static_cast(switchPro), !switchPro ? switchPro.getErrorMessage() : ""}, - }; - - for (auto &[name, is_enabled, reason_disabled] : gps) { - if (!is_enabled) { - BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled; - } - } - - return gps; - } -} // namespace platf::gamepad diff --git a/src/platform/linux/input/inputtino_gamepad.h b/src/platform/linux/input/inputtino_gamepad.h deleted file mode 100644 index 76d385318fe..00000000000 --- a/src/platform/linux/input/inputtino_gamepad.h +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_gamepad.h - * @brief Declarations for inputtino gamepad input handling. - */ -#pragma once - -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "src/platform/common.h" - -using namespace std::literals; - -namespace platf::gamepad { - - /** - * @brief Enumerates supported controller type options. - */ - enum ControllerType { - XboxOneWired, ///< Xbox One Wired Controller - DualSenseWired, ///< DualSense Wired Controller - SwitchProWired ///< Switch Pro Wired Controller - }; - - /** - * @brief Allocate and initialize platform input state for a stream. - * - * @param raw Platform-specific input backend state. - * @param id Identifier for the controller, session, display, or resource. - * @param metadata Output structure populated with HDR metadata. - * @param feedback_queue Feedback queue. - * @return Allocated object or identifier, or an error value on failure. - */ - int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); - - /** - * @brief Release backend resources for the indexed gamepad. - * - * @param raw Platform-specific input backend state. - * @param nr Controller index assigned by the client. - */ - void free(input_raw_t *raw, int nr); - - /** - * @brief Apply the supplied state update to the platform backend. - * - * @param raw Platform-specific input backend state. - * @param nr Controller index assigned by the client. - * @param gamepad_state Gamepad state. - */ - void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state); - - /** - * @brief Apply controller touchpad data to the backend device. - * - * @param raw Platform-specific input backend state. - * @param touch Touch event data to apply to the virtual device. - */ - void touch(input_raw_t *raw, const gamepad_touch_t &touch); - - /** - * @brief Apply controller motion sensor data to the backend device. - * - * @param raw Platform-specific input backend state. - * @param motion Motion sensor data to apply to the virtual device. - */ - void motion(input_raw_t *raw, const gamepad_motion_t &motion); - - /** - * @brief Apply controller battery status to the backend device. - * - * @param raw Platform-specific input backend state. - * @param battery Battery status data reported by the virtual device. - */ - void battery(input_raw_t *raw, const gamepad_battery_t &battery); - - /** - * @brief Return gamepad slots supported by the inputtino backend. - * - * @param input Platform input backend that receives the event. - * @return Mutable list of supported virtual gamepads for the input backend. - */ - std::vector &supported_gamepads(input_t *input); -} // namespace platf::gamepad diff --git a/src/platform/linux/input/inputtino_keyboard.cpp b/src/platform/linux/input/inputtino_keyboard.cpp deleted file mode 100644 index 7d466566e1e..00000000000 --- a/src/platform/linux/input/inputtino_keyboard.cpp +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_keyboard.cpp - * @brief Definitions for inputtino keyboard input handling. - */ -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "inputtino_keyboard.h" -#include "src/config.h" -#include "src/logging.h" -#include "src/platform/common.h" -#include "src/utility.h" - -using namespace std::literals; - -namespace platf::keyboard { - - /** - * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase) - * - * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471 - * - * adapted from: https://stackoverflow.com/a/7639754 - * @param str UTF-8 text to encode as hexadecimal. - * @return Value converted to hex. - */ - std::string to_hex(const std::basic_string &str) { - std::stringstream ss; - ss << std::hex << std::setfill('0'); - for (const auto &ch : str) { - ss << static_cast(ch); - } - - std::string hex_unicode(ss.str()); - std::ranges::transform(hex_unicode, hex_unicode.begin(), ::toupper); - return hex_unicode; - } - - /** - * A map of linux scan code -> Moonlight keyboard code - */ - static const std::map key_mappings = { - {KEY_BACKSPACE, 0x08}, - {KEY_TAB, 0x09}, - {KEY_ENTER, 0x0D}, - {KEY_LEFTSHIFT, 0x10}, - {KEY_LEFTCTRL, 0x11}, - {KEY_CAPSLOCK, 0x14}, - {KEY_ESC, 0x1B}, - {KEY_SPACE, 0x20}, - {KEY_PAGEUP, 0x21}, - {KEY_PAGEDOWN, 0x22}, - {KEY_END, 0x23}, - {KEY_HOME, 0x24}, - {KEY_LEFT, 0x25}, - {KEY_UP, 0x26}, - {KEY_RIGHT, 0x27}, - {KEY_DOWN, 0x28}, - {KEY_SYSRQ, 0x2C}, - {KEY_INSERT, 0x2D}, - {KEY_DELETE, 0x2E}, - {KEY_0, 0x30}, - {KEY_1, 0x31}, - {KEY_2, 0x32}, - {KEY_3, 0x33}, - {KEY_4, 0x34}, - {KEY_5, 0x35}, - {KEY_6, 0x36}, - {KEY_7, 0x37}, - {KEY_8, 0x38}, - {KEY_9, 0x39}, - {KEY_A, 0x41}, - {KEY_B, 0x42}, - {KEY_C, 0x43}, - {KEY_D, 0x44}, - {KEY_E, 0x45}, - {KEY_F, 0x46}, - {KEY_G, 0x47}, - {KEY_H, 0x48}, - {KEY_I, 0x49}, - {KEY_J, 0x4A}, - {KEY_K, 0x4B}, - {KEY_L, 0x4C}, - {KEY_M, 0x4D}, - {KEY_N, 0x4E}, - {KEY_O, 0x4F}, - {KEY_P, 0x50}, - {KEY_Q, 0x51}, - {KEY_R, 0x52}, - {KEY_S, 0x53}, - {KEY_T, 0x54}, - {KEY_U, 0x55}, - {KEY_V, 0x56}, - {KEY_W, 0x57}, - {KEY_X, 0x58}, - {KEY_Y, 0x59}, - {KEY_Z, 0x5A}, - {KEY_LEFTMETA, 0x5B}, - {KEY_RIGHTMETA, 0x5C}, - {KEY_KP0, 0x60}, - {KEY_KP1, 0x61}, - {KEY_KP2, 0x62}, - {KEY_KP3, 0x63}, - {KEY_KP4, 0x64}, - {KEY_KP5, 0x65}, - {KEY_KP6, 0x66}, - {KEY_KP7, 0x67}, - {KEY_KP8, 0x68}, - {KEY_KP9, 0x69}, - {KEY_KPASTERISK, 0x6A}, - {KEY_KPPLUS, 0x6B}, - {KEY_KPMINUS, 0x6D}, - {KEY_KPDOT, 0x6E}, - {KEY_KPSLASH, 0x6F}, - {KEY_F1, 0x70}, - {KEY_F2, 0x71}, - {KEY_F3, 0x72}, - {KEY_F4, 0x73}, - {KEY_F5, 0x74}, - {KEY_F6, 0x75}, - {KEY_F7, 0x76}, - {KEY_F8, 0x77}, - {KEY_F9, 0x78}, - {KEY_F10, 0x79}, - {KEY_F11, 0x7A}, - {KEY_F12, 0x7B}, - {KEY_F13, 0x7C}, - {KEY_F14, 0x7D}, - {KEY_F15, 0x7E}, - {KEY_F16, 0x7F}, - {KEY_F17, 0x80}, - {KEY_F18, 0x81}, - {KEY_F19, 0x82}, - {KEY_F20, 0x83}, - {KEY_F21, 0x84}, - {KEY_F22, 0x85}, - {KEY_F23, 0x86}, - {KEY_F24, 0x87}, - {KEY_NUMLOCK, 0x90}, - {KEY_SCROLLLOCK, 0x91}, - {KEY_LEFTSHIFT, 0xA0}, - {KEY_RIGHTSHIFT, 0xA1}, - {KEY_LEFTCTRL, 0xA2}, - {KEY_RIGHTCTRL, 0xA3}, - {KEY_LEFTALT, 0xA4}, - {KEY_RIGHTALT, 0xA5}, - {KEY_SEMICOLON, 0xBA}, - {KEY_EQUAL, 0xBB}, - {KEY_COMMA, 0xBC}, - {KEY_MINUS, 0xBD}, - {KEY_DOT, 0xBE}, - {KEY_SLASH, 0xBF}, - {KEY_GRAVE, 0xC0}, - {KEY_LEFTBRACE, 0xDB}, - {KEY_BACKSLASH, 0xDC}, - {KEY_RIGHTBRACE, 0xDD}, - {KEY_APOSTROPHE, 0xDE}, - {KEY_102ND, 0xE2} - }; - - /** - * @brief Apply the supplied state update to the platform backend. - */ - void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags) { - if (raw->keyboard) { - if (release) { - (*raw->keyboard).release(modcode); - } else { - (*raw->keyboard).press(modcode); - } - } - } - - /** - * @brief Submit UTF-8 text input to the keyboard backend. - */ - void unicode(input_raw_t *raw, char *utf8, int size) { - if (raw->keyboard) { - /* Reading input text as UTF-8 */ - auto utf8_str = boost::locale::conv::to_utf(utf8, utf8 + size, "UTF-8"); - /* Converting to UTF-32 */ - auto utf32_str = boost::locale::conv::utf_to_utf(utf8_str); - /* To HEX string */ - auto hex_unicode = to_hex(utf32_str); - BOOST_LOG(debug) << "Unicode, typing U+"sv << hex_unicode; - - /* pressing + + U */ - (*raw->keyboard).press(0xA2); // LEFTCTRL - (*raw->keyboard).press(0xA0); // LEFTSHIFT - (*raw->keyboard).press(0x55); // U - (*raw->keyboard).release(0x55); // U - - /* input each HEX character */ - for (auto &ch : hex_unicode) { - auto key_str = "KEY_"s + ch; - auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); - auto wincode = key_mappings.find(keycode); - if (keycode == -1 || wincode == key_mappings.end()) { - BOOST_LOG(warning) << "Unicode, unable to find keycode for: "sv << ch; - } else { - (*raw->keyboard).press(wincode->second); - (*raw->keyboard).release(wincode->second); - } - } - - /* releasing and */ - (*raw->keyboard).release(0xA0); // LEFTSHIFT - (*raw->keyboard).release(0xA2); // LEFTCTRL - } - } -} // namespace platf::keyboard diff --git a/src/platform/linux/input/inputtino_keyboard.h b/src/platform/linux/input/inputtino_keyboard.h deleted file mode 100644 index 2bf28575ae1..00000000000 --- a/src/platform/linux/input/inputtino_keyboard.h +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_keyboard.h - * @brief Declarations for inputtino keyboard input handling. - */ -#pragma once - -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" - -using namespace std::literals; - -namespace platf::keyboard { - /** - * @brief Apply the supplied state update to the platform backend. - * - * @param raw Platform-specific input backend state. - * @param modcode Modifier key code to update. - * @param release Whether the key or button event is a release. - * @param flags Bit flags that modify the requested operation. - */ - void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags); - - /** - * @brief Submit UTF-8 text input to the keyboard backend. - * - * @param raw Platform-specific input backend state. - * @param utf8 UTF-8 text submitted by the client. - * @param size Number of bytes or elements requested. - */ - void unicode(input_raw_t *raw, char *utf8, int size); -} // namespace platf::keyboard diff --git a/src/platform/linux/input/inputtino_mouse.cpp b/src/platform/linux/input/inputtino_mouse.cpp deleted file mode 100644 index c837c8154b3..00000000000 --- a/src/platform/linux/input/inputtino_mouse.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_mouse.cpp - * @brief Definitions for inputtino mouse input handling. - */ -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "inputtino_mouse.h" -#include "src/config.h" -#include "src/logging.h" -#include "src/platform/common.h" -#include "src/utility.h" - -using namespace std::literals; - -namespace platf::mouse { - - /** - * @brief Apply a relative pointer movement to the virtual mouse. - */ - void move(input_raw_t *raw, int deltaX, int deltaY) { - if (raw->mouse) { - (*raw->mouse).move(deltaX, deltaY); - } - } - - /** - * @brief Move abs using the backend coordinate system. - */ - void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) { - if (raw->mouse) { - (*raw->mouse).move_abs(x, y, touch_port.width, touch_port.height); - } - } - - /** - * @brief Press or release a virtual mouse button. - */ - void button(input_raw_t *raw, int button, bool release) { - if (raw->mouse) { - inputtino::Mouse::MOUSE_BUTTON btn_type; - switch (button) { - case BUTTON_LEFT: - btn_type = inputtino::Mouse::LEFT; - break; - case BUTTON_MIDDLE: - btn_type = inputtino::Mouse::MIDDLE; - break; - case BUTTON_RIGHT: - btn_type = inputtino::Mouse::RIGHT; - break; - case BUTTON_X1: - btn_type = inputtino::Mouse::SIDE; - break; - case BUTTON_X2: - btn_type = inputtino::Mouse::EXTRA; - break; - default: - BOOST_LOG(warning) << "Unknown mouse button: " << button; - return; - } - if (release) { - (*raw->mouse).release(btn_type); - } else { - (*raw->mouse).press(btn_type); - } - } - } - - /** - * @brief Apply a vertical scroll event to the virtual mouse. - */ - void scroll(input_raw_t *raw, int high_res_distance) { - if (raw->mouse) { - (*raw->mouse).vertical_scroll(high_res_distance); - } - } - - /** - * @brief Apply a horizontal scroll event to the virtual mouse. - */ - void hscroll(input_raw_t *raw, int high_res_distance) { - if (raw->mouse) { - (*raw->mouse).horizontal_scroll(high_res_distance); - } - } - - /** - * @brief Return the current virtual pointer location. - */ - util::point_t get_location(input_raw_t *raw) { - if (raw->mouse) { - // TODO: decide what to do after https://github.com/games-on-whales/inputtino/issues/6 is resolved. - // TODO: auto x = (*raw->mouse).get_absolute_x(); - // TODO: auto y = (*raw->mouse).get_absolute_y(); - return {0, 0}; - } - return {0, 0}; - } -} // namespace platf::mouse diff --git a/src/platform/linux/input/inputtino_mouse.h b/src/platform/linux/input/inputtino_mouse.h deleted file mode 100644 index 0bd5a2e7119..00000000000 --- a/src/platform/linux/input/inputtino_mouse.h +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_mouse.h - * @brief Declarations for inputtino mouse input handling. - */ -#pragma once -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "src/platform/common.h" - -using namespace std::literals; - -namespace platf::mouse { - /** - * @brief Apply a relative pointer movement to the virtual mouse. - * - * @param raw Platform-specific input backend state. - * @param deltaX Horizontal relative movement in client coordinates. - * @param deltaY Vertical relative movement in client coordinates. - */ - void move(input_raw_t *raw, int deltaX, int deltaY); - - /** - * @brief Move the pointer to an absolute client-provided touch coordinate. - * - * @param raw Platform-specific input backend state. - * @param touch_port Touch coordinate bounds used for scaling. - * @param x Horizontal absolute coordinate from the client. - * @param y Vertical absolute coordinate from the client. - */ - void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y); - - /** - * @brief Press or release a virtual mouse button. - * - * @param raw Platform-specific input backend state. - * @param button Mouse button identifier to press or release. - * @param release Whether the key or button event is a release. - */ - void button(input_raw_t *raw, int button, bool release); - - /** - * @brief Apply a vertical scroll event to the virtual mouse. - * - * @param raw Platform-specific input backend state. - * @param high_res_distance High-resolution scroll distance reported by the client. - */ - void scroll(input_raw_t *raw, int high_res_distance); - - /** - * @brief Apply a horizontal scroll event to the virtual mouse. - * - * @param raw Platform-specific input backend state. - * @param high_res_distance High-resolution scroll distance reported by the client. - */ - void hscroll(input_raw_t *raw, int high_res_distance); - - /** - * @brief Return the current virtual pointer location. - * - * @param raw Platform-specific input backend state. - * @return Current virtual pointer location in screen coordinates. - */ - util::point_t get_location(input_raw_t *raw); -} // namespace platf::mouse diff --git a/src/platform/linux/input/inputtino_pen.cpp b/src/platform/linux/input/inputtino_pen.cpp deleted file mode 100644 index b4e245e1eaf..00000000000 --- a/src/platform/linux/input/inputtino_pen.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_pen.cpp - * @brief Definitions for inputtino pen input handling. - */ -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "inputtino_pen.h" -#include "src/config.h" -#include "src/logging.h" -#include "src/platform/common.h" -#include "src/utility.h" - -using namespace std::literals; - -namespace platf::pen { - /** - * @brief Apply the supplied state update to the platform backend. - */ - void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen) { - if (raw->pen) { - // First set the buttons - (*raw->pen).set_btn(inputtino::PenTablet::PRIMARY, pen.penButtons & LI_PEN_BUTTON_PRIMARY); - (*raw->pen).set_btn(inputtino::PenTablet::SECONDARY, pen.penButtons & LI_PEN_BUTTON_SECONDARY); - (*raw->pen).set_btn(inputtino::PenTablet::TERTIARY, pen.penButtons & LI_PEN_BUTTON_TERTIARY); - - // Set the tool - inputtino::PenTablet::TOOL_TYPE tool; - switch (pen.toolType) { - case LI_TOOL_TYPE_PEN: - tool = inputtino::PenTablet::PEN; - break; - case LI_TOOL_TYPE_ERASER: - tool = inputtino::PenTablet::ERASER; - break; - default: - tool = inputtino::PenTablet::SAME_AS_BEFORE; - break; - } - - // Normalize rotation value to 0-359 degree range - auto rotation = pen.rotation; - if (rotation != LI_ROT_UNKNOWN) { - rotation %= 360; - } - - // Here we receive: - // - Rotation: degrees from vertical in Y dimension (parallel to screen, 0..360) - // - Tilt: degrees from vertical in Z dimension (perpendicular to screen, 0..90) - float tilt_x = 0; - float tilt_y = 0; - // Convert polar coordinates into Y tilt angles - if (pen.tilt != LI_TILT_UNKNOWN && rotation != LI_ROT_UNKNOWN) { - auto rotation_rads = deg2rad(rotation); - auto tilt_rads = deg2rad(pen.tilt); - auto r = std::sin(tilt_rads); - auto z = std::cos(tilt_rads); - - tilt_x = std::atan2(std::sin(-rotation_rads) * r, z) * 180.f / M_PI; - tilt_y = std::atan2(std::cos(-rotation_rads) * r, z) * 180.f / M_PI; - } - - bool is_touching = pen.eventType == LI_TOUCH_EVENT_DOWN || pen.eventType == LI_TOUCH_EVENT_MOVE; - - (*raw->pen).place_tool(tool, pen.x, pen.y, is_touching ? pen.pressureOrDistance : -1, is_touching ? -1 : pen.pressureOrDistance, tilt_x, tilt_y); - } - } -} // namespace platf::pen diff --git a/src/platform/linux/input/inputtino_pen.h b/src/platform/linux/input/inputtino_pen.h deleted file mode 100644 index 0c2cb3f8c43..00000000000 --- a/src/platform/linux/input/inputtino_pen.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_pen.h - * @brief Declarations for inputtino pen input handling. - */ -#pragma once - -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "src/platform/common.h" - -using namespace std::literals; - -namespace platf::pen { - /** - * @brief Apply the supplied state update to the platform backend. - * - * @param raw Platform-specific input backend state. - * @param touch_port Touch coordinate bounds used for scaling. - * @param pen Pen event data to inject. - */ - void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen); -} // namespace platf::pen diff --git a/src/platform/linux/input/inputtino_seat.cpp b/src/platform/linux/input/inputtino_seat.cpp deleted file mode 100644 index e4f2a210157..00000000000 --- a/src/platform/linux/input/inputtino_seat.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_seat.cpp - * @brief Implementation for multi-seat naming (udev-only). - */ -// lib includes -#include - -// local includes -#include "inputtino_seat.h" - -namespace platf::inputtino_seat { - - std::string get_target_seat() { - if (std::string seat; lizardbyte::common::get_env("XDG_SEAT", seat) && !seat.empty()) { - return seat; - } - - return {}; - } - -} // namespace platf::inputtino_seat diff --git a/src/platform/linux/input/inputtino_seat.h b/src/platform/linux/input/inputtino_seat.h deleted file mode 100644 index 00474f7ecef..00000000000 --- a/src/platform/linux/input/inputtino_seat.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_seat.h - * @brief Helpers for multi-seat naming (udev-only). - */ -#pragma once - -#include - -namespace platf::inputtino_seat { - - /** - * Determine the target seat for the current Sunshine instance. - * Returns empty string if no seat could be determined. - * - * @return Seat name used for virtual input devices, or an empty string when unknown. - */ - std::string get_target_seat(); - -} // namespace platf::inputtino_seat diff --git a/src/platform/linux/input/inputtino_touch.cpp b/src/platform/linux/input/inputtino_touch.cpp deleted file mode 100644 index e8c535661c6..00000000000 --- a/src/platform/linux/input/inputtino_touch.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_touch.cpp - * @brief Definitions for inputtino touch input handling. - */ -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "inputtino_touch.h" -#include "src/config.h" -#include "src/logging.h" -#include "src/platform/common.h" -#include "src/utility.h" - -using namespace std::literals; - -namespace platf::touch { - /** - * @brief Apply the supplied state update to the platform backend. - */ - void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch) { - if (raw->touch) { - switch (touch.eventType) { - case LI_TOUCH_EVENT_HOVER: - case LI_TOUCH_EVENT_DOWN: - case LI_TOUCH_EVENT_MOVE: - { - // Convert our 0..360 range to -90..90 relative to Y axis - int adjusted_angle = touch.rotation; - - if (adjusted_angle > 90 && adjusted_angle < 270) { - // Lower hemisphere - adjusted_angle = 180 - adjusted_angle; - } - - // Wrap the value if it's out of range - if (adjusted_angle > 90) { - adjusted_angle -= 360; - } else if (adjusted_angle < -90) { - adjusted_angle += 360; - } - (*raw->touch).place_finger(touch.pointerId, touch.x, touch.y, touch.pressureOrDistance, adjusted_angle); - break; - } - case LI_TOUCH_EVENT_CANCEL: - case LI_TOUCH_EVENT_UP: - case LI_TOUCH_EVENT_HOVER_LEAVE: - { - (*raw->touch).release_finger(touch.pointerId); - break; - } - // TODO: LI_TOUCH_EVENT_CANCEL_ALL - } - } - } -} // namespace platf::touch diff --git a/src/platform/linux/input/inputtino_touch.h b/src/platform/linux/input/inputtino_touch.h deleted file mode 100644 index 026e9dc2455..00000000000 --- a/src/platform/linux/input/inputtino_touch.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @file src/platform/linux/input/inputtino_touch.h - * @brief Declarations for inputtino touch input handling. - */ -#pragma once - -// lib includes -#include -#include -#include - -// local includes -#include "inputtino_common.h" -#include "src/platform/common.h" - -using namespace std::literals; - -namespace platf::touch { - /** - * @brief Apply the supplied state update to the platform backend. - * - * @param raw Platform-specific input backend state. - * @param touch_port Touch coordinate bounds used for scaling. - * @param touch Touch event data to apply to the virtual device. - */ - void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch); -} // namespace platf::touch diff --git a/src/platform/linux/input/virtualhid.cpp b/src/platform/linux/input/virtualhid.cpp new file mode 100644 index 00000000000..cda9d4ed836 --- /dev/null +++ b/src/platform/linux/input/virtualhid.cpp @@ -0,0 +1,155 @@ +/** + * @file src/platform/linux/input/virtualhid.cpp + * @brief Definitions for libvirtualhid Unix input handling. + */ + +// standard includes +#include + +// local includes +#include "src/platform/virtualhid_input.h" + +using namespace std::literals; + +namespace platf { + namespace { + + /** + * @brief Global libvirtualhid devices shared by clients. + */ + struct input_raw_t { + virtualhid::input_context_t virtualhid; ///< libvirtualhid input context. + }; + + /** + * @brief Per-client libvirtualhid devices. + */ + struct client_input_raw_t: client_input_t { + /** + * @brief Create per-client libvirtualhid devices. + * + * @param input Platform input backend that receives the event. + */ + explicit client_input_raw_t(input_t &input): + virtualhid {((input_raw_t *) input.get())->virtualhid} {} + + virtualhid::client_context_t virtualhid; ///< libvirtualhid client context. + }; + + } // namespace + + input_t input() { + return {new input_raw_t {}}; + } + + std::unique_ptr allocate_client_input_context(input_t &input) { + return std::make_unique(input); + } + + void freeInput(void *p) { + auto *input = (input_raw_t *) p; + delete input; + } + + void move_mouse(input_t &input, int deltaX, int deltaY) { + virtualhid::move_mouse(((input_raw_t *) input.get())->virtualhid, deltaX, deltaY); + } + + void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + virtualhid::abs_mouse(((input_raw_t *) input.get())->virtualhid, touch_port, x, y); + } + + void button_mouse(input_t &input, int button, bool release) { + virtualhid::button_mouse(((input_raw_t *) input.get())->virtualhid, button, release); + } + + void scroll(input_t &input, int high_res_distance) { + virtualhid::scroll(((input_raw_t *) input.get())->virtualhid, high_res_distance); + } + + void hscroll(input_t &input, int high_res_distance) { + virtualhid::hscroll(((input_raw_t *) input.get())->virtualhid, high_res_distance); + } + + /** + * @brief Press or release a virtual keyboard key. + * + * @param input Platform input backend that receives the event. + * @param modcode Modifier key code to update. + * @param release Whether the key or button event is a release. + * @param flags Bit flags that modify the requested operation; ignored by this backend. + */ + void keyboard_update(input_t &input, std::uint16_t modcode, bool release, [[maybe_unused]] std::uint8_t flags) { + virtualhid::keyboard_update(((input_raw_t *) input.get())->virtualhid, modcode, release); + } + + void unicode(input_t &input, char *utf8, int size) { + virtualhid::unicode(((input_raw_t *) input.get())->virtualhid, utf8, size); + } + + void touch_update(client_input_t *input, const touch_port_t & /*touch_port*/, const touch_input_t &touch) { + virtualhid::touch_update(((client_input_raw_t *) input)->virtualhid, touch); + } + + void pen_update(client_input_t *input, const touch_port_t & /*touch_port*/, const pen_input_t &pen) { + virtualhid::pen_update(((client_input_raw_t *) input)->virtualhid, pen); + } + + int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + return virtualhid::alloc_gamepad(((input_raw_t *) input.get())->virtualhid, id, metadata, std::move(feedback_queue)); + } + + void free_gamepad(input_t &input, int nr) { + virtualhid::free_gamepad(((input_raw_t *) input.get())->virtualhid, nr); + } + + void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + virtualhid::gamepad_update(((input_raw_t *) input.get())->virtualhid, nr, gamepad_state); + } + + void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + virtualhid::gamepad_touch(((input_raw_t *) input.get())->virtualhid, touch); + } + + void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + virtualhid::gamepad_motion(((input_raw_t *) input.get())->virtualhid, motion); + } + + void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + virtualhid::gamepad_battery(((input_raw_t *) input.get())->virtualhid, battery); + } + + platform_caps::caps_t get_capabilities() { + platform_caps::caps_t caps = 0; + const auto runtime = virtualhid::create_runtime(); + if (!runtime) { + return caps; + } + + const auto &capabilities = runtime->capabilities(); + if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) { + caps |= platform_caps::pen_touch; + } + if (virtualhid::configured_gamepad_supports_touchpad()) { + caps |= platform_caps::controller_touch; + } + + return caps; + } + + util::point_t get_mouse_loc(input_t & /*input*/) { + return {0, 0}; + } + + std::vector &supported_gamepads(input_t *input) { + static std::vector gamepads; + if (!input || !input->get()) { + gamepads = virtualhid::static_supported_gamepads(); + return gamepads; + } + + const auto raw = (input_raw_t *) input->get(); + gamepads = virtualhid::supported_gamepads(raw->virtualhid.runtime.get()); + return gamepads; + } +} // namespace platf diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp new file mode 100644 index 00000000000..88b361d54b3 --- /dev/null +++ b/src/platform/virtualhid_input.cpp @@ -0,0 +1,786 @@ +/** + * @file src/platform/virtualhid_input.cpp + * @brief Definitions for libvirtualhid-backed input helpers. + */ + +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "src/config.h" +#include "src/logging.h" +#include "virtualhid_input.h" + +using namespace std::literals; + +namespace platf::virtualhid { + /** + * @brief Runtime state for one virtual gamepad. + */ + struct gamepad_context_t { + std::unique_ptr adapter; ///< State adapter for the virtual gamepad. + feedback_queue_t feedback_queue; ///< Feedback queue for client output events. + std::array, 2> touch_ids; ///< Client touch IDs assigned to libvirtualhid slots. + std::uint8_t client_relative_index = 0; ///< Client-relative controller index. + bool has_last_rumble = false; ///< Whether last rumble values are valid. + std::uint16_t last_low_frequency_rumble = 0; ///< Last low-frequency rumble value. + std::uint16_t last_high_frequency_rumble = 0; ///< Last high-frequency rumble value. + bool has_last_trigger_rumble = false; ///< Whether last trigger rumble values are valid. + std::uint16_t last_left_trigger_rumble = 0; ///< Last left trigger rumble value. + std::uint16_t last_right_trigger_rumble = 0; ///< Last right trigger rumble value. + bool has_last_rgb = false; ///< Whether last RGB values are valid. + std::uint8_t last_red = 0; ///< Last red LED value. + std::uint8_t last_green = 0; ///< Last green LED value. + std::uint8_t last_blue = 0; ///< Last blue LED value. + }; + + namespace { + + /** + * @brief Gamepad profile exposed through Sunshine config. + */ + struct gamepad_profile_t { + std::string_view name; ///< Sunshine config value. + lvh::GamepadProfileKind kind; ///< libvirtualhid profile kind. + lvh::DeviceProfile (*profile)(); ///< Profile factory. + }; + + /** + * @brief Supported libvirtualhid gamepad profiles. + */ + constexpr std::array gamepad_profiles { + gamepad_profile_t {"generic", lvh::GamepadProfileKind::generic, lvh::profiles::generic_gamepad}, + gamepad_profile_t {"x360", lvh::GamepadProfileKind::xbox_360, lvh::profiles::xbox_360}, + gamepad_profile_t {"xone", lvh::GamepadProfileKind::xbox_one, lvh::profiles::xbox_one}, + gamepad_profile_t {"xseries", lvh::GamepadProfileKind::xbox_series, lvh::profiles::xbox_series}, + gamepad_profile_t {"ds4", lvh::GamepadProfileKind::dualshock4, lvh::profiles::dualshock4}, + gamepad_profile_t {"ds5", lvh::GamepadProfileKind::dualsense, lvh::profiles::dualsense}, + gamepad_profile_t {"switch", lvh::GamepadProfileKind::switch_pro, lvh::profiles::switch_pro}, + }; + + void log_failure(std::string_view operation, const lvh::OperationStatus &status) { + if (!status.ok()) { + BOOST_LOG(warning) << operation << ": "sv << status.message(); + } + } + + float normalize_axis(std::int16_t value) { + if (value < 0) { + return std::max(-1.0F, static_cast(value) / 32768.0F); + } + + return std::min(1.0F, static_cast(value) / 32767.0F); + } + + float normalize_trigger(std::uint8_t value) { + return static_cast(value) / static_cast(std::numeric_limits::max()); + } + + lvh::ClientControllerType client_controller_type(std::uint8_t type) { + switch (type) { + case LI_CTYPE_XBOX: + return lvh::ClientControllerType::xbox; + case LI_CTYPE_PS: + return lvh::ClientControllerType::playstation; + case LI_CTYPE_NINTENDO: + return lvh::ClientControllerType::nintendo; + case LI_CTYPE_UNKNOWN: + default: + return lvh::ClientControllerType::unknown; + } + } + + const gamepad_profile_t &profile_for_name(std::string_view name) { + const auto iter = std::ranges::find(gamepad_profiles, name, &gamepad_profile_t::name); + if (iter != gamepad_profiles.end()) { + return *iter; + } + + return gamepad_profiles[3]; // Xbox Series. + } + + const gamepad_profile_t &profile_for_metadata(const gamepad_arrival_t &metadata) { + if (config::input.gamepad != "auto"sv) { + return profile_for_name(config::input.gamepad); + } + + if (metadata.type == LI_CTYPE_PS) { + BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by client-reported type)"sv; + return profile_for_name("ds5"sv); + } + if (metadata.type == LI_CTYPE_NINTENDO) { + BOOST_LOG(info) << "Gamepad will be Nintendo Switch Pro controller (auto-selected by client-reported type)"sv; + return profile_for_name("switch"sv); + } + if (metadata.type == LI_CTYPE_XBOX) { + BOOST_LOG(info) << "Gamepad will be Xbox Series controller (auto-selected by client-reported type)"sv; + return profile_for_name("xseries"sv); + } + if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by motion sensor presence)"sv; + return profile_for_name("ds5"sv); + } + if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { + BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by touchpad presence)"sv; + return profile_for_name("ds5"sv); + } + + BOOST_LOG(info) << "Gamepad will be Xbox Series controller (default)"sv; + return profile_for_name("xseries"sv); + } + + std::string random_private_mac() { + std::random_device random; + return std::format( + "02:00:{:02x}:{:02x}:{:02x}:{:02x}", + random() & 0xFF, + random() & 0xFF, + random() & 0xFF, + random() & 0xFF + ); + } + + std::string gamepad_stable_id(const gamepad_id_t &id, const lvh::DeviceProfile &profile) { + if (profile.gamepad_kind != lvh::GamepadProfileKind::dualshock4 && + profile.gamepad_kind != lvh::GamepadProfileKind::dualsense) { + return std::format("sunshine-gamepad-{}", id.globalIndex); + } + + if (config::input.virtualhid_randomize_mac || id.globalIndex < 0 || id.globalIndex > 255) { + return random_private_mac(); + } + + return std::format("02:00:00:00:00:{:02x}", id.globalIndex); + } + + lvh::GamepadMetadata gamepad_metadata(const gamepad_id_t &id, const gamepad_arrival_t &metadata, const lvh::DeviceProfile &profile) { + lvh::GamepadMetadata result; + result.global_index = id.globalIndex; + result.client_relative_index = id.clientRelativeIndex; + result.client_type = client_controller_type(metadata.type); + result.has_motion_sensors = metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO); + result.has_touchpad = metadata.capabilities & LI_CCAP_TOUCHPAD; + result.has_rgb_led = metadata.capabilities & LI_CCAP_RGB_LED; + result.has_battery = metadata.capabilities & LI_CCAP_BATTERY_STATE; + result.stable_id = gamepad_stable_id(id, profile); + return result; + } + + void warn_unsupported_client_features(int global_index, const gamepad_arrival_t &metadata, const lvh::GamepadProfileSupport &support) { + if ((metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) && !support.supports_motion) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " has motion sensors, but the selected virtual profile cannot expose them"sv; + } + if ((metadata.capabilities & LI_CCAP_TOUCHPAD) && !support.supports_touchpad) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " has a touchpad, but the selected virtual profile cannot expose it"sv; + } + if ((metadata.capabilities & LI_CCAP_RGB_LED) && !support.supports_rgb_led) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " has an RGB LED, but the selected virtual profile cannot expose it"sv; + } + } + + void warn_missing_client_features(int global_index, const gamepad_arrival_t &metadata, const lvh::GamepadProfileSupport &support) { + if (support.supports_motion && !(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating a motion-capable controller, but the client gamepad does not have motion sensors active"sv; + } + if (support.supports_touchpad && !(metadata.capabilities & LI_CCAP_TOUCHPAD)) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating a touchpad-capable controller, but the client gamepad does not have a touchpad"sv; + } + } + + lvh::GamepadState make_gamepad_state(const gamepad_state_t &state, const lvh::GamepadProfileSupport &support) { + lvh::GamepadState result; + const auto flags = state.buttonFlags; + + result.buttons.set(lvh::GamepadButton::dpad_up, flags & DPAD_UP); + result.buttons.set(lvh::GamepadButton::dpad_down, flags & DPAD_DOWN); + result.buttons.set(lvh::GamepadButton::dpad_left, flags & DPAD_LEFT); + result.buttons.set(lvh::GamepadButton::dpad_right, flags & DPAD_RIGHT); + result.buttons.set(lvh::GamepadButton::start, flags & START); + result.buttons.set(lvh::GamepadButton::back, flags & BACK); + result.buttons.set(lvh::GamepadButton::left_stick, flags & LEFT_STICK); + result.buttons.set(lvh::GamepadButton::right_stick, flags & RIGHT_STICK); + result.buttons.set(lvh::GamepadButton::left_shoulder, flags & LEFT_BUTTON); + result.buttons.set(lvh::GamepadButton::right_shoulder, flags & RIGHT_BUTTON); + result.buttons.set(lvh::GamepadButton::guide, flags & HOME); + result.buttons.set(lvh::GamepadButton::a, flags & A); + result.buttons.set(lvh::GamepadButton::b, flags & B); + result.buttons.set(lvh::GamepadButton::x, flags & X); + result.buttons.set(lvh::GamepadButton::y, flags & Y); + result.buttons.set(lvh::GamepadButton::misc1, support.supports_misc1_button && (flags & MISC_BUTTON)); + result.buttons.set(lvh::GamepadButton::touchpad, support.supports_touchpad_button && (flags & TOUCHPAD_BUTTON)); + + if (support.supports_touchpad_button && + config::input.ds4_back_as_touchpad_click && + (config::input.gamepad == "ds4"sv || config::input.gamepad == "ds5"sv) && + (flags & BACK)) { + result.buttons.set(lvh::GamepadButton::touchpad); + } + + result.left_stick = {.x = normalize_axis(state.lsX), .y = normalize_axis(state.lsY)}; + result.right_stick = {.x = normalize_axis(state.rsX), .y = normalize_axis(state.rsY)}; + result.left_trigger = normalize_trigger(state.lt); + result.right_trigger = normalize_trigger(state.rt); + return result; + } + + std::optional mouse_button(int button) { + switch (button) { + case BUTTON_LEFT: + return lvh::MouseButton::left; + case BUTTON_MIDDLE: + return lvh::MouseButton::middle; + case BUTTON_RIGHT: + return lvh::MouseButton::right; + case BUTTON_X1: + return lvh::MouseButton::side; + case BUTTON_X2: + return lvh::MouseButton::extra; + default: + BOOST_LOG(warning) << "Unknown mouse button: "sv << button; + return std::nullopt; + } + } + + lvh::GamepadBatteryState battery_state(std::uint8_t state) { + switch (state) { + case LI_BATTERY_STATE_DISCHARGING: + return lvh::GamepadBatteryState::discharging; + case LI_BATTERY_STATE_CHARGING: + return lvh::GamepadBatteryState::charging; + case LI_BATTERY_STATE_FULL: + return lvh::GamepadBatteryState::full; + case LI_BATTERY_STATE_NOT_PRESENT: + case LI_BATTERY_STATE_NOT_CHARGING: + return lvh::GamepadBatteryState::charging_error; + case LI_BATTERY_STATE_UNKNOWN: + default: + return lvh::GamepadBatteryState::unknown; + } + } + + std::int32_t touch_orientation(std::uint16_t rotation) { + if (rotation == LI_ROT_UNKNOWN) { + return 0; + } + + auto adjusted = static_cast(rotation); + if (adjusted > 90 && adjusted < 270) { + adjusted = 180 - adjusted; + } + if (adjusted > 90) { + adjusted -= 360; + } else if (adjusted < -90) { + adjusted += 360; + } + + return adjusted; + } + + lvh::PenToolType pen_tool(std::uint8_t tool) { + switch (tool) { + case LI_TOOL_TYPE_PEN: + return lvh::PenToolType::pen; + case LI_TOOL_TYPE_ERASER: + return lvh::PenToolType::eraser; + case LI_TOOL_TYPE_UNKNOWN: + default: + return lvh::PenToolType::unchanged; + } + } + + void raise_feedback(const std::shared_ptr &gamepad, const gamepad_feedback_msg_t &message) { + if (gamepad->feedback_queue) { + gamepad->feedback_queue->raise(message); + } + } + + void handle_output(const std::shared_ptr &gamepad, const lvh::GamepadOutput &output) { + switch (output.kind) { + case lvh::GamepadOutputKind::rumble: + if (gamepad->has_last_rumble && + gamepad->last_low_frequency_rumble == output.low_frequency_rumble && + gamepad->last_high_frequency_rumble == output.high_frequency_rumble) { + return; + } + gamepad->has_last_rumble = true; + gamepad->last_low_frequency_rumble = output.low_frequency_rumble; + gamepad->last_high_frequency_rumble = output.high_frequency_rumble; + raise_feedback(gamepad, gamepad_feedback_msg_t::make_rumble(gamepad->client_relative_index, output.low_frequency_rumble, output.high_frequency_rumble)); + break; + case lvh::GamepadOutputKind::trigger_rumble: + if (gamepad->has_last_trigger_rumble && + gamepad->last_left_trigger_rumble == output.left_trigger_rumble && + gamepad->last_right_trigger_rumble == output.right_trigger_rumble) { + return; + } + gamepad->has_last_trigger_rumble = true; + gamepad->last_left_trigger_rumble = output.left_trigger_rumble; + gamepad->last_right_trigger_rumble = output.right_trigger_rumble; + raise_feedback(gamepad, gamepad_feedback_msg_t::make_rumble_triggers(gamepad->client_relative_index, output.left_trigger_rumble, output.right_trigger_rumble)); + break; + case lvh::GamepadOutputKind::rgb_led: + if (gamepad->has_last_rgb && + gamepad->last_red == output.red && + gamepad->last_green == output.green && + gamepad->last_blue == output.blue) { + return; + } + gamepad->has_last_rgb = true; + gamepad->last_red = output.red; + gamepad->last_green = output.green; + gamepad->last_blue = output.blue; + raise_feedback(gamepad, gamepad_feedback_msg_t::make_rgb_led(gamepad->client_relative_index, output.red, output.green, output.blue)); + break; + case lvh::GamepadOutputKind::adaptive_triggers: + raise_feedback(gamepad, gamepad_feedback_msg_t::make_adaptive_triggers(gamepad->client_relative_index, output.adaptive_trigger_flags, output.left_trigger_effect_type, output.right_trigger_effect_type, output.left_trigger_effect, output.right_trigger_effect)); + break; + case lvh::GamepadOutputKind::raw_report: + break; + } + } + + void release_all_touches(client_context_t &context) { + if (!context.touch) { + return; + } + + for (const auto id : context.active_touches) { + log_failure("release libvirtualhid touch contact"sv, context.touch->release_contact(id)); + } + context.active_touches.clear(); + } + + } // namespace + + input_context_t::input_context_t(): + runtime {create_runtime()}, + gamepads(MAX_GAMEPADS) { + if (!runtime) { + BOOST_LOG(warning) << "Unable to create libvirtualhid runtime"sv; + return; + } + + const auto &capabilities = runtime->capabilities(); + if (capabilities.supports_keyboard) { + lvh::CreateKeyboardOptions options; + options.profile = lvh::profiles::keyboard(); + options.stable_id = "sunshine-keyboard"; + auto created = runtime->create_keyboard(options); + if (created) { + keyboard = std::move(created.keyboard); + } else { + log_failure("create libvirtualhid keyboard"sv, created.status); + } + } + if (capabilities.supports_mouse) { + lvh::CreateMouseOptions options; + options.profile = lvh::profiles::mouse(); + options.stable_id = "sunshine-mouse"; + auto created = runtime->create_mouse(options); + if (created) { + mouse = std::move(created.mouse); + } else { + log_failure("create libvirtualhid mouse"sv, created.status); + } + } + } + + client_context_t::client_context_t(input_context_t &input): + global {&input} { + if (!global->runtime) { + return; + } + + const auto &capabilities = global->runtime->capabilities(); + if (capabilities.supports_touchscreen) { + lvh::CreateTouchscreenOptions options; + options.profile = lvh::profiles::touchscreen(); + options.stable_id = "sunshine-touchscreen"; + auto created = global->runtime->create_touchscreen(options); + if (created) { + touch = std::move(created.touchscreen); + } else { + log_failure("create libvirtualhid touchscreen"sv, created.status); + } + } + if (capabilities.supports_pen_tablet) { + lvh::CreatePenTabletOptions options; + options.profile = lvh::profiles::pen_tablet(); + options.stable_id = "sunshine-pen-tablet"; + auto created = global->runtime->create_pen_tablet(options); + if (created) { + pen = std::move(created.pen_tablet); + } else { + log_failure("create libvirtualhid pen tablet"sv, created.status); + } + } + } + + std::unique_ptr create_runtime() { + lvh::RuntimeOptions options; + options.backend = lvh::BackendKind::platform_default; + return lvh::Runtime::create(options); + } + + std::vector static_supported_gamepads() { + std::vector gamepads { + supported_gamepad_t {"auto", true, ""}, + }; + for (const auto &profile : gamepad_profiles) { + gamepads.push_back({std::string {profile.name}, false, ""}); + } + + return gamepads; + } + + std::vector supported_gamepads(lvh::Runtime *runtime, bool fallback_vigem_available) { + if (!runtime) { + return static_supported_gamepads(); + } + + const auto libvirtualhid_available = runtime->capabilities().supports_gamepad; + const auto reason = libvirtualhid_available ? "" : "gamepads.virtualhid-not-available"; + const auto auto_enabled = libvirtualhid_available || fallback_vigem_available; + std::vector gamepads { + supported_gamepad_t {"auto", auto_enabled, auto_enabled ? "" : reason}, + }; + + for (const auto &profile : gamepad_profiles) { + const auto fallback_supported = fallback_vigem_available && (profile.name == "x360"sv || profile.name == "ds4"sv); + const auto enabled = libvirtualhid_available || fallback_supported; + gamepads.push_back({std::string {profile.name}, enabled, enabled ? "" : reason}); + } + + for (auto &[name, is_enabled, reason_disabled] : gamepads) { + if (!is_enabled) { + BOOST_LOG(warning) << "Gamepad "sv << name << " is disabled due to "sv << reason_disabled; + } + } + + return gamepads; + } + + int alloc_gamepad(input_context_t &context, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + if (!context.runtime || !context.runtime->capabilities().supports_gamepad) { + return -1; + } + if (id.globalIndex < 0 || id.globalIndex >= static_cast(context.gamepads.size())) { + BOOST_LOG(warning) << "Invalid libvirtualhid gamepad index: "sv << id.globalIndex; + return -1; + } + + const auto &selection = profile_for_metadata(metadata); + auto profile = selection.profile(); + if (config::input.gamepad != "auto"sv) { + BOOST_LOG(info) << "Gamepad "sv << id.globalIndex << " will be "sv << profile.name << " (manual selection)"sv; + } else { + BOOST_LOG(info) << "Gamepad "sv << id.globalIndex << " will be "sv << profile.name; + } + + lvh::CreateGamepadOptions options; + options.profile = profile; + options.metadata = gamepad_metadata(id, metadata, profile); + auto created = lvh::GamepadStateAdapter::create(*context.runtime, options); + if (!created) { + log_failure("create libvirtualhid gamepad"sv, created.status); + return -1; + } + + auto gamepad = std::make_shared(); + gamepad->adapter = std::move(created.adapter); + gamepad->feedback_queue = std::move(feedback_queue); + gamepad->client_relative_index = id.clientRelativeIndex; + gamepad->adapter->set_output_callback([gamepad](const lvh::GamepadOutput &output) { + handle_output(gamepad, output); + }); + + const auto &support = gamepad->adapter->support(); + warn_unsupported_client_features(id.globalIndex, metadata, support); + warn_missing_client_features(id.globalIndex, metadata, support); + if (support.supports_motion) { + raise_feedback(gamepad, gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100)); + raise_feedback(gamepad, gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100)); + } + + context.gamepads[id.globalIndex] = std::move(gamepad); + return 0; + } + + bool has_gamepad(const input_context_t &context, int nr) { + return nr >= 0 && nr < context.gamepads.size() && context.gamepads[nr] && context.gamepads[nr]->adapter; + } + + void free_gamepad(input_context_t &context, int nr) { + if (has_gamepad(context, nr)) { + context.gamepads[nr].reset(); + } + } + + void gamepad_update(input_context_t &context, int nr, const gamepad_state_t &state) { + if (!has_gamepad(context, nr)) { + return; + } + + auto &gamepad = context.gamepads[nr]; + log_failure("submit libvirtualhid gamepad state"sv, gamepad->adapter->set_state(make_gamepad_state(state, gamepad->adapter->support()))); + } + + void gamepad_touch(input_context_t &context, const gamepad_touch_t &touch) { + if (!has_gamepad(context, touch.id.globalIndex)) { + return; + } + + auto &gamepad = context.gamepads[touch.id.globalIndex]; + if (!gamepad->adapter->support().supports_touchpad) { + return; + } + + if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + for (std::size_t index = 0; index < gamepad->touch_ids.size(); ++index) { + if (gamepad->touch_ids[index]) { + log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(index)); + gamepad->touch_ids[index].reset(); + } + } + return; + } + + auto slot = std::ranges::find(gamepad->touch_ids, touch.pointerId); + if (touch.eventType == LI_TOUCH_EVENT_DOWN && slot == gamepad->touch_ids.end()) { + slot = std::ranges::find_if(gamepad->touch_ids, [](const auto &id) { + return !id.has_value(); + }); + if (slot == gamepad->touch_ids.end()) { + BOOST_LOG(warning) << "No free libvirtualhid gamepad touch slots"sv; + return; + } + *slot = touch.pointerId; + } + + if (slot == gamepad->touch_ids.end()) { + return; + } + + const auto index = static_cast(std::distance(gamepad->touch_ids.begin(), slot)); + if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) { + log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(index)); + slot->reset(); + return; + } + if (touch.eventType != LI_TOUCH_EVENT_DOWN && touch.eventType != LI_TOUCH_EVENT_MOVE) { + return; + } + + lvh::GamepadTouchContact contact; + contact.id = static_cast(index); + contact.active = touch.pressure > 0.5F; + contact.x = std::clamp(touch.x, 0.0F, 1.0F); + contact.y = std::clamp(touch.y, 0.0F, 1.0F); + log_failure("submit libvirtualhid gamepad touch"sv, gamepad->adapter->set_touchpad_contact(index, contact)); + } + + void gamepad_motion(input_context_t &context, const gamepad_motion_t &motion) { + if (!has_gamepad(context, motion.id.globalIndex)) { + return; + } + + auto &gamepad = context.gamepads[motion.id.globalIndex]; + switch (motion.motionType) { + case LI_MOTION_TYPE_ACCEL: + log_failure("submit libvirtualhid gamepad acceleration"sv, gamepad->adapter->set_acceleration(lvh::Vector3 {motion.x, motion.y, motion.z})); + break; + case LI_MOTION_TYPE_GYRO: + log_failure("submit libvirtualhid gamepad gyroscope"sv, gamepad->adapter->set_gyroscope(lvh::Vector3 {motion.x, motion.y, motion.z})); + break; + default: + break; + } + } + + void gamepad_battery(input_context_t &context, const gamepad_battery_t &battery) { + if (!has_gamepad(context, battery.id.globalIndex)) { + return; + } + + auto &gamepad = context.gamepads[battery.id.globalIndex]; + if (battery.state == LI_BATTERY_STATE_UNKNOWN || battery.state == LI_BATTERY_STATE_NOT_PRESENT) { + log_failure("clear libvirtualhid gamepad battery"sv, gamepad->adapter->clear_battery()); + return; + } + + lvh::GamepadBattery value; + value.state = battery_state(battery.state); + value.percentage = battery.percentage == LI_BATTERY_PERCENTAGE_UNKNOWN ? 100 : std::min(battery.percentage, 100); + log_failure("submit libvirtualhid gamepad battery"sv, gamepad->adapter->set_battery(value)); + } + + void move_mouse(input_context_t &context, int delta_x, int delta_y) { + if (context.mouse) { + log_failure("submit libvirtualhid mouse movement"sv, context.mouse->move_relative(delta_x, delta_y)); + } + } + + void abs_mouse(input_context_t &context, const touch_port_t &touch_port, float x, float y) { + if (context.mouse) { + log_failure( + "submit libvirtualhid absolute mouse movement"sv, + context.mouse->move_absolute( + static_cast(std::lround(x)), + static_cast(std::lround(y)), + touch_port.width, + touch_port.height + ) + ); + } + } + + void button_mouse(input_context_t &context, int button, bool release) { + if (context.mouse) { + const auto converted = mouse_button(button); + if (!converted) { + return; + } + + log_failure("submit libvirtualhid mouse button"sv, context.mouse->button(*converted, !release)); + } + } + + void scroll(input_context_t &context, int high_res_distance) { + if (context.mouse) { + log_failure("submit libvirtualhid vertical scroll"sv, context.mouse->vertical_scroll(high_res_distance)); + } + } + + void hscroll(input_context_t &context, int high_res_distance) { + if (context.mouse) { + log_failure("submit libvirtualhid horizontal scroll"sv, context.mouse->horizontal_scroll(high_res_distance)); + } + } + + void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release) { + if (context.keyboard) { + log_failure("submit libvirtualhid keyboard input"sv, context.keyboard->submit({.key_code = modcode, .pressed = !release})); + } + } + + void unicode(input_context_t &context, const char *utf8, int size) { + if (context.keyboard && utf8 && size > 0) { + log_failure("submit libvirtualhid text input"sv, context.keyboard->type_text({.text = std::string {utf8, static_cast(size)}})); + } + } + + void touch_update(client_context_t &context, const touch_input_t &touch) { + if (!context.touch) { + return; + } + + switch (touch.eventType) { + case LI_TOUCH_EVENT_CANCEL_ALL: + release_all_touches(context); + return; + case LI_TOUCH_EVENT_UP: + case LI_TOUCH_EVENT_CANCEL: + case LI_TOUCH_EVENT_HOVER_LEAVE: + log_failure("release libvirtualhid touch contact"sv, context.touch->release_contact(static_cast(touch.pointerId))); + context.active_touches.erase(static_cast(touch.pointerId)); + return; + case LI_TOUCH_EVENT_HOVER: + case LI_TOUCH_EVENT_DOWN: + case LI_TOUCH_EVENT_MOVE: + { + lvh::TouchContact contact; + contact.id = static_cast(touch.pointerId); + contact.x = std::clamp(touch.x, 0.0F, 1.0F); + contact.y = std::clamp(touch.y, 0.0F, 1.0F); + contact.pressure = std::clamp(touch.pressureOrDistance, 0.0F, 1.0F); + contact.orientation = touch_orientation(touch.rotation); + log_failure("submit libvirtualhid touch contact"sv, context.touch->place_contact(contact)); + context.active_touches.insert(contact.id); + return; + } + default: + return; + } + } + + void pen_update(client_context_t &context, const pen_input_t &pen) { + if (!context.pen) { + return; + } + + const std::array button_states { + std::pair {lvh::PenButton::primary, (pen.penButtons & LI_PEN_BUTTON_PRIMARY) != 0}, + std::pair {lvh::PenButton::secondary, (pen.penButtons & LI_PEN_BUTTON_SECONDARY) != 0}, + std::pair {lvh::PenButton::tertiary, (pen.penButtons & LI_PEN_BUTTON_TERTIARY) != 0}, + }; + for (const auto &[button, pressed] : button_states) { + const auto was_pressed = context.pressed_pen_buttons.contains(button); + if (pressed == was_pressed) { + continue; + } + + log_failure("submit libvirtualhid pen button"sv, context.pen->button(button, pressed)); + if (pressed) { + context.pressed_pen_buttons.insert(button); + } else { + context.pressed_pen_buttons.erase(button); + } + } + + if (pen.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + for (const auto button : context.pressed_pen_buttons) { + log_failure("release libvirtualhid pen button"sv, context.pen->button(button, false)); + } + context.pressed_pen_buttons.clear(); + return; + } + + auto rotation = pen.rotation; + if (rotation != LI_ROT_UNKNOWN) { + rotation %= 360; + } + + float tilt_x = 0.0F; + float tilt_y = 0.0F; + if (pen.tilt != LI_TILT_UNKNOWN && rotation != LI_ROT_UNKNOWN) { + const auto rotation_rads = static_cast(rotation) * std::numbers::pi_v / 180.0F; + const auto tilt_rads = static_cast(pen.tilt) * std::numbers::pi_v / 180.0F; + const auto r = std::sin(tilt_rads); + const auto z = std::cos(tilt_rads); + + tilt_x = std::atan2(std::sin(-rotation_rads) * r, z) * 180.0F / std::numbers::pi_v; + tilt_y = std::atan2(std::cos(-rotation_rads) * r, z) * 180.0F / std::numbers::pi_v; + } + + const auto is_touching = pen.eventType == LI_TOUCH_EVENT_DOWN || pen.eventType == LI_TOUCH_EVENT_MOVE; + lvh::PenToolState state; + state.tool = pen_tool(pen.toolType); + state.x = std::clamp(pen.x, 0.0F, 1.0F); + state.y = std::clamp(pen.y, 0.0F, 1.0F); + state.pressure = is_touching ? std::clamp(pen.pressureOrDistance, 0.0F, 1.0F) : -1.0F; + state.distance = is_touching ? -1.0F : std::clamp(pen.pressureOrDistance, 0.0F, 1.0F); + state.tilt_x = tilt_x; + state.tilt_y = tilt_y; + log_failure("submit libvirtualhid pen state"sv, context.pen->place_tool(state)); + } + + bool configured_gamepad_supports_touchpad() { + if (config::input.gamepad == "auto"sv) { + return true; + } + + const auto profile = profile_for_name(config::input.gamepad).profile(); + return lvh::gamepad_profile_support(profile).supports_touchpad; + } + +} // namespace platf::virtualhid diff --git a/src/platform/virtualhid_input.h b/src/platform/virtualhid_input.h new file mode 100644 index 00000000000..ae213d9b7bd --- /dev/null +++ b/src/platform/virtualhid_input.h @@ -0,0 +1,224 @@ +/** + * @file src/platform/virtualhid_input.h + * @brief Declarations for libvirtualhid-backed input helpers. + */ +#pragma once + +// standard includes +#include +#include +#include +#include +#include + +// lib includes +#include + +// local includes +#include "src/platform/common.h" + +namespace platf::virtualhid { + + /** + * @brief Runtime and virtual devices owned by one platform input context. + */ + struct input_context_t { + /** + * @brief Construct the libvirtualhid input context. + */ + input_context_t(); + + std::unique_ptr runtime; ///< libvirtualhid runtime. + std::unique_ptr keyboard; ///< Shared virtual keyboard. + std::unique_ptr mouse; ///< Shared virtual mouse. + std::vector> gamepads; ///< Virtual gamepad slots. + }; + + /** + * @brief Per-client virtual touch and pen state. + */ + struct client_context_t { + /** + * @brief Create per-client libvirtualhid devices. + * + * @param input Global input context. + */ + explicit client_context_t(input_context_t &input); + + input_context_t *global = nullptr; ///< Shared global input context. + std::unique_ptr touch; ///< Per-client touchscreen. + std::unique_ptr pen; ///< Per-client pen tablet. + std::set active_touches; ///< Active touchscreen contacts. + std::set pressed_pen_buttons; ///< Active pen tablet buttons. + }; + + /** + * @brief Create a platform-default libvirtualhid runtime. + * + * @return Runtime instance. + */ + std::unique_ptr create_runtime(); + + /** + * @brief Return static gamepad choices for config validation. + * + * @return Supported gamepad choices. + */ + std::vector static_supported_gamepads(); + + /** + * @brief Return gamepad choices and runtime availability. + * + * @param runtime Runtime to probe. + * @param fallback_vigem_available Whether Windows ViGEm fallback can create gamepads. + * @return Supported gamepad choices. + */ + std::vector supported_gamepads(lvh::Runtime *runtime, bool fallback_vigem_available = false); + + /** + * @brief Allocate a libvirtualhid gamepad. + * + * @param context Input context. + * @param id Sunshine gamepad identifiers. + * @param metadata Client-reported controller metadata. + * @param feedback_queue Queue used to return gamepad feedback to the client. + * @return 0 on success, otherwise -1. + */ + int alloc_gamepad(input_context_t &context, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); + + /** + * @brief Check whether a libvirtualhid gamepad exists in a slot. + * + * @param context Input context. + * @param nr Gamepad slot index. + * @return True when a virtual gamepad is active. + */ + bool has_gamepad(const input_context_t &context, int nr); + + /** + * @brief Release a libvirtualhid gamepad slot. + * + * @param context Input context. + * @param nr Gamepad slot index. + */ + void free_gamepad(input_context_t &context, int nr); + + /** + * @brief Submit a full gamepad state. + * + * @param context Input context. + * @param nr Gamepad slot index. + * @param state Sunshine gamepad state. + */ + void gamepad_update(input_context_t &context, int nr, const gamepad_state_t &state); + + /** + * @brief Submit a gamepad touch event. + * + * @param context Input context. + * @param touch Sunshine touch event. + */ + void gamepad_touch(input_context_t &context, const gamepad_touch_t &touch); + + /** + * @brief Submit a gamepad motion event. + * + * @param context Input context. + * @param motion Sunshine motion event. + */ + void gamepad_motion(input_context_t &context, const gamepad_motion_t &motion); + + /** + * @brief Submit gamepad battery metadata. + * + * @param context Input context. + * @param battery Sunshine battery event. + */ + void gamepad_battery(input_context_t &context, const gamepad_battery_t &battery); + + /** + * @brief Move the virtual mouse relatively. + * + * @param context Input context. + * @param delta_x Horizontal delta. + * @param delta_y Vertical delta. + */ + void move_mouse(input_context_t &context, int delta_x, int delta_y); + + /** + * @brief Move the virtual mouse absolutely inside a target touch port. + * + * @param context Input context. + * @param touch_port Target coordinate space. + * @param x Absolute X coordinate. + * @param y Absolute Y coordinate. + */ + void abs_mouse(input_context_t &context, const touch_port_t &touch_port, float x, float y); + + /** + * @brief Submit a mouse button event. + * + * @param context Input context. + * @param button Moonlight mouse button. + * @param release Whether the button was released. + */ + void button_mouse(input_context_t &context, int button, bool release); + + /** + * @brief Submit vertical scroll input. + * + * @param context Input context. + * @param high_res_distance High-resolution scroll distance. + */ + void scroll(input_context_t &context, int high_res_distance); + + /** + * @brief Submit horizontal scroll input. + * + * @param context Input context. + * @param high_res_distance High-resolution scroll distance. + */ + void hscroll(input_context_t &context, int high_res_distance); + + /** + * @brief Submit a keyboard key transition. + * + * @param context Input context. + * @param modcode Portable key code. + * @param release Whether the key was released. + */ + void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release); + + /** + * @brief Submit UTF-8 text input. + * + * @param context Input context. + * @param utf8 UTF-8 text buffer. + * @param size Text buffer size. + */ + void unicode(input_context_t &context, const char *utf8, int size); + + /** + * @brief Submit a touchscreen event. + * + * @param context Client context. + * @param touch Touch event. + */ + void touch_update(client_context_t &context, const touch_input_t &touch); + + /** + * @brief Submit a pen event. + * + * @param context Client context. + * @param pen Pen event. + */ + void pen_update(client_context_t &context, const pen_input_t &pen); + + /** + * @brief Return whether the configured gamepad profile can expose touchpad input. + * + * @return True when controller touchpad input should be advertised. + */ + bool configured_gamepad_supports_touchpad(); + +} // namespace platf::virtualhid diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 7fcbc3a2ef1..6e7bcb48414 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -31,6 +31,7 @@ #include "src/globals.h" #include "src/logging.h" #include "src/platform/common.h" +#include "src/platform/virtualhid_input.h" namespace platf { using namespace std::literals; @@ -269,7 +270,8 @@ namespace platf { VIGEM_ERROR status = vigem_connect(client.get()); if (!VIGEM_SUCCESS(status)) { // Log a special fatal message for this case to show the error in the web UI - BOOST_LOG(fatal) << "ViGEmBus is not installed or running. You must install ViGEmBus for gamepad support!"sv; + BOOST_LOG(fatal) << "libvirtualhid gamepad support is unavailable and ViGEmBus fallback is not installed or running"sv; + return -1; } else { vigem_disconnect(client.get()); } @@ -501,13 +503,14 @@ namespace platf { } /** - * @brief Global inputtino device handles shared by clients. + * @brief Global virtual input device handles shared by clients. */ struct input_raw_t { ~input_raw_t() { delete vigem; } + virtualhid::input_context_t virtualhid; ///< libvirtualhid input context. vigem_t *vigem; ///< Vigem. decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; ///< Fn create synthetic pointer device. @@ -519,10 +522,13 @@ namespace platf { input_t result {new input_raw_t {}}; auto &raw = *(input_raw_t *) result.get(); - raw.vigem = new vigem_t {}; - if (raw.vigem->init()) { - delete raw.vigem; - raw.vigem = nullptr; + raw.vigem = nullptr; + if (!raw.virtualhid.runtime || !raw.virtualhid.runtime->capabilities().supports_gamepad) { + raw.vigem = new vigem_t {}; + if (raw.vigem->init()) { + delete raw.vigem; + raw.vigem = nullptr; + } } // Get pointers to virtual touch/pen input functions (Win10 1809+) @@ -533,6 +539,38 @@ namespace platf { return result; } + /** + * @brief Check whether the configured virtual gamepad can fall back to ViGEm. + * + * @return True when the ViGEm fallback can satisfy the configured profile. + */ + bool vigem_fallback_allowed() { + return config::input.gamepad == "auto"sv || + config::input.gamepad == "x360"sv || + config::input.gamepad == "ds4"sv; + } + + /** + * @brief Create the ViGEm fallback context if it is not already available. + * + * @param raw Platform input context. + * @return True when ViGEm fallback is available. + */ + bool ensure_vigem(input_raw_t *raw) { + if (raw->vigem) { + return true; + } + + raw->vigem = new vigem_t {}; + if (raw->vigem->init()) { + delete raw->vigem; + raw->vigem = nullptr; + return false; + } + + return true; + } + /** * @brief Calls SendInput() and switches input desktops if required. * @param i The `INPUT` struct to send. @@ -726,7 +764,7 @@ namespace platf { } /** - * @brief Per-client inputtino devices for touch and pen input. + * @brief Per-client virtual devices for touch and pen input. */ struct client_input_raw_t: public client_input_t { /** @@ -754,7 +792,7 @@ namespace platf { } } - input_raw_t *global; + input_raw_t *global; ///< Global input context shared by this client context. // Device state and handles for pen and touch input must be stored in the per-client // input context, because each connected client may be sending their own independent @@ -1248,10 +1286,19 @@ namespace platf { int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { auto raw = (input_raw_t *) input.get(); - if (!raw->vigem) { + if (virtualhid::alloc_gamepad(raw->virtualhid, id, metadata, feedback_queue) == 0) { return 0; } + if (!vigem_fallback_allowed()) { + BOOST_LOG(warning) << "libvirtualhid could not create the requested gamepad profile, and ViGEm fallback cannot emulate "sv << config::input.gamepad; + return -1; + } + + if (!ensure_vigem(raw)) { + return -1; + } + VIGEM_TARGET_TYPE selectedGamepadType; if (config::input.gamepad == "x360"sv) { @@ -1302,6 +1349,11 @@ namespace platf { void free_gamepad(input_t &input, int nr) { auto raw = (input_raw_t *) input.get(); + if (virtualhid::has_gamepad(raw->virtualhid, nr)) { + virtualhid::free_gamepad(raw->virtualhid, nr); + return; + } + if (!raw->vigem) { return; } @@ -1560,7 +1612,13 @@ namespace platf { * @param gamepad_state The gamepad button/axis state sent from the client. */ void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + if (virtualhid::has_gamepad(raw->virtualhid, nr)) { + virtualhid::gamepad_update(raw->virtualhid, nr, gamepad_state); + return; + } + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1592,7 +1650,13 @@ namespace platf { * @param touch The touch event. */ void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + if (virtualhid::has_gamepad(raw->virtualhid, touch.id.globalIndex)) { + virtualhid::gamepad_touch(raw->virtualhid, touch); + return; + } + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1698,7 +1762,13 @@ namespace platf { * @param motion The motion event. */ void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + if (virtualhid::has_gamepad(raw->virtualhid, motion.id.globalIndex)) { + virtualhid::gamepad_motion(raw->virtualhid, motion); + return; + } + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1725,7 +1795,13 @@ namespace platf { * @param battery The battery event. */ void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + if (virtualhid::has_gamepad(raw->virtualhid, battery.id.globalIndex)) { + virtualhid::gamepad_battery(raw->virtualhid, battery); + return; + } + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1799,33 +1875,14 @@ namespace platf { } std::vector &supported_gamepads(input_t *input) { - if (!input) { - static std::vector gps { - supported_gamepad_t {"auto", true, ""}, - supported_gamepad_t {"x360", false, ""}, - supported_gamepad_t {"ds4", false, ""}, - }; - + static std::vector gps; + if (!input || !input->get()) { + gps = virtualhid::static_supported_gamepads(); return gps; } - auto vigem = ((input_raw_t *) input)->vigem; - auto enabled = vigem != nullptr; - auto reason = enabled ? "" : "gamepads.vigem-not-available"; - - // ds4 == ps4 - static std::vector gps { - supported_gamepad_t {"auto", true, reason}, - supported_gamepad_t {"x360", enabled, reason}, - supported_gamepad_t {"ds4", enabled, reason} - }; - - for (auto &[name, is_enabled, reason_disabled] : gps) { - if (!is_enabled) { - BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled; - } - } - + const auto raw = (input_raw_t *) input->get(); + gps = virtualhid::supported_gamepads(raw->virtualhid.runtime.get(), raw->vigem != nullptr); return gps; } @@ -1836,8 +1893,7 @@ namespace platf { platform_caps::caps_t get_capabilities() { platform_caps::caps_t caps = 0; - // We support controller touchpad input as long as we're not emulating X360 - if (config::input.gamepad != "x360"sv) { + if (virtualhid::configured_gamepad_supports_touchpad()) { caps |= platform_caps::controller_touch; } diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index a9476ea02bf..cf36f0001f7 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -227,7 +227,7 @@

{{ $t('config.configuration') }}

"ds4_back_as_touchpad_click": "enabled", "motion_as_ds4": "enabled", "touchpad_as_ds4": "enabled", - "ds5_inputtino_randomize_mac": "enabled", + "virtualhid_randomize_mac": "enabled", "back_button_timeout": -1, "keyboard": "enabled", "key_repeat_delay": 500, diff --git a/src_assets/common/assets/web/configs/tabs/Inputs.vue b/src_assets/common/assets/web/configs/tabs/Inputs.vue index 7fa76a20721..fca4520fe93 100644 --- a/src_assets/common/assets/web/configs/tabs/Inputs.vue +++ b/src_assets/common/assets/web/configs/tabs/Inputs.vue @@ -29,19 +29,33 @@ const config = ref(props.config) @@ -88,12 +102,12 @@ const config = ref(props.config) default="true" > - -