Skip to content

Commit 923eb82

Browse files
Add player LED feedback and extension capability
Introduce a new gamepad feedback path for player indicator LEDs, including a new feedback message type, control-stream packet encoding (0x5504), and virtual HID output handling with deduplication of repeated LED states. Broaden controller extension capability detection from touchpad-only to touchpad-or-motion and use it consistently across Linux, macOS, and Windows capability reporting. Update unit tests to cover both touchpad support and controller extension advertising, plus player LED routing/dedup behavior, and bump the libvirtualhid submodule.
1 parent e3c8713 commit 923eb82

9 files changed

Lines changed: 152 additions & 21 deletions

File tree

src/platform/common.h

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ namespace platf {
116116
rumble_triggers, ///< Rumble triggers
117117
set_motion_event_state, ///< Set motion event state
118118
set_rgb_led, ///< Set RGB LED
119+
set_player_leds, ///< Set player indicator LEDs
119120
set_adaptive_triggers, ///< Set adaptive triggers
120121
};
121122

@@ -189,6 +190,22 @@ namespace platf {
189190
return msg;
190191
}
191192

193+
/**
194+
* @brief Create player indicator LED state.
195+
*
196+
* @param id Identifier for the controller, session, display, or resource.
197+
* @param solid Four-bit mask of solid player indicators.
198+
* @param flashing Four-bit mask of flashing player indicators.
199+
* @return Constructed player indicator LED object.
200+
*/
201+
static gamepad_feedback_msg_t make_player_leds(std::uint16_t id, std::uint8_t solid, std::uint8_t flashing) {
202+
gamepad_feedback_msg_t msg;
203+
msg.type = gamepad_feedback_e::set_player_leds;
204+
msg.id = id;
205+
msg.data.player_leds = {solid, flashing};
206+
return msg;
207+
}
208+
192209
/**
193210
* @brief Create adaptive triggers.
194211
*
@@ -233,6 +250,11 @@ namespace platf {
233250
std::uint8_t b;
234251
} rgb_led;
235252

253+
struct {
254+
std::uint8_t solid;
255+
std::uint8_t flashing;
256+
} player_leds;
257+
236258
struct {
237259
uint16_t controllerNumber;
238260
uint8_t event_flags;
@@ -380,9 +402,9 @@ namespace platf {
380402
*/
381403
constexpr caps_t pen_touch = 0x01; // Pen and touch events
382404
/**
383-
* @brief Capability bit indicating controller touchpad support.
405+
* @brief Capability bit indicating controller touchpad and motion support.
384406
*/
385-
constexpr caps_t controller_touch = 0x02; // Controller touch events
407+
constexpr caps_t controller_touch = 0x02; // Controller touch and motion events
386408
}; // namespace platform_caps
387409

388410
/**

src/platform/linux/input/virtualhid.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ namespace platf {
2828
if (const auto &capabilities = runtime->capabilities(); config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {
2929
caps |= platform_caps::pen_touch;
3030
}
31-
if (virtualhid::configured_gamepad_supports_touchpad()) {
31+
if (virtualhid::configured_gamepad_supports_controller_extensions()) {
3232
caps |= platform_caps::controller_touch;
3333
}
3434

src/platform/macos/input.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ namespace platf {
3737
}
3838

3939
const auto &capabilities = runtime->capabilities();
40-
if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_touchpad()) {
40+
if (capabilities.supports_gamepad && virtualhid::configured_gamepad_supports_controller_extensions()) {
4141
caps |= platform_caps::controller_touch;
4242
}
4343
if (config::input.native_pen_touch && (capabilities.supports_touchscreen || capabilities.supports_pen_tablet)) {

src/platform/virtualhid_input.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ namespace platf::virtualhid {
4545
std::uint8_t last_red = 0; ///< Last red LED value.
4646
std::uint8_t last_green = 0; ///< Last green LED value.
4747
std::uint8_t last_blue = 0; ///< Last blue LED value.
48+
bool has_last_player_leds = false; ///< Whether last player indicator LED values are valid.
49+
std::uint8_t last_solid_player_leds = 0; ///< Last solid player indicator mask.
50+
std::uint8_t last_flashing_player_leds = 0; ///< Last flashing player indicator mask.
4851
};
4952

5053
namespace {
@@ -316,6 +319,22 @@ namespace platf::virtualhid {
316319
return event;
317320
}
318321

322+
/**
323+
* @brief Pack four player indicator states into a protocol bit mask.
324+
*
325+
* @param leds Player indicator states ordered from player one through four.
326+
* @return Four-bit player indicator mask.
327+
*/
328+
std::uint8_t player_led_mask(const std::array<bool, 4> &leds) {
329+
std::byte mask {};
330+
for (std::size_t index = 0; index < leds.size(); ++index) {
331+
if (leds[index]) {
332+
mask |= std::byte {1} << index;
333+
}
334+
}
335+
return std::to_integer<std::uint8_t>(mask);
336+
}
337+
319338
lvh::PenToolType pen_tool(std::uint8_t tool) {
320339
using enum lvh::PenToolType;
321340

@@ -390,6 +409,19 @@ namespace platf::virtualhid {
390409
gamepad->last_blue = output.blue;
391410
raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_rgb_led(gamepad->client_relative_index, output.red, output.green, output.blue));
392411
break;
412+
case lvh::GamepadOutputKind::player_leds:
413+
{
414+
const auto solid = player_led_mask(output.player_leds);
415+
const auto flashing = player_led_mask(output.flashing_player_leds);
416+
if (gamepad->has_last_player_leds && gamepad->last_solid_player_leds == solid && gamepad->last_flashing_player_leds == flashing) {
417+
return;
418+
}
419+
gamepad->has_last_player_leds = true;
420+
gamepad->last_solid_player_leds = solid;
421+
gamepad->last_flashing_player_leds = flashing;
422+
raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_player_leds(gamepad->client_relative_index, solid, flashing));
423+
break;
424+
}
393425
case lvh::GamepadOutputKind::adaptive_triggers:
394426
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));
395427
break;
@@ -595,6 +627,7 @@ namespace platf::virtualhid {
595627
gamepad->has_last_rumble = false;
596628
gamepad->has_last_trigger_rumble = false;
597629
gamepad->has_last_rgb = false;
630+
gamepad->has_last_player_leds = false;
598631

599632
if (gamepad->adapter->support().supports_motion) {
600633
raise_feedback_unlocked(gamepad, gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100));
@@ -913,6 +946,16 @@ namespace platf::virtualhid {
913946
return lvh::gamepad_profile_support(profile).supports_touchpad;
914947
}
915948

949+
bool configured_gamepad_supports_controller_extensions() {
950+
if (config::input.gamepad == "auto"sv) {
951+
return true;
952+
}
953+
954+
const auto profile = profile_for_name(config::input.gamepad).profile();
955+
const auto &support = lvh::gamepad_profile_support(profile);
956+
return support.supports_touchpad || support.supports_motion;
957+
}
958+
916959
} // namespace platf::virtualhid
917960

918961
namespace platf {

src/platform/virtualhid_input.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,14 @@ namespace platf::virtualhid {
279279
*/
280280
bool configured_gamepad_supports_touchpad();
281281

282+
/**
283+
* @brief Return whether the configured gamepad profile needs Moonlight controller extensions.
284+
*
285+
* Moonlight uses the controller-touch feature flag to authorize both controller
286+
* touchpad and motion packets.
287+
*
288+
* @return True when controller touchpad or motion input should be advertised.
289+
*/
290+
bool configured_gamepad_supports_controller_extensions();
291+
282292
} // namespace platf::virtualhid

src/platform/windows/input.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1234,7 +1234,7 @@ namespace platf {
12341234
platform_caps::caps_t get_capabilities() {
12351235
platform_caps::caps_t caps = 0;
12361236

1237-
if (virtualhid::configured_gamepad_supports_touchpad()) {
1237+
if (virtualhid::configured_gamepad_supports_controller_extensions()) {
12381238
caps |= platform_caps::controller_touch;
12391239
}
12401240

src/stream.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ constexpr int IDX_RUMBLE_TRIGGER_DATA = 12; ///< Control-stream message index f
4949
constexpr int IDX_SET_MOTION_EVENT = 13; ///< Control-stream message index for set motion event.
5050
constexpr int IDX_SET_RGB_LED = 14; ///< Control-stream message index for set rgb led.
5151
constexpr int IDX_SET_ADAPTIVE_TRIGGERS = 15; ///< Control-stream message index for set adaptive triggers.
52+
constexpr int IDX_SET_PLAYER_LEDS = 16; ///< Control-stream message index for set player indicator LEDs.
5253

5354
static const short packetTypes[] = {
5455
0x0305, // Start A
@@ -67,6 +68,7 @@ static const short packetTypes[] = {
6768
0x5501, // Set motion event (Sunshine protocol extension)
6869
0x5502, // Set RGB LED (Sunshine protocol extension)
6970
0x5503, // Set Adaptive triggers (Sunshine protocol extension)
71+
0x5504, // Set player indicator LEDs (Sunshine protocol extension)
7072
};
7173

7274
namespace asio = boost::asio;
@@ -239,6 +241,17 @@ namespace stream {
239241
std::uint8_t b; ///< Blue LED channel.
240242
};
241243

244+
/**
245+
* @brief Control payload that sets controller player indicator LEDs.
246+
*/
247+
struct control_set_player_leds_t {
248+
control_header_v2 header; ///< Control message header preceding this payload.
249+
250+
std::uint16_t id; ///< Controller identifier associated with this message.
251+
std::uint8_t solid; ///< Four-bit mask of solid player indicators.
252+
std::uint8_t flashing; ///< Four-bit mask of flashing player indicators.
253+
};
254+
242255
/**
243256
* @brief Control payload that configures DualSense adaptive triggers.
244257
*/
@@ -1034,6 +1047,22 @@ namespace stream {
10341047
std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
10351048
encrypted_payload;
10361049

1050+
payload = encode_control(session, util::view(plaintext), encrypted_payload);
1051+
} else if (msg.type == platf::gamepad_feedback_e::set_player_leds) {
1052+
control_set_player_leds_t plaintext;
1053+
plaintext.header.type = packetTypes[IDX_SET_PLAYER_LEDS];
1054+
plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);
1055+
1056+
auto &data = msg.data.player_leds;
1057+
1058+
plaintext.id = util::endian::little(msg.id);
1059+
plaintext.solid = data.solid;
1060+
plaintext.flashing = data.flashing;
1061+
1062+
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();
1063+
std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
1064+
encrypted_payload;
1065+
10371066
payload = encode_control(session, util::view(plaintext), encrypted_payload);
10381067
} else if (msg.type == platf::gamepad_feedback_e::set_adaptive_triggers) {
10391068
control_adaptive_triggers_t plaintext;

tests/unit/platform/test_virtualhid_input.cpp

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@ namespace {
2828
/**
2929
* @brief Expected touchpad support for a configured gamepad.
3030
*/
31-
struct gamepad_touchpad_case_t {
31+
struct gamepad_capabilities_case_t {
3232
std::string_view gamepad; ///< Configured gamepad name.
33-
bool expected; ///< Whether the configured gamepad supports touchpad input.
33+
bool expected_touchpad; ///< Whether the configured gamepad supports touchpad input.
34+
bool expected_controller_extensions; ///< Whether Moonlight controller extensions should be advertised.
3435
};
3536

3637
/**
3738
* @brief Parameterized fixture that restores the configured gamepad after each test.
3839
*/
39-
class VirtualHidInputTest: public ::testing::TestWithParam<gamepad_touchpad_case_t> {
40+
class VirtualHidInputTest: public ::testing::TestWithParam<gamepad_capabilities_case_t> {
4041
protected:
4142
/**
4243
* @brief Preserve the configured gamepad.
@@ -59,25 +60,34 @@ namespace {
5960
} // namespace
6061

6162
TEST_P(VirtualHidInputTest, ReportsExpectedTouchpadSupport) {
62-
const auto &[gamepad, expected] = GetParam();
63-
config::input.gamepad = gamepad;
64-
EXPECT_EQ(platf::virtualhid::configured_gamepad_supports_touchpad(), expected) << gamepad;
63+
const auto &test_case = GetParam();
64+
config::input.gamepad = test_case.gamepad;
65+
EXPECT_EQ(platf::virtualhid::configured_gamepad_supports_touchpad(), test_case.expected_touchpad) << test_case.gamepad;
66+
}
67+
68+
TEST_P(VirtualHidInputTest, ReportsExpectedControllerExtensionSupport) {
69+
const auto &test_case = GetParam();
70+
config::input.gamepad = test_case.gamepad;
71+
EXPECT_EQ(
72+
platf::virtualhid::configured_gamepad_supports_controller_extensions(),
73+
test_case.expected_controller_extensions
74+
) << test_case.gamepad;
6575
}
6676

6777
INSTANTIATE_TEST_SUITE_P(
6878
ConfiguredGamepads,
6979
VirtualHidInputTest,
7080
::testing::Values(
71-
gamepad_touchpad_case_t {"auto"sv, true},
72-
gamepad_touchpad_case_t {"generic"sv, false},
73-
gamepad_touchpad_case_t {"x360"sv, false},
74-
gamepad_touchpad_case_t {"xone"sv, false},
75-
gamepad_touchpad_case_t {"xseries"sv, false},
76-
gamepad_touchpad_case_t {"ds4"sv, true},
77-
gamepad_touchpad_case_t {"ds5"sv, true},
78-
gamepad_touchpad_case_t {"switch"sv, false}
81+
gamepad_capabilities_case_t {"auto"sv, true, true},
82+
gamepad_capabilities_case_t {"generic"sv, false, false},
83+
gamepad_capabilities_case_t {"x360"sv, false, false},
84+
gamepad_capabilities_case_t {"xone"sv, false, false},
85+
gamepad_capabilities_case_t {"xseries"sv, false, false},
86+
gamepad_capabilities_case_t {"ds4"sv, true, true},
87+
gamepad_capabilities_case_t {"ds5"sv, true, true},
88+
gamepad_capabilities_case_t {"switch"sv, false, true}
7989
),
80-
[](const ::testing::TestParamInfo<gamepad_touchpad_case_t> &info) {
90+
[](const ::testing::TestParamInfo<gamepad_capabilities_case_t> &info) {
8191
return std::string {info.param.gamepad};
8292
}
8393
);
@@ -406,6 +416,23 @@ TEST_F(VirtualHidDeviceTest, RoutesAndDeduplicatesGamepadFeedback) {
406416
ASSERT_TRUE(adapter->dispatch_output(output).ok());
407417
EXPECT_TRUE(feedback_queue()->pop(10ms));
408418

419+
output.kind = lvh::GamepadOutputKind::player_leds;
420+
output.player_leds = {true, false, true, false};
421+
output.flashing_player_leds = {false, true, false, true};
422+
ASSERT_TRUE(adapter->dispatch_output(output).ok());
423+
feedback = feedback_queue()->pop(10ms);
424+
ASSERT_TRUE(feedback);
425+
EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_player_leds);
426+
EXPECT_EQ(feedback->data.player_leds.solid, 0x05);
427+
EXPECT_EQ(feedback->data.player_leds.flashing, 0x0A);
428+
ASSERT_TRUE(adapter->dispatch_output(output).ok());
429+
EXPECT_FALSE(feedback_queue()->pop(0ms));
430+
output.player_leds[3] = true;
431+
ASSERT_TRUE(adapter->dispatch_output(output).ok());
432+
feedback = feedback_queue()->pop(10ms);
433+
ASSERT_TRUE(feedback);
434+
EXPECT_EQ(feedback->data.player_leds.solid, 0x0D);
435+
409436
output.kind = lvh::GamepadOutputKind::adaptive_triggers;
410437
output.adaptive_trigger_flags = 5;
411438
output.left_trigger_effect_type = 6;

0 commit comments

Comments
 (0)