diff --git a/README.md b/README.md index 2ffd99b7aa3..1c3a3e3258e 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,13 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz ✅2 ✅3 + + Steam Controller (2nd generation) + 🟡1 + ✅ + ❌ + ✅ + Xbox 360 🟡1 diff --git a/docs/configuration.md b/docs/configuration.md index 6d51304d62b..8ae6d1f14a8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -383,7 +383,7 @@ supported on the current platform. @endcode - Choices + Choices generic Generic HID gamepad @@ -399,6 +399,10 @@ supported on the current platform. switch Switch Pro controller + + steam_triton + Steam Controller (2nd generation) + x360 Xbox 360 controller diff --git a/docs/getting_started.md b/docs/getting_started.md index 4f583676956..e8ce2860c3c 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -414,6 +414,12 @@ keyboard or mouse input. Sunshine’s menu bar **Virtual HID Broker** submenu sh 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. +The `steam_triton` option requires a Virtual HID Broker build with support for the Steam Controller +(2nd generation). It creates a native Steam Controller profile on macOS; +Sunshine forwards its pads, rear buttons, motion, battery state, and feedback +through the same client protocol used on Windows and Linux. The macOS path +still needs a signed-broker, physical-controller test. + #### DMG ##### Install @@ -587,10 +593,15 @@ gamepad support. ViGEmBus remains available as a limited alternative for Xbox 36 When Virtual HID Broker is used, Sunshine requires libvirtualhid version `2026.914.1218.10` or newer. -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 Broker is actively developed and -supported by the LizardByte team. +Compared with the ViGEmBus fallback, Virtual HID Broker can create the Steam Controller (2nd generation), Xbox One, +Xbox Series, DualSense, Nintendo Switch Pro, and Generic gamepads in addition to Xbox 360 and DualShock 4. The +`steam_triton` selection specifically emulates the Steam Controller (2nd generation), not the original Steam Controller. It exposes the standard controls, +Home and miscellaneous buttons, four rear buttons, motion, battery state, both trackpads, and separate clicks for both +trackpads when the Moonlight client reports those capabilities. Standard rumble and the controller's independently +addressable trackpad haptics are returned to capable Moonlight clients. Older clients retain pressure- and +trigger-position-based click inference and do not receive addressable haptic effects. Virtual HID Broker +also exposes controller-specific features such as LEDs and adaptive triggers for other profiles when supported, and 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 applications using Raw Input can receive them. Unicode text input and keys outside the supported HID keyboard page diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 889bea61639..81e537d161f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -318,6 +318,12 @@ and the broker or license is unavailable. Choose **None** under **Configuration virtual gamepads and that notice without affecting keyboard or mouse input. Reconnect the Moonlight session after choosing a different emulated gamepad profile. +For `steam_triton`, use a broker build that includes the Steam Controller (2nd generation) profile. +Older macOS broker builds reject that profile during creation. The +new macOS path still needs validation with a signed broker and physical +controller; if it fails, check the broker version and permission before +changing Moonlight's gamepad mapping. + ### Dynamic session lookup failed If you get this error: @@ -340,9 +346,13 @@ gamepad support. ViGEmBus is a limited alternative for Xbox 360 and DualShock 4 When Virtual HID Broker is used, Sunshine requires libvirtualhid version `2026.914.1218.10` or newer. -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 Broker is actively developed and supported by the LizardByte team. +Virtual HID Broker adds the Steam Controller (2nd generation), 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. The +`steam_triton` option is exclusively for the Steam Controller (2nd generation), not the original Steam Controller. It requires a +compatible Virtual HID Broker and cannot fall back to ViGEmBus. Native trackpad clicks, trigger clicks, stick and grip +touch sensors, exact battery reporting, and addressable trackpad haptics also require a capable Moonlight client; older +clients use Sunshine's pressure and trigger-position click fallback. Unlike the discontinued ViGEmBus project, Virtual +HID Broker is actively developed and supported by the LizardByte team. 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 diff --git a/gh-pages-template/_data/features.yml b/gh-pages-template/_data/features.yml index 067bdcd7a2f..1012c8274b5 100644 --- a/gh-pages-template/_data/features.yml +++ b/gh-pages-template/_data/features.yml @@ -27,8 +27,8 @@ - title: "Control" icon_fa: "fas fa-gamepad" description: > - Sunshine emulates Xbox 360|One|Series, PlayStation DualShock 4|DualSense, or Nintendo Switch Pro controllers. - Use nearly any controller on your Moonlight client! + Sunshine emulates Steam Controller (2nd Generation), Xbox 360|One|Series, PlayStation DualShock 4|DualSense, + or Nintendo Switch Pro controllers. Use nearly any controller on your Moonlight client! - title: "Configurable" icon_fa: "fas fa-gear" diff --git a/src/input.cpp b/src/input.cpp index b30352205b8..6c3ad23ca5c 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -1505,6 +1505,7 @@ namespace input { from_clamped_netfloat(packet->x, 0.0f, 1.0f), from_clamped_netfloat(packet->y, 0.0f, 1.0f), from_clamped_netfloat(packet->pressure, 0.0f, 1.0f), + packet->touchpadIndex, }; platf::gamepad_touch(platf_input, touch); @@ -2474,6 +2475,28 @@ namespace input { ::input::passthrough(input, &packet); } + void send_controller_touch_packet( + std::shared_ptr &input, + std::uint8_t controller_number, + std::uint8_t event_type, + std::uint8_t touchpad_index, + std::uint32_t pointer_id, + float x, + float y, + float pressure + ) { + SS_CONTROLLER_TOUCH_PACKET packet {}; + packet.controllerNumber = controller_number; + packet.eventType = event_type; + packet.touchpadIndex = touchpad_index; + packet.pointerId = util::endian::little(pointer_id); + boost::endian::endian_store(packet.x, x); + boost::endian::endian_store(packet.y, y); + boost::endian::endian_store(packet.pressure, pressure); + + ::input::passthrough(input, &packet); + } + void reset_keyboard_state() { task_pool.cancel(key_press_repeat_id); key_press_repeat_id = nullptr; diff --git a/src/input.h b/src/input.h index 6d78447e53b..7948c26a16d 100644 --- a/src/input.h +++ b/src/input.h @@ -163,6 +163,29 @@ namespace input { */ void send_keyboard_packet(std::shared_ptr &input, std::uint16_t key_code, std::uint8_t modifiers, std::uint8_t flags, bool release); + /** + * @brief Process one client controller-touch packet on the calling thread. + * + * @param input Retained input state. + * @param controller_number Client-relative controller index. + * @param event_type Moonlight touch event type. + * @param touchpad_index Zero-based touchpad index carried by the packet. + * @param pointer_id Client-provided contact identifier. + * @param x Normalized horizontal coordinate. + * @param y Normalized vertical coordinate. + * @param pressure Normalized contact pressure. + */ + void send_controller_touch_packet( + std::shared_ptr &input, + std::uint8_t controller_number, + std::uint8_t event_type, + std::uint8_t touchpad_index, + std::uint32_t pointer_id, + float x, + float y, + float pressure + ); + /** * @brief Forget every key Sunshine tracks as pressed and cancel any pending key repeat. */ diff --git a/src/platform/common.h b/src/platform/common.h index e8aff70ec2c..9419a797ad6 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -100,6 +100,14 @@ namespace platf { constexpr std::uint32_t PADDLE4 = 0x080000; ///< Moonlight gamepad button mask bit for paddle 4. constexpr std::uint32_t TOUCHPAD_BUTTON = 0x100000; ///< Moonlight gamepad button mask bit for touchpad click. constexpr std::uint32_t MISC_BUTTON = 0x200000; ///< Moonlight gamepad button mask bit for the miscellaneous button. + constexpr std::uint32_t STEAM_LEFT_TOUCHPAD_BUTTON = STEAM_LEFT_TOUCHPAD_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) left trackpad click. + constexpr std::uint32_t STEAM_RIGHT_TOUCHPAD_BUTTON = STEAM_RIGHT_TOUCHPAD_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) right trackpad click. + constexpr std::uint32_t STEAM_LEFT_TRIGGER_CLICK = STEAM_LEFT_TRIGGER_CLICK_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) left trigger click. + constexpr std::uint32_t STEAM_RIGHT_TRIGGER_CLICK = STEAM_RIGHT_TRIGGER_CLICK_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) right trigger click. + constexpr std::uint32_t STEAM_LEFT_STICK_TOUCH = STEAM_LEFT_STICK_TOUCH_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) left stick touch sensor. + constexpr std::uint32_t STEAM_RIGHT_STICK_TOUCH = STEAM_RIGHT_STICK_TOUCH_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) right stick touch sensor. + constexpr std::uint32_t STEAM_LEFT_GRIP_TOUCH = STEAM_LEFT_GRIP_TOUCH_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) left grip touch sensor. + constexpr std::uint32_t STEAM_RIGHT_GRIP_TOUCH = STEAM_RIGHT_GRIP_TOUCH_FLAG; ///< Moonlight bit for the Steam Controller (2nd generation) right grip touch sensor. /** * @brief Gamepad type exposed to clients and why it may be disabled. @@ -120,6 +128,26 @@ namespace platf { set_rgb_led, ///< Set RGB LED set_player_leds, ///< Set player indicator LEDs set_adaptive_triggers, ///< Set adaptive triggers + set_haptics, ///< Play an addressable haptic effect + }; + + /** + * @brief Profile-neutral addressable gamepad haptic effect. + */ + struct gamepad_haptic_effect_t { + std::uint8_t target; ///< Target actuator selection. + std::uint8_t kind; ///< Haptic effect category. + std::int8_t gain_db; ///< Signed gain in decibels. + std::uint16_t intensity; ///< Profile-defined intensity value. + std::uint16_t frequency_hz; ///< Primary tone frequency in hertz. + std::int32_t duration_us; ///< Effect duration in microseconds; negative means indefinite. + std::uint32_t interval_us; ///< Off interval between pulses in microseconds. + std::uint16_t repeat_count; ///< Pulse repeat count. + std::uint16_t lfo_frequency_hz; ///< Low-frequency oscillator frequency in hertz. + std::uint8_t lfo_depth_percent; ///< Low-frequency oscillator depth in percent. + std::uint16_t start_frequency_hz; ///< Sweep start frequency in hertz. + std::uint16_t end_frequency_hz; ///< Sweep end frequency in hertz. + std::uint8_t script_id; ///< Controller-defined scripted effect identifier. }; /** @@ -227,6 +255,21 @@ namespace platf { return msg; } + /** + * @brief Create an addressable haptic effect command. + * + * @param id Identifier for the controller. + * @param effect Profile-neutral haptic effect parameters. + * @return Constructed haptic effect command. + */ + static gamepad_feedback_msg_t make_haptics(std::uint16_t id, const gamepad_haptic_effect_t &effect) { + gamepad_feedback_msg_t msg; + msg.type = gamepad_feedback_e::set_haptics; + msg.id = id; + msg.data.haptics = effect; + return msg; + } + gamepad_feedback_e type; ///< Feedback command type stored in the union payload. std::uint16_t id; ///< Controller identifier associated with this message. @@ -264,6 +307,8 @@ namespace platf { std::array left; ///< Left adaptive-trigger effect parameters. std::array right; ///< Right adaptive-trigger effect parameters. } adaptive_triggers; ///< Adaptive-trigger effect payload. + + gamepad_haptic_effect_t haptics; ///< Addressable haptic effect payload. } data; ///< Controller feedback payload for the selected feedback type. }; @@ -454,6 +499,7 @@ namespace platf { float x; ///< Horizontal coordinate or vector component. float y; ///< Vertical coordinate or vector component. float pressure; ///< Contact pressure reported by the client. + std::uint8_t touchpadIndex = 0; ///< Zero-based Moonlight touchpad index. }; /** diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp index db710dae6d4..ae4eb371685 100644 --- a/src/platform/virtualhid_input.cpp +++ b/src/platform/virtualhid_input.cpp @@ -27,14 +27,9 @@ using namespace std::literals; namespace platf::virtualhid { /** - * @brief Runtime state for one virtual gamepad. + * @brief Last feedback values forwarded to the current client session. */ - struct gamepad_context_t { - std::unique_ptr adapter; ///< State adapter for the virtual gamepad. - std::mutex feedback_mutex; ///< Synchronizes feedback routing with session rebinding. - feedback_queue_t feedback_queue; ///< Feedback queue for client output events. - std::array, 2> touch_ids; ///< Client touch IDs assigned to libvirtualhid slots. - std::uint8_t client_relative_index = 0; ///< Client-relative controller index. + struct feedback_cache_t { bool has_last_rumble = false; ///< Whether last rumble values are valid. std::uint16_t last_low_frequency_rumble = 0; ///< Last low-frequency rumble value. std::uint16_t last_high_frequency_rumble = 0; ///< Last high-frequency rumble value. @@ -50,8 +45,33 @@ namespace platf::virtualhid { std::uint8_t last_flashing_player_leds = 0; ///< Last flashing player indicator mask. }; + /** + * @brief Runtime state for one virtual gamepad. + */ + struct gamepad_context_t { + std::unique_ptr adapter; ///< State adapter for the virtual gamepad. + std::mutex feedback_mutex; ///< Synchronizes feedback routing with session rebinding. + feedback_queue_t feedback_queue; ///< Feedback queue for client output events. + std::array, 2> touch_ids; ///< Client touch IDs assigned to libvirtualhid slots. + std::array inferred_touchpad_clicks {}; ///< Steam touchpad clicks inferred from client pressure. + std::array inferred_trigger_clicks {}; ///< Steam trigger clicks inferred from client axes. + std::uint32_t client_button_flags = 0; ///< Latest client button state, before Steam click inference. + std::uint32_t supported_button_flags = 0; ///< Button flags the client can report natively. + std::uint8_t client_relative_index = 0; ///< Client-relative controller index. + bool supports_haptics = false; ///< Whether the client can play addressable haptic effects. + feedback_cache_t feedback_cache; ///< Feedback values already sent to this client session. + }; + namespace { + // Moonlight currently carries Steam trackpad pressure and analog trigger + // travel but not all native click bits. Keep this compatibility fallback + // specific to the Steam Controller (2nd generation) profile. + constexpr float steam_touchpad_click_press_threshold = 0.35F; + constexpr float steam_touchpad_click_release_threshold = 0.25F; + constexpr float steam_trigger_click_press_threshold = 0.95F; + constexpr float steam_trigger_click_release_threshold = 0.90F; + /** * @brief Gamepad profile exposed through Sunshine config. */ @@ -72,6 +92,7 @@ namespace platf::virtualhid { gamepad_profile_t {"ds4", lvh::GamepadProfileKind::dualshock4, lvh::profiles::dualshock4}, gamepad_profile_t {"ds5", lvh::GamepadProfileKind::dualsense, lvh::profiles::dualsense}, gamepad_profile_t {"switch", lvh::GamepadProfileKind::switch_pro, lvh::profiles::switch_pro}, + gamepad_profile_t {"steam_triton", lvh::GamepadProfileKind::steam_triton, lvh::profiles::steam_triton}, }; void log_failure(std::string_view operation, const lvh::OperationStatus &status) { @@ -121,6 +142,10 @@ namespace platf::virtualhid { return profile_for_name(config::input.gamepad); } + if (metadata.type == LI_CTYPE_STEAM) { + BOOST_LOG(info) << "Gamepad will be Steam Controller (2nd generation), auto-selected by client-reported type"sv; + return profile_for_name("steam_triton"sv); + } if (metadata.type == LI_CTYPE_PS) { BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by client-reported type)"sv; return profile_for_name("ds5"sv); @@ -133,6 +158,10 @@ namespace platf::virtualhid { BOOST_LOG(info) << "Gamepad will be Xbox Series controller (auto-selected by client-reported type)"sv; return profile_for_name("xseries"sv); } + if (metadata.capabilities & LI_CCAP_DUAL_TOUCHPAD) { + BOOST_LOG(info) << "Gamepad will be Steam Controller (2nd generation), auto-selected by dual-touchpad capability"sv; + return profile_for_name("steam_triton"sv); + } if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(info) << "Gamepad will be DualSense controller (auto-selected by motion sensor presence)"sv; return profile_for_name("ds5"sv); @@ -175,7 +204,7 @@ namespace platf::virtualhid { result.client_relative_index = id.clientRelativeIndex; result.client_type = client_controller_type(metadata.type); result.has_motion_sensors = metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO); - result.has_touchpad = metadata.capabilities & LI_CCAP_TOUCHPAD; + result.has_touchpad = metadata.capabilities & (LI_CCAP_TOUCHPAD | LI_CCAP_DUAL_TOUCHPAD); result.has_rgb_led = metadata.capabilities & LI_CCAP_RGB_LED; result.has_battery = metadata.capabilities & LI_CCAP_BATTERY_STATE; result.stable_id = gamepad_stable_id(id, profile); @@ -186,26 +215,36 @@ namespace platf::virtualhid { if ((metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) && !support.supports_motion) { BOOST_LOG(warning) << "Gamepad "sv << global_index << " has motion sensors, but the selected virtual profile cannot expose them"sv; } - if ((metadata.capabilities & LI_CCAP_TOUCHPAD) && !support.supports_touchpad) { + if ((metadata.capabilities & (LI_CCAP_TOUCHPAD | LI_CCAP_DUAL_TOUCHPAD)) && !support.supports_touchpad) { BOOST_LOG(warning) << "Gamepad "sv << global_index << " has a touchpad, but the selected virtual profile cannot expose it"sv; } if ((metadata.capabilities & LI_CCAP_RGB_LED) && !support.supports_rgb_led) { BOOST_LOG(warning) << "Gamepad "sv << global_index << " has an RGB LED, but the selected virtual profile cannot expose it"sv; } + if ((metadata.capabilities & LI_CCAP_HAPTICS) && !support.supports_haptics) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " has addressable haptics, but the selected virtual profile cannot expose them"sv; + } } void warn_missing_client_features(int global_index, const gamepad_arrival_t &metadata, const lvh::GamepadProfileSupport &support) { if (support.supports_motion && !(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating a motion-capable controller, but the client gamepad does not have motion sensors active"sv; } - if (support.supports_touchpad && !(metadata.capabilities & LI_CCAP_TOUCHPAD)) { + if (support.supports_touchpad && !(metadata.capabilities & (LI_CCAP_TOUCHPAD | LI_CCAP_DUAL_TOUCHPAD))) { BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating a touchpad-capable controller, but the client gamepad does not have a touchpad"sv; } + if (support.supported_touchpad_count > 1U && !(metadata.capabilities & LI_CCAP_DUAL_TOUCHPAD)) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating the Steam Controller (2nd generation), but the client cannot report its second touchpad"sv; + } + if (support.supports_haptics && !(metadata.capabilities & LI_CCAP_HAPTICS)) { + BOOST_LOG(warning) << "Gamepad "sv << global_index << " is emulating addressable haptics, but the client cannot play them"sv; + } } - lvh::GamepadState make_gamepad_state(const gamepad_state_t &state, const lvh::GamepadProfileSupport &support) { + lvh::GamepadState make_gamepad_state(const gamepad_state_t &state, const lvh::DeviceProfile &profile) { using enum lvh::GamepadButton; + const auto support = lvh::gamepad_profile_support(profile); lvh::GamepadState result; const auto flags = state.buttonFlags; @@ -226,6 +265,21 @@ namespace platf::virtualhid { result.buttons.set(y, flags & Y); result.buttons.set(misc1, support.supports_misc1_button && (flags & MISC_BUTTON)); result.buttons.set(touchpad, support.supports_touchpad_button && (flags & TOUCHPAD_BUTTON)); + result.buttons.set(paddle1, support.supported_rear_paddle_count >= 1U && (flags & PADDLE1)); + result.buttons.set(paddle2, support.supported_rear_paddle_count >= 2U && (flags & PADDLE2)); + result.buttons.set(paddle3, support.supported_rear_paddle_count >= 3U && (flags & PADDLE3)); + result.buttons.set(paddle4, support.supported_rear_paddle_count >= 4U && (flags & PADDLE4)); + + if (profile.gamepad_kind == lvh::GamepadProfileKind::steam_triton) { + result.buttons.set(left_touchpad, flags & STEAM_LEFT_TOUCHPAD_BUTTON); + result.buttons.set(right_touchpad, flags & STEAM_RIGHT_TOUCHPAD_BUTTON); + result.buttons.set(left_trigger_click, flags & STEAM_LEFT_TRIGGER_CLICK); + result.buttons.set(right_trigger_click, flags & STEAM_RIGHT_TRIGGER_CLICK); + result.buttons.set(left_stick_touch, flags & STEAM_LEFT_STICK_TOUCH); + result.buttons.set(right_stick_touch, flags & STEAM_RIGHT_STICK_TOUCH); + result.buttons.set(left_grip_touch, flags & STEAM_LEFT_GRIP_TOUCH); + result.buttons.set(right_grip_touch, flags & STEAM_RIGHT_GRIP_TOUCH); + } if (support.supports_touchpad_button && config::input.ds4_back_as_touchpad_click && configured_gamepad_supports_touchpad() && (flags & BACK)) { result.buttons.set(touchpad); @@ -238,6 +292,84 @@ namespace platf::virtualhid { return result; } + bool threshold_button_state(bool pressed, float value, float press_threshold, float release_threshold) { + return pressed ? value > release_threshold : value >= press_threshold; + } + + bool is_steam_triton(const std::shared_ptr &gamepad) { + return gamepad->adapter->gamepad()->profile().gamepad_kind == lvh::GamepadProfileKind::steam_triton; + } + + void apply_inferred_steam_clicks(const std::shared_ptr &gamepad, lvh::GamepadState &state) { + if (!is_steam_triton(gamepad)) { + return; + } + + using enum lvh::GamepadButton; + state.buttons.set(left_touchpad, state.buttons.test(left_touchpad) || gamepad->inferred_touchpad_clicks[0]); + state.buttons.set(right_touchpad, state.buttons.test(right_touchpad) || gamepad->inferred_touchpad_clicks[1]); + state.buttons.set(left_trigger_click, state.buttons.test(left_trigger_click) || gamepad->inferred_trigger_clicks[0]); + state.buttons.set(right_trigger_click, state.buttons.test(right_trigger_click) || gamepad->inferred_trigger_clicks[1]); + } + + void update_inferred_steam_trigger_clicks( + const std::shared_ptr &gamepad, + float left_trigger, + float right_trigger + ) { + if (!is_steam_triton(gamepad)) { + return; + } + + gamepad->inferred_trigger_clicks[0] = threshold_button_state( + gamepad->inferred_trigger_clicks[0], + left_trigger, + steam_trigger_click_press_threshold, + steam_trigger_click_release_threshold + ); + gamepad->inferred_trigger_clicks[1] = threshold_button_state( + gamepad->inferred_trigger_clicks[1], + right_trigger, + steam_trigger_click_press_threshold, + steam_trigger_click_release_threshold + ); + if (gamepad->supported_button_flags & STEAM_LEFT_TRIGGER_CLICK) { + gamepad->inferred_trigger_clicks[0] = false; + } + if (gamepad->supported_button_flags & STEAM_RIGHT_TRIGGER_CLICK) { + gamepad->inferred_trigger_clicks[1] = false; + } + } + + void update_inferred_steam_touchpad_click( + const std::shared_ptr &gamepad, + std::size_t index, + bool active, + float pressure + ) { + if (!is_steam_triton(gamepad) || index >= gamepad->inferred_touchpad_clicks.size()) { + return; + } + + auto &inferred = gamepad->inferred_touchpad_clicks[index]; + inferred = active && threshold_button_state( + inferred, + pressure, + steam_touchpad_click_press_threshold, + steam_touchpad_click_release_threshold + ); + + const auto explicit_flag = index == 0U ? STEAM_LEFT_TOUCHPAD_BUTTON : STEAM_RIGHT_TOUCHPAD_BUTTON; + if (gamepad->supported_button_flags & explicit_flag) { + inferred = false; + } + const auto button = index == 0U ? lvh::GamepadButton::left_touchpad : lvh::GamepadButton::right_touchpad; + log_failure( + "submit inferred Steam touchpad click"sv, + gamepad->adapter->set_button(button, inferred || (gamepad->client_button_flags & explicit_flag)) + ); + } + std::optional mouse_button(int button) { using enum lvh::MouseButton; @@ -382,54 +514,147 @@ namespace platf::virtualhid { std::lock_guard lock {gamepad->feedback_mutex}; switch (output.kind) { case lvh::GamepadOutputKind::rumble: - if (gamepad->has_last_rumble && gamepad->last_low_frequency_rumble == output.low_frequency_rumble && gamepad->last_high_frequency_rumble == output.high_frequency_rumble) { + if (gamepad->feedback_cache.has_last_rumble && gamepad->feedback_cache.last_low_frequency_rumble == output.low_frequency_rumble && gamepad->feedback_cache.last_high_frequency_rumble == output.high_frequency_rumble) { return; } - gamepad->has_last_rumble = true; - gamepad->last_low_frequency_rumble = output.low_frequency_rumble; - gamepad->last_high_frequency_rumble = output.high_frequency_rumble; + gamepad->feedback_cache.has_last_rumble = true; + gamepad->feedback_cache.last_low_frequency_rumble = output.low_frequency_rumble; + gamepad->feedback_cache.last_high_frequency_rumble = output.high_frequency_rumble; raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_rumble(gamepad->client_relative_index, output.low_frequency_rumble, output.high_frequency_rumble)); break; case lvh::GamepadOutputKind::trigger_rumble: - if (gamepad->has_last_trigger_rumble && gamepad->last_left_trigger_rumble == output.left_trigger_rumble && gamepad->last_right_trigger_rumble == output.right_trigger_rumble) { + if (gamepad->feedback_cache.has_last_trigger_rumble && gamepad->feedback_cache.last_left_trigger_rumble == output.left_trigger_rumble && gamepad->feedback_cache.last_right_trigger_rumble == output.right_trigger_rumble) { return; } - gamepad->has_last_trigger_rumble = true; - gamepad->last_left_trigger_rumble = output.left_trigger_rumble; - gamepad->last_right_trigger_rumble = output.right_trigger_rumble; + gamepad->feedback_cache.has_last_trigger_rumble = true; + gamepad->feedback_cache.last_left_trigger_rumble = output.left_trigger_rumble; + gamepad->feedback_cache.last_right_trigger_rumble = output.right_trigger_rumble; raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_rumble_triggers(gamepad->client_relative_index, output.left_trigger_rumble, output.right_trigger_rumble)); break; case lvh::GamepadOutputKind::rgb_led: - if (gamepad->has_last_rgb && gamepad->last_red == output.red && gamepad->last_green == output.green && gamepad->last_blue == output.blue) { + if (gamepad->feedback_cache.has_last_rgb && gamepad->feedback_cache.last_red == output.red && gamepad->feedback_cache.last_green == output.green && gamepad->feedback_cache.last_blue == output.blue) { return; } - gamepad->has_last_rgb = true; - gamepad->last_red = output.red; - gamepad->last_green = output.green; - gamepad->last_blue = output.blue; + gamepad->feedback_cache.has_last_rgb = true; + gamepad->feedback_cache.last_red = output.red; + gamepad->feedback_cache.last_green = output.green; + gamepad->feedback_cache.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) { + if (gamepad->feedback_cache.has_last_player_leds && gamepad->feedback_cache.last_solid_player_leds == solid && gamepad->feedback_cache.last_flashing_player_leds == flashing) { return; } - gamepad->has_last_player_leds = true; - gamepad->last_solid_player_leds = solid; - gamepad->last_flashing_player_leds = flashing; + gamepad->feedback_cache.has_last_player_leds = true; + gamepad->feedback_cache.last_solid_player_leds = solid; + gamepad->feedback_cache.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; + case lvh::GamepadOutputKind::haptics: + if (gamepad->supports_haptics && output.haptic_effect) { + const auto &effect = *output.haptic_effect; + raise_feedback_unlocked( + gamepad, + gamepad_feedback_msg_t::make_haptics( + gamepad->client_relative_index, + { + .target = std::to_underlying(effect.target), + .kind = std::to_underlying(effect.kind), + .gain_db = effect.gain_db, + .intensity = effect.intensity, + .frequency_hz = effect.frequency_hz, + .duration_us = effect.duration_us, + .interval_us = effect.interval_us, + .repeat_count = effect.repeat_count, + .lfo_frequency_hz = effect.lfo_frequency_hz, + .lfo_depth_percent = effect.lfo_depth_percent, + .start_frequency_hz = effect.start_frequency_hz, + .end_frequency_hz = effect.end_frequency_hz, + .script_id = effect.script_id, + } + ) + ); + } + break; case lvh::GamepadOutputKind::raw_report: break; } } + void cancel_all_gamepad_touches(const std::shared_ptr &gamepad) { + for (std::size_t index = 0; index < gamepad->touch_ids.size(); ++index) { + update_inferred_steam_touchpad_click(gamepad, index, false, 0.0F); + if (gamepad->touch_ids[index].has_value()) { + log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(index)); + gamepad->touch_ids[index].reset(); + } + } + } + + std::optional indexed_touch_slot( + const std::shared_ptr &gamepad, + const gamepad_touch_t &touch, + const lvh::GamepadProfileSupport &support + ) { + if (touch.touchpadIndex >= support.supported_touchpad_count || touch.touchpadIndex >= gamepad->touch_ids.size()) { + BOOST_LOG(warning) << "Invalid libvirtualhid gamepad touchpad index: "sv << static_cast(touch.touchpadIndex); + return std::nullopt; + } + + auto &slot = gamepad->touch_ids[touch.touchpadIndex]; + if (touch.eventType == LI_TOUCH_EVENT_DOWN) { + if (slot.has_value() && slot.value() != touch.pointerId) { + BOOST_LOG(warning) << "No free libvirtualhid gamepad touch slot for touchpad "sv << static_cast(touch.touchpadIndex); + return std::nullopt; + } + slot = touch.pointerId; + } else if (!slot.has_value() || slot.value() != touch.pointerId) { + return std::nullopt; + } + + return touch.touchpadIndex; + } + + std::optional shared_touch_slot( + const std::shared_ptr &gamepad, + const gamepad_touch_t &touch + ) { + auto slot = std::ranges::find(gamepad->touch_ids, touch.pointerId); + if (touch.eventType == LI_TOUCH_EVENT_DOWN && slot == gamepad->touch_ids.end()) { + slot = std::ranges::find_if(gamepad->touch_ids, [](const auto &id) { + return !id.has_value(); + }); + if (slot == gamepad->touch_ids.end()) { + BOOST_LOG(warning) << "No free libvirtualhid gamepad touch slots"sv; + return std::nullopt; + } + *slot = touch.pointerId; + } + + if (slot == gamepad->touch_ids.end()) { + return std::nullopt; + } + return static_cast(std::distance(gamepad->touch_ids.begin(), slot)); + } + + std::optional gamepad_touch_slot( + const std::shared_ptr &gamepad, + const gamepad_touch_t &touch, + const lvh::GamepadProfileSupport &support + ) { + if (support.supported_touchpad_count > 1U) { + return indexed_touch_slot(gamepad, touch, support); + } + return shared_touch_slot(gamepad, touch); + } + void cancel_all_touches(client_context_t &context) { if (!context.touch) { return; @@ -614,6 +839,9 @@ namespace platf::virtualhid { const auto &selection = profile_for_metadata(metadata); auto profile = selection.profile(); + if (selection.kind == lvh::GamepadProfileKind::steam_triton) { + profile.name = "Steam Controller (2nd generation)"; + } profile.name = std::format("Sunshine {}", profile.name); if (config::input.gamepad != "auto"sv) { BOOST_LOG(info) << "Gamepad "sv << id.globalIndex << " will be "sv << profile.name << " (manual selection)"sv; @@ -633,7 +861,9 @@ namespace platf::virtualhid { auto gamepad = std::make_shared(); gamepad->adapter = std::move(created.adapter); gamepad->feedback_queue = std::move(feedback_queue); + gamepad->supported_button_flags = metadata.supportedButtons; gamepad->client_relative_index = id.clientRelativeIndex; + gamepad->supports_haptics = metadata.capabilities & LI_CCAP_HAPTICS; gamepad->adapter->set_output_callback([weak_gamepad = std::weak_ptr {gamepad}](const lvh::GamepadOutput &output) { if (const auto gamepad = weak_gamepad.lock()) { handle_output(gamepad, output); @@ -661,10 +891,7 @@ namespace platf::virtualhid { std::lock_guard lock {gamepad->feedback_mutex}; gamepad->feedback_queue = std::move(feedback_queue); gamepad->client_relative_index = id.clientRelativeIndex; - gamepad->has_last_rumble = false; - gamepad->has_last_trigger_rumble = false; - gamepad->has_last_rgb = false; - gamepad->has_last_player_leds = false; + gamepad->feedback_cache = {}; 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)); @@ -696,7 +923,10 @@ namespace platf::virtualhid { } auto &gamepad = context.gamepads[nr]; - auto updated_state = make_gamepad_state(state, gamepad->adapter->support()); + gamepad->client_button_flags = state.buttonFlags; + auto updated_state = make_gamepad_state(state, gamepad->adapter->gamepad()->profile()); + update_inferred_steam_trigger_clicks(gamepad, updated_state.left_trigger, updated_state.right_trigger); + apply_inferred_steam_clicks(gamepad, updated_state); const auto &cached_state = gamepad->adapter->state(); updated_state.acceleration = cached_state.acceleration; updated_state.gyroscope = cached_state.gyroscope; @@ -711,40 +941,25 @@ namespace platf::virtualhid { } auto &gamepad = context.gamepads[touch.id.globalIndex]; - if (!gamepad->adapter->support().supports_touchpad) { + const auto &support = gamepad->adapter->support(); + if (!support.supports_touchpad) { return; } if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { - for (std::size_t index = 0; index < gamepad->touch_ids.size(); ++index) { - if (gamepad->touch_ids[index].has_value()) { - log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(index)); - gamepad->touch_ids[index].reset(); - } - } + cancel_all_gamepad_touches(gamepad); return; } - auto slot = std::ranges::find(gamepad->touch_ids, touch.pointerId); - if (touch.eventType == LI_TOUCH_EVENT_DOWN && slot == gamepad->touch_ids.end()) { - slot = std::ranges::find_if(gamepad->touch_ids, [](const auto &id) { - return !id.has_value(); - }); - if (slot == gamepad->touch_ids.end()) { - BOOST_LOG(warning) << "No free libvirtualhid gamepad touch slots"sv; - return; - } - *slot = touch.pointerId; - } - - if (slot == gamepad->touch_ids.end()) { + const auto slot = gamepad_touch_slot(gamepad, touch, support); + if (!slot.has_value()) { return; } - const auto index = static_cast(std::distance(gamepad->touch_ids.begin(), slot)); if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) { - log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(index)); - slot->reset(); + update_inferred_steam_touchpad_click(gamepad, *slot, false, 0.0F); + log_failure("release libvirtualhid gamepad touch"sv, gamepad->adapter->clear_touchpad_contact(*slot)); + gamepad->touch_ids[*slot].reset(); return; } if (touch.eventType != LI_TOUCH_EVENT_DOWN && touch.eventType != LI_TOUCH_EVENT_MOVE) { @@ -752,11 +967,13 @@ namespace platf::virtualhid { } lvh::GamepadTouchContact contact; - contact.id = static_cast(index); - contact.active = touch.pressure > 0.5F; + contact.id = static_cast(*slot); + contact.active = true; contact.x = std::clamp(touch.x, 0.0F, 1.0F); contact.y = std::clamp(touch.y, 0.0F, 1.0F); - log_failure("submit libvirtualhid gamepad touch"sv, gamepad->adapter->set_touchpad_contact(index, contact)); + contact.pressure = std::clamp(touch.pressure, 0.0F, 1.0F); + log_failure("submit libvirtualhid gamepad touch"sv, gamepad->adapter->set_touchpad_contact(*slot, contact)); + update_inferred_steam_touchpad_click(gamepad, *slot, true, contact.pressure); } void gamepad_motion(input_context_t &context, const gamepad_motion_t &motion) { diff --git a/src/stream.cpp b/src/stream.cpp index 1291c9c625e..99a1416234e 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -53,6 +53,7 @@ constexpr int IDX_SET_MOTION_EVENT = 13; ///< Control-stream message index for 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. +constexpr int IDX_SET_CONTROLLER_HAPTICS = 17; ///< Control-stream message index for addressable controller haptics. static const short packetTypes[] = { 0x0305, // Start A @@ -72,6 +73,7 @@ static const short packetTypes[] = { 0x5502, // Set RGB LED (Sunshine protocol extension) 0x5503, // Set Adaptive triggers (Sunshine protocol extension) 0x5504, // Set player indicator LEDs (Sunshine protocol extension) + 0x5505, // Set controller haptics (Sunshine protocol extension) }; namespace asio = boost::asio; @@ -273,6 +275,28 @@ namespace stream { std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE]; ///< Right adaptive-trigger effect payload. }; + /** + * @brief Control payload that plays an addressable controller haptic effect. + */ + struct control_set_controller_haptics_t { + control_header_v2 header; ///< Control message header preceding this payload. + + std::uint16_t id; ///< Controller identifier associated with this message. + std::uint8_t target; ///< Target actuator selection. + std::uint8_t kind; ///< Haptic effect category. + std::int8_t gain_db; ///< Signed gain in decibels. + std::uint16_t intensity; ///< Profile-defined intensity value. + std::uint16_t frequency_hz; ///< Primary tone frequency in hertz. + std::uint32_t duration_us; ///< Signed effect duration encoded as a little-endian two's-complement value. + std::uint32_t interval_us; ///< Off interval between pulses in microseconds. + std::uint16_t repeat_count; ///< Pulse repeat count. + std::uint16_t lfo_frequency_hz; ///< Low-frequency oscillator frequency in hertz. + std::uint8_t lfo_depth_percent; ///< Low-frequency oscillator depth in percent. + std::uint16_t start_frequency_hz; ///< Sweep start frequency in hertz. + std::uint16_t end_frequency_hz; ///< Sweep end frequency in hertz. + std::uint8_t script_id; ///< Controller-defined scripted effect identifier. + }; + /** * @brief Control payload that toggles HDR mode and carries metadata. */ @@ -1101,6 +1125,31 @@ namespace stream { plaintext.type_right = msg.data.adaptive_triggers.type_right; std::ranges::copy(msg.data.adaptive_triggers.right, plaintext.right); + std::array + encrypted_payload; + + payload = encode_control(session, util::view(plaintext), encrypted_payload); + } else if (msg.type == platf::gamepad_feedback_e::set_haptics) { + control_set_controller_haptics_t plaintext; + plaintext.header.type = packetTypes[IDX_SET_CONTROLLER_HAPTICS]; + plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); + + const auto &data = msg.data.haptics; + plaintext.id = util::endian::little(msg.id); + plaintext.target = data.target; + plaintext.kind = data.kind; + plaintext.gain_db = data.gain_db; + plaintext.intensity = util::endian::little(data.intensity); + plaintext.frequency_hz = util::endian::little(data.frequency_hz); + plaintext.duration_us = util::endian::little(static_cast(data.duration_us)); + plaintext.interval_us = util::endian::little(data.interval_us); + plaintext.repeat_count = util::endian::little(data.repeat_count); + plaintext.lfo_frequency_hz = util::endian::little(data.lfo_frequency_hz); + plaintext.lfo_depth_percent = data.lfo_depth_percent; + plaintext.start_frequency_hz = util::endian::little(data.start_frequency_hz); + plaintext.end_frequency_hz = util::endian::little(data.end_frequency_hz); + plaintext.script_id = data.script_id; + std::array encrypted_payload; diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 53fc797d391..9734328a064 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -166,9 +166,9 @@ namespace system_tray { */ std::array &virtualhid_benefits_menu_storage() { static std::array benefits_menu {{ - {.text = "Xbox One, Xbox Series, DualSense (DS5), Switch Pro, and Generic", .disabled = 1}, + {.text = "Steam Controller (2nd generation), Xbox One, Xbox Series, DualSense (DS5), Switch Pro, and Generic", .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 = "Motion, dual touchpads, rear buttons, LEDs, and adaptive triggers where supported", .disabled = 1}, {.text = "Actively developed and supported by LizardByte", .disabled = 1}, {}, }}; diff --git a/src_assets/common/assets/web/configs/tabs/Inputs.vue b/src_assets/common/assets/web/configs/tabs/Inputs.vue index 0bf664555ff..87f2b809d8c 100644 --- a/src_assets/common/assets/web/configs/tabs/Inputs.vue +++ b/src_assets/common/assets/web/configs/tabs/Inputs.vue @@ -149,6 +149,7 @@ watch( +
{{ $t('config.gamepad_desc') }}
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 882a29be111..17719b815e1 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -271,6 +271,7 @@ "gamepad_ds4_manual": "PlayStation-style gamepad options", "gamepad_ds5": "DS5 (PS5)", "gamepad_generic": "Generic HID gamepad", + "gamepad_steam_triton": "Steam Controller (2nd generation)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", @@ -645,13 +646,13 @@ "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 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_macos_desc": "Virtual HID Broker creates Xbox, PlayStation, Nintendo Switch Pro, Generic, and Steam Controller (2nd generation) 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": "Supports controller-specific features such as motion, dual touchpads, rear buttons, 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": "Creates the Steam Controller (2nd generation), 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", diff --git a/tests/unit/platform/test_virtualhid_input.cpp b/tests/unit/platform/test_virtualhid_input.cpp index bbdcd710e17..94fbe1223d4 100644 --- a/tests/unit/platform/test_virtualhid_input.cpp +++ b/tests/unit/platform/test_virtualhid_input.cpp @@ -91,7 +91,8 @@ INSTANTIATE_TEST_SUITE_P( 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_capabilities_case_t {"switch"sv, false, true}, + gamepad_capabilities_case_t {"steam_triton"sv, true, true} ), [](const ::testing::TestParamInfo &info) { return std::string {info.param.gamepad}; @@ -148,11 +149,12 @@ namespace { std::uint8_t type = LI_CTYPE_UNKNOWN, std::uint16_t capabilities = 0, int global_index = 0, - std::uint8_t client_index = 3 + std::uint8_t client_index = 3, + std::uint32_t supported_buttons = 0 ) { config::input.gamepad = profile; const platf::gamepad_id_t id {global_index, client_index}; - const platf::gamepad_arrival_t metadata {type, capabilities, 0}; + const platf::gamepad_arrival_t metadata {type, capabilities, supported_buttons}; if (platf::virtualhid::alloc_gamepad(*context_, id, metadata, feedback_queue_) != 0) { ADD_FAILURE() << "Failed to allocate fake gamepad profile " << profile; return nullptr; @@ -251,6 +253,7 @@ TEST_F(VirtualHidDeviceTest, SelectsVigembusFallbackByBackendAndConfiguredProfil EXPECT_TRUE(platf::virtualhid::should_try_vigembus_fallback("x360", true, config::GAMEPAD_DRIVER_ALL)); EXPECT_TRUE(platf::virtualhid::should_try_vigembus_fallback("ds4", true, config::GAMEPAD_DRIVER_ALL)); EXPECT_FALSE(platf::virtualhid::should_try_vigembus_fallback("xseries", true, config::GAMEPAD_DRIVER_ALL)); + EXPECT_FALSE(platf::virtualhid::should_try_vigembus_fallback("steam_triton", true, config::GAMEPAD_DRIVER_ALL)); EXPECT_TRUE(platf::virtualhid::should_try_vigembus_fallback("xseries", false, config::GAMEPAD_DRIVER_ALL)); EXPECT_TRUE(platf::virtualhid::should_try_vigembus_fallback("xseries", false, "")); @@ -260,7 +263,7 @@ TEST_F(VirtualHidDeviceTest, SelectsVigembusFallbackByBackendAndConfiguredProfil } TEST_F(VirtualHidDeviceTest, ReportsStaticAndRuntimeGamepadChoices) { - constexpr std::array expected_names {"auto"sv, "generic"sv, "x360"sv, "xone"sv, "xseries"sv, "ds4"sv, "ds5"sv, "switch"sv}; + constexpr std::array expected_names {"auto"sv, "generic"sv, "x360"sv, "xone"sv, "xseries"sv, "ds4"sv, "ds5"sv, "switch"sv, "steam_triton"sv}; const auto static_gamepads = platf::virtualhid::static_supported_gamepads(); ASSERT_EQ(static_gamepads.size(), expected_names.size()); @@ -415,6 +418,182 @@ TEST_F(VirtualHidDeviceTest, AllocatesManualProfileAndTranslatesFullState) { platf::virtualhid::gamepad_update(*context(), 1, input_state); } +TEST_F(VirtualHidDeviceTest, AllocatesSecondGenerationSteamControllerAndTranslatesExtendedState) { + constexpr auto capabilities = static_cast(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_DUAL_TOUCHPAD | LI_CCAP_BATTERY_STATE); + auto *adapter = allocate_gamepad("steam_triton"sv, LI_CTYPE_UNKNOWN, capabilities); + ASSERT_NE(adapter, nullptr); + ASSERT_NE(adapter->gamepad(), nullptr); + EXPECT_EQ(adapter->gamepad()->profile().gamepad_kind, lvh::GamepadProfileKind::steam_triton); + EXPECT_EQ(adapter->gamepad()->profile().name, "Sunshine Steam Controller (2nd generation)"); + EXPECT_TRUE(adapter->gamepad()->metadata().has_motion_sensors); + EXPECT_TRUE(adapter->gamepad()->metadata().has_touchpad); + EXPECT_TRUE(adapter->gamepad()->metadata().has_battery); + EXPECT_EQ(adapter->support().supported_touchpad_count, 2U); + EXPECT_EQ(adapter->support().supported_rear_paddle_count, 4U); + + constexpr auto button_flags = platf::DPAD_UP | platf::DPAD_DOWN | platf::DPAD_LEFT | platf::DPAD_RIGHT | + platf::START | platf::BACK | platf::LEFT_STICK | platf::RIGHT_STICK | + platf::LEFT_BUTTON | platf::RIGHT_BUTTON | platf::HOME | + platf::A | platf::B | platf::X | platf::Y | platf::MISC_BUTTON | + platf::PADDLE1 | platf::PADDLE2 | platf::PADDLE3 | platf::PADDLE4 | + platf::STEAM_LEFT_TOUCHPAD_BUTTON | platf::STEAM_RIGHT_TOUCHPAD_BUTTON | + platf::STEAM_LEFT_TRIGGER_CLICK | platf::STEAM_RIGHT_TRIGGER_CLICK | + platf::STEAM_LEFT_STICK_TOUCH | platf::STEAM_RIGHT_STICK_TOUCH | + platf::STEAM_LEFT_GRIP_TOUCH | platf::STEAM_RIGHT_GRIP_TOUCH; + platf::virtualhid::gamepad_update(*context(), 0, {button_flags, 64, 192, -32768, 32767, -16384, 16384}); + + using enum lvh::GamepadButton; + for (const auto button : { + dpad_up, + dpad_down, + dpad_left, + dpad_right, + start, + back, + left_stick, + right_stick, + left_shoulder, + right_shoulder, + guide, + a, + b, + x, + y, + misc1, + paddle1, + paddle2, + paddle3, + paddle4, + left_touchpad, + right_touchpad, + left_trigger_click, + right_trigger_click, + left_stick_touch, + right_stick_touch, + left_grip_touch, + right_grip_touch, + }) { + EXPECT_TRUE(adapter->state().buttons.test(button)); + } + EXPECT_FALSE(adapter->state().buttons.test(touchpad)); + EXPECT_FLOAT_EQ(adapter->state().left_trigger, 64.0F / 255.0F); + EXPECT_FLOAT_EQ(adapter->state().right_trigger, 192.0F / 255.0F); + EXPECT_FLOAT_EQ(adapter->state().left_stick.x, -1.0F); + EXPECT_FLOAT_EQ(adapter->state().left_stick.y, 1.0F); + + platf::virtualhid::gamepad_update(*context(), 0, {0, 242, 255, 0, 0, 0, 0}); + EXPECT_FALSE(adapter->state().buttons.test(left_trigger_click)); + EXPECT_TRUE(adapter->state().buttons.test(right_trigger_click)); + platf::virtualhid::gamepad_update(*context(), 0, {0, 243, 230, 0, 0, 0, 0}); + EXPECT_TRUE(adapter->state().buttons.test(left_trigger_click)); + EXPECT_TRUE(adapter->state().buttons.test(right_trigger_click)); + platf::virtualhid::gamepad_update(*context(), 0, {0, 230, 229, 0, 0, 0, 0}); + EXPECT_TRUE(adapter->state().buttons.test(left_trigger_click)); + EXPECT_FALSE(adapter->state().buttons.test(right_trigger_click)); + platf::virtualhid::gamepad_update(*context(), 0, {0, 229, 0, 0, 0, 0, 0}); + EXPECT_FALSE(adapter->state().buttons.test(left_trigger_click)); + + platf::gamepad_touch_t left_touch {{0, 3}, LI_TOUCH_EVENT_DOWN, 40, 0.25F, 0.75F, 0.8F, 0}; + platf::gamepad_touch_t right_touch {{0, 3}, LI_TOUCH_EVENT_DOWN, 40, 0.75F, 0.25F, 0.6F, 1}; + platf::virtualhid::gamepad_touch(*context(), left_touch); + platf::virtualhid::gamepad_touch(*context(), right_touch); + EXPECT_TRUE(adapter->state().touchpad_contacts[0].active); + EXPECT_TRUE(adapter->state().touchpad_contacts[1].active); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].x, 0.25F); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].pressure, 0.8F); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[1].x, 0.75F); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[1].pressure, 0.6F); + EXPECT_TRUE(adapter->state().buttons.test(left_touchpad)); + EXPECT_TRUE(adapter->state().buttons.test(right_touchpad)); + platf::virtualhid::gamepad_update(*context(), 0, {}); + EXPECT_TRUE(adapter->state().buttons.test(left_touchpad)); + EXPECT_TRUE(adapter->state().buttons.test(right_touchpad)); + + const auto dual_touch_submit_count = adapter->gamepad()->submit_count(); + right_touch.pointerId = 41; + platf::virtualhid::gamepad_touch(*context(), right_touch); + EXPECT_EQ(adapter->gamepad()->submit_count(), dual_touch_submit_count); + right_touch.eventType = LI_TOUCH_EVENT_MOVE; + platf::virtualhid::gamepad_touch(*context(), right_touch); + EXPECT_EQ(adapter->gamepad()->submit_count(), dual_touch_submit_count); + right_touch.touchpadIndex = 2; + platf::virtualhid::gamepad_touch(*context(), right_touch); + EXPECT_EQ(adapter->gamepad()->submit_count(), dual_touch_submit_count); + + left_touch.eventType = LI_TOUCH_EVENT_UP; + platf::virtualhid::gamepad_touch(*context(), left_touch); + EXPECT_FALSE(adapter->state().touchpad_contacts[0].active); + EXPECT_FALSE(adapter->state().buttons.test(left_touchpad)); + EXPECT_TRUE(adapter->state().buttons.test(right_touchpad)); + right_touch.eventType = LI_TOUCH_EVENT_CANCEL_ALL; + platf::virtualhid::gamepad_touch(*context(), right_touch); + EXPECT_FALSE(adapter->state().touchpad_contacts[1].active); + EXPECT_FALSE(adapter->state().buttons.test(right_touchpad)); + + const auto mouse_submit_count = context()->mouse->submit_count(); + right_touch.touchpadIndex = 1; + right_touch.pointerId = 43; + right_touch.eventType = LI_TOUCH_EVENT_DOWN; + right_touch.x = 0.6F; + right_touch.y = 0.4F; + platf::virtualhid::gamepad_touch(*context(), right_touch); + right_touch.eventType = LI_TOUCH_EVENT_UP; + platf::virtualhid::gamepad_touch(*context(), right_touch); + EXPECT_FALSE(adapter->state().touchpad_contacts[1].active); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[1].x, 0.6F); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[1].y, 0.4F); + EXPECT_EQ(context()->mouse->submit_count(), mouse_submit_count); + + left_touch.eventType = LI_TOUCH_EVENT_DOWN; + left_touch.pointerId = 42; + left_touch.pressure = 0.34F; + platf::virtualhid::gamepad_touch(*context(), left_touch); + EXPECT_FALSE(adapter->state().buttons.test(left_touchpad)); + left_touch.eventType = LI_TOUCH_EVENT_MOVE; + left_touch.pressure = 0.35F; + platf::virtualhid::gamepad_touch(*context(), left_touch); + EXPECT_TRUE(adapter->state().buttons.test(left_touchpad)); + left_touch.pressure = 0.30F; + platf::virtualhid::gamepad_touch(*context(), left_touch); + EXPECT_TRUE(adapter->state().buttons.test(left_touchpad)); + left_touch.pressure = 0.25F; + platf::virtualhid::gamepad_touch(*context(), left_touch); + EXPECT_FALSE(adapter->state().buttons.test(left_touchpad)); + left_touch.eventType = LI_TOUCH_EVENT_UP; + platf::virtualhid::gamepad_touch(*context(), left_touch); + + platf::virtualhid::gamepad_motion(*context(), {{0, 3}, LI_MOTION_TYPE_GYRO, 1.0F, 2.0F, 3.0F}); + EXPECT_TRUE(adapter->state().gyroscope.has_value()); + platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_DISCHARGING, 75}); + EXPECT_EQ(adapter->state().battery->percentage, 75); +} + +TEST_F(VirtualHidDeviceTest, UsesSteamClickInferenceOnlyForUnsupportedButtons) { + constexpr auto capabilities = static_cast(LI_CCAP_DUAL_TOUCHPAD); + constexpr auto native_clicks = platf::STEAM_LEFT_TOUCHPAD_BUTTON | platf::STEAM_RIGHT_TOUCHPAD_BUTTON | + platf::STEAM_LEFT_TRIGGER_CLICK | platf::STEAM_RIGHT_TRIGGER_CLICK; + auto *adapter = allocate_gamepad("steam_triton"sv, LI_CTYPE_STEAM, capabilities, 0, 3, native_clicks); + ASSERT_NE(adapter, nullptr); + + using enum lvh::GamepadButton; + platf::virtualhid::gamepad_update(*context(), 0, {0, 255, 255, 0, 0, 0, 0}); + EXPECT_FALSE(adapter->state().buttons.test(left_trigger_click)); + EXPECT_FALSE(adapter->state().buttons.test(right_trigger_click)); + + platf::gamepad_touch_t left_touch {{0, 3}, LI_TOUCH_EVENT_DOWN, 10, 0.5F, 0.5F, 1.0F, 0}; + platf::gamepad_touch_t right_touch {{0, 3}, LI_TOUCH_EVENT_DOWN, 11, 0.5F, 0.5F, 1.0F, 1}; + platf::virtualhid::gamepad_touch(*context(), left_touch); + platf::virtualhid::gamepad_touch(*context(), right_touch); + EXPECT_FALSE(adapter->state().buttons.test(left_touchpad)); + EXPECT_FALSE(adapter->state().buttons.test(right_touchpad)); + + platf::virtualhid::gamepad_update(*context(), 0, {native_clicks, 255, 255, 0, 0, 0, 0}); + EXPECT_TRUE(adapter->state().buttons.test(left_trigger_click)); + EXPECT_TRUE(adapter->state().buttons.test(right_trigger_click)); + EXPECT_TRUE(adapter->state().buttons.test(left_touchpad)); + EXPECT_TRUE(adapter->state().buttons.test(right_touchpad)); +} + TEST_P(VirtualHidAutoProfileTest, SelectsProfileFromClientMetadata) { const auto &test_case = GetParam(); auto *adapter = allocate_gamepad("auto"sv, test_case.type, test_case.capabilities); @@ -430,6 +609,8 @@ INSTANTIATE_TEST_SUITE_P( auto_profile_case_t {"PlayStation", LI_CTYPE_PS, 0, lvh::GamepadProfileKind::dualsense}, auto_profile_case_t {"Nintendo", LI_CTYPE_NINTENDO, 0, lvh::GamepadProfileKind::switch_pro}, auto_profile_case_t {"Xbox", LI_CTYPE_XBOX, 0, lvh::GamepadProfileKind::xbox_series}, + auto_profile_case_t {"SteamTriton", LI_CTYPE_STEAM, LI_CCAP_DUAL_TOUCHPAD, lvh::GamepadProfileKind::steam_triton}, + auto_profile_case_t {"DualTouchpad", LI_CTYPE_UNKNOWN, LI_CCAP_DUAL_TOUCHPAD, lvh::GamepadProfileKind::steam_triton}, auto_profile_case_t {"UnknownMotion", LI_CTYPE_UNKNOWN, LI_CCAP_ACCEL, lvh::GamepadProfileKind::dualsense}, auto_profile_case_t {"UnknownTouchpad", LI_CTYPE_UNKNOWN, LI_CCAP_TOUCHPAD, lvh::GamepadProfileKind::dualsense}, auto_profile_case_t {"UnknownDefault", LI_CTYPE_UNKNOWN, 0, lvh::GamepadProfileKind::xbox_series} @@ -553,9 +734,34 @@ TEST_F(VirtualHidDeviceTest, RoutesAndDeduplicatesGamepadFeedback) { EXPECT_EQ(feedback->id, 7); EXPECT_FALSE(feedback_queue()->pop(0ms)); + output.kind = lvh::GamepadOutputKind::trigger_rumble; + ASSERT_TRUE(adapter->dispatch_output(output).ok()); + feedback = resumed_feedback->pop(10ms); + ASSERT_TRUE(feedback); + EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::rumble_triggers); + EXPECT_EQ(feedback->id, 7); + + output.kind = lvh::GamepadOutputKind::rgb_led; + ASSERT_TRUE(adapter->dispatch_output(output).ok()); + feedback = resumed_feedback->pop(10ms); + ASSERT_TRUE(feedback); + EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_rgb_led); + EXPECT_EQ(feedback->id, 7); + + output.kind = lvh::GamepadOutputKind::player_leds; + ASSERT_TRUE(adapter->dispatch_output(output).ok()); + feedback = resumed_feedback->pop(10ms); + ASSERT_TRUE(feedback); + EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_player_leds); + EXPECT_EQ(feedback->id, 7); + EXPECT_FALSE(feedback_queue()->pop(0ms)); + output.kind = lvh::GamepadOutputKind::raw_report; ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_FALSE(feedback_queue()->pop(0ms)); + output.kind = lvh::GamepadOutputKind::haptics; + ASSERT_TRUE(adapter->dispatch_output(output).ok()); + EXPECT_FALSE(feedback_queue()->pop(0ms)); platf::virtualhid::free_gamepad(*context(), 0); const platf::gamepad_id_t id {0, 0}; @@ -567,6 +773,51 @@ TEST_F(VirtualHidDeviceTest, RoutesAndDeduplicatesGamepadFeedback) { ASSERT_TRUE(adapter->dispatch_output(output).ok()); } +TEST_F(VirtualHidDeviceTest, RoutesAddressableHapticsToCapableClient) { + constexpr auto capabilities = static_cast(LI_CCAP_DUAL_TOUCHPAD | LI_CCAP_HAPTICS); + auto *adapter = allocate_gamepad("steam_triton"sv, LI_CTYPE_STEAM, capabilities); + ASSERT_NE(adapter, nullptr); + ASSERT_TRUE(feedback_queue()->pop(10ms)); + ASSERT_TRUE(feedback_queue()->pop(10ms)); + + lvh::GamepadOutput output; + output.kind = lvh::GamepadOutputKind::haptics; + output.haptic_effect = lvh::GamepadHapticEffect { + .target = lvh::GamepadHapticTarget::both, + .kind = lvh::GamepadHapticEffectKind::pulse, + .gain_db = -6, + .intensity = 42, + .frequency_hz = 440, + .duration_us = 1000, + .interval_us = 2000, + .repeat_count = 5, + .lfo_frequency_hz = 7, + .lfo_depth_percent = 80, + .start_frequency_hz = 100, + .end_frequency_hz = 200, + .script_id = 3, + }; + ASSERT_TRUE(adapter->dispatch_output(output).ok()); + + const auto feedback = feedback_queue()->pop(10ms); + ASSERT_TRUE(feedback); + EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_haptics); + EXPECT_EQ(feedback->id, 3); + EXPECT_EQ(feedback->data.haptics.target, static_cast(lvh::GamepadHapticTarget::both)); + EXPECT_EQ(feedback->data.haptics.kind, static_cast(lvh::GamepadHapticEffectKind::pulse)); + EXPECT_EQ(feedback->data.haptics.gain_db, -6); + EXPECT_EQ(feedback->data.haptics.intensity, 42); + EXPECT_EQ(feedback->data.haptics.frequency_hz, 440); + EXPECT_EQ(feedback->data.haptics.duration_us, 1000); + EXPECT_EQ(feedback->data.haptics.interval_us, 2000U); + EXPECT_EQ(feedback->data.haptics.repeat_count, 5); + EXPECT_EQ(feedback->data.haptics.lfo_frequency_hz, 7); + EXPECT_EQ(feedback->data.haptics.lfo_depth_percent, 80); + EXPECT_EQ(feedback->data.haptics.start_frequency_hz, 100); + EXPECT_EQ(feedback->data.haptics.end_frequency_hz, 200); + EXPECT_EQ(feedback->data.haptics.script_id, 3); +} + TEST_F(VirtualHidDeviceTest, TranslatesGamepadTouchMotionAndBattery) { const auto capabilities = static_cast(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_BATTERY_STATE); auto *adapter = allocate_gamepad("ds5"sv, LI_CTYPE_PS, capabilities); @@ -580,14 +831,16 @@ TEST_F(VirtualHidDeviceTest, TranslatesGamepadTouchMotionAndBattery) { EXPECT_TRUE(adapter->state().touchpad_contacts[0].active); EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].x, 0.0F); EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].y, 1.0F); + EXPECT_FALSE(adapter->state().buttons.test(lvh::GamepadButton::touchpad)); touch.eventType = LI_TOUCH_EVENT_MOVE; touch.pressure = 0.25F; touch.x = 0.4F; touch.y = 0.6F; platf::virtualhid::gamepad_touch(*context(), touch); - EXPECT_FALSE(adapter->state().touchpad_contacts[0].active); + EXPECT_TRUE(adapter->state().touchpad_contacts[0].active); EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].x, 0.4F); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].pressure, 0.25F); touch.eventType = LI_TOUCH_EVENT_DOWN; touch.pointerId = 11; diff --git a/tests/unit/test_input.cpp b/tests/unit/test_input.cpp index 10c5461ac8a..7169100f7ea 100644 --- a/tests/unit/test_input.cpp +++ b/tests/unit/test_input.cpp @@ -393,6 +393,35 @@ TEST_F(InputGamepadSessionTest, ReusesGamepadsAcrossPauseAndDestroysThemOnTermin EXPECT_NE(replacement, resumed); } +TEST_F(InputGamepadSessionTest, PreservesControllerTouchpadIndex) { + config::input.gamepad = "steam_triton"; + auto stream_input = input::alloc(std::make_shared(), "dual-touchpad-client"); + ASSERT_NE(stream_input, nullptr); + + constexpr auto capabilities = static_cast(LI_CCAP_TOUCHPAD | LI_CCAP_DUAL_TOUCHPAD); + const platf::gamepad_arrival_t metadata {LI_CTYPE_STEAM, capabilities, 0}; + const auto global_id = input::testing::alloc_gamepad(stream_input, 0, metadata); + ASSERT_GE(global_id, 0); + + auto *adapter = platf::virtualhid::gamepad_adapter_for_testing(context(), global_id); + ASSERT_NE(adapter, nullptr); + input::testing::send_controller_touch_packet( + stream_input, + 0, + LI_TOUCH_EVENT_DOWN, + 1, + 42, + 0.75F, + 0.25F, + 1.0F + ); + + EXPECT_FALSE(adapter->state().touchpad_contacts[0].active); + EXPECT_TRUE(adapter->state().touchpad_contacts[1].active); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[1].x, 0.75F); + EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[1].y, 0.25F); +} + TEST_F(InputGamepadSessionTest, RefreshesSharedVirtualInputAfterLicenseStateChanges) { ASSERT_NE(context().keyboard, nullptr); ASSERT_NE(context().mouse, nullptr); diff --git a/tests/unit/test_system_tray.cpp b/tests/unit/test_system_tray.cpp index 8cc3af49772..1db1aa6f1cf 100644 --- a/tests/unit/test_system_tray.cpp +++ b/tests/unit/test_system_tray.cpp @@ -85,11 +85,11 @@ namespace { */ void verify_virtualhid_benefits_menu(const struct tray_menu *benefits_menu) { ASSERT_NE(benefits_menu, nullptr); - EXPECT_STREQ(benefits_menu[0].text, "Xbox One, Xbox Series, DualSense (DS5), Switch Pro, and Generic"); + EXPECT_STREQ(benefits_menu[0].text, "Steam Controller (2nd generation), 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 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_STREQ(benefits_menu[2].text, "Motion, dual touchpads, rear buttons, LEDs, and adaptive triggers where supported"); EXPECT_EQ(benefits_menu[2].disabled, 1); EXPECT_STREQ(benefits_menu[3].text, "Actively developed and supported by LizardByte"); EXPECT_EQ(benefits_menu[3].disabled, 1); diff --git a/tests/web/Configuration.test.js b/tests/web/Configuration.test.js index 744b6922e67..a05353a11a9 100644 --- a/tests/web/Configuration.test.js +++ b/tests/web/Configuration.test.js @@ -145,7 +145,7 @@ describe('gamepad input configuration', () => { 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', + 'auto', 'generic', 'x360', 'xone', 'xseries', 'ds4', 'ds5', 'switch', 'steam_triton', ]) expect(wrapper.find('#motion_as_ds4').exists()).toBe(true) expect(wrapper.find('#touchpad_as_ds4').exists()).toBe(true) diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid index 52d788119aa..e9ede639d78 160000 --- a/third-party/libvirtualhid +++ b/third-party/libvirtualhid @@ -1 +1 @@ -Subproject commit 52d788119aa4ba32bb1d6e9f68d347a47ce3dffe +Subproject commit e9ede639d784ad1acb0aa29010bf81ce4b73496b diff --git a/third-party/moonlight-common-c b/third-party/moonlight-common-c index f900dd47677..804001be564 160000 --- a/third-party/moonlight-common-c +++ b/third-party/moonlight-common-c @@ -1 +1 @@ -Subproject commit f900dd4767759c7b9d0e93bcea666b55c69ea62f +Subproject commit 804001be56430294977b94fb72a71d18d7d7374d