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
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.
@@ -459,7 +468,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
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!
-
Nintendo Switch emulation is only available on Linux.
Gamepad emulation is not currently supported on macOS.
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"
>
-
-
+
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 397abb4a32e..e952c7e9743 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -224,8 +224,8 @@
"dd_wa_hdr_toggle_delay": "High-contrast workaround for HDR",
"ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click",
"ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click",
- "ds5_inputtino_randomize_mac": "Randomize virtual controller MAC",
- "ds5_inputtino_randomize_mac_desc": "Upon controller registration use a random MAC instead of one based on the controllers internal index to avoid mixing configuration settings of different controllers when the are swapped on client-side.",
+ "virtualhid_randomize_mac": "Randomize virtual controller MAC",
+ "virtualhid_randomize_mac_desc": "Use a random MAC address for PlayStation-style virtual controllers instead of one based on the controller index. This avoids mixing per-controller settings when controllers are swapped on the client.",
"encoder": "Force a Specific Encoder",
"encoder_desc": "Force a specific encoder, otherwise Sunshine will select the best available option. Note: If you specify a hardware encoder on Windows, it must match the GPU where the display is connected.",
"encoder_software": "Software",
@@ -246,10 +246,12 @@
"gamepad_ds4_manual": "DS4 selection options",
"gamepad_ds5": "DS5 (PS5)",
"gamepad_ds5_manual": "DS5 selection options",
+ "gamepad_generic": "Generic HID gamepad",
"gamepad_switch": "Nintendo Pro (Switch)",
"gamepad_manual": "Manual DS4 options",
"gamepad_x360": "X360 (Xbox 360)",
"gamepad_xone": "XOne (Xbox One)",
+ "gamepad_xseries": "Xbox Series",
"global_prep_cmd": "Command Preparations",
"global_prep_cmd_desc": "Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.",
"hevc_mode": "HEVC Support",
@@ -433,10 +435,10 @@
"startup_errors": "Attention! Sunshine detected these errors during startup. We STRONGLY RECOMMEND fixing them before streaming.",
"version_dirty": "Thank you for helping to make Sunshine a better software!",
"version_latest": "You are running the latest version of Sunshine",
- "vigembus_not_installed_desc": "Virtual gamepad support will not work without the ViGEmBus driver. Click the button below to install it.",
- "vigembus_not_installed_title": "ViGEmBus Driver Not Installed",
- "vigembus_outdated_desc": "You are running an outdated version of ViGEmBus (v{version}). Version 1.17 or higher is required for proper gamepad support. Click the button below to update.",
- "vigembus_outdated_title": "ViGEmBus Driver Outdated",
+ "vigembus_not_installed_desc": "libvirtualhid is the primary virtual gamepad backend. ViGEmBus is only needed as a fallback for Xbox 360 and DualShock 4 gamepads when libvirtualhid is unavailable. Click the button below to install it.",
+ "vigembus_not_installed_title": "ViGEmBus Fallback Not Installed",
+ "vigembus_outdated_desc": "You are running an outdated version of the ViGEmBus fallback (v{version}). Version 1.17 or higher is required for fallback gamepad support. Click the button below to update.",
+ "vigembus_outdated_title": "ViGEmBus Fallback Outdated",
"welcome": "Hello, Sunshine!"
},
"navbar": {
@@ -532,16 +534,16 @@
"unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.",
"unpair_single_unknown": "Unknown Client",
"unpair_title": "Unpair Devices",
- "vigembus_compatible": "ViGEmBus is installed and compatible.",
+ "vigembus_compatible": "ViGEmBus fallback is installed and compatible.",
"vigembus_current_version": "Current Version",
- "vigembus_desc": "ViGEmBus is required for virtual gamepad support. Install or update the driver if it's missing or outdated (version 1.17 or higher required).",
- "vigembus_incompatible": "ViGEmBus version is too old. Please install version 1.17 or higher.",
+ "vigembus_desc": "libvirtualhid is the primary virtual gamepad backend. Install or update ViGEmBus only if you need Xbox 360 or DualShock 4 fallback support when libvirtualhid is unavailable.",
+ "vigembus_incompatible": "ViGEmBus fallback version is too old. Please install version 1.17 or higher.",
"vigembus_install": "ViGEmBus Driver",
"vigembus_install_button": "Install ViGEmBus v{version}",
"vigembus_install_error": "Failed to install ViGEmBus driver.",
"vigembus_install_success": "ViGEmBus driver installed successfully! You may need to restart your computer.",
"vigembus_force_reinstall_button": "Force Reinstall ViGEmBus v{version}",
- "vigembus_not_installed": "ViGEmBus is not installed."
+ "vigembus_not_installed": "ViGEmBus fallback is not installed."
},
"featured": {
"categories": {
diff --git a/tests/unit/test_mouse.cpp b/tests/unit/test_mouse.cpp
index eda5fd02ed5..899400fa2f4 100644
--- a/tests/unit/test_mouse.cpp
+++ b/tests/unit/test_mouse.cpp
@@ -15,8 +15,8 @@ struct MouseHIDTest: PlatformTestSuite, testing::WithParamInterface