diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml
index dcc9bce0018..a2fe716ab1c 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:
@@ -178,9 +179,14 @@ jobs:
- name: Sync Python tools
run: |
- uv sync --locked \
- --python "${PYTHON_VERSION}" \
- --no-python-downloads --no-install-project
+ uv_sync_args=(
+ --locked
+ --python "${PYTHON_VERSION}"
+ --no-python-downloads
+ --no-install-project
+ )
+ # uv.lock pins external artifacts; reviewed local build hooks are required for gcovr.
+ uv sync "${uv_sync_args[@]}" # NOSONAR(githubactions:S8541)
- name: Run tests
id: test
diff --git a/.gitmodules b/.gitmodules
index 1355d7a85ef..88f8e617fba 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -13,14 +13,14 @@
[submodule "third-party/glad"]
path = third-party/glad
url = https://github.com/Dav1dde/glad.git
-[submodule "third-party/inputtino"]
- path = third-party/inputtino
- url = https://github.com/games-on-whales/inputtino.git
- branch = stable
[submodule "third-party/libdisplaydevice"]
path = third-party/libdisplaydevice
url = https://github.com/LizardByte/libdisplaydevice.git
branch = master
+[submodule "third-party/libvirtualhid"]
+ path = third-party/libvirtualhid
+ url = https://github.com/LizardByte/libvirtualhid.git
+ branch = master
[submodule "third-party/lizardbyte-common"]
path = third-party/lizardbyte-common
url = https://github.com/LizardByte/lizardbyte-common.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f6019b50180..7d75954efe0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,8 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
# `CMAKE_CUDA_ARCHITECTURES` requires 3.18
# `set_source_files_properties` requires 3.18
# `cmake_path(CONVERT ... TO_NATIVE_PATH_LIST ...)` requires 3.20
+# `third-party/libvirtualhid` requires 3.24
# todo - set this conditionally
project(Sunshine VERSION 0.0.0
diff --git a/README.md b/README.md
index bc1fed99602..ccb5c3b3794 100644
--- a/README.md
+++ b/README.md
@@ -52,43 +52,60 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz
Clients may support other gamepads.
+
+ | Generic |
+ 🟡1 |
+ ✅ |
+ ❌ |
+ ✅ |
+
| DualShock / DS4 (PlayStation 4) |
- ➖ |
- ➖ |
+ 🟡1 |
+ ✅ |
❌ |
✅ |
| DualSense / DS5 (PlayStation 5) |
- ❌ |
+ 🟡1 |
✅ |
❌ |
- ❌ |
+ ✅ |
| Nintendo Switch Pro |
- ✅ |
+ 🟡1 |
✅ |
❌ |
- ❌ |
+ ✅ |
| Xbox 360 |
- ➖ |
- ➖ |
+ 🟡1 |
+ ✅ |
❌ |
✅ |
- | Xbox One/Series |
- ✅ |
+ Xbox One |
+ 🟡1 |
✅ |
❌ |
+ ✅ |
+
+
+ | Xbox Series |
+ 🟡1 |
+ ✅ |
❌ |
+ ✅ |
+> [!NOTE]
+> 1 Missing motion, touchpad input, battery state, RGB LEDs, adaptive triggers, and raw HID output reports.
+
Encoding API
@@ -381,7 +398,7 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz
| OS |
- FreeBSD: 14.4+ |
+ FreeBSD: 15.1+ |
| Linux/Debian: 13+ (trixie) |
@@ -396,7 +413,7 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz
macOS: 14.2+ |
- | Windows: 11+ (Windows Server does not support virtual gamepads) |
+ Windows: 11+ |
| Network |
diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake
index 7e733bb94e5..c662a00193b 100644
--- a/cmake/compile_definitions/common.cmake
+++ b/cmake/compile_definitions/common.cmake
@@ -59,6 +59,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()
+
set(NVENC_PUBLIC_SOURCES
"${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_config.h"
"${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_d3d11_interface.h"
diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake
index ed5133377bc..485e419adb5 100644
--- a/cmake/compile_definitions/linux.cmake
+++ b/cmake/compile_definitions/linux.cmake
@@ -296,24 +296,12 @@ if(NOT ${CUDA_FOUND}
message(FATAL_ERROR "Couldn't find either cuda, libdrm, libva, kwin, pipewire, portal, wayland or x11")
endif()
-# These need to be set before adding the inputtino subdirectory in order for them to be picked up
+# These need to be set before 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}")
-if(FREEBSD)
- set(USE_UHID OFF)
-endif()
-
-add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/inputtino")
-list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino)
-file(GLOB_RECURSE INPUTTINO_SOURCES
- ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.h
- ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.cpp)
-list(APPEND PLATFORM_TARGET_FILES ${INPUTTINO_SOURCES})
-# build libevdev before the libinputtino target
-if(EXTERNAL_PROJECT_LIBEVDEV_USED)
- add_dependencies(libinputtino libevdev)
-endif()
+list(APPEND PLATFORM_TARGET_FILES
+ "${CMAKE_SOURCE_DIR}/src/platform/linux/input/virtualhid.cpp")
# AppImage and Flatpak
if (${SUNSHINE_BUILD_APPIMAGE})
diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake
index f255d38ab67..a7c18bde3d0 100644
--- a/cmake/compile_definitions/windows.cmake
+++ b/cmake/compile_definitions/windows.cmake
@@ -62,11 +62,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/common.cmake b/cmake/packaging/common.cmake
index 776266f70ee..980138c2266 100644
--- a/cmake/packaging/common.cmake
+++ b/cmake/packaging/common.cmake
@@ -36,6 +36,14 @@ configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/assets/web/images/logo-sunshine.svg"
COPYONLY)
+# Copy the Virtual HID Driver icon for Windows tray notifications.
+if(WIN32)
+ configure_file(
+ "${CMAKE_SOURCE_DIR}/third-party/libvirtualhid/libvirtualhid.svg"
+ "${CMAKE_CURRENT_BINARY_DIR}/assets/web/images/logo-libvirtualhid.svg"
+ COPYONLY)
+endif()
+
# install built vite assets
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets/web"
DESTINATION "${SUNSHINE_ASSETS_DIR}")
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/configuration.md b/docs/configuration.md
index e03d9eccedf..f43966d6b1f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -317,6 +317,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
Description |
The type of gamepad to emulate on the host.
+ @note{This option applies to FreeBSD, Linux, and Windows.}
|
@@ -332,30 +333,33 @@ editing the `conf` file in a text editor. Use the examples as reference.
@endcode
- | Choices |
+ Choices |
+ generic |
+ Generic HID gamepad |
+
+
| ds4 |
- DualShock 4 controller (PS4)
- @note{This option applies to Windows only.} |
+ DualShock 4 controller (PS4) |
| ds5 |
- DualShock 5 controller (PS5)
- @note{This option applies to FreeBSD and Linux only.} |
+ DualShock 5 controller (PS5) |
| switch |
- Switch Pro controller
- @note{This option applies to FreeBSD and Linux only.} |
+ Switch Pro controller |
| x360 |
- Xbox 360 controller
- @note{This option applies to Windows only.} |
+ Xbox 360 controller |
| xone |
- Xbox One controller
- @note{This option applies to FreeBSD and Linux only.} |
+ Xbox One controller |
+
+
+ | xseries |
+ Xbox Series controller |
@@ -365,9 +369,9 @@ editing the `conf` file in a text editor. Use the examples as reference.
| Description |
- Allow Select/Back inputs to also trigger DS4 touchpad click. Useful for clients looking to
- emulate touchpad click on Xinput devices.
- @hint{Only applies when gamepad is set to ds4 manually. Unused in other gamepad modes.}
+ Allow Select/Back inputs to also trigger a PlayStation-style gamepad touchpad click. Useful
+ for clients looking to emulate touchpad click on XInput devices.
+ @hint{Applies to ds4, ds5, and automatically selected PlayStation-style gamepads.}
|
@@ -391,7 +395,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
| Description |
If a client reports that a connected gamepad has motion sensor support, emulate it on the
- host as a DS4 controller.
+ host as a PlayStation-style controller.
When disabled, motion sensors will not be taken into account during gamepad type selection.
@@ -418,8 +422,8 @@ editing the `conf` file in a text editor. Use the examples as reference.
|
| Description |
- If a client reports that a connected gamepad has a touchpad, emulate it on the host
- as a DS4 controller.
+ If a client reports that a connected gamepad has a touchpad, emulate it on the host as a
+ PlayStation-style controller.
When disabled, touchpad presence will not be taken into account during gamepad type selection.
@@ -440,14 +444,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 +462,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
| Example |
@code{}
- ds5_inputtino_randomize_mac = enabled
+ virtualhid_randomize_mac = enabled
@endcode |
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 34429447f5d..062875653fd 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -474,11 +474,21 @@ 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
-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
+[Virtual HID Driver](https://github.com/LizardByte/libvirtualhid/releases/latest) 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.
-
+Compared with the ViGEmBus fallback, Virtual HID Driver can create Xbox One, Xbox Series, DualSense, Nintendo Switch
+Pro, and Generic gamepads in addition to Xbox 360 and DualShock 4. It can also expose controller-specific features such
+as motion, touchpads, LEDs, and adaptive triggers when supported. Virtual HID Driver is actively developed and
+supported by the LizardByte team.
+
+The Virtual HID Driver also requires an active machine license. Sunshine shows the current license status and actions
+on the Web UI Troubleshooting page and in the **Virtual HID Driver** system tray submenu. When Sunshine starts on an
+unactivated machine, select its tray notification to open the activation and purchase options in the Web UI.
+
+After installing or updating virtual input drivers, it is recommended to restart your computer.
## Usage
@@ -612,7 +622,8 @@ All shortcuts start with `Ctrl+Alt+Shift`, just like Moonlight.
The following are known limitations.
* Only X11 and Wayland capture are supported
- * DualSense/DS5 emulation is not available due to missing uhid features
+ * Gamepads use libvirtualhid's uinput backend, so descriptor-driven features such as motion, touchpad input,
+ battery state, RGB LEDs, adaptive triggers, and raw HID output reports are unavailable
### HDR Support
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 55495d04544..3e0785a5411 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -205,7 +205,7 @@ If needed, you can override it manually in your systemd service file or shell en
When the seat is not `seat0`, Sunshine appends the seat name to its virtual device names, for example:
- Keyboard passthrough (seat1)
-- Sunshine PS5 (virtual) pad (seat1)
+- Sunshine (libvirtualhid) PS5 Controller (seat1)
Sunshine creates two mouse devices: a relative one and an absolute one.
@@ -290,10 +290,19 @@ 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.
-
-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
+[Virtual HID Driver](https://github.com/LizardByte/libvirtualhid/releases/latest) 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 the [ViGEmBus fallback](https://github.com/nefarius/ViGEmBus/releases/latest), you must use
+version 1.17 or newer.
+
+Virtual HID Driver adds Xbox One, Xbox Series, DualSense, Nintendo Switch Pro, and Generic gamepads, plus advanced
+controller features such as motion, touchpads, LEDs, and adaptive triggers when supported. Unlike the discontinued
+ViGEmBus project, Virtual HID Driver is actively developed and supported by the LizardByte team.
+
+An active Virtual HID Driver machine license is required before Sunshine can create libvirtualhid gamepads. Follow
+the warning on the Web UI home page, the startup tray notification, or the **Virtual HID Driver** tray submenu to open
+the license section on the Troubleshooting page, where you can activate a key or follow the purchase link.
After installation, it is recommended to restart your computer.
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/audio.cpp b/src/audio.cpp
index 5f4c9e43a17..28a3d810df9 100644
--- a/src/audio.cpp
+++ b/src/audio.cpp
@@ -54,7 +54,7 @@ namespace audio {
2,
1,
1,
- platf::speaker::map_stereo,
+ platf::speaker::map_stereo.data(),
96000,
},
{
@@ -62,7 +62,7 @@ namespace audio {
2,
1,
1,
- platf::speaker::map_stereo,
+ platf::speaker::map_stereo.data(),
512000,
},
{
@@ -70,7 +70,7 @@ namespace audio {
6,
4,
2,
- platf::speaker::map_surround51,
+ platf::speaker::map_surround51.data(),
256000,
},
{
@@ -78,7 +78,7 @@ namespace audio {
6,
6,
0,
- platf::speaker::map_surround51,
+ platf::speaker::map_surround51.data(),
1536000,
},
{
@@ -86,7 +86,7 @@ namespace audio {
8,
5,
3,
- platf::speaker::map_surround71,
+ platf::speaker::map_surround71.data(),
450000,
},
{
@@ -94,7 +94,7 @@ namespace audio {
8,
8,
0,
- platf::speaker::map_surround71,
+ platf::speaker::map_surround71.data(),
2048000,
},
};
diff --git a/src/config.cpp b/src/config.cpp
index 5392bca2ee0..990a8ff45ac 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -33,6 +33,15 @@
#include
#endif
+#if defined(_WIN32) && !defined(DOXYGEN)
+ #ifdef _GLIBCXX_USE_C99_INTTYPES
+ #undef _GLIBCXX_USE_C99_INTTYPES
+ #endif
+ #include
+ #include
+ #include
+#endif
+
#if !defined(__ANDROID__) && !defined(__APPLE__)
// For NVENC legacy constants
#include
@@ -140,13 +149,6 @@ namespace config {
constexpr int AMF_VIDEO_ENCODER_UNDEFINED = 0; ///< Fallback AMF enum value for undefined.
constexpr int AMF_VIDEO_ENCODER_CABAC = 1; ///< Fallback AMF enum value for cabac.
constexpr int AMF_VIDEO_ENCODER_CALV = 2; ///< Fallback AMF enum value for calv.
-#else
- #ifdef _GLIBCXX_USE_C99_INTTYPES
- #undef _GLIBCXX_USE_C99_INTTYPES
- #endif
- #include
- #include
- #include
#endif
/**
@@ -847,10 +849,10 @@ namespace config {
platf::supported_gamepads(nullptr).front().name.data(),
platf::supported_gamepads(nullptr).front().name.size(),
}, // Default gamepad
- 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, // back as touchpad click enabled for PlayStation-style gamepads
+ true, // client gamepads with motion events use PlayStation-style emulation
+ true, // client gamepads with touchpads use PlayStation-style emulation
+ true, // virtualhid_randomize_mac
true, // keyboard enabled
true, // mouse enabled
@@ -1163,6 +1165,23 @@ namespace config {
input = temp.string();
}
+ /**
+ * @brief Parse a decimal or hexadecimal integer configuration value.
+ *
+ * @param value Raw configuration value, optionally surrounded by quotes.
+ * @return Parsed integer value.
+ */
+ int parse_config_integer(std::string_view value) {
+ if (value.size() >= 2 && value.front() == '"') {
+ value = value.substr(1, value.size() - 2);
+ }
+
+ if (value.starts_with("0x"sv)) {
+ return util::from_hex(value.substr(2));
+ }
+ return static_cast(util::from_view(value));
+ }
+
/**
* @brief Consume an integer setting from decimal or hexadecimal configuration text.
*
@@ -1177,20 +1196,7 @@ namespace config {
return;
}
- std::string_view val = it->second;
-
- // If value is something like: "756" instead of 756
- if (val.size() >= 2 && val[0] == '"') {
- val = val.substr(1, val.size() - 2);
- }
-
- // If that integer is in hexadecimal
- if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
- input = util::from_hex(val.substr(2));
- } else {
- input = (int) util::from_view(val);
- }
-
+ input = parse_config_integer(it->second);
vars.erase(it);
}
@@ -1208,20 +1214,7 @@ namespace config {
return;
}
- std::string_view val = it->second;
-
- // If value is something like: "756" instead of 756
- if (val.size() >= 2 && val[0] == '"') {
- val = val.substr(1, val.size() - 2);
- }
-
- // If that integer is in hexadecimal
- if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
- input = util::from_hex(val.substr(2));
- } else {
- input = util::from_view(val);
- }
-
+ input = parse_config_integer(it->second);
vars.erase(it);
}
@@ -1792,7 +1785,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 2795cf17958..836c1d24b7a 100644
--- a/src/config.h
+++ b/src/config.h
@@ -275,10 +275,10 @@ namespace config {
std::chrono::duration key_repeat_period; ///< Interval between repeated keyboard key events.
std::string gamepad; ///< Virtual controller backend selected by configuration.
- 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 ds4_back_as_touchpad_click; ///< Map Back/Select to touchpad click for PlayStation-style gamepads.
+ bool motion_as_ds4; ///< Prefer PlayStation-style emulation for client gamepads with motion controls.
+ bool touchpad_as_ds4; ///< Prefer PlayStation-style emulation for client gamepads with touchpad input.
+ 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/confighttp.cpp b/src/confighttp.cpp
index b04b1b6e587..eae0b108345 100644
--- a/src/confighttp.cpp
+++ b/src/confighttp.cpp
@@ -8,23 +8,30 @@
// standard includes
#include
+#include
#include
#include
#include
+#include
+#include
#include
+#include
+#include
// lib includes
#include
#include
#include
+#include
#include
#include
#include
#ifdef _WIN32
+ #include "platform/virtualhid_input.h"
#include "platform/windows/misc.h"
+ #include "platform/windows/utf_utils.h"
- #include
#include
#endif
@@ -42,6 +49,7 @@
#include "platform/common.h"
#include "process.h"
#include "rtsp.h"
+#include "system_tray.h"
#include "utility.h"
#include "uuid.h"
@@ -72,6 +80,28 @@ namespace confighttp {
*/
using https_handler_t = std::function;
+ namespace {
+ using license_status_provider_t = std::function; ///< Provider for the current libvirtualhid license status.
+
+ /**
+ * @brief Return the current libvirtualhid license status provider.
+ *
+ * Unit-test builds expose a mutable provider so the HTTP fixture can avoid
+ * contacting an installed Windows broker. Production builds keep the
+ * provider const and always call libvirtualhid directly.
+ *
+ * @return License status provider for the current build.
+ */
+ auto &virtual_input_license_status_provider() {
+#ifdef SUNSHINE_TESTS
+ static license_status_provider_t status_provider = lvh::get_license_status;
+#else
+ static const license_status_provider_t status_provider = lvh::get_license_status;
+#endif
+ return status_provider;
+ }
+ } // namespace
+
/**
* @brief Client certificate operations accepted by the configuration API.
*/
@@ -80,6 +110,48 @@ namespace confighttp {
REMOVE ///< Remove client
};
+ /**
+ * @brief Overwrite a request-local sensitive string when leaving scope.
+ */
+ class scoped_sensitive_string_clear_t {
+ public:
+ /**
+ * @brief Register a sensitive string for best-effort clearing.
+ *
+ * @param value Mutable sensitive string.
+ */
+ explicit scoped_sensitive_string_clear_t(std::string &value):
+ value_ {value} {}
+
+ scoped_sensitive_string_clear_t(const scoped_sensitive_string_clear_t &) = delete;
+ scoped_sensitive_string_clear_t &operator=(const scoped_sensitive_string_clear_t &) = delete;
+
+ /**
+ * @brief Overwrite and clear the registered string.
+ */
+ ~scoped_sensitive_string_clear_t() {
+ std::fill(value_.begin(), value_.end(), '\0');
+ value_.clear();
+ }
+
+ private:
+ std::string &value_; ///< Sensitive request-local string.
+ };
+
+#ifdef SUNSHINE_TESTS
+ void set_virtual_input_license_status_provider_for_testing(virtual_input_license_status_provider_t status_provider) {
+ virtual_input_license_status_provider() = std::move(status_provider);
+ }
+
+ void reset_virtual_input_license_status_provider_for_testing() {
+ virtual_input_license_status_provider() = lvh::get_license_status;
+ }
+
+ void clear_sensitive_string_for_testing(std::string &value) {
+ const scoped_sensitive_string_clear_t clear_value {value};
+ }
+#endif
+
// CSRF token management
/**
* @brief CSRF token value and its expiration deadline.
@@ -102,6 +174,302 @@ 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. // NOSONAR(cpp:S1313): not an IP address
+
+ /**
+ * @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.has_value()) {
+ 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;
+ }
+
+ /**
+ * @brief Return a stable Web UI name for a libvirtualhid license state.
+ *
+ * @param state License state.
+ * @return Lowercase state name.
+ */
+ std::string_view virtualhid_license_state_name(lvh::LicenseState state) {
+ using enum lvh::LicenseState;
+
+ switch (state) {
+ case unlicensed:
+ return "unlicensed";
+ case licensed:
+ return "licensed";
+ case expired:
+ return "expired";
+ case disabled:
+ return "disabled";
+ case invalid:
+ return "invalid";
+ case unavailable:
+ default:
+ return "unavailable";
+ }
+ }
+
+ nlohmann::json build_virtualhid_license_status(const lvh::LicenseResult &result) {
+ const auto &license = result.license;
+ nlohmann::json output_tree;
+ output_tree["operation_ok"] = result.status.ok();
+ output_tree["service_available"] = license.service_available;
+ output_tree["state"] = virtualhid_license_state_name(license.state);
+ output_tree["licensed"] = license.licensed();
+ output_tree["active_devices"] = license.active_devices;
+ output_tree["activation_limit"] = license.activation_limit;
+ output_tree["activation_usage"] = license.activation_usage;
+ output_tree["plan_name"] = license.plan_name;
+ output_tree["customer_email"] = license.customer_email;
+ output_tree["expires_at"] = license.expires_at;
+ output_tree["message"] = license.message;
+ output_tree["purchase_url"] = license.purchase_url;
+ output_tree["manage_account_url"] = license.manage_account_url;
+ output_tree["error"] = result.status.ok() ? "" : result.status.message();
+ return output_tree;
+ }
+
+ namespace {
+ /**
+ * @brief Handle a virtual-input license request using the configured status provider.
+ *
+ * @param response HTTP response object.
+ * @param request Authenticated HTTP request.
+ */
+ void get_virtual_input_license(const resp_https_t &response, const req_https_t &request) {
+ if (!authenticate(response, request)) {
+ return;
+ }
+
+ print_req(request);
+ send_response(response, build_virtualhid_license_status(virtual_input_license_status_provider()()));
+ }
+ } // namespace
+
+#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) {
+ std::wstring subkey_name(256, L'\0');
+ auto subkey_name_size = static_cast(subkey_name.size());
+ const auto enum_status = RegEnumKeyExW(root_key.get(), index, subkey_name.data(), &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, 0, 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,131 +1799,162 @@ 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 runtime_error_message;
+
+ 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::bad_alloc &exception) {
+ runtime_error_message = exception.what();
}
- print_req(request);
+ output_tree["backend_name"] = backend_name;
+ output_tree["requires_installed_driver"] = requires_installed_driver;
+ if (!runtime_error_message.empty()) {
+ output_tree["error"] = runtime_error_message;
+ }
+#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);
- }
- }
+ std::string system_root;
+ if (!lizardbyte::common::get_env("SystemRoot", system_root)) {
+ system_root = "C:\\Windows";
+ }
+ const std::filesystem::path driver_path = std::filesystem::path(system_root) / "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;
+ output_tree["virtualhid"] = get_virtualhid_driver_status();
+ output_tree["vigembus"] = get_vigembus_driver_status();
+ send_response(response, 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";
+ /**
+ * @brief Get the current libvirtualhid machine license status.
+ *
+ * @param response HTTP response object.
+ * @param request Authenticated HTTP request.
+ *
+ * @api_examples{/api/virtual-input/license| GET| null}
+ */
+ void getVirtualInputLicense(const resp_https_t &response, const req_https_t &request) {
+ get_virtual_input_license(response, request);
+ }
- if (!std::filesystem::exists(installer_path)) {
- output_tree["status"] = false;
- output_tree["error"] = "ViGEmBus installer not found";
- send_response(response, output_tree);
+ /**
+ * @brief Activate, validate, or deactivate the libvirtualhid machine license.
+ *
+ * Submitted license keys are used only for the synchronous broker call. They are
+ * never logged or saved in Sunshine's configuration, and extracted mutable copies
+ * are overwritten before the handler returns.
+ *
+ * @param response HTTP response object.
+ * @param request Authenticated HTTP request with a JSON action.
+ *
+ * @api_examples{/api/virtual-input/license| POST| {"action":"validate"}}
+ */
+ void updateVirtualInputLicense(const resp_https_t &response, const req_https_t &request) {
+ if (!authenticate(response, request)) {
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);
+ const auto client_id = get_client_id(request);
+ if (!validate_csrf_token(response, request, client_id)) {
return;
}
- // Wait for the installer to complete
- child.wait(ec);
+ print_req(request);
+ try {
+ std::stringstream content;
+ content << request->content.rdbuf();
+ auto input_tree = nlohmann::json::parse(content);
+ const auto action = input_tree.value("action", "");
+
+ lvh::LicenseResult result;
+ if (action == "activate") {
+ auto license_key = input_tree.value("license_key", "");
+ input_tree["license_key"] = "";
+ if (license_key.empty()) {
+ bad_request(response, request, "License key is required");
+ return;
+ }
- 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);
+ const scoped_sensitive_string_clear_t clear_license_key {license_key};
+ result = lvh::activate_license(license_key);
+ } else if (action == "validate") {
+ result = lvh::validate_license();
+ } else if (action == "deactivate") {
+ result = lvh::deactivate_license();
+ } else {
+ bad_request(response, request, "Unknown license action");
+ return;
}
- }
-#else
- output_tree["status"] = false;
- output_tree["error"] = "ViGEmBus installation is only available on Windows";
-#endif
- send_response(response, output_tree);
+#if defined(_WIN32) && defined(SUNSHINE_TRAY) && SUNSHINE_TRAY >= 1
+ system_tray::update_tray_virtualhid_license(result.license, false);
+#endif
+ send_response(response, build_virtualhid_license_status(result));
+ } catch (const nlohmann::json::exception &) {
+ bad_request(response, request, "Invalid license request");
+ }
}
/**
@@ -1812,8 +2211,9 @@ 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/license$"]["GET"] = getVirtualInputLicense;
+ server.resource["^/api/virtual-input/license$"]["POST"] = updateVirtualInputLicense;
+ 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..d096836fc53 100644
--- a/src/confighttp.h
+++ b/src/confighttp.h
@@ -6,10 +6,13 @@
// standard includes
#include
+#include
#include
#include
+#include
// lib includes
+#include
#include
#include
@@ -75,6 +78,79 @@ 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);
+
+ /**
+ * @brief Convert a libvirtualhid license result into a Web UI response.
+ *
+ * @param result License operation result.
+ * @return License status JSON object without the submitted license key.
+ */
+ nlohmann::json build_virtualhid_license_status(const lvh::LicenseResult &result);
+
+ /**
+ * @brief Build libvirtualhid driver version and installation status.
+ *
+ * @return libvirtualhid driver status JSON.
+ */
+ nlohmann::json get_virtualhid_driver_status();
+
+ /**
+ * @brief Build ViGEmBus fallback driver version and installation status.
+ *
+ * @return ViGEmBus fallback driver status JSON.
+ */
+ nlohmann::json get_vigembus_driver_status();
+
+ void getVirtualInputStatus(const resp_https_t &response, const req_https_t &request);
+
+ void getVirtualInputLicense(const resp_https_t &response, const req_https_t &request);
+
+ void updateVirtualInputLicense(const resp_https_t &response, const req_https_t &request);
+
+#ifdef SUNSHINE_TESTS
+ using virtual_input_license_status_provider_t = std::function; ///< Test provider for current libvirtualhid license status.
+
+ /**
+ * @brief Replace the virtual-input license status provider for unit tests.
+ *
+ * @param status_provider Provider returning the license status for the response.
+ */
+ void set_virtual_input_license_status_provider_for_testing(virtual_input_license_status_provider_t status_provider);
+
+ /**
+ * @brief Restore the production virtual-input license status provider after a unit test.
+ */
+ void reset_virtual_input_license_status_provider_for_testing();
+
+ /**
+ * @brief Exercise request-local sensitive string clearing for unit tests.
+ *
+ * @param value Mutable sensitive string to overwrite and clear.
+ */
+ void clear_sensitive_string_for_testing(std::string &value);
+#endif
+
// Browse helper functions (also exposed for unit testing)
/**
* @brief Checks whether a directory entry qualifies as an executable file.
diff --git a/src/input.cpp b/src/input.cpp
index 270409a3be2..4f1c7e57c33 100644
--- a/src/input.cpp
+++ b/src/input.cpp
@@ -9,10 +9,12 @@ extern "C" {
}
// standard includes
+#include
#include
#include
#include
#include
+#include
#include
#include
@@ -187,7 +189,7 @@ namespace input {
~gamepad_t() {
if (id >= 0) {
task_pool.push([id = this->id]() {
- free_gamepad(platf_input, id);
+ ::input::free_gamepad(platf_input, id);
});
}
}
@@ -610,8 +612,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
@@ -1037,7 +1039,7 @@ namespace input {
*
* @param packet Protocol packet being processed.
*/
- void passthrough(PNV_UNICODE_PACKET packet) {
+ void passthrough(const NV_UNICODE_PACKET *packet) {
if (!config::input.keyboard) {
return;
}
@@ -1112,54 +1114,81 @@ namespace input {
}
/**
- * @brief Called to pass a touch message to the platform backend.
- * @param input The input context pointer.
- * @param packet The touch packet.
+ * @brief Shared normalized data prepared for a touch or pen event.
*/
- void passthrough(std::shared_ptr &input, PSS_TOUCH_PACKET packet) {
+ struct absolute_pointer_data_t {
+ platf::touch_port_t touch_port; ///< Monitor-local touch port.
+ std::pair coords; ///< Normalized monitor-local coordinates.
+ std::uint16_t rotation; ///< Normalized rotation in degrees.
+ std::pair contact_area; ///< Scaled major and minor contact axes.
+ };
+
+ /**
+ * @brief Normalize the fields shared by touch and pen packets.
+ *
+ * @tparam Packet Pointer type for a Moonlight touch or pen packet.
+ * @param input Input context that supplies the current touch-port metadata.
+ * @param packet Touch or pen packet to normalize.
+ * @return Normalized pointer data, or `std::nullopt` when input is disabled or dimensions are invalid.
+ */
+ template
+ std::optional prepare_absolute_pointer_data(std::shared_ptr &input, Packet packet) {
if (!config::input.mouse) {
- return;
+ return std::nullopt;
}
- // Convert the client normalized coordinates to touchport coordinates
- auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f});
+ auto coords = client_to_touchport(
+ input,
+ {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f},
+ {65535.f, 65535.f}
+ );
if (!coords) {
- return;
+ return std::nullopt;
}
- auto &touch_port = input->touch_port;
-
- auto abs_port = monitor_touch_port(touch_port, *coords);
- if (!abs_port) {
- return;
+ auto touch_port = monitor_touch_port(input->touch_port, *coords);
+ if (!touch_port) {
+ return std::nullopt;
}
- // Normalize rotation value to 0-359 degree range
auto rotation = util::endian::little(packet->rotation);
if (rotation != LI_ROT_UNKNOWN) {
rotation %= 360;
}
- // Normalize the contact area based on the touchport
- auto contact_area = scale_client_contact_area(
+ const auto contact_area = scale_client_contact_area(
{from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f,
from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f},
rotation,
- {abs_port->width / 65535.f, abs_port->height / 65535.f}
+ {touch_port->width / 65535.f, touch_port->height / 65535.f}
);
+ return absolute_pointer_data_t {*touch_port, *coords, rotation, contact_area};
+ }
+
+ /**
+ * @brief Called to pass a touch message to the platform backend.
+ * @param input The input context pointer.
+ * @param packet The touch packet.
+ */
+ void passthrough(std::shared_ptr &input, PSS_TOUCH_PACKET packet) {
+ const auto pointer_data = prepare_absolute_pointer_data(input, packet);
+ if (!pointer_data) {
+ return;
+ }
+
platf::touch_input_t touch {
packet->eventType,
- rotation,
+ pointer_data->rotation,
util::endian::little(packet->pointerId),
- coords->first,
- coords->second,
+ pointer_data->coords.first,
+ pointer_data->coords.second,
from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f),
- contact_area.first,
- contact_area.second,
+ pointer_data->contact_area.first,
+ pointer_data->contact_area.second,
};
- platf::touch_update(input->client_context.get(), *abs_port, touch);
+ platf::touch_update(input->client_context.get(), pointer_data->touch_port, touch);
}
/**
@@ -1168,51 +1197,25 @@ namespace input {
* @param packet The pen packet.
*/
void passthrough(std::shared_ptr &input, PSS_PEN_PACKET packet) {
- if (!config::input.mouse) {
- return;
- }
-
- // Convert the client normalized coordinates to touchport coordinates
- auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f});
- if (!coords) {
+ const auto pointer_data = prepare_absolute_pointer_data(input, packet);
+ if (!pointer_data) {
return;
}
- auto &touch_port = input->touch_port;
-
- auto abs_port = monitor_touch_port(touch_port, *coords);
- if (!abs_port) {
- return;
- }
-
- // Normalize rotation value to 0-359 degree range
- auto rotation = util::endian::little(packet->rotation);
- if (rotation != LI_ROT_UNKNOWN) {
- rotation %= 360;
- }
-
- // Normalize the contact area based on the touchport
- auto contact_area = scale_client_contact_area(
- {from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f,
- from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f},
- rotation,
- {abs_port->width / 65535.f, abs_port->height / 65535.f}
- );
-
platf::pen_input_t pen {
packet->eventType,
packet->toolType,
packet->penButtons,
packet->tilt,
- rotation,
- coords->first,
- coords->second,
+ pointer_data->rotation,
+ pointer_data->coords.first,
+ pointer_data->coords.second,
from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f),
- contact_area.first,
- contact_area.second,
+ pointer_data->contact_area.first,
+ pointer_data->contact_area.second,
};
- platf::pen_update(input->client_context.get(), *abs_port, pen);
+ platf::pen_update(input->client_context.get(), pointer_data->touch_port, pen);
}
/**
@@ -1345,7 +1348,7 @@ namespace input {
gamepad.id = id;
} else if (!(packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id >= 0) {
// If this is the final event for a gamepad being removed, free the gamepad and return.
- free_gamepad(platf_input, gamepad.id);
+ ::input::free_gamepad(platf_input, gamepad.id);
gamepad.id = -1;
return;
}
@@ -1782,7 +1785,7 @@ namespace input {
passthrough(input, (PNV_KEYBOARD_PACKET) payload);
break;
case UTF8_TEXT_EVENT_MAGIC:
- passthrough((PNV_UNICODE_PACKET) payload);
+ passthrough(static_cast(static_cast(payload)));
break;
case MULTI_CONTROLLER_MAGIC_GEN5:
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET) payload);
@@ -1874,14 +1877,10 @@ 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);
- for (auto &gamepad : gamepads) {
- if (gamepad.is_enabled && gamepad.name != "auto") {
- return false;
- }
- }
- return true;
+ const auto &gamepads = platf::supported_gamepads(std::addressof(platf_input));
+ return std::ranges::none_of(gamepads, [](const auto &gamepad) {
+ return gamepad.is_enabled && gamepad.name != "auto";
+ });
}
/**
diff --git a/src/main.cpp b/src/main.cpp
index 78ec6565c93..6530fdbd9a1 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -452,6 +452,7 @@ int main(int argc, char *argv[]) {
if (tray_is_enabled && config::sunshine.system_tray) {
BOOST_LOG(info) << "Starting system tray"sv;
#ifdef _WIN32
+ system_tray::prepare_tray_virtualhid_license();
// TODO: Windows has a weird bug where when running as a service and on the first Windows boot,
// the tray icon would not appear even though Sunshine is running correctly otherwise.
// Restarting the service would allow the icon to appear normally.
diff --git a/src/platform/common.h b/src/platform/common.h
index 9aa7c47d803..c65a7e26ce6 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
// lib includes
@@ -267,14 +268,14 @@ namespace platf {
/**
* @brief Moonlight speaker order for stereo audio.
*/
- constexpr std::uint8_t map_stereo[] {
+ constexpr std::array map_stereo {
FRONT_LEFT,
FRONT_RIGHT
};
/**
* @brief Moonlight speaker order for 5.1 surround audio.
*/
- constexpr std::uint8_t map_surround51[] {
+ constexpr std::array map_surround51 {
FRONT_LEFT,
FRONT_RIGHT,
FRONT_CENTER,
@@ -285,7 +286,7 @@ namespace platf {
/**
* @brief Moonlight speaker order for 7.1 surround audio.
*/
- constexpr std::uint8_t map_surround71[] {
+ constexpr std::array map_surround71 {
FRONT_LEFT,
FRONT_RIGHT,
FRONT_CENTER,
@@ -837,17 +838,22 @@ namespace platf {
virtual ~audio_control_t() = default;
};
+ /**
+ * @brief Platform-specific input backend context.
+ */
+ struct input_raw_t;
+
/**
* @brief Release a platform input backend created by input().
*
- * @param p Pointer passed to the deleter or conversion helper.
+ * @param input Platform input backend to release.
*/
- void freeInput(void *);
+ void freeInput(input_raw_t *input);
/**
* @brief Owning pointer for a platform input backend.
*/
- using input_t = util::safe_ptr;
+ using input_t = util::safe_ptr;
std::filesystem::path appdata();
@@ -1102,14 +1108,21 @@ 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.
+ * @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.
*
@@ -1166,7 +1179,7 @@ namespace platf {
* @param utf8 UTF-8 text submitted by the client.
* @param size Number of bytes or elements requested.
*/
- void unicode(input_t &input, char *utf8, int size);
+ void unicode(input_t &input, const char *utf8, int size);
/**
* @brief Per-client input context allocated by a platform backend.
diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp
index 11fe0555130..bfd3ab77b4b 100644
--- a/src/platform/linux/audio.cpp
+++ b/src/platform/linux/audio.cpp
@@ -481,7 +481,7 @@ namespace platf {
sink.host = sink_name;
if (index.stereo == PA_INVALID_INDEX) {
- index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo));
+ index.stereo = load_null(stereo, speaker::map_stereo.data(), static_cast(speaker::map_stereo.size()));
if (index.stereo == PA_INVALID_INDEX) {
BOOST_LOG(warning) << "Couldn't create virtual sink for stereo: "sv << pa_strerror(pa_context_errno(ctx.get()));
} else {
@@ -490,7 +490,7 @@ namespace platf {
}
if (index.surround51 == PA_INVALID_INDEX) {
- index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51));
+ index.surround51 = load_null(surround51, speaker::map_surround51.data(), static_cast(speaker::map_surround51.size()));
if (index.surround51 == PA_INVALID_INDEX) {
BOOST_LOG(warning) << "Couldn't create virtual sink for surround-51: "sv << pa_strerror(pa_context_errno(ctx.get()));
} else {
@@ -499,7 +499,7 @@ namespace platf {
}
if (index.surround71 == PA_INVALID_INDEX) {
- index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71));
+ index.surround71 = load_null(surround71, speaker::map_surround71.data(), static_cast(speaker::map_surround71.size()));
if (index.surround71 == PA_INVALID_INDEX) {
BOOST_LOG(warning) << "Couldn't create virtual sink for surround-71: "sv << pa_strerror(pa_context_errno(ctx.get()));
} else {
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..f18233d8e79
--- /dev/null
+++ b/src/platform/linux/input/virtualhid.cpp
@@ -0,0 +1,79 @@
+/**
+ * @file src/platform/linux/input/virtualhid.cpp
+ * @brief Definitions for libvirtualhid Unix input handling.
+ */
+
+// standard includes
+#include
+#include
+
+// platform includes
+#ifdef SUNSHINE_BUILD_X11
+ #include
+#endif
+
+// local includes
+#include "src/platform/virtualhid_input.h"
+
+using namespace std::literals;
+
+namespace platf {
+ platform_caps::caps_t get_capabilities() {
+ platform_caps::caps_t caps = 0;
+ const auto runtime = virtualhid::create_runtime();
+ if (!runtime) {
+ return caps;
+ }
+
+ if (const auto &capabilities = runtime->capabilities(); 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;
+ }
+
+ std::optional get_mouse_loc(input_t & /*input*/) {
+#ifdef SUNSHINE_BUILD_X11
+ auto *display = XOpenDisplay(nullptr);
+ if (!display) {
+ return std::nullopt;
+ }
+
+ 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 std::nullopt;
+ }
+
+ return util::point_t {
+ static_cast(root_x),
+ static_cast(root_y)
+ };
+#else
+ return std::nullopt;
+#endif
+ }
+
+ std::vector &supported_gamepads(input_t *input) {
+ static std::vector gamepads;
+ if (!input || !input->get()) {
+ gamepads = virtualhid::static_supported_gamepads();
+ return gamepads;
+ }
+
+ gamepads = virtualhid::supported_gamepads(virtualhid::get_input_context(*input).runtime.get());
+ return gamepads;
+ }
+} // namespace platf
diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp
index fa1227b063e..6ec815a5651 100644
--- a/src/platform/macos/input.cpp
+++ b/src/platform/macos/input.cpp
@@ -1,756 +1,61 @@
/**
* @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.
- */
- 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.
- };
-
- // 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.
- */
- 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;
- }
-
- /**
- * @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.
- };
-
- /**
- * @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);
+ std::optional get_mouse_loc(input_t & /*input*/) {
+ const auto event = CGEventCreate(nullptr);
+ if (!event) {
+ return std::nullopt;
}
- CGEventSetFlags(event, macos_input->kb_flags);
- CGEventPost(kCGSessionEventTap, event);
+ const auto current = CGEventGetLocation(event);
CFRelease(event);
+ return util::point_t {current.x, current.y};
}
- 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;
- }
-
- 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;
- }
-
- // 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
- };
- }
-
- /**
- * @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);
- }
-
- /**
- * @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 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 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 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)));
- }
-
- /**
- * @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);
- }
-
- /**
- * @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;
+ platform_caps::caps_t get_capabilities() {
+ platform_caps::caps_t caps = 0;
+ const auto runtime = virtualhid::create_runtime();
+ if (!runtime) {
+ return caps;
}
- const auto macos_input = static_cast(input.get());
- CGEventRef event = CGEventCreateScrollWheelEvent(macos_input->source, kCGScrollEventUnitPixel, 2, wheelY, wheelX);
- if (!event) {
- return;
+ const auto &capabilities = runtime->capabilities();
+ if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_touchpad()) {
+ caps |= platform_caps::controller_touch;
}
-
- CGEventSetIntegerValueField(event, kCGScrollWheelEventIsContinuous, 1);
- CGEventPost(kCGHIDEventTap, event);
- CFRelease(event);
- }
-
- void scroll(input_t &input, const int high_res_distance) {
- post_scroll(input, scroll_pixels(static_cast(input.get()), high_res_distance), 0);
- }
-
- void hscroll(input_t &input, int high_res_distance) {
- post_scroll(input, 0, scroll_pixels(static_cast(input.get()), high_res_distance));
- }
-
- /**
- * @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;
- }
-
- /**
- * @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
- }
-
- /**
- * @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
- }
-
- /**
- * @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
- }
-
- /**
- * @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
- }
-
- /**
- * @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
- }
-
- 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;
- }
- }
- }
+ if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
+ caps |= platform_caps::pen_touch;
}
- // 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;
- }
-
- void freeInput(void *p) {
- const auto *input = static_cast(p);
-
- CFRelease(input->source);
- CFRelease(input->keyboard_source);
- CFRelease(input->mouse_event);
-
- 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;
+ }
+ gamepads = virtualhid::supported_gamepads(virtualhid::get_input_context(*input).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/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp
new file mode 100644
index 00000000000..6132de04dea
--- /dev/null
+++ b/src/platform/virtualhid_input.cpp
@@ -0,0 +1,962 @@
+/**
+ * @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
+#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) {
+ using enum lvh::ClientControllerType;
+
+ switch (type) {
+ case LI_CTYPE_XBOX:
+ return xbox;
+ case LI_CTYPE_PS:
+ return playstation;
+ case LI_CTYPE_NINTENDO:
+ return nintendo;
+ case LI_CTYPE_UNKNOWN:
+ default:
+ return unknown;
+ }
+ }
+
+ const gamepad_profile_t &profile_for_name(std::string_view name) {
+ if (const auto iter = std::ranges::find(gamepad_profiles, name, &gamepad_profile_t::name); 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) {
+ using enum lvh::GamepadButton;
+
+ lvh::GamepadState result;
+ const auto flags = state.buttonFlags;
+
+ result.buttons.set(dpad_up, flags & DPAD_UP);
+ result.buttons.set(dpad_down, flags & DPAD_DOWN);
+ result.buttons.set(dpad_left, flags & DPAD_LEFT);
+ result.buttons.set(dpad_right, flags & DPAD_RIGHT);
+ result.buttons.set(start, flags & START);
+ result.buttons.set(back, flags & BACK);
+ result.buttons.set(left_stick, flags & LEFT_STICK);
+ result.buttons.set(right_stick, flags & RIGHT_STICK);
+ result.buttons.set(left_shoulder, flags & LEFT_BUTTON);
+ result.buttons.set(right_shoulder, flags & RIGHT_BUTTON);
+ result.buttons.set(guide, flags & HOME);
+ result.buttons.set(a, flags & A);
+ result.buttons.set(b, flags & B);
+ result.buttons.set(x, flags & X);
+ result.buttons.set(y, flags & Y);
+ result.buttons.set(misc1, support.supports_misc1_button && (flags & MISC_BUTTON));
+ result.buttons.set(touchpad, support.supports_touchpad_button && (flags & TOUCHPAD_BUTTON));
+
+ if (support.supports_touchpad_button && config::input.ds4_back_as_touchpad_click && configured_gamepad_supports_touchpad() && (flags & BACK)) {
+ result.buttons.set(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) {
+ using enum lvh::MouseButton;
+
+ switch (button) {
+ case BUTTON_LEFT:
+ return left;
+ case BUTTON_MIDDLE:
+ return middle;
+ case BUTTON_RIGHT:
+ return right;
+ case BUTTON_X1:
+ return side;
+ case BUTTON_X2:
+ return extra;
+ default:
+ BOOST_LOG(warning) << "Unknown mouse button: "sv << button;
+ return std::nullopt;
+ }
+ }
+
+ lvh::GamepadBatteryState battery_state(std::uint8_t state) {
+ using enum lvh::GamepadBatteryState;
+
+ switch (state) {
+ case LI_BATTERY_STATE_DISCHARGING:
+ return discharging;
+ case LI_BATTERY_STATE_CHARGING:
+ return charging;
+ case LI_BATTERY_STATE_FULL:
+ return full;
+ case LI_BATTERY_STATE_NOT_PRESENT:
+ case LI_BATTERY_STATE_NOT_CHARGING:
+ return charging_error;
+ case LI_BATTERY_STATE_UNKNOWN:
+ default:
+ return 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::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 = (static_cast(flags) & static_cast(SS_KBE_FLAG_NON_NORMALIZED)) == std::byte {};
+ event.prefer_native_scan_code = config::input.always_send_scancodes;
+#else
+ (void) flags;
+#endif
+ return event;
+ }
+
+ lvh::PenToolType pen_tool(std::uint8_t tool) {
+ using enum lvh::PenToolType;
+
+ switch (tool) {
+ case LI_TOOL_TYPE_PEN:
+ return pen;
+ case LI_TOOL_TYPE_ERASER:
+ return eraser;
+ case LI_TOOL_TYPE_UNKNOWN:
+ default:
+ return 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 cancel_all_touches(client_context_t &context) {
+ if (!context.touch) {
+ return;
+ }
+
+ for (const auto id : context.active_touches) {
+ log_failure("cancel libvirtualhid touch contact"sv, context.touch->cancel_contact(id));
+ }
+ context.active_touches.clear();
+ }
+
+ } // namespace
+
+ input_context_t::input_context_t():
+ input_context_t {lvh::BackendKind::platform_default} {}
+
+ input_context_t::input_context_t(lvh::BackendKind backend):
+ runtime {create_runtime(backend)} {
+ 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::BackendKind backend) {
+ lvh::RuntimeOptions options;
+ options.backend = backend;
+ 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.emplace_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.emplace_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();
+ profile.name = std::format("Sunshine {}", profile.name);
+ 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;
+ }
+
+#ifdef SUNSHINE_TESTS
+ lvh::GamepadStateAdapter *gamepad_adapter_for_testing(input_context_t &context, int nr) {
+ return has_gamepad(context, nr) ? context.gamepads[nr]->adapter.get() : nullptr;
+ }
+#endif
+
+ 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].has_value()) {
+ 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, std::uint8_t flags) {
+ if (context.keyboard) {
+ log_failure("submit libvirtualhid keyboard input"sv, context.keyboard->submit(keyboard_event(modcode, release, flags)));
+ }
+ }
+
+ 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_port_t &touch_port, const touch_input_t &touch) {
+ if (!context.touch) {
+ return;
+ }
+
+ switch (touch.eventType) {
+ case LI_TOUCH_EVENT_CANCEL_ALL:
+ 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("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:
+ 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);
+ 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;
+ }
+ default:
+ return;
+ }
+ }
+
+ void pen_update(client_context_t &context, const touch_port_t &touch_port, const pen_input_t &pen) {
+ if (!context.pen) {
+ return;
+ }
+
+ const auto pen_buttons = static_cast(pen.penButtons);
+ const std::array button_states {
+ std::pair {lvh::PenButton::primary, (pen_buttons & static_cast(LI_PEN_BUTTON_PRIMARY)) != std::byte {}},
+ std::pair {lvh::PenButton::secondary, (pen_buttons & static_cast(LI_PEN_BUTTON_SECONDARY)) != std::byte {}},
+ std::pair {lvh::PenButton::tertiary, (pen_buttons & static_cast(LI_PEN_BUTTON_TERTIARY)) != std::byte {}},
+ };
+ 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();
+ }
+
+ using enum lvh::PointerTransition;
+ auto transition = update;
+ switch (pen.eventType) {
+ case LI_TOUCH_EVENT_CANCEL:
+ case LI_TOUCH_EVENT_CANCEL_ALL:
+ transition = cancel;
+ break;
+ case LI_TOUCH_EVENT_UP:
+ transition = release;
+ break;
+ case LI_TOUCH_EVENT_HOVER_LEAVE:
+ transition = leave;
+ break;
+ default:
+ break;
+ }
+
+ 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 = transition == 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);
+ 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;
+ state.transition = transition;
+ state.viewport = pointer_viewport(touch_port);
+ 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
+
+namespace platf {
+
+#ifndef _WIN32
+ /**
+ * @brief Global libvirtualhid devices shared by clients.
+ */
+ struct input_raw_t {
+ virtualhid::input_context_t virtualhid; ///< libvirtualhid input context.
+ };
+
+ namespace {
+
+ /**
+ * @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->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(input_raw_t *input) {
+ std::default_delete