Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,10 @@ and enter its device name in the [audio_sink](configuration.md#audio_sink) field
### 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
DualShock 4 gamepads when libvirtualhid is unavailable.
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.

Sunshine requires Virtual HID Driver version `2026.823.352.3` or newer. Earlier releases use incompatible Windows
Sunshine requires Virtual HID Driver version `2026.826.2024.22` 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.

Expand All @@ -488,15 +488,21 @@ Pro, and Generic gamepads in addition to Xbox 360 and DualShock 4. It can also e
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 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.
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
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.

Expand Down
26 changes: 18 additions & 8 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,11 @@ launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist
### 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 mouse and full virtual gamepad support. ViGEmBus is detected only as a limited fallback for Xbox 360 and
DualShock 4 gamepads when libvirtualhid is unavailable. If you use the
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. 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.826.2024.22` 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.

Expand All @@ -305,21 +305,31 @@ controller features such as motion, touchpads, LEDs, and adaptive triggers when
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
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
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
Expand Down
4 changes: 2 additions & 2 deletions src/confighttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ namespace confighttp {
*/
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.826.2024.22"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

/**
Expand Down Expand Up @@ -1958,7 +1958,7 @@ namespace confighttp {
#endif
#ifdef _WIN32
if (result.status.ok()) {
input::refresh_virtual_mouse();
input::refresh_virtual_input();
}
#endif
send_response(response, build_virtualhid_license_status(result));
Expand Down
9 changes: 7 additions & 2 deletions src/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2091,11 +2091,16 @@ namespace input {
});
}

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();
platf::virtualhid::get_input_context(platf_input).refresh_mouse();
reset_keyboard_keys();
auto &context = platf::virtualhid::get_input_context(platf_input);
context.refresh_keyboard();
context.refresh_mouse();
}
});
}
Expand Down
6 changes: 3 additions & 3 deletions src/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ namespace input {
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_mouse();
void refresh_virtual_input();

/**
* @brief Allocate and initialize platform input state for a stream.
Expand Down
22 changes: 22 additions & 0 deletions src/platform/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ namespace platf {
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
};

Expand Down Expand Up @@ -189,6 +190,22 @@ namespace platf {
return msg;
}

/**
* @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.
*
Expand Down Expand Up @@ -233,6 +250,11 @@ namespace platf {
std::uint8_t b;
} rgb_led;

struct {
std::uint8_t solid;
std::uint8_t flashing;
} player_leds;

struct {
uint16_t controllerNumber;
uint8_t event_flags;
Expand Down
63 changes: 51 additions & 12 deletions src/platform/virtualhid_input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ namespace platf::virtualhid {
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 {
Expand Down Expand Up @@ -316,6 +319,22 @@ namespace platf::virtualhid {
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) {
using enum lvh::PenToolType;

Expand Down Expand Up @@ -390,6 +409,19 @@ namespace platf::virtualhid {
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;
Expand Down Expand Up @@ -421,21 +453,27 @@ namespace platf::virtualhid {
return;
}

const auto &capabilities = runtime->capabilities();
if (capabilities.supports_keyboard) {
lvh::CreateKeyboardOptions options;
options.profile = lvh::profiles::keyboard();
options.stable_id = "sunshine-keyboard";
auto created = runtime->create_keyboard(options);
if (created) {
keyboard = std::move(created.keyboard);
} else {
log_failure("create libvirtualhid keyboard"sv, created.status);
}
}
refresh_keyboard();
refresh_mouse();
}

void input_context_t::refresh_keyboard() {
keyboard.reset();
if (!runtime || !runtime->capabilities().supports_keyboard) {
return;
}

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() {
mouse.reset();
if (!runtime || !runtime->capabilities().supports_mouse) {
Expand Down Expand Up @@ -589,6 +627,7 @@ namespace platf::virtualhid {
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));
Expand Down
5 changes: 5 additions & 0 deletions src/platform/virtualhid_input.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ namespace platf::virtualhid {
*/
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.
*/
Expand Down
29 changes: 29 additions & 0 deletions src/stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ constexpr int IDX_RUMBLE_TRIGGER_DATA = 12; ///< Control-stream message index f
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
Expand All @@ -67,6 +68,7 @@ static const short packetTypes[] = {
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;
Expand Down Expand Up @@ -239,6 +241,17 @@ namespace stream {
std::uint8_t b; ///< Blue LED channel.
};

/**
* @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.
*/
Expand Down Expand Up @@ -1034,6 +1047,22 @@ namespace stream {
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;

payload = encode_control(session, util::view(plaintext), encrypted_payload);
} else if (msg.type == platf::gamepad_feedback_e::set_adaptive_triggers) {
control_adaptive_triggers_t plaintext;
Expand Down
Loading
Loading