▸
.github/workflows/ci-archlinux.yml
+9
−0
@@ -149,5 +149,14 @@
_support_headless_testing="${_support_headless_testing}" \
makepkg -si --noconfirm
# Native packages must install the host rule used by dynamically
# created libvirtualhid hidraw and input event nodes.
test -f /usr/lib/udev/rules.d/60-sunshine.rules
grep -Fq 'IMPORT{parent}="HID_*"' \
/usr/lib/udev/rules.d/60-sunshine.rules
grep -Fq 'ENV{HID_PHYS}=="libvirtualhid/uhid/*"' \
/usr/lib/udev/rules.d/60-sunshine.rules
udevadm verify /usr/lib/udev/rules.d/60-sunshine.rules
# Remove debug package
rm -f sunshine-debug*.pkg.tar.zst
▸
.github/workflows/ci-linux.yml
+5
−0
@@ -103,6 +103,11 @@
run: |
# install sunshine to the DESTDIR
DESTDIR=AppDir ninja install
test -f AppDir/usr/share/sunshine/udev/rules.d/60-sunshine.rules
grep -Fq 'IMPORT{parent}="HID_*"' \
AppDir/usr/share/sunshine/udev/rules.d/60-sunshine.rules
grep -Fq 'ENV{HID_PHYS}=="libvirtualhid/uhid/*"' \
AppDir/usr/share/sunshine/udev/rules.d/60-sunshine.rules
# custom AppRun file
cp -f ../packaging/linux/AppImage/AppRun ./AppDir/
▸
.github/workflows/ci-macos.yml
+3
−0
@@ -58,9 +58,11 @@
- os: "macos-14"
name: "macOS-arm64"
arch: "arm64"
build_docs: "ON"
- os: "macos-15-intel"
name: "macOS-x86_64"
arch: "x86_64"
build_docs: "OFF"
steps:
- name: Install Apple certificate
uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0
@@ -121,6 +123,7 @@
cmake \
-B build \
-S . \
-DBUILD_DOCS="${{ matrix.build_docs }}" \
-DBUILD_WERROR=ON \
-DCMAKE_BUILD_TYPE=Release \
-DICU_ROOT="$(brew --prefix icu4c@78 2>/dev/null)" \
▸
cmake/packaging/linux.cmake
+8
−2
@@ -30,9 +30,15 @@
find_package(Udev)
if(UDEV_FOUND)
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules"
DESTINATION "${UDEV_RULES_INSTALL_DIR}")
set(SUNSHINE_UDEV_RULES_INSTALL_DIR "${UDEV_RULES_INSTALL_DIR}")
else()
set(SUNSHINE_UDEV_RULES_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/udev/rules.d")
message(WARNING
"Could not determine the host udev rules directory; "
"installing Sunshine rules to ${SUNSHINE_UDEV_RULES_INSTALL_DIR}")
endif()
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules"
DESTINATION "${SUNSHINE_UDEV_RULES_INSTALL_DIR}")
if(SYSTEMD_FOUND)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/app-${PROJECT_FQDN}.service"
DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}")
▸
docs/getting_started.md
+18
−8
@@ -265,6 +265,9 @@
```
##### Additional installation (required)
Run this command after installing or updating the Flatpak so the privileged host
udev rules stay synchronized with Sunshine:
```bash
flatpak run --command=additional-install.sh dev.lizardbyte.app.Sunshine
```
@@ -293,6 +296,7 @@
brew upgrade
brew tap LizardByte/homebrew
brew install sunshine
sudo "$(brew --prefix sunshine)/bin/postinst"
```
##### Uninstall
@@ -476,9 +480,9 @@
### Windows
Sunshine uses libvirtualhid for virtual input on Windows. You must install the
[Virtual HID Driver](https://github.com/LizardByte/libvirtualhid/releases/latest) separately for a driver-backed Raw
Input mouse and full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox 360 and
Input keyboard and mouse plus full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox
360 and DualShock 4 gamepads when libvirtualhid is unavailable.
DualShock 4 gamepads when libvirtualhid is unavailable.
Sunshine requires Virtual HID Driver version `2026.829.2338.54` or newer. Earlier releases use incompatible Windows
Sunshine requires Virtual HID Driver version `2026.823.352.3` or newer. Earlier releases use incompatible Windows
control and broker protocols and must be upgraded together with Sunshine's embedded libvirtualhid library. Local
development driver builds using a `0.0.0.*` version remain supported.
@@ -488,14 +492,20 @@
as motion, touchpads, LEDs, and adaptive triggers when supported. Virtual HID Driver is actively developed and
supported by the LizardByte team.
With a compatible driver and active license, relative mouse movement, buttons, and scrolling are exposed as a real HID
With a compatible driver and active license, normal key transitions are exposed through a real HID keyboard so
applications 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 retains its legacy SendInput fallback.
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 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. When Sunshine starts on an unactivated machine, select its tray
notification to open the activation and purchase options in the Web UI. Sunshine recreates the shared keyboard and
Input mouse. Sunshine shows the current license status and actions on the Web UI Troubleshooting page and in the
**Virtual HID Driver** system tray submenu. When Sunshine starts on an unactivated machine, select its tray notification
to open the activation and purchase options in the Web UI. Sunshine recreates the shared mouse after a successful
license action, so switching between the HID and SendInput paths does not require restarting Sunshine.
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.
▸
docs/troubleshooting.md
+30
−8
@@ -188,6 +188,18 @@
After installation, the `udev` rules need to be reloaded. Our post-install script tries to do this for you
automatically, but if it fails, you may need to restart your system.
Sunshine recreates virtual gamepad device nodes for each streaming session. Manual `chmod` or `setfacl`
changes therefore disappear when the client reconnects. Confirm that the installed Sunshine rule contains the
parent-property import and `libvirtualhid/uhid/*` match, then reload it and reapply it to existing gamepad nodes:
```bash
grep -R -E 'IMPORT\{parent\}="HID_\*"|ENV\{HID_PHYS\}=="libvirtualhid/uhid/\*"' \
/etc/udev/rules.d /usr/lib/udev/rules.d /lib/udev/rules.d 2>/dev/null
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=hidraw
sudo udevadm trigger --subsystem-match=input
```
If the input is still not working, you may need to add your user to the `input` group.
```bash
@@ -292,10 +304,10 @@
### No gamepad detected
Sunshine uses libvirtualhid for virtual input on Windows. Install the
[Virtual HID Driver](https://github.com/LizardByte/libvirtualhid/releases/latest) separately for a driver-backed Raw
Input keyboard and mouse plus full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox
Input mouse and full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox 360 and
360 and DualShock 4 gamepads when libvirtualhid is unavailable. If you use the
DualShock 4 gamepads when libvirtualhid is unavailable. If you use the
[ViGEmBus fallback](https://github.com/nefarius/ViGEmBus/releases/latest), you must use version 1.17 or newer.
Sunshine requires Virtual HID Driver version `2026.823.352.3` or newer. Earlier releases use incompatible Windows
Sunshine requires Virtual HID Driver version `2026.829.2338.54` or newer. Earlier releases use incompatible Windows
control and broker protocols. The Troubleshooting page reports an older installed package as unsupported and links
to the current driver release. Local development driver builds using a `0.0.0.*` version remain supported.
@@ -305,12 +317,22 @@
ViGEmBus project, Virtual HID Driver is actively developed and supported by the LizardByte team.
An active Virtual HID Driver machine license is required before Sunshine can create driver-backed libvirtualhid
devices, including gamepads and the Raw Input mouse. Follow the warning on the Web UI home page, the startup tray
notification, or the **Virtual HID Driver** tray submenu to open the license section on the Troubleshooting page, where
you can activate a key or follow the purchase link.
devices, including gamepads and the Raw Input keyboard and mouse. Follow the warning on the Web UI home page, the
startup tray notification, or the **Virtual HID Driver** tray submenu to open the license section on the
Troubleshooting page, where you can activate a key or follow the purchase link.
After installation, it is recommended to restart your computer.
### Games do not detect keyboard input
With a compatible Virtual HID Driver 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
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
through a real HID device so games using Raw Input can receive them. Absolute positioning still uses Windows input
@@ -318,8 +340,8 @@
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
is disabled. Sunshine recreates the shared 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.
is disabled. The same live refresh used by the keyboard path also switches the mouse between HID and SendInput without
requiring a Sunshine restart.
### Permission denied
Since Sunshine runs as a service on Windows, it may not have the same level of access that your regular user account
▸
packaging/linux/AppImage/AppRun
+4
−1
@@ -28,7 +28,7 @@
------ # This message
$ARGV0 --install, -i
# Install input rules sunshine.service files. Restart required.
# Install or update input rules and sunshine.service files.
$ARGV0 --remove, -r
# Remove input rules sunshine.service files.
@@ -52,6 +52,8 @@
sudo udevadm control --reload-rules
sudo udevadm trigger --property-match=DEVNAME=/dev/uinput
sudo udevadm trigger --property-match=DEVNAME=/dev/uhid
sudo udevadm trigger --subsystem-match=hidraw
sudo udevadm trigger --subsystem-match=input
# sunshine service
mkdir -p ~/.config/systemd/user
@@ -66,6 +68,7 @@
function remove() {
# remove input rules
sudo rm -f /etc/udev/rules.d/60-sunshine.rules
sudo udevadm control --reload-rules
# remove uhid module loading config
sudo rm -f /etc/modules-load.d/60-sunshine.conf
▸
packaging/linux/Arch/sunshine.install
+5
−1
@@ -1,11 +1,15 @@
# shellcheck shell=bash
do_setcap() {
setcap cap_sys_admin,cap_sys_nice+p $(readlink -f usr/bin/sunshine)
setcap cap_sys_admin,cap_sys_nice+p "$(readlink -f usr/bin/sunshine)"
}
do_udev_reload() {
udevadm control --reload-rules
udevadm trigger --property-match=DEVNAME=/dev/uinput
udevadm trigger --property-match=DEVNAME=/dev/uhid
udevadm trigger --subsystem-match=hidraw
udevadm trigger --subsystem-match=input
modprobe uinput || true
modprobe uhid || true
}
▸
packaging/linux/copr/Sunshine.spec
+5
−3
@@ -414,21 +414,23 @@
%post
# Note: this is copied from the postinst script
# Load uhid (DS5 emulation)
echo "Loading uhid kernel module for DS5 emulation."
# Load uhid for descriptor-driven gamepad emulation
echo "Loading uhid kernel module for gamepad emulation."
modprobe uhid
# Check if we're in an rpm-ostree environment
if [ ! -x "$(command -v rpm-ostree)" ]; then
echo "Not in an rpm-ostree environment, proceeding with post install steps."
# Trigger udev rule reload for /dev/uinput and /dev/uhid
# Reload the rules and reapply them to virtual gamepad child nodes.
path_to_udevadm=$(which udevadm)
if [ -x "$path_to_udevadm" ]; then
echo "Reloading udev rules."
$path_to_udevadm control --reload-rules
$path_to_udevadm trigger --property-match=DEVNAME=/dev/uinput
$path_to_udevadm trigger --property-match=DEVNAME=/dev/uhid
$path_to_udevadm trigger --subsystem-match=hidraw
$path_to_udevadm trigger --subsystem-match=input
echo "Udev rules reloaded successfully."
else
echo "error: udevadm not found or not executable."
▸
packaging/linux/flatpak/scripts/additional-install.sh
+9
−4
@@ -6,14 +6,19 @@
echo "Sunshine User Service has been installed."
echo "Use [systemctl --user enable app-dev.lizardbyte.app.Sunshine] once to autostart Sunshine on login."
# Load uhid (DS5 emulation)
# Load uhid for descriptor-driven gamepad emulation
UHID=$(cat /app/share/sunshine/modules-load.d/60-sunshine.conf)
echo "Enabling DS5 emulation."
echo "Enabling gamepad emulation."
flatpak-spawn --host pkexec sh -c "echo '$UHID' > /etc/modules-load.d/60-sunshine.conf"
flatpak-spawn --host pkexec modprobe uhid
# Udev rule
UDEV=$(cat /app/share/sunshine/udev/rules.d/60-sunshine.rules)
echo "Configuring mouse permission."
echo "Configuring virtual input permissions."
flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/60-sunshine.rules"
flatpak-spawn --host pkexec udevadm control --reload-rules
echo "Restart computer for mouse permission to take effect."
flatpak-spawn --host pkexec udevadm trigger --property-match=DEVNAME=/dev/uinput
flatpak-spawn --host pkexec udevadm trigger --property-match=DEVNAME=/dev/uhid
flatpak-spawn --host pkexec udevadm trigger --subsystem-match=hidraw
flatpak-spawn --host pkexec udevadm trigger --subsystem-match=input
echo "Virtual input permissions have been updated."
▸
packaging/linux/flatpak/scripts/remove-additional-install.sh
+2
−1
@@ -9,4 +9,5 @@
# Remove rules
flatpak-spawn --host pkexec sh -c "rm /etc/modules-load.d/60-sunshine.conf"
flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/60-sunshine.rules"
echo "Input rules removed. Restart computer to take effect."
flatpak-spawn --host pkexec udevadm control --reload-rules
echo "Input rules removed."
▸
packaging/sunshine.rb
+5
−0
@@ -476,6 +476,11 @@
# test that the binary runs at all
system bin/"sunshine", "--version"
if OS.linux?
assert_path_exists lib/"udev/rules.d/60-sunshine.rules"
assert_path_exists lib/"modules-load.d/60-sunshine.conf"
end
if IS_UPSTREAM_REPO
artifact_dir = release_homebrew_testpath
if artifact_dir
▸
src/confighttp.cpp
+2
−2
@@ -175,7 +175,7 @@
*/
constexpr auto CSRF_TOKEN_LIFETIME = std::chrono::hours(1); // Tokens valid for 1 hour
constexpr auto LIBVIRTUALHID_MINIMUM_VERSION = "2026.823.352.3"sv; ///< Minimum supported libvirtualhid driver version. // NOSONAR(cpp:S1313): not an IP address
constexpr auto LIBVIRTUALHID_MINIMUM_VERSION = "2026.829.2338.54"sv; ///< Minimum supported libvirtualhid driver version. // NOSONAR(cpp:S1313): not an IP address
constexpr auto VIGEMBUS_MINIMUM_VERSION = "1.17.0.0"sv; ///< Minimum supported ViGEmBus fallback driver version. // NOSONAR(cpp:S1313): not an IP address
/**
@@ -1958,7 +1958,7 @@
#endif
#ifdef _WIN32
if (result.status.ok()) {
input::refresh_virtual_mouse();
input::refresh_virtual_input();
}
#endif
send_response(response, build_virtualhid_license_status(result));
▸
src/input.cpp
+7
−2
@@ -2091,11 +2091,16 @@
});
}
void refresh_virtual_mouse() {
void refresh_virtual_input() {
dispatch_input_task([]() {
if (platf_input) {
task_pool.cancel(key_press_repeat_id);
key_press_repeat_id = nullptr;
reset_mouse_buttons();
reset_keyboard_keys();
platf::virtualhid::get_input_context(platf_input).refresh_mouse();
auto &context = platf::virtualhid::get_input_context(platf_input);
context.refresh_keyboard();
context.refresh_mouse();
}
});
}
▸
src/input.h
+3
−3
@@ -63,12 +63,12 @@
bool probe_gamepads();
/**
* @brief Recreate the shared libvirtualhid mouse after a license-state change.
* @brief Recreate shared libvirtualhid keyboard and mouse devices after a license-state change.
*
* The work is serialized with streamed input so the mouse backend can switch
* The work is serialized with streamed input so both backends can switch
* safely between the Windows HID and SendInput paths.
*/
void refresh_virtual_input();
void refresh_virtual_mouse();
/**
* @brief Allocate and initialize platform input state for a stream.
▸
src/platform/common.h
+24
−2
@@ -116,5 +116,6 @@
rumble_triggers, ///< Rumble triggers
set_motion_event_state, ///< Set motion event state
set_rgb_led, ///< Set RGB LED
set_player_leds, ///< Set player indicator LEDs
set_adaptive_triggers, ///< Set adaptive triggers
};
@@ -190,6 +191,22 @@
}
/**
* @brief Create player indicator LED state.
*
* @param id Identifier for the controller, session, display, or resource.
* @param solid Four-bit mask of solid player indicators.
* @param flashing Four-bit mask of flashing player indicators.
* @return Constructed player indicator LED object.
*/
static gamepad_feedback_msg_t make_player_leds(std::uint16_t id, std::uint8_t solid, std::uint8_t flashing) {
gamepad_feedback_msg_t msg;
msg.type = gamepad_feedback_e::set_player_leds;
msg.id = id;
msg.data.player_leds = {solid, flashing};
return msg;
}
/**
* @brief Create adaptive triggers.
*
* @param id Identifier for the controller, session, display, or resource.
@@ -234,6 +251,11 @@
} rgb_led;
struct {
std::uint8_t solid;
std::uint8_t flashing;
} player_leds;
struct {
uint16_t controllerNumber;
uint8_t event_flags;
uint8_t type_left;
@@ -380,9 +402,9 @@
*/
constexpr caps_t pen_touch = 0x01; // Pen and touch events
/**
* @brief Capability bit indicating controller touchpad support.
* @brief Capability bit indicating controller touchpad and motion support.
*/
constexpr caps_t controller_touch = 0x02; // Controller touch and motion events
constexpr caps_t controller_touch = 0x02; // Controller touch events
}; // namespace platform_caps
/**
▸
src/platform/linux/input/virtualhid.cpp
+1
−1
@@ -28,6 +28,6 @@
if (const auto &capabilities = runtime->capabilities(); config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
caps |= platform_caps::pen_touch;
}
if (virtualhid::configured_gamepad_supports_touchpad()) {
if (virtualhid::configured_gamepad_supports_controller_extensions()) {
caps |= platform_caps::controller_touch;
}
▸
src/platform/macos/input.cpp
+1
−1
@@ -37,7 +37,7 @@
}
const auto &capabilities = runtime->capabilities();
if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_touchpad()) {
if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_controller_extensions()) {
caps |= platform_caps::controller_touch;
}
if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
▸
src/platform/virtualhid_input.cpp
+61
−12
@@ -45,6 +45,9 @@
std::uint8_t last_red = 0; ///< Last red LED value.
std::uint8_t last_green = 0; ///< Last green LED value.
std::uint8_t last_blue = 0; ///< Last blue LED value.
bool has_last_player_leds = false; ///< Whether last player indicator LED values are valid.
std::uint8_t last_solid_player_leds = 0; ///< Last solid player indicator mask.
std::uint8_t last_flashing_player_leds = 0; ///< Last flashing player indicator mask.
};
namespace {
@@ -314,6 +317,22 @@
(void) flags;
#endif
return event;
}
/**
* @brief Pack four player indicator states into a protocol bit mask.
*
* @param leds Player indicator states ordered from player one through four.
* @return Four-bit player indicator mask.
*/
std::uint8_t player_led_mask(const std::array<bool, 4> &leds) {
std::byte mask {};
for (std::size_t index = 0; index < leds.size(); ++index) {
if (leds[index]) {
mask |= std::byte {1} << index;
}
}
return std::to_integer<std::uint8_t>(mask);
}
lvh::PenToolType pen_tool(std::uint8_t tool) {
@@ -390,6 +409,19 @@
gamepad->last_blue = output.blue;
raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_rgb_led(gamepad->client_relative_index, output.red, output.green, output.blue));
break;
case lvh::GamepadOutputKind::player_leds:
{
const auto solid = player_led_mask(output.player_leds);
const auto flashing = player_led_mask(output.flashing_player_leds);
if (gamepad->has_last_player_leds && gamepad->last_solid_player_leds == solid && gamepad->last_flashing_player_leds == flashing) {
return;
}
gamepad->has_last_player_leds = true;
gamepad->last_solid_player_leds = solid;
gamepad->last_flashing_player_leds = flashing;
raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_player_leds(gamepad->client_relative_index, solid, flashing));
break;
}
case lvh::GamepadOutputKind::adaptive_triggers:
raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_adaptive_triggers(gamepad->client_relative_index, output.adaptive_trigger_flags, output.left_trigger_effect_type, output.right_trigger_effect_type, output.left_trigger_effect, output.right_trigger_effect));
break;
@@ -421,19 +453,25 @@
return;
}
refresh_keyboard();
refresh_mouse();
}
void input_context_t::refresh_keyboard() {
const auto &capabilities = runtime->capabilities();
if (capabilities.supports_keyboard) {
lvh::CreateKeyboardOptions options;
options.profile = lvh::profiles::keyboard();
options.stable_id = "sunshine-keyboard";
auto created = runtime->create_keyboard(options);
if (created) {
keyboard = std::move(created.keyboard);
} else {
log_failure("create libvirtualhid keyboard"sv, created.status);
}
keyboard.reset();
if (!runtime || !runtime->capabilities().supports_keyboard) {
return;
}
refresh_mouse();
lvh::CreateKeyboardOptions options;
options.profile = lvh::profiles::keyboard();
options.stable_id = "sunshine-keyboard";
auto created = runtime->create_keyboard(options);
if (created) {
keyboard = std::move(created.keyboard);
} else {
log_failure("create libvirtualhid keyboard"sv, created.status);
}
}
void input_context_t::refresh_mouse() {
@@ -589,6 +627,7 @@
gamepad->has_last_rumble = false;
gamepad->has_last_trigger_rumble = false;
gamepad->has_last_rgb = false;
gamepad->has_last_player_leds = false;
if (gamepad->adapter->support().supports_motion) {
raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100));
@@ -905,6 +944,16 @@
const auto profile = profile_for_name(config::input.gamepad).profile();
return lvh::gamepad_profile_support(profile).supports_touchpad;
}
bool configured_gamepad_supports_controller_extensions() {
if (config::input.gamepad == "auto"sv) {
return true;
}
const auto profile = profile_for_name(config::input.gamepad).profile();
const auto &support = lvh::gamepad_profile_support(profile);
return support.supports_touchpad || support.supports_motion;
}
} // namespace platf::virtualhid
▸
src/platform/virtualhid_input.h
+15
−0
@@ -36,6 +36,11 @@
explicit input_context_t(lvh::BackendKind backend);
/**
* @brief Recreate the shared keyboard using the runtime's current driver and license state.
*/
void refresh_keyboard();
/**
* @brief Recreate the shared mouse using the runtime's current driver and license state.
*/
void refresh_mouse();
@@ -273,5 +278,15 @@
* @return True when controller touchpad input should be advertised.
*/
bool configured_gamepad_supports_touchpad();
/**
* @brief Return whether the configured gamepad profile needs Moonlight controller extensions.
*
* Moonlight uses the controller-touch feature flag to authorize both controller
* touchpad and motion packets.
*
* @return True when controller touchpad or motion input should be advertised.
*/
bool configured_gamepad_supports_controller_extensions();
} // namespace platf::virtualhid
▸
src/platform/windows/input.cpp
+1
−1
@@ -1234,6 +1234,6 @@
platform_caps::caps_t get_capabilities() {
platform_caps::caps_t caps = 0;
if (virtualhid::configured_gamepad_supports_touchpad()) {
if (virtualhid::configured_gamepad_supports_controller_extensions()) {
caps |= platform_caps::controller_touch;
}
▸
src/stream.cpp
+29
−0
@@ -49,6 +49,7 @@
constexpr int IDX_SET_MOTION_EVENT = 13; ///< Control-stream message index for set motion event.
constexpr int IDX_SET_RGB_LED = 14; ///< Control-stream message index for set rgb led.
constexpr int IDX_SET_ADAPTIVE_TRIGGERS = 15; ///< Control-stream message index for set adaptive triggers.
constexpr int IDX_SET_PLAYER_LEDS = 16; ///< Control-stream message index for set player indicator LEDs.
static const short packetTypes[] = {
0x0305, // Start A
@@ -67,6 +68,7 @@
0x5501, // Set motion event (Sunshine protocol extension)
0x5502, // Set RGB LED (Sunshine protocol extension)
0x5503, // Set Adaptive triggers (Sunshine protocol extension)
0x5504, // Set player indicator LEDs (Sunshine protocol extension)
};
namespace asio = boost::asio;
@@ -240,6 +242,17 @@
};
/**
* @brief Control payload that sets controller player indicator LEDs.
*/
struct control_set_player_leds_t {
control_header_v2 header; ///< Control message header preceding this payload.
std::uint16_t id; ///< Controller identifier associated with this message.
std::uint8_t solid; ///< Four-bit mask of solid player indicators.
std::uint8_t flashing; ///< Four-bit mask of flashing player indicators.
};
/**
* @brief Control payload that configures DualSense adaptive triggers.
*/
struct control_adaptive_triggers_t {
@@ -1031,5 +1044,21 @@
plaintext.b = data.b;
BOOST_LOG(verbose) << "RGB: "sv << msg.id << " :: "sv << util::hex(data.r).to_string_view() << util::hex(data.g).to_string_view() << util::hex(data.b).to_string_view();
std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
encrypted_payload;
payload = encode_control(session, util::view(plaintext), encrypted_payload);
} else if (msg.type == platf::gamepad_feedback_e::set_player_leds) {
control_set_player_leds_t plaintext;
plaintext.header.type = packetTypes[IDX_SET_PLAYER_LEDS];
plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);
auto &data = msg.data.player_leds;
plaintext.id = util::endian::little(msg.id);
plaintext.solid = data.solid;
plaintext.flashing = data.flashing;
BOOST_LOG(verbose) << "Player LEDs: "sv << msg.id << " :: solid "sv << util::hex(data.solid).to_string_view() << " :: flashing "sv << util::hex(data.flashing).to_string_view();
std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
encrypted_payload;
▸
src/system_tray.cpp
+3
−3
@@ -151,7 +151,7 @@
constexpr auto LIBVIRTUALHID_RELEASES_URL = "https://github.com/LizardByte/libvirtualhid/releases/latest"sv; ///< Latest Virtual HID Driver release.
static std::array<struct tray_menu, 7> virtualhid_benefits_menu {{
{.text = "Xbox One, Xbox Series, DualSense (DS5), Switch Pro, and Generic", .disabled = 1},
{.text = "Raw Input mouse for relative movement, buttons, and scrolling", .disabled = 1},
{.text = "Raw Input keyboard and mouse for physical-style input", .disabled = 1},
{.text = "Motion, touchpads, LEDs, and adaptive triggers where supported", .disabled = 1},
{.text = "Actively developed and supported by LizardByte", .disabled = 1},
{.text = "-"},
@@ -435,7 +435,7 @@
set_virtualhid_license_menu_item(6, "Manage License", false, tray_virtualhid_license_cb);
} else {
set_virtualhid_license_menu_item(1, std::string {virtualhid_license_state_detail(license.state)}, true);
set_virtualhid_license_menu_item(2, "Driver-backed gamepads and Raw Input mouse are locked", true);
set_virtualhid_license_menu_item(2, "Driver-backed keyboard, mouse, and gamepads are locked", true);
set_virtualhid_license_menu_item(
3,
license.service_available ? "License service: Available" : "License service: Unavailable",
@@ -470,7 +470,7 @@
if (notify_if_unlicensed && !license.licensed()) {
tray.notification_title = "Activate Virtual HID Driver";
tray.notification_text =
"Adds a Raw Input keyboard and mouse plus Xbox One/Series, DualSense (DS5), Switch Pro, and Generic gamepads. Actively maintained by LizardByte. Click to activate or buy a license; details remain in the tray menu.";
"Adds a Raw Input mouse plus Xbox One/Series, DualSense (DS5), Switch Pro, and Generic gamepads. Actively maintained by LizardByte. Click to activate or buy a license; details remain in the tray menu.";
tray.notification_icon = tray.allIconPaths[4];
tray.notification_cb = []() {
launch_ui("/troubleshooting#virtualhid-license");
▸
src_assets/common/assets/web/public/assets/locale/en.json
+7
−7
@@ -453,11 +453,11 @@
"startup_errors": "<b>Attention!</b> Sunshine detected these errors during startup. We <b>STRONGLY RECOMMEND</b> fixing them before streaming.",
"version_dirty": "Thank you for helping to make Sunshine a better software!",
"version_latest": "You are running the latest version of Sunshine",
"virtualhid_license_required_desc": "Virtual HID Driver adds a Raw Input mouse plus Xbox One, Xbox Series, DualSense, Nintendo Switch Pro, and Generic gamepads. It is actively maintained by the LizardByte team. Activate or manage the license to unlock driver-backed input.",
"virtualhid_license_required_desc": "Virtual HID Driver adds a Raw Input keyboard and mouse plus Xbox One, Xbox Series, DualSense, Nintendo Switch Pro, and Generic gamepads. It is actively maintained by the LizardByte team. Activate or manage the license to unlock driver-backed input.",
"virtualhid_license_required_title": "Virtual HID Driver License Missing",
"virtualhid_missing_vigembus_installed_desc": "ViGEmBus is installed, but without the libvirtualhid driver Sunshine can only offer limited fallback gamepad support.",
"virtualhid_missing_vigembus_installed_title": "Gamepad Support Is Limited",
"virtualhid_not_installed_desc": "Sunshine requires the libvirtualhid Windows driver for a Raw Input mouse and full virtual gamepad support. Install the libvirtualhid driver package, then restart Sunshine.",
"virtualhid_not_installed_desc": "Sunshine requires the libvirtualhid Windows driver for a Raw Input keyboard and mouse plus full virtual gamepad support. Install the libvirtualhid driver package, then restart Sunshine.",
"virtualhid_not_installed_title": "libvirtualhid Driver Not Installed",
"virtualhid_outdated_desc": "The installed libvirtualhid driver version (v{version}) is not supported. Supported versions: {supported_versions}.",
"virtualhid_outdated_title": "libvirtualhid Driver Outdated",
@@ -576,15 +576,15 @@
"unpair_title": "Unpair Devices",
"vigembus_driver": "ViGEmBus Fallback Driver",
"virtual_gamepad": "Virtual Input",
"virtual_gamepad_desc": "Sunshine uses libvirtualhid for a driver-backed mouse and full virtual gamepad support on Windows. ViGEmBus is detected only as a limited fallback for Xbox 360 and DualShock 4 gamepads.",
"virtual_gamepad_desc": "Sunshine uses libvirtualhid for a driver-backed keyboard and mouse plus full virtual gamepad support on Windows. ViGEmBus is detected only as a limited fallback for Xbox 360 and DualShock 4 gamepads.",
"virtual_gamepad_drivers": "Windows Driver Status",
"virtual_gamepad_drivers_desc": "Installed versions are compared with each project's latest stable GitHub release. Prereleases are ignored.",
"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 relative movement, buttons, and scrolling as a real HID mouse so games using Raw Input can receive them.",
"virtualhid_benefit_mouse_title": "Raw Input Mouse",
"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": "Actively developed and supported by the LizardByte team instead of relying only on the discontinued ViGEmBus project.",
"virtualhid_benefit_maintained_title": "Actively Supported",
"virtualhid_benefits_title": "Virtual HID Driver Benefits",
@@ -597,12 +597,12 @@
"virtualhid_license_buy": "Buy License",
"virtualhid_license_customer": "Customer",
"virtualhid_license_deactivate": "Deactivate This Machine",
"virtualhid_license_desc": "An active license is required before Sunshine can create driver-backed libvirtualhid devices, including gamepads and the Raw Input keyboard and mouse. Activation is handled by the local libvirtualhid service; Sunshine does not save your license key.",
"virtualhid_license_desc": "An active license is required before Sunshine can create driver-backed libvirtualhid devices, including gamepads and the Raw Input mouse. 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_machine_activated_desc": "Sunshine can create Virtual HID Driver gamepads and a Raw Input mouse while this license remains valid.",
"virtualhid_license_manage": "Manage Account and Activations",
"virtualhid_license_not_reported": "Not reported",
"virtualhid_license_plan": "Plan",
▸
src_assets/linux/misc/60-sunshine.conf
+1
−1
@@ -1,2 +1,2 @@
# Sunshine needs uhid for DS5 emulation
# Sunshine needs uhid for descriptor-driven gamepad emulation
uhid
▸
src_assets/linux/misc/60-sunshine.rules
+5
−2
@@ -5,7 +5,10 @@
KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess"
# Joypads
# HID physical and unique identifiers are uevent properties on the hidraw
# node's HID parent, rather than sysfs attributes exposed to ATTRS matching.
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", IMPORT{parent}="HID_*"
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ENV{HID_PHYS}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ENV{HID_NAME}=="Sunshine (libvirtualhid)*", GROUP="input", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{name}=="Sunshine (libvirtualhid)*", GROUP="input", MODE="0660", TAG+="uaccess"
SUBSYSTEMS=="input", ATTRS{name}=="Sunshine (libvirtualhid)*", GROUP="input", MODE="0660", TAG+="uaccess"
▸
src_assets/linux/misc/postinst
+26
−5
@@ -1,28 +1,49 @@
#!/bin/sh
# Load uhid (DS5 emulation)
echo "Loading uhid kernel module for DS5 emulation."
# Load uhid for descriptor-driven gamepad emulation
echo "Loading uhid kernel module for gamepad emulation."
modprobe uhid
# Homebrew installs host integration files inside its Cellar. Copy them to
# host directories that systemd-udevd and systemd-modules-load monitor.
script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
packaged_udev_rule="$script_dir/../lib/udev/rules.d/60-sunshine.rules"
packaged_modules_file="$script_dir/../lib/modules-load.d/60-sunshine.conf"
if [ -f "$packaged_udev_rule" ]; then
echo "Installing Sunshine udev rules."
install -Dm644 "$packaged_udev_rule" /etc/udev/rules.d/60-sunshine.rules
fi
if [ -f "$packaged_modules_file" ]; then
echo "Installing Sunshine modules-load configuration."
install -Dm644 "$packaged_modules_file" /etc/modules-load.d/60-sunshine.conf
fi
# Ensure Sunshine can grab images from KMS
path_to_setcap=$(which setcap)
path_to_sunshine=$(readlink -f "$(which sunshine)")
if [ -x "$script_dir/sunshine" ]; then
path_to_sunshine=$(readlink -f "$script_dir/sunshine")
else
path_to_sunshine=$(readlink -f "$(which sunshine)")
fi
if [ -x "$path_to_setcap" ] ; then
echo "Setting CAP_SYS_ADMIN, CAP_SYS_NICE capabilities on Sunshine binary."
echo "$path_to_setcap cap_sys_admin,cap_sys_nice+p $path_to_sunshine"
$path_to_setcap cap_sys_admin,cap_sys_nice+p $path_to_sunshine
$path_to_setcap cap_sys_admin,cap_sys_nice+p "$path_to_sunshine"
echo "CAP_SYS_ADMIN, CAP_SYS_NICE capabilities set on Sunshine binary."
else
echo "error: setcap not found or not executable."
fi
# Reload the rules and reapply them to both virtual device factories and any
# gamepad child nodes that already exist.
# Trigger udev rule reload for /dev/uinput and /dev/uhid
path_to_udevadm=$(which udevadm)
if [ -x "$path_to_udevadm" ] ; then
echo "Reloading udev rules."
$path_to_udevadm control --reload-rules
$path_to_udevadm trigger --property-match=DEVNAME=/dev/uinput
$path_to_udevadm trigger --property-match=DEVNAME=/dev/uhid
$path_to_udevadm trigger --subsystem-match=hidraw
$path_to_udevadm trigger --subsystem-match=input
echo "Udev rules reloaded successfully."
else
echo "error: udevadm not found or not executable."
▸
tests/unit/platform/test_virtualhid_input.cpp
+46
−15
@@ -28,15 +28,16 @@
/**
* @brief Expected touchpad support for a configured gamepad.
*/
struct gamepad_touchpad_case_t {
struct gamepad_capabilities_case_t {
std::string_view gamepad; ///< Configured gamepad name.
bool expected; ///< Whether the configured gamepad supports touchpad input.
bool expected_touchpad; ///< Whether the configured gamepad supports touchpad input.
bool expected_controller_extensions; ///< Whether Moonlight controller extensions should be advertised.
};
/**
* @brief Parameterized fixture that restores the configured gamepad after each test.
*/
class VirtualHidInputTest: public ::testing::TestWithParam<gamepad_touchpad_case_t> {
class VirtualHidInputTest: public ::testing::TestWithParam<gamepad_capabilities_case_t> {
protected:
/**
* @brief Preserve the configured gamepad.
@@ -59,25 +60,34 @@
} // namespace
TEST_P(VirtualHidInputTest, ReportsExpectedTouchpadSupport) {
const auto &test_case = GetParam();
config::input.gamepad = test_case.gamepad;
EXPECT_EQ(platf::virtualhid::configured_gamepad_supports_touchpad(), test_case.expected_touchpad) << test_case.gamepad;
}
TEST_P(VirtualHidInputTest, ReportsExpectedControllerExtensionSupport) {
const auto &test_case = GetParam();
config::input.gamepad = test_case.gamepad;
EXPECT_EQ(
const auto &[gamepad, expected] = GetParam();
config::input.gamepad = gamepad;
EXPECT_EQ(platf::virtualhid::configured_gamepad_supports_touchpad(), expected) << gamepad;
platf::virtualhid::configured_gamepad_supports_controller_extensions(),
test_case.expected_controller_extensions
) << test_case.gamepad;
}
INSTANTIATE_TEST_SUITE_P(
ConfiguredGamepads,
VirtualHidInputTest,
::testing::Values(
gamepad_capabilities_case_t {"auto"sv, true, true},
gamepad_capabilities_case_t {"generic"sv, false, false},
gamepad_touchpad_case_t {"auto"sv, true},
gamepad_capabilities_case_t {"x360"sv, false, false},
gamepad_capabilities_case_t {"xone"sv, false, false},
gamepad_capabilities_case_t {"xseries"sv, false, false},
gamepad_capabilities_case_t {"ds4"sv, true, true},
gamepad_capabilities_case_t {"ds5"sv, true, true},
gamepad_capabilities_case_t {"switch"sv, false, true}
gamepad_touchpad_case_t {"generic"sv, false},
gamepad_touchpad_case_t {"x360"sv, false},
gamepad_touchpad_case_t {"xone"sv, false},
gamepad_touchpad_case_t {"xseries"sv, false},
gamepad_touchpad_case_t {"ds4"sv, true},
gamepad_touchpad_case_t {"ds5"sv, true},
gamepad_touchpad_case_t {"switch"sv, false}
),
[](const ::testing::TestParamInfo<gamepad_capabilities_case_t> &info) {
[](const ::testing::TestParamInfo<gamepad_touchpad_case_t> &info) {
return std::string {info.param.gamepad};
}
);
@@ -240,6 +250,10 @@
platf::virtualhid::input_context_t no_runtime {lvh::BackendKind::fake};
no_runtime.runtime.reset();
no_runtime.refresh_keyboard();
no_runtime.refresh_mouse();
EXPECT_EQ(no_runtime.keyboard, nullptr);
EXPECT_EQ(no_runtime.mouse, nullptr);
EXPECT_EQ(platf::virtualhid::alloc_gamepad(no_runtime, valid_id, metadata, nullptr), -1);
EXPECT_EQ(platf::virtualhid::alloc_gamepad(*context(), {-1, 0}, metadata, nullptr), -1);
@@ -401,6 +415,23 @@
++output.blue;
ASSERT_TRUE(adapter->dispatch_output(output).ok());
EXPECT_TRUE(feedback_queue()->pop(10ms));
output.kind = lvh::GamepadOutputKind::player_leds;
output.player_leds = {true, false, true, false};
output.flashing_player_leds = {false, true, false, true};
ASSERT_TRUE(adapter->dispatch_output(output).ok());
feedback = feedback_queue()->pop(10ms);
ASSERT_TRUE(feedback);
EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_player_leds);
EXPECT_EQ(feedback->data.player_leds.solid, 0x05);
EXPECT_EQ(feedback->data.player_leds.flashing, 0x0A);
ASSERT_TRUE(adapter->dispatch_output(output).ok());
EXPECT_FALSE(feedback_queue()->pop(0ms));
output.player_leds[3] = true;
ASSERT_TRUE(adapter->dispatch_output(output).ok());
feedback = feedback_queue()->pop(10ms);
ASSERT_TRUE(feedback);
EXPECT_EQ(feedback->data.player_leds.solid, 0x0D);
output.kind = lvh::GamepadOutputKind::adaptive_triggers;
output.adaptive_trigger_flags = 5;
▸
tests/unit/test_confighttp.cpp
+2
−2
@@ -1390,8 +1390,8 @@
EXPECT_TRUE(virtualhid.contains("version_compatible"));
EXPECT_TRUE(virtualhid.contains("backend_name"));
EXPECT_TRUE(virtualhid.contains("requires_installed_driver"));
EXPECT_EQ(virtualhid["minimum_version"].get<std::string>(), "2026.823.352.3"); // NOSONAR(cpp:S1313): not an IP address
EXPECT_EQ(virtualhid["supported_versions"].get<std::string>(), ">= 2026.823.352.3"); // NOSONAR(cpp:S1313): not an IP address
EXPECT_EQ(virtualhid["minimum_version"].get<std::string>(), "2026.829.2338.54"); // NOSONAR(cpp:S1313): not an IP address
EXPECT_EQ(virtualhid["supported_versions"].get<std::string>(), ">= 2026.829.2338.54"); // NOSONAR(cpp:S1313): not an IP address
const auto vigembus = confighttp::get_vigembus_driver_status();
EXPECT_TRUE(vigembus.contains("installed"));
▸
tests/unit/test_input.cpp
+6
−2
@@ -105,14 +105,18 @@
EXPECT_NE(replacement, resumed);
}
TEST_F(InputGamepadSessionTest, RefreshesSharedMouseAfterLicenseStateChanges) {
TEST_F(InputGamepadSessionTest, RefreshesSharedVirtualInputAfterLicenseStateChanges) {
ASSERT_NE(context().keyboard, nullptr);
ASSERT_NE(context().mouse, nullptr);
const auto original_keyboard_id = context().keyboard->device_id();
const auto original_mouse_id = context().mouse->device_id();
const auto active_devices = runtime().active_device_count();
input::refresh_virtual_mouse();
input::refresh_virtual_input();
ASSERT_NE(context().keyboard, nullptr);
ASSERT_NE(context().mouse, nullptr);
EXPECT_NE(context().keyboard->device_id(), original_keyboard_id);
EXPECT_NE(context().mouse->device_id(), original_mouse_id);
EXPECT_EQ(runtime().active_device_count(), active_devices);
}
▸
tests/unit/test_system_tray.cpp
+3
−3
@@ -84,7 +84,7 @@
ASSERT_NE(benefits_menu, nullptr);
EXPECT_STREQ(benefits_menu[0].text, "Xbox One, Xbox Series, DualSense (DS5), Switch Pro, and Generic");
EXPECT_EQ(benefits_menu[0].disabled, 1);
EXPECT_STREQ(benefits_menu[1].text, "Raw Input mouse for relative movement, buttons, and scrolling");
EXPECT_STREQ(benefits_menu[1].text, "Raw Input keyboard and mouse for physical-style input");
EXPECT_EQ(benefits_menu[1].disabled, 1);
EXPECT_STREQ(benefits_menu[2].text, "Motion, touchpads, LEDs, and adaptive triggers where supported");
EXPECT_EQ(benefits_menu[2].disabled, 1);
@@ -384,7 +384,7 @@
ASSERT_NE(license_menu, nullptr);
EXPECT_STREQ(license_menu[0].text, std::format("Status: {}", state_label).c_str());
EXPECT_STREQ(license_menu[1].text, state_detail);
EXPECT_STREQ(license_menu[2].text, "Driver-backed gamepads and Raw Input mouse are locked");
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");
EXPECT_STREQ(license_menu[4].text, "Activate this machine to use Virtual HID Driver");
EXPECT_STREQ(license_menu[5].text, "-");
@@ -396,7 +396,7 @@
EXPECT_STREQ(tray_data.notification_title, "Activate Virtual HID Driver");
EXPECT_STREQ(
tray_data.notification_text,
"Adds a Raw Input keyboard and mouse plus Xbox One/Series, DualSense (DS5), Switch Pro, and Generic gamepads. Actively maintained by LizardByte. Click to activate or buy a license; details remain in the tray menu."
"Adds a Raw Input mouse plus Xbox One/Series, DualSense (DS5), Switch Pro, and Generic gamepads. Actively maintained by LizardByte. Click to activate or buy a license; details remain in the tray menu."
);
EXPECT_STREQ(tray_data.notification_icon, tray_data.allIconPaths[4]);
EXPECT_NE(tray_data.notification_cb, nullptr);
▸
third-party/libvirtualhid
+0
−0