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
Date: Fri, 3 Jul 2026 21:09:04 -0400
Subject: [PATCH 02/10] build(linux): fix link error on ArchLinux
---
cmake/targets/linux.cmake | 9 ++++++++-
packaging/linux/Arch/PKGBUILD | 1 +
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake
index b7cce8a7d17..ad49a0f811c 100644
--- a/cmake/targets/linux.cmake
+++ b/cmake/targets/linux.cmake
@@ -4,6 +4,13 @@ if(NOT FREEBSD)
# Using newer c++ compilers / features on older distros causes runtime dyn link errors
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
-static-libgcc
- -static-libstdc++
)
+
+ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15)
+ # GCC 15's static libstdc++ does not provide the out-of-line wait/notify
+ # symbol used by std::stop_token, so libstdc++ must be a direct DSO input.
+ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES stdc++)
+ else()
+ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES -static-libstdc++)
+ endif()
endif()
diff --git a/packaging/linux/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD
index ef624407035..6cd89f9d3e9 100644
--- a/packaging/linux/Arch/PKGBUILD
+++ b/packaging/linux/Arch/PKGBUILD
@@ -31,6 +31,7 @@ fi
depends=(
'avahi'
'curl'
+ 'gcc-libs'
'gtk3'
'hicolor-icon-theme'
'libayatana-appindicator'
From 24f9a141b6c56425c06e7c5ee4ec8f6299e0cb5f Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Sun, 5 Jul 2026 09:19:59 -0400
Subject: [PATCH 03/10] Refactor virtual input driver status
Replace the ViGEmBus install/status flow with a unified virtual input driver status endpoint and UI. Update Windows packaging, docs, locale strings, and troubleshooting pages to reflect libvirtualhid as the primary backend, with ViGEmBus shown only as a fallback. Add unit coverage for driver version parsing and status JSON helpers.
---
cmake/compile_definitions/windows.cmake | 5 -
cmake/packaging/windows.cmake | 25 --
docs/api.md | 7 +-
docs/getting_started.md | 9 +-
docs/troubleshooting.md | 9 +-
src/confighttp.cpp | 394 +++++++++++++-----
src/confighttp.h | 23 +
src_assets/common/assets/web/index.html | 37 +-
.../assets/web/public/assets/locale/en.json | 33 +-
.../common/assets/web/troubleshooting.html | 167 ++++----
tests/unit/test_confighttp.cpp | 50 +++
11 files changed, 507 insertions(+), 252 deletions(-)
diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake
index 257c61dba82..7fd56ee2f00 100644
--- a/cmake/compile_definitions/windows.cmake
+++ b/cmake/compile_definitions/windows.cmake
@@ -58,11 +58,6 @@ set_target_properties(sunshine_rc_object PROPERTIES
INCLUDE_DIRECTORIES ""
)
-# ViGEmBus version
-set(VIGEMBUS_PACKAGED_V "1.21.442")
-set(VIGEMBUS_PACKAGED_V_2 "${VIGEMBUS_PACKAGED_V}.0")
-list(APPEND SUNSHINE_DEFINITIONS VIGEMBUS_PACKAGED_VERSION="${VIGEMBUS_PACKAGED_V_2}")
-
set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h"
diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake
index 69830da6b9a..c826c70d5a0 100644
--- a/cmake/packaging/windows.cmake
+++ b/cmake/packaging/windows.cmake
@@ -9,24 +9,6 @@ if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64" AND DEFINED _MINHOOK_DLL)
install(FILES "${_MINHOOK_DLL}" DESTINATION "." COMPONENT application)
endif()
-# ViGEmBus installer
-set(SUNSHINE_THIRD_PARTY_DIR "third-party")
-set(VIGEMBUS_INSTALLER "${CMAKE_BINARY_DIR}/${SUNSHINE_THIRD_PARTY_DIR}/vigembus_installer.exe")
-set(VIGEMBUS_DOWNLOAD_URL_1 "https://github.com/nefarius/ViGEmBus/releases/download")
-set(VIGEMBUS_DOWNLOAD_URL_2 "v${VIGEMBUS_PACKAGED_V_2}/ViGEmBus_${VIGEMBUS_PACKAGED_V}_x64_x86_arm64.exe")
-file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/${SUNSHINE_THIRD_PARTY_DIR}")
-file(DOWNLOAD
- "${VIGEMBUS_DOWNLOAD_URL_1}/${VIGEMBUS_DOWNLOAD_URL_2}"
- ${VIGEMBUS_INSTALLER}
- SHOW_PROGRESS
- EXPECTED_HASH SHA256=155c50f1eec07bdc28d2f61a3e3c2c6c132fee7328412de224695f89143316bc
- TIMEOUT 60
-)
-install(FILES ${VIGEMBUS_INSTALLER}
- DESTINATION "${SUNSHINE_THIRD_PARTY_DIR}"
- RENAME "vigembus_installer.exe"
- COMPONENT gamepad)
-
# Adding tools
install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi)
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
@@ -80,8 +62,6 @@ set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}")
# Setting components groups and dependencies
set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true)
-set(CPACK_COMPONENT_GROUP_THIRDPARTY_DISPLAY_NAME "Third Party")
-set(CPACK_COMPONENT_GROUP_THIRDPARTY_DESCRIPTION "Bundled third-party installers and optional components.")
# sunshine binary
set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}")
@@ -116,11 +96,6 @@ set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "Add Firewall Exclusions")
set(CPACK_COMPONENT_FIREWALL_DESCRIPTION "Scripts to enable or disable firewall rules.")
set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts")
-# gamepad third-party installer
-set(CPACK_COMPONENT_GAMEPAD_DISPLAY_NAME "Virtual Gamepad")
-set(CPACK_COMPONENT_GAMEPAD_DESCRIPTION "ViGEmBus installer for virtual gamepad support.")
-set(CPACK_COMPONENT_GAMEPAD_GROUP "ThirdParty")
-
# include specific packaging
include(${CMAKE_MODULE_PATH}/packaging/windows_nsis.cmake)
include(${CMAKE_MODULE_PATH}/packaging/windows_wix.cmake)
diff --git a/docs/api.md b/docs/api.md
index c62545b6edb..056def1a0b5 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -94,11 +94,8 @@ curl -u user:pass -H "X-CSRF-Token: your_token_here" \
## POST /api/restart
@copydoc confighttp::restart()
-## GET /api/vigembus/status
-@copydoc confighttp::getViGEmBusStatus()
-
-## POST /api/vigembus/install
-@copydoc confighttp::installViGEmBus()
+## GET /api/virtual-input/status
+@copydoc confighttp::getVirtualInputStatus()
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 16de4081688..21a282e2f16 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -479,12 +479,11 @@ and enter its device name in the [audio_sink](configuration.md#audio_sink) field
> Gamepads are not currently supported.
### Windows
-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.
+Sunshine uses libvirtualhid for virtual gamepads on Windows. You must install the libvirtualhid Windows driver
+separately for full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox 360 and
+DualShock 4 gamepads when libvirtualhid is unavailable.
-
+After installing or updating virtual input drivers, it is recommended to restart your computer.
## Usage
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 4ae6839e3db..188743eea88 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -255,12 +255,9 @@ launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist
## Windows
### No gamepad detected
-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.
+Sunshine uses libvirtualhid for virtual gamepads on Windows. Install the libvirtualhid Windows driver separately
+for full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox 360 and DualShock 4
+gamepads when libvirtualhid is unavailable. If you use ViGEmBus fallback, you must use version 1.17 or newer.
After installation, it is recommended to restart your computer.
diff --git a/src/confighttp.cpp b/src/confighttp.cpp
index 8b9468e3482..dff4839257f 100644
--- a/src/confighttp.cpp
+++ b/src/confighttp.cpp
@@ -8,10 +8,13 @@
// standard includes
#include
+#include
#include
#include
#include
+#include
#include
+#include
// lib includes
#include
@@ -22,9 +25,10 @@
#include
#ifdef _WIN32
+ #include "platform/virtualhid_input.h"
#include "platform/windows/misc.h"
+ #include "platform/windows/utf_utils.h"
- #include
#include
#endif
@@ -102,6 +106,246 @@ namespace confighttp {
*/
constexpr auto CSRF_TOKEN_LIFETIME = std::chrono::hours(1); // Tokens valid for 1 hour
+ constexpr auto LIBVIRTUALHID_MINIMUM_VERSION = ""sv; ///< Minimum supported libvirtualhid driver version; empty means any version.
+ constexpr auto VIGEMBUS_MINIMUM_VERSION = "1.17.0.0"sv; ///< Minimum supported ViGEmBus fallback driver version.
+
+ /**
+ * @brief Parse one dotted driver-version component.
+ *
+ * @param part Version component text.
+ * @return Parsed component value, or empty when invalid.
+ */
+ std::optional parse_driver_version_part(std::string_view part) {
+ if (part.empty()) {
+ return std::nullopt;
+ }
+
+ unsigned int value = 0;
+ const auto *begin = part.data();
+ const auto *end = part.data() + part.size();
+ const auto [ptr, ec] = std::from_chars(begin, end, value);
+ if (ec != std::errc {} || ptr != end) {
+ return std::nullopt;
+ }
+
+ return value;
+ }
+
+ /**
+ * @brief Parse a dotted driver version into numeric components.
+ *
+ * @param version Driver version text.
+ * @return Parsed version parts, or empty when invalid.
+ */
+ std::optional> parse_driver_version(std::string_view version) {
+ if (version.empty()) {
+ return std::nullopt;
+ }
+
+ std::vector parts;
+ std::size_t start = 0;
+ while (start <= version.size()) {
+ const auto dot = version.find('.', start);
+ const auto length = dot == std::string_view::npos ? std::string_view::npos : dot - start;
+ const auto part = parse_driver_version_part(version.substr(start, length));
+ if (!part) {
+ return std::nullopt;
+ }
+
+ parts.push_back(*part);
+ if (dot == std::string_view::npos) {
+ break;
+ }
+ start = dot + 1;
+ }
+
+ return parts;
+ }
+
+ bool is_driver_version_supported(std::string_view version, std::string_view minimum_version) {
+ if (minimum_version.empty()) {
+ return true;
+ }
+
+ const auto version_parts = parse_driver_version(version);
+ const auto minimum_parts = parse_driver_version(minimum_version);
+ if (!version_parts || !minimum_parts) {
+ return false;
+ }
+
+ const auto part_count = std::max(version_parts->size(), minimum_parts->size());
+ for (std::size_t i = 0; i < part_count; ++i) {
+ const auto version_part = i < version_parts->size() ? (*version_parts)[i] : 0U;
+ const auto minimum_part = i < minimum_parts->size() ? (*minimum_parts)[i] : 0U;
+ if (version_part != minimum_part) {
+ return version_part > minimum_part;
+ }
+ }
+
+ return true;
+ }
+
+ nlohmann::json build_driver_status(bool installed, const std::string &version, std::string_view minimum_version) {
+ const auto minimum_version_text = std::string {minimum_version};
+
+ nlohmann::json output_tree;
+ output_tree["installed"] = installed;
+ output_tree["version"] = version;
+ output_tree["minimum_version"] = minimum_version_text;
+ output_tree["supported_versions"] = minimum_version.empty() ? "Any" : std::format(">= {}", minimum_version_text);
+ output_tree["version_compatible"] = installed && is_driver_version_supported(version, minimum_version);
+
+ return output_tree;
+ }
+
+#ifdef _WIN32
+ /**
+ * @brief RAII wrapper for a Windows registry key handle.
+ */
+ class registry_key_t {
+ public:
+ /**
+ * @brief Construct an empty registry key wrapper.
+ */
+ registry_key_t() = default;
+
+ /**
+ * @brief Copy construction is disabled because the wrapper owns a handle.
+ */
+ registry_key_t(const registry_key_t &) = delete;
+
+ /**
+ * @brief Copy assignment is disabled because the wrapper owns a handle.
+ *
+ * @return This registry key wrapper.
+ */
+ registry_key_t &operator=(const registry_key_t &) = delete;
+
+ /**
+ * @brief Close the owned registry key handle.
+ */
+ ~registry_key_t() {
+ close();
+ }
+
+ /**
+ * @brief Get the owned registry key handle.
+ *
+ * @return Registry key handle.
+ */
+ HKEY get() const {
+ return handle;
+ }
+
+ /**
+ * @brief Prepare the wrapper to receive a registry key handle.
+ *
+ * @return Address of the wrapped handle.
+ */
+ HKEY *put() {
+ close();
+ return &handle;
+ }
+
+ private:
+ /**
+ * @brief Close the owned registry key handle if one is open.
+ */
+ void close() {
+ if (handle) {
+ RegCloseKey(handle);
+ handle = nullptr;
+ }
+ }
+
+ HKEY handle = nullptr; ///< Owned Windows registry key handle.
+ };
+
+ /**
+ * @brief Read a string value from a Windows registry key.
+ *
+ * @param key Registry key to query.
+ * @param value_name Registry value name.
+ * @return Registry string value, or empty when unavailable.
+ */
+ std::optional read_registry_string_value(HKEY key, const wchar_t *value_name) {
+ DWORD value_type = 0;
+ DWORD value_size = 0;
+ if (RegGetValueW(key, nullptr, value_name, RRF_RT_REG_SZ, &value_type, nullptr, &value_size) != ERROR_SUCCESS ||
+ value_size == 0) {
+ return std::nullopt;
+ }
+
+ std::wstring value(value_size / sizeof(wchar_t), L'\0');
+ if (RegGetValueW(key, nullptr, value_name, RRF_RT_REG_SZ, &value_type, value.data(), &value_size) != ERROR_SUCCESS) {
+ return std::nullopt;
+ }
+
+ while (!value.empty() && value.back() == L'\0') {
+ value.pop_back();
+ }
+ return value;
+ }
+
+ /**
+ * @brief Read the installed libvirtualhid driver version from the Windows device registry.
+ *
+ * @return Driver version string, or empty when unavailable.
+ */
+ std::string read_libvirtualhid_driver_version() {
+ registry_key_t root_key;
+ if (RegOpenKeyExW(
+ HKEY_LOCAL_MACHINE,
+ L"SYSTEM\\CurrentControlSet\\Enum\\ROOT\\LIBVIRTUALHID",
+ 0,
+ KEY_READ,
+ root_key.put()
+ ) != ERROR_SUCCESS) {
+ return {};
+ }
+
+ for (DWORD index = 0;; ++index) {
+ wchar_t subkey_name[256] = {};
+ DWORD subkey_name_size = _countof(subkey_name);
+ const auto enum_status = RegEnumKeyExW(root_key.get(), index, subkey_name, &subkey_name_size, nullptr, nullptr, nullptr, nullptr);
+ if (enum_status == ERROR_NO_MORE_ITEMS) {
+ break;
+ }
+ if (enum_status != ERROR_SUCCESS) {
+ continue;
+ }
+
+ std::wstring device_key_path = L"SYSTEM\\CurrentControlSet\\Enum\\ROOT\\LIBVIRTUALHID\\";
+ device_key_path.append(subkey_name, subkey_name_size);
+
+ registry_key_t device_key;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, device_key_path.c_str(), 0, KEY_READ, device_key.put()) != ERROR_SUCCESS) {
+ continue;
+ }
+
+ const auto driver_key_suffix = read_registry_string_value(device_key.get(), L"Driver");
+ if (!driver_key_suffix) {
+ continue;
+ }
+
+ std::wstring driver_key_path = L"SYSTEM\\CurrentControlSet\\Control\\Class\\";
+ driver_key_path += *driver_key_suffix;
+
+ registry_key_t driver_key;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, driver_key_path.c_str(), 0, KEY_READ, driver_key.put()) != ERROR_SUCCESS) {
+ continue;
+ }
+
+ if (const auto version = read_registry_string_value(driver_key.get(), L"DriverVersion")) {
+ return utf_utils::to_utf8(*version);
+ }
+ }
+
+ return {};
+ }
+
+#endif
+
/**
* @brief Log the request details.
* @param request The HTTP request object.
@@ -1431,130 +1675,87 @@ namespace confighttp {
}
/**
- * @brief Get ViGEmBus driver version and installation status.
- * @param response The HTTP response object.
- * @param request The HTTP request object.
+ * @brief Build libvirtualhid driver version and installation status.
*
- * @api_examples{/api/vigembus/status| GET| null}
+ * @return libvirtualhid driver status JSON.
*/
- void getViGEmBusStatus(const resp_https_t &response, const req_https_t &request) {
- if (!authenticate(response, request)) {
- return;
+ nlohmann::json get_virtualhid_driver_status() {
+#ifdef _WIN32
+ const auto version_str = read_libvirtualhid_driver_version();
+ auto output_tree = build_driver_status(false, version_str, LIBVIRTUALHID_MINIMUM_VERSION);
+ bool requires_installed_driver = true;
+ std::string backend_name;
+ std::string error;
+
+ try {
+ const auto runtime = platf::virtualhid::create_runtime();
+ if (runtime) {
+ const auto &capabilities = runtime->capabilities();
+ backend_name = capabilities.backend_name;
+ requires_installed_driver = capabilities.requires_installed_driver;
+ output_tree = build_driver_status(capabilities.supports_gamepad, version_str, LIBVIRTUALHID_MINIMUM_VERSION);
+ }
+ } catch (const std::exception &e) {
+ error = e.what();
}
- print_req(request);
+ output_tree["backend_name"] = backend_name;
+ output_tree["requires_installed_driver"] = requires_installed_driver;
+ if (!error.empty()) {
+ output_tree["error"] = error;
+ }
+#else
+ auto output_tree = build_driver_status(false, "", LIBVIRTUALHID_MINIMUM_VERSION);
+ output_tree["error"] = "libvirtualhid driver status is only available on Windows";
+ output_tree["backend_name"] = "";
+ output_tree["requires_installed_driver"] = false;
+#endif
- nlohmann::json output_tree;
+ return output_tree;
+ }
+ /**
+ * @brief Build ViGEmBus fallback driver version and installation status.
+ *
+ * @return ViGEmBus fallback driver status JSON.
+ */
+ nlohmann::json get_vigembus_driver_status() {
#ifdef _WIN32
std::string version_str;
- bool installed = false;
- bool version_compatible = false;
// Check if ViGEmBus driver exists
- std::filesystem::path driver_path = std::filesystem::path(std::getenv("SystemRoot") ? std::getenv("SystemRoot") : "C:\\Windows") / "System32" / "drivers" / "ViGEmBus.sys";
-
- if (std::filesystem::exists(driver_path)) {
- installed = platf::getFileVersionInfo(driver_path, version_str);
- if (installed) {
- // Parse version string to check compatibility (>= 1.17.0.0)
- std::vector version_parts;
- std::stringstream ss(version_str);
- std::string part;
- while (std::getline(ss, part, '.')) {
- version_parts.push_back(part);
- }
-
- if (version_parts.size() >= 2) {
- int major = std::stoi(version_parts[0]);
- int minor = std::stoi(version_parts[1]);
- version_compatible = (major > 1) || (major == 1 && minor >= 17);
- }
- }
+ const std::filesystem::path driver_path = std::filesystem::path(std::getenv("SystemRoot") ? std::getenv("SystemRoot") : "C:\\Windows") / "System32" / "drivers" / "ViGEmBus.sys";
+ const auto installed = std::filesystem::exists(driver_path);
+ if (installed) {
+ platf::getFileVersionInfo(driver_path, version_str);
}
- output_tree["installed"] = installed;
- output_tree["version"] = version_str;
- output_tree["version_compatible"] = version_compatible;
- output_tree["packaged_version"] = VIGEMBUS_PACKAGED_VERSION;
+ auto output_tree = build_driver_status(installed, version_str, VIGEMBUS_MINIMUM_VERSION);
#else
+ auto output_tree = build_driver_status(false, "", VIGEMBUS_MINIMUM_VERSION);
output_tree["error"] = "ViGEmBus is only available on Windows";
- output_tree["installed"] = false;
- output_tree["version"] = "";
- output_tree["version_compatible"] = false;
- output_tree["packaged_version"] = "";
#endif
- send_response(response, output_tree);
+ return output_tree;
}
/**
- * @brief Install ViGEmBus driver with elevated permissions.
+ * @brief Get virtual input driver version and installation status.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
- * @api_examples{/api/vigembus/install| POST| null}
+ * @api_examples{/api/virtual-input/status| GET| null}
*/
- void installViGEmBus(const resp_https_t &response, const req_https_t &request) {
+ void getVirtualInputStatus(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
- std::string client_id = get_client_id(request);
- if (!validate_csrf_token(response, request, client_id)) {
- return;
- }
-
print_req(request);
nlohmann::json output_tree;
-
-#ifdef _WIN32
- // Get the path to the packaged ViGEmBus installer.
- const std::filesystem::path installer_path = platf::appdata().parent_path() / "third-party" / "vigembus_installer.exe";
-
- if (!std::filesystem::exists(installer_path)) {
- output_tree["status"] = false;
- output_tree["error"] = "ViGEmBus installer not found";
- send_response(response, output_tree);
- return;
- }
-
- // Run the installer with elevated permissions
- std::error_code ec;
- boost::filesystem::path working_dir = boost::filesystem::path(installer_path.string()).parent_path();
- boost::process::v1::environment env = boost::this_process::environment();
-
- // Run with elevated permissions, non-interactive
- const std::string install_cmd = std::format("{} /quiet", installer_path.string());
- auto child = platf::run_command(true, false, install_cmd, working_dir, env, nullptr, ec, nullptr);
-
- if (ec) {
- output_tree["status"] = false;
- output_tree["error"] = "Failed to start installer: " + ec.message();
- send_response(response, output_tree);
- return;
- }
-
- // Wait for the installer to complete
- child.wait(ec);
-
- if (ec) {
- output_tree["status"] = false;
- output_tree["error"] = "Installer failed: " + ec.message();
- } else {
- int exit_code = child.exit_code();
- output_tree["status"] = (exit_code == 0);
- output_tree["exit_code"] = exit_code;
- if (exit_code != 0) {
- output_tree["error"] = std::format("Installer exited with code {}", exit_code);
- }
- }
-#else
- output_tree["status"] = false;
- output_tree["error"] = "ViGEmBus installation is only available on Windows";
-#endif
-
+ output_tree["virtualhid"] = get_virtualhid_driver_status();
+ output_tree["vigembus"] = get_vigembus_driver_status();
send_response(response, output_tree);
}
@@ -1812,8 +2013,7 @@ namespace confighttp {
server.resource["^/api/logs$"]["GET"] = getLogs;
server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence;
server.resource["^/api/restart$"]["POST"] = restart;
- server.resource["^/api/vigembus/status$"]["GET"] = getViGEmBusStatus;
- server.resource["^/api/vigembus/install$"]["POST"] = installViGEmBus;
+ server.resource["^/api/virtual-input/status$"]["GET"] = getVirtualInputStatus;
// static/dynamic resources
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
diff --git a/src/confighttp.h b/src/confighttp.h
index 0806ad4afa0..78ab5c479c4 100644
--- a/src/confighttp.h
+++ b/src/confighttp.h
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
// lib includes
#include
@@ -75,6 +76,28 @@ namespace confighttp {
void getLocale(const resp_https_t &response, const req_https_t &request);
void getCSRFToken(const resp_https_t &response, const req_https_t &request);
+ /**
+ * @brief Check whether a detected driver version satisfies a minimum version.
+ *
+ * Empty minimum versions accept any detected version. Non-empty minimum versions
+ * require a fully numeric dotted version string.
+ *
+ * @param version Detected driver version.
+ * @param minimum_version Minimum supported driver version, or empty for any version.
+ * @return True when the driver version is supported.
+ */
+ bool is_driver_version_supported(std::string_view version, std::string_view minimum_version);
+
+ /**
+ * @brief Build a standard driver status response.
+ *
+ * @param installed Whether the driver was detected.
+ * @param version Detected driver version.
+ * @param minimum_version Minimum supported driver version, or empty for any version.
+ * @return Driver status JSON object.
+ */
+ nlohmann::json build_driver_status(bool installed, const std::string &version, std::string_view minimum_version);
+
// Browse helper functions (also exposed for unit testing)
/**
* @brief Checks whether a directory entry qualifies as an executable file.
diff --git a/src_assets/common/assets/web/index.html b/src_assets/common/assets/web/index.html
index 57ab0bc298e..b2cbc03e642 100644
--- a/src_assets/common/assets/web/index.html
+++ b/src_assets/common/assets/web/index.html
@@ -28,29 +28,39 @@
this.preReleaseVersion = new SunshineVersion((await fetch("https://api.github.com/repos/LizardByte/Sunshine/releases").then((r) => r.json())).find(release => release.prerelease), null);
console.log("Pre-Release Version: ", this.preReleaseVersion.version)
- // Fetch ViGEmBus status only on Windows when controller is enabled
+ // Fetch virtual input driver status only on Windows when controller is enabled
if (this.platform === 'windows' && this.controllerEnabled) {
try {
- this.vigembus = await fetch("./api/vigembus/status").then((r) => r.json());
+ const virtualInputStatus = await fetch("./api/virtual-input/status").then((r) => r.json());
+ this.virtualhid = virtualInputStatus.virtualhid;
+ this.vigembus = virtualInputStatus.vigembus;
} catch (e) {
- console.error("Failed to fetch ViGEmBus status:", e);
+ console.error("Failed to fetch virtual input driver status:", e);
}
}
} catch (e) {
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 e952c7e9743..81463444d3f 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -435,10 +435,12 @@
"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": "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",
+ "virtualhid_missing_vigembus_installed_desc": "ViGEmBus is installed, but without the libvirtualhid driver Sunshine can only offer limited fallback gamepad support.",
+ "virtualhid_missing_vigembus_installed_title": "Gamepad Support Is Limited",
+ "virtualhid_not_installed_desc": "Sunshine requires the libvirtualhid Windows driver for full virtual gamepad support. Install the libvirtualhid driver package, then restart Sunshine.",
+ "virtualhid_not_installed_title": "libvirtualhid Driver Not Installed",
+ "virtualhid_outdated_desc": "The installed libvirtualhid driver version (v{version}) is not supported. Supported versions: {supported_versions}.",
+ "virtualhid_outdated_title": "libvirtualhid Driver Outdated",
"welcome": "Hello, Sunshine!"
},
"navbar": {
@@ -515,6 +517,15 @@
"dd_reset_desc": "If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.",
"dd_reset_error": "Error while resetting persistence!",
"dd_reset_success": "Success resetting persistence!",
+ "driver_installed_version": "Installed Version",
+ "driver_name": "Driver",
+ "driver_not_installed": "Not installed",
+ "driver_status": "Status",
+ "driver_status_compatible": "Compatible",
+ "driver_status_not_installed": "Not installed",
+ "driver_status_unsupported": "Unsupported version",
+ "driver_supported_versions": "Supported Versions",
+ "driver_version_unknown": "Not reported",
"force_close": "Force Close",
"force_close_desc": "If Moonlight complains about an app currently running, force closing the app should fix the issue.",
"force_close_error": "Error while closing Application",
@@ -534,16 +545,10 @@
"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 fallback is installed and compatible.",
- "vigembus_current_version": "Current Version",
- "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 fallback is not installed."
+ "vigembus_driver": "ViGEmBus Fallback Driver",
+ "virtual_gamepad": "Virtual Gamepad",
+ "virtual_gamepad_desc": "Sunshine uses libvirtualhid for full virtual gamepad support on Windows. ViGEmBus is detected only as a limited fallback for Xbox 360 and DualShock 4 gamepads.",
+ "virtualhid_driver": "libvirtualhid Driver"
},
"featured": {
"categories": {
diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html
index 498267a0f6e..8b8eb58c9ea 100644
--- a/src_assets/common/assets/web/troubleshooting.html
+++ b/src_assets/common/assets/web/troubleshooting.html
@@ -9,43 +9,44 @@
.then((r) => {
this.platform = r.platform;
this.controllerEnabled = r.controller !== "disabled";
- // Fetch ViGEmBus status only on Windows when gamepad is enabled
+ // Fetch virtual input driver status only on Windows when gamepad is enabled
if (this.platform === 'windows' && this.controllerEnabled) {
- this.refreshViGEmBusStatus();
+ this.refreshVirtualInputStatus();
}
});
@@ -538,54 +542,51 @@
{{ $t('troubleshooting.logs') }}
}, 5000);
});
},
- refreshViGEmBusStatus() {
- fetch("/api/vigembus/status")
+ refreshVirtualInputStatus() {
+ fetch("/api/virtual-input/status")
.then((r) => r.json())
.then((r) => {
+ const virtualhid = r.virtualhid || {};
+ const vigembus = r.vigembus || {};
+ this.virtualhid = {
+ installed: virtualhid.installed || false,
+ version: virtualhid.version || '',
+ version_compatible: virtualhid.version_compatible || false,
+ minimum_version: virtualhid.minimum_version || '',
+ supported_versions: virtualhid.supported_versions || '',
+ };
this.vigembus = {
- installed: r.installed || false,
- version: r.version || '',
- version_compatible: r.version_compatible || false,
- packaged_version: r.packaged_version,
+ installed: vigembus.installed || false,
+ version: vigembus.version || '',
+ version_compatible: vigembus.version_compatible || false,
+ minimum_version: vigembus.minimum_version || '',
+ supported_versions: vigembus.supported_versions || '',
};
})
.catch((err) => {
- console.error("Failed to fetch ViGEmBus status:", err);
+ console.error("Failed to fetch virtual input driver status:", err);
});
},
- installViGEmBus() {
- this.vigemBusInstallPressed = true;
- this.vigemBusInstallStatus = null;
- this.vigemBusInstallError = null;
- apiFetch("/api/vigembus/install", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- }
- })
- .then((r) => r.json())
- .then((r) => {
- this.vigemBusInstallPressed = false;
- this.vigemBusInstallStatus = r.status;
- if (!r.status && r.error) {
- this.vigemBusInstallError = r.error;
- }
- setTimeout(() => {
- this.vigemBusInstallStatus = null;
- this.vigemBusInstallError = null;
- }, 10000);
- // Refresh status after installation attempt
- this.refreshViGEmBusStatus();
- })
- .catch((err) => {
- this.vigemBusInstallPressed = false;
- this.vigemBusInstallStatus = false;
- this.vigemBusInstallError = err.message;
- setTimeout(() => {
- this.vigemBusInstallStatus = null;
- this.vigemBusInstallError = null;
- }, 10000);
- });
+ driverVersion(driver) {
+ if (!driver.installed) {
+ return this.$t('troubleshooting.driver_not_installed');
+ }
+ return driver.version || this.$t('troubleshooting.driver_version_unknown');
+ },
+ driverStatusClass(driver) {
+ if (!driver.installed) {
+ return 'badge text-bg-secondary';
+ }
+ return driver.version_compatible ? 'badge text-bg-success' : 'badge text-bg-danger';
+ },
+ driverStatusText(driver) {
+ if (!driver.installed) {
+ return this.$t('troubleshooting.driver_status_not_installed');
+ }
+ if (driver.version_compatible) {
+ return this.$t('troubleshooting.driver_status_compatible');
+ }
+ return this.$t('troubleshooting.driver_status_unsupported');
},
navigateToLog(direction) {
const indices = this.errorWarningEntries;
diff --git a/tests/unit/test_confighttp.cpp b/tests/unit/test_confighttp.cpp
index a633ae3b5a3..dec56702f00 100644
--- a/tests/unit/test_confighttp.cpp
+++ b/tests/unit/test_confighttp.cpp
@@ -1188,6 +1188,56 @@ TEST_F(BrowseDirectoryTest, BrowseUnixRootParentEqualsSelf) {
}
#endif
+// ============================================================
+// Direct unit tests for virtual input driver status helpers
+// ============================================================
+
+// Test: empty minimum driver versions accept any detected version
+TEST(ConfigHttpDriverStatusTest, IsDriverVersionSupported_EmptyMinimum_ReturnsTrue) {
+ ASSERT_TRUE(confighttp::is_driver_version_supported("", ""));
+ ASSERT_TRUE(confighttp::is_driver_version_supported("1.0.0.0", ""));
+}
+
+// Test: equal and newer numeric driver versions are supported
+TEST(ConfigHttpDriverStatusTest, IsDriverVersionSupported_EqualOrNewerVersion_ReturnsTrue) {
+ ASSERT_TRUE(confighttp::is_driver_version_supported("1.17.0.0", "1.17.0.0"));
+ ASSERT_TRUE(confighttp::is_driver_version_supported("1.18.0.0", "1.17.0.0"));
+ ASSERT_TRUE(confighttp::is_driver_version_supported("2.0", "1.17.0.0"));
+}
+
+// Test: older numeric driver versions are unsupported
+TEST(ConfigHttpDriverStatusTest, IsDriverVersionSupported_OlderVersion_ReturnsFalse) {
+ ASSERT_FALSE(confighttp::is_driver_version_supported("1.16.9.9", "1.17.0.0"));
+ ASSERT_FALSE(confighttp::is_driver_version_supported("1.16", "1.17.0.0"));
+}
+
+// Test: invalid driver versions are unsupported when a minimum is required
+TEST(ConfigHttpDriverStatusTest, IsDriverVersionSupported_InvalidVersion_ReturnsFalse) {
+ ASSERT_FALSE(confighttp::is_driver_version_supported("", "1.17.0.0"));
+ ASSERT_FALSE(confighttp::is_driver_version_supported("1.17.beta", "1.17.0.0"));
+ ASSERT_FALSE(confighttp::is_driver_version_supported("1.17.", "1.17.0.0"));
+}
+
+// Test: driver status JSON includes compatibility and supported version metadata
+TEST(ConfigHttpDriverStatusTest, BuildDriverStatus_IncludesExpectedFields) {
+ const auto status = confighttp::build_driver_status(true, "1.17.0.0", "1.17.0.0");
+
+ ASSERT_TRUE(status["installed"].get());
+ ASSERT_EQ(status["version"].get(), "1.17.0.0");
+ ASSERT_EQ(status["minimum_version"].get(), "1.17.0.0");
+ ASSERT_EQ(status["supported_versions"].get(), ">= 1.17.0.0");
+ ASSERT_TRUE(status["version_compatible"].get());
+}
+
+// Test: missing drivers are not compatible even when any version would be accepted
+TEST(ConfigHttpDriverStatusTest, BuildDriverStatus_NotInstalledIsNotCompatible) {
+ const auto status = confighttp::build_driver_status(false, "", "");
+
+ ASSERT_FALSE(status["installed"].get());
+ ASSERT_EQ(status["supported_versions"].get(), "Any");
+ ASSERT_FALSE(status["version_compatible"].get());
+}
+
// ============================================================
// Direct unit tests for browseDirectory helper functions
// ============================================================
From 7366057d3ab3c10dfac7e7a84d2c1f2111704b1d Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Sun, 5 Jul 2026 11:20:42 -0400
Subject: [PATCH 04/10] Update libvirtualhid submodule
---
third-party/libvirtualhid | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid
index 9f7c24ec90d..a9b06f19f2a 160000
--- a/third-party/libvirtualhid
+++ b/third-party/libvirtualhid
@@ -1 +1 @@
-Subproject commit 9f7c24ec90d142e613361d81124e4f77446a910a
+Subproject commit a9b06f19f2adf91650b6a3d2e4c2c983c6ae6279
From 722dc5aeef4022a6e18c03f80511b5289fa140b4 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Tue, 7 Jul 2026 16:20:52 -0400
Subject: [PATCH 05/10] Migrate Windows input to libvirtualhid
Replace Windows-native synthetic pointer/keyboard input implementations with libvirtualhid equivalents:
- Remove keylayout.h and its VK-to-scancode mapping (now handled by libvirtualhid)
- Remove Windows-specific touch/pen device management (HSYNTHETICPOINTERDEVICE, repeat tasks, slot compaction)
- Pass touch_port and flags through to libvirtualhid for keyboard, touch, and pen events
- Add pointer_viewport() and keyboard_event() helpers in virtualhid_input.cpp
- Differentiate LI_TOUCH_EVENT_UP, CANCEL, and HOVER_LEAVE handling for touch contacts
- Add PointerTransition support for pen events
- Update capability detection to use libvirtualhid runtime
- Update libvirtualhid submodule to c1cc7e0
---
src/platform/linux/input/virtualhid.cpp | 12 +-
src/platform/virtualhid_input.cpp | 72 ++-
src/platform/virtualhid_input.h | 9 +-
src/platform/windows/input.cpp | 700 +-----------------------
src/platform/windows/keylayout.h | 275 ----------
third-party/libvirtualhid | 2 +-
6 files changed, 96 insertions(+), 974 deletions(-)
delete mode 100644 src/platform/windows/keylayout.h
diff --git a/src/platform/linux/input/virtualhid.cpp b/src/platform/linux/input/virtualhid.cpp
index cda9d4ed836..ca85bfd8e63 100644
--- a/src/platform/linux/input/virtualhid.cpp
+++ b/src/platform/linux/input/virtualhid.cpp
@@ -79,20 +79,20 @@ namespace platf {
* @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 keyboard_update(input_t &input, std::uint16_t modcode, bool release, std::uint8_t flags) {
+ virtualhid::keyboard_update(((input_raw_t *) input.get())->virtualhid, modcode, release, flags);
}
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 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_port, 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);
+ 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, touch_port, pen);
}
int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp
index 88b361d54b3..baded4737d2 100644
--- a/src/platform/virtualhid_input.cpp
+++ b/src/platform/virtualhid_input.cpp
@@ -286,6 +286,30 @@ namespace platf::virtualhid {
return adjusted;
}
+ lvh::PointerViewport pointer_viewport(const touch_port_t &touch_port) {
+ return {
+ .offset_x = touch_port.offset_x,
+ .offset_y = touch_port.offset_y,
+ .width = touch_port.width,
+ .height = touch_port.height,
+ };
+ }
+
+ lvh::KeyboardEvent keyboard_event(std::uint16_t modcode, bool release, std::uint8_t flags) {
+ lvh::KeyboardEvent event {
+ .key_code = modcode,
+ .pressed = !release,
+ };
+
+#ifdef _WIN32
+ event.uses_normalized_key_code = !(flags & SS_KBE_FLAG_NON_NORMALIZED);
+ event.prefer_native_scan_code = config::input.always_send_scancodes;
+#else
+ (void) flags;
+#endif
+ return event;
+ }
+
lvh::PenToolType pen_tool(std::uint8_t tool) {
switch (tool) {
case LI_TOOL_TYPE_PEN:
@@ -349,13 +373,13 @@ namespace platf::virtualhid {
}
}
- void release_all_touches(client_context_t &context) {
+ void cancel_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));
+ log_failure("cancel libvirtualhid touch contact"sv, context.touch->cancel_contact(id));
}
context.active_touches.clear();
}
@@ -667,9 +691,9 @@ namespace platf::virtualhid {
}
}
- void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release) {
+ void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, std::uint8_t flags) {
if (context.keyboard) {
- log_failure("submit libvirtualhid keyboard input"sv, context.keyboard->submit({.key_code = modcode, .pressed = !release}));
+ log_failure("submit libvirtualhid keyboard input"sv, context.keyboard->submit(keyboard_event(modcode, release, flags)));
}
}
@@ -679,19 +703,25 @@ namespace platf::virtualhid {
}
}
- void touch_update(client_context_t &context, const touch_input_t &touch) {
+ void touch_update(client_context_t &context, const touch_port_t &touch_port, const touch_input_t &touch) {
if (!context.touch) {
return;
}
switch (touch.eventType) {
case LI_TOUCH_EVENT_CANCEL_ALL:
- release_all_touches(context);
+ cancel_all_touches(context);
return;
case LI_TOUCH_EVENT_UP:
+ 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_CANCEL:
+ log_failure("cancel libvirtualhid touch contact"sv, context.touch->cancel_contact(static_cast(touch.pointerId)));
+ context.active_touches.erase(static_cast(touch.pointerId));
+ return;
case LI_TOUCH_EVENT_HOVER_LEAVE:
- log_failure("release libvirtualhid touch contact"sv, context.touch->release_contact(static_cast(touch.pointerId)));
+ log_failure("leave libvirtualhid touch contact"sv, context.touch->leave_contact(static_cast(touch.pointerId)));
context.active_touches.erase(static_cast(touch.pointerId));
return;
case LI_TOUCH_EVENT_HOVER:
@@ -704,6 +734,10 @@ namespace platf::virtualhid {
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);
+ contact.touching = touch.eventType != LI_TOUCH_EVENT_HOVER;
+ contact.viewport = pointer_viewport(touch_port);
+ contact.contact_major_axis = touch.contactAreaMajor;
+ contact.contact_minor_axis = touch.contactAreaMinor;
log_failure("submit libvirtualhid touch contact"sv, context.touch->place_contact(contact));
context.active_touches.insert(contact.id);
return;
@@ -713,7 +747,7 @@ namespace platf::virtualhid {
}
}
- void pen_update(client_context_t &context, const pen_input_t &pen) {
+ void pen_update(client_context_t &context, const touch_port_t &touch_port, const pen_input_t &pen) {
if (!context.pen) {
return;
}
@@ -742,7 +776,22 @@ namespace platf::virtualhid {
log_failure("release libvirtualhid pen button"sv, context.pen->button(button, false));
}
context.pressed_pen_buttons.clear();
- return;
+ }
+
+ auto transition = lvh::PointerTransition::update;
+ switch (pen.eventType) {
+ case LI_TOUCH_EVENT_CANCEL:
+ case LI_TOUCH_EVENT_CANCEL_ALL:
+ transition = lvh::PointerTransition::cancel;
+ break;
+ case LI_TOUCH_EVENT_UP:
+ transition = lvh::PointerTransition::release;
+ break;
+ case LI_TOUCH_EVENT_HOVER_LEAVE:
+ transition = lvh::PointerTransition::leave;
+ break;
+ default:
+ break;
}
auto rotation = pen.rotation;
@@ -762,7 +811,8 @@ namespace platf::virtualhid {
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;
+ const auto is_touching = transition == lvh::PointerTransition::update &&
+ (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);
@@ -771,6 +821,8 @@ namespace platf::virtualhid {
state.distance = is_touching ? -1.0F : std::clamp(pen.pressureOrDistance, 0.0F, 1.0F);
state.tilt_x = tilt_x;
state.tilt_y = tilt_y;
+ state.transition = transition;
+ state.viewport = pointer_viewport(touch_port);
log_failure("submit libvirtualhid pen state"sv, context.pen->place_tool(state));
}
diff --git a/src/platform/virtualhid_input.h b/src/platform/virtualhid_input.h
index ae213d9b7bd..a94543d7a4a 100644
--- a/src/platform/virtualhid_input.h
+++ b/src/platform/virtualhid_input.h
@@ -186,8 +186,9 @@ namespace platf::virtualhid {
* @param context Input context.
* @param modcode Portable key code.
* @param release Whether the key was released.
+ * @param flags Bit flags that modify the requested operation.
*/
- void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release);
+ void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, std::uint8_t flags);
/**
* @brief Submit UTF-8 text input.
@@ -202,17 +203,19 @@ namespace platf::virtualhid {
* @brief Submit a touchscreen event.
*
* @param context Client context.
+ * @param touch_port Touch coordinate bounds used for scaling.
* @param touch Touch event.
*/
- void touch_update(client_context_t &context, const touch_input_t &touch);
+ void touch_update(client_context_t &context, const touch_port_t &touch_port, const touch_input_t &touch);
/**
* @brief Submit a pen event.
*
* @param context Client context.
+ * @param touch_port Touch coordinate bounds used for scaling.
* @param pen Pen event.
*/
- void pen_update(client_context_t &context, const pen_input_t &pen);
+ void pen_update(client_context_t &context, const touch_port_t &touch_port, const pen_input_t &pen);
/**
* @brief Return whether the configured gamepad profile can expose touchpad input.
diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp
index 6e7bcb48414..8762521e1ae 100644
--- a/src/platform/windows/input.cpp
+++ b/src/platform/windows/input.cpp
@@ -17,7 +17,6 @@
#include
// standard includes
-#include
#include
#include
@@ -25,7 +24,6 @@
#include
// local includes
-#include "keylayout.h"
#include "misc.h"
#include "src/config.h"
#include "src/globals.h"
@@ -36,18 +34,6 @@
namespace platf {
using namespace std::literals;
- thread_local HDESK _lastKnownInputDesktop = nullptr; ///< Last known input desktop.
-
- /**
- * @brief Target touch port.
- */
- constexpr touch_port_t target_touch_port {
- 0,
- 0,
- 65535,
- 65535
- };
-
/**
* @brief ViGEm client pointer released with `vigem_free`.
*/
@@ -512,10 +498,6 @@ namespace platf {
virtualhid::input_context_t virtualhid; ///< libvirtualhid input context.
vigem_t *vigem; ///< Vigem.
-
- decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; ///< Fn create synthetic pointer device.
- decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; ///< Fn inject synthetic pointer input.
- decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice; ///< Fn destroy synthetic pointer device.
};
input_t input() {
@@ -531,11 +513,6 @@ namespace platf {
}
}
- // Get pointers to virtual touch/pen input functions (Win10 1809+)
- raw.fnCreateSyntheticPointerDevice = (decltype(CreateSyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice");
- raw.fnInjectSyntheticPointerInput = (decltype(InjectSyntheticPointerInput) *) GetProcAddress(GetModuleHandleA("user32.dll"), "InjectSyntheticPointerInput");
- raw.fnDestroySyntheticPointerDevice = (decltype(DestroySyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "DestroySyntheticPointerDevice");
-
return result;
}
@@ -571,80 +548,12 @@ namespace platf {
return true;
}
- /**
- * @brief Calls SendInput() and switches input desktops if required.
- * @param i The `INPUT` struct to send.
- */
- void send_input(INPUT &i) {
- retry:
- auto send = SendInput(1, &i, sizeof(INPUT));
- if (send != 1) {
- auto hDesk = syncThreadDesktop();
- if (_lastKnownInputDesktop != hDesk) {
- _lastKnownInputDesktop = hDesk;
- goto retry;
- }
- BOOST_LOG(error) << "Couldn't send input"sv;
- }
- }
-
- /**
- * @brief Calls InjectSyntheticPointerInput() and switches input desktops if required.
- * @details Must only be called if InjectSyntheticPointerInput() is available.
- * @param input The global input context.
- * @param device The synthetic pointer device handle.
- * @param pointerInfo An array of `POINTER_TYPE_INFO` structs.
- * @param count The number of elements in `pointerInfo`.
- * @return true if input was successfully injected.
- */
- bool inject_synthetic_pointer_input(input_raw_t *input, HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointerInfo, UINT32 count) {
- retry:
- if (!input->fnInjectSyntheticPointerInput(device, pointerInfo, count)) {
- auto hDesk = syncThreadDesktop();
- if (_lastKnownInputDesktop != hDesk) {
- _lastKnownInputDesktop = hDesk;
- goto retry;
- }
- return false;
- }
- return true;
- }
-
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags =
- MOUSEEVENTF_MOVE |
- MOUSEEVENTF_ABSOLUTE |
-
- // MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop
- MOUSEEVENTF_VIRTUALDESK;
-
- // Note: x and y already include the display offset (offset_x/offset_y) from client_to_touchport(),
- // so we must not add offset_x/offset_y again here to avoid double-offsetting on multi-monitor setups.
- auto scaled_x = std::lround(x * ((float) target_touch_port.width / (float) touch_port.width));
- auto scaled_y = std::lround(y * ((float) target_touch_port.height / (float) touch_port.height));
-
- mi.dx = scaled_x;
- mi.dy = scaled_y;
-
- send_input(i);
+ virtualhid::abs_mouse(((input_raw_t *) input.get())->virtualhid, touch_port, x, y);
}
void move_mouse(input_t &input, int deltaX, int deltaY) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags = MOUSEEVENTF_MOVE;
- mi.dx = deltaX;
- mi.dy = deltaY;
-
- send_input(i);
+ virtualhid::move_mouse(((input_raw_t *) input.get())->virtualhid, deltaX, deltaY);
}
util::point_t get_mouse_loc(input_t &input) {
@@ -662,105 +571,19 @@ namespace platf {
}
void button_mouse(input_t &input, int button, bool release) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- if (button == 1) {
- mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN;
- } else if (button == 2) {
- mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN;
- } else if (button == 3) {
- mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN;
- } else if (button == 4) {
- mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
- mi.mouseData = XBUTTON1;
- } else {
- mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
- mi.mouseData = XBUTTON2;
- }
-
- send_input(i);
+ virtualhid::button_mouse(((input_raw_t *) input.get())->virtualhid, button, release);
}
void scroll(input_t &input, int distance) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags = MOUSEEVENTF_WHEEL;
- mi.mouseData = distance;
-
- send_input(i);
+ virtualhid::scroll(((input_raw_t *) input.get())->virtualhid, distance);
}
void hscroll(input_t &input, int distance) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags = MOUSEEVENTF_HWHEEL;
- mi.mouseData = distance;
-
- send_input(i);
+ virtualhid::hscroll(((input_raw_t *) input.get())->virtualhid, distance);
}
void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
- INPUT i {};
- i.type = INPUT_KEYBOARD;
- auto &ki = i.ki;
-
- // If the client did not normalize this VK code to a US English layout, we can't accurately convert it to a scancode.
- // If we're set to always send scancodes, we will use the current keyboard layout to convert to a scancode. This will
- // assume the client and host have the same keyboard layout, but it's probably better than always using US English.
- if (!(flags & SS_KBE_FLAG_NON_NORMALIZED)) {
- // Mask off the extended key byte
- ki.wScan = VK_TO_SCANCODE_MAP[modcode & 0xFF];
- } else if (config::input.always_send_scancodes && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {
- // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/
- ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
- }
-
- // If we can map this to a scancode, send it as a scancode for maximum game compatibility.
- if (ki.wScan) {
- ki.dwFlags = KEYEVENTF_SCANCODE;
- } else {
- // If there is no scancode mapping or it's non-normalized, send it as a regular VK event.
- ki.wVk = modcode;
- }
-
- // https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags
- switch (modcode) {
- case VK_LWIN:
- case VK_RWIN:
- case VK_RMENU:
- case VK_RCONTROL:
- case VK_INSERT:
- case VK_DELETE:
- case VK_HOME:
- case VK_END:
- case VK_PRIOR:
- case VK_NEXT:
- case VK_UP:
- case VK_DOWN:
- case VK_LEFT:
- case VK_RIGHT:
- case VK_DIVIDE:
- case VK_APPS:
- ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
- break;
- default:
- break;
- }
-
- if (release) {
- ki.dwFlags |= KEYEVENTF_KEYUP;
- }
-
- send_input(i);
+ virtualhid::keyboard_update(((input_raw_t *) input.get())->virtualhid, modcode, release, flags);
}
/**
@@ -772,41 +595,10 @@ namespace platf {
*
* @param input Platform input backend that receives the event.
*/
- client_input_raw_t(input_t &input) {
- global = (input_raw_t *) input.get();
- }
-
- ~client_input_raw_t() override {
- if (penRepeatTask) {
- task_pool.cancel(penRepeatTask);
- }
- if (touchRepeatTask) {
- task_pool.cancel(touchRepeatTask);
- }
-
- if (pen) {
- global->fnDestroySyntheticPointerDevice(pen);
- }
- if (touch) {
- global->fnDestroySyntheticPointerDevice(touch);
- }
- }
-
- input_raw_t *global; ///< Global input context shared by this client context.
+ explicit client_input_raw_t(input_t &input):
+ virtualhid {((input_raw_t *) input.get())->virtualhid} {}
- // 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.
-
- HSYNTHETICPOINTERDEVICE pen {}; ///< Windows synthetic pointer device used for pen events.
- POINTER_TYPE_INFO penInfo {}; ///< Pen info.
- thread_pool_util::ThreadPool::task_id_t penRepeatTask {}; ///< Pen repeat task.
-
- HSYNTHETICPOINTERDEVICE touch {}; ///< Windows synthetic pointer device used for touch events.
- POINTER_TYPE_INFO touchInfo[10] {}; ///< Touch info.
- UINT32 activeTouchSlots {}; ///< Active touch slots.
- thread_pool_util::ThreadPool::task_id_t touchRepeatTask {}; ///< Touch repeat task.
+ virtualhid::client_context_t virtualhid; ///< libvirtualhid client context.
};
/**
@@ -818,469 +610,16 @@ namespace platf {
return std::make_unique(input);
}
- /**
- * @brief Compacts the touch slots into a contiguous block and updates the active count.
- * @details Since this swaps entries around, all slot pointers/references are invalid after compaction.
- * @param raw The client-specific input context.
- */
- void perform_touch_compaction(client_input_raw_t *raw) {
- // Windows requires all active touches be contiguous when fed into InjectSyntheticPointerInput().
- UINT32 i;
- for (i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {
- // This is an empty slot. Look for a later entry to move into this slot.
- for (UINT32 j = i + 1; j < ARRAYSIZE(raw->touchInfo); j++) {
- if (raw->touchInfo[j].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- std::swap(raw->touchInfo[i], raw->touchInfo[j]);
- break;
- }
- }
-
- // If we didn't find anything, we've reached the end of active slots.
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {
- break;
- }
- }
- }
-
- // Update the number of active touch slots
- raw->activeTouchSlots = i;
- }
-
- /**
- * @brief Gets a pointer slot by client-relative pointer ID, claiming a new one if necessary.
- * @param raw The raw client-specific input context.
- * @param pointerId The client's pointer ID.
- * @param eventType The LI_TOUCH_EVENT value from the client.
- * @return A pointer to the slot entry.
- */
- POINTER_TYPE_INFO *pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) {
- // Compact active touches into a single contiguous block
- perform_touch_compaction(raw);
-
- // Try to find a matching pointer ID
- for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerId == pointerId &&
- raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- if (eventType == LI_TOUCH_EVENT_DOWN && (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT)) {
- BOOST_LOG(warning) << "Pointer "sv << pointerId << " already down. Did the client drop an up/cancel event?"sv;
- }
-
- return &raw->touchInfo[i];
- }
- }
-
- if (eventType != LI_TOUCH_EVENT_HOVER && eventType != LI_TOUCH_EVENT_DOWN) {
- BOOST_LOG(warning) << "Unexpected new pointer "sv << pointerId << " for event "sv << (uint32_t) eventType << ". Did the client drop a down/hover event?"sv;
- }
-
- // If there was none, grab an unused entry and increment the active slot count
- for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {
- raw->touchInfo[i].touchInfo.pointerInfo.pointerId = pointerId;
- raw->activeTouchSlots = i + 1;
- return &raw->touchInfo[i];
- }
- }
-
- return nullptr;
- }
-
- /**
- * @brief Populate common `POINTER_INFO` members shared between pen and touch events.
- * @param pointerInfo The pointer info to populate.
- * @param touchPort The current viewport for translating to screen coordinates.
- * @param eventType The type of touch/pen event.
- * @param x The normalized 0.0-1.0 X coordinate.
- * @param y The normalized 0.0-1.0 Y coordinate.
- */
- void populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) {
- switch (eventType) {
- case LI_TOUCH_EVENT_HOVER:
- pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT;
- pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
- case LI_TOUCH_EVENT_DOWN:
- pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
- case LI_TOUCH_EVENT_UP:
- // We expect to get another LI_TOUCH_EVENT_HOVER if the pointer remains in range
- pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
- pointerInfo.pointerFlags |= POINTER_FLAG_UP;
- break;
- case LI_TOUCH_EVENT_MOVE:
- pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
- case LI_TOUCH_EVENT_CANCEL:
- case LI_TOUCH_EVENT_CANCEL_ALL:
- // If we were in contact with the touch surface at the time of the cancellation,
- // we'll set POINTER_FLAG_UP, otherwise set POINTER_FLAG_UPDATE.
- if (pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) {
- pointerInfo.pointerFlags |= POINTER_FLAG_UP;
- } else {
- pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
- }
- pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
- pointerInfo.pointerFlags |= POINTER_FLAG_CANCELED;
- break;
- case LI_TOUCH_EVENT_HOVER_LEAVE:
- pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
- pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
- break;
- case LI_TOUCH_EVENT_BUTTON_ONLY:
- // On Windows, we can only pass buttons if we have an active pointer
- if (pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
- }
- break;
- default:
- BOOST_LOG(warning) << "Unknown touch event: "sv << (uint32_t) eventType;
- break;
- }
- }
-
- // Active pointer interactions sent via InjectSyntheticPointerInput() seem to be automatically
- // cancelled by Windows if not repeated/updated within about a second. To avoid this, refresh
- // the injected input periodically.
- constexpr auto ISPI_REPEAT_INTERVAL = 50ms; ///< Protocol or platform constant for ispi repeat interval.
-
- /**
- * @brief Repeats the current touch state to avoid the interactions timing out.
- * @param raw The raw client-specific input context.
- */
- void repeat_touch(client_input_raw_t *raw) {
- if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to refresh virtual touch input: "sv << err;
- }
-
- raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
-
- /**
- * @brief Repeats the current pen state to avoid the interactions timing out.
- * @param raw The raw client-specific input context.
- */
- void repeat_pen(client_input_raw_t *raw) {
- if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to refresh virtual pen input: "sv << err;
- }
-
- raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
-
- /**
- * @brief Cancels all active touches.
- * @param raw The raw client-specific input context.
- */
- void cancel_all_active_touches(client_input_raw_t *raw) {
- // Cancel touch repeat callbacks
- if (raw->touchRepeatTask) {
- task_pool.cancel(raw->touchRepeatTask);
- raw->touchRepeatTask = nullptr;
- }
-
- // Compact touches to update activeTouchSlots
- perform_touch_compaction(raw);
-
- // If we have active slots, cancel them all
- if (raw->activeTouchSlots > 0) {
- for (UINT32 i = 0; i < raw->activeTouchSlots; i++) {
- populate_common_pointer_info(raw->touchInfo[i].touchInfo.pointerInfo, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f);
- raw->touchInfo[i].touchInfo.touchMask = TOUCH_MASK_NONE;
- }
- if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to cancel all virtual touch input: "sv << err;
- }
- }
-
- // Zero all touch state
- std::memset(raw->touchInfo, 0, sizeof(raw->touchInfo));
- raw->activeTouchSlots = 0;
- }
-
- // These are edge-triggered pointer state flags that should always be cleared next frame
- constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; ///< Protocol or platform constant for edge triggered pointer flags.
-
- /**
- * @brief Sends a touch event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param touch The touch event.
- */
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;
-
- // Bail if we're not running on an OS that supports virtual touch input
- if (!raw->global->fnCreateSyntheticPointerDevice ||
- !raw->global->fnInjectSyntheticPointerInput ||
- !raw->global->fnDestroySyntheticPointerDevice) {
- BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv;
- return;
- }
-
- // If there's not already a virtual touch device, create one now
- if (!raw->touch) {
- if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) {
- BOOST_LOG(info) << "Creating virtual touch input device"sv;
- raw->touch = raw->global->fnCreateSyntheticPointerDevice(PT_TOUCH, ARRAYSIZE(raw->touchInfo), POINTER_FEEDBACK_DEFAULT);
- if (!raw->touch) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to create virtual touch device: "sv << err;
- return;
- }
- } else {
- // No need to cancel anything if we had no touch input device
- return;
- }
- }
-
- // Cancel touch repeat callbacks
- if (raw->touchRepeatTask) {
- task_pool.cancel(raw->touchRepeatTask);
- raw->touchRepeatTask = nullptr;
- }
-
- // If this is a special request to cancel all touches, do that and return
- if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {
- cancel_all_active_touches(raw);
- return;
- }
-
- // Find or allocate an entry for this touch pointer ID
- auto pointer = pointer_by_id(raw, touch.pointerId, touch.eventType);
- if (!pointer) {
- BOOST_LOG(error) << "No unused pointer entries! Cancelling all active touches!"sv;
- cancel_all_active_touches(raw);
- pointer = pointer_by_id(raw, touch.pointerId, touch.eventType);
- }
-
- pointer->type = PT_TOUCH;
-
- auto &touchInfo = pointer->touchInfo;
- touchInfo.pointerInfo.pointerType = PT_TOUCH;
-
- // Populate shared pointer info fields
- populate_common_pointer_info(touchInfo.pointerInfo, touch_port, touch.eventType, touch.x, touch.y);
-
- touchInfo.touchMask = TOUCH_MASK_NONE;
-
- // Pressure and contact area only apply to in-contact pointers.
- //
- // The clients also pass distance and tool size for hovers, but Windows doesn't
- // provide APIs to receive that data.
- if (touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) {
- if (touch.pressureOrDistance != 0.0f) {
- touchInfo.touchMask |= TOUCH_MASK_PRESSURE;
-
- // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses
- touchInfo.pressure = (UINT32) (touch.pressureOrDistance * 1024);
- } else {
- // The default touch pressure is 512
- touchInfo.pressure = 512;
- }
-
- if (touch.contactAreaMajor != 0.0f && touch.contactAreaMinor != 0.0f) {
- // For the purposes of contact area calculation, we will assume the touches
- // are at a 45 degree angle if rotation is unknown. This will scale the major
- // axis value by width and height equally.
- float rotationAngleDegs = touch.rotation == LI_ROT_UNKNOWN ? 45 : touch.rotation;
-
- float majorAxisAngle = rotationAngleDegs * (M_PI / 180);
- float minorAxisAngle = majorAxisAngle + (M_PI / 2);
-
- // Estimate the contact rectangle
- float contactWidth = (std::cos(majorAxisAngle) * touch.contactAreaMajor) + (std::cos(minorAxisAngle) * touch.contactAreaMinor);
- float contactHeight = (std::sin(majorAxisAngle) * touch.contactAreaMajor) + (std::sin(minorAxisAngle) * touch.contactAreaMinor);
-
- // Convert into screen coordinates centered at the touch location and constrained by screen dimensions
- touchInfo.rcContact.left = std::max(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2));
- touchInfo.rcContact.right = std::min(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2));
- touchInfo.rcContact.top = std::max(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2));
- touchInfo.rcContact.bottom = std::min(touch_port.offset_y + touch_port.height, touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2));
-
- touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA;
- }
- } else {
- touchInfo.pressure = 0;
- touchInfo.rcContact = {};
- }
-
- if (touch.rotation != LI_ROT_UNKNOWN) {
- touchInfo.touchMask |= TOUCH_MASK_ORIENTATION;
- touchInfo.orientation = touch.rotation;
- } else {
- touchInfo.orientation = 0;
- }
-
- if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to inject virtual touch input: "sv << err;
- return;
- }
-
- // Clear pointer flags that should only remain set for one frame
- touchInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS;
-
- // If we still have an active touch, refresh the touch state periodically
- if (raw->activeTouchSlots > 1 || touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
+ virtualhid::touch_update(((client_input_raw_t *) input)->virtualhid, touch_port, touch);
}
- /**
- * @brief Sends a pen event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param pen The pen event.
- */
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;
-
- // Bail if we're not running on an OS that supports virtual pen input
- if (!raw->global->fnCreateSyntheticPointerDevice ||
- !raw->global->fnInjectSyntheticPointerInput ||
- !raw->global->fnDestroySyntheticPointerDevice) {
- BOOST_LOG(warning) << "Pen input requires Windows 10 1809 or later"sv;
- return;
- }
-
- // If there's not already a virtual pen device, create one now
- if (!raw->pen) {
- if (pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL) {
- BOOST_LOG(info) << "Creating virtual pen input device"sv;
- raw->pen = raw->global->fnCreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT);
- if (!raw->pen) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to create virtual pen device: "sv << err;
- return;
- }
- } else {
- // No need to cancel anything if we had no pen input device
- return;
- }
- }
-
- // Cancel pen repeat callbacks
- if (raw->penRepeatTask) {
- task_pool.cancel(raw->penRepeatTask);
- raw->penRepeatTask = nullptr;
- }
-
- raw->penInfo.type = PT_PEN;
-
- auto &penInfo = raw->penInfo.penInfo;
- penInfo.pointerInfo.pointerType = PT_PEN;
- penInfo.pointerInfo.pointerId = 0;
-
- // Populate shared pointer info fields
- populate_common_pointer_info(penInfo.pointerInfo, touch_port, pen.eventType, pen.x, pen.y);
-
- // Windows only supports a single pen button, so send all buttons as the barrel button
- if (pen.penButtons) {
- penInfo.penFlags |= PEN_FLAG_BARREL;
- } else {
- penInfo.penFlags &= ~PEN_FLAG_BARREL;
- }
-
- switch (pen.toolType) {
- default:
- case LI_TOOL_TYPE_PEN:
- penInfo.penFlags &= ~PEN_FLAG_ERASER;
- break;
- case LI_TOOL_TYPE_ERASER:
- penInfo.penFlags |= PEN_FLAG_ERASER;
- break;
- case LI_TOOL_TYPE_UNKNOWN:
- // Leave tool flags alone
- break;
- }
-
- penInfo.penMask = PEN_MASK_NONE;
-
- // Windows doesn't support hover distance, so only pass pressure/distance when the pointer is in contact
- if ((penInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) && pen.pressureOrDistance != 0.0f) {
- penInfo.penMask |= PEN_MASK_PRESSURE;
-
- // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses
- penInfo.pressure = (UINT32) (pen.pressureOrDistance * 1024);
- } else {
- // The default pen pressure is 0
- penInfo.pressure = 0;
- }
-
- if (pen.rotation != LI_ROT_UNKNOWN) {
- penInfo.penMask |= PEN_MASK_ROTATION;
- penInfo.rotation = pen.rotation;
- } else {
- penInfo.rotation = 0;
- }
-
- // We require rotation and tilt to perform the conversion to X and Y tilt angles
- if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) {
- auto rotationRads = pen.rotation * (M_PI / 180.f);
- auto tiltRads = pen.tilt * (M_PI / 180.f);
- auto r = std::sin(tiltRads);
- auto z = std::cos(tiltRads);
-
- // Convert polar coordinates into X and Y tilt angles
- penInfo.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y;
- penInfo.tiltX = (INT32) (std::atan2(std::sin(-rotationRads) * r, z) * 180.f / M_PI);
- penInfo.tiltY = (INT32) (std::atan2(std::cos(-rotationRads) * r, z) * 180.f / M_PI);
- } else {
- penInfo.tiltX = 0;
- penInfo.tiltY = 0;
- }
-
- if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to inject virtual pen input: "sv << err;
- return;
- }
-
- // Clear pointer flags that should only remain set for one frame
- penInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS;
-
- // If we still have an active pen interaction, refresh the pen state periodically
- if (penInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
+ virtualhid::pen_update(((client_input_raw_t *) input)->virtualhid, touch_port, pen);
}
void unicode(input_t &input, char *utf8, int size) {
- // We can do no worse than one UTF-16 character per byte of UTF-8
- std::vector wide(size);
-
- int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, size, wide.data(), size);
- if (chars <= 0) {
- return;
- }
-
- // Send all key down events
- for (int i = 0; i < chars; i++) {
- INPUT input {};
- input.type = INPUT_KEYBOARD;
- input.ki.wScan = wide[i];
- input.ki.dwFlags = KEYEVENTF_UNICODE;
- send_input(input);
- }
-
- // Send all key up events
- for (int i = 0; i < chars; i++) {
- INPUT input {};
- input.type = INPUT_KEYBOARD;
- input.ki.wScan = wide[i];
- input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
- send_input(input);
- }
+ virtualhid::unicode(((input_raw_t *) input.get())->virtualhid, utf8, size);
}
int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
@@ -1897,13 +1236,16 @@ namespace platf {
caps |= platform_caps::controller_touch;
}
- // We support pen and touch input on Win10 1809+
- if (GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice") != nullptr) {
- if (config::input.native_pen_touch) {
- caps |= platform_caps::pen_touch;
+ if (config::input.native_pen_touch) {
+ const auto runtime = virtualhid::create_runtime();
+ if (runtime) {
+ const auto &capabilities = runtime->capabilities();
+ if (capabilities.supports_touchscreen || capabilities.supports_pen_tablet) {
+ caps |= platform_caps::pen_touch;
+ }
+ } else {
+ BOOST_LOG(warning) << "Unable to create libvirtualhid runtime for touch/pen capability detection"sv;
}
- } else {
- BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv;
}
return caps;
diff --git a/src/platform/windows/keylayout.h b/src/platform/windows/keylayout.h
deleted file mode 100644
index ac0a4fa2060..00000000000
--- a/src/platform/windows/keylayout.h
+++ /dev/null
@@ -1,275 +0,0 @@
-/**
- * @file src/platform/windows/keylayout.h
- * @brief Keyboard layout mapping for scancode translation
- */
-#pragma once
-
-// standard includes
-#include
-#include
-
-namespace platf {
- // Virtual Key to Scan Code mapping for the US English layout (00000409).
- // GameStream uses this as the canonical key layout for scancode conversion.
- /**
- * @brief Protocol or platform constant for vk to scancode map.
- */
- constexpr std::array::max() + 1> VK_TO_SCANCODE_MAP {
- 0, /* 0x00 */
- 0, /* 0x01 */
- 0, /* 0x02 */
- 70, /* 0x03 */
- 0, /* 0x04 */
- 0, /* 0x05 */
- 0, /* 0x06 */
- 0, /* 0x07 */
- 14, /* 0x08 */
- 15, /* 0x09 */
- 0, /* 0x0a */
- 0, /* 0x0b */
- 76, /* 0x0c */
- 28, /* 0x0d */
- 0, /* 0x0e */
- 0, /* 0x0f */
- 42, /* 0x10 */
- 29, /* 0x11 */
- 56, /* 0x12 */
- 0, /* 0x13 */
- 58, /* 0x14 */
- 0, /* 0x15 */
- 0, /* 0x16 */
- 0, /* 0x17 */
- 0, /* 0x18 */
- 0, /* 0x19 */
- 0, /* 0x1a */
- 1, /* 0x1b */
- 0, /* 0x1c */
- 0, /* 0x1d */
- 0, /* 0x1e */
- 0, /* 0x1f */
- 57, /* 0x20 */
- 73, /* 0x21 */
- 81, /* 0x22 */
- 79, /* 0x23 */
- 71, /* 0x24 */
- 75, /* 0x25 */
- 72, /* 0x26 */
- 77, /* 0x27 */
- 80, /* 0x28 */
- 0, /* 0x29 */
- 0, /* 0x2a */
- 0, /* 0x2b */
- 84, /* 0x2c */
- 82, /* 0x2d */
- 83, /* 0x2e */
- 99, /* 0x2f */
- 11, /* 0x30 */
- 2, /* 0x31 */
- 3, /* 0x32 */
- 4, /* 0x33 */
- 5, /* 0x34 */
- 6, /* 0x35 */
- 7, /* 0x36 */
- 8, /* 0x37 */
- 9, /* 0x38 */
- 10, /* 0x39 */
- 0, /* 0x3a */
- 0, /* 0x3b */
- 0, /* 0x3c */
- 0, /* 0x3d */
- 0, /* 0x3e */
- 0, /* 0x3f */
- 0, /* 0x40 */
- 30, /* 0x41 */
- 48, /* 0x42 */
- 46, /* 0x43 */
- 32, /* 0x44 */
- 18, /* 0x45 */
- 33, /* 0x46 */
- 34, /* 0x47 */
- 35, /* 0x48 */
- 23, /* 0x49 */
- 36, /* 0x4a */
- 37, /* 0x4b */
- 38, /* 0x4c */
- 50, /* 0x4d */
- 49, /* 0x4e */
- 24, /* 0x4f */
- 25, /* 0x50 */
- 16, /* 0x51 */
- 19, /* 0x52 */
- 31, /* 0x53 */
- 20, /* 0x54 */
- 22, /* 0x55 */
- 47, /* 0x56 */
- 17, /* 0x57 */
- 45, /* 0x58 */
- 21, /* 0x59 */
- 44, /* 0x5a */
- 91, /* 0x5b */
- 92, /* 0x5c */
- 93, /* 0x5d */
- 0, /* 0x5e */
- 95, /* 0x5f */
- 82, /* 0x60 */
- 79, /* 0x61 */
- 80, /* 0x62 */
- 81, /* 0x63 */
- 75, /* 0x64 */
- 76, /* 0x65 */
- 77, /* 0x66 */
- 71, /* 0x67 */
- 72, /* 0x68 */
- 73, /* 0x69 */
- 55, /* 0x6a */
- 78, /* 0x6b */
- 0, /* 0x6c */
- 74, /* 0x6d */
- 83, /* 0x6e */
- 53, /* 0x6f */
- 59, /* 0x70 */
- 60, /* 0x71 */
- 61, /* 0x72 */
- 62, /* 0x73 */
- 63, /* 0x74 */
- 64, /* 0x75 */
- 65, /* 0x76 */
- 66, /* 0x77 */
- 67, /* 0x78 */
- 68, /* 0x79 */
- 87, /* 0x7a */
- 88, /* 0x7b */
- 100, /* 0x7c */
- 101, /* 0x7d */
- 102, /* 0x7e */
- 103, /* 0x7f */
- 104, /* 0x80 */
- 105, /* 0x81 */
- 106, /* 0x82 */
- 107, /* 0x83 */
- 108, /* 0x84 */
- 109, /* 0x85 */
- 110, /* 0x86 */
- 118, /* 0x87 */
- 0, /* 0x88 */
- 0, /* 0x89 */
- 0, /* 0x8a */
- 0, /* 0x8b */
- 0, /* 0x8c */
- 0, /* 0x8d */
- 0, /* 0x8e */
- 0, /* 0x8f */
- 69, /* 0x90 */
- 70, /* 0x91 */
- 0, /* 0x92 */
- 0, /* 0x93 */
- 0, /* 0x94 */
- 0, /* 0x95 */
- 0, /* 0x96 */
- 0, /* 0x97 */
- 0, /* 0x98 */
- 0, /* 0x99 */
- 0, /* 0x9a */
- 0, /* 0x9b */
- 0, /* 0x9c */
- 0, /* 0x9d */
- 0, /* 0x9e */
- 0, /* 0x9f */
- 42, /* 0xa0 */
- 54, /* 0xa1 */
- 29, /* 0xa2 */
- 29, /* 0xa3 */
- 56, /* 0xa4 */
- 56, /* 0xa5 */
- 106, /* 0xa6 */
- 105, /* 0xa7 */
- 103, /* 0xa8 */
- 104, /* 0xa9 */
- 101, /* 0xaa */
- 102, /* 0xab */
- 50, /* 0xac */
- 32, /* 0xad */
- 46, /* 0xae */
- 48, /* 0xaf */
- 25, /* 0xb0 */
- 16, /* 0xb1 */
- 36, /* 0xb2 */
- 34, /* 0xb3 */
- 108, /* 0xb4 */
- 109, /* 0xb5 */
- 107, /* 0xb6 */
- 33, /* 0xb7 */
- 0, /* 0xb8 */
- 0, /* 0xb9 */
- 39, /* 0xba */
- 13, /* 0xbb */
- 51, /* 0xbc */
- 12, /* 0xbd */
- 52, /* 0xbe */
- 53, /* 0xbf */
- 41, /* 0xc0 */
- 115, /* 0xc1 */
- 126, /* 0xc2 */
- 0, /* 0xc3 */
- 0, /* 0xc4 */
- 0, /* 0xc5 */
- 0, /* 0xc6 */
- 0, /* 0xc7 */
- 0, /* 0xc8 */
- 0, /* 0xc9 */
- 0, /* 0xca */
- 0, /* 0xcb */
- 0, /* 0xcc */
- 0, /* 0xcd */
- 0, /* 0xce */
- 0, /* 0xcf */
- 0, /* 0xd0 */
- 0, /* 0xd1 */
- 0, /* 0xd2 */
- 0, /* 0xd3 */
- 0, /* 0xd4 */
- 0, /* 0xd5 */
- 0, /* 0xd6 */
- 0, /* 0xd7 */
- 0, /* 0xd8 */
- 0, /* 0xd9 */
- 0, /* 0xda */
- 26, /* 0xdb */
- 43, /* 0xdc */
- 27, /* 0xdd */
- 40, /* 0xde */
- 0, /* 0xdf */
- 0, /* 0xe0 */
- 0, /* 0xe1 */
- 86, /* 0xe2 */
- 0, /* 0xe3 */
- 0, /* 0xe4 */
- 0, /* 0xe5 */
- 0, /* 0xe6 */
- 0, /* 0xe7 */
- 0, /* 0xe8 */
- 113, /* 0xe9 */
- 92, /* 0xea */
- 123, /* 0xeb */
- 0, /* 0xec */
- 111, /* 0xed */
- 90, /* 0xee */
- 0, /* 0xef */
- 0, /* 0xf0 */
- 91, /* 0xf1 */
- 0, /* 0xf2 */
- 95, /* 0xf3 */
- 0, /* 0xf4 */
- 94, /* 0xf5 */
- 0, /* 0xf6 */
- 0, /* 0xf7 */
- 0, /* 0xf8 */
- 93, /* 0xf9 */
- 0, /* 0xfa */
- 98, /* 0xfb */
- 0, /* 0xfc */
- 0, /* 0xfd */
- 0, /* 0xfe */
- 0, /* 0xff */
- };
-} // namespace platf
diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid
index a9b06f19f2a..c1cc7e04242 160000
--- a/third-party/libvirtualhid
+++ b/third-party/libvirtualhid
@@ -1 +1 @@
-Subproject commit a9b06f19f2adf91650b6a3d2e4c2c983c6ae6279
+Subproject commit c1cc7e04242eef4aeff272e1e366261b3d0877fb
From 98da9e24ba9b2b8e5e8dc377370726a3055133b0 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Tue, 7 Jul 2026 21:46:04 -0400
Subject: [PATCH 06/10] Use libvirtualhid for macOS input
Replace the macOS input implementation with a libvirtualhid-backed backend and route mouse, keyboard, scroll, touch/pen, and gamepad paths through shared virtualhid contexts. Update macOS CMake wiring to build/link libvirtualhid and include the new shared virtualhid input sources, and bump the libvirtualhid submodule to the required revision.
---
cmake/compile_definitions/macos.cmake | 5 +
src/platform/macos/input.cpp | 768 +++-----------------------
third-party/libvirtualhid | 2 +-
3 files changed, 90 insertions(+), 685 deletions(-)
diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake
index 1d184929ac9..f07b7d39898 100644
--- a/cmake/compile_definitions/macos.cmake
+++ b/cmake/compile_definitions/macos.cmake
@@ -25,7 +25,10 @@ if(NOT BOOST_USE_STATIC AND NOT FETCH_CONTENT_BOOST_USED)
ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK)
endif()
+add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libvirtualhid")
+
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
+ libvirtualhid::libvirtualhid
${APP_KIT_LIBRARY}
${APP_SERVICES_LIBRARY}
${AUDIO_TOOLBOX_LIBRARY}
@@ -56,6 +59,8 @@ set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h"
"${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp"
+ "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.h"
+ "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.cpp"
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c"
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h"
${APPLE_PLIST_FILE})
diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp
index fe17a9b35a1..711260c0c67 100644
--- a/src/platform/macos/input.cpp
+++ b/src/platform/macos/input.cpp
@@ -1,757 +1,157 @@
/**
* @file src/platform/macos/input.cpp
- * @brief Definitions for macOS input handling.
+ * @brief Definitions for libvirtualhid-backed macOS input handling.
*/
-// standard includes
-#include
-#include
-#include
-#include
-#include
-#include
// platform includes
#include
-#import
-#include
-#include
-#include
-// local includes
-#include "src/display_device.h"
-#include "src/input.h"
-#include "src/logging.h"
-#include "src/platform/common.h"
-#include "src/utility.h"
+// standard includes
+#include
+#include
+#include
+#include
-constexpr auto MULTICLICK_DELAY_MS = std::chrono::milliseconds(500); ///< Maximum gap between clicks that macOS should treat as a double click.
+// local includes
+#include "src/config.h"
+#include "src/platform/virtualhid_input.h"
namespace platf {
- using namespace std::literals;
-
- constexpr int WHEEL_DELTA = 120; ///< Protocol or platform constant for wheel delta.
- constexpr double DEFAULT_SCROLLWHEEL_SCALING = 0.3125; ///< Protocol or platform constant for default scrollwheel scaling.
- constexpr int DEFAULT_SCROLL_LINES_PER_DETENT = 5; ///< Protocol or platform constant for default scroll lines per detent.
/**
- * @brief macOS input source and target display state.
+ * @brief Global libvirtualhid devices shared by clients.
*/
- struct macos_input_t {
- public:
- CGDirectDisplayID display {}; ///< CoreGraphics identifier for the display receiving injected input.
- CGFloat displayScaling {}; ///< Scale factor used to translate client coordinates to display pixels.
- CGEventSourceRef source {}; ///< CoreGraphics event source used for mouse and scroll events.
-
- // keyboard related stuff
- CGEventSourceRef keyboard_source {}; ///< CoreGraphics event source used for keyboard injection.
- CGEventFlags kb_flags {}; ///< Active keyboard modifier flags currently held down by the client.
-
- // mouse related stuff
- CGEventRef mouse_event {}; ///< Reusable CoreGraphics mouse event updated before posting.
- double scrollwheel_scaling {DEFAULT_SCROLLWHEEL_SCALING}; ///< Multiplier applied to incoming scroll-wheel deltas.
- int scroll_lines_per_detent {DEFAULT_SCROLL_LINES_PER_DETENT}; ///< Number of logical scroll lines represented by one wheel detent.
- /**
- * @brief Tracks whether the mouse button is currently pressed.
- */
- bool mouse_down[3] {}; // mouse button status
- /**
- * @brief Last mouse event.
- */
- std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events
- };
-
- // A struct to hold a Windows keycode to Mac virtual keycode mapping.
- /**
- * @brief Mapping from Sunshine key symbols to macOS virtual key codes.
- */
- struct KeyCodeMap {
- int win_keycode; ///< Sunshine/Windows virtual key code received from the client.
- int mac_keycode; ///< macOS virtual key code sent through CoreGraphics.
+ struct input_raw_t {
+ virtualhid::input_context_t virtualhid; ///< libvirtualhid input context.
};
- // Customized less operator for using std::lower_bound() on a KeyCodeMap array.
- /**
- * @brief Order key mappings by Sunshine key code for binary search.
- *
- * @param a Left-hand mapping being compared.
- * @param b Right-hand mapping being compared.
- * @return True when `a` has a lower Sunshine key code than `b`.
- */
- bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) {
- return a.win_keycode < b.win_keycode;
- }
-
- // clang-format off
-/**
- * @brief Key codes map.
- */
-const KeyCodeMap kKeyCodesMap[] = {
- { 0x08 /* VKEY_BACK */, kVK_Delete },
- { 0x09 /* VKEY_TAB */, kVK_Tab },
- { 0x0A /* VKEY_BACKTAB */, 0x21E4 },
- { 0x0C /* VKEY_CLEAR */, kVK_ANSI_KeypadClear },
- { 0x0D /* VKEY_RETURN */, kVK_Return },
- { 0x10 /* VKEY_SHIFT */, kVK_Shift },
- { 0x11 /* VKEY_CONTROL */, kVK_Control },
- { 0x12 /* VKEY_MENU */, kVK_Option },
- { 0x13 /* VKEY_PAUSE */, -1 },
- { 0x14 /* VKEY_CAPITAL */, kVK_CapsLock },
- { 0x15 /* VKEY_KANA */, kVK_JIS_Kana },
- { 0x15 /* VKEY_HANGUL */, -1 },
- { 0x17 /* VKEY_JUNJA */, -1 },
- { 0x18 /* VKEY_FINAL */, -1 },
- { 0x19 /* VKEY_HANJA */, -1 },
- { 0x19 /* VKEY_KANJI */, -1 },
- { 0x1B /* VKEY_ESCAPE */, kVK_Escape },
- { 0x1C /* VKEY_CONVERT */, -1 },
- { 0x1D /* VKEY_NONCONVERT */, -1 },
- { 0x1E /* VKEY_ACCEPT */, -1 },
- { 0x1F /* VKEY_MODECHANGE */, -1 },
- { 0x20 /* VKEY_SPACE */, kVK_Space },
- { 0x21 /* VKEY_PRIOR */, kVK_PageUp },
- { 0x22 /* VKEY_NEXT */, kVK_PageDown },
- { 0x23 /* VKEY_END */, kVK_End },
- { 0x24 /* VKEY_HOME */, kVK_Home },
- { 0x25 /* VKEY_LEFT */, kVK_LeftArrow },
- { 0x26 /* VKEY_UP */, kVK_UpArrow },
- { 0x27 /* VKEY_RIGHT */, kVK_RightArrow },
- { 0x28 /* VKEY_DOWN */, kVK_DownArrow },
- { 0x29 /* VKEY_SELECT */, -1 },
- { 0x2A /* VKEY_PRINT */, -1 },
- { 0x2B /* VKEY_EXECUTE */, -1 },
- { 0x2C /* VKEY_SNAPSHOT */, -1 },
- { 0x2D /* VKEY_INSERT */, kVK_Help },
- { 0x2E /* VKEY_DELETE */, kVK_ForwardDelete },
- { 0x2F /* VKEY_HELP */, kVK_Help },
- { 0x30 /* VKEY_0 */, kVK_ANSI_0 },
- { 0x31 /* VKEY_1 */, kVK_ANSI_1 },
- { 0x32 /* VKEY_2 */, kVK_ANSI_2 },
- { 0x33 /* VKEY_3 */, kVK_ANSI_3 },
- { 0x34 /* VKEY_4 */, kVK_ANSI_4 },
- { 0x35 /* VKEY_5 */, kVK_ANSI_5 },
- { 0x36 /* VKEY_6 */, kVK_ANSI_6 },
- { 0x37 /* VKEY_7 */, kVK_ANSI_7 },
- { 0x38 /* VKEY_8 */, kVK_ANSI_8 },
- { 0x39 /* VKEY_9 */, kVK_ANSI_9 },
- { 0x41 /* VKEY_A */, kVK_ANSI_A },
- { 0x42 /* VKEY_B */, kVK_ANSI_B },
- { 0x43 /* VKEY_C */, kVK_ANSI_C },
- { 0x44 /* VKEY_D */, kVK_ANSI_D },
- { 0x45 /* VKEY_E */, kVK_ANSI_E },
- { 0x46 /* VKEY_F */, kVK_ANSI_F },
- { 0x47 /* VKEY_G */, kVK_ANSI_G },
- { 0x48 /* VKEY_H */, kVK_ANSI_H },
- { 0x49 /* VKEY_I */, kVK_ANSI_I },
- { 0x4A /* VKEY_J */, kVK_ANSI_J },
- { 0x4B /* VKEY_K */, kVK_ANSI_K },
- { 0x4C /* VKEY_L */, kVK_ANSI_L },
- { 0x4D /* VKEY_M */, kVK_ANSI_M },
- { 0x4E /* VKEY_N */, kVK_ANSI_N },
- { 0x4F /* VKEY_O */, kVK_ANSI_O },
- { 0x50 /* VKEY_P */, kVK_ANSI_P },
- { 0x51 /* VKEY_Q */, kVK_ANSI_Q },
- { 0x52 /* VKEY_R */, kVK_ANSI_R },
- { 0x53 /* VKEY_S */, kVK_ANSI_S },
- { 0x54 /* VKEY_T */, kVK_ANSI_T },
- { 0x55 /* VKEY_U */, kVK_ANSI_U },
- { 0x56 /* VKEY_V */, kVK_ANSI_V },
- { 0x57 /* VKEY_W */, kVK_ANSI_W },
- { 0x58 /* VKEY_X */, kVK_ANSI_X },
- { 0x59 /* VKEY_Y */, kVK_ANSI_Y },
- { 0x5A /* VKEY_Z */, kVK_ANSI_Z },
- { 0x5B /* VKEY_LWIN */, kVK_Command },
- { 0x5C /* VKEY_RWIN */, kVK_RightCommand },
- { 0x5D /* VKEY_APPS */, kVK_RightCommand },
- { 0x5F /* VKEY_SLEEP */, -1 },
- { 0x60 /* VKEY_NUMPAD0 */, kVK_ANSI_Keypad0 },
- { 0x61 /* VKEY_NUMPAD1 */, kVK_ANSI_Keypad1 },
- { 0x62 /* VKEY_NUMPAD2 */, kVK_ANSI_Keypad2 },
- { 0x63 /* VKEY_NUMPAD3 */, kVK_ANSI_Keypad3 },
- { 0x64 /* VKEY_NUMPAD4 */, kVK_ANSI_Keypad4 },
- { 0x65 /* VKEY_NUMPAD5 */, kVK_ANSI_Keypad5 },
- { 0x66 /* VKEY_NUMPAD6 */, kVK_ANSI_Keypad6 },
- { 0x67 /* VKEY_NUMPAD7 */, kVK_ANSI_Keypad7 },
- { 0x68 /* VKEY_NUMPAD8 */, kVK_ANSI_Keypad8 },
- { 0x69 /* VKEY_NUMPAD9 */, kVK_ANSI_Keypad9 },
- { 0x6A /* VKEY_MULTIPLY */, kVK_ANSI_KeypadMultiply },
- { 0x6B /* VKEY_ADD */, kVK_ANSI_KeypadPlus },
- { 0x6C /* VKEY_SEPARATOR */, -1 },
- { 0x6D /* VKEY_SUBTRACT */, kVK_ANSI_KeypadMinus },
- { 0x6E /* VKEY_DECIMAL */, kVK_ANSI_KeypadDecimal },
- { 0x6F /* VKEY_DIVIDE */, kVK_ANSI_KeypadDivide },
- { 0x70 /* VKEY_F1 */, kVK_F1 },
- { 0x71 /* VKEY_F2 */, kVK_F2 },
- { 0x72 /* VKEY_F3 */, kVK_F3 },
- { 0x73 /* VKEY_F4 */, kVK_F4 },
- { 0x74 /* VKEY_F5 */, kVK_F5 },
- { 0x75 /* VKEY_F6 */, kVK_F6 },
- { 0x76 /* VKEY_F7 */, kVK_F7 },
- { 0x77 /* VKEY_F8 */, kVK_F8 },
- { 0x78 /* VKEY_F9 */, kVK_F9 },
- { 0x79 /* VKEY_F10 */, kVK_F10 },
- { 0x7A /* VKEY_F11 */, kVK_F11 },
- { 0x7B /* VKEY_F12 */, kVK_F12 },
- { 0x7C /* VKEY_F13 */, kVK_F13 },
- { 0x7D /* VKEY_F14 */, kVK_F14 },
- { 0x7E /* VKEY_F15 */, kVK_F15 },
- { 0x7F /* VKEY_F16 */, kVK_F16 },
- { 0x80 /* VKEY_F17 */, kVK_F17 },
- { 0x81 /* VKEY_F18 */, kVK_F18 },
- { 0x82 /* VKEY_F19 */, kVK_F19 },
- { 0x83 /* VKEY_F20 */, kVK_F20 },
- { 0x84 /* VKEY_F21 */, -1 },
- { 0x85 /* VKEY_F22 */, -1 },
- { 0x86 /* VKEY_F23 */, -1 },
- { 0x87 /* VKEY_F24 */, -1 },
- { 0x90 /* VKEY_NUMLOCK */, -1 },
- { 0x91 /* VKEY_SCROLL */, -1 },
- { 0xA0 /* VKEY_LSHIFT */, kVK_Shift },
- { 0xA1 /* VKEY_RSHIFT */, kVK_RightShift },
- { 0xA2 /* VKEY_LCONTROL */, kVK_Control },
- { 0xA3 /* VKEY_RCONTROL */, kVK_RightControl },
- { 0xA4 /* VKEY_LMENU */, kVK_Option },
- { 0xA5 /* VKEY_RMENU */, kVK_RightOption },
- { 0xA6 /* VKEY_BROWSER_BACK */, -1 },
- { 0xA7 /* VKEY_BROWSER_FORWARD */, -1 },
- { 0xA8 /* VKEY_BROWSER_REFRESH */, -1 },
- { 0xA9 /* VKEY_BROWSER_STOP */, -1 },
- { 0xAA /* VKEY_BROWSER_SEARCH */, -1 },
- { 0xAB /* VKEY_BROWSER_FAVORITES */, -1 },
- { 0xAC /* VKEY_BROWSER_HOME */, -1 },
- { 0xAD /* VKEY_VOLUME_MUTE */, -1 },
- { 0xAE /* VKEY_VOLUME_DOWN */, -1 },
- { 0xAF /* VKEY_VOLUME_UP */, -1 },
- { 0xB0 /* VKEY_MEDIA_NEXT_TRACK */, -1 },
- { 0xB1 /* VKEY_MEDIA_PREV_TRACK */, -1 },
- { 0xB2 /* VKEY_MEDIA_STOP */, -1 },
- { 0xB3 /* VKEY_MEDIA_PLAY_PAUSE */, -1 },
- { 0xB4 /* VKEY_MEDIA_LAUNCH_MAIL */, -1 },
- { 0xB5 /* VKEY_MEDIA_LAUNCH_MEDIA_SELECT */, -1 },
- { 0xB6 /* VKEY_MEDIA_LAUNCH_APP1 */, -1 },
- { 0xB7 /* VKEY_MEDIA_LAUNCH_APP2 */, -1 },
- { 0xBA /* VKEY_OEM_1 */, kVK_ANSI_Semicolon },
- { 0xBB /* VKEY_OEM_PLUS */, kVK_ANSI_Equal },
- { 0xBC /* VKEY_OEM_COMMA */, kVK_ANSI_Comma },
- { 0xBD /* VKEY_OEM_MINUS */, kVK_ANSI_Minus },
- { 0xBE /* VKEY_OEM_PERIOD */, kVK_ANSI_Period },
- { 0xBF /* VKEY_OEM_2 */, kVK_ANSI_Slash },
- { 0xC0 /* VKEY_OEM_3 */, kVK_ANSI_Grave },
- { 0xDB /* VKEY_OEM_4 */, kVK_ANSI_LeftBracket },
- { 0xDC /* VKEY_OEM_5 */, kVK_ANSI_Backslash },
- { 0xDD /* VKEY_OEM_6 */, kVK_ANSI_RightBracket },
- { 0xDE /* VKEY_OEM_7 */, kVK_ANSI_Quote },
- { 0xDF /* VKEY_OEM_8 */, -1 },
- { 0xE2 /* VKEY_OEM_102 */, -1 },
- { 0xE5 /* VKEY_PROCESSKEY */, -1 },
- { 0xE7 /* VKEY_PACKET */, -1 },
- { 0xF6 /* VKEY_ATTN */, -1 },
- { 0xF7 /* VKEY_CRSEL */, -1 },
- { 0xF8 /* VKEY_EXSEL */, -1 },
- { 0xF9 /* VKEY_EREOF */, -1 },
- { 0xFA /* VKEY_PLAY */, -1 },
- { 0xFB /* VKEY_ZOOM */, -1 },
- { 0xFC /* VKEY_NONAME */, -1 },
- { 0xFD /* VKEY_PA1 */, -1 },
- { 0xFE /* VKEY_OEM_CLEAR */, kVK_ANSI_KeypadClear }
-};
- // clang-format on
-
/**
- * @brief Translate a platform keycode to a Sunshine key symbol.
- *
- * @param keycode Platform keycode being translated or emitted.
- * @return Sunshine key symbol, or 0 when the keycode is unmapped.
+ * @brief Per-client libvirtualhid devices.
*/
- int keysym(int keycode) {
- KeyCodeMap key_map {};
-
- key_map.win_keycode = keycode;
- const KeyCodeMap *temp_map = std::lower_bound(
- kKeyCodesMap,
- kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]),
- key_map
- );
-
- if (temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) ||
- temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) {
- return -1;
- }
-
- return temp_map->mac_keycode;
- }
+ 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} {}
- /**
- * @brief macOS modifier flags split into generic and device-specific bits.
- */
- struct modifier_flags_t {
- CGEventFlags generic {}; ///< Modifier bits represented by device-independent CoreGraphics flags.
- CGEventFlags device {}; ///< Modifier bits represented by left/right device-specific flags.
- CGEventFlags all_devices {}; ///< Mask covering all device-specific variants for this modifier.
+ virtualhid::client_context_t virtualhid; ///< libvirtualhid client context.
};
- /**
- * @brief Resolve the CoreGraphics modifier masks associated with a key code.
- *
- * @param key Sunshine key code that may represent a modifier key.
- * @param flags Output masks for the matching CoreGraphics modifier.
- * @return True when modifier flags were found for the key.
- */
- bool modifier_flags_for_key(int key, modifier_flags_t &flags) {
- switch (key) {
- case kVK_Shift:
- flags = {kCGEventFlagMaskShift, NX_DEVICELSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK | NX_DEVICERSHIFTKEYMASK};
- return true;
- case kVK_RightShift:
- flags = {kCGEventFlagMaskShift, NX_DEVICERSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK | NX_DEVICERSHIFTKEYMASK};
- return true;
- case kVK_Command:
- flags = {kCGEventFlagMaskCommand, NX_DEVICELCMDKEYMASK, NX_DEVICELCMDKEYMASK | NX_DEVICERCMDKEYMASK};
- return true;
- case kVK_RightCommand:
- flags = {kCGEventFlagMaskCommand, NX_DEVICERCMDKEYMASK, NX_DEVICELCMDKEYMASK | NX_DEVICERCMDKEYMASK};
- return true;
- case kVK_Option:
- flags = {kCGEventFlagMaskAlternate, NX_DEVICELALTKEYMASK, NX_DEVICELALTKEYMASK | NX_DEVICERALTKEYMASK};
- return true;
- case kVK_RightOption:
- flags = {kCGEventFlagMaskAlternate, NX_DEVICERALTKEYMASK, NX_DEVICELALTKEYMASK | NX_DEVICERALTKEYMASK};
- return true;
- case kVK_Control:
- flags = {kCGEventFlagMaskControl, NX_DEVICELCTLKEYMASK, NX_DEVICELCTLKEYMASK | NX_DEVICERCTLKEYMASK};
- return true;
- case kVK_RightControl:
- flags = {kCGEventFlagMaskControl, NX_DEVICERCTLKEYMASK, NX_DEVICELCTLKEYMASK | NX_DEVICERCTLKEYMASK};
- return true;
- default:
- return false;
- }
- }
-
- void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
- auto key = keysym(modcode);
-
- BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release;
-
- if (key < 0) {
- return;
- }
-
- auto macos_input = ((macos_input_t *) input.get());
- CGEventRef event = nullptr;
- modifier_flags_t modifier_flags;
-
- if (modifier_flags_for_key(key, modifier_flags)) {
- event = CGEventCreateKeyboardEvent(macos_input->keyboard_source, key, !release);
- if (!event) {
- return;
- }
-
- CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key);
-
- if (release) {
- macos_input->kb_flags &= ~modifier_flags.device;
- if ((macos_input->kb_flags & modifier_flags.all_devices) == 0) {
- macos_input->kb_flags &= ~modifier_flags.generic;
- }
- } else {
- macos_input->kb_flags |= modifier_flags.generic | modifier_flags.device;
- }
-
- CGEventSetType(event, kCGEventFlagsChanged);
- } else {
- event = CGEventCreateKeyboardEvent(macos_input->keyboard_source, key, !release);
- if (!event) {
- return;
- }
-
- CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown);
- }
-
- CGEventSetFlags(event, macos_input->kb_flags);
- CGEventPost(kCGSessionEventTap, event);
- CFRelease(event);
- }
-
- void unicode(input_t &input, char *utf8, int size) {
- BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv;
- }
-
- int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
- BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv;
- return -1;
- }
-
- void free_gamepad(input_t &input, int nr) {
- BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv;
+ input_t input() {
+ return {new input_raw_t {}};
}
- void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
- BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv;
+ std::unique_ptr allocate_client_input_context(input_t &input) {
+ return std::make_unique(input);
}
- // returns current mouse location:
- util::point_t get_mouse_loc(input_t &input) {
- // Creating a new event every time to avoid any reuse risk
- const auto macos_input = static_cast(input.get());
- const auto snapshot_event = CGEventCreate(macos_input->source);
- const auto current = CGEventGetLocation(snapshot_event);
- CFRelease(snapshot_event);
- return util::point_t {
- current.x,
- current.y
- };
+ void freeInput(void *p) {
+ auto *input = (input_raw_t *) p;
+ delete input;
}
- /**
- * @brief Post a mouse event at the clamped display location.
- *
- * @param input Platform input context.
- * @param button Mouse button.
- * @param type CoreGraphics mouse event type.
- * @param raw_location Requested mouse location.
- * @param previous_location Previous mouse location.
- * @param click_count Click count for the event.
- */
- void post_mouse(
- input_t &input,
- const CGMouseButton button,
- const CGEventType type,
- const util::point_t raw_location,
- const util::point_t previous_location,
- const int click_count
- ) {
- BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << raw_location.x << ":"sv << raw_location.y << " click_count: "sv << click_count;
-
- const auto macos_input = static_cast(input.get());
- const auto display = macos_input->display;
- const auto event = macos_input->mouse_event;
-
- // get display bounds for current display
- const CGRect display_bounds = CGDisplayBounds(display);
-
- // limit mouse to current display bounds
- const auto location = CGPoint {
- std::clamp(raw_location.x, display_bounds.origin.x, display_bounds.origin.x + display_bounds.size.width - 1),
- std::clamp(raw_location.y, display_bounds.origin.y, display_bounds.origin.y + display_bounds.size.height - 1)
- };
-
- CGEventSetType(event, type);
- CGEventSetLocation(event, location);
- CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button);
- CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count);
-
- // Include deltas so some 3D applications can consume changes (game cameras, etc)
- const double deltaX = raw_location.x - previous_location.x;
- const double deltaY = raw_location.y - previous_location.y;
- CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
- CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
-
- // Inject modifier flags into mouse events so that shift+click and similar combinations work correctly.
- CGEventSetFlags(event, macos_input->kb_flags);
- CGEventPost(kCGHIDEventTap, event);
- // For why this is here, see:
- // https://stackoverflow.com/questions/15194409/simulated-mouseevent-not-working-properly-osx
- CGWarpMouseCursorPosition(location);
+ void move_mouse(input_t &input, int deltaX, int deltaY) {
+ virtualhid::move_mouse(((input_raw_t *) input.get())->virtualhid, deltaX, deltaY);
}
- /**
- * @brief Choose the CoreGraphics mouse event type for the current button action.
- *
- * @param input Platform input backend that receives the event.
- * @return CoreGraphics mouse event type for button press or release.
- */
- inline CGEventType event_type_mouse(input_t &input) {
- const auto macos_input = static_cast(input.get());
-
- if (macos_input->mouse_down[0]) {
- return kCGEventLeftMouseDragged;
- }
- if (macos_input->mouse_down[1]) {
- return kCGEventOtherMouseDragged;
- }
- if (macos_input->mouse_down[2]) {
- return kCGEventRightMouseDragged;
- }
- return kCGEventMouseMoved;
+ 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 move_mouse(
- input_t &input,
- const int deltaX,
- const int deltaY
- ) {
- const auto current = get_mouse_loc(input);
-
- const auto location = util::point_t {current.x + deltaX, current.y + deltaY};
- post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, current, 0);
+ void button_mouse(input_t &input, int button, bool release) {
+ virtualhid::button_mouse(((input_raw_t *) input.get())->virtualhid, button, release);
}
- void abs_mouse(
- input_t &input,
- const touch_port_t &touch_port,
- const float x,
- const float y
- ) {
- const auto macos_input = static_cast(input.get());
- const auto scaling = macos_input->displayScaling;
- const auto display = macos_input->display;
-
- auto location = util::point_t {x * scaling, y * scaling};
- CGRect display_bounds = CGDisplayBounds(display);
- // in order to get the correct mouse location for capturing display , we need to add the display bounds to the location
- location.x += display_bounds.origin.x;
- location.y += display_bounds.origin.y;
-
- post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, get_mouse_loc(input), 0);
+ void scroll(input_t &input, int high_res_distance) {
+ virtualhid::scroll(((input_raw_t *) input.get())->virtualhid, high_res_distance);
}
- void button_mouse(input_t &input, const int button, const bool release) {
- CGMouseButton mac_button;
- CGEventType event;
-
- const auto macos_input = static_cast(input.get());
-
- switch (button) {
- case 1:
- mac_button = kCGMouseButtonLeft;
- event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown;
- break;
- case 2:
- mac_button = kCGMouseButtonCenter;
- event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown;
- break;
- case 3:
- mac_button = kCGMouseButtonRight;
- event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown;
- break;
- default:
- BOOST_LOG(warning) << "Unsupported mouse button for MacOS: "sv << button;
- return;
- }
-
- macos_input->mouse_down[mac_button] = !release;
-
- // if the last mouse down was less than MULTICLICK_DELAY_MS, we send a double click event
- const auto now = std::chrono::steady_clock::now();
- const auto mouse_position = get_mouse_loc(input);
-
- if (now < macos_input->last_mouse_event[mac_button][release] + MULTICLICK_DELAY_MS) {
- post_mouse(input, mac_button, event, mouse_position, mouse_position, 2);
- } else {
- post_mouse(input, mac_button, event, mouse_position, mouse_position, 1);
- }
-
- macos_input->last_mouse_event[mac_button][release] = now;
- }
-
- /**
- * @brief Get scroll lines per detent.
- *
- * @param scrollwheel_scaling Scrollwheel scaling.
- * @return Number of logical lines represented by one wheel detent.
- */
- int get_scroll_lines_per_detent(double &scrollwheel_scaling) {
- double scale = DEFAULT_SCROLLWHEEL_SCALING;
- const auto value = CFPreferencesCopyValue(CFSTR("com.apple.scrollwheel.scaling"), kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
- if (value) {
- if (CFGetTypeID(value) == CFNumberGetTypeID()) {
- CFNumberGetValue(static_cast(value), kCFNumberDoubleType, &scale);
- } else if (CFGetTypeID(value) == CFStringGetTypeID()) {
- scale = CFStringGetDoubleValue(static_cast(value));
- }
- CFRelease(value);
- }
-
- if (!std::isfinite(scale)) {
- scale = DEFAULT_SCROLLWHEEL_SCALING;
- }
-
- scrollwheel_scaling = scale;
-
- // com.apple.scrollwheel.scaling stores the Mouse scroll speed slider position, not
- // the scroll multiplier itself. The slider is 0..1 and Apple's default is 0.3125,
- // so anchor 0 at one line per wheel detent and 0.3125 at five lines.
- const auto scroll_scale = std::clamp(scale, 0.0, 1.0);
- constexpr double lines_per_scroll_scale = (DEFAULT_SCROLL_LINES_PER_DETENT - 1.0) / DEFAULT_SCROLLWHEEL_SCALING;
-
- return std::max(1, static_cast(std::ceil(1.0 + scroll_scale * lines_per_scroll_scale)));
+ void hscroll(input_t &input, int high_res_distance) {
+ virtualhid::hscroll(((input_raw_t *) input.get())->virtualhid, high_res_distance);
}
- /**
- * @brief Convert a high-resolution wheel delta to CoreGraphics scroll pixels.
- *
- * @param macos_input macOS input state containing scroll scaling settings.
- * @param high_res_distance Wheel delta in Windows high-resolution units.
- * @return Pixel distance to send to CoreGraphics.
- */
- int scroll_pixels(const macos_input_t *macos_input, const int high_res_distance) {
- const auto source_pixels_per_line = CGEventSourceGetPixelsPerLine(macos_input->source);
- const auto pixels_per_line = source_pixels_per_line > 0 ? static_cast(source_pixels_per_line + 0.5) : 10;
- const auto scaled_pixels = static_cast(high_res_distance) * std::max(1, pixels_per_line) * std::max(1, macos_input->scroll_lines_per_detent);
-
- return static_cast(scaled_pixels / WHEEL_DELTA);
+ void keyboard_update(input_t &input, std::uint16_t modcode, bool release, std::uint8_t flags) {
+ virtualhid::keyboard_update(((input_raw_t *) input.get())->virtualhid, modcode, release, flags);
}
- /**
- * @brief Post a macOS scroll event to the target display.
- *
- * @param input Platform input backend that receives the event.
- * @param wheelY Wheel y.
- * @param wheelX Wheel x.
- */
- void post_scroll(input_t &input, const int wheelY, const int wheelX) {
- if (wheelY == 0 && wheelX == 0) {
- return;
- }
-
- const auto macos_input = static_cast(input.get());
- CGEventRef event = CGEventCreateScrollWheelEvent(macos_input->source, kCGScrollEventUnitPixel, 2, wheelY, wheelX);
- if (!event) {
- return;
- }
-
- CGEventSetIntegerValueField(event, kCGScrollWheelEventIsContinuous, 1);
- CGEventPost(kCGHIDEventTap, event);
- CFRelease(event);
+ void unicode(input_t &input, char *utf8, int size) {
+ virtualhid::unicode(((input_raw_t *) input.get())->virtualhid, utf8, size);
}
- void scroll(input_t &input, const int high_res_distance) {
- post_scroll(input, scroll_pixels(static_cast(input.get()), high_res_distance), 0);
+ 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_port, touch);
}
- void hscroll(input_t &input, int high_res_distance) {
- post_scroll(input, 0, scroll_pixels(static_cast(input.get()), high_res_distance));
+ 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, touch_port, pen);
}
- /**
- * @brief Allocates a context to store per-client input data.
- * @param input The global input context.
- * @return A unique pointer to a per-client input data context.
- */
- std::unique_ptr allocate_client_input_context(input_t &input) {
- // Unused
- return nullptr;
+ 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));
}
- /**
- * @brief Sends a touch event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param touch The touch event.
- */
- void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {
- // Unimplemented feature - platform_caps::pen_touch
+ void free_gamepad(input_t &input, int nr) {
+ virtualhid::free_gamepad(((input_raw_t *) input.get())->virtualhid, nr);
}
- /**
- * @brief Sends a pen event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param pen The pen event.
- */
- void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {
- // Unimplemented feature - platform_caps::pen_touch
+ 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);
}
- /**
- * @brief Sends a gamepad touch event to the OS.
- * @param input The global input context.
- * @param touch The touch event.
- */
void gamepad_touch(input_t &input, const gamepad_touch_t &touch) {
- // Unimplemented feature - platform_caps::controller_touch
+ virtualhid::gamepad_touch(((input_raw_t *) input.get())->virtualhid, touch);
}
- /**
- * @brief Sends a gamepad motion event to the OS.
- * @param input The global input context.
- * @param motion The motion event.
- */
void gamepad_motion(input_t &input, const gamepad_motion_t &motion) {
- // Unimplemented
+ virtualhid::gamepad_motion(((input_raw_t *) input.get())->virtualhid, motion);
}
- /**
- * @brief Sends a gamepad battery event to the OS.
- * @param input The global input context.
- * @param battery The battery event.
- */
void gamepad_battery(input_t &input, const gamepad_battery_t &battery) {
- // Unimplemented
+ virtualhid::gamepad_battery(((input_raw_t *) input.get())->virtualhid, battery);
}
- input_t input() {
- input_t result {new macos_input_t()};
-
- const auto macos_input = static_cast(result.get());
-
- // Default to main display
- macos_input->display = CGMainDisplayID();
-
- auto output_name = display_device::map_output_name(config::video.output_name);
- // If output_name is set, try to find the display with that display id
- if (!output_name.empty()) {
- const int MAX_DISPLAYS = 32;
- uint32_t max_display = MAX_DISPLAYS;
- uint32_t display_count;
- CGDirectDisplayID displays[MAX_DISPLAYS];
- if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) {
- BOOST_LOG(error) << "Unable to get active display list , error: "sv << std::endl;
- } else {
- for (int i = 0; i < display_count; i++) {
- CGDirectDisplayID display_id = displays[i];
- if (display_id == std::atoi(output_name.c_str())) {
- macos_input->display = display_id;
- }
- }
- }
+ util::point_t get_mouse_loc(input_t & /*input*/) {
+ const auto event = CGEventCreate(nullptr);
+ if (!event) {
+ return {0.0, 0.0};
}
- // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor
- const CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display);
- macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode));
- CFRelease(mode);
-
- macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
- macos_input->keyboard_source = CGEventSourceCreate(kCGEventSourceStatePrivate);
- macos_input->scroll_lines_per_detent = get_scroll_lines_per_detent(macos_input->scrollwheel_scaling);
-
- macos_input->kb_flags = 0;
-
- macos_input->mouse_event = CGEventCreate(macos_input->source);
- macos_input->mouse_down[0] = false;
- macos_input->mouse_down[1] = false;
- macos_input->mouse_down[2] = false;
-
- BOOST_LOG(debug) << "macOS scroll speed: com.apple.scrollwheel.scaling="sv << macos_input->scrollwheel_scaling << ", lines per detent="sv << macos_input->scroll_lines_per_detent << ", pixels per line="sv << CGEventSourceGetPixelsPerLine(macos_input->source);
- BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimension: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display);
-
- return result;
+ const auto current = CGEventGetLocation(event);
+ CFRelease(event);
+ return {current.x, current.y};
}
- void freeInput(void *p) {
- const auto *input = static_cast(p);
+ platform_caps::caps_t get_capabilities() {
+ platform_caps::caps_t caps = 0;
+ const auto runtime = virtualhid::create_runtime();
+ if (!runtime) {
+ return caps;
+ }
- CFRelease(input->source);
- CFRelease(input->keyboard_source);
- CFRelease(input->mouse_event);
+ const auto &capabilities = runtime->capabilities();
+ if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_touchpad()) {
+ caps |= platform_caps::controller_touch;
+ }
+ if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
+ caps |= platform_caps::pen_touch;
+ }
- delete input;
+ return caps;
}
std::vector &supported_gamepads(input_t *input) {
- static std::vector gamepads {
- supported_gamepad_t {"", false, "gamepads.macos_not_implemented"}
- };
+ 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;
}
- /**
- * @brief Returns the supported platform capabilities to advertise to the client.
- * @return Capability flags.
- */
- platform_caps::caps_t get_capabilities() {
- return 0;
- }
} // namespace platf
diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid
index c1cc7e04242..4d1d1cf1c69 160000
--- a/third-party/libvirtualhid
+++ b/third-party/libvirtualhid
@@ -1 +1 @@
-Subproject commit c1cc7e04242eef4aeff272e1e366261b3d0877fb
+Subproject commit 4d1d1cf1c692e24ec53d83f62bcd0859a3f8eb2c
From ff7d76b46a1e5baad49c168772a6592587feaa9c Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Tue, 7 Jul 2026 23:56:55 -0400
Subject: [PATCH 07/10] Centralize libvirtualhid CMake setup
Move libvirtualhid integration from Linux/macOS/Windows compile definition files into common.cmake. This removes duplicated target/library/source wiring, keeps Linux-specific libevdev override variables set before inclusion, and preserves the libevdev -> libvirtualhid build dependency when using the ExternalProject fallback.
---
cmake/compile_definitions/common.cmake | 12 ++++++++++++
cmake/compile_definitions/linux.cmake | 11 +----------
cmake/compile_definitions/macos.cmake | 5 -----
cmake/compile_definitions/windows.cmake | 7 -------
4 files changed, 13 insertions(+), 22 deletions(-)
diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake
index 924f2f5fae3..b13e29cdb1e 100644
--- a/cmake/compile_definitions/common.cmake
+++ b/cmake/compile_definitions/common.cmake
@@ -53,6 +53,18 @@ elseif(UNIX)
endif()
endif()
+# libvirtualhid
+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")
+
+# build libevdev before the libvirtualhid target when using the ExternalProject fallback
+if(EXTERNAL_PROJECT_LIBEVDEV_USED AND TARGET libvirtualhid)
+ add_dependencies(libvirtualhid libevdev)
+endif()
+
include_directories(BEFORE SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include")
file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h")
list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES})
diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake
index a68b0a64c9a..485e419adb5 100644
--- a/cmake/compile_definitions/linux.cmake
+++ b/cmake/compile_definitions/linux.cmake
@@ -296,22 +296,13 @@ 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 libvirtualhid subdirectory in order for them to be picked up
+# These need to be set before common.cmake adds 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}")
-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 libvirtualhid target when using the ExternalProject fallback
-if(EXTERNAL_PROJECT_LIBEVDEV_USED AND TARGET libvirtualhid)
- add_dependencies(libvirtualhid libevdev)
-endif()
-
# AppImage and Flatpak
if (${SUNSHINE_BUILD_APPIMAGE})
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_BUILD_APPIMAGE=1)
diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake
index f07b7d39898..1d184929ac9 100644
--- a/cmake/compile_definitions/macos.cmake
+++ b/cmake/compile_definitions/macos.cmake
@@ -25,10 +25,7 @@ if(NOT BOOST_USE_STATIC AND NOT FETCH_CONTENT_BOOST_USED)
ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK)
endif()
-add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libvirtualhid")
-
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
- libvirtualhid::libvirtualhid
${APP_KIT_LIBRARY}
${APP_SERVICES_LIBRARY}
${AUDIO_TOOLBOX_LIBRARY}
@@ -59,8 +56,6 @@ set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h"
"${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp"
- "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.h"
- "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.cpp"
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c"
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h"
${APPLE_PLIST_FILE})
diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake
index 7fd56ee2f00..4bdcf9374b9 100644
--- a/cmake/compile_definitions/windows.cmake
+++ b/cmake/compile_definitions/windows.cmake
@@ -40,9 +40,6 @@ 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")
@@ -71,8 +68,6 @@ 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,5 +100,3 @@ list(PREPEND PLATFORM_LIBRARIES
ws2_32
wsock32
)
-
-list(APPEND SUNSHINE_EXTERNAL_LIBRARIES libvirtualhid::libvirtualhid)
From cc44a811b7c52ef632c4102f0416319f12d41657 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 8 Jul 2026 08:34:56 -0400
Subject: [PATCH 08/10] Fix AbsMoveInputTest for macOS/offsets
Refactor absolute mouse test to use platform-aware touch port helper and account for display origin offsets when verifying expected mouse position. Removes the incorrect `has_input_moved` assertion and updates libvirtualhid submodule.
---
tests/unit/test_mouse.cpp | 68 +++++++++++++++++++++++++++------------
third-party/libvirtualhid | 2 +-
2 files changed, 48 insertions(+), 22 deletions(-)
diff --git a/tests/unit/test_mouse.cpp b/tests/unit/test_mouse.cpp
index 899400fa2f4..1ca416607cc 100644
--- a/tests/unit/test_mouse.cpp
+++ b/tests/unit/test_mouse.cpp
@@ -6,6 +6,49 @@
#include
+#if defined(__APPLE__) && defined(__MACH__)
+ #include
+#endif
+
+namespace {
+
+ platf::touch_port_t absolute_mouse_test_port() {
+#ifdef _WIN32
+ return platf::touch_port_t {
+ .offset_x = 0,
+ .offset_y = 0,
+ .width = 65535,
+ .height = 65535,
+ };
+#elif defined(__APPLE__) && defined(__MACH__)
+ const auto display_bounds = CGDisplayBounds(CGMainDisplayID());
+ return platf::touch_port_t {
+ .offset_x = static_cast(display_bounds.origin.x),
+ .offset_y = static_cast(display_bounds.origin.y),
+ .width = static_cast(display_bounds.size.width),
+ .height = static_cast(display_bounds.size.height),
+ };
+#elif defined(__linux__) || defined(__FreeBSD__)
+ return platf::touch_port_t {
+ .offset_x = 0,
+ .offset_y = 0,
+ .width = 19200,
+ .height = 12000,
+ };
+#else
+ return platf::touch_port_t {};
+#endif
+ }
+
+ util::point_t expected_absolute_mouse_location(const util::point_t &mouse_pos, const platf::touch_port_t &touch_port) {
+ return {
+ static_cast(touch_port.offset_x) + mouse_pos.x,
+ static_cast(touch_port.offset_y) + mouse_pos.y,
+ };
+ }
+
+} // namespace
+
struct MouseHIDTest: PlatformTestSuite, testing::WithParamInterface {
void SetUp() override {
BaseTest::SetUp();
@@ -83,23 +126,8 @@ TEST_P(MouseHIDTest, AbsMoveInputTest) {
auto old_loc = platf::get_mouse_loc(input);
BOOST_LOG(tests) << "AbsMoveInputTest:: got current mouse loc: " << old_loc;
-#ifdef _WIN32
- platf::touch_port_t abs_port {
- 0,
- 0,
- 65535,
- 65535
- };
-#elif defined(__linux__) || defined(__FreeBSD__)
- platf::touch_port_t abs_port {
- 0,
- 0,
- 19200,
- 12000
- };
-#else
- platf::touch_port_t abs_port {};
-#endif
+ const auto abs_port = absolute_mouse_test_port();
+ const auto expected_pos = expected_absolute_mouse_location(mouse_pos, abs_port);
BOOST_LOG(tests) << "AbsMoveInputTest:: move: " << mouse_pos;
platf::abs_mouse(input, abs_port, mouse_pos.x, mouse_pos.y);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
@@ -117,9 +145,7 @@ TEST_P(MouseHIDTest, AbsMoveInputTest) {
BOOST_LOG(tests) << "AbsMoveInputTest:: moved";
}
- EXPECT_TRUE(has_input_moved);
-
// Verify we moved to the absolute coordinate
- EXPECT_EQ(new_loc.x, mouse_pos.x);
- EXPECT_EQ(new_loc.y, mouse_pos.y);
+ EXPECT_EQ(new_loc.x, expected_pos.x);
+ EXPECT_EQ(new_loc.y, expected_pos.y);
}
diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid
index 4d1d1cf1c69..bcf273c2d63 160000
--- a/third-party/libvirtualhid
+++ b/third-party/libvirtualhid
@@ -1 +1 @@
-Subproject commit 4d1d1cf1c692e24ec53d83f62bcd0859a3f8eb2c
+Subproject commit bcf273c2d638e3859324c8b738ec6248530f3947
From a066fe6da3f9d67610f4c5d992ff58382500b5f1 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 8 Jul 2026 09:36:56 -0400
Subject: [PATCH 09/10] Fix get_mouse_loc on Windows and Linux/X11
Implement get_mouse_loc using GetCursorPos on Windows and XQueryPointer on Linux/X11. Remove the GTEST_SKIP for Windows and Linux virtual mouse tests. Replace fixed sleep with polling loop to wait for mouse movement, fix absolute mouse port to use virtual screen metrics on Windows, and relax movement assertions to direction-only checks to account for OS pointer acceleration.
---
src/platform/common.h | 7 ++-
src/platform/linux/input/virtualhid.cpp | 32 ++++++++++
src/platform/windows/input.cpp | 4 +-
tests/unit/test_mouse.cpp | 81 ++++++++++++++++++-------
4 files changed, 98 insertions(+), 26 deletions(-)
diff --git a/src/platform/common.h b/src/platform/common.h
index 24da9f53cfa..c6884d5474a 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -1102,9 +1102,14 @@ namespace platf {
*/
input_t input();
/**
- * @brief Get the current mouse position on screen
+ * @brief Get the current mouse position for platform input tests.
+ *
* @param input The input_t instance to use.
* @return Screen coordinates of the mouse.
+ *
+ * @note This helper exists only so tests can observe virtual mouse movement. Production input paths should submit
+ * mouse events through `move_mouse()` or `abs_mouse()` instead of reading the host cursor location.
+ *
* @examples
* auto [x, y] = get_mouse_loc(input);
* @examples_end
diff --git a/src/platform/linux/input/virtualhid.cpp b/src/platform/linux/input/virtualhid.cpp
index ca85bfd8e63..896f79310ee 100644
--- a/src/platform/linux/input/virtualhid.cpp
+++ b/src/platform/linux/input/virtualhid.cpp
@@ -6,6 +6,11 @@
// standard includes
#include
+// platform includes
+#ifdef SUNSHINE_BUILD_X11
+ #include
+#endif
+
// local includes
#include "src/platform/virtualhid_input.h"
@@ -138,7 +143,34 @@ namespace platf {
}
util::point_t get_mouse_loc(input_t & /*input*/) {
+#ifdef SUNSHINE_BUILD_X11
+ auto *display = XOpenDisplay(nullptr);
+ if (!display) {
+ return {0, 0};
+ }
+
+ const auto root = DefaultRootWindow(display);
+ Window root_return {};
+ Window child_return {};
+ int root_x = 0;
+ int root_y = 0;
+ int window_x = 0;
+ int window_y = 0;
+ unsigned int mask = 0;
+ const auto queried = XQueryPointer(display, root, &root_return, &child_return, &root_x, &root_y, &window_x, &window_y, &mask);
+ XCloseDisplay(display);
+
+ if (!queried) {
+ return {0, 0};
+ }
+
+ return {
+ static_cast(root_x),
+ static_cast(root_y)
+ };
+#else
return {0, 0};
+#endif
}
std::vector &supported_gamepads(input_t *input) {
diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp
index 8762521e1ae..6095a073c27 100644
--- a/src/platform/windows/input.cpp
+++ b/src/platform/windows/input.cpp
@@ -556,9 +556,7 @@ namespace platf {
virtualhid::move_mouse(((input_raw_t *) input.get())->virtualhid, deltaX, deltaY);
}
- util::point_t get_mouse_loc(input_t &input) {
- throw std::runtime_error("not implemented yet, has to pass tests");
- // TODO: Tests are failing, something wrong here?
+ util::point_t get_mouse_loc(input_t & /*input*/) {
POINT p;
if (!GetCursorPos(&p)) {
return util::point_t {0.0, 0.0};
diff --git a/tests/unit/test_mouse.cpp b/tests/unit/test_mouse.cpp
index 1ca416607cc..e3a2bdf1eab 100644
--- a/tests/unit/test_mouse.cpp
+++ b/tests/unit/test_mouse.cpp
@@ -4,21 +4,63 @@
*/
#include "../tests_common.h"
+#include
+#include
+#include
#include
+#include
+
+#ifdef _WIN32
+ #include
+#endif
#if defined(__APPLE__) && defined(__MACH__)
#include
#endif
namespace {
+ constexpr double mouse_position_tolerance = 1.0;
+ constexpr auto mouse_input_wait = std::chrono::milliseconds(500);
+ constexpr auto mouse_input_poll_interval = std::chrono::milliseconds(10);
+
+ bool is_near(double actual, double expected) {
+ return std::abs(actual - expected) <= mouse_position_tolerance;
+ }
+
+ bool is_relative_axis_moved(double actual_delta, double requested_delta) {
+ if (requested_delta > 0.0) {
+ return actual_delta > 0.0;
+ }
+ if (requested_delta < 0.0) {
+ return actual_delta < 0.0;
+ }
+
+ return is_near(actual_delta, 0.0);
+ }
+
+ void expect_relative_axis_moved(double actual_delta, double requested_delta) {
+ EXPECT_TRUE(is_relative_axis_moved(actual_delta, requested_delta));
+ }
+
+ template
+ util::point_t wait_for_mouse_location(platf::input_t &input, Predicate predicate) {
+ const auto deadline = std::chrono::steady_clock::now() + mouse_input_wait;
+ auto location = platf::get_mouse_loc(input);
+ while (!predicate(location) && std::chrono::steady_clock::now() < deadline) {
+ std::this_thread::sleep_for(mouse_input_poll_interval);
+ location = platf::get_mouse_loc(input);
+ }
+
+ return location;
+ }
platf::touch_port_t absolute_mouse_test_port() {
#ifdef _WIN32
return platf::touch_port_t {
- .offset_x = 0,
- .offset_y = 0,
- .width = 65535,
- .height = 65535,
+ .offset_x = GetSystemMetrics(SM_XVIRTUALSCREEN),
+ .offset_y = GetSystemMetrics(SM_YVIRTUALSCREEN),
+ .width = std::max(1, GetSystemMetrics(SM_CXVIRTUALSCREEN)),
+ .height = std::max(1, GetSystemMetrics(SM_CYVIRTUALSCREEN)),
};
#elif defined(__APPLE__) && defined(__MACH__)
const auto display_bounds = CGDisplayBounds(CGMainDisplayID());
@@ -52,15 +94,6 @@ namespace {
struct MouseHIDTest: PlatformTestSuite, testing::WithParamInterface {
void SetUp() override {
BaseTest::SetUp();
-#ifdef _WIN32
- // TODO: Windows tests are failing, `get_mouse_loc` seems broken and `platf::abs_mouse` too
- // the alternative `platf::abs_mouse` method seem to work better during tests,
- // but I'm not sure about real work
- GTEST_SKIP() << "TODO Windows";
-#elif defined(__linux__) || defined(__FreeBSD__)
- // TODO: Absolute virtual mouse location validation is not implemented for Unix backends yet.
- GTEST_SKIP() << "TODO Unix virtual mouse";
-#endif
}
void TearDown() override {
@@ -93,11 +126,13 @@ TEST_P(MouseHIDTest, MoveInputTest) {
BOOST_LOG(tests) << "MoveInputTest:: move: " << mouse_delta;
platf::move_mouse(input, mouse_delta.x, mouse_delta.y);
- std::this_thread::sleep_for(std::chrono::milliseconds(200));
BOOST_LOG(tests) << "MoveInputTest:: moved: " << mouse_delta;
BOOST_LOG(tests) << "MoveInputTest:: get updated mouse loc";
- auto new_loc = platf::get_mouse_loc(input);
+ auto new_loc = wait_for_mouse_location(input, [&old_loc, &mouse_delta](const auto &location) {
+ return is_relative_axis_moved(location.x - old_loc.x, mouse_delta.x) &&
+ is_relative_axis_moved(location.y - old_loc.y, mouse_delta.y);
+ });
BOOST_LOG(tests) << "MoveInputTest:: got updated mouse loc: " << new_loc;
bool has_input_moved = old_loc.x != new_loc.x && old_loc.y != new_loc.y;
@@ -110,9 +145,9 @@ TEST_P(MouseHIDTest, MoveInputTest) {
EXPECT_TRUE(has_input_moved);
- // Verify we moved as much as we requested
- EXPECT_EQ(new_loc.x - old_loc.x, mouse_delta.x);
- EXPECT_EQ(new_loc.y - old_loc.y, mouse_delta.y);
+ // Verify the OS moved in the requested direction. Relative pointer input can be accelerated by host settings.
+ expect_relative_axis_moved(new_loc.x - old_loc.x, mouse_delta.x);
+ expect_relative_axis_moved(new_loc.y - old_loc.y, mouse_delta.y);
}
TEST_P(MouseHIDTest, AbsMoveInputTest) {
@@ -128,13 +163,15 @@ TEST_P(MouseHIDTest, AbsMoveInputTest) {
const auto abs_port = absolute_mouse_test_port();
const auto expected_pos = expected_absolute_mouse_location(mouse_pos, abs_port);
+ BOOST_LOG(tests) << "AbsMoveInputTest:: touch port: " << abs_port.offset_x << 'x' << abs_port.offset_y << ' ' << abs_port.width << 'x' << abs_port.height;
BOOST_LOG(tests) << "AbsMoveInputTest:: move: " << mouse_pos;
platf::abs_mouse(input, abs_port, mouse_pos.x, mouse_pos.y);
- std::this_thread::sleep_for(std::chrono::milliseconds(200));
BOOST_LOG(tests) << "AbsMoveInputTest:: moved: " << mouse_pos;
BOOST_LOG(tests) << "AbsMoveInputTest:: get updated mouse loc";
- auto new_loc = platf::get_mouse_loc(input);
+ auto new_loc = wait_for_mouse_location(input, [&expected_pos](const auto &location) {
+ return is_near(location.x, expected_pos.x) && is_near(location.y, expected_pos.y);
+ });
BOOST_LOG(tests) << "AbsMoveInputTest:: got updated mouse loc: " << new_loc;
bool has_input_moved = old_loc.x != new_loc.x || old_loc.y != new_loc.y;
@@ -146,6 +183,6 @@ TEST_P(MouseHIDTest, AbsMoveInputTest) {
}
// Verify we moved to the absolute coordinate
- EXPECT_EQ(new_loc.x, expected_pos.x);
- EXPECT_EQ(new_loc.y, expected_pos.y);
+ EXPECT_NEAR(new_loc.x, expected_pos.x, mouse_position_tolerance);
+ EXPECT_NEAR(new_loc.y, expected_pos.y, mouse_position_tolerance);
}
From 67787f0e181667bde6a6c6a42a5dc4f4e0aaf134 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 8 Jul 2026 11:00:32 -0400
Subject: [PATCH 10/10] Return optional from get_mouse_loc
Change `get_mouse_loc()` to return `std::optional` instead of a sentinel value, properly indicating when the mouse location cannot be observed on a platform. Update all platform implementations (Linux, macOS, Windows) and tests to handle the optional return type. Add libxtst-dev dependency to Linux CI workflow.
---
.github/workflows/ci-linux.yml | 3 +-
src/platform/common.h | 9 ++++--
src/platform/linux/input/virtualhid.cpp | 10 +++---
src/platform/macos/input.cpp | 6 ++--
src/platform/windows/input.cpp | 4 +--
tests/unit/test_mouse.cpp | 43 +++++++++++++++++--------
6 files changed, 48 insertions(+), 27 deletions(-)
diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml
index 18f1dee3cce..939d67f73e2 100644
--- a/.github/workflows/ci-linux.yml
+++ b/.github/workflows/ci-linux.yml
@@ -59,7 +59,8 @@ jobs:
libwayland-dev \
libx11-xcb-dev \
libxcb-dri3-dev \
- libxfixes-dev
+ libxfixes-dev \
+ libxtst-dev
- name: Build latest libva
env:
diff --git a/src/platform/common.h b/src/platform/common.h
index c6884d5474a..ebd7e42d8ae 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
// lib includes
@@ -1105,16 +1106,18 @@ namespace platf {
* @brief Get the current mouse position for platform input tests.
*
* @param input The input_t instance to use.
- * @return Screen coordinates of the mouse.
+ * @return Screen coordinates of the mouse, or `std::nullopt` when the platform cannot observe the cursor.
*
* @note This helper exists only so tests can observe virtual mouse movement. Production input paths should submit
* mouse events through `move_mouse()` or `abs_mouse()` instead of reading the host cursor location.
*
* @examples
- * auto [x, y] = get_mouse_loc(input);
+ * if (auto location = get_mouse_loc(input)) {
+ * auto [x, y] = *location;
+ * }
* @examples_end
*/
- util::point_t get_mouse_loc(input_t &input);
+ std::optional get_mouse_loc(input_t &input);
/**
* @brief Move mouse using the backend coordinate system.
*
diff --git a/src/platform/linux/input/virtualhid.cpp b/src/platform/linux/input/virtualhid.cpp
index 896f79310ee..54d5716792e 100644
--- a/src/platform/linux/input/virtualhid.cpp
+++ b/src/platform/linux/input/virtualhid.cpp
@@ -142,11 +142,11 @@ namespace platf {
return caps;
}
- util::point_t get_mouse_loc(input_t & /*input*/) {
+ std::optional get_mouse_loc(input_t & /*input*/) {
#ifdef SUNSHINE_BUILD_X11
auto *display = XOpenDisplay(nullptr);
if (!display) {
- return {0, 0};
+ return std::nullopt;
}
const auto root = DefaultRootWindow(display);
@@ -161,15 +161,15 @@ namespace platf {
XCloseDisplay(display);
if (!queried) {
- return {0, 0};
+ return std::nullopt;
}
- return {
+ return util::point_t {
static_cast(root_x),
static_cast