feat(macos): add gamepad support using libvirtualhid (#5807)

This commit is contained in:
Dave Lane authored and GitHub committed 2026-09-27 16:20:34 -04:00
1 parent c69d370c35
commit 8fc40974c9
34 files changed
+993 -296

No files matched your search

+10 -1
View File
@@ -9,9 +9,18 @@ project(Sunshine VERSION 0.0.0
DESCRIPTION "Self-hosted game stream host for Moonlight"
HOMEPAGE_URL "https://app.lizardbyte.dev/Sunshine")
# Minimum separately installed Virtual HID Driver version supported by this build.
# Minimum separately installed Virtual HID Broker version supported by this build.
set(LIBVIRTUALHID_MINIMUM_VERSION "2026.914.1218.10")
list(APPEND SUNSHINE_DEFINITIONS LIBVIRTUALHID_MINIMUM_VERSION="${LIBVIRTUALHID_MINIMUM_VERSION}")
if(APPLE)
# macOS bundle versions have three components; the Windows driver uses four.
if(LIBVIRTUALHID_MINIMUM_VERSION MATCHES "^([0-9]+\\.[0-9]+\\.[0-9]+)\\.[0-9]+$")
set(LIBVIRTUALHID_MACOS_MINIMUM_VERSION "${CMAKE_MATCH_1}")
else()
set(LIBVIRTUALHID_MACOS_MINIMUM_VERSION "${LIBVIRTUALHID_MINIMUM_VERSION}")
endif()
list(APPEND SUNSHINE_DEFINITIONS LIBVIRTUALHID_MACOS_MINIMUM_VERSION="${LIBVIRTUALHID_MACOS_MINIMUM_VERSION}")
endif()
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+16 -14
View File
@@ -56,55 +56,57 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz
<td>Generic</td>
<td>🟡<sup>1</sup></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅<sup>2</sup></td>
<td>✅<sup>3</sup></td>
</tr>
<tr>
<td>DualShock / DS4 (PlayStation 4)</td>
<td>🟡<sup>1</sup></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅<sup>2</sup></td>
<td>✅<sup>3</sup></td>
</tr>
<tr>
<td>DualSense / DS5 (PlayStation 5)</td>
<td>🟡<sup>1</sup></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅<sup>2</sup></td>
<td>✅<sup>3</sup></td>
</tr>
<tr>
<td>Nintendo Switch Pro</td>
<td>🟡<sup>1</sup></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅<sup>2</sup></td>
<td>✅<sup>3</sup></td>
</tr>
<tr>
<td>Xbox 360</td>
<td>🟡<sup>1</sup></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅<sup>2</sup></td>
<td>✅<sup>3</sup></td>
</tr>
<tr>
<td>Xbox One</td>
<td>🟡<sup>1</sup></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅<sup>2</sup></td>
<td>✅<sup>3</sup></td>
</tr>
<tr>
<td>Xbox Series</td>
<td>🟡<sup>1</sup></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
<td>✅<sup>2</sup></td>
<td>✅<sup>3</sup></td>
</tr>
</table>
> [!NOTE]
> <sup>1</sup> Missing motion, touchpad input, battery state, RGB LEDs, adaptive triggers, and raw HID output reports.
> <sup>2</sup> Requires the separately installed and licensed Virtual HID Broker.
> <sup>3</sup> All profiles are available through the separately installed and licensed Virtual HID Broker. Xbox 360 and DualShock 4 can also use ViGEmBus.
<table>
<caption id="encoding_api">Encoding API</caption>
+10 -1
View File
@@ -16,9 +16,17 @@ set(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/sunshine.png)
set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}")
set(CPACK_STRIP_FILES YES)
# Keep macOS assets in the Runtime component so app assets are installed
# before macos.cmake signs the finished bundle.
set(_sunshine_common_asset_component)
if(APPLE)
set(_sunshine_common_asset_component COMPONENT Runtime)
endif()
# install common assets
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}"
${_sunshine_common_asset_component}
PATTERN "web" EXCLUDE)
# copy assets to build directory, for running without install
file(GLOB_RECURSE ALL_ASSETS
@@ -31,7 +39,8 @@ endforeach()
# install built vite assets
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets/web"
DESTINATION "${SUNSHINE_ASSETS_DIR}")
DESTINATION "${SUNSHINE_ASSETS_DIR}"
${_sunshine_common_asset_component})
# platform specific packaging
if(WIN32)
+16 -11
View File
@@ -319,12 +319,12 @@ supported on the current platform.
<tr>
<td>Description</td>
<td colspan="2">
Controls which virtual gamepad drivers Sunshine may use. The Web UI and startup notification continue to
request a choice while this option is not set. If Sunshine detects an active Virtual HID Driver license,
it automatically sets this option to `all` when it is missing.
Controls which virtual gamepad backend Sunshine may use on Windows and macOS. On Windows, the Web UI and
startup notification request a choice while this option is not set. If Sunshine detects an active Virtual
HID Broker license on Windows, it automatically sets this option to `all` when it is missing.
@warning{ViGEmBus has limited gamepad features, supports only Xbox 360 and DualShock 4 emulation, and has
reached end of life. Selecting `vigembus` also suppresses Virtual HID Driver startup notifications.}
@note{This option applies only to Windows.}
reached end of life. Selecting `vigembus` also suppresses Virtual HID Broker startup notifications.}
@note{`all` and `vigembus` apply only to Windows. On macOS, an unset value uses Virtual HID Broker.}
</td>
</tr>
<tr>
@@ -340,17 +340,21 @@ supported on the current platform.
@endcode</td>
</tr>
<tr>
<td rowspan="3">Choices</td>
<td rowspan="4">Choices</td>
<td>all</td>
<td>Prefer Virtual HID Driver when it is available and licensed, with ViGEmBus as a limited fallback.</td>
<td>Windows only: prefer Virtual HID Broker when it is available and licensed, with ViGEmBus as a limited fallback.</td>
</tr>
<tr>
<td>virtualhid</td>
<td>Use only Virtual HID Driver. An active paid license is required; ViGEmBus fallback is disabled.</td>
<td>Use Virtual HID Broker for gamepads. An active paid license is required; Windows ViGEmBus fallback is disabled.</td>
</tr>
<tr>
<td>vigembus</td>
<td>Use only ViGEmBus for gamepads and hide Virtual HID Driver status and licensing details.</td>
<td>Windows only: use ViGEmBus for gamepads and hide Virtual HID Broker status and licensing details.</td>
</tr>
<tr>
<td>none</td>
<td>Disable virtual gamepads on Windows and macOS without changing keyboard or mouse input. Suppress broker and driver choice notices.</td>
</tr>
</table>
@@ -360,8 +364,9 @@ supported on the current platform.
<tr>
<td>Description</td>
<td colspan="2">
The type of gamepad to emulate on the host.
@note{This option applies to FreeBSD, Linux, and Windows.}
The type of gamepad to emulate on the host. Automatic selection uses the controller type
reported by the client. If the type is unknown, Sunshine can select a PlayStation-style
controller from reported motion or touchpad support; otherwise it uses an Xbox-style controller.
@note{When gamepad_driver is `vigembus` on Windows, only auto, x360, and ds4 are available.}
</td>
</tr>
+27 -14
View File
@@ -400,7 +400,19 @@ brew uninstall sunshine
### macOS
> [!IMPORTANT]
> Sunshine on macOS is experimental. Gamepads do not work.
> Virtual gamepads require the separately installed, licensed
> [Virtual HID Broker](https://github.com/LizardByte/libvirtualhid/releases/latest).
To use gamepads, download the macOS universal libvirtualhid DMG from the link above. Open it and run
**Install libvirtualhid.command** to install Virtual HID Broker. In **System Settings > Privacy & Security >
Device Control and Data Access**, add `/Applications/VirtualHIDBroker.app` and enable it. Restart the broker if
you changed this permission with `sudo launchctl kickstart -k system/dev.lizardbyte.app.libvirtualhid`.
Activate the machine license in Sunshine’s
**Troubleshooting > Virtual HID Broker License** section. In **Configuration > Input**, turn on **Enable Gamepad Input**
and choose the emulated gamepad. The Gamepad Backend setting can be set to **None** to disable gamepads without affecting
keyboard or mouse input. Sunshine’s menu bar **Virtual HID Broker** submenu shows the license status and links
for license management and downloads. Sunshine’s DMG does not contain the broker. Keyboard and mouse input use the
standard macOS synthetic-input permission path.
#### DMG
@@ -569,15 +581,15 @@ and enter its device name in the [audio_sink](configuration.md#audio_sink) field
### Windows
Sunshine supports two virtual gamepad backends on Windows. You can install the
[Virtual HID Driver](https://github.com/LizardByte/libvirtualhid/releases/latest) separately as an optional paid upgrade
for a driver-backed Raw Input keyboard and mouse plus full virtual gamepad support. ViGEmBus remains available as a
limited alternative for Xbox 360 and DualShock 4 gamepads, but it has reached end of life.
[Virtual HID Broker](https://github.com/LizardByte/libvirtualhid/releases/latest) separately as an optional paid upgrade.
Its Windows package includes a broker service and user-mode driver for a Raw Input keyboard and mouse plus full virtual
gamepad support. ViGEmBus remains available as a limited alternative for Xbox 360 and DualShock 4 gamepads, but it has reached end of life.
When Virtual HID Driver is used, Sunshine requires version `2026.914.1218.10` or newer.
When Virtual HID Broker is used, Sunshine requires libvirtualhid version `2026.914.1218.10` or newer.
Compared with the ViGEmBus fallback, Virtual HID Driver can create Xbox One, Xbox Series, DualSense, Nintendo Switch
Compared with the ViGEmBus fallback, Virtual HID Broker can create Xbox One, Xbox Series, DualSense, Nintendo Switch
Pro, and Generic gamepads in addition to Xbox 360 and DualShock 4. It can also expose controller-specific features such
as motion, touchpads, LEDs, and adaptive triggers when supported. Virtual HID Driver is actively developed and
as motion, touchpads, LEDs, and adaptive triggers when supported. Virtual HID Broker is actively developed and
supported by the LizardByte team.
With a compatible driver and active license, normal key transitions are exposed through a real HID keyboard so
@@ -589,15 +601,16 @@ Relative mouse movement, buttons, and scrolling are exposed as a real HID
mouse so applications using Raw Input can receive them. Absolute mouse positioning continues to use Windows input
injection. When the driver-backed mouse cannot be created, libvirtualhid retains its legacy SendInput fallback.
The Virtual HID Driver requires an active paid machine license for driver-backed devices, including gamepads and the Raw
The Virtual HID Broker requires an active paid machine license for driver-backed devices, including gamepads and the Raw
Input keyboard and mouse. Sunshine shows the current license status and actions on the Web UI Troubleshooting page and
in the **Virtual HID Driver** system tray submenu. In **Configuration > Input**, choose whether Sunshine may use **All
Available Drivers**, only **Virtual HID Driver**, or only **ViGEmBus**. Sunshine continues to show the selection prompt
until this setting is saved. If an active Virtual HID Driver license is already present, Sunshine selects **All Available
Drivers** automatically. That policy prefers Virtual HID Driver. When its license is not valid, Sunshine falls back to
in the **Virtual HID Broker** system tray submenu. In **Configuration > Input**, choose whether Sunshine may use **All
Available Drivers**, only **Virtual HID Broker**, or only **ViGEmBus**. Sunshine continues to show the selection prompt
until this setting is saved. If an active Virtual HID Broker license is already present, Sunshine selects **All Available
Drivers** automatically. That policy prefers Virtual HID Broker. When its license is not valid, Sunshine falls back to
ViGEmBus for Xbox 360 and DualShock 4 gamepads and to SendInput for keyboard and mouse. Selecting only **ViGEmBus**
suppresses Virtual HID Driver startup notifications and limits the available emulated gamepads to Xbox 360 and
DualShock 4. Sunshine recreates the shared keyboard and mouse after a successful license action, so switching between
suppresses Virtual HID Broker startup notifications and limits the available emulated gamepads to Xbox 360 and
DualShock 4. **None** disables all virtual gamepads and their startup notices without affecting keyboard or mouse
input. Sunshine recreates the shared keyboard and mouse after a successful license action, so switching between
the HID and SendInput paths does not require restarting Sunshine.
After installing or updating virtual input drivers, it is recommended to restart your computer.
+29 -16
View File
@@ -306,6 +306,18 @@ Some users have reported stuttering issues when streaming games running within G
## macOS
### No gamepad detected
Confirm **Enable Gamepad Input** is on under **Configuration > Input**. Install
[Virtual HID Broker](https://github.com/LizardByte/libvirtualhid/releases/latest) separately from Sunshine,
using the macOS installer in its universal DMG. Confirm that `/Applications/VirtualHIDBroker.app` is allowed in
**System Settings > Privacy & Security > Device Control and Data Access**, and activate a machine license under
**Troubleshooting > Virtual HID Broker License** in Sunshine. The menu bar **Virtual HID Broker** submenu also
shows license status and links for license management and downloads. If the broker or license is unavailable, the
Troubleshooting page shows the broker response. Sunshine can show a startup notice when gamepad input is enabled
and the broker or license is unavailable. Choose **None** under **Configuration > Input > Gamepad Backend** to disable
virtual gamepads and that notice without affecting keyboard or mouse input. Reconnect the Moonlight session after choosing
a different emulated gamepad profile.
### Dynamic session lookup failed
If you get this error:
@@ -321,46 +333,47 @@ launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist
### No gamepad detected
Sunshine supports two virtual gamepad backends on Windows. You can install the
[Virtual HID Driver](https://github.com/LizardByte/libvirtualhid/releases/latest) separately as an optional paid upgrade
for a driver-backed Raw Input keyboard and mouse plus full virtual gamepad support. ViGEmBus is a limited alternative
for Xbox 360 and DualShock 4 support that has reached end of life. If you use the
[Virtual HID Broker](https://github.com/LizardByte/libvirtualhid/releases/latest) separately as an optional paid upgrade.
Its Windows package includes a broker service and user-mode driver for a Raw Input keyboard and mouse plus full virtual
gamepad support. ViGEmBus is a limited alternative for Xbox 360 and DualShock 4 support that has reached end of life. If you use the
[ViGEmBus fallback](https://github.com/nefarius/ViGEmBus/releases/latest), you must use version 1.17 or newer.
When Virtual HID Driver is used, Sunshine requires version `2026.914.1218.10` or newer.
When Virtual HID Broker is used, Sunshine requires libvirtualhid version `2026.914.1218.10` or newer.
Virtual HID Driver adds Xbox One, Xbox Series, DualSense, Nintendo Switch Pro, and Generic gamepads, plus advanced
Virtual HID Broker adds Xbox One, Xbox Series, DualSense, Nintendo Switch Pro, and Generic gamepads, plus advanced
controller features such as motion, touchpads, LEDs, and adaptive triggers when supported. Unlike the discontinued
ViGEmBus project, Virtual HID Driver is actively developed and supported by the LizardByte team.
ViGEmBus project, Virtual HID Broker is actively developed and supported by the LizardByte team.
An active paid Virtual HID Driver machine license is required before Sunshine can create driver-backed libvirtualhid
An active paid Virtual HID Broker machine license is required before Sunshine can create driver-backed libvirtualhid
devices, including gamepads and the Raw Input keyboard and mouse. Use the message on the Web UI home page, the startup
tray notification, or **Get/Manage License** in the **Virtual HID Driver** tray submenu to open the license section on
the Troubleshooting page. In **Configuration > Input**, select **All Available Drivers**, only **Virtual HID Driver**, or
tray notification, or **Get/Manage License** in the **Virtual HID Broker** tray submenu to open the license section on
the Troubleshooting page. In **Configuration > Input**, select **All Available Drivers**, only **Virtual HID Broker**, or
only **ViGEmBus**. Sunshine keeps prompting until this setting is saved, but automatically selects **All Available
Drivers** when it detects an existing active Virtual HID Driver license. Whenever the Virtual HID Driver license is not
Drivers** when it detects an existing active Virtual HID Broker license. Whenever the Virtual HID Broker license is not
valid, **All Available Drivers** falls back to a compatible ViGEmBus installation for Xbox 360 and DualShock 4 gamepads
and to SendInput for keyboard and mouse. Selecting only **ViGEmBus** suppresses Virtual HID Driver startup notifications
and hides its status and license details from the Troubleshooting page.
and to SendInput for keyboard and mouse. Selecting only **ViGEmBus** suppresses Virtual HID Broker startup notifications
and hides its status and license details from the Troubleshooting page. Selecting **None** disables all virtual
gamepads and suppresses broker and driver choice notices without affecting keyboard or mouse input.
After installation, it is recommended to restart your computer.
### Games do not detect keyboard input
With a compatible Virtual HID Driver and active license, Sunshine sends normal key transitions through a real HID
With a compatible Virtual HID Broker and active license, Sunshine sends normal key transitions through a real HID
keyboard so games using Raw Input can receive them. Unicode text input and keys outside the supported HID keyboard
page continue to use Windows input injection. When the driver-backed keyboard cannot be created because the driver,
broker, or license is unavailable, libvirtualhid falls back to SendInput.
Check the Virtual HID Driver version and license sections on the Web UI Troubleshooting page. Sunshine recreates the
Check the libvirtualhid driver version and Virtual HID Broker license on the Web UI Troubleshooting page. Sunshine recreates the
shared keyboard and mouse after a successful license activation, validation, or deactivation, so you do not need to
restart Sunshine merely to switch between the HID and SendInput paths.
### Games do not detect mouse input
With a compatible Virtual HID Driver and active license, Sunshine sends relative mouse movement, buttons, and scrolling
With a compatible Virtual HID Broker and active license, Sunshine sends relative mouse movement, buttons, and scrolling
through a real HID device so games using Raw Input can receive them. Absolute positioning still uses Windows input
injection. When the driver-backed mouse cannot be created, libvirtualhid falls back to SendInput; the Windows cursor may
still move even though a game that listens only for Raw Input receives nothing.
Check the Virtual HID Driver version and license sections on the Web UI Troubleshooting page even when controller input
Check the libvirtualhid driver version and Virtual HID Broker license on the Web UI Troubleshooting page even when controller input
is disabled. The same live refresh used by the keyboard path also switches the mouse between HID and SendInput without
requiring a Sunshine restart.
+1 -4
View File
@@ -27,11 +27,8 @@
- title: "Control"
icon_fa: "fas fa-gamepad"
description: >
Sunshine emulates an Xbox, PlayStation, or Nintendo Switch controller.
Sunshine emulates Xbox 360|One|Series, PlayStation DualShock 4|DualSense, or Nintendo Switch Pro controllers.
Use nearly any controller on your Moonlight client!
<br><small><ul>
<li>Gamepad emulation is not currently supported on macOS.</li>
</ul></small>
- title: "Configurable"
icon_fa: "fas fa-gear"
+14 -13
View File
@@ -850,11 +850,8 @@ namespace config {
500ms, // key_repeat_delay
std::chrono::duration<double> {1 / 24.9}, // key_repeat_period
{
platf::supported_gamepads(nullptr).front().name.data(),
platf::supported_gamepads(nullptr).front().name.size(),
}, // Default gamepad
{}, // gamepad_driver remains unset until the user chooses a Windows driver policy
"auto", // Default gamepad profile.
{}, // Windows requests a backend choice; macOS defaults to Virtual HID Broker.
true, // back as touchpad click enabled for PlayStation-style gamepads
true, // client gamepads with motion events use PlayStation-style emulation
true, // client gamepads with touchpads use PlayStation-style emulation
@@ -1571,14 +1568,17 @@ namespace config {
*
* @return Platform-supported gamepad backend names accepted by configuration.
*/
std::vector<std::string_view> &get_supported_gamepad_options() {
const auto options = platf::supported_gamepads(nullptr);
static std::vector<std::string_view> opts {};
opts.reserve(options.size());
for (auto &opt : options) {
opts.emplace_back(opt.name);
}
return opts;
const std::vector<std::string_view> &get_supported_gamepad_options() {
static const auto gamepads = platf::supported_gamepads(nullptr);
static const auto options = []() {
std::vector<std::string_view> names;
names.reserve(gamepads.size());
for (const auto &gamepad : gamepads) {
names.emplace_back(gamepad.name);
}
return names;
}();
return options;
}
/**
@@ -1828,6 +1828,7 @@ namespace config {
GAMEPAD_DRIVER_ALL,
GAMEPAD_DRIVER_VIRTUALHID,
GAMEPAD_DRIVER_VIGEMBUS,
GAMEPAD_DRIVER_NONE,
});
string_restricted_f(vars, "gamepad"s, input.gamepad, get_supported_gamepad_options());
#ifdef _WIN32
+3 -2
View File
@@ -34,8 +34,9 @@ namespace config {
constexpr int PACKETSIZE_LARGE = 1456; ///< Default large packet size that avoids common MTU fragmentation.
inline constexpr std::string_view GAMEPAD_DRIVER_ALL = "all"; ///< Allow every available Windows virtual gamepad driver.
inline constexpr std::string_view GAMEPAD_DRIVER_VIRTUALHID = "virtualhid"; ///< Allow only Virtual HID Driver for Windows gamepads.
inline constexpr std::string_view GAMEPAD_DRIVER_VIRTUALHID = "virtualhid"; ///< Use Virtual HID Broker for gamepads on Windows or macOS.
inline constexpr std::string_view GAMEPAD_DRIVER_VIGEMBUS = "vigembus"; ///< Allow only ViGEmBus for Windows gamepads.
inline constexpr std::string_view GAMEPAD_DRIVER_NONE = "none"; ///< Disable virtual gamepads on Windows and macOS.
// track modified config options
inline std::unordered_map<std::string, std::string> modified_config_settings; ///< Configuration keys changed during the current parse or UI update.
@@ -291,7 +292,7 @@ namespace config {
std::chrono::duration<double> key_repeat_period; ///< Interval between repeated keyboard key events.
std::string gamepad; ///< Virtual controller profile selected by configuration.
std::string gamepad_driver; ///< Windows virtual gamepad driver policy, or empty until the user chooses one.
std::string gamepad_driver; ///< Virtual gamepad backend policy on Windows and macOS.
bool ds4_back_as_touchpad_click; ///< Map Back/Select to touchpad click for PlayStation-style gamepads.
bool motion_as_ds4; ///< Prefer PlayStation-style emulation for client gamepads with motion controls.
bool touchpad_as_ds4; ///< Prefer PlayStation-style emulation for client gamepads with touchpad input.
+62 -9
View File
@@ -8,6 +8,7 @@
// standard includes
#include <algorithm>
#include <array>
#include <charconv>
#include <filesystem>
#include <format>
@@ -27,12 +28,17 @@
#include <Simple-Web-Server/crypto.hpp>
#include <Simple-Web-Server/server_https.hpp>
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
#include "platform/virtualhid_input.h"
#endif
#ifdef _WIN32
#include "platform/windows/misc.h"
#include "platform/windows/utf_utils.h"
#include <Windows.h>
#elif defined(__APPLE__)
#include <CoreFoundation/CoreFoundation.h>
#endif
// local includes
@@ -208,7 +214,12 @@ namespace confighttp {
*/
constexpr auto CSRF_TOKEN_LIFETIME = std::chrono::hours(1); // Tokens valid for 1 hour
constexpr std::string_view libvirtualhid_minimum_version = LIBVIRTUALHID_MINIMUM_VERSION; ///< Minimum supported libvirtualhid driver version.
#ifndef __APPLE__
constexpr std::string_view libvirtualhid_minimum_version = LIBVIRTUALHID_MINIMUM_VERSION; ///< Minimum supported Windows broker version.
#endif
#ifdef __APPLE__
constexpr std::string_view libvirtualhid_macos_minimum_version = LIBVIRTUALHID_MACOS_MINIMUM_VERSION; ///< Minimum supported macOS broker bundle version.
#endif
constexpr auto VIGEMBUS_MINIMUM_VERSION = "1.17.0.0"sv; ///< Minimum supported ViGEmBus fallback driver version. // NOSONAR(cpp:S1313): not an IP address
/**
@@ -509,6 +520,42 @@ namespace confighttp {
#endif
#ifdef __APPLE__
/**
* @brief Read the installed macOS Virtual HID Broker app version.
*
* @return Three-part bundle version, or empty when the app is unavailable.
*/
std::string read_libvirtualhid_broker_version() {
const auto bundle_url = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
CFSTR("/Applications/VirtualHIDBroker.app"),
kCFURLPOSIXPathStyle,
true
);
if (!bundle_url) {
return {};
}
const auto bundle = CFBundleCreate(kCFAllocatorDefault, bundle_url);
CFRelease(bundle_url);
if (!bundle) {
return {};
}
std::string version;
const auto value = CFBundleGetValueForInfoDictionaryKey(bundle, CFSTR("CFBundleShortVersionString"));
if (value && CFGetTypeID(value) == CFStringGetTypeID()) {
std::array<char, 64> buffer {};
if (CFStringGetCString(static_cast<CFStringRef>(value), buffer.data(), buffer.size(), kCFStringEncodingUTF8)) {
version = buffer.data();
}
}
CFRelease(bundle);
return version;
}
#endif
/**
* @brief Log the request details.
* @param request The HTTP request object.
@@ -1959,15 +2006,21 @@ namespace confighttp {
}
/**
* @brief Build libvirtualhid driver version and installation status.
* @brief Build Virtual HID Broker version and installation status.
*
* @return libvirtualhid driver status JSON.
* @return Virtual HID Broker status JSON.
*/
nlohmann::json get_virtualhid_driver_status() {
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
#ifdef _WIN32
const auto version_str = read_libvirtualhid_driver_version();
const auto minimum_version = libvirtualhid_minimum_version;
#else
const auto version_str = read_libvirtualhid_broker_version();
const auto minimum_version = libvirtualhid_macos_minimum_version;
#endif
const auto driver_detected = !version_str.empty();
auto output_tree = build_driver_status(driver_detected, version_str, libvirtualhid_minimum_version);
auto output_tree = build_driver_status(driver_detected, version_str, minimum_version);
bool requires_installed_driver = true;
std::string backend_name;
std::string runtime_error_message;
@@ -1978,7 +2031,7 @@ namespace confighttp {
const auto &capabilities = runtime->capabilities();
backend_name = capabilities.backend_name;
requires_installed_driver = capabilities.requires_installed_driver;
output_tree = build_driver_status(driver_detected || capabilities.supports_gamepad, version_str, libvirtualhid_minimum_version);
output_tree = build_driver_status(driver_detected || capabilities.supports_gamepad, version_str, minimum_version);
}
} catch (const std::bad_alloc &exception) {
runtime_error_message = exception.what();
@@ -1991,7 +2044,7 @@ namespace confighttp {
}
#else
auto output_tree = build_driver_status(false, "", libvirtualhid_minimum_version);
output_tree["error"] = "libvirtualhid driver status is only available on Windows";
output_tree["error"] = "Virtual HID Broker status is only available on Windows and macOS";
output_tree["backend_name"] = "";
output_tree["requires_installed_driver"] = false;
#endif
@@ -2112,7 +2165,7 @@ namespace confighttp {
#ifdef _WIN32
config::select_all_gamepad_drivers_if_licensed(result.license.licensed());
#endif
#if defined(_WIN32) && defined(SUNSHINE_TRAY) && SUNSHINE_TRAY >= 1
#if (defined(_WIN32) || defined(__APPLE__)) && defined(SUNSHINE_TRAY) && SUNSHINE_TRAY >= 1
system_tray::update_tray_virtualhid_license(result.license, false);
#endif
#ifdef _WIN32
+2 -2
View File
@@ -160,9 +160,9 @@ namespace confighttp {
nlohmann::json build_virtualhid_license_status(const lvh::LicenseResult &result);
/**
* @brief Build libvirtualhid driver version and installation status.
* @brief Build Virtual HID Broker version and installation status.
*
* @return libvirtualhid driver status JSON.
* @return Virtual HID Broker status JSON.
*/
nlohmann::json get_virtualhid_driver_status();
+5 -5
View File
@@ -1333,7 +1333,7 @@ namespace input {
* @param packet The controller arrival packet.
*/
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_ARRIVAL_PACKET packet) {
if (!config::input.controller) {
if (!config::input.controller || config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
return;
}
@@ -1483,7 +1483,7 @@ namespace input {
* @param packet The controller touch packet.
*/
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_TOUCH_PACKET packet) {
if (!config::input.controller) {
if (!config::input.controller || config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
return;
}
@@ -1516,7 +1516,7 @@ namespace input {
* @param packet The controller motion packet.
*/
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_MOTION_PACKET packet) {
if (!config::input.controller) {
if (!config::input.controller || config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
return;
}
@@ -1548,7 +1548,7 @@ namespace input {
* @param packet The controller battery packet.
*/
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_BATTERY_PACKET packet) {
if (!config::input.controller) {
if (!config::input.controller || config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
return;
}
@@ -1579,7 +1579,7 @@ namespace input {
* @param packet Protocol packet being processed.
*/
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
if (!config::input.controller) {
if (!config::input.controller || config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
return;
}
+3 -1
View File
@@ -501,9 +501,11 @@ int main(int argc, char *argv[]) {
if (tray_is_enabled && config::sunshine.system_tray) {
BOOST_LOG(info) << "Starting system tray"sv;
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
system_tray::prepare_tray_virtualhid_license();
system_tray::prepare_tray_virtualhid_driver();
#endif
#ifdef _WIN32
// TODO: Windows has a weird bug where when running as a service and on the first Windows boot,
// the tray icon would not appear even though Sunshine is running correctly otherwise.
// Restarting the service would allow the icon to appear normally.
+11 -2
View File
@@ -12,6 +12,9 @@
#include <utility>
#include <vector>
// lib includes
#include <libvirtualhid/license.hpp>
// local includes
#include "src/config.h"
#include "src/platform/virtualhid_input.h"
@@ -37,7 +40,7 @@ namespace platf {
}
const auto &capabilities = runtime->capabilities();
if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_controller_extensions()) {
if (config::input.gamepad_driver != config::GAMEPAD_DRIVER_NONE && capabilities.supports_gamepad && lvh::get_license_status().license.licensed() && virtualhid::configured_gamepad_supports_controller_extensions()) {
caps |= platform_caps::controller_touch;
}
if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
@@ -54,7 +57,13 @@ namespace platf {
return gamepads;
}
gamepads = virtualhid::supported_gamepads(virtualhid::get_input_context(*input).runtime.get());
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
gamepads.clear();
return gamepads;
}
const auto licensed = lvh::get_license_status().license.licensed();
gamepads = virtualhid::supported_gamepads(virtualhid::get_input_context(*input).runtime.get(), false, licensed, true);
return gamepads;
}
+5 -4
View File
@@ -542,14 +542,15 @@ namespace platf::virtualhid {
std::vector<supported_gamepad_t> supported_gamepads(
lvh::Runtime *runtime,
const bool fallback_vigem_available,
const bool virtualhid_licensed
const bool virtualhid_licensed,
const bool require_license
) {
if (!runtime) {
return static_supported_gamepads();
}
const auto &capabilities = runtime->capabilities();
const auto license_valid = !capabilities.requires_installed_driver || virtualhid_licensed;
const auto license_valid = (!capabilities.requires_installed_driver && !require_license) || virtualhid_licensed;
const auto libvirtualhid_available = capabilities.supports_gamepad && license_valid;
std::string reason;
if (!capabilities.supports_gamepad) {
@@ -582,7 +583,7 @@ namespace platf::virtualhid {
const std::string_view gamepad_driver,
const bool virtualhid_licensed
) {
return gamepad_driver != config::GAMEPAD_DRIVER_VIGEMBUS && capabilities.supports_gamepad &&
return gamepad_driver != config::GAMEPAD_DRIVER_VIGEMBUS && gamepad_driver != config::GAMEPAD_DRIVER_NONE && capabilities.supports_gamepad &&
(!capabilities.requires_installed_driver || virtualhid_licensed);
}
@@ -591,7 +592,7 @@ namespace platf::virtualhid {
const bool virtualhid_selected,
const std::string_view gamepad_driver
) {
if (gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID) {
if (gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID || gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
return false;
}
if (gamepad_driver == config::GAMEPAD_DRIVER_VIGEMBUS || !virtualhid_selected) {
+4 -2
View File
@@ -106,13 +106,15 @@ namespace platf::virtualhid {
*
* @param runtime Runtime to probe.
* @param fallback_vigem_available Whether Windows ViGEm fallback can create gamepads.
* @param virtualhid_licensed Whether an installed-driver runtime has a valid license.
* @param virtualhid_licensed Whether the broker has a valid license.
* @param require_license Whether this platform requires a broker license for gamepads.
* @return Supported gamepad choices.
*/
std::vector<supported_gamepad_t> supported_gamepads(
lvh::Runtime *runtime,
bool fallback_vigem_available = false,
bool virtualhid_licensed = true
bool virtualhid_licensed = true,
bool require_license = false
);
/**
+6 -1
View File
@@ -1274,6 +1274,11 @@ namespace platf {
}
const auto raw = (input_raw_t *) input->get();
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
gps.clear();
return gps;
}
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIGEMBUS) {
gps = vigembus_supported_gamepads(raw->vigem != nullptr);
return gps;
@@ -1310,7 +1315,7 @@ namespace platf {
platform_caps::caps_t get_capabilities() {
platform_caps::caps_t caps = 0;
if (virtualhid::configured_gamepad_supports_controller_extensions()) {
if (config::input.gamepad_driver != config::GAMEPAD_DRIVER_NONE && virtualhid::configured_gamepad_supports_controller_extensions()) {
caps |= platform_caps::controller_touch;
}
+79 -37
View File
@@ -27,7 +27,7 @@
#define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.svg"
/**
* @def TRAY_ICON_VIRTUALHID
* @brief Path to the Virtual HID Driver notification icon.
* @brief Path to the Virtual HID Broker notification icon.
*/
#define TRAY_ICON_VIRTUALHID WEB_DIR "images/logo-libvirtualhid.svg"
@@ -65,7 +65,7 @@
// lib includes
#include <boost/filesystem.hpp>
#include <tray.h>
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
#include <libvirtualhid/license.hpp>
#endif
@@ -134,9 +134,9 @@ namespace system_tray {
}
};
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
/**
* @brief Access storage for dynamic Virtual HID Driver license menu labels.
* @brief Access storage for dynamic Virtual HID Broker license menu labels.
*
* @return Persistent string storage backing the tray menu label pointers.
*/
@@ -146,7 +146,7 @@ namespace system_tray {
}
/**
* @brief Access storage for the Virtual HID Driver compatibility notification.
* @brief Access storage for the Virtual HID Broker compatibility notification.
*
* @return Persistent string storage backing the tray notification pointer.
*/
@@ -155,13 +155,14 @@ namespace system_tray {
return notification_text;
}
#ifdef _WIN32
/**
* @brief Access persistent storage for the Virtual HID Driver benefits menu.
* @brief Access persistent storage for the Virtual HID Broker benefits menu.
*
* The tray C API requires a mutable submenu pointer even though Sunshine
* treats these entries as immutable.
*
* @return Persistent Virtual HID Driver benefits menu storage.
* @return Persistent Virtual HID Broker benefits menu storage.
*/
std::array<struct tray_menu, 5> &virtualhid_benefits_menu_storage() {
static std::array<struct tray_menu, 5> benefits_menu {{
@@ -173,11 +174,12 @@ namespace system_tray {
}};
return benefits_menu;
}
#endif
#endif
} // namespace
#ifdef _WIN32
constexpr auto LIBVIRTUALHID_RELEASES_URL = "https://github.com/LizardByte/libvirtualhid/releases/latest"sv; ///< Latest Virtual HID Driver release.
#if defined(_WIN32) || defined(__APPLE__)
constexpr auto LIBVIRTUALHID_RELEASES_URL = "https://github.com/LizardByte/libvirtualhid/releases/latest"sv; ///< Latest Virtual HID Broker release.
#endif
void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item) {
@@ -197,14 +199,18 @@ namespace system_tray {
platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher");
}
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
void tray_virtualhid_license_cb([[maybe_unused]] struct tray_menu *item) {
BOOST_LOG(info) << "Opening Virtual HID Driver license settings from system tray"sv;
BOOST_LOG(info) << "Opening Virtual HID Broker license settings from system tray"sv;
#ifdef _WIN32
launch_ui(config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIGEMBUS ? "/config#gamepad_driver" : "/troubleshooting#virtualhid-license");
#else
launch_ui("/troubleshooting#virtualhid-license");
#endif
}
void tray_virtualhid_download_cb([[maybe_unused]] struct tray_menu *item) {
BOOST_LOG(info) << "Opening Virtual HID Driver download from system tray"sv;
BOOST_LOG(info) << "Opening Virtual HID Broker download from system tray"sv;
platf::open_url(std::string {LIBVIRTUALHID_RELEASES_URL});
}
#endif
@@ -261,9 +267,9 @@ namespace system_tray {
lifetime::exit_sunshine(0, true);
}
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
/**
* @brief Create the initial Virtual HID Driver license submenu.
* @brief Create the initial Virtual HID Broker license submenu.
*
* @return Menu storage with a checking state, benefits, license settings, and driver download.
*/
@@ -272,12 +278,16 @@ namespace system_tray {
menu[0] = {.text = "Status: Checking", .disabled = 1};
menu[1] = {.text = "-"};
menu[2] = {.text = "Get/Manage License", .cb = tray_virtualhid_license_cb};
menu[3] = {.text = "Virtual HID Driver Benefits", .submenu = virtualhid_benefits_menu_storage().data()};
menu[4] = {.text = "Download Virtual HID Driver", .cb = tray_virtualhid_download_cb};
#ifdef _WIN32
menu[3] = {.text = "Virtual HID Broker Benefits", .submenu = virtualhid_benefits_menu_storage().data()};
menu[4] = {.text = "Download Virtual HID Broker", .cb = tray_virtualhid_download_cb};
#else
menu[3] = {.text = "Download Virtual HID Broker", .cb = tray_virtualhid_download_cb};
#endif
return menu;
}
static auto virtualhid_license_menu = initial_virtualhid_license_menu(); ///< Virtual HID Driver license submenu.
static auto virtualhid_license_menu = initial_virtualhid_license_menu(); ///< Virtual HID Broker license submenu.
#endif
// Tray menu
@@ -289,8 +299,8 @@ namespace system_tray {
// Tray menu labels currently use the project's English source strings.
{.text = "Open Sunshine", .cb = tray_open_ui_cb},
{.text = "-"},
#ifdef _WIN32
{.text = "Virtual HID Driver", .submenu = virtualhid_license_menu.data()},
#if defined(_WIN32) || defined(__APPLE__)
{.text = "Virtual HID Broker", .submenu = virtualhid_license_menu.data()},
{.text = "-"},
#endif
{.text = "Donate",
@@ -310,7 +320,7 @@ namespace system_tray {
{.text = "Quit", .cb = tray_quit_cb},
{.text = nullptr}
},
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
.iconPathCount = 5,
.allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING, TRAY_ICON_VIRTUALHID},
#else
@@ -336,7 +346,7 @@ namespace system_tray {
tray.notification_text = nullptr;
tray.notification_title = nullptr;
tray.notification_cb = nullptr;
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
virtualhid_license_menu_text_storage() = {};
virtualhid_driver_notification_text_storage().clear();
virtualhid_license_menu = initial_virtualhid_license_menu();
@@ -344,9 +354,9 @@ namespace system_tray {
}
#endif
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
/**
* @brief Return the user-visible label for a Virtual HID Driver license state.
* @brief Return the user-visible label for a Virtual HID Broker license state.
*
* @param state License state reported by libvirtualhid.
* @return Short state label suitable for a tray menu.
@@ -398,7 +408,7 @@ namespace system_tray {
}
/**
* @brief Assign text and behavior to one Virtual HID Driver submenu item.
* @brief Assign text and behavior to one Virtual HID Broker submenu item.
*
* @tparam Callback Callback type accepted by the tray library.
* @param index Submenu index to populate.
@@ -423,7 +433,7 @@ namespace system_tray {
}
/**
* @brief Rebuild the Virtual HID Driver submenu for the latest license state.
* @brief Rebuild the Virtual HID Broker submenu for the latest license state.
*
* @param license Latest machine license details.
*/
@@ -446,13 +456,17 @@ namespace system_tray {
set_virtualhid_license_menu_item(
3,
license.activation_limit == 0 ?
"Machine activations: Not reported" :
std::format("Machine activations: {} / {}", license.activation_usage, license.activation_limit),
"Machine activation limit: Not reported" :
std::format("Machine activation limit: {}", license.activation_limit),
true
);
} else {
set_virtualhid_license_menu_item(1, std::string {virtualhid_license_state_detail(license.state)}, true);
#ifdef _WIN32
set_virtualhid_license_menu_item(2, "Driver-backed keyboard, mouse, and gamepads are locked", true);
#else
set_virtualhid_license_menu_item(2, "Virtual gamepads are locked", true);
#endif
set_virtualhid_license_menu_item(
3,
license.service_available ? "License service: Available" : "License service: Unavailable",
@@ -461,9 +475,13 @@ namespace system_tray {
}
virtualhid_license_menu[4] = {.text = "-"};
set_virtualhid_license_menu_item(5, "Get/Manage License", false, tray_virtualhid_license_cb);
set_virtualhid_license_menu_item(6, "Virtual HID Driver Benefits", false);
#ifdef _WIN32
set_virtualhid_license_menu_item(6, "Virtual HID Broker Benefits", false);
virtualhid_license_menu[6].submenu = virtualhid_benefits_menu_storage().data();
set_virtualhid_license_menu_item(7, "Download Virtual HID Driver", false, tray_virtualhid_download_cb);
set_virtualhid_license_menu_item(7, "Download Virtual HID Broker", false, tray_virtualhid_download_cb);
#else
set_virtualhid_license_menu_item(6, "Download Virtual HID Broker", false, tray_virtualhid_download_cb);
#endif
}
/**
@@ -481,16 +499,17 @@ namespace system_tray {
clear_tray_notification();
rebuild_virtualhid_license_menu(license);
if (config::input.gamepad_driver.empty()) {
tray.notification_title = "Choose a Gamepad Driver";
#ifdef _WIN32
if (config::input.controller && config::input.gamepad_driver.empty()) {
tray.notification_title = "Choose a Gamepad Backend";
tray.notification_text =
"Choose a driver in Input settings. Virtual HID Driver is a paid upgrade; ViGEmBus is limited and has reached end of life.";
"Choose a gamepad backend in Input settings. Virtual HID Broker is a paid upgrade; ViGEmBus is limited and has reached end of life.";
tray.notification_icon = tray.allIconPaths[4];
tray.notification_cb = []() {
launch_ui("/config#gamepad_driver");
};
} else if (config::input.gamepad_driver != config::GAMEPAD_DRIVER_VIGEMBUS && notify_if_unlicensed && !license.licensed()) {
tray.notification_title = "Virtual HID Driver License";
} else if (config::input.controller && config::input.gamepad_driver != config::GAMEPAD_DRIVER_NONE && config::input.gamepad_driver != config::GAMEPAD_DRIVER_VIGEMBUS && notify_if_unlicensed && !license.licensed()) {
tray.notification_title = "Virtual HID Broker License";
tray.notification_text =
"Get or manage a license, or use the limited, end-of-life ViGEmBus driver.";
tray.notification_icon = tray.allIconPaths[4];
@@ -498,6 +517,24 @@ namespace system_tray {
launch_ui("/troubleshooting#virtualhid-license");
};
}
#else
if (config::input.controller && config::input.gamepad_driver != config::GAMEPAD_DRIVER_NONE && notify_if_unlicensed && !license.licensed()) {
if (license.service_available) {
tray.notification_title = "Virtual HID Broker License";
tray.notification_text = "Activate a machine license to use virtual gamepads. Click to manage the license.";
tray.notification_cb = []() {
launch_ui("/troubleshooting#virtualhid-license");
};
} else {
tray.notification_title = "Virtual HID Broker Is Unavailable";
tray.notification_text = "Install and start Virtual HID Broker to use virtual gamepads. Click to download.";
tray.notification_cb = []() {
tray_virtualhid_download_cb(nullptr);
};
}
tray.notification_icon = tray.allIconPaths[4];
}
#endif
if (tray_initialized_state().load()) {
tray_update(&tray);
@@ -515,9 +552,14 @@ namespace system_tray {
const bool version_compatible,
const std::string_view supported_versions
) {
if (config::input.gamepad_driver.empty() || config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIGEMBUS || !installed || version_compatible) {
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_NONE || !installed || version_compatible) {
return;
}
#ifdef _WIN32
if (config::input.gamepad_driver.empty() || config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIGEMBUS) {
return;
}
#endif
const std::scoped_lock lock(tray_state_mutex());
clear_tray_notification();
@@ -525,11 +567,11 @@ namespace system_tray {
const auto displayed_version = version.empty() ? "unknown" : std::format("v{}", version);
auto &notification_text = virtualhid_driver_notification_text_storage();
notification_text = std::format(
"Installed Virtual HID Driver {} is not supported by this version of Sunshine. Supported versions: {}. Restart Sunshine after updating. Click for instructions.",
"Installed Virtual HID Broker {} is not supported by this version of Sunshine. Supported versions: {}. Restart Sunshine after updating. Click for instructions.",
displayed_version,
supported_versions
);
tray.notification_title = "Update Virtual HID Driver";
tray.notification_title = "Update Virtual HID Broker";
tray.notification_text = notification_text.c_str();
tray.notification_icon = tray.allIconPaths[4];
tray.notification_cb = []() {
+14 -14
View File
@@ -8,7 +8,7 @@
#include <string>
#include <string_view>
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
namespace lvh {
struct LicenseStatus;
}
@@ -42,15 +42,15 @@ namespace system_tray {
*/
void tray_donate_paypal_cb([[maybe_unused]] struct tray_menu *item);
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
/**
* @brief Callback for opening Virtual HID Driver license settings in the Web UI.
* @brief Callback for opening Virtual HID Broker license settings in the Web UI.
* @param item The tray menu item.
*/
void tray_virtualhid_license_cb([[maybe_unused]] struct tray_menu *item);
/**
* @brief Callback for opening the latest Virtual HID Driver release.
* @brief Callback for opening the latest Virtual HID Broker release.
* @param item The tray menu item.
*/
void tray_virtualhid_download_cb([[maybe_unused]] struct tray_menu *item);
@@ -115,28 +115,28 @@ namespace system_tray {
*/
void update_tray_require_pin();
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
/**
* @brief Update the Virtual HID Driver license submenu and optional notification.
* @brief Update the Virtual HID Broker license submenu and optional notification.
*
* @param license Latest machine license details.
* @param notify_if_unlicensed Whether to notify the user when the machine is not activated and ViGEmBus is not exclusively selected.
* @param notify_if_unlicensed Whether to notify the user when the broker needs a license.
*/
void update_tray_virtualhid_license(const lvh::LicenseStatus &license, bool notify_if_unlicensed);
/**
* @brief Query the Virtual HID Driver license and prepare the startup tray state.
* @brief Query the Virtual HID Broker license and prepare the startup tray state.
*/
void prepare_tray_virtualhid_license();
/**
* @brief Show an update notification for an unsupported Virtual HID Driver.
* @brief Show an update notification for an unsupported Virtual HID Broker.
*
* Existing notifications are preserved while the driver choice is unset, when ViGEmBus is exclusively selected,
* or when the driver is absent or supported.
* Existing notifications are preserved when gamepad input is disabled, the broker is absent or supported,
* or the Windows backend choice does not use the broker.
*
* @param installed Whether the driver is installed.
* @param version Installed driver version.
* @param installed Whether the broker is installed.
* @param version Installed broker version.
* @param version_compatible Whether Sunshine supports the installed version.
* @param supported_versions User-visible supported version range.
*/
@@ -148,7 +148,7 @@ namespace system_tray {
);
/**
* @brief Query the Virtual HID Driver version and prepare its startup notification.
* @brief Query the Virtual HID Broker version and prepare its startup notification.
*/
void prepare_tray_virtualhid_driver();
#endif
+36 -10
View File
@@ -21,7 +21,7 @@
</div>
</div>
<!-- Windows virtual input status -->
<!-- Virtual gamepad broker status -->
<div class="alert my-4" :class="virtualInputNotice.alertClass" v-if="virtualInputNotice">
<div>
<div class="d-flex align-items-center mb-3">
@@ -188,19 +188,23 @@
this.preReleaseVersion = new SunshineVersion((await fetch("https://api.github.com/repos/LizardByte/Sunshine/releases").then((r) => r.json())).find(release => release.prerelease), null);
console.log("Pre-Release Version: ", this.preReleaseVersion.version)
// The Virtual HID Driver also backs relative mouse input when controllers are disabled.
if (this.platform === 'windows') {
// Read the broker version on both platforms to identify development builds.
if (this.platform === 'windows' || this.platform === 'macos') {
try {
const virtualInputStatus = await fetch("./api/virtual-input/status").then((r) => r.json());
this.virtualhid = virtualInputStatus.virtualhid;
this.vigembus = virtualInputStatus.vigembus;
if (this.platform === 'windows') {
this.vigembus = virtualInputStatus.vigembus;
}
} catch (e) {
console.error("Failed to fetch virtual input driver status:", e);
}
}
if (this.platform === 'windows' || this.platform === 'macos') {
try {
this.virtualhidLicense = await fetch("./api/virtual-input/license").then((r) => r.json());
} catch (e) {
console.error("Failed to fetch Virtual HID Driver license status:", e);
console.error("Failed to fetch Virtual HID Broker license status:", e);
}
}
} catch (e) {
@@ -215,11 +219,18 @@
},
computed: {
/**
* Build the single Windows virtual-input message shown on the home page.
* Warnings are reserved for an unusable selected backend, an unsupported
* installed driver, or an installed driver with an invalid license.
* Build the virtual-input message shown on the home page.
* Warn about broker or gamepad driver issues when virtual gamepads are enabled.
*/
virtualInputNotice() {
if (!this.controllerEnabled || this.gamepadDriver === 'none') {
return null;
}
if (this.platform === 'macos') {
return this.buildMacosVirtualInputNotice();
}
if (this.platform !== 'windows' || !this.virtualhid || !this.vigembus) {
return null;
}
@@ -245,7 +256,7 @@
}
if (this.gamepadDriver === 'virtualhid') {
return this.buildVirtualInputNotice(true, 'index.virtualhid_required_title', [{ key: 'index.virtualhid_required_desc' }]);
return this.buildVirtualInputNotice(true, 'index.virtualhid_broker_unavailable_title', [{ key: 'index.virtualhid_required_desc' }]);
}
if (this.controllerEnabled && !vigembusUsable) {
@@ -294,7 +305,22 @@
},
methods: {
/**
* Build a home-page notice for the current Windows virtual-input state.
* Build the macOS broker notice, prioritizing license and service warnings.
*
* @returns {object|null} Warning or development-build notice, when applicable.
*/
buildMacosVirtualInputNotice() {
if (this.virtualhidLicense && !this.virtualhidLicense.licensed) {
return this.virtualhidLicense.service_available
? this.buildVirtualInputNotice(true, 'index.virtualhid_macos_license_title', [{ key: 'index.virtualhid_macos_license_desc' }])
: this.buildVirtualInputNotice(true, 'index.virtualhid_broker_unavailable_title', [{ key: 'index.virtualhid_macos_broker_desc' }]);
}
return this.virtualhid?.development_version
? this.buildVirtualInputNotice(false, 'index.virtualhid_development_title', [{ key: 'index.virtualhid_development_desc' }])
: null;
},
/**
* Build a home-page notice for the current virtual-input state.
*
* @param {boolean} warning Whether the notice represents an actionable warning.
* @param {string} title Localization key for the notice title.
@@ -2,8 +2,8 @@
<Navbar></Navbar>
<div id="content" class="container">
<h1 class="my-4">{{ $t('troubleshooting.troubleshooting') }}</h1>
<!-- Virtual input driver and license -->
<div class="card my-4 virtual-gamepad-card" v-if="platform === 'windows'">
<!-- Virtual gamepad broker and license -->
<div class="card my-4 virtual-gamepad-card" v-if="platform === 'windows' || platform === 'macos'">
<div class="card-body">
<header class="virtual-gamepad-hero">
<div class="virtual-gamepad-heading-icon" aria-hidden="true">
@@ -12,7 +12,7 @@
<div>
<h2 id="virtualhid" class="mb-1">{{ $t('troubleshooting.virtual_gamepad') }}</h2>
<p class="mb-0">{{ $t(virtualInputDescriptionKey) }}</p>
<RouterLink v-if="gamepadDriver === 'vigembus'"
<RouterLink v-if="platform === 'windows' && gamepadDriver === 'vigembus'"
class="btn btn-primary mt-3"
to="/config#gamepad_driver">
<gamepad-2 :size="18" class="icon"></gamepad-2>
@@ -62,7 +62,7 @@
</article>
</div>
<section class="virtual-gamepad-section">
<section class="virtual-gamepad-section" v-if="platform === 'windows' || platform === 'macos'">
<div class="virtual-gamepad-section-heading">
<div>
<h3 class="h4 mb-1">{{ $t('troubleshooting.virtual_gamepad_drivers') }}</h3>
@@ -160,8 +160,8 @@
<section class="virtualhid-license-section" aria-labelledby="virtualhid-license" v-if="showVirtualhid">
<div class="virtualhid-license-heading">
<div>
<h3 id="virtualhid-license" class="h4 mb-1">{{ $t('troubleshooting.virtualhid_license') }}</h3>
<p class="mb-0">{{ $t('troubleshooting.virtualhid_license_desc') }}</p>
<h3 id="virtualhid-license" class="h4 mb-1">{{ $t('troubleshooting.virtualhid_broker_license') }}</h3>
<p class="mb-0">{{ $t(platform === 'macos' ? 'troubleshooting.virtualhid_macos_license_desc' : 'troubleshooting.virtualhid_license_desc') }}</p>
</div>
<span :class="licenseStatusClass()">{{ licenseStatusText() }}</span>
</div>
@@ -172,7 +172,7 @@
</div>
<div class="alert alert-warning alert-inline" role="alert" v-else-if="!virtualhidLicense.service_available">
<alert-triangle :size="18" class="icon flex-shrink-0"></alert-triangle>
<span>{{ virtualhidLicense.message || $t('troubleshooting.virtualhid_license_unavailable') }}</span>
<span>{{ virtualhidLicense.message || $t('troubleshooting.virtualhid_broker_unavailable') }}</span>
</div>
<div class="virtualhid-license-stats">
@@ -181,8 +181,8 @@
<dd>{{ virtualhidLicense.plan_name }}</dd>
</dl>
<dl class="virtualhid-license-stat">
<dt>{{ $t('troubleshooting.virtualhid_license_activations') }}</dt>
<dd>{{ licenseActivationText() }}</dd>
<dt>{{ $t('troubleshooting.virtualhid_license_activation_limit') }}</dt>
<dd>{{ licenseActivationLimitText() }}</dd>
</dl>
<dl class="virtualhid-license-stat">
<dt>{{ $t('troubleshooting.virtualhid_license_active_devices') }}</dt>
@@ -262,7 +262,7 @@
<check-circle :size="22" aria-hidden="true"></check-circle>
<div>
<strong>{{ $t('troubleshooting.virtualhid_license_machine_activated') }}</strong>
<p class="mb-0">{{ $t('troubleshooting.virtualhid_license_machine_activated_desc') }}</p>
<p class="mb-0">{{ $t('troubleshooting.virtualhid_broker_activated_desc') }}</p>
</div>
</div>
</section>
@@ -555,18 +555,24 @@
},
computed: {
showVirtualhid() {
return this.gamepadDriver !== 'vigembus';
return this.gamepadDriver !== 'none' && (this.platform === 'macos' || this.gamepadDriver !== 'vigembus');
},
showVigembus() {
return this.gamepadDriver !== 'virtualhid';
return this.platform === 'windows' && this.gamepadDriver !== 'none' && this.gamepadDriver !== 'virtualhid';
},
showVirtualhidBenefits() {
return !(this.virtualhid.installed && this.virtualhidLicense.licensed);
return this.platform === 'windows' && this.gamepadDriver !== 'none' && !(this.virtualhid.installed && this.virtualhidLicense.licensed);
},
virtualInputDescriptionKey() {
if (this.gamepadDriver === 'none') {
return 'troubleshooting.virtual_gamepad_none_desc';
}
if (this.platform === 'macos') {
return 'troubleshooting.virtual_gamepad_macos_desc';
}
if (!this.gamepadDriver) {
return 'troubleshooting.virtual_gamepad_unset_desc';
}
@@ -709,12 +715,12 @@
.then((r) => {
this.platform = r.platform;
this.gamepadDriver = r.gamepad_driver || '';
// The Virtual HID Driver also backs relative mouse input when gamepads are disabled.
if (this.platform === 'windows') {
// The Windows broker also backs relative mouse input when gamepads are disabled.
if (this.platform === 'windows' || this.platform === 'macos') {
this.refreshDriverInformation();
if (this.showVirtualhid) {
this.refreshLicenseStatus();
}
}
if ((this.platform === 'windows' || this.platform === 'macos') && this.showVirtualhid) {
this.refreshLicenseStatus();
}
});
@@ -885,14 +891,14 @@
});
},
/**
* @brief Refresh the installed driver details and latest stable Virtual HID Driver release.
* @brief Refresh the installed backend details and latest stable Virtual HID Broker release.
*/
refreshDriverInformation() {
this.refreshVirtualInputStatus();
this.refreshDriverReleases();
},
/**
* @brief Refresh the latest stable release metadata for Virtual HID Driver.
* @brief Refresh the latest stable release metadata for Virtual HID Broker.
*/
refreshDriverReleases() {
if (this.showVirtualhid) {
@@ -1147,11 +1153,11 @@
const key = `troubleshooting.virtualhid_license_state_${this.virtualhidLicense.state}`;
return this.$t(key);
},
licenseActivationText() {
licenseActivationLimitText() {
if (!this.virtualhidLicense.activation_limit) {
return this.$t('troubleshooting.virtualhid_license_not_reported');
}
return `${this.virtualhidLicense.activation_usage} / ${this.virtualhidLicense.activation_limit}`;
return String(this.virtualhidLicense.activation_limit);
},
driverVersion(driver) {
if (!driver.installed) {
@@ -1,6 +1,5 @@
<script setup>
import { ref, watch } from 'vue'
import PlatformLayout from '../../PlatformLayout.vue'
import { computed, ref, watch } from 'vue'
import Checkbox from "../../Checkbox.vue";
import VirtualKeyCodeSelect from '../VirtualKeyCodeSelect.vue'
import { isValidVirtualKeyCode } from '../virtual_key_codes.js'
@@ -74,6 +73,11 @@ const config = ref(props.config)
const keybindingPairs = ref(parseKeybindings(config.value.keybindings))
const vigembusGamepads = new Set(['auto', 'x360', 'ds4'])
const vigembusOnly = computed(() => props.platform === 'windows' && config.value.gamepad_driver === 'vigembus')
const gamepadDriverSelection = computed({
get: () => props.platform === 'macos' && !config.value.gamepad_driver ? 'virtualhid' : config.value.gamepad_driver,
set: value => { config.value.gamepad_driver = value },
})
/**
* @brief Add an empty keybinding row.
@@ -119,62 +123,39 @@ watch(
default="true"
></Checkbox>
<!-- Windows virtual gamepad driver policy -->
<div class="mb-3" v-if="platform === 'windows'">
<!-- Virtual gamepad backend policy -->
<div class="mb-3" v-if="platform === 'windows' || platform === 'macos'">
<label for="gamepad_driver" class="form-label">{{ $t('config.gamepad_driver') }}</label>
<select id="gamepad_driver" class="form-select" v-model="config.gamepad_driver" required>
<option value="" disabled>{{ $t('config.gamepad_driver_select') }}</option>
<option value="all">{{ $t('config.gamepad_driver_all') }}</option>
<select id="gamepad_driver" class="form-select" v-model="gamepadDriverSelection" required>
<option v-if="platform === 'windows'" value="" disabled>{{ $t('config.gamepad_driver_select') }}</option>
<option v-if="platform === 'windows'" value="all">{{ $t('config.gamepad_driver_all') }}</option>
<option value="virtualhid">{{ $t('config.gamepad_driver_virtualhid') }}</option>
<option value="vigembus">{{ $t('config.gamepad_driver_vigembus') }}</option>
<option v-if="platform === 'windows'" value="vigembus">{{ $t('config.gamepad_driver_vigembus') }}</option>
<option value="none">{{ $t('config.gamepad_driver_none') }}</option>
</select>
<div class="form-text">{{ $t('config.gamepad_driver_desc') }}</div>
<div class="form-text">{{ $t(platform === 'macos' ? 'config.gamepad_driver_macos_desc' : 'config.gamepad_driver_desc') }}</div>
</div>
<!-- Emulated Gamepad Type -->
<div class="mb-3" v-if="config.controller === 'enabled' && platform !== 'macos'">
<div class="mb-3" v-if="config.controller === 'enabled' && config.gamepad_driver !== 'none'">
<label for="gamepad" class="form-label">{{ $t('config.gamepad') }}</label>
<select id="gamepad" class="form-select" v-model="config.gamepad">
<option value="auto">{{ $t('_common.auto') }}</option>
<PlatformLayout :platform="platform">
<template #freebsd>
<option value="generic">{{ $t("config.gamepad_generic") }}</option>
<option value="x360">{{ $t('config.gamepad_x360') }}</option>
<option value="xone">{{ $t("config.gamepad_xone") }}</option>
<option value="xseries">{{ $t("config.gamepad_xseries") }}</option>
<option value="ds4">{{ $t('config.gamepad_ds4') }}</option>
<option value="ds5">{{ $t("config.gamepad_ds5") }}</option>
<option value="switch">{{ $t("config.gamepad_switch") }}</option>
</template>
<template #linux>
<option value="generic">{{ $t("config.gamepad_generic") }}</option>
<option value="x360">{{ $t('config.gamepad_x360') }}</option>
<option value="xone">{{ $t("config.gamepad_xone") }}</option>
<option value="xseries">{{ $t("config.gamepad_xseries") }}</option>
<option value="ds4">{{ $t('config.gamepad_ds4') }}</option>
<option value="ds5">{{ $t("config.gamepad_ds5") }}</option>
<option value="switch">{{ $t("config.gamepad_switch") }}</option>
</template>
<template #windows>
<option v-if="config.gamepad_driver !== 'vigembus'" value="generic">{{ $t("config.gamepad_generic") }}</option>
<option value="x360">{{ $t('config.gamepad_x360') }}</option>
<option v-if="config.gamepad_driver !== 'vigembus'" value="xone">{{ $t("config.gamepad_xone") }}</option>
<option v-if="config.gamepad_driver !== 'vigembus'" value="xseries">{{ $t("config.gamepad_xseries") }}</option>
<option value="ds4">{{ $t('config.gamepad_ds4') }}</option>
<option v-if="config.gamepad_driver !== 'vigembus'" value="ds5">{{ $t("config.gamepad_ds5") }}</option>
<option v-if="config.gamepad_driver !== 'vigembus'" value="switch">{{ $t("config.gamepad_switch") }}</option>
</template>
</PlatformLayout>
<option v-if="!vigembusOnly" value="generic">{{ $t("config.gamepad_generic") }}</option>
<option value="x360">{{ $t('config.gamepad_x360') }}</option>
<option v-if="!vigembusOnly" value="xone">{{ $t("config.gamepad_xone") }}</option>
<option v-if="!vigembusOnly" value="xseries">{{ $t("config.gamepad_xseries") }}</option>
<option value="ds4">{{ $t('config.gamepad_ds4') }}</option>
<option v-if="!vigembusOnly" value="ds5">{{ $t("config.gamepad_ds5") }}</option>
<option v-if="!vigembusOnly" value="switch">{{ $t("config.gamepad_switch") }}</option>
</select>
<div class="form-text">{{ $t('config.gamepad_desc') }}</div>
</div>
<!-- Additional options based on gamepad type -->
<template v-if="config.controller === 'enabled'">
<template v-if="config.gamepad === 'ds4' || config.gamepad === 'ds5' || (config.gamepad === 'auto' && platform !== 'macos')">
<template v-if="config.controller === 'enabled' && config.gamepad_driver !== 'none'">
<template v-if="config.gamepad === 'ds4' || config.gamepad === 'ds5' || config.gamepad === 'auto'">
<div class="mb-3 accordion">
<div class="accordion-item">
<h2 class="accordion-header">
@@ -187,7 +168,7 @@ watch(
aria-labelledby="panelsStayOpen-headingOne">
<div class="accordion-body">
<!-- Automatic PlayStation-style detection options -->
<template v-if="config.gamepad === 'auto' && (platform === 'windows' || platform === 'linux')">
<template v-if="config.gamepad === 'auto'">
<!-- Gamepad with motion capability as a PlayStation-style controller -->
<Checkbox class="mb-3"
id="motion_as_ds4"
@@ -213,7 +194,7 @@ watch(
></Checkbox>
</template>
<!-- Virtual HID option: Controller MAC randomization -->
<template v-if="config.gamepad_driver !== 'vigembus' && (config.gamepad === 'ds4' || config.gamepad === 'ds5' || (config.gamepad === 'auto' && platform !== 'macos'))">
<template v-if="config.gamepad_driver !== 'vigembus' && (config.gamepad === 'ds4' || config.gamepad === 'ds5' || config.gamepad === 'auto')">
<Checkbox class="mb-3"
id="virtualhid_randomize_mac"
locale-prefix="config"
@@ -229,7 +210,7 @@ watch(
</template>
<!-- Home/Guide Button Emulation Timeout -->
<div class="mb-3" v-if="config.controller === 'enabled'">
<div class="mb-3" v-if="config.controller === 'enabled' && config.gamepad_driver !== 'none'">
<label for="back_button_timeout" class="form-label">{{ $t('config.back_button_timeout') }}</label>
<input type="text" class="form-control" id="back_button_timeout" placeholder="-1"
v-model="config.back_button_timeout" />
@@ -259,12 +259,14 @@
"gamepad": "Emulated Gamepad Type",
"gamepad_auto": "Automatic selection options",
"gamepad_desc": "Choose which type of gamepad to emulate on the host",
"gamepad_driver": "Windows Gamepad Drivers",
"gamepad_driver": "Gamepad Backend",
"gamepad_driver_all": "All Available Drivers",
"gamepad_driver_desc": "Choose which virtual gamepad drivers Sunshine may use. All Available Drivers prefers Virtual HID Driver when licensed and falls back to ViGEmBus. Virtual HID Driver is an optional paid upgrade. ViGEmBus supports only Xbox 360 and DualShock 4, has limited features, and has reached end of life.",
"gamepad_driver_select": "Choose a driver...",
"gamepad_driver_desc": "Choose which Windows gamepad backend Sunshine may use. All Available Drivers prefers Virtual HID Broker when licensed and falls back to ViGEmBus. Virtual HID Broker is an optional paid upgrade. ViGEmBus supports only Xbox 360 and DualShock 4, has limited features, and has reached end of life. None disables gamepads and broker notices without affecting keyboard or mouse input.",
"gamepad_driver_macos_desc": "Choose Virtual HID Broker for virtual gamepads on macOS, or None to disable gamepads and broker notices. Keyboard and mouse input are unaffected.",
"gamepad_driver_none": "None (disable gamepads)",
"gamepad_driver_select": "Choose a backend...",
"gamepad_driver_vigembus": "ViGEmBus",
"gamepad_driver_virtualhid": "Virtual HID Driver",
"gamepad_driver_virtualhid": "Virtual HID Broker",
"gamepad_ds4": "DS4 (PS4)",
"gamepad_ds4_manual": "PlayStation-style gamepad options",
"gamepad_ds5": "DS5 (PS5)",
@@ -476,7 +478,7 @@
"index": {
"description": "Sunshine is a self-hosted game stream host for Moonlight.",
"download": "Download",
"choose_gamepad_driver": "Choose Gamepad Drivers",
"choose_gamepad_driver": "Choose Gamepad Backend",
"fix_now": "Fix Now",
"installed_version_not_stable": "You are running a pre-release version of Sunshine. You may experience bugs or other issues. Please report any issues you encounter. Thank you for helping to make Sunshine a better software!",
"loading_latest": "Loading latest release...",
@@ -486,25 +488,28 @@
"review_virtual_input": "Review Virtual Input",
"version_dirty": "Thank you for helping to make Sunshine a better software!",
"version_latest": "You are running the latest version of Sunshine",
"virtualhid_development_desc": "Thank you for using a development build of Virtual HID Driver. Your testing helps make Virtual HID Driver better!",
"virtualhid_development_title": "Virtual HID Driver Development Build",
"gamepad_driver_choice_desc": "Choose whether Sunshine may use all available drivers, only Virtual HID Driver, or only ViGEmBus. Virtual HID Driver is an optional paid upgrade. ViGEmBus has limited features and has reached end of life.",
"gamepad_driver_choice_title": "Choose Windows Gamepad Drivers",
"virtualhid_development_desc": "Thank you for using a development build of Virtual HID Broker. Your testing helps make Virtual HID Broker better!",
"virtualhid_development_title": "Virtual HID Broker Development Build",
"gamepad_driver_choice_desc": "Choose whether Sunshine may use all available drivers, only Virtual HID Broker, or only ViGEmBus. Virtual HID Broker is an optional paid upgrade. ViGEmBus has limited features and has reached end of life.",
"gamepad_driver_choice_title": "Choose Gamepad Backend",
"vigembus_not_installed_desc": "ViGEmBus is selected, but it is not installed or running. Install ViGEmBus or choose a different gamepad driver before using gamepad input.",
"vigembus_outdated_desc": "ViGEmBus is selected, but the installed version (v{version}) is not supported. Supported versions: {supported_versions}.",
"vigembus_required_title": "ViGEmBus Is Unavailable",
"virtual_input_unavailable_desc": "Sunshine cannot find a usable virtual gamepad driver. Install and license the optional paid Virtual HID Driver, or install ViGEmBus for limited Xbox 360 and DualShock 4 support. ViGEmBus has reached end of life.",
"virtual_input_unavailable_desc": "Sunshine cannot find a usable virtual gamepad driver. Install and license the optional paid Virtual HID Broker, or install ViGEmBus for limited Xbox 360 and DualShock 4 support. ViGEmBus has reached end of life.",
"virtual_input_unavailable_title": "No Virtual Gamepad Driver Available",
"virtualhid_attention_title": "Virtual HID Driver Needs Attention",
"virtualhid_license_invalid_desc": "The installed Virtual HID Driver does not have a valid paid license, so Sunshine cannot use it. Keyboard and mouse input will fall back to SendInput. Get or manage a license, or allow the limited ViGEmBus alternative in Input configuration. ViGEmBus has reached end of life.",
"virtualhid_license_invalid_fallback_desc": "The installed Virtual HID Driver does not have a valid paid license. Sunshine will fall back to ViGEmBus for Xbox 360 and DualShock 4 gamepads and to SendInput for keyboard and mouse. ViGEmBus has limited features and has reached end of life.",
"virtualhid_optional_desc": "Virtual HID Driver is an optional paid upgrade that adds a Raw Input keyboard and mouse, more virtual gamepad types, and modern controller features. Gamepad input is currently disabled.",
"virtualhid_optional_title": "Optional Virtual HID Driver Upgrade",
"virtualhid_optional_vigembus_desc": "ViGEmBus is installed and can provide limited Xbox 360 and DualShock 4 gamepad support. Virtual HID Driver is an optional paid upgrade for more gamepad types, a Raw Input keyboard and mouse, and modern controller features. ViGEmBus has reached end of life.",
"virtualhid_attention_title": "Virtual HID Broker Needs Attention",
"virtualhid_license_invalid_desc": "The installed Virtual HID Broker does not have a valid paid license, so Sunshine cannot use it. Keyboard and mouse input will fall back to SendInput. Get or manage a license, or allow the limited ViGEmBus alternative in Input configuration. ViGEmBus has reached end of life.",
"virtualhid_license_invalid_fallback_desc": "The installed Virtual HID Broker does not have a valid paid license. Sunshine will fall back to ViGEmBus for Xbox 360 and DualShock 4 gamepads and to SendInput for keyboard and mouse. ViGEmBus has limited features and has reached end of life.",
"virtualhid_broker_unavailable_title": "Virtual HID Broker Is Unavailable",
"virtualhid_macos_broker_desc": "Install and start Virtual HID Broker to use gamepads on macOS.",
"virtualhid_macos_license_desc": "A machine license is required before Sunshine can create virtual gamepads on macOS.",
"virtualhid_macos_license_title": "Activate Virtual HID Broker",
"virtualhid_optional_desc": "Virtual HID Broker is an optional paid upgrade that adds a Raw Input keyboard and mouse, more virtual gamepad types, and modern controller features. Gamepad input is currently disabled.",
"virtualhid_optional_title": "Optional Virtual HID Broker Upgrade",
"virtualhid_optional_vigembus_desc": "ViGEmBus is installed and can provide limited Xbox 360 and DualShock 4 gamepad support. Virtual HID Broker is an optional paid upgrade for more gamepad types, a Raw Input keyboard and mouse, and modern controller features. ViGEmBus has reached end of life.",
"virtualhid_outdated_desc": "The installed libvirtualhid driver version (v{version}) is not supported. Supported versions: {supported_versions}.",
"virtualhid_outdated_title": "libvirtualhid Driver Outdated",
"virtualhid_required_desc": "Virtual HID Driver is selected, but it is not installed. Install it and activate a paid license, or choose a different gamepad driver in Input configuration.",
"virtualhid_required_title": "Virtual HID Driver Is Not Installed",
"virtualhid_required_desc": "Virtual HID Broker is selected, but it is not installed. Install it and activate a paid license, or choose a different gamepad driver in Input configuration.",
"welcome": "Hello, Sunshine!"
},
"navbar": {
@@ -585,7 +590,7 @@
"third_party_notice": "Third Party Notice"
},
"troubleshooting": {
"change_gamepad_driver": "Change Gamepad Driver",
"change_gamepad_driver": "Change Gamepad Backend",
"dd_reset": "Reset Persistent Display Device Settings",
"dd_reset_desc": "If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.",
"dd_reset_error": "Error while resetting persistence!",
@@ -635,37 +640,41 @@
"unpair_title": "Unpair Devices",
"vigembus_driver": "ViGEmBus Driver",
"virtual_gamepad": "Virtual Input",
"virtual_gamepad_desc": "Virtual HID Driver is an optional paid upgrade for a driver-backed keyboard and mouse plus full virtual gamepad support on Windows. ViGEmBus has limited Xbox 360 and DualShock 4 support and has reached end of life.",
"virtual_gamepad_drivers": "Windows Driver Status",
"virtual_gamepad_desc": "Virtual HID Broker is an optional paid upgrade for a driver-backed keyboard and mouse plus full virtual gamepad support on Windows. ViGEmBus has limited Xbox 360 and DualShock 4 support and has reached end of life.",
"virtual_gamepad_drivers": "Virtual Input Backend Status",
"virtual_gamepad_drivers_desc": "Installed versions are compared with each project's latest stable GitHub release. Prereleases are ignored.",
"virtual_gamepad_licensed_all_desc": "Virtual HID Driver is installed, licensed, and ready. Sunshine will prefer it for full virtual input support and keep ViGEmBus available as a limited fallback.",
"virtual_gamepad_licensed_desc": "Virtual HID Driver is installed, licensed, and ready for full virtual input support.",
"virtual_gamepad_unset_desc": "Choose which Windows virtual gamepad drivers Sunshine may use. Virtual HID Driver is an optional paid upgrade. ViGEmBus has limited features and has reached end of life.",
"virtual_gamepad_vigembus_desc": "Sunshine is using ViGEmBus for limited Xbox 360 and DualShock 4 emulation and SendInput for keyboard and mouse. ViGEmBus has reached end of life. Consider upgrading to Virtual HID Driver for broader, actively supported virtual input.",
"virtual_gamepad_licensed_all_desc": "Virtual HID Broker is installed, licensed, and ready. Sunshine will prefer it for full virtual input support and keep ViGEmBus available as a limited fallback.",
"virtual_gamepad_licensed_desc": "Virtual HID Broker is installed, licensed, and ready for full virtual input support.",
"virtual_gamepad_macos_desc": "Virtual HID Broker creates Xbox, PlayStation, Nintendo Switch Pro, and Generic gamepads on macOS. Install the separate broker and activate its machine license to use controller input.",
"virtual_gamepad_none_desc": "Virtual gamepad emulation is disabled in Input configuration.",
"virtual_gamepad_unset_desc": "Choose which Windows virtual gamepad drivers Sunshine may use. Virtual HID Broker is an optional paid upgrade. ViGEmBus has limited features and has reached end of life.",
"virtual_gamepad_vigembus_desc": "Sunshine is using ViGEmBus for limited Xbox 360 and DualShock 4 emulation and SendInput for keyboard and mouse. ViGEmBus has reached end of life. Consider upgrading to Virtual HID Broker for broader, actively supported virtual input.",
"virtualhid_benefit_features": "Supports controller-specific features such as motion, touchpads, LEDs, and adaptive triggers when available.",
"virtualhid_benefit_features_title": "Modern Controller Features",
"virtualhid_benefit_gamepads": "Creates Xbox One, Xbox Series, DualSense, Nintendo Switch Pro, and Generic gamepads in addition to Xbox 360 and DualShock 4.",
"virtualhid_benefit_gamepads_title": "More Gamepad Types",
"virtualhid_benefit_mouse": "Exposes normal key transitions and relative mouse input through real HID devices so games using Raw Input can receive them.",
"virtualhid_benefit_mouse_title": "Raw Input Keyboard and Mouse",
"virtualhid_benefit_maintained": "Virtual HID Driver is actively developed and supported by the LizardByte team. ViGEmBus has reached end of life.",
"virtualhid_benefit_maintained": "Virtual HID Broker is actively developed and supported by the LizardByte team. ViGEmBus has reached end of life.",
"virtualhid_benefit_maintained_title": "Actively Supported",
"virtualhid_benefits_title": "Virtual HID Driver Benefits",
"virtualhid_download": "Download Virtual HID Driver",
"virtualhid_driver": "libvirtualhid Driver",
"virtualhid_license": "Virtual HID Driver License",
"virtualhid_benefits_title": "Virtual HID Broker Benefits",
"virtualhid_broker_activated_desc": "Sunshine can create virtual gamepads while Virtual HID Broker is running and this license remains valid.",
"virtualhid_broker_download": "Download Virtual HID Broker",
"virtualhid_broker_license": "Virtual HID Broker License",
"virtualhid_broker_unavailable": "Virtual HID Broker is unavailable. Verify that libvirtualhid is installed and its broker service is running.",
"virtualhid_driver": "Virtual HID Broker",
"virtualhid_license_activate": "Activate License",
"virtualhid_license_activations": "Machine Activations",
"virtualhid_license_activation_limit": "Machine Activation Limit",
"virtualhid_license_active_devices": "Active Virtual HID Devices",
"virtualhid_license_buy": "Buy License",
"virtualhid_license_customer": "Customer",
"virtualhid_license_deactivate": "Deactivate This Machine",
"virtualhid_license_desc": "Virtual HID Driver is optional paid software. An active license is required before Sunshine can create its gamepads or Raw Input keyboard and mouse. Without a valid license, Sunshine uses SendInput for keyboard and mouse. To use the limited ViGEmBus alternative instead, change Windows Gamepad Drivers in Input configuration. ViGEmBus has reached end of life. Activation is handled by the local libvirtualhid service; Sunshine does not save your license key.",
"virtualhid_license_desc": "Virtual HID Broker is optional paid software. An active license is required before Sunshine can create its gamepads or Raw Input keyboard and mouse. Without a valid license, Sunshine uses SendInput for keyboard and mouse. To use the limited ViGEmBus alternative instead, change Gamepad Backend in Input configuration. ViGEmBus has reached end of life. Activation is handled by the local libvirtualhid service; Sunshine does not save your license key.",
"virtualhid_license_key": "License Key",
"virtualhid_license_key_desc": "The key is sent only to the local libvirtualhid service and is cleared from this page after successful activation.",
"virtualhid_license_key_placeholder": "Enter your product key",
"virtualhid_license_machine_activated": "This machine is activated",
"virtualhid_license_machine_activated_desc": "Sunshine can create Virtual HID Driver gamepads and a Raw Input keyboard and mouse while this license remains valid.",
"virtualhid_license_manage": "Manage Account and Activations",
"virtualhid_license_not_reported": "Not reported",
"virtualhid_license_plan": "Plan",
@@ -678,7 +687,7 @@
"virtualhid_license_state_licensed": "Licensed",
"virtualhid_license_state_unavailable": "Unavailable",
"virtualhid_license_state_unlicensed": "Unlicensed",
"virtualhid_license_unavailable": "The local libvirtualhid license service is unavailable. Verify that the driver package and broker service are installed and running."
"virtualhid_macos_license_desc": "An active machine license is required for virtual gamepads on macOS. The broker must be installed, running, and allowed under System Settings > Privacy & Security > Device Control and Data Access. Sunshine sends license actions to the local broker and does not save your key."
},
"featured": {
"category_filter": "Category filter",
+1 -1
View File
@@ -120,7 +120,7 @@ if(SUNSHINE_ENABLE_TRAY AND (WIN32 OR APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Linux
"${SUNSHINE_TRAY_TEST_ICON_DESTINATION_DIR}/logo-sunshine.svg"
COPYONLY)
if(WIN32)
if(WIN32 OR APPLE)
configure_file(
"${CMAKE_SOURCE_DIR}/third-party/libvirtualhid/libvirtualhid.svg"
"${SUNSHINE_TRAY_TEST_ICON_DESTINATION_DIR}/logo-libvirtualhid.svg"
+7 -7
View File
@@ -246,7 +246,7 @@ TEST_F(AVAudioTest, AudioConverterComplexInputProc) {
}
}
AudioConverterInputData inputInfo = {0};
AudioConverterInputData inputInfo = {};
inputInfo.inputData = testData;
inputInfo.inputFrames = frameCount;
inputInfo.framesProvided = 0;
@@ -255,7 +255,7 @@ TEST_F(AVAudioTest, AudioConverterComplexInputProc) {
// Test the method
UInt32 requestedPackets = 128;
AudioBufferList bufferList = {0};
AudioBufferList bufferList = {};
// Use a dummy AudioConverterRef (can be null for our test since our implementation doesn't use it)
AudioConverterRef dummyConverter = nullptr;
OSStatus result = platf::audioConverterComplexInputProc(dummyConverter, &requestedPackets, &bufferList, nullptr, &inputInfo);
@@ -284,7 +284,7 @@ TEST_F(AVAudioTest, AudioConverterInputProcNoMoreData) {
UInt32 channels = 2;
float *testData = (float *) calloc(frameCount * channels, sizeof(float));
AudioConverterInputData inputInfo = {0};
AudioConverterInputData inputInfo = {};
inputInfo.inputData = testData;
inputInfo.inputFrames = frameCount;
inputInfo.framesProvided = frameCount; // Already provided all frames
@@ -292,7 +292,7 @@ TEST_F(AVAudioTest, AudioConverterInputProcNoMoreData) {
inputInfo.avAudio = avAudio;
UInt32 requestedPackets = 128;
AudioBufferList bufferList = {0};
AudioBufferList bufferList = {};
// Use a dummy AudioConverterRef (can be null for our test since our implementation doesn't use it)
AudioConverterRef dummyConverter = nullptr;
OSStatus result = platf::audioConverterComplexInputProc(dummyConverter, &requestedPackets, &bufferList, nullptr, &inputInfo);
@@ -404,7 +404,7 @@ TEST_P(ProcessSystemAudioIOProcTest, ProcessAudioInput) {
[avAudio initializeAudioBuffer:params.channels];
// Create timestamps
AudioTimeStamp timeStamp = {0};
AudioTimeStamp timeStamp = {};
timeStamp.mFlags = kAudioTimeStampSampleTimeValid;
timeStamp.mSampleTime = 0;
@@ -438,7 +438,7 @@ TEST_P(ProcessSystemAudioIOProcTest, ProcessAudioInput) {
TPCircularBufferTail(&avAudio->audioSampleBuffer, &initialAvailableBytes);
// Create IOProc data structure for the C++ function
AVAudioIOProcData procData = {0};
AVAudioIOProcData procData = {};
procData.avAudio = avAudio;
procData.clientRequestedChannels = params.channels;
procData.clientRequestedFrameSize = params.frameCount;
@@ -448,7 +448,7 @@ TEST_P(ProcessSystemAudioIOProcTest, ProcessAudioInput) {
procData.audioConverter = nullptr; // No conversion needed for most tests
// Create a dummy output buffer (not used in our implementation but required by signature)
AudioBufferList dummyOutputBufferList = {0};
AudioBufferList dummyOutputBufferList = {};
// Test the systemAudioIOProcWrapper function
OSStatus result = platf::systemAudioIOProc(0, // device ID (not used in our logic)
+73 -1
View File
@@ -22,6 +22,10 @@
#include "src/globals.h"
#include "src/platform/virtualhid_input.h"
#if defined(_WIN32) || defined(__APPLE__)
#include <libvirtualhid/license.hpp>
#endif
using namespace std::chrono_literals;
using namespace std::literals;
@@ -236,6 +240,7 @@ TEST_F(VirtualHidDeviceTest, SelectsVirtualHidGamepadRuntimeByBackendLicenseAndP
EXPECT_TRUE(platf::virtualhid::should_use_gamepad_runtime(capabilities, config::GAMEPAD_DRIVER_ALL, true));
EXPECT_TRUE(platf::virtualhid::should_use_gamepad_runtime(capabilities, config::GAMEPAD_DRIVER_VIRTUALHID, true));
EXPECT_FALSE(platf::virtualhid::should_use_gamepad_runtime(capabilities, config::GAMEPAD_DRIVER_VIGEMBUS, true));
EXPECT_FALSE(platf::virtualhid::should_use_gamepad_runtime(capabilities, config::GAMEPAD_DRIVER_NONE, true));
capabilities.supports_gamepad = false;
EXPECT_FALSE(platf::virtualhid::should_use_gamepad_runtime(capabilities, config::GAMEPAD_DRIVER_ALL, true));
@@ -251,6 +256,7 @@ TEST_F(VirtualHidDeviceTest, SelectsVigembusFallbackByBackendAndConfiguredProfil
EXPECT_TRUE(platf::virtualhid::should_try_vigembus_fallback("xseries", false, ""));
EXPECT_TRUE(platf::virtualhid::should_try_vigembus_fallback("xseries", true, config::GAMEPAD_DRIVER_VIGEMBUS));
EXPECT_FALSE(platf::virtualhid::should_try_vigembus_fallback("auto", false, config::GAMEPAD_DRIVER_VIRTUALHID));
EXPECT_FALSE(platf::virtualhid::should_try_vigembus_fallback("auto", false, config::GAMEPAD_DRIVER_NONE));
}
TEST_F(VirtualHidDeviceTest, ReportsStaticAndRuntimeGamepadChoices) {
@@ -271,10 +277,65 @@ TEST_F(VirtualHidDeviceTest, ReportsStaticAndRuntimeGamepadChoices) {
EXPECT_TRUE(gamepad.reason_disabled.empty()) << gamepad.name;
}
const auto licensed_broker_gamepads = platf::virtualhid::supported_gamepads(context()->runtime.get(), false, true, true);
for (const auto &gamepad : licensed_broker_gamepads) {
EXPECT_TRUE(gamepad.is_enabled) << gamepad.name;
}
const auto unlicensed_gamepads = platf::virtualhid::supported_gamepads(context()->runtime.get(), false, false, true);
ASSERT_EQ(unlicensed_gamepads.size(), expected_names.size());
for (const auto &gamepad : unlicensed_gamepads) {
EXPECT_FALSE(gamepad.is_enabled) << gamepad.name;
EXPECT_EQ(gamepad.reason_disabled, "gamepads.virtualhid-license-invalid") << gamepad.name;
}
const auto no_runtime_gamepads = platf::virtualhid::supported_gamepads(nullptr, true);
EXPECT_EQ(no_runtime_gamepads.size(), expected_names.size());
}
#if defined(_WIN32) || defined(__APPLE__)
TEST_F(VirtualHidDeviceTest, NoneSelectionHidesAvailableGamepads) {
config::input.gamepad_driver = config::GAMEPAD_DRIVER_NONE;
auto platform_input = platf::input();
ASSERT_TRUE(platform_input);
EXPECT_TRUE(platf::supported_gamepads(std::addressof(platform_input)).empty());
EXPECT_EQ(platf::get_capabilities() & platf::platform_caps::controller_touch, 0U);
}
TEST_F(VirtualHidDeviceTest, BrokerCreatesAndUpdatesEveryGamepadProfileWhenLicensed) {
if (!lvh::get_license_status().license.licensed()) {
GTEST_SKIP() << "Virtual HID Broker is not activated";
}
config::input.gamepad_driver = config::GAMEPAD_DRIVER_VIRTUALHID;
auto platform_input = platf::input();
ASSERT_TRUE(platform_input);
const auto &context = platf::virtualhid::get_input_context(platform_input);
if (!context.runtime || !context.runtime->capabilities().supports_gamepad) {
GTEST_SKIP() << "Virtual HID Broker is not installed";
}
const auto &available = platf::supported_gamepads(std::addressof(platform_input));
for (const auto &gamepad : available) {
EXPECT_TRUE(gamepad.is_enabled) << gamepad.name;
}
constexpr std::array profiles {"generic"sv, "x360"sv, "xone"sv, "xseries"sv, "ds4"sv, "ds5"sv, "switch"sv};
const platf::gamepad_id_t id {0, 0};
const platf::gamepad_arrival_t metadata {LI_CTYPE_UNKNOWN, 0, 0};
for (const auto profile : profiles) {
config::input.gamepad = profile;
ASSERT_EQ(platf::alloc_gamepad(platform_input, id, metadata, nullptr), 0) << profile;
platf::gamepad_update(platform_input, 0, {platf::A, 255, 0, 0, 0, 0, 0});
const auto *adapter = platf::virtualhid::gamepad_adapter_for_testing(platf::virtualhid::get_input_context(platform_input), 0);
ASSERT_NE(adapter, nullptr);
EXPECT_TRUE(adapter->state().buttons.test(lvh::GamepadButton::a)) << profile;
platf::free_gamepad(platform_input, 0);
}
}
#endif
TEST_F(VirtualHidDeviceTest, RejectsUnavailableAndInvalidGamepadSlots) {
const platf::gamepad_arrival_t metadata {LI_CTYPE_XBOX, 0, 0};
const platf::gamepad_id_t valid_id {0, 0};
@@ -912,7 +973,13 @@ TEST_F(VirtualHidDeviceTest, PlatformWrappersForwardToVirtualHidContext) {
const auto &supported = platf::supported_gamepads(std::addressof(platform_input));
ASSERT_FALSE(supported.empty());
#ifdef __APPLE__
const auto licensed = lvh::get_license_status().license.licensed();
EXPECT_EQ(supported.front().is_enabled, licensed);
EXPECT_EQ(supported.front().reason_disabled, licensed ? "" : "gamepads.virtualhid-license-invalid");
#else
EXPECT_TRUE(supported.front().is_enabled);
#endif
EXPECT_FALSE(platf::supported_gamepads(nullptr).empty());
#ifdef _WIN32
config::input.gamepad_driver = config::GAMEPAD_DRIVER_VIGEMBUS;
@@ -924,7 +991,12 @@ TEST_F(VirtualHidDeviceTest, PlatformWrappersForwardToVirtualHidContext) {
EXPECT_EQ(vigembus_gamepads[2].name, "ds4");
#endif
#ifdef __APPLE__
EXPECT_EQ(platf::get_capabilities() & platf::platform_caps::controller_touch, 0U);
const auto controller_touch = platf::get_capabilities() & platf::platform_caps::controller_touch;
if (lvh::get_license_status().license.licensed()) {
EXPECT_NE(controller_touch, 0U);
} else {
EXPECT_EQ(controller_touch, 0U);
}
#else
EXPECT_NE(platf::get_capabilities() & platf::platform_caps::controller_touch, 0U);
#endif
+25
View File
@@ -7,6 +7,7 @@
#include "../tests_common.h"
// standard includes
#include <array>
#include <filesystem>
#include <string>
#include <utility>
@@ -17,6 +18,10 @@
using namespace std::literals;
TEST(ConfigDefaultsTest, UsesAutomaticGamepadSelection) {
EXPECT_EQ(config::input.gamepad, "auto");
}
using NvencPresetNameParam = std::pair<int, std::string_view>;
/**
@@ -57,6 +62,8 @@ namespace {
void SetUp() override {
original_config_file_ = config::sunshine.config_file;
original_gamepad_driver_ = config::input.gamepad_driver;
original_gamepad_ = config::input.gamepad;
original_apps_file_ = config::stream.file_apps;
config_file_ = std::filesystem::temp_directory_path() / "sunshine_test_config_persistence.conf"; // NOSONAR(cpp:S5443): safe for tests
std::filesystem::remove(config_file_);
config::sunshine.config_file = config_file_.string();
@@ -69,6 +76,9 @@ namespace {
void TearDown() override {
config::sunshine.config_file = std::move(original_config_file_);
config::input.gamepad_driver = std::move(original_gamepad_driver_);
config::input.gamepad = std::move(original_gamepad_);
config::stream.file_apps = std::move(original_apps_file_);
std::filesystem::remove(config_file_.parent_path() / "sunshine_test_config_apps.json");
std::filesystem::remove(config_file_);
}
@@ -85,6 +95,8 @@ namespace {
std::filesystem::path config_file_; ///< Temporary configuration file used by the test.
std::string original_config_file_; ///< Active configuration path restored after each test.
std::string original_gamepad_driver_; ///< Gamepad driver preference restored after each test.
std::string original_gamepad_; ///< Gamepad profile restored after each test.
std::string original_apps_file_; ///< Applications file path restored after each test.
};
} // namespace
@@ -124,3 +136,16 @@ TEST_F(ConfigPersistenceTest, SelectsAllDriversOnlyForLicensedUsersWithoutAPrefe
EXPECT_FALSE(config::select_all_gamepad_drivers_if_licensed(true));
EXPECT_EQ(file_handler::read_file(config_file().string().c_str()), "gamepad_driver = all\n");
}
TEST_F(ConfigPersistenceTest, AcceptsAllGamepadProfilesAcrossRepeatedConfigurationChanges) {
const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
config::stream.file_apps = apps_file.string();
constexpr std::array profiles {"generic"sv, "x360"sv, "xone"sv, "xseries"sv, "ds4"sv, "ds5"sv, "switch"sv};
for (const auto profile : profiles) {
config::input.gamepad = "auto";
config::apply_config_for_test(std::string {"gamepad = "} + std::string {profile} + "\n");
EXPECT_EQ(config::input.gamepad, profile);
}
}
+11 -2
View File
@@ -1636,8 +1636,17 @@ TEST(ConfigHttpDriverStatusTest, BuildsLiveVirtualInputDriverStatus) {
EXPECT_TRUE(virtualhid.contains("development_version"));
EXPECT_TRUE(virtualhid.contains("backend_name"));
EXPECT_TRUE(virtualhid.contains("requires_installed_driver"));
#ifdef __APPLE__
EXPECT_EQ(virtualhid["minimum_version"].get<std::string>(), LIBVIRTUALHID_MACOS_MINIMUM_VERSION);
EXPECT_EQ(virtualhid["supported_versions"].get<std::string>(), std::format(">= {}", LIBVIRTUALHID_MACOS_MINIMUM_VERSION));
if (std::filesystem::exists("/Applications/VirtualHIDBroker.app/Contents/Info.plist")) {
EXPECT_TRUE(virtualhid["installed"].get<bool>());
EXPECT_FALSE(virtualhid["version"].get<std::string>().empty());
}
#else
EXPECT_EQ(virtualhid["minimum_version"].get<std::string>(), LIBVIRTUALHID_MINIMUM_VERSION);
EXPECT_EQ(virtualhid["supported_versions"].get<std::string>(), std::format(">= {}", LIBVIRTUALHID_MINIMUM_VERSION));
#endif
const auto vigembus = confighttp::get_vigembus_driver_status();
EXPECT_TRUE(vigembus.contains("installed"));
@@ -1709,7 +1718,7 @@ TEST(ConfigHttpLicenseStatusTest, BuildVirtualHidLicenseStatus_IncludesExpectedF
license.state = lvh::LicenseState::licensed;
license.active_devices = 2;
license.activation_limit = 5;
license.activation_usage = 3;
license.activation_usage = 1;
license.plan_name = "Yearly";
license.customer_email = "customer@example.com";
license.message = "License is active";
@@ -1721,7 +1730,7 @@ TEST(ConfigHttpLicenseStatusTest, BuildVirtualHidLicenseStatus_IncludesExpectedF
EXPECT_TRUE(output["service_available"].get<bool>());
EXPECT_EQ(output["active_devices"].get<unsigned int>(), 2U);
EXPECT_EQ(output["activation_limit"].get<unsigned int>(), 5U);
EXPECT_EQ(output["activation_usage"].get<unsigned int>(), 3U);
EXPECT_EQ(output["activation_usage"].get<unsigned int>(), 1U);
EXPECT_EQ(output["plan_name"].get<std::string>(), "Yearly");
EXPECT_EQ(output["customer_email"].get<std::string>(), "customer@example.com");
EXPECT_FALSE(output.contains("expires_at"));
+167 -19
View File
@@ -22,7 +22,7 @@
// lib includes
#include <tray.h>
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
#include <libvirtualhid/license.hpp>
#endif
@@ -79,7 +79,7 @@ namespace {
#ifdef _WIN32
/**
* @brief Verify the persistent Virtual HID Driver benefits submenu.
* @brief Verify the persistent Virtual HID Broker benefits submenu.
*
* @param benefits_menu Benefits submenu to verify.
*/
@@ -97,7 +97,7 @@ namespace {
}
/**
* @brief Verify the shared action entries in a populated Virtual HID Driver menu.
* @brief Verify the shared action entries in a populated Virtual HID Broker menu.
*
* @param license_menu License submenu to verify.
*/
@@ -106,10 +106,10 @@ namespace {
EXPECT_STREQ(license_menu[4].text, "-");
EXPECT_STREQ(license_menu[5].text, "Get/Manage License");
EXPECT_NE(license_menu[5].cb, nullptr);
EXPECT_STREQ(license_menu[6].text, "Virtual HID Driver Benefits");
EXPECT_STREQ(license_menu[6].text, "Virtual HID Broker Benefits");
EXPECT_EQ(license_menu[6].cb, nullptr);
verify_virtualhid_benefits_menu(license_menu[6].submenu);
EXPECT_STREQ(license_menu[7].text, "Download Virtual HID Driver");
EXPECT_STREQ(license_menu[7].text, "Download Virtual HID Broker");
EXPECT_NE(license_menu[7].cb, nullptr);
EXPECT_EQ(license_menu[8].text, nullptr);
}
@@ -127,20 +127,26 @@ namespace {
EXPECT_STREQ(tray_data.menu[1].text, "-");
EXPECT_EQ(tray_data.menu[1].cb, nullptr);
#ifdef _WIN32
EXPECT_STREQ(tray_data.menu[2].text, "Virtual HID Driver");
#if defined(_WIN32) || defined(__APPLE__)
EXPECT_STREQ(tray_data.menu[2].text, "Virtual HID Broker");
ASSERT_NE(tray_data.menu[2].submenu, nullptr);
EXPECT_STREQ(tray_data.menu[2].submenu[0].text, "Status: Checking");
EXPECT_EQ(tray_data.menu[2].submenu[0].disabled, 1);
EXPECT_STREQ(tray_data.menu[2].submenu[1].text, "-");
EXPECT_STREQ(tray_data.menu[2].submenu[2].text, "Get/Manage License");
EXPECT_NE(tray_data.menu[2].submenu[2].cb, nullptr);
EXPECT_STREQ(tray_data.menu[2].submenu[3].text, "Virtual HID Driver Benefits");
#ifdef _WIN32
EXPECT_STREQ(tray_data.menu[2].submenu[3].text, "Virtual HID Broker Benefits");
EXPECT_EQ(tray_data.menu[2].submenu[3].cb, nullptr);
verify_virtualhid_benefits_menu(tray_data.menu[2].submenu[3].submenu);
EXPECT_STREQ(tray_data.menu[2].submenu[4].text, "Download Virtual HID Driver");
EXPECT_STREQ(tray_data.menu[2].submenu[4].text, "Download Virtual HID Broker");
EXPECT_NE(tray_data.menu[2].submenu[4].cb, nullptr);
EXPECT_EQ(tray_data.menu[2].submenu[5].text, nullptr);
#else
EXPECT_STREQ(tray_data.menu[2].submenu[3].text, "Download Virtual HID Broker");
EXPECT_NE(tray_data.menu[2].submenu[3].cb, nullptr);
EXPECT_EQ(tray_data.menu[2].submenu[4].text, nullptr);
#endif
EXPECT_STREQ(tray_data.menu[3].text, "-");
EXPECT_STREQ(tray_data.menu[4].text, "Donate");
ASSERT_NE(tray_data.menu[4].submenu, nullptr);
@@ -149,6 +155,7 @@ namespace {
EXPECT_STREQ(tray_data.menu[4].submenu[2].text, "PayPal");
EXPECT_EQ(tray_data.menu[4].submenu[3].text, nullptr);
EXPECT_STREQ(tray_data.menu[5].text, "-");
#ifdef _WIN32
EXPECT_STREQ(tray_data.menu[6].text, "Reset Display Device Config");
EXPECT_NE(tray_data.menu[6].cb, nullptr);
EXPECT_STREQ(tray_data.menu[7].text, "Restart");
@@ -156,6 +163,13 @@ namespace {
EXPECT_STREQ(tray_data.menu[8].text, "Quit");
EXPECT_NE(tray_data.menu[8].cb, nullptr);
EXPECT_EQ(tray_data.menu[9].text, nullptr);
#else
EXPECT_STREQ(tray_data.menu[6].text, "Restart");
EXPECT_NE(tray_data.menu[6].cb, nullptr);
EXPECT_STREQ(tray_data.menu[7].text, "Quit");
EXPECT_NE(tray_data.menu[7].cb, nullptr);
EXPECT_EQ(tray_data.menu[8].text, nullptr);
#endif
#else
EXPECT_STREQ(tray_data.menu[2].text, "Donate");
ASSERT_NE(tray_data.menu[2].submenu, nullptr);
@@ -243,6 +257,11 @@ protected:
#ifdef _WIN32
original_gamepad_driver_ = config::input.gamepad_driver;
config::input.gamepad_driver = config::GAMEPAD_DRIVER_ALL;
#elif defined(__APPLE__)
original_controller_ = config::input.controller;
original_gamepad_driver_ = config::input.gamepad_driver;
config::input.controller = true;
config::input.gamepad_driver = config::GAMEPAD_DRIVER_VIRTUALHID;
#endif
EXPECT_EQ(system_tray::end_tray(), 0);
#ifndef _WIN32
@@ -270,6 +289,9 @@ protected:
system_tray::reset_tray_data_for_testing();
#ifdef _WIN32
config::input.gamepad_driver = std::move(original_gamepad_driver_);
#elif defined(__APPLE__)
config::input.controller = original_controller_;
config::input.gamepad_driver = std::move(original_gamepad_driver_);
#endif
}
@@ -291,6 +313,9 @@ protected:
private:
#ifdef _WIN32
std::string original_gamepad_driver_; ///< Original gamepad driver preference restored after each test.
#elif defined(__APPLE__)
bool original_controller_ = false; ///< Original gamepad input setting restored after each test.
std::string original_gamepad_driver_; ///< Original gamepad backend setting restored after each test.
#endif
};
@@ -345,7 +370,7 @@ TEST_F(SystemTrayTest, PreparesLicensedVirtualHidMenuBeforeInitialization) {
license.state = lvh::LicenseState::licensed;
license.plan_name = "Yearly";
license.customer_email = "customer@example.com";
license.activation_usage = 2;
license.activation_usage = 1;
license.activation_limit = 5;
system_tray::update_tray_virtualhid_license(license, true);
@@ -356,7 +381,7 @@ TEST_F(SystemTrayTest, PreparesLicensedVirtualHidMenuBeforeInitialization) {
EXPECT_STREQ(license_menu[0].text, "Status: Licensed");
EXPECT_STREQ(license_menu[1].text, "Plan: Yearly");
EXPECT_STREQ(license_menu[2].text, "Customer: customer@example.com");
EXPECT_STREQ(license_menu[3].text, "Machine activations: 2 / 5");
EXPECT_STREQ(license_menu[3].text, "Machine activation limit: 5");
EXPECT_STREQ(license_menu[4].text, "-");
EXPECT_STREQ(license_menu[5].text, "Get/Manage License");
EXPECT_NE(license_menu[5].cb, nullptr);
@@ -373,7 +398,7 @@ TEST_F(SystemTrayTest, PreparesLicensedVirtualHidMenuBeforeInitialization) {
EXPECT_STREQ(license_menu[1].text, "This machine is activated");
EXPECT_STREQ(license_menu[2].text, "Customer: Not reported");
EXPECT_STREQ(license_menu[3].text, "Machine activations: Not reported");
EXPECT_STREQ(license_menu[3].text, "Machine activation limit: Not reported");
}
TEST_F(SystemTrayTest, PromptsForUnsetGamepadDriverEvenWhenLicensed) {
@@ -385,10 +410,10 @@ TEST_F(SystemTrayTest, PromptsForUnsetGamepadDriverEvenWhenLicensed) {
system_tray::update_tray_virtualhid_license(license, false);
const auto &tray_data = system_tray::tray_data_for_testing();
EXPECT_STREQ(tray_data.notification_title, "Choose a Gamepad Driver");
EXPECT_STREQ(tray_data.notification_title, "Choose a Gamepad Backend");
EXPECT_STREQ(
tray_data.notification_text,
"Choose a driver in Input settings. Virtual HID Driver is a paid upgrade; ViGEmBus is limited and has reached end of life."
"Choose a gamepad backend in Input settings. Virtual HID Broker is a paid upgrade; ViGEmBus is limited and has reached end of life."
);
EXPECT_STREQ(tray_data.notification_icon, tray_data.allIconPaths[4]);
EXPECT_NE(tray_data.notification_cb, nullptr);
@@ -400,10 +425,10 @@ TEST_F(SystemTrayTest, NotifiesWhenVirtualHidDriverIsUnsupported) {
const auto &tray_data = system_tray::tray_data_for_testing();
const auto expected_notification = std::format(
"Installed Virtual HID Driver v2026.829.2338.54 is not supported by this version of Sunshine. Supported versions: {}. Restart Sunshine after updating. Click for instructions.",
"Installed Virtual HID Broker v2026.829.2338.54 is not supported by this version of Sunshine. Supported versions: {}. Restart Sunshine after updating. Click for instructions.",
supported_versions
);
EXPECT_STREQ(tray_data.notification_title, "Update Virtual HID Driver");
EXPECT_STREQ(tray_data.notification_title, "Update Virtual HID Broker");
EXPECT_STREQ(tray_data.notification_text, expected_notification.c_str());
EXPECT_STREQ(tray_data.notification_icon, tray_data.allIconPaths[4]);
EXPECT_NE(tray_data.notification_cb, nullptr);
@@ -455,8 +480,27 @@ TEST_F(SystemTrayTest, VigembusSelectionSuppressesVirtualHidNotifications) {
EXPECT_EQ(tray_data.notification_cb, nullptr);
}
TEST_F(SystemTrayTest, NoneSelectionSuppressesWindowsVirtualHidNotifications) {
lvh::LicenseStatus license;
license.service_available = false;
license.state = lvh::LicenseState::unavailable;
config::input.gamepad_driver = config::GAMEPAD_DRIVER_NONE;
system_tray::update_tray_virtualhid_license(license, true);
const auto &tray_data = system_tray::tray_data_for_testing();
EXPECT_EQ(tray_data.notification_title, nullptr);
EXPECT_EQ(tray_data.notification_text, nullptr);
EXPECT_EQ(tray_data.notification_cb, nullptr);
EXPECT_STREQ(tray_data.menu[2].submenu[0].text, "Status: Unavailable");
const auto supported_versions = std::format(">= {}", LIBVIRTUALHID_MINIMUM_VERSION);
system_tray::update_tray_virtualhid_driver(true, "2026.829.2338.54", false, supported_versions); // NOSONAR(cpp:S1313): not IP addresses
EXPECT_EQ(tray_data.notification_title, nullptr);
}
/**
* @brief License-state fixture for the Windows Virtual HID Driver tray menu.
* @brief License-state fixture for the Windows Virtual HID Broker tray menu.
*/
class UnlicensedVirtualHidTrayTest:
public SystemTrayTest,
@@ -478,7 +522,7 @@ TEST_P(UnlicensedVirtualHidTrayTest, PreparesMenuAndStartupNotification) {
EXPECT_STREQ(license_menu[2].text, "Driver-backed keyboard, mouse, and gamepads are locked");
EXPECT_STREQ(license_menu[3].text, service_available ? "License service: Available" : "License service: Unavailable");
verify_virtualhid_actions_menu(license_menu);
EXPECT_STREQ(tray_data.notification_title, "Virtual HID Driver License");
EXPECT_STREQ(tray_data.notification_title, "Virtual HID Broker License");
EXPECT_STREQ(
tray_data.notification_text,
"Get or manage a license, or use the limited, end-of-life ViGEmBus driver."
@@ -508,6 +552,110 @@ INSTANTIATE_TEST_SUITE_P(
);
#endif
#ifdef __APPLE__
TEST_F(SystemTrayTest, NotifiesWhenMacBrokerVersionIsUnsupported) {
const auto supported_versions = std::format(">= {}", LIBVIRTUALHID_MACOS_MINIMUM_VERSION);
system_tray::update_tray_virtualhid_driver(true, "2026.829.2338", false, supported_versions); // NOSONAR(cpp:S1313): not an IP address
const auto &tray_data = system_tray::tray_data_for_testing();
const auto expected_notification = std::format(
"Installed Virtual HID Broker v2026.829.2338 is not supported by this version of Sunshine. Supported versions: {}. Restart Sunshine after updating. Click for instructions.",
supported_versions
);
EXPECT_STREQ(tray_data.notification_title, "Update Virtual HID Broker");
EXPECT_STREQ(tray_data.notification_text, expected_notification.c_str());
EXPECT_STREQ(tray_data.notification_icon, tray_data.allIconPaths[4]);
EXPECT_NE(tray_data.notification_cb, nullptr);
}
TEST_F(SystemTrayTest, MacBrokerVersionNotificationRespectsDisabledGamepads) {
const auto supported_versions = std::format(">= {}", LIBVIRTUALHID_MACOS_MINIMUM_VERSION);
config::input.gamepad_driver = config::GAMEPAD_DRIVER_NONE;
system_tray::update_tray_virtualhid_driver(true, "2026.829.2338", false, supported_versions); // NOSONAR(cpp:S1313): not an IP address
const auto &tray_data = system_tray::tray_data_for_testing();
EXPECT_EQ(tray_data.notification_title, nullptr);
EXPECT_EQ(tray_data.notification_text, nullptr);
config::input.gamepad_driver.clear();
system_tray::update_tray_virtualhid_driver(true, "2026.829.2338", false, supported_versions); // NOSONAR(cpp:S1313): not an IP address
EXPECT_STREQ(tray_data.notification_title, "Update Virtual HID Broker");
}
TEST_F(SystemTrayTest, ShowsLicensedMacBrokerDetailsBeforeInitialization) {
lvh::LicenseStatus license;
license.service_available = true;
license.state = lvh::LicenseState::licensed;
license.plan_name = "Yearly";
license.customer_email = "customer@example.com";
license.activation_usage = 1;
license.activation_limit = 5;
system_tray::update_tray_virtualhid_license(license, true);
const auto &tray_data = system_tray::tray_data_for_testing();
ASSERT_EQ(tray_data.iconPathCount, 5);
const auto *menu = tray_data.menu[2].submenu;
ASSERT_NE(menu, nullptr);
EXPECT_STREQ(menu[0].text, "Status: Licensed");
EXPECT_STREQ(menu[1].text, "Plan: Yearly");
EXPECT_STREQ(menu[2].text, "Customer: customer@example.com");
EXPECT_STREQ(menu[3].text, "Machine activation limit: 5");
EXPECT_STREQ(menu[4].text, "-");
EXPECT_STREQ(menu[5].text, "Get/Manage License");
EXPECT_NE(menu[5].cb, nullptr);
EXPECT_STREQ(menu[6].text, "Download Virtual HID Broker");
EXPECT_NE(menu[6].cb, nullptr);
EXPECT_EQ(menu[7].text, nullptr);
EXPECT_EQ(tray_data.notification_title, nullptr);
}
TEST_F(SystemTrayTest, ShowsMacBrokerLicenseAndAvailabilityWithOptionalStartupNotifications) {
lvh::LicenseStatus license;
license.service_available = true;
license.state = lvh::LicenseState::unlicensed;
system_tray::update_tray_virtualhid_license(license, true);
const auto &tray_data = system_tray::tray_data_for_testing();
const auto *menu = tray_data.menu[2].submenu;
ASSERT_NE(menu, nullptr);
EXPECT_STREQ(menu[0].text, "Status: Not Activated");
EXPECT_STREQ(menu[1].text, "No license is active on this machine");
EXPECT_STREQ(menu[2].text, "Virtual gamepads are locked");
EXPECT_STREQ(menu[3].text, "License service: Available");
EXPECT_STREQ(tray_data.notification_title, "Virtual HID Broker License");
EXPECT_STREQ(tray_data.notification_text, "Activate a machine license to use virtual gamepads. Click to manage the license.");
EXPECT_STREQ(tray_data.notification_icon, tray_data.allIconPaths[4]);
EXPECT_NE(tray_data.notification_cb, nullptr);
license.service_available = false;
license.state = lvh::LicenseState::unavailable;
system_tray::update_tray_virtualhid_license(license, true);
EXPECT_STREQ(menu[0].text, "Status: Unavailable");
EXPECT_STREQ(menu[3].text, "License service: Unavailable");
EXPECT_STREQ(tray_data.notification_title, "Virtual HID Broker Is Unavailable");
EXPECT_STREQ(tray_data.notification_text, "Install and start Virtual HID Broker to use virtual gamepads. Click to download.");
EXPECT_STREQ(tray_data.notification_icon, tray_data.allIconPaths[4]);
EXPECT_NE(tray_data.notification_cb, nullptr);
system_tray::update_tray_virtualhid_license(license, false);
EXPECT_EQ(tray_data.notification_title, nullptr);
EXPECT_EQ(tray_data.notification_cb, nullptr);
config::input.gamepad_driver = config::GAMEPAD_DRIVER_NONE;
system_tray::update_tray_virtualhid_license(license, true);
EXPECT_EQ(tray_data.notification_title, nullptr);
EXPECT_EQ(tray_data.notification_cb, nullptr);
EXPECT_STREQ(menu[0].text, "Status: Unavailable");
config::input.gamepad_driver = config::GAMEPAD_DRIVER_VIRTUALHID;
config::input.controller = false;
system_tray::update_tray_virtualhid_license(license, true);
EXPECT_EQ(tray_data.notification_title, nullptr);
}
#endif
#ifndef _WIN32
TEST_F(SystemTrayTest, LifecycleMenuAndStateTransitions) {
if (const int result = initialize_tray(); result != 0) {
@@ -612,7 +760,7 @@ TEST_F(SystemTrayVisualTest, CapturesIconTooltipNotificationsAndMenu) {
}
const auto &tray_data = system_tray::tray_data_for_testing();
#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
ASSERT_EQ(tray_data.iconPathCount, 5);
#else
ASSERT_EQ(tray_data.iconPathCount, 4);
+79
View File
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import DisplayDeviceOptions from '../../src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue'
import General from '../../src_assets/common/assets/web/configs/tabs/General.vue'
import Inputs from '../../src_assets/common/assets/web/configs/tabs/Inputs.vue'
import Network from '../../src_assets/common/assets/web/configs/tabs/Network.vue'
const globalOptions = {
@@ -113,3 +114,81 @@ describe('network configuration', () => {
expect(tableText).not.toContain('NaN')
})
})
describe('gamepad input configuration', () => {
function mountInputs(platform, gamepadDriver = '', gamepad = 'auto', controller = 'enabled') {
return mount(Inputs, {
props: {
platform,
config: {
controller,
gamepad,
gamepad_driver: gamepadDriver,
keybindings: '[]',
motion_as_ds4: 'enabled',
touchpad_as_ds4: 'enabled',
ds4_back_as_touchpad_click: 'enabled',
virtualhid_randomize_mac: 'enabled',
keyboard: 'disabled',
mouse: 'disabled',
},
},
global: {
mocks: { $t: key => key },
stubs: { Checkbox: true, VirtualKeyCodeSelect: true },
},
})
}
it.each(['freebsd', 'linux', 'macos', 'windows'])('offers all profiles and automatic detection on %s', platform => {
const wrapper = mountInputs(platform, platform === 'windows' ? 'virtualhid' : '')
expect(wrapper.find('#gamepad_driver').exists()).toBe(platform === 'windows' || platform === 'macos')
expect(wrapper.get('#gamepad').findAll('option').map(option => option.attributes('value'))).toEqual([
'auto', 'generic', 'x360', 'xone', 'xseries', 'ds4', 'ds5', 'switch',
])
expect(wrapper.find('#motion_as_ds4').exists()).toBe(true)
expect(wrapper.find('#touchpad_as_ds4').exists()).toBe(true)
})
it('shows the existing Enable Gamepad Input control on macOS', () => {
const enabled = mountInputs('macos')
expect(enabled.find('#controller').exists()).toBe(true)
expect(enabled.find('#gamepad').exists()).toBe(true)
const disabled = mountInputs('macos', '', 'auto', 'disabled')
expect(disabled.find('#controller').exists()).toBe(true)
expect(disabled.find('#gamepad_driver').exists()).toBe(true)
expect(disabled.find('#gamepad').exists()).toBe(false)
expect(disabled.find('#keyboard').exists()).toBe(true)
expect(disabled.find('#mouse').exists()).toBe(true)
})
it.each(['macos', 'windows'])('offers None to disable gamepads on %s', async platform => {
const wrapper = mountInputs(platform)
const backend = wrapper.get('#gamepad_driver')
expect(backend.findAll('option').map(option => option.attributes('value'))).toContain('none')
if (platform === 'macos') {
expect(backend.element.value).toBe('virtualhid')
}
await backend.setValue('none')
expect(wrapper.find('#gamepad').exists()).toBe(false)
expect(wrapper.find('#motion_as_ds4').exists()).toBe(false)
expect(wrapper.find('#back_button_timeout').exists()).toBe(false)
expect(wrapper.find('#keyboard').exists()).toBe(true)
expect(wrapper.find('#mouse').exists()).toBe(true)
})
it('limits Windows ViGEmBus to Xbox 360 and DualShock 4', async () => {
const wrapper = mountInputs('windows', 'virtualhid', 'xone')
await wrapper.get('#gamepad_driver').setValue('vigembus')
expect(wrapper.get('#gamepad').findAll('option').map(option => option.attributes('value'))).toEqual([
'auto', 'x360', 'ds4',
])
expect(wrapper.get('#gamepad').element.value).toBe('auto')
})
})
+94
View File
@@ -0,0 +1,94 @@
import { mount, flushPromises } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../src_assets/common/assets/web/Navbar.vue', () => ({
default: { template: '<div />' },
}))
vi.mock('../../src_assets/common/assets/web/ResourceCard.vue', () => ({
default: { template: '<div />' },
}))
import Home from '../../src_assets/common/assets/web/Home.vue'
async function mountHome(platform, { developmentVersion = true, licensed = true, serviceAvailable = true, gamepadDriver = 'virtualhid' } = {}) {
vi.stubGlobal('fetch', vi.fn(async url => {
if (url === './api/config') {
return { json: async () => ({ platform, controller: 'enabled', gamepad_driver: gamepadDriver, version: '2026.927.1200' }) }
}
if (url === './api/virtual-input/status') {
return {
json: async () => ({
virtualhid: { installed: true, development_version: developmentVersion, version_compatible: true },
vigembus: { installed: false, version_compatible: false },
}),
}
}
if (url === './api/virtual-input/license') {
return { json: async () => ({ licensed, service_available: serviceAvailable }) }
}
if (url === './api/logs') {
return { text: async () => '' }
}
if (url === 'https://api.github.com/repos/LizardByte/Sunshine/releases/latest') {
return { json: async () => ({ tag_name: 'v2026.927.1200' }) }
}
if (url === 'https://api.github.com/repos/LizardByte/Sunshine/releases') {
return { json: async () => [{ tag_name: 'v2026.927.1200', prerelease: true }] }
}
throw new Error(`Unexpected fetch: ${url}`)
}))
const wrapper = mount(Home, {
global: {
mocks: { $t: key => key },
stubs: { RouterLink: true },
},
})
await flushPromises()
return wrapper
}
afterEach(() => {
vi.unstubAllGlobals()
})
describe('development broker home notice', () => {
it.each(['windows', 'macos'])('shows the development card on %s', async platform => {
const wrapper = await mountHome(platform)
expect(fetch).toHaveBeenCalledWith('./api/virtual-input/status')
expect(wrapper.get('.alert.my-4').text()).toContain('index.virtualhid_development_title')
expect(wrapper.get('.alert.my-4').text()).toContain('index.virtualhid_development_desc')
wrapper.unmount()
})
it('prioritizes a macOS license warning over the development card', async () => {
const wrapper = await mountHome('macos', { licensed: false })
expect(wrapper.get('.alert.my-4').text()).toContain('index.virtualhid_macos_license_title')
expect(wrapper.get('.alert.my-4').text()).not.toContain('index.virtualhid_development_title')
wrapper.unmount()
})
it('shows the macOS broker warning when the license service is unavailable', async () => {
const wrapper = await mountHome('macos', { licensed: false, serviceAvailable: false })
expect(wrapper.get('.alert.my-4').text()).toContain('index.virtualhid_broker_unavailable_title')
expect(wrapper.get('.alert.my-4').text()).not.toContain('index.virtualhid_development_title')
wrapper.unmount()
})
it('does not show a development card for a stable macOS broker', async () => {
const wrapper = await mountHome('macos', { developmentVersion: false })
expect(wrapper.find('.alert.my-4').exists()).toBe(false)
wrapper.unmount()
})
it('does not show a broker card when gamepads are disabled', async () => {
const wrapper = await mountHome('macos', { gamepadDriver: 'none' })
expect(wrapper.find('.alert.my-4').exists()).toBe(false)
wrapper.unmount()
})
})
+84
View File
@@ -0,0 +1,84 @@
import { mount, flushPromises } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../src_assets/common/assets/web/Navbar.vue', () => ({
default: { template: '<div />' },
}))
import Troubleshooting from '../../src_assets/common/assets/web/Troubleshooting.vue'
async function mountTroubleshooting(platform, gamepadDriver, licenseStatus = {}) {
vi.stubGlobal('fetch', vi.fn(async url => {
if (url === '/api/config') {
return { json: async () => ({ platform, gamepad_driver: gamepadDriver }) }
}
if (url === '/api/virtual-input/status') {
return {
json: async () => ({
virtualhid: {
installed: true,
version: '2026.914.1218',
version_compatible: true,
supported_versions: '>= 2026.914.1218',
},
vigembus: { installed: false },
}),
}
}
if (url === '/api/virtual-input/license') {
return { ok: true, json: async () => ({ service_available: true, state: 'licensed', licensed: true, ...licenseStatus }) }
}
if (url === './api/logs') {
return { text: async () => '' }
}
if (url === './api/clients/list') {
return { json: async () => ({ status: true, named_certs: [] }) }
}
return {
ok: true,
json: async () => ({ tag_name: 'v2026.914.1218.10', html_url: 'https://example.com/release' }),
}
}))
const wrapper = mount(Troubleshooting, {
global: {
mocks: { $t: key => key },
stubs: { Navbar: true, RouterLink: true },
},
})
await flushPromises()
return wrapper
}
afterEach(() => {
vi.unstubAllGlobals()
})
describe('virtual input troubleshooting', () => {
it('shows the broker version table without ViGEmBus on macOS', async () => {
const wrapper = await mountTroubleshooting('macos', 'all')
expect(wrapper.find('.virtual-gamepad-section').exists()).toBe(true)
expect(wrapper.findAll('.driver-table-shell tbody tr')).toHaveLength(1)
expect(wrapper.find('.driver-table-shell').text()).toContain('2026.914.1218')
expect(fetch).toHaveBeenCalledWith('/api/virtual-input/status')
wrapper.unmount()
})
it.each(['macos', 'windows'])('shows only the license activation limit on %s', async platform => {
const wrapper = await mountTroubleshooting(platform, 'all', { activation_usage: 1, activation_limit: 5 })
const activation = wrapper.findAll('.virtualhid-license-stat').find(stat =>
stat.find('dt').text() === 'troubleshooting.virtualhid_license_activation_limit'
)
expect(activation.find('dd').text()).toBe('5')
wrapper.unmount()
})
it('keeps both backend rows on Windows when all backends are enabled', async () => {
const wrapper = await mountTroubleshooting('windows', 'all')
expect(wrapper.findAll('.driver-table-shell tbody tr')).toHaveLength(2)
wrapper.unmount()
})
})
+1 -1
View File
@@ -43,7 +43,7 @@ const emitTrayIconsPlugin = {
});
const virtualHidIcon = resolve(projectRoot, 'third-party/libvirtualhid/libvirtualhid.svg');
if (process.platform === 'win32' && fs.existsSync(virtualHidIcon)) {
if ((process.platform === 'win32' || process.platform === 'darwin') && fs.existsSync(virtualHidIcon)) {
this.emitFile({
type: 'asset',
fileName: 'images/logo-libvirtualhid.svg',