Skip to content

Commit 91434da

Browse files
Add player LED gamepad feedback handling
Introduces a new `set_player_leds` gamepad feedback message and wires it through virtualhid and control-stream encoding. Player LED output is converted to solid/flashing bitmasks, deduplicated like other feedback signals, reset on reconnect, and sent with a new Sunshine extension packet type (`0x5504`). Unit tests were expanded to verify routing and deduplication behavior for player LED updates, and the libvirtualhid submodule was bumped to a version that provides player LED output events.
1 parent e3c8713 commit 91434da

5 files changed

Lines changed: 102 additions & 1 deletion

File tree

src/platform/common.h

Lines changed: 22 additions & 0 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;

src/platform/virtualhid_input.cpp

Lines changed: 33 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));

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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,23 @@ TEST_F(VirtualHidDeviceTest, RoutesAndDeduplicatesGamepadFeedback) {
406406
ASSERT_TRUE(adapter->dispatch_output(output).ok());
407407
EXPECT_TRUE(feedback_queue()->pop(10ms));
408408

409+
output.kind = lvh::GamepadOutputKind::player_leds;
410+
output.player_leds = {true, false, true, false};
411+
output.flashing_player_leds = {false, true, false, true};
412+
ASSERT_TRUE(adapter->dispatch_output(output).ok());
413+
feedback = feedback_queue()->pop(10ms);
414+
ASSERT_TRUE(feedback);
415+
EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_player_leds);
416+
EXPECT_EQ(feedback->data.player_leds.solid, 0x05);
417+
EXPECT_EQ(feedback->data.player_leds.flashing, 0x0A);
418+
ASSERT_TRUE(adapter->dispatch_output(output).ok());
419+
EXPECT_FALSE(feedback_queue()->pop(0ms));
420+
output.player_leds[3] = true;
421+
ASSERT_TRUE(adapter->dispatch_output(output).ok());
422+
feedback = feedback_queue()->pop(10ms);
423+
ASSERT_TRUE(feedback);
424+
EXPECT_EQ(feedback->data.player_leds.solid, 0x0D);
425+
409426
output.kind = lvh::GamepadOutputKind::adaptive_triggers;
410427
output.adaptive_trigger_flags = 5;
411428
output.left_trigger_effect_type = 6;

0 commit comments

Comments
 (0)