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 @@ -459,7 +468,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
Description - Randomize the MAC-Address for the generated virtual controller. - @hint{Only applies on linux for gamepads created as PS5-style controllers} + Randomize the MAC address for PlayStation-style virtual controllers created by libvirtualhid.
Example @code{} - ds5_inputtino_randomize_mac = enabled + virtualhid_randomize_mac = enabled @endcode
diff --git a/docs/getting_started.md b/docs/getting_started.md index 1e3582c4d56..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. -![ViGEmBus Installation](images/vigembus-installer.png) +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!
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 @@

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

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