diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml
index 18f1dee3cce..939d67f73e2 100644
--- a/.github/workflows/ci-linux.yml
+++ b/.github/workflows/ci-linux.yml
@@ -59,7 +59,8 @@ jobs:
libwayland-dev \
libx11-xcb-dev \
libxcb-dri3-dev \
- libxfixes-dev
+ libxfixes-dev \
+ libxtst-dev
- name: Build latest libva
env:
diff --git a/.gitmodules b/.gitmodules
index 308be2aa567..4320fe5f0e1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -13,14 +13,14 @@
[submodule "third-party/glad"]
path = third-party/glad
url = https://github.com/Dav1dde/glad.git
-[submodule "third-party/inputtino"]
- path = third-party/inputtino
- url = https://github.com/games-on-whales/inputtino.git
- branch = stable
[submodule "third-party/libdisplaydevice"]
path = third-party/libdisplaydevice
url = https://github.com/LizardByte/libdisplaydevice.git
branch = master
+[submodule "third-party/libvirtualhid"]
+ path = third-party/libvirtualhid
+ url = https://github.com/LizardByte/libvirtualhid.git
+ branch = master
[submodule "third-party/lizardbyte-common"]
path = third-party/lizardbyte-common
url = https://github.com/LizardByte/lizardbyte-common.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3c94c1ffa5b..7e544083d4b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,8 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
# `CMAKE_CUDA_ARCHITECTURES` requires 3.18
# `set_source_files_properties` requires 3.18
# `cmake_path(CONVERT ... TO_NATIVE_PATH_LIST ...)` requires 3.20
+# `third-party/libvirtualhid` requires 3.24
# todo - set this conditionally
project(Sunshine VERSION 0.0.0
diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake
index 924f2f5fae3..b13e29cdb1e 100644
--- a/cmake/compile_definitions/common.cmake
+++ b/cmake/compile_definitions/common.cmake
@@ -53,6 +53,18 @@ elseif(UNIX)
endif()
endif()
+# libvirtualhid
+add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libvirtualhid")
+list(APPEND SUNSHINE_EXTERNAL_LIBRARIES libvirtualhid::libvirtualhid)
+list(APPEND PLATFORM_TARGET_FILES
+ "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.h"
+ "${CMAKE_SOURCE_DIR}/src/platform/virtualhid_input.cpp")
+
+# build libevdev before the libvirtualhid target when using the ExternalProject fallback
+if(EXTERNAL_PROJECT_LIBEVDEV_USED AND TARGET libvirtualhid)
+ add_dependencies(libvirtualhid libevdev)
+endif()
+
include_directories(BEFORE SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include")
file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h")
list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES})
diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake
index 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 e3e57339f18..4bdcf9374b9 100644
--- a/cmake/compile_definitions/windows.cmake
+++ b/cmake/compile_definitions/windows.cmake
@@ -55,11 +55,6 @@ set_target_properties(sunshine_rc_object PROPERTIES
INCLUDE_DIRECTORIES ""
)
-# ViGEmBus version
-set(VIGEMBUS_PACKAGED_V "1.21.442")
-set(VIGEMBUS_PACKAGED_V_2 "${VIGEMBUS_PACKAGED_V}.0")
-list(APPEND SUNSHINE_DEFINITIONS VIGEMBUS_PACKAGED_VERSION="${VIGEMBUS_PACKAGED_V_2}")
-
set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h"
diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake
index 69830da6b9a..c826c70d5a0 100644
--- a/cmake/packaging/windows.cmake
+++ b/cmake/packaging/windows.cmake
@@ -9,24 +9,6 @@ if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64" AND DEFINED _MINHOOK_DLL)
install(FILES "${_MINHOOK_DLL}" DESTINATION "." COMPONENT application)
endif()
-# ViGEmBus installer
-set(SUNSHINE_THIRD_PARTY_DIR "third-party")
-set(VIGEMBUS_INSTALLER "${CMAKE_BINARY_DIR}/${SUNSHINE_THIRD_PARTY_DIR}/vigembus_installer.exe")
-set(VIGEMBUS_DOWNLOAD_URL_1 "https://github.com/nefarius/ViGEmBus/releases/download")
-set(VIGEMBUS_DOWNLOAD_URL_2 "v${VIGEMBUS_PACKAGED_V_2}/ViGEmBus_${VIGEMBUS_PACKAGED_V}_x64_x86_arm64.exe")
-file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/${SUNSHINE_THIRD_PARTY_DIR}")
-file(DOWNLOAD
- "${VIGEMBUS_DOWNLOAD_URL_1}/${VIGEMBUS_DOWNLOAD_URL_2}"
- ${VIGEMBUS_INSTALLER}
- SHOW_PROGRESS
- EXPECTED_HASH SHA256=155c50f1eec07bdc28d2f61a3e3c2c6c132fee7328412de224695f89143316bc
- TIMEOUT 60
-)
-install(FILES ${VIGEMBUS_INSTALLER}
- DESTINATION "${SUNSHINE_THIRD_PARTY_DIR}"
- RENAME "vigembus_installer.exe"
- COMPONENT gamepad)
-
# Adding tools
install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi)
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
@@ -80,8 +62,6 @@ set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}")
# Setting components groups and dependencies
set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true)
-set(CPACK_COMPONENT_GROUP_THIRDPARTY_DISPLAY_NAME "Third Party")
-set(CPACK_COMPONENT_GROUP_THIRDPARTY_DESCRIPTION "Bundled third-party installers and optional components.")
# sunshine binary
set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}")
@@ -116,11 +96,6 @@ set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "Add Firewall Exclusions")
set(CPACK_COMPONENT_FIREWALL_DESCRIPTION "Scripts to enable or disable firewall rules.")
set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts")
-# gamepad third-party installer
-set(CPACK_COMPONENT_GAMEPAD_DISPLAY_NAME "Virtual Gamepad")
-set(CPACK_COMPONENT_GAMEPAD_DESCRIPTION "ViGEmBus installer for virtual gamepad support.")
-set(CPACK_COMPONENT_GAMEPAD_GROUP "ThirdParty")
-
# include specific packaging
include(${CMAKE_MODULE_PATH}/packaging/windows_nsis.cmake)
include(${CMAKE_MODULE_PATH}/packaging/windows_wix.cmake)
diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake
index b7cce8a7d17..ad49a0f811c 100644
--- a/cmake/targets/linux.cmake
+++ b/cmake/targets/linux.cmake
@@ -4,6 +4,13 @@ if(NOT FREEBSD)
# Using newer c++ compilers / features on older distros causes runtime dyn link errors
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
-static-libgcc
- -static-libstdc++
)
+
+ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15)
+ # GCC 15's static libstdc++ does not provide the out-of-line wait/notify
+ # symbol used by std::stop_token, so libstdc++ must be a direct DSO input.
+ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES stdc++)
+ else()
+ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES -static-libstdc++)
+ endif()
endif()
diff --git a/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 e9fe004ddde..ad9bc91c36a 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -332,30 +332,40 @@ editing the `conf` file in a text editor. Use the examples as reference.
@endcode
-
Choices
+
Choices
+
generic
+
Generic HID gamepad
+ @note{This option applies to FreeBSD, Linux, and Windows.}
+
+
ds4
DualShock 4 controller (PS4)
- @note{This option applies to Windows only.}
+ @note{This option applies to FreeBSD, Linux, and Windows.}
ds5
DualShock 5 controller (PS5)
- @note{This option applies to FreeBSD and Linux only.}
+ @note{This option applies to FreeBSD, Linux, and Windows.}
switch
Switch Pro controller
- @note{This option applies to FreeBSD and Linux only.}
+ @note{This option applies to FreeBSD, Linux, and Windows.}
x360
Xbox 360 controller
- @note{This option applies to Windows only.}
+ @note{This option applies to FreeBSD, Linux, and Windows.}
xone
Xbox One controller
- @note{This option applies to FreeBSD and Linux only.}
+ @note{This option applies to FreeBSD, Linux, and Windows.}
+
+
+
xseries
+
Xbox Series controller
+ @note{This option applies to FreeBSD, Linux, and Windows.}
@@ -440,14 +450,13 @@ editing the `conf` file in a text editor. Use the examples as reference.
-### ds5_inputtino_randomize_mac
+### virtualhid_randomize_mac
Description
- Randomize the MAC-Address for the generated virtual controller.
- @hint{Only applies on linux for gamepads created as PS5-style controllers}
+ Randomize the MAC address for PlayStation-style virtual controllers created by libvirtualhid.
@@ -459,7 +468,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 1e3582c4d56..21a282e2f16 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -479,11 +479,11 @@ 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 libvirtualhid Windows driver
+separately for full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox 360 and
+DualShock 4 gamepads when libvirtualhid is unavailable.
-
+After installing or updating virtual input drivers, it is recommended to restart your computer.
## Usage
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index f59b7ef213f..188743eea88 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -255,10 +255,9 @@ launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist
## Windows
### No gamepad detected
-You must install ViGEmBus to use virtual gamepads. You can install this from the troubleshooting tab of the web UI.
-
-Alternatively, you can manually install it from
-[ViGEmBus releases](https://github.com/nefarius/ViGEmBus/releases/latest). You must use version 1.17 or newer.
+Sunshine uses libvirtualhid for virtual gamepads on Windows. Install the libvirtualhid Windows driver separately
+for full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox 360 and DualShock 4
+gamepads when libvirtualhid is unavailable. If you use ViGEmBus fallback, you must use version 1.17 or newer.
After installation, it is recommended to restart your computer.
diff --git a/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/packaging/linux/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD
index ef624407035..6cd89f9d3e9 100644
--- a/packaging/linux/Arch/PKGBUILD
+++ b/packaging/linux/Arch/PKGBUILD
@@ -31,6 +31,7 @@ fi
depends=(
'avahi'
'curl'
+ 'gcc-libs'
'gtk3'
'hicolor-icon-theme'
'libayatana-appindicator'
diff --git a/src/config.cpp b/src/config.cpp
index e1dab3516fb..b286c688d57 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -758,7 +758,7 @@ namespace config {
true, // back as touchpad click enabled (manual DS4 only)
true, // client gamepads with motion events are emulated as DS4
true, // client gamepads with touchpads are emulated as DS4
- true, // ds5_inputtino_randomize_mac
+ true, // virtualhid_randomize_mac
true, // keyboard enabled
true, // mouse enabled
@@ -1690,7 +1690,7 @@ namespace config {
bool_f(vars, "ds4_back_as_touchpad_click", input.ds4_back_as_touchpad_click);
bool_f(vars, "motion_as_ds4", input.motion_as_ds4);
bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4);
- bool_f(vars, "ds5_inputtino_randomize_mac", input.ds5_inputtino_randomize_mac);
+ bool_f(vars, "virtualhid_randomize_mac", input.virtualhid_randomize_mac);
bool_f(vars, "mouse", input.mouse);
bool_f(vars, "keyboard", input.keyboard);
diff --git a/src/config.h b/src/config.h
index cbe0978ede4..0d8681dddf1 100644
--- a/src/config.h
+++ b/src/config.h
@@ -274,7 +274,7 @@ namespace config {
bool ds4_back_as_touchpad_click; ///< Map the DS4 Back button to a touchpad click.
bool motion_as_ds4; ///< Expose motion controls through the DS4 protocol.
bool touchpad_as_ds4; ///< Expose touchpad input through the DS4 protocol.
- bool ds5_inputtino_randomize_mac; ///< Randomize the inputtino DualSense MAC address.
+ bool virtualhid_randomize_mac; ///< Randomize the libvirtualhid virtual controller MAC address.
bool keyboard; ///< Enable keyboard input from clients.
bool key_rightalt_to_key_win; ///< Map the client Right Alt key to the Windows key.
diff --git a/src/confighttp.cpp b/src/confighttp.cpp
index 8b9468e3482..dff4839257f 100644
--- a/src/confighttp.cpp
+++ b/src/confighttp.cpp
@@ -8,10 +8,13 @@
// standard includes
#include
+#include
#include
#include
#include
+#include
#include
+#include
// lib includes
#include
@@ -22,9 +25,10 @@
#include
#ifdef _WIN32
+ #include "platform/virtualhid_input.h"
#include "platform/windows/misc.h"
+ #include "platform/windows/utf_utils.h"
- #include
#include
#endif
@@ -102,6 +106,246 @@ namespace confighttp {
*/
constexpr auto CSRF_TOKEN_LIFETIME = std::chrono::hours(1); // Tokens valid for 1 hour
+ constexpr auto LIBVIRTUALHID_MINIMUM_VERSION = ""sv; ///< Minimum supported libvirtualhid driver version; empty means any version.
+ constexpr auto VIGEMBUS_MINIMUM_VERSION = "1.17.0.0"sv; ///< Minimum supported ViGEmBus fallback driver version.
+
+ /**
+ * @brief Parse one dotted driver-version component.
+ *
+ * @param part Version component text.
+ * @return Parsed component value, or empty when invalid.
+ */
+ std::optional parse_driver_version_part(std::string_view part) {
+ if (part.empty()) {
+ return std::nullopt;
+ }
+
+ unsigned int value = 0;
+ const auto *begin = part.data();
+ const auto *end = part.data() + part.size();
+ const auto [ptr, ec] = std::from_chars(begin, end, value);
+ if (ec != std::errc {} || ptr != end) {
+ return std::nullopt;
+ }
+
+ return value;
+ }
+
+ /**
+ * @brief Parse a dotted driver version into numeric components.
+ *
+ * @param version Driver version text.
+ * @return Parsed version parts, or empty when invalid.
+ */
+ std::optional> parse_driver_version(std::string_view version) {
+ if (version.empty()) {
+ return std::nullopt;
+ }
+
+ std::vector parts;
+ std::size_t start = 0;
+ while (start <= version.size()) {
+ const auto dot = version.find('.', start);
+ const auto length = dot == std::string_view::npos ? std::string_view::npos : dot - start;
+ const auto part = parse_driver_version_part(version.substr(start, length));
+ if (!part) {
+ return std::nullopt;
+ }
+
+ parts.push_back(*part);
+ if (dot == std::string_view::npos) {
+ break;
+ }
+ start = dot + 1;
+ }
+
+ return parts;
+ }
+
+ bool is_driver_version_supported(std::string_view version, std::string_view minimum_version) {
+ if (minimum_version.empty()) {
+ return true;
+ }
+
+ const auto version_parts = parse_driver_version(version);
+ const auto minimum_parts = parse_driver_version(minimum_version);
+ if (!version_parts || !minimum_parts) {
+ return false;
+ }
+
+ const auto part_count = std::max(version_parts->size(), minimum_parts->size());
+ for (std::size_t i = 0; i < part_count; ++i) {
+ const auto version_part = i < version_parts->size() ? (*version_parts)[i] : 0U;
+ const auto minimum_part = i < minimum_parts->size() ? (*minimum_parts)[i] : 0U;
+ if (version_part != minimum_part) {
+ return version_part > minimum_part;
+ }
+ }
+
+ return true;
+ }
+
+ nlohmann::json build_driver_status(bool installed, const std::string &version, std::string_view minimum_version) {
+ const auto minimum_version_text = std::string {minimum_version};
+
+ nlohmann::json output_tree;
+ output_tree["installed"] = installed;
+ output_tree["version"] = version;
+ output_tree["minimum_version"] = minimum_version_text;
+ output_tree["supported_versions"] = minimum_version.empty() ? "Any" : std::format(">= {}", minimum_version_text);
+ output_tree["version_compatible"] = installed && is_driver_version_supported(version, minimum_version);
+
+ return output_tree;
+ }
+
+#ifdef _WIN32
+ /**
+ * @brief RAII wrapper for a Windows registry key handle.
+ */
+ class registry_key_t {
+ public:
+ /**
+ * @brief Construct an empty registry key wrapper.
+ */
+ registry_key_t() = default;
+
+ /**
+ * @brief Copy construction is disabled because the wrapper owns a handle.
+ */
+ registry_key_t(const registry_key_t &) = delete;
+
+ /**
+ * @brief Copy assignment is disabled because the wrapper owns a handle.
+ *
+ * @return This registry key wrapper.
+ */
+ registry_key_t &operator=(const registry_key_t &) = delete;
+
+ /**
+ * @brief Close the owned registry key handle.
+ */
+ ~registry_key_t() {
+ close();
+ }
+
+ /**
+ * @brief Get the owned registry key handle.
+ *
+ * @return Registry key handle.
+ */
+ HKEY get() const {
+ return handle;
+ }
+
+ /**
+ * @brief Prepare the wrapper to receive a registry key handle.
+ *
+ * @return Address of the wrapped handle.
+ */
+ HKEY *put() {
+ close();
+ return &handle;
+ }
+
+ private:
+ /**
+ * @brief Close the owned registry key handle if one is open.
+ */
+ void close() {
+ if (handle) {
+ RegCloseKey(handle);
+ handle = nullptr;
+ }
+ }
+
+ HKEY handle = nullptr; ///< Owned Windows registry key handle.
+ };
+
+ /**
+ * @brief Read a string value from a Windows registry key.
+ *
+ * @param key Registry key to query.
+ * @param value_name Registry value name.
+ * @return Registry string value, or empty when unavailable.
+ */
+ std::optional read_registry_string_value(HKEY key, const wchar_t *value_name) {
+ DWORD value_type = 0;
+ DWORD value_size = 0;
+ if (RegGetValueW(key, nullptr, value_name, RRF_RT_REG_SZ, &value_type, nullptr, &value_size) != ERROR_SUCCESS ||
+ value_size == 0) {
+ return std::nullopt;
+ }
+
+ std::wstring value(value_size / sizeof(wchar_t), L'\0');
+ if (RegGetValueW(key, nullptr, value_name, RRF_RT_REG_SZ, &value_type, value.data(), &value_size) != ERROR_SUCCESS) {
+ return std::nullopt;
+ }
+
+ while (!value.empty() && value.back() == L'\0') {
+ value.pop_back();
+ }
+ return value;
+ }
+
+ /**
+ * @brief Read the installed libvirtualhid driver version from the Windows device registry.
+ *
+ * @return Driver version string, or empty when unavailable.
+ */
+ std::string read_libvirtualhid_driver_version() {
+ registry_key_t root_key;
+ if (RegOpenKeyExW(
+ HKEY_LOCAL_MACHINE,
+ L"SYSTEM\\CurrentControlSet\\Enum\\ROOT\\LIBVIRTUALHID",
+ 0,
+ KEY_READ,
+ root_key.put()
+ ) != ERROR_SUCCESS) {
+ return {};
+ }
+
+ for (DWORD index = 0;; ++index) {
+ wchar_t subkey_name[256] = {};
+ DWORD subkey_name_size = _countof(subkey_name);
+ const auto enum_status = RegEnumKeyExW(root_key.get(), index, subkey_name, &subkey_name_size, nullptr, nullptr, nullptr, nullptr);
+ if (enum_status == ERROR_NO_MORE_ITEMS) {
+ break;
+ }
+ if (enum_status != ERROR_SUCCESS) {
+ continue;
+ }
+
+ std::wstring device_key_path = L"SYSTEM\\CurrentControlSet\\Enum\\ROOT\\LIBVIRTUALHID\\";
+ device_key_path.append(subkey_name, subkey_name_size);
+
+ registry_key_t device_key;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, device_key_path.c_str(), 0, KEY_READ, device_key.put()) != ERROR_SUCCESS) {
+ continue;
+ }
+
+ const auto driver_key_suffix = read_registry_string_value(device_key.get(), L"Driver");
+ if (!driver_key_suffix) {
+ continue;
+ }
+
+ std::wstring driver_key_path = L"SYSTEM\\CurrentControlSet\\Control\\Class\\";
+ driver_key_path += *driver_key_suffix;
+
+ registry_key_t driver_key;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, driver_key_path.c_str(), 0, KEY_READ, driver_key.put()) != ERROR_SUCCESS) {
+ continue;
+ }
+
+ if (const auto version = read_registry_string_value(driver_key.get(), L"DriverVersion")) {
+ return utf_utils::to_utf8(*version);
+ }
+ }
+
+ return {};
+ }
+
+#endif
+
/**
* @brief Log the request details.
* @param request The HTTP request object.
@@ -1431,130 +1675,87 @@ namespace confighttp {
}
/**
- * @brief Get ViGEmBus driver version and installation status.
- * @param response The HTTP response object.
- * @param request The HTTP request object.
+ * @brief Build libvirtualhid driver version and installation status.
*
- * @api_examples{/api/vigembus/status| GET| null}
+ * @return libvirtualhid driver status JSON.
*/
- void getViGEmBusStatus(const resp_https_t &response, const req_https_t &request) {
- if (!authenticate(response, request)) {
- return;
+ nlohmann::json get_virtualhid_driver_status() {
+#ifdef _WIN32
+ const auto version_str = read_libvirtualhid_driver_version();
+ auto output_tree = build_driver_status(false, version_str, LIBVIRTUALHID_MINIMUM_VERSION);
+ bool requires_installed_driver = true;
+ std::string backend_name;
+ std::string error;
+
+ try {
+ const auto runtime = platf::virtualhid::create_runtime();
+ if (runtime) {
+ const auto &capabilities = runtime->capabilities();
+ backend_name = capabilities.backend_name;
+ requires_installed_driver = capabilities.requires_installed_driver;
+ output_tree = build_driver_status(capabilities.supports_gamepad, version_str, LIBVIRTUALHID_MINIMUM_VERSION);
+ }
+ } catch (const std::exception &e) {
+ error = e.what();
}
- print_req(request);
+ output_tree["backend_name"] = backend_name;
+ output_tree["requires_installed_driver"] = requires_installed_driver;
+ if (!error.empty()) {
+ output_tree["error"] = error;
+ }
+#else
+ auto output_tree = build_driver_status(false, "", LIBVIRTUALHID_MINIMUM_VERSION);
+ output_tree["error"] = "libvirtualhid driver status is only available on Windows";
+ output_tree["backend_name"] = "";
+ output_tree["requires_installed_driver"] = false;
+#endif
- nlohmann::json output_tree;
+ return output_tree;
+ }
+ /**
+ * @brief Build ViGEmBus fallback driver version and installation status.
+ *
+ * @return ViGEmBus fallback driver status JSON.
+ */
+ nlohmann::json get_vigembus_driver_status() {
#ifdef _WIN32
std::string version_str;
- bool installed = false;
- bool version_compatible = false;
// Check if ViGEmBus driver exists
- std::filesystem::path driver_path = std::filesystem::path(std::getenv("SystemRoot") ? std::getenv("SystemRoot") : "C:\\Windows") / "System32" / "drivers" / "ViGEmBus.sys";
-
- if (std::filesystem::exists(driver_path)) {
- installed = platf::getFileVersionInfo(driver_path, version_str);
- if (installed) {
- // Parse version string to check compatibility (>= 1.17.0.0)
- std::vector version_parts;
- std::stringstream ss(version_str);
- std::string part;
- while (std::getline(ss, part, '.')) {
- version_parts.push_back(part);
- }
-
- if (version_parts.size() >= 2) {
- int major = std::stoi(version_parts[0]);
- int minor = std::stoi(version_parts[1]);
- version_compatible = (major > 1) || (major == 1 && minor >= 17);
- }
- }
+ const std::filesystem::path driver_path = std::filesystem::path(std::getenv("SystemRoot") ? std::getenv("SystemRoot") : "C:\\Windows") / "System32" / "drivers" / "ViGEmBus.sys";
+ const auto installed = std::filesystem::exists(driver_path);
+ if (installed) {
+ platf::getFileVersionInfo(driver_path, version_str);
}
- output_tree["installed"] = installed;
- output_tree["version"] = version_str;
- output_tree["version_compatible"] = version_compatible;
- output_tree["packaged_version"] = VIGEMBUS_PACKAGED_VERSION;
+ auto output_tree = build_driver_status(installed, version_str, VIGEMBUS_MINIMUM_VERSION);
#else
+ auto output_tree = build_driver_status(false, "", VIGEMBUS_MINIMUM_VERSION);
output_tree["error"] = "ViGEmBus is only available on Windows";
- output_tree["installed"] = false;
- output_tree["version"] = "";
- output_tree["version_compatible"] = false;
- output_tree["packaged_version"] = "";
#endif
- send_response(response, output_tree);
+ return output_tree;
}
/**
- * @brief Install ViGEmBus driver with elevated permissions.
+ * @brief Get virtual input driver version and installation status.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
- * @api_examples{/api/vigembus/install| POST| null}
+ * @api_examples{/api/virtual-input/status| GET| null}
*/
- void installViGEmBus(const resp_https_t &response, const req_https_t &request) {
+ void getVirtualInputStatus(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
- std::string client_id = get_client_id(request);
- if (!validate_csrf_token(response, request, client_id)) {
- return;
- }
-
print_req(request);
nlohmann::json output_tree;
-
-#ifdef _WIN32
- // Get the path to the packaged ViGEmBus installer.
- const std::filesystem::path installer_path = platf::appdata().parent_path() / "third-party" / "vigembus_installer.exe";
-
- if (!std::filesystem::exists(installer_path)) {
- output_tree["status"] = false;
- output_tree["error"] = "ViGEmBus installer not found";
- send_response(response, output_tree);
- return;
- }
-
- // Run the installer with elevated permissions
- std::error_code ec;
- boost::filesystem::path working_dir = boost::filesystem::path(installer_path.string()).parent_path();
- boost::process::v1::environment env = boost::this_process::environment();
-
- // Run with elevated permissions, non-interactive
- const std::string install_cmd = std::format("{} /quiet", installer_path.string());
- auto child = platf::run_command(true, false, install_cmd, working_dir, env, nullptr, ec, nullptr);
-
- if (ec) {
- output_tree["status"] = false;
- output_tree["error"] = "Failed to start installer: " + ec.message();
- send_response(response, output_tree);
- return;
- }
-
- // Wait for the installer to complete
- child.wait(ec);
-
- if (ec) {
- output_tree["status"] = false;
- output_tree["error"] = "Installer failed: " + ec.message();
- } else {
- int exit_code = child.exit_code();
- output_tree["status"] = (exit_code == 0);
- output_tree["exit_code"] = exit_code;
- if (exit_code != 0) {
- output_tree["error"] = std::format("Installer exited with code {}", exit_code);
- }
- }
-#else
- output_tree["status"] = false;
- output_tree["error"] = "ViGEmBus installation is only available on Windows";
-#endif
-
+ output_tree["virtualhid"] = get_virtualhid_driver_status();
+ output_tree["vigembus"] = get_vigembus_driver_status();
send_response(response, output_tree);
}
@@ -1812,8 +2013,7 @@ namespace confighttp {
server.resource["^/api/logs$"]["GET"] = getLogs;
server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence;
server.resource["^/api/restart$"]["POST"] = restart;
- server.resource["^/api/vigembus/status$"]["GET"] = getViGEmBusStatus;
- server.resource["^/api/vigembus/install$"]["POST"] = installViGEmBus;
+ server.resource["^/api/virtual-input/status$"]["GET"] = getVirtualInputStatus;
// static/dynamic resources
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
diff --git a/src/confighttp.h b/src/confighttp.h
index 0806ad4afa0..78ab5c479c4 100644
--- a/src/confighttp.h
+++ b/src/confighttp.h
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
// lib includes
#include
@@ -75,6 +76,28 @@ namespace confighttp {
void getLocale(const resp_https_t &response, const req_https_t &request);
void getCSRFToken(const resp_https_t &response, const req_https_t &request);
+ /**
+ * @brief Check whether a detected driver version satisfies a minimum version.
+ *
+ * Empty minimum versions accept any detected version. Non-empty minimum versions
+ * require a fully numeric dotted version string.
+ *
+ * @param version Detected driver version.
+ * @param minimum_version Minimum supported driver version, or empty for any version.
+ * @return True when the driver version is supported.
+ */
+ bool is_driver_version_supported(std::string_view version, std::string_view minimum_version);
+
+ /**
+ * @brief Build a standard driver status response.
+ *
+ * @param installed Whether the driver was detected.
+ * @param version Detected driver version.
+ * @param minimum_version Minimum supported driver version, or empty for any version.
+ * @return Driver status JSON object.
+ */
+ nlohmann::json build_driver_status(bool installed, const std::string &version, std::string_view minimum_version);
+
// Browse helper functions (also exposed for unit testing)
/**
* @brief Checks whether a directory entry qualifies as an executable file.
diff --git a/src/input.cpp b/src/input.cpp
index 8108bcba378..0716a29381f 100644
--- a/src/input.cpp
+++ b/src/input.cpp
@@ -13,6 +13,7 @@ extern "C" {
#include
#include
#include
+#include
#include
#include
@@ -610,8 +611,8 @@ namespace input {
This final operation is a bit weird and has been brought about with lots of trial and error. A better
way to do this may exist.
- Basically, this is what makes the touchscreen map to the coordinates inputtino expects properly.
- Since inputtino's dimensions are now logical (because scaling breaks everything otherwise), using the previous
+ Basically, this is what makes the touchscreen map to the logical virtual input coordinates properly.
+ Since the virtual input dimensions are logical (because scaling breaks everything otherwise), using the previous
x and y coordinates would be incorrect when screens are scaled, because the touch port is smaller (or larger)
by a factor (that factor is touch_port.scalar_tpcoords), and that factor must be used to account for that difference
when moving the cursor. Otherwise, it will move either slower or faster than your finger proportionally to
@@ -1880,8 +1881,7 @@ namespace input {
* @brief Probe connected gamepads and update input capability state.
*/
bool probe_gamepads() {
- auto input = static_cast(platf_input.get());
- const auto gamepads = platf::supported_gamepads(input);
+ const auto gamepads = platf::supported_gamepads(std::addressof(platf_input));
for (auto &gamepad : gamepads) {
if (gamepad.is_enabled && gamepad.name != "auto") {
return false;
diff --git a/src/platform/common.h b/src/platform/common.h
index 24da9f53cfa..ebd7e42d8ae 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
// lib includes
@@ -1102,14 +1103,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.
*
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..54d5716792e
--- /dev/null
+++ b/src/platform/linux/input/virtualhid.cpp
@@ -0,0 +1,187 @@
+/**
+ * @file src/platform/linux/input/virtualhid.cpp
+ * @brief Definitions for libvirtualhid Unix input handling.
+ */
+
+// standard includes
+#include
+
+// platform includes
+#ifdef SUNSHINE_BUILD_X11
+ #include
+#endif
+
+// local includes
+#include "src/platform/virtualhid_input.h"
+
+using namespace std::literals;
+
+namespace platf {
+ namespace {
+
+ /**
+ * @brief Global libvirtualhid devices shared by clients.
+ */
+ struct input_raw_t {
+ virtualhid::input_context_t virtualhid; ///< libvirtualhid input context.
+ };
+
+ /**
+ * @brief Per-client libvirtualhid devices.
+ */
+ struct client_input_raw_t: client_input_t {
+ /**
+ * @brief Create per-client libvirtualhid devices.
+ *
+ * @param input Platform input backend that receives the event.
+ */
+ explicit client_input_raw_t(input_t &input):
+ virtualhid {((input_raw_t *) input.get())->virtualhid} {}
+
+ virtualhid::client_context_t virtualhid; ///< libvirtualhid client context.
+ };
+
+ } // namespace
+
+ input_t input() {
+ return {new input_raw_t {}};
+ }
+
+ std::unique_ptr allocate_client_input_context(input_t &input) {
+ return std::make_unique(input);
+ }
+
+ void freeInput(void *p) {
+ auto *input = (input_raw_t *) p;
+ delete input;
+ }
+
+ void move_mouse(input_t &input, int deltaX, int deltaY) {
+ virtualhid::move_mouse(((input_raw_t *) input.get())->virtualhid, deltaX, deltaY);
+ }
+
+ void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
+ virtualhid::abs_mouse(((input_raw_t *) input.get())->virtualhid, touch_port, x, y);
+ }
+
+ void button_mouse(input_t &input, int button, bool release) {
+ virtualhid::button_mouse(((input_raw_t *) input.get())->virtualhid, button, release);
+ }
+
+ void scroll(input_t &input, int high_res_distance) {
+ virtualhid::scroll(((input_raw_t *) input.get())->virtualhid, high_res_distance);
+ }
+
+ void hscroll(input_t &input, int high_res_distance) {
+ virtualhid::hscroll(((input_raw_t *) input.get())->virtualhid, high_res_distance);
+ }
+
+ /**
+ * @brief Press or release a virtual keyboard key.
+ *
+ * @param input Platform input backend that receives the event.
+ * @param modcode Modifier key code to update.
+ * @param release Whether the key or button event is a release.
+ * @param flags Bit flags that modify the requested operation; ignored by this backend.
+ */
+ void keyboard_update(input_t &input, std::uint16_t modcode, bool release, std::uint8_t flags) {
+ virtualhid::keyboard_update(((input_raw_t *) input.get())->virtualhid, modcode, release, flags);
+ }
+
+ void unicode(input_t &input, char *utf8, int size) {
+ virtualhid::unicode(((input_raw_t *) input.get())->virtualhid, utf8, size);
+ }
+
+ void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {
+ virtualhid::touch_update(((client_input_raw_t *) input)->virtualhid, touch_port, touch);
+ }
+
+ void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {
+ virtualhid::pen_update(((client_input_raw_t *) input)->virtualhid, touch_port, pen);
+ }
+
+ int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
+ return virtualhid::alloc_gamepad(((input_raw_t *) input.get())->virtualhid, id, metadata, std::move(feedback_queue));
+ }
+
+ void free_gamepad(input_t &input, int nr) {
+ virtualhid::free_gamepad(((input_raw_t *) input.get())->virtualhid, nr);
+ }
+
+ void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
+ virtualhid::gamepad_update(((input_raw_t *) input.get())->virtualhid, nr, gamepad_state);
+ }
+
+ void gamepad_touch(input_t &input, const gamepad_touch_t &touch) {
+ virtualhid::gamepad_touch(((input_raw_t *) input.get())->virtualhid, touch);
+ }
+
+ void gamepad_motion(input_t &input, const gamepad_motion_t &motion) {
+ virtualhid::gamepad_motion(((input_raw_t *) input.get())->virtualhid, motion);
+ }
+
+ void gamepad_battery(input_t &input, const gamepad_battery_t &battery) {
+ virtualhid::gamepad_battery(((input_raw_t *) input.get())->virtualhid, battery);
+ }
+
+ platform_caps::caps_t get_capabilities() {
+ platform_caps::caps_t caps = 0;
+ const auto runtime = virtualhid::create_runtime();
+ if (!runtime) {
+ return caps;
+ }
+
+ const auto &capabilities = runtime->capabilities();
+ if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
+ caps |= platform_caps::pen_touch;
+ }
+ if (virtualhid::configured_gamepad_supports_touchpad()) {
+ caps |= platform_caps::controller_touch;
+ }
+
+ return caps;
+ }
+
+ 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;
+ }
+
+ const auto raw = (input_raw_t *) input->get();
+ gamepads = virtualhid::supported_gamepads(raw->virtualhid.runtime.get());
+ return gamepads;
+ }
+} // namespace platf
diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp
index fe17a9b35a1..222878f30ad 100644
--- a/src/platform/macos/input.cpp
+++ b/src/platform/macos/input.cpp
@@ -1,757 +1,157 @@
/**
* @file src/platform/macos/input.cpp
- * @brief Definitions for macOS input handling.
+ * @brief Definitions for libvirtualhid-backed macOS input handling.
*/
-// standard includes
-#include
-#include
-#include
-#include
-#include
-#include
// platform includes
#include
-#import
-#include
-#include
-#include
-// local includes
-#include "src/display_device.h"
-#include "src/input.h"
-#include "src/logging.h"
-#include "src/platform/common.h"
-#include "src/utility.h"
+// standard includes
+#include
+#include
+#include
+#include
-constexpr auto MULTICLICK_DELAY_MS = std::chrono::milliseconds(500); ///< Maximum gap between clicks that macOS should treat as a double click.
+// local includes
+#include "src/config.h"
+#include "src/platform/virtualhid_input.h"
namespace platf {
- using namespace std::literals;
-
- constexpr int WHEEL_DELTA = 120; ///< Protocol or platform constant for wheel delta.
- constexpr double DEFAULT_SCROLLWHEEL_SCALING = 0.3125; ///< Protocol or platform constant for default scrollwheel scaling.
- constexpr int DEFAULT_SCROLL_LINES_PER_DETENT = 5; ///< Protocol or platform constant for default scroll lines per detent.
/**
- * @brief macOS input source and target display state.
+ * @brief Global libvirtualhid devices shared by clients.
*/
- struct macos_input_t {
- public:
- CGDirectDisplayID display {}; ///< CoreGraphics identifier for the display receiving injected input.
- CGFloat displayScaling {}; ///< Scale factor used to translate client coordinates to display pixels.
- CGEventSourceRef source {}; ///< CoreGraphics event source used for mouse and scroll events.
-
- // keyboard related stuff
- CGEventSourceRef keyboard_source {}; ///< CoreGraphics event source used for keyboard injection.
- CGEventFlags kb_flags {}; ///< Active keyboard modifier flags currently held down by the client.
-
- // mouse related stuff
- CGEventRef mouse_event {}; ///< Reusable CoreGraphics mouse event updated before posting.
- double scrollwheel_scaling {DEFAULT_SCROLLWHEEL_SCALING}; ///< Multiplier applied to incoming scroll-wheel deltas.
- int scroll_lines_per_detent {DEFAULT_SCROLL_LINES_PER_DETENT}; ///< Number of logical scroll lines represented by one wheel detent.
- /**
- * @brief Tracks whether the mouse button is currently pressed.
- */
- bool mouse_down[3] {}; // mouse button status
- /**
- * @brief Last mouse event.
- */
- std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events
- };
-
- // A struct to hold a Windows keycode to Mac virtual keycode mapping.
- /**
- * @brief Mapping from Sunshine key symbols to macOS virtual key codes.
- */
- struct KeyCodeMap {
- int win_keycode; ///< Sunshine/Windows virtual key code received from the client.
- int mac_keycode; ///< macOS virtual key code sent through CoreGraphics.
+ struct input_raw_t {
+ virtualhid::input_context_t virtualhid; ///< libvirtualhid input context.
};
- // Customized less operator for using std::lower_bound() on a KeyCodeMap array.
- /**
- * @brief Order key mappings by Sunshine key code for binary search.
- *
- * @param a Left-hand mapping being compared.
- * @param b Right-hand mapping being compared.
- * @return True when `a` has a lower Sunshine key code than `b`.
- */
- bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) {
- return a.win_keycode < b.win_keycode;
- }
-
- // clang-format off
-/**
- * @brief Key codes map.
- */
-const KeyCodeMap kKeyCodesMap[] = {
- { 0x08 /* VKEY_BACK */, kVK_Delete },
- { 0x09 /* VKEY_TAB */, kVK_Tab },
- { 0x0A /* VKEY_BACKTAB */, 0x21E4 },
- { 0x0C /* VKEY_CLEAR */, kVK_ANSI_KeypadClear },
- { 0x0D /* VKEY_RETURN */, kVK_Return },
- { 0x10 /* VKEY_SHIFT */, kVK_Shift },
- { 0x11 /* VKEY_CONTROL */, kVK_Control },
- { 0x12 /* VKEY_MENU */, kVK_Option },
- { 0x13 /* VKEY_PAUSE */, -1 },
- { 0x14 /* VKEY_CAPITAL */, kVK_CapsLock },
- { 0x15 /* VKEY_KANA */, kVK_JIS_Kana },
- { 0x15 /* VKEY_HANGUL */, -1 },
- { 0x17 /* VKEY_JUNJA */, -1 },
- { 0x18 /* VKEY_FINAL */, -1 },
- { 0x19 /* VKEY_HANJA */, -1 },
- { 0x19 /* VKEY_KANJI */, -1 },
- { 0x1B /* VKEY_ESCAPE */, kVK_Escape },
- { 0x1C /* VKEY_CONVERT */, -1 },
- { 0x1D /* VKEY_NONCONVERT */, -1 },
- { 0x1E /* VKEY_ACCEPT */, -1 },
- { 0x1F /* VKEY_MODECHANGE */, -1 },
- { 0x20 /* VKEY_SPACE */, kVK_Space },
- { 0x21 /* VKEY_PRIOR */, kVK_PageUp },
- { 0x22 /* VKEY_NEXT */, kVK_PageDown },
- { 0x23 /* VKEY_END */, kVK_End },
- { 0x24 /* VKEY_HOME */, kVK_Home },
- { 0x25 /* VKEY_LEFT */, kVK_LeftArrow },
- { 0x26 /* VKEY_UP */, kVK_UpArrow },
- { 0x27 /* VKEY_RIGHT */, kVK_RightArrow },
- { 0x28 /* VKEY_DOWN */, kVK_DownArrow },
- { 0x29 /* VKEY_SELECT */, -1 },
- { 0x2A /* VKEY_PRINT */, -1 },
- { 0x2B /* VKEY_EXECUTE */, -1 },
- { 0x2C /* VKEY_SNAPSHOT */, -1 },
- { 0x2D /* VKEY_INSERT */, kVK_Help },
- { 0x2E /* VKEY_DELETE */, kVK_ForwardDelete },
- { 0x2F /* VKEY_HELP */, kVK_Help },
- { 0x30 /* VKEY_0 */, kVK_ANSI_0 },
- { 0x31 /* VKEY_1 */, kVK_ANSI_1 },
- { 0x32 /* VKEY_2 */, kVK_ANSI_2 },
- { 0x33 /* VKEY_3 */, kVK_ANSI_3 },
- { 0x34 /* VKEY_4 */, kVK_ANSI_4 },
- { 0x35 /* VKEY_5 */, kVK_ANSI_5 },
- { 0x36 /* VKEY_6 */, kVK_ANSI_6 },
- { 0x37 /* VKEY_7 */, kVK_ANSI_7 },
- { 0x38 /* VKEY_8 */, kVK_ANSI_8 },
- { 0x39 /* VKEY_9 */, kVK_ANSI_9 },
- { 0x41 /* VKEY_A */, kVK_ANSI_A },
- { 0x42 /* VKEY_B */, kVK_ANSI_B },
- { 0x43 /* VKEY_C */, kVK_ANSI_C },
- { 0x44 /* VKEY_D */, kVK_ANSI_D },
- { 0x45 /* VKEY_E */, kVK_ANSI_E },
- { 0x46 /* VKEY_F */, kVK_ANSI_F },
- { 0x47 /* VKEY_G */, kVK_ANSI_G },
- { 0x48 /* VKEY_H */, kVK_ANSI_H },
- { 0x49 /* VKEY_I */, kVK_ANSI_I },
- { 0x4A /* VKEY_J */, kVK_ANSI_J },
- { 0x4B /* VKEY_K */, kVK_ANSI_K },
- { 0x4C /* VKEY_L */, kVK_ANSI_L },
- { 0x4D /* VKEY_M */, kVK_ANSI_M },
- { 0x4E /* VKEY_N */, kVK_ANSI_N },
- { 0x4F /* VKEY_O */, kVK_ANSI_O },
- { 0x50 /* VKEY_P */, kVK_ANSI_P },
- { 0x51 /* VKEY_Q */, kVK_ANSI_Q },
- { 0x52 /* VKEY_R */, kVK_ANSI_R },
- { 0x53 /* VKEY_S */, kVK_ANSI_S },
- { 0x54 /* VKEY_T */, kVK_ANSI_T },
- { 0x55 /* VKEY_U */, kVK_ANSI_U },
- { 0x56 /* VKEY_V */, kVK_ANSI_V },
- { 0x57 /* VKEY_W */, kVK_ANSI_W },
- { 0x58 /* VKEY_X */, kVK_ANSI_X },
- { 0x59 /* VKEY_Y */, kVK_ANSI_Y },
- { 0x5A /* VKEY_Z */, kVK_ANSI_Z },
- { 0x5B /* VKEY_LWIN */, kVK_Command },
- { 0x5C /* VKEY_RWIN */, kVK_RightCommand },
- { 0x5D /* VKEY_APPS */, kVK_RightCommand },
- { 0x5F /* VKEY_SLEEP */, -1 },
- { 0x60 /* VKEY_NUMPAD0 */, kVK_ANSI_Keypad0 },
- { 0x61 /* VKEY_NUMPAD1 */, kVK_ANSI_Keypad1 },
- { 0x62 /* VKEY_NUMPAD2 */, kVK_ANSI_Keypad2 },
- { 0x63 /* VKEY_NUMPAD3 */, kVK_ANSI_Keypad3 },
- { 0x64 /* VKEY_NUMPAD4 */, kVK_ANSI_Keypad4 },
- { 0x65 /* VKEY_NUMPAD5 */, kVK_ANSI_Keypad5 },
- { 0x66 /* VKEY_NUMPAD6 */, kVK_ANSI_Keypad6 },
- { 0x67 /* VKEY_NUMPAD7 */, kVK_ANSI_Keypad7 },
- { 0x68 /* VKEY_NUMPAD8 */, kVK_ANSI_Keypad8 },
- { 0x69 /* VKEY_NUMPAD9 */, kVK_ANSI_Keypad9 },
- { 0x6A /* VKEY_MULTIPLY */, kVK_ANSI_KeypadMultiply },
- { 0x6B /* VKEY_ADD */, kVK_ANSI_KeypadPlus },
- { 0x6C /* VKEY_SEPARATOR */, -1 },
- { 0x6D /* VKEY_SUBTRACT */, kVK_ANSI_KeypadMinus },
- { 0x6E /* VKEY_DECIMAL */, kVK_ANSI_KeypadDecimal },
- { 0x6F /* VKEY_DIVIDE */, kVK_ANSI_KeypadDivide },
- { 0x70 /* VKEY_F1 */, kVK_F1 },
- { 0x71 /* VKEY_F2 */, kVK_F2 },
- { 0x72 /* VKEY_F3 */, kVK_F3 },
- { 0x73 /* VKEY_F4 */, kVK_F4 },
- { 0x74 /* VKEY_F5 */, kVK_F5 },
- { 0x75 /* VKEY_F6 */, kVK_F6 },
- { 0x76 /* VKEY_F7 */, kVK_F7 },
- { 0x77 /* VKEY_F8 */, kVK_F8 },
- { 0x78 /* VKEY_F9 */, kVK_F9 },
- { 0x79 /* VKEY_F10 */, kVK_F10 },
- { 0x7A /* VKEY_F11 */, kVK_F11 },
- { 0x7B /* VKEY_F12 */, kVK_F12 },
- { 0x7C /* VKEY_F13 */, kVK_F13 },
- { 0x7D /* VKEY_F14 */, kVK_F14 },
- { 0x7E /* VKEY_F15 */, kVK_F15 },
- { 0x7F /* VKEY_F16 */, kVK_F16 },
- { 0x80 /* VKEY_F17 */, kVK_F17 },
- { 0x81 /* VKEY_F18 */, kVK_F18 },
- { 0x82 /* VKEY_F19 */, kVK_F19 },
- { 0x83 /* VKEY_F20 */, kVK_F20 },
- { 0x84 /* VKEY_F21 */, -1 },
- { 0x85 /* VKEY_F22 */, -1 },
- { 0x86 /* VKEY_F23 */, -1 },
- { 0x87 /* VKEY_F24 */, -1 },
- { 0x90 /* VKEY_NUMLOCK */, -1 },
- { 0x91 /* VKEY_SCROLL */, -1 },
- { 0xA0 /* VKEY_LSHIFT */, kVK_Shift },
- { 0xA1 /* VKEY_RSHIFT */, kVK_RightShift },
- { 0xA2 /* VKEY_LCONTROL */, kVK_Control },
- { 0xA3 /* VKEY_RCONTROL */, kVK_RightControl },
- { 0xA4 /* VKEY_LMENU */, kVK_Option },
- { 0xA5 /* VKEY_RMENU */, kVK_RightOption },
- { 0xA6 /* VKEY_BROWSER_BACK */, -1 },
- { 0xA7 /* VKEY_BROWSER_FORWARD */, -1 },
- { 0xA8 /* VKEY_BROWSER_REFRESH */, -1 },
- { 0xA9 /* VKEY_BROWSER_STOP */, -1 },
- { 0xAA /* VKEY_BROWSER_SEARCH */, -1 },
- { 0xAB /* VKEY_BROWSER_FAVORITES */, -1 },
- { 0xAC /* VKEY_BROWSER_HOME */, -1 },
- { 0xAD /* VKEY_VOLUME_MUTE */, -1 },
- { 0xAE /* VKEY_VOLUME_DOWN */, -1 },
- { 0xAF /* VKEY_VOLUME_UP */, -1 },
- { 0xB0 /* VKEY_MEDIA_NEXT_TRACK */, -1 },
- { 0xB1 /* VKEY_MEDIA_PREV_TRACK */, -1 },
- { 0xB2 /* VKEY_MEDIA_STOP */, -1 },
- { 0xB3 /* VKEY_MEDIA_PLAY_PAUSE */, -1 },
- { 0xB4 /* VKEY_MEDIA_LAUNCH_MAIL */, -1 },
- { 0xB5 /* VKEY_MEDIA_LAUNCH_MEDIA_SELECT */, -1 },
- { 0xB6 /* VKEY_MEDIA_LAUNCH_APP1 */, -1 },
- { 0xB7 /* VKEY_MEDIA_LAUNCH_APP2 */, -1 },
- { 0xBA /* VKEY_OEM_1 */, kVK_ANSI_Semicolon },
- { 0xBB /* VKEY_OEM_PLUS */, kVK_ANSI_Equal },
- { 0xBC /* VKEY_OEM_COMMA */, kVK_ANSI_Comma },
- { 0xBD /* VKEY_OEM_MINUS */, kVK_ANSI_Minus },
- { 0xBE /* VKEY_OEM_PERIOD */, kVK_ANSI_Period },
- { 0xBF /* VKEY_OEM_2 */, kVK_ANSI_Slash },
- { 0xC0 /* VKEY_OEM_3 */, kVK_ANSI_Grave },
- { 0xDB /* VKEY_OEM_4 */, kVK_ANSI_LeftBracket },
- { 0xDC /* VKEY_OEM_5 */, kVK_ANSI_Backslash },
- { 0xDD /* VKEY_OEM_6 */, kVK_ANSI_RightBracket },
- { 0xDE /* VKEY_OEM_7 */, kVK_ANSI_Quote },
- { 0xDF /* VKEY_OEM_8 */, -1 },
- { 0xE2 /* VKEY_OEM_102 */, -1 },
- { 0xE5 /* VKEY_PROCESSKEY */, -1 },
- { 0xE7 /* VKEY_PACKET */, -1 },
- { 0xF6 /* VKEY_ATTN */, -1 },
- { 0xF7 /* VKEY_CRSEL */, -1 },
- { 0xF8 /* VKEY_EXSEL */, -1 },
- { 0xF9 /* VKEY_EREOF */, -1 },
- { 0xFA /* VKEY_PLAY */, -1 },
- { 0xFB /* VKEY_ZOOM */, -1 },
- { 0xFC /* VKEY_NONAME */, -1 },
- { 0xFD /* VKEY_PA1 */, -1 },
- { 0xFE /* VKEY_OEM_CLEAR */, kVK_ANSI_KeypadClear }
-};
- // clang-format on
-
/**
- * @brief Translate a platform keycode to a Sunshine key symbol.
- *
- * @param keycode Platform keycode being translated or emitted.
- * @return Sunshine key symbol, or 0 when the keycode is unmapped.
+ * @brief Per-client libvirtualhid devices.
*/
- int keysym(int keycode) {
- KeyCodeMap key_map {};
-
- key_map.win_keycode = keycode;
- const KeyCodeMap *temp_map = std::lower_bound(
- kKeyCodesMap,
- kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]),
- key_map
- );
-
- if (temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) ||
- temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) {
- return -1;
- }
-
- return temp_map->mac_keycode;
- }
+ struct client_input_raw_t: client_input_t {
+ /**
+ * @brief Create per-client libvirtualhid devices.
+ *
+ * @param input Platform input backend that receives the event.
+ */
+ explicit client_input_raw_t(input_t &input):
+ virtualhid {((input_raw_t *) input.get())->virtualhid} {}
- /**
- * @brief macOS modifier flags split into generic and device-specific bits.
- */
- struct modifier_flags_t {
- CGEventFlags generic {}; ///< Modifier bits represented by device-independent CoreGraphics flags.
- CGEventFlags device {}; ///< Modifier bits represented by left/right device-specific flags.
- CGEventFlags all_devices {}; ///< Mask covering all device-specific variants for this modifier.
+ virtualhid::client_context_t virtualhid; ///< libvirtualhid client context.
};
- /**
- * @brief Resolve the CoreGraphics modifier masks associated with a key code.
- *
- * @param key Sunshine key code that may represent a modifier key.
- * @param flags Output masks for the matching CoreGraphics modifier.
- * @return True when modifier flags were found for the key.
- */
- bool modifier_flags_for_key(int key, modifier_flags_t &flags) {
- switch (key) {
- case kVK_Shift:
- flags = {kCGEventFlagMaskShift, NX_DEVICELSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK | NX_DEVICERSHIFTKEYMASK};
- return true;
- case kVK_RightShift:
- flags = {kCGEventFlagMaskShift, NX_DEVICERSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK | NX_DEVICERSHIFTKEYMASK};
- return true;
- case kVK_Command:
- flags = {kCGEventFlagMaskCommand, NX_DEVICELCMDKEYMASK, NX_DEVICELCMDKEYMASK | NX_DEVICERCMDKEYMASK};
- return true;
- case kVK_RightCommand:
- flags = {kCGEventFlagMaskCommand, NX_DEVICERCMDKEYMASK, NX_DEVICELCMDKEYMASK | NX_DEVICERCMDKEYMASK};
- return true;
- case kVK_Option:
- flags = {kCGEventFlagMaskAlternate, NX_DEVICELALTKEYMASK, NX_DEVICELALTKEYMASK | NX_DEVICERALTKEYMASK};
- return true;
- case kVK_RightOption:
- flags = {kCGEventFlagMaskAlternate, NX_DEVICERALTKEYMASK, NX_DEVICELALTKEYMASK | NX_DEVICERALTKEYMASK};
- return true;
- case kVK_Control:
- flags = {kCGEventFlagMaskControl, NX_DEVICELCTLKEYMASK, NX_DEVICELCTLKEYMASK | NX_DEVICERCTLKEYMASK};
- return true;
- case kVK_RightControl:
- flags = {kCGEventFlagMaskControl, NX_DEVICERCTLKEYMASK, NX_DEVICELCTLKEYMASK | NX_DEVICERCTLKEYMASK};
- return true;
- default:
- return false;
- }
- }
-
- void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
- auto key = keysym(modcode);
-
- BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release;
-
- if (key < 0) {
- return;
- }
-
- auto macos_input = ((macos_input_t *) input.get());
- CGEventRef event = nullptr;
- modifier_flags_t modifier_flags;
-
- if (modifier_flags_for_key(key, modifier_flags)) {
- event = CGEventCreateKeyboardEvent(macos_input->keyboard_source, key, !release);
- if (!event) {
- return;
- }
-
- CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key);
-
- if (release) {
- macos_input->kb_flags &= ~modifier_flags.device;
- if ((macos_input->kb_flags & modifier_flags.all_devices) == 0) {
- macos_input->kb_flags &= ~modifier_flags.generic;
- }
- } else {
- macos_input->kb_flags |= modifier_flags.generic | modifier_flags.device;
- }
-
- CGEventSetType(event, kCGEventFlagsChanged);
- } else {
- event = CGEventCreateKeyboardEvent(macos_input->keyboard_source, key, !release);
- if (!event) {
- return;
- }
-
- CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown);
- }
-
- CGEventSetFlags(event, macos_input->kb_flags);
- CGEventPost(kCGSessionEventTap, event);
- CFRelease(event);
- }
-
- void unicode(input_t &input, char *utf8, int size) {
- BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv;
- }
-
- int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
- BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv;
- return -1;
- }
-
- void free_gamepad(input_t &input, int nr) {
- BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv;
+ input_t input() {
+ return {new input_raw_t {}};
}
- void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
- BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv;
+ std::unique_ptr allocate_client_input_context(input_t &input) {
+ return std::make_unique(input);
}
- // returns current mouse location:
- util::point_t get_mouse_loc(input_t &input) {
- // Creating a new event every time to avoid any reuse risk
- const auto macos_input = static_cast(input.get());
- const auto snapshot_event = CGEventCreate(macos_input->source);
- const auto current = CGEventGetLocation(snapshot_event);
- CFRelease(snapshot_event);
- return util::point_t {
- current.x,
- current.y
- };
+ void freeInput(void *p) {
+ auto *input = (input_raw_t *) p;
+ delete input;
}
- /**
- * @brief Post a mouse event at the clamped display location.
- *
- * @param input Platform input context.
- * @param button Mouse button.
- * @param type CoreGraphics mouse event type.
- * @param raw_location Requested mouse location.
- * @param previous_location Previous mouse location.
- * @param click_count Click count for the event.
- */
- void post_mouse(
- input_t &input,
- const CGMouseButton button,
- const CGEventType type,
- const util::point_t raw_location,
- const util::point_t previous_location,
- const int click_count
- ) {
- BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << raw_location.x << ":"sv << raw_location.y << " click_count: "sv << click_count;
-
- const auto macos_input = static_cast(input.get());
- const auto display = macos_input->display;
- const auto event = macos_input->mouse_event;
-
- // get display bounds for current display
- const CGRect display_bounds = CGDisplayBounds(display);
-
- // limit mouse to current display bounds
- const auto location = CGPoint {
- std::clamp(raw_location.x, display_bounds.origin.x, display_bounds.origin.x + display_bounds.size.width - 1),
- std::clamp(raw_location.y, display_bounds.origin.y, display_bounds.origin.y + display_bounds.size.height - 1)
- };
-
- CGEventSetType(event, type);
- CGEventSetLocation(event, location);
- CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button);
- CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count);
-
- // Include deltas so some 3D applications can consume changes (game cameras, etc)
- const double deltaX = raw_location.x - previous_location.x;
- const double deltaY = raw_location.y - previous_location.y;
- CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
- CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
-
- // Inject modifier flags into mouse events so that shift+click and similar combinations work correctly.
- CGEventSetFlags(event, macos_input->kb_flags);
- CGEventPost(kCGHIDEventTap, event);
- // For why this is here, see:
- // https://stackoverflow.com/questions/15194409/simulated-mouseevent-not-working-properly-osx
- CGWarpMouseCursorPosition(location);
+ void move_mouse(input_t &input, int deltaX, int deltaY) {
+ virtualhid::move_mouse(((input_raw_t *) input.get())->virtualhid, deltaX, deltaY);
}
- /**
- * @brief Choose the CoreGraphics mouse event type for the current button action.
- *
- * @param input Platform input backend that receives the event.
- * @return CoreGraphics mouse event type for button press or release.
- */
- inline CGEventType event_type_mouse(input_t &input) {
- const auto macos_input = static_cast(input.get());
-
- if (macos_input->mouse_down[0]) {
- return kCGEventLeftMouseDragged;
- }
- if (macos_input->mouse_down[1]) {
- return kCGEventOtherMouseDragged;
- }
- if (macos_input->mouse_down[2]) {
- return kCGEventRightMouseDragged;
- }
- return kCGEventMouseMoved;
+ void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
+ virtualhid::abs_mouse(((input_raw_t *) input.get())->virtualhid, touch_port, x, y);
}
- void move_mouse(
- input_t &input,
- const int deltaX,
- const int deltaY
- ) {
- const auto current = get_mouse_loc(input);
-
- const auto location = util::point_t {current.x + deltaX, current.y + deltaY};
- post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, current, 0);
+ void button_mouse(input_t &input, int button, bool release) {
+ virtualhid::button_mouse(((input_raw_t *) input.get())->virtualhid, button, release);
}
- void abs_mouse(
- input_t &input,
- const touch_port_t &touch_port,
- const float x,
- const float y
- ) {
- const auto macos_input = static_cast(input.get());
- const auto scaling = macos_input->displayScaling;
- const auto display = macos_input->display;
-
- auto location = util::point_t {x * scaling, y * scaling};
- CGRect display_bounds = CGDisplayBounds(display);
- // in order to get the correct mouse location for capturing display , we need to add the display bounds to the location
- location.x += display_bounds.origin.x;
- location.y += display_bounds.origin.y;
-
- post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, get_mouse_loc(input), 0);
+ void scroll(input_t &input, int high_res_distance) {
+ virtualhid::scroll(((input_raw_t *) input.get())->virtualhid, high_res_distance);
}
- void button_mouse(input_t &input, const int button, const bool release) {
- CGMouseButton mac_button;
- CGEventType event;
-
- const auto macos_input = static_cast(input.get());
-
- switch (button) {
- case 1:
- mac_button = kCGMouseButtonLeft;
- event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown;
- break;
- case 2:
- mac_button = kCGMouseButtonCenter;
- event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown;
- break;
- case 3:
- mac_button = kCGMouseButtonRight;
- event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown;
- break;
- default:
- BOOST_LOG(warning) << "Unsupported mouse button for MacOS: "sv << button;
- return;
- }
-
- macos_input->mouse_down[mac_button] = !release;
-
- // if the last mouse down was less than MULTICLICK_DELAY_MS, we send a double click event
- const auto now = std::chrono::steady_clock::now();
- const auto mouse_position = get_mouse_loc(input);
-
- if (now < macos_input->last_mouse_event[mac_button][release] + MULTICLICK_DELAY_MS) {
- post_mouse(input, mac_button, event, mouse_position, mouse_position, 2);
- } else {
- post_mouse(input, mac_button, event, mouse_position, mouse_position, 1);
- }
-
- macos_input->last_mouse_event[mac_button][release] = now;
- }
-
- /**
- * @brief Get scroll lines per detent.
- *
- * @param scrollwheel_scaling Scrollwheel scaling.
- * @return Number of logical lines represented by one wheel detent.
- */
- int get_scroll_lines_per_detent(double &scrollwheel_scaling) {
- double scale = DEFAULT_SCROLLWHEEL_SCALING;
- const auto value = CFPreferencesCopyValue(CFSTR("com.apple.scrollwheel.scaling"), kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
- if (value) {
- if (CFGetTypeID(value) == CFNumberGetTypeID()) {
- CFNumberGetValue(static_cast(value), kCFNumberDoubleType, &scale);
- } else if (CFGetTypeID(value) == CFStringGetTypeID()) {
- scale = CFStringGetDoubleValue(static_cast(value));
- }
- CFRelease(value);
- }
-
- if (!std::isfinite(scale)) {
- scale = DEFAULT_SCROLLWHEEL_SCALING;
- }
-
- scrollwheel_scaling = scale;
-
- // com.apple.scrollwheel.scaling stores the Mouse scroll speed slider position, not
- // the scroll multiplier itself. The slider is 0..1 and Apple's default is 0.3125,
- // so anchor 0 at one line per wheel detent and 0.3125 at five lines.
- const auto scroll_scale = std::clamp(scale, 0.0, 1.0);
- constexpr double lines_per_scroll_scale = (DEFAULT_SCROLL_LINES_PER_DETENT - 1.0) / DEFAULT_SCROLLWHEEL_SCALING;
-
- return std::max(1, static_cast(std::ceil(1.0 + scroll_scale * lines_per_scroll_scale)));
+ void hscroll(input_t &input, int high_res_distance) {
+ virtualhid::hscroll(((input_raw_t *) input.get())->virtualhid, high_res_distance);
}
- /**
- * @brief Convert a high-resolution wheel delta to CoreGraphics scroll pixels.
- *
- * @param macos_input macOS input state containing scroll scaling settings.
- * @param high_res_distance Wheel delta in Windows high-resolution units.
- * @return Pixel distance to send to CoreGraphics.
- */
- int scroll_pixels(const macos_input_t *macos_input, const int high_res_distance) {
- const auto source_pixels_per_line = CGEventSourceGetPixelsPerLine(macos_input->source);
- const auto pixels_per_line = source_pixels_per_line > 0 ? static_cast(source_pixels_per_line + 0.5) : 10;
- const auto scaled_pixels = static_cast(high_res_distance) * std::max(1, pixels_per_line) * std::max(1, macos_input->scroll_lines_per_detent);
-
- return static_cast(scaled_pixels / WHEEL_DELTA);
+ void keyboard_update(input_t &input, std::uint16_t modcode, bool release, std::uint8_t flags) {
+ virtualhid::keyboard_update(((input_raw_t *) input.get())->virtualhid, modcode, release, flags);
}
- /**
- * @brief Post a macOS scroll event to the target display.
- *
- * @param input Platform input backend that receives the event.
- * @param wheelY Wheel y.
- * @param wheelX Wheel x.
- */
- void post_scroll(input_t &input, const int wheelY, const int wheelX) {
- if (wheelY == 0 && wheelX == 0) {
- return;
- }
-
- const auto macos_input = static_cast(input.get());
- CGEventRef event = CGEventCreateScrollWheelEvent(macos_input->source, kCGScrollEventUnitPixel, 2, wheelY, wheelX);
- if (!event) {
- return;
- }
-
- CGEventSetIntegerValueField(event, kCGScrollWheelEventIsContinuous, 1);
- CGEventPost(kCGHIDEventTap, event);
- CFRelease(event);
+ void unicode(input_t &input, char *utf8, int size) {
+ virtualhid::unicode(((input_raw_t *) input.get())->virtualhid, utf8, size);
}
- void scroll(input_t &input, const int high_res_distance) {
- post_scroll(input, scroll_pixels(static_cast(input.get()), high_res_distance), 0);
+ void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {
+ virtualhid::touch_update(((client_input_raw_t *) input)->virtualhid, touch_port, touch);
}
- void hscroll(input_t &input, int high_res_distance) {
- post_scroll(input, 0, scroll_pixels(static_cast(input.get()), high_res_distance));
+ void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {
+ virtualhid::pen_update(((client_input_raw_t *) input)->virtualhid, touch_port, pen);
}
- /**
- * @brief Allocates a context to store per-client input data.
- * @param input The global input context.
- * @return A unique pointer to a per-client input data context.
- */
- std::unique_ptr allocate_client_input_context(input_t &input) {
- // Unused
- return nullptr;
+ int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
+ return virtualhid::alloc_gamepad(((input_raw_t *) input.get())->virtualhid, id, metadata, std::move(feedback_queue));
}
- /**
- * @brief Sends a touch event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param touch The touch event.
- */
- void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {
- // Unimplemented feature - platform_caps::pen_touch
+ void free_gamepad(input_t &input, int nr) {
+ virtualhid::free_gamepad(((input_raw_t *) input.get())->virtualhid, nr);
}
- /**
- * @brief Sends a pen event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param pen The pen event.
- */
- void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {
- // Unimplemented feature - platform_caps::pen_touch
+ void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
+ virtualhid::gamepad_update(((input_raw_t *) input.get())->virtualhid, nr, gamepad_state);
}
- /**
- * @brief Sends a gamepad touch event to the OS.
- * @param input The global input context.
- * @param touch The touch event.
- */
void gamepad_touch(input_t &input, const gamepad_touch_t &touch) {
- // Unimplemented feature - platform_caps::controller_touch
+ virtualhid::gamepad_touch(((input_raw_t *) input.get())->virtualhid, touch);
}
- /**
- * @brief Sends a gamepad motion event to the OS.
- * @param input The global input context.
- * @param motion The motion event.
- */
void gamepad_motion(input_t &input, const gamepad_motion_t &motion) {
- // Unimplemented
+ virtualhid::gamepad_motion(((input_raw_t *) input.get())->virtualhid, motion);
}
- /**
- * @brief Sends a gamepad battery event to the OS.
- * @param input The global input context.
- * @param battery The battery event.
- */
void gamepad_battery(input_t &input, const gamepad_battery_t &battery) {
- // Unimplemented
+ virtualhid::gamepad_battery(((input_raw_t *) input.get())->virtualhid, battery);
}
- input_t input() {
- input_t result {new macos_input_t()};
-
- const auto macos_input = static_cast(result.get());
-
- // Default to main display
- macos_input->display = CGMainDisplayID();
-
- auto output_name = display_device::map_output_name(config::video.output_name);
- // If output_name is set, try to find the display with that display id
- if (!output_name.empty()) {
- const int MAX_DISPLAYS = 32;
- uint32_t max_display = MAX_DISPLAYS;
- uint32_t display_count;
- CGDirectDisplayID displays[MAX_DISPLAYS];
- if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) {
- BOOST_LOG(error) << "Unable to get active display list , error: "sv << std::endl;
- } else {
- for (int i = 0; i < display_count; i++) {
- CGDirectDisplayID display_id = displays[i];
- if (display_id == std::atoi(output_name.c_str())) {
- macos_input->display = display_id;
- }
- }
- }
+ std::optional get_mouse_loc(input_t & /*input*/) {
+ const auto event = CGEventCreate(nullptr);
+ if (!event) {
+ return std::nullopt;
}
- // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor
- const CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display);
- macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode));
- CFRelease(mode);
-
- macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
- macos_input->keyboard_source = CGEventSourceCreate(kCGEventSourceStatePrivate);
- macos_input->scroll_lines_per_detent = get_scroll_lines_per_detent(macos_input->scrollwheel_scaling);
-
- macos_input->kb_flags = 0;
-
- macos_input->mouse_event = CGEventCreate(macos_input->source);
- macos_input->mouse_down[0] = false;
- macos_input->mouse_down[1] = false;
- macos_input->mouse_down[2] = false;
-
- BOOST_LOG(debug) << "macOS scroll speed: com.apple.scrollwheel.scaling="sv << macos_input->scrollwheel_scaling << ", lines per detent="sv << macos_input->scroll_lines_per_detent << ", pixels per line="sv << CGEventSourceGetPixelsPerLine(macos_input->source);
- BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimension: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display);
-
- return result;
+ const auto current = CGEventGetLocation(event);
+ CFRelease(event);
+ return util::point_t {current.x, current.y};
}
- void freeInput(void *p) {
- const auto *input = static_cast(p);
+ platform_caps::caps_t get_capabilities() {
+ platform_caps::caps_t caps = 0;
+ const auto runtime = virtualhid::create_runtime();
+ if (!runtime) {
+ return caps;
+ }
- CFRelease(input->source);
- CFRelease(input->keyboard_source);
- CFRelease(input->mouse_event);
+ const auto &capabilities = runtime->capabilities();
+ if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_touchpad()) {
+ caps |= platform_caps::controller_touch;
+ }
+ if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
+ caps |= platform_caps::pen_touch;
+ }
- delete input;
+ return caps;
}
std::vector &supported_gamepads(input_t *input) {
- static std::vector gamepads {
- supported_gamepad_t {"", false, "gamepads.macos_not_implemented"}
- };
+ static std::vector gamepads;
+ if (!input || !input->get()) {
+ gamepads = virtualhid::static_supported_gamepads();
+ return gamepads;
+ }
+ const auto raw = (input_raw_t *) input->get();
+ gamepads = virtualhid::supported_gamepads(raw->virtualhid.runtime.get());
return gamepads;
}
- /**
- * @brief Returns the supported platform capabilities to advertise to the client.
- * @return Capability flags.
- */
- platform_caps::caps_t get_capabilities() {
- return 0;
- }
} // namespace platf
diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp
new file mode 100644
index 00000000000..baded4737d2
--- /dev/null
+++ b/src/platform/virtualhid_input.cpp
@@ -0,0 +1,838 @@
+/**
+ * @file src/platform/virtualhid_input.cpp
+ * @brief Definitions for libvirtualhid-backed input helpers.
+ */
+
+// standard includes
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// local includes
+#include "src/config.h"
+#include "src/logging.h"
+#include "virtualhid_input.h"
+
+using namespace std::literals;
+
+namespace platf::virtualhid {
+ /**
+ * @brief Runtime state for one virtual gamepad.
+ */
+ struct gamepad_context_t {
+ std::unique_ptr adapter; ///< State adapter for the virtual gamepad.
+ feedback_queue_t feedback_queue; ///< Feedback queue for client output events.
+ std::array, 2> touch_ids; ///< Client touch IDs assigned to libvirtualhid slots.
+ std::uint8_t client_relative_index = 0; ///< Client-relative controller index.
+ bool has_last_rumble = false; ///< Whether last rumble values are valid.
+ std::uint16_t last_low_frequency_rumble = 0; ///< Last low-frequency rumble value.
+ std::uint16_t last_high_frequency_rumble = 0; ///< Last high-frequency rumble value.
+ bool has_last_trigger_rumble = false; ///< Whether last trigger rumble values are valid.
+ std::uint16_t last_left_trigger_rumble = 0; ///< Last left trigger rumble value.
+ std::uint16_t last_right_trigger_rumble = 0; ///< Last right trigger rumble value.
+ bool has_last_rgb = false; ///< Whether last RGB values are valid.
+ std::uint8_t last_red = 0; ///< Last red LED value.
+ std::uint8_t last_green = 0; ///< Last green LED value.
+ std::uint8_t last_blue = 0; ///< Last blue LED value.
+ };
+
+ namespace {
+
+ /**
+ * @brief Gamepad profile exposed through Sunshine config.
+ */
+ struct gamepad_profile_t {
+ std::string_view name; ///< Sunshine config value.
+ lvh::GamepadProfileKind kind; ///< libvirtualhid profile kind.
+ lvh::DeviceProfile (*profile)(); ///< Profile factory.
+ };
+
+ /**
+ * @brief Supported libvirtualhid gamepad profiles.
+ */
+ constexpr std::array gamepad_profiles {
+ gamepad_profile_t {"generic", lvh::GamepadProfileKind::generic, lvh::profiles::generic_gamepad},
+ gamepad_profile_t {"x360", lvh::GamepadProfileKind::xbox_360, lvh::profiles::xbox_360},
+ gamepad_profile_t {"xone", lvh::GamepadProfileKind::xbox_one, lvh::profiles::xbox_one},
+ gamepad_profile_t {"xseries", lvh::GamepadProfileKind::xbox_series, lvh::profiles::xbox_series},
+ gamepad_profile_t {"ds4", lvh::GamepadProfileKind::dualshock4, lvh::profiles::dualshock4},
+ gamepad_profile_t {"ds5", lvh::GamepadProfileKind::dualsense, lvh::profiles::dualsense},
+ gamepad_profile_t {"switch", lvh::GamepadProfileKind::switch_pro, lvh::profiles::switch_pro},
+ };
+
+ void log_failure(std::string_view operation, const lvh::OperationStatus &status) {
+ if (!status.ok()) {
+ BOOST_LOG(warning) << operation << ": "sv << status.message();
+ }
+ }
+
+ float normalize_axis(std::int16_t value) {
+ if (value < 0) {
+ return std::max(-1.0F, static_cast(value) / 32768.0F);
+ }
+
+ return std::min(1.0F, static_cast(value) / 32767.0F);
+ }
+
+ float normalize_trigger(std::uint8_t value) {
+ return static_cast(value) / static_cast(std::numeric_limits::max());
+ }
+
+ lvh::ClientControllerType client_controller_type(std::uint8_t type) {
+ switch (type) {
+ case LI_CTYPE_XBOX:
+ return lvh::ClientControllerType::xbox;
+ case LI_CTYPE_PS:
+ return lvh::ClientControllerType::playstation;
+ case LI_CTYPE_NINTENDO:
+ return lvh::ClientControllerType::nintendo;
+ case LI_CTYPE_UNKNOWN:
+ default:
+ return lvh::ClientControllerType::unknown;
+ }
+ }
+
+ const gamepad_profile_t &profile_for_name(std::string_view name) {
+ const auto iter = std::ranges::find(gamepad_profiles, name, &gamepad_profile_t::name);
+ if (iter != gamepad_profiles.end()) {
+ return *iter;
+ }
+
+ return gamepad_profiles[3]; // Xbox Series.
+ }
+
+ const gamepad_profile_t &profile_for_metadata(const gamepad_arrival_t &metadata) {
+ if (config::input.gamepad != "auto"sv) {
+ return profile_for_name(config::input.gamepad);
+ }
+
+ if (metadata.type == LI_CTYPE_PS) {
+ BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by client-reported type)"sv;
+ return profile_for_name("ds5"sv);
+ }
+ if (metadata.type == LI_CTYPE_NINTENDO) {
+ BOOST_LOG(info) << "Gamepad will be Nintendo Switch Pro controller (auto-selected by client-reported type)"sv;
+ return profile_for_name("switch"sv);
+ }
+ if (metadata.type == LI_CTYPE_XBOX) {
+ BOOST_LOG(info) << "Gamepad will be Xbox Series controller (auto-selected by client-reported type)"sv;
+ return profile_for_name("xseries"sv);
+ }
+ if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) {
+ BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by motion sensor presence)"sv;
+ return profile_for_name("ds5"sv);
+ }
+ if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) {
+ BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by touchpad presence)"sv;
+ return profile_for_name("ds5"sv);
+ }
+
+ BOOST_LOG(info) << "Gamepad will be Xbox Series controller (default)"sv;
+ return profile_for_name("xseries"sv);
+ }
+
+ std::string random_private_mac() {
+ std::random_device random;
+ return std::format(
+ "02:00:{:02x}:{:02x}:{:02x}:{:02x}",
+ random() & 0xFF,
+ random() & 0xFF,
+ random() & 0xFF,
+ random() & 0xFF
+ );
+ }
+
+ std::string gamepad_stable_id(const gamepad_id_t &id, const lvh::DeviceProfile &profile) {
+ if (profile.gamepad_kind != lvh::GamepadProfileKind::dualshock4 &&
+ profile.gamepad_kind != lvh::GamepadProfileKind::dualsense) {
+ return std::format("sunshine-gamepad-{}", id.globalIndex);
+ }
+
+ if (config::input.virtualhid_randomize_mac || id.globalIndex < 0 || id.globalIndex > 255) {
+ return random_private_mac();
+ }
+
+ return std::format("02:00:00:00:00:{:02x}", id.globalIndex);
+ }
+
+ lvh::GamepadMetadata gamepad_metadata(const gamepad_id_t &id, const gamepad_arrival_t &metadata, const lvh::DeviceProfile &profile) {
+ lvh::GamepadMetadata result;
+ result.global_index = id.globalIndex;
+ result.client_relative_index = id.clientRelativeIndex;
+ result.client_type = client_controller_type(metadata.type);
+ result.has_motion_sensors = metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO);
+ result.has_touchpad = metadata.capabilities & LI_CCAP_TOUCHPAD;
+ result.has_rgb_led = metadata.capabilities & LI_CCAP_RGB_LED;
+ result.has_battery = metadata.capabilities & LI_CCAP_BATTERY_STATE;
+ result.stable_id = gamepad_stable_id(id, profile);
+ return result;
+ }
+
+ void warn_unsupported_client_features(int global_index, const gamepad_arrival_t &metadata, const lvh::GamepadProfileSupport &support) {
+ if ((metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) && !support.supports_motion) {
+ BOOST_LOG(warning) << "Gamepad "sv << global_index << " has motion sensors, but the selected virtual profile cannot expose them"sv;
+ }
+ if ((metadata.capabilities & LI_CCAP_TOUCHPAD) && !support.supports_touchpad) {
+ BOOST_LOG(warning) << "Gamepad "sv << global_index << " has a touchpad, but the selected virtual profile cannot expose it"sv;
+ }
+ if ((metadata.capabilities & LI_CCAP_RGB_LED) && !support.supports_rgb_led) {
+ BOOST_LOG(warning) << "Gamepad "sv << global_index << " has an RGB LED, but the selected virtual profile cannot expose it"sv;
+ }
+ }
+
+ void warn_missing_client_features(int global_index, const gamepad_arrival_t &metadata, const lvh::GamepadProfileSupport &support) {
+ if (support.supports_motion && !(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) {
+ BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating a motion-capable controller, but the client gamepad does not have motion sensors active"sv;
+ }
+ if (support.supports_touchpad && !(metadata.capabilities & LI_CCAP_TOUCHPAD)) {
+ BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating a touchpad-capable controller, but the client gamepad does not have a touchpad"sv;
+ }
+ }
+
+ lvh::GamepadState make_gamepad_state(const gamepad_state_t &state, const lvh::GamepadProfileSupport &support) {
+ lvh::GamepadState result;
+ const auto flags = state.buttonFlags;
+
+ result.buttons.set(lvh::GamepadButton::dpad_up, flags & DPAD_UP);
+ result.buttons.set(lvh::GamepadButton::dpad_down, flags & DPAD_DOWN);
+ result.buttons.set(lvh::GamepadButton::dpad_left, flags & DPAD_LEFT);
+ result.buttons.set(lvh::GamepadButton::dpad_right, flags & DPAD_RIGHT);
+ result.buttons.set(lvh::GamepadButton::start, flags & START);
+ result.buttons.set(lvh::GamepadButton::back, flags & BACK);
+ result.buttons.set(lvh::GamepadButton::left_stick, flags & LEFT_STICK);
+ result.buttons.set(lvh::GamepadButton::right_stick, flags & RIGHT_STICK);
+ result.buttons.set(lvh::GamepadButton::left_shoulder, flags & LEFT_BUTTON);
+ result.buttons.set(lvh::GamepadButton::right_shoulder, flags & RIGHT_BUTTON);
+ result.buttons.set(lvh::GamepadButton::guide, flags & HOME);
+ result.buttons.set(lvh::GamepadButton::a, flags & A);
+ result.buttons.set(lvh::GamepadButton::b, flags & B);
+ result.buttons.set(lvh::GamepadButton::x, flags & X);
+ result.buttons.set(lvh::GamepadButton::y, flags & Y);
+ result.buttons.set(lvh::GamepadButton::misc1, support.supports_misc1_button && (flags & MISC_BUTTON));
+ result.buttons.set(lvh::GamepadButton::touchpad, support.supports_touchpad_button && (flags & TOUCHPAD_BUTTON));
+
+ if (support.supports_touchpad_button &&
+ config::input.ds4_back_as_touchpad_click &&
+ (config::input.gamepad == "ds4"sv || config::input.gamepad == "ds5"sv) &&
+ (flags & BACK)) {
+ result.buttons.set(lvh::GamepadButton::touchpad);
+ }
+
+ result.left_stick = {.x = normalize_axis(state.lsX), .y = normalize_axis(state.lsY)};
+ result.right_stick = {.x = normalize_axis(state.rsX), .y = normalize_axis(state.rsY)};
+ result.left_trigger = normalize_trigger(state.lt);
+ result.right_trigger = normalize_trigger(state.rt);
+ return result;
+ }
+
+ std::optional mouse_button(int button) {
+ switch (button) {
+ case BUTTON_LEFT:
+ return lvh::MouseButton::left;
+ case BUTTON_MIDDLE:
+ return lvh::MouseButton::middle;
+ case BUTTON_RIGHT:
+ return lvh::MouseButton::right;
+ case BUTTON_X1:
+ return lvh::MouseButton::side;
+ case BUTTON_X2:
+ return lvh::MouseButton::extra;
+ default:
+ BOOST_LOG(warning) << "Unknown mouse button: "sv << button;
+ return std::nullopt;
+ }
+ }
+
+ lvh::GamepadBatteryState battery_state(std::uint8_t state) {
+ switch (state) {
+ case LI_BATTERY_STATE_DISCHARGING:
+ return lvh::GamepadBatteryState::discharging;
+ case LI_BATTERY_STATE_CHARGING:
+ return lvh::GamepadBatteryState::charging;
+ case LI_BATTERY_STATE_FULL:
+ return lvh::GamepadBatteryState::full;
+ case LI_BATTERY_STATE_NOT_PRESENT:
+ case LI_BATTERY_STATE_NOT_CHARGING:
+ return lvh::GamepadBatteryState::charging_error;
+ case LI_BATTERY_STATE_UNKNOWN:
+ default:
+ return lvh::GamepadBatteryState::unknown;
+ }
+ }
+
+ std::int32_t touch_orientation(std::uint16_t rotation) {
+ if (rotation == LI_ROT_UNKNOWN) {
+ return 0;
+ }
+
+ auto adjusted = static_cast(rotation);
+ if (adjusted > 90 && adjusted < 270) {
+ adjusted = 180 - adjusted;
+ }
+ if (adjusted > 90) {
+ adjusted -= 360;
+ } else if (adjusted < -90) {
+ adjusted += 360;
+ }
+
+ return adjusted;
+ }
+
+ lvh::PointerViewport pointer_viewport(const touch_port_t &touch_port) {
+ return {
+ .offset_x = touch_port.offset_x,
+ .offset_y = touch_port.offset_y,
+ .width = touch_port.width,
+ .height = touch_port.height,
+ };
+ }
+
+ lvh::KeyboardEvent keyboard_event(std::uint16_t modcode, bool release, std::uint8_t flags) {
+ lvh::KeyboardEvent event {
+ .key_code = modcode,
+ .pressed = !release,
+ };
+
+#ifdef _WIN32
+ event.uses_normalized_key_code = !(flags & SS_KBE_FLAG_NON_NORMALIZED);
+ event.prefer_native_scan_code = config::input.always_send_scancodes;
+#else
+ (void) flags;
+#endif
+ return event;
+ }
+
+ lvh::PenToolType pen_tool(std::uint8_t tool) {
+ switch (tool) {
+ case LI_TOOL_TYPE_PEN:
+ return lvh::PenToolType::pen;
+ case LI_TOOL_TYPE_ERASER:
+ return lvh::PenToolType::eraser;
+ case LI_TOOL_TYPE_UNKNOWN:
+ default:
+ return lvh::PenToolType::unchanged;
+ }
+ }
+
+ void raise_feedback(const std::shared_ptr &gamepad, const gamepad_feedback_msg_t &message) {
+ if (gamepad->feedback_queue) {
+ gamepad->feedback_queue->raise(message);
+ }
+ }
+
+ void handle_output(const std::shared_ptr &gamepad, const lvh::GamepadOutput &output) {
+ switch (output.kind) {
+ case lvh::GamepadOutputKind::rumble:
+ if (gamepad->has_last_rumble &&
+ gamepad->last_low_frequency_rumble == output.low_frequency_rumble &&
+ gamepad->last_high_frequency_rumble == output.high_frequency_rumble) {
+ return;
+ }
+ gamepad->has_last_rumble = true;
+ gamepad->last_low_frequency_rumble = output.low_frequency_rumble;
+ gamepad->last_high_frequency_rumble = output.high_frequency_rumble;
+ raise_feedback(gamepad, gamepad_feedback_msg_t::make_rumble(gamepad->client_relative_index, output.low_frequency_rumble, output.high_frequency_rumble));
+ break;
+ case lvh::GamepadOutputKind::trigger_rumble:
+ if (gamepad->has_last_trigger_rumble &&
+ gamepad->last_left_trigger_rumble == output.left_trigger_rumble &&
+ gamepad->last_right_trigger_rumble == output.right_trigger_rumble) {
+ return;
+ }
+ gamepad->has_last_trigger_rumble = true;
+ gamepad->last_left_trigger_rumble = output.left_trigger_rumble;
+ gamepad->last_right_trigger_rumble = output.right_trigger_rumble;
+ raise_feedback(gamepad, gamepad_feedback_msg_t::make_rumble_triggers(gamepad->client_relative_index, output.left_trigger_rumble, output.right_trigger_rumble));
+ break;
+ case lvh::GamepadOutputKind::rgb_led:
+ if (gamepad->has_last_rgb &&
+ gamepad->last_red == output.red &&
+ gamepad->last_green == output.green &&
+ gamepad->last_blue == output.blue) {
+ return;
+ }
+ gamepad->has_last_rgb = true;
+ gamepad->last_red = output.red;
+ gamepad->last_green = output.green;
+ gamepad->last_blue = output.blue;
+ raise_feedback(gamepad, gamepad_feedback_msg_t::make_rgb_led(gamepad->client_relative_index, output.red, output.green, output.blue));
+ break;
+ case lvh::GamepadOutputKind::adaptive_triggers:
+ raise_feedback(gamepad, gamepad_feedback_msg_t::make_adaptive_triggers(gamepad->client_relative_index, output.adaptive_trigger_flags, output.left_trigger_effect_type, output.right_trigger_effect_type, output.left_trigger_effect, output.right_trigger_effect));
+ break;
+ case lvh::GamepadOutputKind::raw_report:
+ break;
+ }
+ }
+
+ void 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():
+ runtime {create_runtime()},
+ gamepads(MAX_GAMEPADS) {
+ if (!runtime) {
+ BOOST_LOG(warning) << "Unable to create libvirtualhid runtime"sv;
+ return;
+ }
+
+ const auto &capabilities = runtime->capabilities();
+ if (capabilities.supports_keyboard) {
+ lvh::CreateKeyboardOptions options;
+ options.profile = lvh::profiles::keyboard();
+ options.stable_id = "sunshine-keyboard";
+ auto created = runtime->create_keyboard(options);
+ if (created) {
+ keyboard = std::move(created.keyboard);
+ } else {
+ log_failure("create libvirtualhid keyboard"sv, created.status);
+ }
+ }
+ if (capabilities.supports_mouse) {
+ lvh::CreateMouseOptions options;
+ options.profile = lvh::profiles::mouse();
+ options.stable_id = "sunshine-mouse";
+ auto created = runtime->create_mouse(options);
+ if (created) {
+ mouse = std::move(created.mouse);
+ } else {
+ log_failure("create libvirtualhid mouse"sv, created.status);
+ }
+ }
+ }
+
+ client_context_t::client_context_t(input_context_t &input):
+ global {&input} {
+ if (!global->runtime) {
+ return;
+ }
+
+ const auto &capabilities = global->runtime->capabilities();
+ if (capabilities.supports_touchscreen) {
+ lvh::CreateTouchscreenOptions options;
+ options.profile = lvh::profiles::touchscreen();
+ options.stable_id = "sunshine-touchscreen";
+ auto created = global->runtime->create_touchscreen(options);
+ if (created) {
+ touch = std::move(created.touchscreen);
+ } else {
+ log_failure("create libvirtualhid touchscreen"sv, created.status);
+ }
+ }
+ if (capabilities.supports_pen_tablet) {
+ lvh::CreatePenTabletOptions options;
+ options.profile = lvh::profiles::pen_tablet();
+ options.stable_id = "sunshine-pen-tablet";
+ auto created = global->runtime->create_pen_tablet(options);
+ if (created) {
+ pen = std::move(created.pen_tablet);
+ } else {
+ log_failure("create libvirtualhid pen tablet"sv, created.status);
+ }
+ }
+ }
+
+ std::unique_ptr create_runtime() {
+ lvh::RuntimeOptions options;
+ options.backend = lvh::BackendKind::platform_default;
+ return lvh::Runtime::create(options);
+ }
+
+ std::vector static_supported_gamepads() {
+ std::vector gamepads {
+ supported_gamepad_t {"auto", true, ""},
+ };
+ for (const auto &profile : gamepad_profiles) {
+ gamepads.push_back({std::string {profile.name}, false, ""});
+ }
+
+ return gamepads;
+ }
+
+ std::vector supported_gamepads(lvh::Runtime *runtime, bool fallback_vigem_available) {
+ if (!runtime) {
+ return static_supported_gamepads();
+ }
+
+ const auto libvirtualhid_available = runtime->capabilities().supports_gamepad;
+ const auto reason = libvirtualhid_available ? "" : "gamepads.virtualhid-not-available";
+ const auto auto_enabled = libvirtualhid_available || fallback_vigem_available;
+ std::vector gamepads {
+ supported_gamepad_t {"auto", auto_enabled, auto_enabled ? "" : reason},
+ };
+
+ for (const auto &profile : gamepad_profiles) {
+ const auto fallback_supported = fallback_vigem_available && (profile.name == "x360"sv || profile.name == "ds4"sv);
+ const auto enabled = libvirtualhid_available || fallback_supported;
+ gamepads.push_back({std::string {profile.name}, enabled, enabled ? "" : reason});
+ }
+
+ for (auto &[name, is_enabled, reason_disabled] : gamepads) {
+ if (!is_enabled) {
+ BOOST_LOG(warning) << "Gamepad "sv << name << " is disabled due to "sv << reason_disabled;
+ }
+ }
+
+ return gamepads;
+ }
+
+ int alloc_gamepad(input_context_t &context, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
+ if (!context.runtime || !context.runtime->capabilities().supports_gamepad) {
+ return -1;
+ }
+ if (id.globalIndex < 0 || id.globalIndex >= static_cast(context.gamepads.size())) {
+ BOOST_LOG(warning) << "Invalid libvirtualhid gamepad index: "sv << id.globalIndex;
+ return -1;
+ }
+
+ const auto &selection = profile_for_metadata(metadata);
+ auto profile = selection.profile();
+ if (config::input.gamepad != "auto"sv) {
+ BOOST_LOG(info) << "Gamepad "sv << id.globalIndex << " will be "sv << profile.name << " (manual selection)"sv;
+ } else {
+ BOOST_LOG(info) << "Gamepad "sv << id.globalIndex << " will be "sv << profile.name;
+ }
+
+ lvh::CreateGamepadOptions options;
+ options.profile = profile;
+ options.metadata = gamepad_metadata(id, metadata, profile);
+ auto created = lvh::GamepadStateAdapter::create(*context.runtime, options);
+ if (!created) {
+ log_failure("create libvirtualhid gamepad"sv, created.status);
+ return -1;
+ }
+
+ auto gamepad = std::make_shared();
+ gamepad->adapter = std::move(created.adapter);
+ gamepad->feedback_queue = std::move(feedback_queue);
+ gamepad->client_relative_index = id.clientRelativeIndex;
+ gamepad->adapter->set_output_callback([gamepad](const lvh::GamepadOutput &output) {
+ handle_output(gamepad, output);
+ });
+
+ const auto &support = gamepad->adapter->support();
+ warn_unsupported_client_features(id.globalIndex, metadata, support);
+ warn_missing_client_features(id.globalIndex, metadata, support);
+ if (support.supports_motion) {
+ raise_feedback(gamepad, gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100));
+ raise_feedback(gamepad, gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100));
+ }
+
+ context.gamepads[id.globalIndex] = std::move(gamepad);
+ return 0;
+ }
+
+ bool has_gamepad(const input_context_t &context, int nr) {
+ return nr >= 0 && nr < context.gamepads.size() && context.gamepads[nr] && context.gamepads[nr]->adapter;
+ }
+
+ void free_gamepad(input_context_t &context, int nr) {
+ if (has_gamepad(context, nr)) {
+ context.gamepads[nr].reset();
+ }
+ }
+
+ void gamepad_update(input_context_t &context, int nr, const gamepad_state_t &state) {
+ if (!has_gamepad(context, nr)) {
+ return;
+ }
+
+ auto &gamepad = context.gamepads[nr];
+ log_failure("submit libvirtualhid gamepad state"sv, gamepad->adapter->set_state(make_gamepad_state(state, gamepad->adapter->support())));
+ }
+
+ void gamepad_touch(input_context_t &context, const gamepad_touch_t &touch) {
+ if (!has_gamepad(context, touch.id.globalIndex)) {
+ return;
+ }
+
+ auto &gamepad = context.gamepads[touch.id.globalIndex];
+ if (!gamepad->adapter->support().supports_touchpad) {
+ return;
+ }
+
+ if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {
+ for (std::size_t index = 0; index < gamepad->touch_ids.size(); ++index) {
+ if (gamepad->touch_ids[index]) {
+ log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(index));
+ gamepad->touch_ids[index].reset();
+ }
+ }
+ return;
+ }
+
+ auto slot = std::ranges::find(gamepad->touch_ids, touch.pointerId);
+ if (touch.eventType == LI_TOUCH_EVENT_DOWN && slot == gamepad->touch_ids.end()) {
+ slot = std::ranges::find_if(gamepad->touch_ids, [](const auto &id) {
+ return !id.has_value();
+ });
+ if (slot == gamepad->touch_ids.end()) {
+ BOOST_LOG(warning) << "No free libvirtualhid gamepad touch slots"sv;
+ return;
+ }
+ *slot = touch.pointerId;
+ }
+
+ if (slot == gamepad->touch_ids.end()) {
+ return;
+ }
+
+ const auto index = static_cast(std::distance(gamepad->touch_ids.begin(), slot));
+ if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) {
+ log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(index));
+ slot->reset();
+ return;
+ }
+ if (touch.eventType != LI_TOUCH_EVENT_DOWN && touch.eventType != LI_TOUCH_EVENT_MOVE) {
+ return;
+ }
+
+ lvh::GamepadTouchContact contact;
+ contact.id = static_cast(index);
+ contact.active = touch.pressure > 0.5F;
+ contact.x = std::clamp(touch.x, 0.0F, 1.0F);
+ contact.y = std::clamp(touch.y, 0.0F, 1.0F);
+ log_failure("submit libvirtualhid gamepad touch"sv, gamepad->adapter->set_touchpad_contact(index, contact));
+ }
+
+ void gamepad_motion(input_context_t &context, const gamepad_motion_t &motion) {
+ if (!has_gamepad(context, motion.id.globalIndex)) {
+ return;
+ }
+
+ auto &gamepad = context.gamepads[motion.id.globalIndex];
+ switch (motion.motionType) {
+ case LI_MOTION_TYPE_ACCEL:
+ log_failure("submit libvirtualhid gamepad acceleration"sv, gamepad->adapter->set_acceleration(lvh::Vector3 {motion.x, motion.y, motion.z}));
+ break;
+ case LI_MOTION_TYPE_GYRO:
+ log_failure("submit libvirtualhid gamepad gyroscope"sv, gamepad->adapter->set_gyroscope(lvh::Vector3 {motion.x, motion.y, motion.z}));
+ break;
+ default:
+ break;
+ }
+ }
+
+ void gamepad_battery(input_context_t &context, const gamepad_battery_t &battery) {
+ if (!has_gamepad(context, battery.id.globalIndex)) {
+ return;
+ }
+
+ auto &gamepad = context.gamepads[battery.id.globalIndex];
+ if (battery.state == LI_BATTERY_STATE_UNKNOWN || battery.state == LI_BATTERY_STATE_NOT_PRESENT) {
+ log_failure("clear libvirtualhid gamepad battery"sv, gamepad->adapter->clear_battery());
+ return;
+ }
+
+ lvh::GamepadBattery value;
+ value.state = battery_state(battery.state);
+ value.percentage = battery.percentage == LI_BATTERY_PERCENTAGE_UNKNOWN ? 100 : std::min(battery.percentage, 100);
+ log_failure("submit libvirtualhid gamepad battery"sv, gamepad->adapter->set_battery(value));
+ }
+
+ void move_mouse(input_context_t &context, int delta_x, int delta_y) {
+ if (context.mouse) {
+ log_failure("submit libvirtualhid mouse movement"sv, context.mouse->move_relative(delta_x, delta_y));
+ }
+ }
+
+ void abs_mouse(input_context_t &context, const touch_port_t &touch_port, float x, float y) {
+ if (context.mouse) {
+ log_failure(
+ "submit libvirtualhid absolute mouse movement"sv,
+ context.mouse->move_absolute(
+ static_cast(std::lround(x)),
+ static_cast(std::lround(y)),
+ touch_port.width,
+ touch_port.height
+ )
+ );
+ }
+ }
+
+ void button_mouse(input_context_t &context, int button, bool release) {
+ if (context.mouse) {
+ const auto converted = mouse_button(button);
+ if (!converted) {
+ return;
+ }
+
+ log_failure("submit libvirtualhid mouse button"sv, context.mouse->button(*converted, !release));
+ }
+ }
+
+ void scroll(input_context_t &context, int high_res_distance) {
+ if (context.mouse) {
+ log_failure("submit libvirtualhid vertical scroll"sv, context.mouse->vertical_scroll(high_res_distance));
+ }
+ }
+
+ void hscroll(input_context_t &context, int high_res_distance) {
+ if (context.mouse) {
+ log_failure("submit libvirtualhid horizontal scroll"sv, context.mouse->horizontal_scroll(high_res_distance));
+ }
+ }
+
+ void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, 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 std::array button_states {
+ std::pair {lvh::PenButton::primary, (pen.penButtons & LI_PEN_BUTTON_PRIMARY) != 0},
+ std::pair {lvh::PenButton::secondary, (pen.penButtons & LI_PEN_BUTTON_SECONDARY) != 0},
+ std::pair {lvh::PenButton::tertiary, (pen.penButtons & LI_PEN_BUTTON_TERTIARY) != 0},
+ };
+ for (const auto &[button, pressed] : button_states) {
+ const auto was_pressed = context.pressed_pen_buttons.contains(button);
+ if (pressed == was_pressed) {
+ continue;
+ }
+
+ log_failure("submit libvirtualhid pen button"sv, context.pen->button(button, pressed));
+ if (pressed) {
+ context.pressed_pen_buttons.insert(button);
+ } else {
+ context.pressed_pen_buttons.erase(button);
+ }
+ }
+
+ if (pen.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {
+ for (const auto button : context.pressed_pen_buttons) {
+ log_failure("release libvirtualhid pen button"sv, context.pen->button(button, false));
+ }
+ context.pressed_pen_buttons.clear();
+ }
+
+ auto transition = lvh::PointerTransition::update;
+ switch (pen.eventType) {
+ case LI_TOUCH_EVENT_CANCEL:
+ case LI_TOUCH_EVENT_CANCEL_ALL:
+ transition = lvh::PointerTransition::cancel;
+ break;
+ case LI_TOUCH_EVENT_UP:
+ transition = lvh::PointerTransition::release;
+ break;
+ case LI_TOUCH_EVENT_HOVER_LEAVE:
+ transition = lvh::PointerTransition::leave;
+ break;
+ default:
+ break;
+ }
+
+ auto rotation = pen.rotation;
+ 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 == lvh::PointerTransition::update &&
+ (pen.eventType == LI_TOUCH_EVENT_DOWN || pen.eventType == LI_TOUCH_EVENT_MOVE);
+ lvh::PenToolState state;
+ state.tool = pen_tool(pen.toolType);
+ state.x = std::clamp(pen.x, 0.0F, 1.0F);
+ 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
diff --git a/src/platform/virtualhid_input.h b/src/platform/virtualhid_input.h
new file mode 100644
index 00000000000..a94543d7a4a
--- /dev/null
+++ b/src/platform/virtualhid_input.h
@@ -0,0 +1,227 @@
+/**
+ * @file src/platform/virtualhid_input.h
+ * @brief Declarations for libvirtualhid-backed input helpers.
+ */
+#pragma once
+
+// standard includes
+#include
+#include
+#include
+#include
+#include
+
+// lib includes
+#include
+
+// local includes
+#include "src/platform/common.h"
+
+namespace platf::virtualhid {
+
+ /**
+ * @brief Runtime and virtual devices owned by one platform input context.
+ */
+ struct input_context_t {
+ /**
+ * @brief Construct the libvirtualhid input context.
+ */
+ input_context_t();
+
+ std::unique_ptr runtime; ///< libvirtualhid runtime.
+ std::unique_ptr keyboard; ///< Shared virtual keyboard.
+ std::unique_ptr mouse; ///< Shared virtual mouse.
+ std::vector> gamepads; ///< Virtual gamepad slots.
+ };
+
+ /**
+ * @brief Per-client virtual touch and pen state.
+ */
+ struct client_context_t {
+ /**
+ * @brief Create per-client libvirtualhid devices.
+ *
+ * @param input Global input context.
+ */
+ explicit client_context_t(input_context_t &input);
+
+ input_context_t *global = nullptr; ///< Shared global input context.
+ std::unique_ptr touch; ///< Per-client touchscreen.
+ std::unique_ptr pen; ///< Per-client pen tablet.
+ std::set active_touches; ///< Active touchscreen contacts.
+ std::set pressed_pen_buttons; ///< Active pen tablet buttons.
+ };
+
+ /**
+ * @brief Create a platform-default libvirtualhid runtime.
+ *
+ * @return Runtime instance.
+ */
+ std::unique_ptr create_runtime();
+
+ /**
+ * @brief Return static gamepad choices for config validation.
+ *
+ * @return Supported gamepad choices.
+ */
+ std::vector static_supported_gamepads();
+
+ /**
+ * @brief Return gamepad choices and runtime availability.
+ *
+ * @param runtime Runtime to probe.
+ * @param fallback_vigem_available Whether Windows ViGEm fallback can create gamepads.
+ * @return Supported gamepad choices.
+ */
+ std::vector supported_gamepads(lvh::Runtime *runtime, bool fallback_vigem_available = false);
+
+ /**
+ * @brief Allocate a libvirtualhid gamepad.
+ *
+ * @param context Input context.
+ * @param id Sunshine gamepad identifiers.
+ * @param metadata Client-reported controller metadata.
+ * @param feedback_queue Queue used to return gamepad feedback to the client.
+ * @return 0 on success, otherwise -1.
+ */
+ int alloc_gamepad(input_context_t &context, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue);
+
+ /**
+ * @brief Check whether a libvirtualhid gamepad exists in a slot.
+ *
+ * @param context Input context.
+ * @param nr Gamepad slot index.
+ * @return True when a virtual gamepad is active.
+ */
+ bool has_gamepad(const input_context_t &context, int nr);
+
+ /**
+ * @brief Release a libvirtualhid gamepad slot.
+ *
+ * @param context Input context.
+ * @param nr Gamepad slot index.
+ */
+ void free_gamepad(input_context_t &context, int nr);
+
+ /**
+ * @brief Submit a full gamepad state.
+ *
+ * @param context Input context.
+ * @param nr Gamepad slot index.
+ * @param state Sunshine gamepad state.
+ */
+ void gamepad_update(input_context_t &context, int nr, const gamepad_state_t &state);
+
+ /**
+ * @brief Submit a gamepad touch event.
+ *
+ * @param context Input context.
+ * @param touch Sunshine touch event.
+ */
+ void gamepad_touch(input_context_t &context, const gamepad_touch_t &touch);
+
+ /**
+ * @brief Submit a gamepad motion event.
+ *
+ * @param context Input context.
+ * @param motion Sunshine motion event.
+ */
+ void gamepad_motion(input_context_t &context, const gamepad_motion_t &motion);
+
+ /**
+ * @brief Submit gamepad battery metadata.
+ *
+ * @param context Input context.
+ * @param battery Sunshine battery event.
+ */
+ void gamepad_battery(input_context_t &context, const gamepad_battery_t &battery);
+
+ /**
+ * @brief Move the virtual mouse relatively.
+ *
+ * @param context Input context.
+ * @param delta_x Horizontal delta.
+ * @param delta_y Vertical delta.
+ */
+ void move_mouse(input_context_t &context, int delta_x, int delta_y);
+
+ /**
+ * @brief Move the virtual mouse absolutely inside a target touch port.
+ *
+ * @param context Input context.
+ * @param touch_port Target coordinate space.
+ * @param x Absolute X coordinate.
+ * @param y Absolute Y coordinate.
+ */
+ void abs_mouse(input_context_t &context, const touch_port_t &touch_port, float x, float y);
+
+ /**
+ * @brief Submit a mouse button event.
+ *
+ * @param context Input context.
+ * @param button Moonlight mouse button.
+ * @param release Whether the button was released.
+ */
+ void button_mouse(input_context_t &context, int button, bool release);
+
+ /**
+ * @brief Submit vertical scroll input.
+ *
+ * @param context Input context.
+ * @param high_res_distance High-resolution scroll distance.
+ */
+ void scroll(input_context_t &context, int high_res_distance);
+
+ /**
+ * @brief Submit horizontal scroll input.
+ *
+ * @param context Input context.
+ * @param high_res_distance High-resolution scroll distance.
+ */
+ void hscroll(input_context_t &context, int high_res_distance);
+
+ /**
+ * @brief Submit a keyboard key transition.
+ *
+ * @param context Input context.
+ * @param modcode Portable key code.
+ * @param release Whether the key was released.
+ * @param flags Bit flags that modify the requested operation.
+ */
+ void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, std::uint8_t flags);
+
+ /**
+ * @brief Submit UTF-8 text input.
+ *
+ * @param context Input context.
+ * @param utf8 UTF-8 text buffer.
+ * @param size Text buffer size.
+ */
+ void unicode(input_context_t &context, const char *utf8, int size);
+
+ /**
+ * @brief Submit a touchscreen event.
+ *
+ * @param context Client context.
+ * @param touch_port Touch coordinate bounds used for scaling.
+ * @param touch Touch event.
+ */
+ void touch_update(client_context_t &context, const touch_port_t &touch_port, const touch_input_t &touch);
+
+ /**
+ * @brief Submit a pen event.
+ *
+ * @param context Client context.
+ * @param touch_port Touch coordinate bounds used for scaling.
+ * @param pen Pen event.
+ */
+ void pen_update(client_context_t &context, const touch_port_t &touch_port, const pen_input_t &pen);
+
+ /**
+ * @brief Return whether the configured gamepad profile can expose touchpad input.
+ *
+ * @return True when controller touchpad input should be advertised.
+ */
+ bool configured_gamepad_supports_touchpad();
+
+} // namespace platf::virtualhid
diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp
index 7fcbc3a2ef1..77afd5d29f4 100644
--- a/src/platform/windows/input.cpp
+++ b/src/platform/windows/input.cpp
@@ -17,7 +17,6 @@
#include
// standard includes
-#include
#include
#include
@@ -25,28 +24,16 @@
#include
// local includes
-#include "keylayout.h"
#include "misc.h"
#include "src/config.h"
#include "src/globals.h"
#include "src/logging.h"
#include "src/platform/common.h"
+#include "src/platform/virtualhid_input.h"
namespace platf {
using namespace std::literals;
- thread_local HDESK _lastKnownInputDesktop = nullptr; ///< Last known input desktop.
-
- /**
- * @brief Target touch port.
- */
- constexpr touch_port_t target_touch_port {
- 0,
- 0,
- 65535,
- 65535
- };
-
/**
* @brief ViGEm client pointer released with `vigem_free`.
*/
@@ -269,7 +256,8 @@ namespace platf {
VIGEM_ERROR status = vigem_connect(client.get());
if (!VIGEM_SUCCESS(status)) {
// Log a special fatal message for this case to show the error in the web UI
- BOOST_LOG(fatal) << "ViGEmBus is not installed or running. You must install ViGEmBus for gamepad support!"sv;
+ BOOST_LOG(fatal) << "libvirtualhid gamepad support is unavailable and ViGEmBus fallback is not installed or running"sv;
+ return -1;
} else {
vigem_disconnect(client.get());
}
@@ -501,120 +489,77 @@ namespace platf {
}
/**
- * @brief Global inputtino device handles shared by clients.
+ * @brief Global virtual input device handles shared by clients.
*/
struct input_raw_t {
~input_raw_t() {
delete vigem;
}
+ virtualhid::input_context_t virtualhid; ///< libvirtualhid input context.
vigem_t *vigem; ///< Vigem.
-
- decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; ///< Fn create synthetic pointer device.
- decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; ///< Fn inject synthetic pointer input.
- decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice; ///< Fn destroy synthetic pointer device.
};
input_t input() {
input_t result {new input_raw_t {}};
auto &raw = *(input_raw_t *) result.get();
- raw.vigem = new vigem_t {};
- if (raw.vigem->init()) {
- delete raw.vigem;
- raw.vigem = nullptr;
+ raw.vigem = nullptr;
+ if (!raw.virtualhid.runtime || !raw.virtualhid.runtime->capabilities().supports_gamepad) {
+ raw.vigem = new vigem_t {};
+ if (raw.vigem->init()) {
+ delete raw.vigem;
+ raw.vigem = nullptr;
+ }
}
- // Get pointers to virtual touch/pen input functions (Win10 1809+)
- raw.fnCreateSyntheticPointerDevice = (decltype(CreateSyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice");
- raw.fnInjectSyntheticPointerInput = (decltype(InjectSyntheticPointerInput) *) GetProcAddress(GetModuleHandleA("user32.dll"), "InjectSyntheticPointerInput");
- raw.fnDestroySyntheticPointerDevice = (decltype(DestroySyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "DestroySyntheticPointerDevice");
-
return result;
}
/**
- * @brief Calls SendInput() and switches input desktops if required.
- * @param i The `INPUT` struct to send.
+ * @brief Check whether the configured virtual gamepad can fall back to ViGEm.
+ *
+ * @return True when the ViGEm fallback can satisfy the configured profile.
*/
- void send_input(INPUT &i) {
- retry:
- auto send = SendInput(1, &i, sizeof(INPUT));
- if (send != 1) {
- auto hDesk = syncThreadDesktop();
- if (_lastKnownInputDesktop != hDesk) {
- _lastKnownInputDesktop = hDesk;
- goto retry;
- }
- BOOST_LOG(error) << "Couldn't send input"sv;
- }
+ bool vigem_fallback_allowed() {
+ return config::input.gamepad == "auto"sv ||
+ config::input.gamepad == "x360"sv ||
+ config::input.gamepad == "ds4"sv;
}
/**
- * @brief Calls InjectSyntheticPointerInput() and switches input desktops if required.
- * @details Must only be called if InjectSyntheticPointerInput() is available.
- * @param input The global input context.
- * @param device The synthetic pointer device handle.
- * @param pointerInfo An array of `POINTER_TYPE_INFO` structs.
- * @param count The number of elements in `pointerInfo`.
- * @return true if input was successfully injected.
+ * @brief Create the ViGEm fallback context if it is not already available.
+ *
+ * @param raw Platform input context.
+ * @return True when ViGEm fallback is available.
*/
- bool inject_synthetic_pointer_input(input_raw_t *input, HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointerInfo, UINT32 count) {
- retry:
- if (!input->fnInjectSyntheticPointerInput(device, pointerInfo, count)) {
- auto hDesk = syncThreadDesktop();
- if (_lastKnownInputDesktop != hDesk) {
- _lastKnownInputDesktop = hDesk;
- goto retry;
- }
+ bool ensure_vigem(input_raw_t *raw) {
+ if (raw->vigem) {
+ return true;
+ }
+
+ raw->vigem = new vigem_t {};
+ if (raw->vigem->init()) {
+ delete raw->vigem;
+ raw->vigem = nullptr;
return false;
}
+
return true;
}
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags =
- MOUSEEVENTF_MOVE |
- MOUSEEVENTF_ABSOLUTE |
-
- // MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop
- MOUSEEVENTF_VIRTUALDESK;
-
- // Note: x and y already include the display offset (offset_x/offset_y) from client_to_touchport(),
- // so we must not add offset_x/offset_y again here to avoid double-offsetting on multi-monitor setups.
- auto scaled_x = std::lround(x * ((float) target_touch_port.width / (float) touch_port.width));
- auto scaled_y = std::lround(y * ((float) target_touch_port.height / (float) touch_port.height));
-
- mi.dx = scaled_x;
- mi.dy = scaled_y;
-
- send_input(i);
+ virtualhid::abs_mouse(((input_raw_t *) input.get())->virtualhid, touch_port, x, y);
}
void move_mouse(input_t &input, int deltaX, int deltaY) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags = MOUSEEVENTF_MOVE;
- mi.dx = deltaX;
- mi.dy = deltaY;
-
- send_input(i);
+ virtualhid::move_mouse(((input_raw_t *) input.get())->virtualhid, deltaX, deltaY);
}
- util::point_t get_mouse_loc(input_t &input) {
- throw std::runtime_error("not implemented yet, has to pass tests");
- // TODO: Tests are failing, something wrong here?
+ std::optional get_mouse_loc(input_t & /*input*/) {
POINT p;
if (!GetCursorPos(&p)) {
- return util::point_t {0.0, 0.0};
+ return std::nullopt;
}
return util::point_t {
@@ -624,109 +569,23 @@ namespace platf {
}
void button_mouse(input_t &input, int button, bool release) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- if (button == 1) {
- mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN;
- } else if (button == 2) {
- mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN;
- } else if (button == 3) {
- mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN;
- } else if (button == 4) {
- mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
- mi.mouseData = XBUTTON1;
- } else {
- mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
- mi.mouseData = XBUTTON2;
- }
-
- send_input(i);
+ virtualhid::button_mouse(((input_raw_t *) input.get())->virtualhid, button, release);
}
void scroll(input_t &input, int distance) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags = MOUSEEVENTF_WHEEL;
- mi.mouseData = distance;
-
- send_input(i);
+ virtualhid::scroll(((input_raw_t *) input.get())->virtualhid, distance);
}
void hscroll(input_t &input, int distance) {
- INPUT i {};
-
- i.type = INPUT_MOUSE;
- auto &mi = i.mi;
-
- mi.dwFlags = MOUSEEVENTF_HWHEEL;
- mi.mouseData = distance;
-
- send_input(i);
+ virtualhid::hscroll(((input_raw_t *) input.get())->virtualhid, distance);
}
void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
- INPUT i {};
- i.type = INPUT_KEYBOARD;
- auto &ki = i.ki;
-
- // If the client did not normalize this VK code to a US English layout, we can't accurately convert it to a scancode.
- // If we're set to always send scancodes, we will use the current keyboard layout to convert to a scancode. This will
- // assume the client and host have the same keyboard layout, but it's probably better than always using US English.
- if (!(flags & SS_KBE_FLAG_NON_NORMALIZED)) {
- // Mask off the extended key byte
- ki.wScan = VK_TO_SCANCODE_MAP[modcode & 0xFF];
- } else if (config::input.always_send_scancodes && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {
- // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/
- ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
- }
-
- // If we can map this to a scancode, send it as a scancode for maximum game compatibility.
- if (ki.wScan) {
- ki.dwFlags = KEYEVENTF_SCANCODE;
- } else {
- // If there is no scancode mapping or it's non-normalized, send it as a regular VK event.
- ki.wVk = modcode;
- }
-
- // https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags
- switch (modcode) {
- case VK_LWIN:
- case VK_RWIN:
- case VK_RMENU:
- case VK_RCONTROL:
- case VK_INSERT:
- case VK_DELETE:
- case VK_HOME:
- case VK_END:
- case VK_PRIOR:
- case VK_NEXT:
- case VK_UP:
- case VK_DOWN:
- case VK_LEFT:
- case VK_RIGHT:
- case VK_DIVIDE:
- case VK_APPS:
- ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
- break;
- default:
- break;
- }
-
- if (release) {
- ki.dwFlags |= KEYEVENTF_KEYUP;
- }
-
- send_input(i);
+ virtualhid::keyboard_update(((input_raw_t *) input.get())->virtualhid, modcode, release, flags);
}
/**
- * @brief Per-client inputtino devices for touch and pen input.
+ * @brief Per-client virtual devices for touch and pen input.
*/
struct client_input_raw_t: public client_input_t {
/**
@@ -734,41 +593,10 @@ namespace platf {
*
* @param input Platform input backend that receives the event.
*/
- client_input_raw_t(input_t &input) {
- global = (input_raw_t *) input.get();
- }
-
- ~client_input_raw_t() override {
- if (penRepeatTask) {
- task_pool.cancel(penRepeatTask);
- }
- if (touchRepeatTask) {
- task_pool.cancel(touchRepeatTask);
- }
+ explicit client_input_raw_t(input_t &input):
+ virtualhid {((input_raw_t *) input.get())->virtualhid} {}
- if (pen) {
- global->fnDestroySyntheticPointerDevice(pen);
- }
- if (touch) {
- global->fnDestroySyntheticPointerDevice(touch);
- }
- }
-
- input_raw_t *global;
-
- // Device state and handles for pen and touch input must be stored in the per-client
- // input context, because each connected client may be sending their own independent
- // pen/touch events. To maintain separation, we expose separate pen and touch devices
- // for each client.
-
- HSYNTHETICPOINTERDEVICE pen {}; ///< Windows synthetic pointer device used for pen events.
- POINTER_TYPE_INFO penInfo {}; ///< Pen info.
- thread_pool_util::ThreadPool::task_id_t penRepeatTask {}; ///< Pen repeat task.
-
- HSYNTHETICPOINTERDEVICE touch {}; ///< Windows synthetic pointer device used for touch events.
- POINTER_TYPE_INFO touchInfo[10] {}; ///< Touch info.
- UINT32 activeTouchSlots {}; ///< Active touch slots.
- thread_pool_util::ThreadPool::task_id_t touchRepeatTask {}; ///< Touch repeat task.
+ virtualhid::client_context_t virtualhid; ///< libvirtualhid client context.
};
/**
@@ -780,478 +608,34 @@ namespace platf {
return std::make_unique(input);
}
- /**
- * @brief Compacts the touch slots into a contiguous block and updates the active count.
- * @details Since this swaps entries around, all slot pointers/references are invalid after compaction.
- * @param raw The client-specific input context.
- */
- void perform_touch_compaction(client_input_raw_t *raw) {
- // Windows requires all active touches be contiguous when fed into InjectSyntheticPointerInput().
- UINT32 i;
- for (i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {
- // This is an empty slot. Look for a later entry to move into this slot.
- for (UINT32 j = i + 1; j < ARRAYSIZE(raw->touchInfo); j++) {
- if (raw->touchInfo[j].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- std::swap(raw->touchInfo[i], raw->touchInfo[j]);
- break;
- }
- }
-
- // If we didn't find anything, we've reached the end of active slots.
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {
- break;
- }
- }
- }
-
- // Update the number of active touch slots
- raw->activeTouchSlots = i;
- }
-
- /**
- * @brief Gets a pointer slot by client-relative pointer ID, claiming a new one if necessary.
- * @param raw The raw client-specific input context.
- * @param pointerId The client's pointer ID.
- * @param eventType The LI_TOUCH_EVENT value from the client.
- * @return A pointer to the slot entry.
- */
- POINTER_TYPE_INFO *pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) {
- // Compact active touches into a single contiguous block
- perform_touch_compaction(raw);
-
- // Try to find a matching pointer ID
- for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerId == pointerId &&
- raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- if (eventType == LI_TOUCH_EVENT_DOWN && (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT)) {
- BOOST_LOG(warning) << "Pointer "sv << pointerId << " already down. Did the client drop an up/cancel event?"sv;
- }
-
- return &raw->touchInfo[i];
- }
- }
-
- if (eventType != LI_TOUCH_EVENT_HOVER && eventType != LI_TOUCH_EVENT_DOWN) {
- BOOST_LOG(warning) << "Unexpected new pointer "sv << pointerId << " for event "sv << (uint32_t) eventType << ". Did the client drop a down/hover event?"sv;
- }
-
- // If there was none, grab an unused entry and increment the active slot count
- for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {
- if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {
- raw->touchInfo[i].touchInfo.pointerInfo.pointerId = pointerId;
- raw->activeTouchSlots = i + 1;
- return &raw->touchInfo[i];
- }
- }
-
- return nullptr;
- }
-
- /**
- * @brief Populate common `POINTER_INFO` members shared between pen and touch events.
- * @param pointerInfo The pointer info to populate.
- * @param touchPort The current viewport for translating to screen coordinates.
- * @param eventType The type of touch/pen event.
- * @param x The normalized 0.0-1.0 X coordinate.
- * @param y The normalized 0.0-1.0 Y coordinate.
- */
- void populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) {
- switch (eventType) {
- case LI_TOUCH_EVENT_HOVER:
- pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT;
- pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
- case LI_TOUCH_EVENT_DOWN:
- pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
- case LI_TOUCH_EVENT_UP:
- // We expect to get another LI_TOUCH_EVENT_HOVER if the pointer remains in range
- pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
- pointerInfo.pointerFlags |= POINTER_FLAG_UP;
- break;
- case LI_TOUCH_EVENT_MOVE:
- pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
- case LI_TOUCH_EVENT_CANCEL:
- case LI_TOUCH_EVENT_CANCEL_ALL:
- // If we were in contact with the touch surface at the time of the cancellation,
- // we'll set POINTER_FLAG_UP, otherwise set POINTER_FLAG_UPDATE.
- if (pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) {
- pointerInfo.pointerFlags |= POINTER_FLAG_UP;
- } else {
- pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
- }
- pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
- pointerInfo.pointerFlags |= POINTER_FLAG_CANCELED;
- break;
- case LI_TOUCH_EVENT_HOVER_LEAVE:
- pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
- pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
- break;
- case LI_TOUCH_EVENT_BUTTON_ONLY:
- // On Windows, we can only pass buttons if we have an active pointer
- if (pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
- }
- break;
- default:
- BOOST_LOG(warning) << "Unknown touch event: "sv << (uint32_t) eventType;
- break;
- }
- }
-
- // Active pointer interactions sent via InjectSyntheticPointerInput() seem to be automatically
- // cancelled by Windows if not repeated/updated within about a second. To avoid this, refresh
- // the injected input periodically.
- constexpr auto ISPI_REPEAT_INTERVAL = 50ms; ///< Protocol or platform constant for ispi repeat interval.
-
- /**
- * @brief Repeats the current touch state to avoid the interactions timing out.
- * @param raw The raw client-specific input context.
- */
- void repeat_touch(client_input_raw_t *raw) {
- if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to refresh virtual touch input: "sv << err;
- }
-
- raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
-
- /**
- * @brief Repeats the current pen state to avoid the interactions timing out.
- * @param raw The raw client-specific input context.
- */
- void repeat_pen(client_input_raw_t *raw) {
- if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to refresh virtual pen input: "sv << err;
- }
-
- raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
-
- /**
- * @brief Cancels all active touches.
- * @param raw The raw client-specific input context.
- */
- void cancel_all_active_touches(client_input_raw_t *raw) {
- // Cancel touch repeat callbacks
- if (raw->touchRepeatTask) {
- task_pool.cancel(raw->touchRepeatTask);
- raw->touchRepeatTask = nullptr;
- }
-
- // Compact touches to update activeTouchSlots
- perform_touch_compaction(raw);
-
- // If we have active slots, cancel them all
- if (raw->activeTouchSlots > 0) {
- for (UINT32 i = 0; i < raw->activeTouchSlots; i++) {
- populate_common_pointer_info(raw->touchInfo[i].touchInfo.pointerInfo, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f);
- raw->touchInfo[i].touchInfo.touchMask = TOUCH_MASK_NONE;
- }
- if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to cancel all virtual touch input: "sv << err;
- }
- }
-
- // Zero all touch state
- std::memset(raw->touchInfo, 0, sizeof(raw->touchInfo));
- raw->activeTouchSlots = 0;
- }
-
- // These are edge-triggered pointer state flags that should always be cleared next frame
- constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; ///< Protocol or platform constant for edge triggered pointer flags.
-
- /**
- * @brief Sends a touch event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param touch The touch event.
- */
void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {
- auto raw = (client_input_raw_t *) input;
-
- // Bail if we're not running on an OS that supports virtual touch input
- if (!raw->global->fnCreateSyntheticPointerDevice ||
- !raw->global->fnInjectSyntheticPointerInput ||
- !raw->global->fnDestroySyntheticPointerDevice) {
- BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv;
- return;
- }
-
- // If there's not already a virtual touch device, create one now
- if (!raw->touch) {
- if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) {
- BOOST_LOG(info) << "Creating virtual touch input device"sv;
- raw->touch = raw->global->fnCreateSyntheticPointerDevice(PT_TOUCH, ARRAYSIZE(raw->touchInfo), POINTER_FEEDBACK_DEFAULT);
- if (!raw->touch) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to create virtual touch device: "sv << err;
- return;
- }
- } else {
- // No need to cancel anything if we had no touch input device
- return;
- }
- }
-
- // Cancel touch repeat callbacks
- if (raw->touchRepeatTask) {
- task_pool.cancel(raw->touchRepeatTask);
- raw->touchRepeatTask = nullptr;
- }
-
- // If this is a special request to cancel all touches, do that and return
- if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {
- cancel_all_active_touches(raw);
- return;
- }
-
- // Find or allocate an entry for this touch pointer ID
- auto pointer = pointer_by_id(raw, touch.pointerId, touch.eventType);
- if (!pointer) {
- BOOST_LOG(error) << "No unused pointer entries! Cancelling all active touches!"sv;
- cancel_all_active_touches(raw);
- pointer = pointer_by_id(raw, touch.pointerId, touch.eventType);
- }
-
- pointer->type = PT_TOUCH;
-
- auto &touchInfo = pointer->touchInfo;
- touchInfo.pointerInfo.pointerType = PT_TOUCH;
-
- // Populate shared pointer info fields
- populate_common_pointer_info(touchInfo.pointerInfo, touch_port, touch.eventType, touch.x, touch.y);
-
- touchInfo.touchMask = TOUCH_MASK_NONE;
-
- // Pressure and contact area only apply to in-contact pointers.
- //
- // The clients also pass distance and tool size for hovers, but Windows doesn't
- // provide APIs to receive that data.
- if (touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) {
- if (touch.pressureOrDistance != 0.0f) {
- touchInfo.touchMask |= TOUCH_MASK_PRESSURE;
-
- // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses
- touchInfo.pressure = (UINT32) (touch.pressureOrDistance * 1024);
- } else {
- // The default touch pressure is 512
- touchInfo.pressure = 512;
- }
-
- if (touch.contactAreaMajor != 0.0f && touch.contactAreaMinor != 0.0f) {
- // For the purposes of contact area calculation, we will assume the touches
- // are at a 45 degree angle if rotation is unknown. This will scale the major
- // axis value by width and height equally.
- float rotationAngleDegs = touch.rotation == LI_ROT_UNKNOWN ? 45 : touch.rotation;
-
- float majorAxisAngle = rotationAngleDegs * (M_PI / 180);
- float minorAxisAngle = majorAxisAngle + (M_PI / 2);
-
- // Estimate the contact rectangle
- float contactWidth = (std::cos(majorAxisAngle) * touch.contactAreaMajor) + (std::cos(minorAxisAngle) * touch.contactAreaMinor);
- float contactHeight = (std::sin(majorAxisAngle) * touch.contactAreaMajor) + (std::sin(minorAxisAngle) * touch.contactAreaMinor);
-
- // Convert into screen coordinates centered at the touch location and constrained by screen dimensions
- touchInfo.rcContact.left = std::max(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2));
- touchInfo.rcContact.right = std::min(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2));
- touchInfo.rcContact.top = std::max(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2));
- touchInfo.rcContact.bottom = std::min(touch_port.offset_y + touch_port.height, touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2));
-
- touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA;
- }
- } else {
- touchInfo.pressure = 0;
- touchInfo.rcContact = {};
- }
-
- if (touch.rotation != LI_ROT_UNKNOWN) {
- touchInfo.touchMask |= TOUCH_MASK_ORIENTATION;
- touchInfo.orientation = touch.rotation;
- } else {
- touchInfo.orientation = 0;
- }
-
- if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to inject virtual touch input: "sv << err;
- return;
- }
-
- // Clear pointer flags that should only remain set for one frame
- touchInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS;
-
- // If we still have an active touch, refresh the touch state periodically
- if (raw->activeTouchSlots > 1 || touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
+ virtualhid::touch_update(((client_input_raw_t *) input)->virtualhid, touch_port, touch);
}
- /**
- * @brief Sends a pen event to the OS.
- * @param input The client-specific input context.
- * @param touch_port The current viewport for translating to screen coordinates.
- * @param pen The pen event.
- */
void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {
- auto raw = (client_input_raw_t *) input;
-
- // Bail if we're not running on an OS that supports virtual pen input
- if (!raw->global->fnCreateSyntheticPointerDevice ||
- !raw->global->fnInjectSyntheticPointerInput ||
- !raw->global->fnDestroySyntheticPointerDevice) {
- BOOST_LOG(warning) << "Pen input requires Windows 10 1809 or later"sv;
- return;
- }
-
- // If there's not already a virtual pen device, create one now
- if (!raw->pen) {
- if (pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL) {
- BOOST_LOG(info) << "Creating virtual pen input device"sv;
- raw->pen = raw->global->fnCreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT);
- if (!raw->pen) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to create virtual pen device: "sv << err;
- return;
- }
- } else {
- // No need to cancel anything if we had no pen input device
- return;
- }
- }
-
- // Cancel pen repeat callbacks
- if (raw->penRepeatTask) {
- task_pool.cancel(raw->penRepeatTask);
- raw->penRepeatTask = nullptr;
- }
-
- raw->penInfo.type = PT_PEN;
-
- auto &penInfo = raw->penInfo.penInfo;
- penInfo.pointerInfo.pointerType = PT_PEN;
- penInfo.pointerInfo.pointerId = 0;
-
- // Populate shared pointer info fields
- populate_common_pointer_info(penInfo.pointerInfo, touch_port, pen.eventType, pen.x, pen.y);
-
- // Windows only supports a single pen button, so send all buttons as the barrel button
- if (pen.penButtons) {
- penInfo.penFlags |= PEN_FLAG_BARREL;
- } else {
- penInfo.penFlags &= ~PEN_FLAG_BARREL;
- }
-
- switch (pen.toolType) {
- default:
- case LI_TOOL_TYPE_PEN:
- penInfo.penFlags &= ~PEN_FLAG_ERASER;
- break;
- case LI_TOOL_TYPE_ERASER:
- penInfo.penFlags |= PEN_FLAG_ERASER;
- break;
- case LI_TOOL_TYPE_UNKNOWN:
- // Leave tool flags alone
- break;
- }
-
- penInfo.penMask = PEN_MASK_NONE;
-
- // Windows doesn't support hover distance, so only pass pressure/distance when the pointer is in contact
- if ((penInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) && pen.pressureOrDistance != 0.0f) {
- penInfo.penMask |= PEN_MASK_PRESSURE;
-
- // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses
- penInfo.pressure = (UINT32) (pen.pressureOrDistance * 1024);
- } else {
- // The default pen pressure is 0
- penInfo.pressure = 0;
- }
-
- if (pen.rotation != LI_ROT_UNKNOWN) {
- penInfo.penMask |= PEN_MASK_ROTATION;
- penInfo.rotation = pen.rotation;
- } else {
- penInfo.rotation = 0;
- }
-
- // We require rotation and tilt to perform the conversion to X and Y tilt angles
- if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) {
- auto rotationRads = pen.rotation * (M_PI / 180.f);
- auto tiltRads = pen.tilt * (M_PI / 180.f);
- auto r = std::sin(tiltRads);
- auto z = std::cos(tiltRads);
-
- // Convert polar coordinates into X and Y tilt angles
- penInfo.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y;
- penInfo.tiltX = (INT32) (std::atan2(std::sin(-rotationRads) * r, z) * 180.f / M_PI);
- penInfo.tiltY = (INT32) (std::atan2(std::cos(-rotationRads) * r, z) * 180.f / M_PI);
- } else {
- penInfo.tiltX = 0;
- penInfo.tiltY = 0;
- }
-
- if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) {
- auto err = GetLastError();
- BOOST_LOG(warning) << "Failed to inject virtual pen input: "sv << err;
- return;
- }
-
- // Clear pointer flags that should only remain set for one frame
- penInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS;
-
- // If we still have an active pen interaction, refresh the pen state periodically
- if (penInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
- raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id;
- }
+ virtualhid::pen_update(((client_input_raw_t *) input)->virtualhid, touch_port, pen);
}
void unicode(input_t &input, char *utf8, int size) {
- // We can do no worse than one UTF-16 character per byte of UTF-8
- std::vector wide(size);
-
- int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, size, wide.data(), size);
- if (chars <= 0) {
- return;
- }
-
- // Send all key down events
- for (int i = 0; i < chars; i++) {
- INPUT input {};
- input.type = INPUT_KEYBOARD;
- input.ki.wScan = wide[i];
- input.ki.dwFlags = KEYEVENTF_UNICODE;
- send_input(input);
- }
-
- // Send all key up events
- for (int i = 0; i < chars; i++) {
- INPUT input {};
- input.type = INPUT_KEYBOARD;
- input.ki.wScan = wide[i];
- input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
- send_input(input);
- }
+ virtualhid::unicode(((input_raw_t *) input.get())->virtualhid, utf8, size);
}
int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
auto raw = (input_raw_t *) input.get();
- if (!raw->vigem) {
+ if (virtualhid::alloc_gamepad(raw->virtualhid, id, metadata, feedback_queue) == 0) {
return 0;
}
+ if (!vigem_fallback_allowed()) {
+ BOOST_LOG(warning) << "libvirtualhid could not create the requested gamepad profile, and ViGEm fallback cannot emulate "sv << config::input.gamepad;
+ return -1;
+ }
+
+ if (!ensure_vigem(raw)) {
+ return -1;
+ }
+
VIGEM_TARGET_TYPE selectedGamepadType;
if (config::input.gamepad == "x360"sv) {
@@ -1302,6 +686,11 @@ namespace platf {
void free_gamepad(input_t &input, int nr) {
auto raw = (input_raw_t *) input.get();
+ if (virtualhid::has_gamepad(raw->virtualhid, nr)) {
+ virtualhid::free_gamepad(raw->virtualhid, nr);
+ return;
+ }
+
if (!raw->vigem) {
return;
}
@@ -1560,7 +949,13 @@ namespace platf {
* @param gamepad_state The gamepad button/axis state sent from the client.
*/
void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
- auto vigem = ((input_raw_t *) input.get())->vigem;
+ auto raw = (input_raw_t *) input.get();
+ if (virtualhid::has_gamepad(raw->virtualhid, nr)) {
+ virtualhid::gamepad_update(raw->virtualhid, nr, gamepad_state);
+ return;
+ }
+
+ auto vigem = raw->vigem;
// If there is no gamepad support
if (!vigem) {
@@ -1592,7 +987,13 @@ namespace platf {
* @param touch The touch event.
*/
void gamepad_touch(input_t &input, const gamepad_touch_t &touch) {
- auto vigem = ((input_raw_t *) input.get())->vigem;
+ auto raw = (input_raw_t *) input.get();
+ if (virtualhid::has_gamepad(raw->virtualhid, touch.id.globalIndex)) {
+ virtualhid::gamepad_touch(raw->virtualhid, touch);
+ return;
+ }
+
+ auto vigem = raw->vigem;
// If there is no gamepad support
if (!vigem) {
@@ -1698,7 +1099,13 @@ namespace platf {
* @param motion The motion event.
*/
void gamepad_motion(input_t &input, const gamepad_motion_t &motion) {
- auto vigem = ((input_raw_t *) input.get())->vigem;
+ auto raw = (input_raw_t *) input.get();
+ if (virtualhid::has_gamepad(raw->virtualhid, motion.id.globalIndex)) {
+ virtualhid::gamepad_motion(raw->virtualhid, motion);
+ return;
+ }
+
+ auto vigem = raw->vigem;
// If there is no gamepad support
if (!vigem) {
@@ -1725,7 +1132,13 @@ namespace platf {
* @param battery The battery event.
*/
void gamepad_battery(input_t &input, const gamepad_battery_t &battery) {
- auto vigem = ((input_raw_t *) input.get())->vigem;
+ auto raw = (input_raw_t *) input.get();
+ if (virtualhid::has_gamepad(raw->virtualhid, battery.id.globalIndex)) {
+ virtualhid::gamepad_battery(raw->virtualhid, battery);
+ return;
+ }
+
+ auto vigem = raw->vigem;
// If there is no gamepad support
if (!vigem) {
@@ -1799,33 +1212,14 @@ namespace platf {
}
std::vector &supported_gamepads(input_t *input) {
- if (!input) {
- static std::vector gps {
- supported_gamepad_t {"auto", true, ""},
- supported_gamepad_t {"x360", false, ""},
- supported_gamepad_t {"ds4", false, ""},
- };
-
+ static std::vector gps;
+ if (!input || !input->get()) {
+ gps = virtualhid::static_supported_gamepads();
return gps;
}
- auto vigem = ((input_raw_t *) input)->vigem;
- auto enabled = vigem != nullptr;
- auto reason = enabled ? "" : "gamepads.vigem-not-available";
-
- // ds4 == ps4
- static std::vector gps {
- supported_gamepad_t {"auto", true, reason},
- supported_gamepad_t {"x360", enabled, reason},
- supported_gamepad_t {"ds4", enabled, reason}
- };
-
- for (auto &[name, is_enabled, reason_disabled] : gps) {
- if (!is_enabled) {
- BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled;
- }
- }
-
+ const auto raw = (input_raw_t *) input->get();
+ gps = virtualhid::supported_gamepads(raw->virtualhid.runtime.get(), raw->vigem != nullptr);
return gps;
}
@@ -1836,18 +1230,20 @@ namespace platf {
platform_caps::caps_t get_capabilities() {
platform_caps::caps_t caps = 0;
- // We support controller touchpad input as long as we're not emulating X360
- if (config::input.gamepad != "x360"sv) {
+ if (virtualhid::configured_gamepad_supports_touchpad()) {
caps |= platform_caps::controller_touch;
}
- // We support pen and touch input on Win10 1809+
- if (GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice") != nullptr) {
- if (config::input.native_pen_touch) {
- caps |= platform_caps::pen_touch;
+ if (config::input.native_pen_touch) {
+ const auto runtime = virtualhid::create_runtime();
+ if (runtime) {
+ const auto &capabilities = runtime->capabilities();
+ if (capabilities.supports_touchscreen || capabilities.supports_pen_tablet) {
+ caps |= platform_caps::pen_touch;
+ }
+ } else {
+ BOOST_LOG(warning) << "Unable to create libvirtualhid runtime for touch/pen capability detection"sv;
}
- } else {
- BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv;
}
return caps;
diff --git a/src/platform/windows/keylayout.h b/src/platform/windows/keylayout.h
deleted file mode 100644
index ac0a4fa2060..00000000000
--- a/src/platform/windows/keylayout.h
+++ /dev/null
@@ -1,275 +0,0 @@
-/**
- * @file src/platform/windows/keylayout.h
- * @brief Keyboard layout mapping for scancode translation
- */
-#pragma once
-
-// standard includes
-#include
-#include
-
-namespace platf {
- // Virtual Key to Scan Code mapping for the US English layout (00000409).
- // GameStream uses this as the canonical key layout for scancode conversion.
- /**
- * @brief Protocol or platform constant for vk to scancode map.
- */
- constexpr std::array::max() + 1> VK_TO_SCANCODE_MAP {
- 0, /* 0x00 */
- 0, /* 0x01 */
- 0, /* 0x02 */
- 70, /* 0x03 */
- 0, /* 0x04 */
- 0, /* 0x05 */
- 0, /* 0x06 */
- 0, /* 0x07 */
- 14, /* 0x08 */
- 15, /* 0x09 */
- 0, /* 0x0a */
- 0, /* 0x0b */
- 76, /* 0x0c */
- 28, /* 0x0d */
- 0, /* 0x0e */
- 0, /* 0x0f */
- 42, /* 0x10 */
- 29, /* 0x11 */
- 56, /* 0x12 */
- 0, /* 0x13 */
- 58, /* 0x14 */
- 0, /* 0x15 */
- 0, /* 0x16 */
- 0, /* 0x17 */
- 0, /* 0x18 */
- 0, /* 0x19 */
- 0, /* 0x1a */
- 1, /* 0x1b */
- 0, /* 0x1c */
- 0, /* 0x1d */
- 0, /* 0x1e */
- 0, /* 0x1f */
- 57, /* 0x20 */
- 73, /* 0x21 */
- 81, /* 0x22 */
- 79, /* 0x23 */
- 71, /* 0x24 */
- 75, /* 0x25 */
- 72, /* 0x26 */
- 77, /* 0x27 */
- 80, /* 0x28 */
- 0, /* 0x29 */
- 0, /* 0x2a */
- 0, /* 0x2b */
- 84, /* 0x2c */
- 82, /* 0x2d */
- 83, /* 0x2e */
- 99, /* 0x2f */
- 11, /* 0x30 */
- 2, /* 0x31 */
- 3, /* 0x32 */
- 4, /* 0x33 */
- 5, /* 0x34 */
- 6, /* 0x35 */
- 7, /* 0x36 */
- 8, /* 0x37 */
- 9, /* 0x38 */
- 10, /* 0x39 */
- 0, /* 0x3a */
- 0, /* 0x3b */
- 0, /* 0x3c */
- 0, /* 0x3d */
- 0, /* 0x3e */
- 0, /* 0x3f */
- 0, /* 0x40 */
- 30, /* 0x41 */
- 48, /* 0x42 */
- 46, /* 0x43 */
- 32, /* 0x44 */
- 18, /* 0x45 */
- 33, /* 0x46 */
- 34, /* 0x47 */
- 35, /* 0x48 */
- 23, /* 0x49 */
- 36, /* 0x4a */
- 37, /* 0x4b */
- 38, /* 0x4c */
- 50, /* 0x4d */
- 49, /* 0x4e */
- 24, /* 0x4f */
- 25, /* 0x50 */
- 16, /* 0x51 */
- 19, /* 0x52 */
- 31, /* 0x53 */
- 20, /* 0x54 */
- 22, /* 0x55 */
- 47, /* 0x56 */
- 17, /* 0x57 */
- 45, /* 0x58 */
- 21, /* 0x59 */
- 44, /* 0x5a */
- 91, /* 0x5b */
- 92, /* 0x5c */
- 93, /* 0x5d */
- 0, /* 0x5e */
- 95, /* 0x5f */
- 82, /* 0x60 */
- 79, /* 0x61 */
- 80, /* 0x62 */
- 81, /* 0x63 */
- 75, /* 0x64 */
- 76, /* 0x65 */
- 77, /* 0x66 */
- 71, /* 0x67 */
- 72, /* 0x68 */
- 73, /* 0x69 */
- 55, /* 0x6a */
- 78, /* 0x6b */
- 0, /* 0x6c */
- 74, /* 0x6d */
- 83, /* 0x6e */
- 53, /* 0x6f */
- 59, /* 0x70 */
- 60, /* 0x71 */
- 61, /* 0x72 */
- 62, /* 0x73 */
- 63, /* 0x74 */
- 64, /* 0x75 */
- 65, /* 0x76 */
- 66, /* 0x77 */
- 67, /* 0x78 */
- 68, /* 0x79 */
- 87, /* 0x7a */
- 88, /* 0x7b */
- 100, /* 0x7c */
- 101, /* 0x7d */
- 102, /* 0x7e */
- 103, /* 0x7f */
- 104, /* 0x80 */
- 105, /* 0x81 */
- 106, /* 0x82 */
- 107, /* 0x83 */
- 108, /* 0x84 */
- 109, /* 0x85 */
- 110, /* 0x86 */
- 118, /* 0x87 */
- 0, /* 0x88 */
- 0, /* 0x89 */
- 0, /* 0x8a */
- 0, /* 0x8b */
- 0, /* 0x8c */
- 0, /* 0x8d */
- 0, /* 0x8e */
- 0, /* 0x8f */
- 69, /* 0x90 */
- 70, /* 0x91 */
- 0, /* 0x92 */
- 0, /* 0x93 */
- 0, /* 0x94 */
- 0, /* 0x95 */
- 0, /* 0x96 */
- 0, /* 0x97 */
- 0, /* 0x98 */
- 0, /* 0x99 */
- 0, /* 0x9a */
- 0, /* 0x9b */
- 0, /* 0x9c */
- 0, /* 0x9d */
- 0, /* 0x9e */
- 0, /* 0x9f */
- 42, /* 0xa0 */
- 54, /* 0xa1 */
- 29, /* 0xa2 */
- 29, /* 0xa3 */
- 56, /* 0xa4 */
- 56, /* 0xa5 */
- 106, /* 0xa6 */
- 105, /* 0xa7 */
- 103, /* 0xa8 */
- 104, /* 0xa9 */
- 101, /* 0xaa */
- 102, /* 0xab */
- 50, /* 0xac */
- 32, /* 0xad */
- 46, /* 0xae */
- 48, /* 0xaf */
- 25, /* 0xb0 */
- 16, /* 0xb1 */
- 36, /* 0xb2 */
- 34, /* 0xb3 */
- 108, /* 0xb4 */
- 109, /* 0xb5 */
- 107, /* 0xb6 */
- 33, /* 0xb7 */
- 0, /* 0xb8 */
- 0, /* 0xb9 */
- 39, /* 0xba */
- 13, /* 0xbb */
- 51, /* 0xbc */
- 12, /* 0xbd */
- 52, /* 0xbe */
- 53, /* 0xbf */
- 41, /* 0xc0 */
- 115, /* 0xc1 */
- 126, /* 0xc2 */
- 0, /* 0xc3 */
- 0, /* 0xc4 */
- 0, /* 0xc5 */
- 0, /* 0xc6 */
- 0, /* 0xc7 */
- 0, /* 0xc8 */
- 0, /* 0xc9 */
- 0, /* 0xca */
- 0, /* 0xcb */
- 0, /* 0xcc */
- 0, /* 0xcd */
- 0, /* 0xce */
- 0, /* 0xcf */
- 0, /* 0xd0 */
- 0, /* 0xd1 */
- 0, /* 0xd2 */
- 0, /* 0xd3 */
- 0, /* 0xd4 */
- 0, /* 0xd5 */
- 0, /* 0xd6 */
- 0, /* 0xd7 */
- 0, /* 0xd8 */
- 0, /* 0xd9 */
- 0, /* 0xda */
- 26, /* 0xdb */
- 43, /* 0xdc */
- 27, /* 0xdd */
- 40, /* 0xde */
- 0, /* 0xdf */
- 0, /* 0xe0 */
- 0, /* 0xe1 */
- 86, /* 0xe2 */
- 0, /* 0xe3 */
- 0, /* 0xe4 */
- 0, /* 0xe5 */
- 0, /* 0xe6 */
- 0, /* 0xe7 */
- 0, /* 0xe8 */
- 113, /* 0xe9 */
- 92, /* 0xea */
- 123, /* 0xeb */
- 0, /* 0xec */
- 111, /* 0xed */
- 90, /* 0xee */
- 0, /* 0xef */
- 0, /* 0xf0 */
- 91, /* 0xf1 */
- 0, /* 0xf2 */
- 95, /* 0xf3 */
- 0, /* 0xf4 */
- 94, /* 0xf5 */
- 0, /* 0xf6 */
- 0, /* 0xf7 */
- 0, /* 0xf8 */
- 93, /* 0xf9 */
- 0, /* 0xfa */
- 98, /* 0xfb */
- 0, /* 0xfc */
- 0, /* 0xfd */
- 0, /* 0xfe */
- 0, /* 0xff */
- };
-} // namespace platf
diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html
index a9476ea02bf..cf36f0001f7 100644
--- a/src_assets/common/assets/web/config.html
+++ b/src_assets/common/assets/web/config.html
@@ -227,7 +227,7 @@
this.preReleaseVersion = new SunshineVersion((await fetch("https://api.github.com/repos/LizardByte/Sunshine/releases").then((r) => r.json())).find(release => release.prerelease), null);
console.log("Pre-Release Version: ", this.preReleaseVersion.version)
- // Fetch ViGEmBus status only on Windows when controller is enabled
+ // Fetch virtual input driver status only on Windows when controller is enabled
if (this.platform === 'windows' && this.controllerEnabled) {
try {
- this.vigembus = await fetch("./api/vigembus/status").then((r) => r.json());
+ const virtualInputStatus = await fetch("./api/virtual-input/status").then((r) => r.json());
+ this.virtualhid = virtualInputStatus.virtualhid;
+ this.vigembus = virtualInputStatus.vigembus;
} catch (e) {
- console.error("Failed to fetch ViGEmBus status:", e);
+ console.error("Failed to fetch virtual input driver status:", e);
}
}
} catch (e) {
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 397abb4a32e..81463444d3f 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -224,8 +224,8 @@
"dd_wa_hdr_toggle_delay": "High-contrast workaround for HDR",
"ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click",
"ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click",
- "ds5_inputtino_randomize_mac": "Randomize virtual controller MAC",
- "ds5_inputtino_randomize_mac_desc": "Upon controller registration use a random MAC instead of one based on the controllers internal index to avoid mixing configuration settings of different controllers when the are swapped on client-side.",
+ "virtualhid_randomize_mac": "Randomize virtual controller MAC",
+ "virtualhid_randomize_mac_desc": "Use a random MAC address for PlayStation-style virtual controllers instead of one based on the controller index. This avoids mixing per-controller settings when controllers are swapped on the client.",
"encoder": "Force a Specific Encoder",
"encoder_desc": "Force a specific encoder, otherwise Sunshine will select the best available option. Note: If you specify a hardware encoder on Windows, it must match the GPU where the display is connected.",
"encoder_software": "Software",
@@ -246,10 +246,12 @@
"gamepad_ds4_manual": "DS4 selection options",
"gamepad_ds5": "DS5 (PS5)",
"gamepad_ds5_manual": "DS5 selection options",
+ "gamepad_generic": "Generic HID gamepad",
"gamepad_switch": "Nintendo Pro (Switch)",
"gamepad_manual": "Manual DS4 options",
"gamepad_x360": "X360 (Xbox 360)",
"gamepad_xone": "XOne (Xbox One)",
+ "gamepad_xseries": "Xbox Series",
"global_prep_cmd": "Command Preparations",
"global_prep_cmd_desc": "Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.",
"hevc_mode": "HEVC Support",
@@ -433,10 +435,12 @@
"startup_errors": "Attention! Sunshine detected these errors during startup. We STRONGLY RECOMMEND fixing them before streaming.",
"version_dirty": "Thank you for helping to make Sunshine a better software!",
"version_latest": "You are running the latest version of Sunshine",
- "vigembus_not_installed_desc": "Virtual gamepad support will not work without the ViGEmBus driver. Click the button below to install it.",
- "vigembus_not_installed_title": "ViGEmBus Driver Not Installed",
- "vigembus_outdated_desc": "You are running an outdated version of ViGEmBus (v{version}). Version 1.17 or higher is required for proper gamepad support. Click the button below to update.",
- "vigembus_outdated_title": "ViGEmBus Driver Outdated",
+ "virtualhid_missing_vigembus_installed_desc": "ViGEmBus is installed, but without the libvirtualhid driver Sunshine can only offer limited fallback gamepad support.",
+ "virtualhid_missing_vigembus_installed_title": "Gamepad Support Is Limited",
+ "virtualhid_not_installed_desc": "Sunshine requires the libvirtualhid Windows driver for full virtual gamepad support. Install the libvirtualhid driver package, then restart Sunshine.",
+ "virtualhid_not_installed_title": "libvirtualhid Driver Not Installed",
+ "virtualhid_outdated_desc": "The installed libvirtualhid driver version (v{version}) is not supported. Supported versions: {supported_versions}.",
+ "virtualhid_outdated_title": "libvirtualhid Driver Outdated",
"welcome": "Hello, Sunshine!"
},
"navbar": {
@@ -513,6 +517,15 @@
"dd_reset_desc": "If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.",
"dd_reset_error": "Error while resetting persistence!",
"dd_reset_success": "Success resetting persistence!",
+ "driver_installed_version": "Installed Version",
+ "driver_name": "Driver",
+ "driver_not_installed": "Not installed",
+ "driver_status": "Status",
+ "driver_status_compatible": "Compatible",
+ "driver_status_not_installed": "Not installed",
+ "driver_status_unsupported": "Unsupported version",
+ "driver_supported_versions": "Supported Versions",
+ "driver_version_unknown": "Not reported",
"force_close": "Force Close",
"force_close_desc": "If Moonlight complains about an app currently running, force closing the app should fix the issue.",
"force_close_error": "Error while closing Application",
@@ -532,16 +545,10 @@
"unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.",
"unpair_single_unknown": "Unknown Client",
"unpair_title": "Unpair Devices",
- "vigembus_compatible": "ViGEmBus is installed and compatible.",
- "vigembus_current_version": "Current Version",
- "vigembus_desc": "ViGEmBus is required for virtual gamepad support. Install or update the driver if it's missing or outdated (version 1.17 or higher required).",
- "vigembus_incompatible": "ViGEmBus version is too old. Please install version 1.17 or higher.",
- "vigembus_install": "ViGEmBus Driver",
- "vigembus_install_button": "Install ViGEmBus v{version}",
- "vigembus_install_error": "Failed to install ViGEmBus driver.",
- "vigembus_install_success": "ViGEmBus driver installed successfully! You may need to restart your computer.",
- "vigembus_force_reinstall_button": "Force Reinstall ViGEmBus v{version}",
- "vigembus_not_installed": "ViGEmBus is not installed."
+ "vigembus_driver": "ViGEmBus Fallback Driver",
+ "virtual_gamepad": "Virtual Gamepad",
+ "virtual_gamepad_desc": "Sunshine uses libvirtualhid for full virtual gamepad support on Windows. ViGEmBus is detected only as a limited fallback for Xbox 360 and DualShock 4 gamepads.",
+ "virtualhid_driver": "libvirtualhid Driver"
},
"featured": {
"categories": {
diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html
index 498267a0f6e..8b8eb58c9ea 100644
--- a/src_assets/common/assets/web/troubleshooting.html
+++ b/src_assets/common/assets/web/troubleshooting.html
@@ -9,43 +9,44 @@
.then((r) => {
this.platform = r.platform;
this.controllerEnabled = r.controller !== "disabled";
- // Fetch ViGEmBus status only on Windows when gamepad is enabled
+ // Fetch virtual input driver status only on Windows when gamepad is enabled
if (this.platform === 'windows' && this.controllerEnabled) {
- this.refreshViGEmBusStatus();
+ this.refreshVirtualInputStatus();
}
});
@@ -538,54 +542,51 @@