Skip to content

Commit b2eb32d

Browse files
Unify Switch Pro packet counter across report types
Replace the separate full-state timer (incrementing by 3) with a single per-device counter shared by both subcommand replies (0x21) and full-state input reports (0x30). The counter now increments by 1 per report, matching native controller behavior. On Windows the counter is stored per-device in the VHF record and stamped at submission time. The shared `next_switch_pro_packet_timer` helper is moved to `switch_pro_protocol.hpp` and accepts an optional override so callers can inject a specific value.
1 parent 16266b7 commit b2eb32d

8 files changed

Lines changed: 53 additions & 38 deletions

File tree

docs/end-user-gamepad-guide.md

Lines changed: 19 additions & 19 deletions
Large diffs are not rendered by default.

docs/platform-support.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ Switch Pro USB and subcommand initialization sequence and accepts the native
8383
`0x30` input layout, so descriptor-aware consumers can initialize those
8484
controllers before sending their native output reports. The Switch Pro profile
8585
uses the `0x0210` hardware revision reported by a physical Nintendo controller,
86-
and the Windows VHF device exposes that revision to HID consumers. Consecutive
87-
full-state reports also advance Nintendo's packet timer by their three packed
88-
IMU samples so consumers can distinguish new sensor batches.
86+
and the Windows VHF device exposes that revision to HID consumers. Full-state
87+
and subcommand-reply reports use the same per-device packet counter on Windows,
88+
matching the counter that a native controller advances for every input report.
8989

9090
Windows VHF devices do not expose a Bluetooth transport identity to HIDAPI.
9191
The Windows backend therefore reports DualShock 4 and DualSense requests as

src/core/report.cpp

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#include <utility>
1919

2020
// local includes
21+
#include "shared/switch_pro_protocol.hpp"
22+
2123
#include <libvirtualhid/report.hpp>
2224

2325
namespace lvh::reports {
@@ -83,13 +85,6 @@ namespace lvh::reports {
8385

8486
constexpr float switch_gyroscope_scale = 14.2842F;
8587

86-
std::uint8_t switch_pro_packet_timer() {
87-
// Native full-state reports carry three IMU samples spaced five
88-
// milliseconds apart, so the controller timer advances by three.
89-
static std::atomic_uint32_t packet_timer = 0;
90-
return static_cast<std::uint8_t>(packet_timer.fetch_add(3U, std::memory_order_relaxed));
91-
}
92-
9388
std::uint8_t decode_switch_home_light_intensity(std::byte encoded_intensity) {
9489
const auto intensity = std::to_integer<std::uint8_t>(encoded_intensity >> 4U);
9590
if (intensity == 0U) {
@@ -1239,7 +1234,7 @@ namespace lvh::reports {
12391234

12401235
ByteReport report(profile.input_report_size, zero_byte);
12411236
report[0] = to_byte(profile.report_id);
1242-
report[1] = to_byte(switch_pro_packet_timer());
1237+
report[1] = to_byte(detail::switch_pro_protocol::next_switch_pro_packet_timer());
12431238
report[2] = switch_battery_and_connection(normalized.battery);
12441239

12451240
if (normalized.buttons.test(x)) {

src/platform/windows/driver/libvirtualhid_umdf.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ namespace {
122122
lvh::detail::windows::VhfInputReportQueue pending_input_reports;
123123
std::shared_ptr<std::vector<std::uint8_t>> in_flight_input_report;
124124
std::size_t active_input_submissions {};
125+
std::uint8_t switch_pro_packet_timer {};
125126
bool vhf_ready_for_input_report {};
126127
bool shutting_down {};
127128
};
@@ -254,6 +255,12 @@ namespace {
254255
}
255256

256257
auto report = std::make_shared<std::vector<std::uint8_t>>(std::move(*pending));
258+
if (
259+
record.request.gamepad_kind == LVH_WINDOWS_GAMEPAD_SWITCH_PRO && report->size() > 1U &&
260+
(report->at(0) == 0x21U || report->at(0) == 0x30U)
261+
) {
262+
report->at(1) = record.switch_pro_packet_timer++;
263+
}
257264
const auto configured_report_id = record.request.hardware_ids.report_id;
258265
const auto report_id = configured_report_id == 0U || report->empty() ? configured_report_id : report->front();
259266

src/shared/switch_pro_protocol.hpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// standard includes
88
#include <algorithm>
99
#include <array>
10+
#include <atomic>
1011
#include <cstddef>
1112
#include <cstdint>
1213
#include <optional>
@@ -17,6 +18,11 @@ namespace lvh::detail::switch_pro_protocol {
1718
inline constexpr std::size_t switch_pro_report_size = 64U;
1819
using SwitchProReport = std::array<std::uint8_t, switch_pro_report_size>;
1920

21+
inline std::uint8_t next_switch_pro_packet_timer() {
22+
static std::atomic_uint32_t packet_timer {0};
23+
return static_cast<std::uint8_t>(packet_timer.fetch_add(1U, std::memory_order_relaxed));
24+
}
25+
2026
namespace switch_pro_protocol_detail {
2127

2228
inline constexpr std::array<std::uint8_t, 6> controller_mac {0x02, 0x00, 0x00, 0x00, 0x00, 0x01};
@@ -149,7 +155,10 @@ namespace lvh::detail::switch_pro_protocol {
149155

150156
} // namespace switch_pro_protocol_detail
151157

152-
inline std::optional<SwitchProReport> make_switch_pro_reply(std::span<const std::uint8_t> output_report) {
158+
inline std::optional<SwitchProReport> make_switch_pro_reply(
159+
std::span<const std::uint8_t> output_report,
160+
std::optional<std::uint8_t> packet_timer = std::nullopt
161+
) {
153162
if (output_report.empty()) {
154163
return std::nullopt;
155164
}
@@ -184,7 +193,10 @@ namespace lvh::detail::switch_pro_protocol {
184193

185194
SwitchProReport reply {};
186195
reply[0] = 0x21;
187-
switch_pro_protocol_detail::set_neutral_controller_state(reply, output_report[1]);
196+
switch_pro_protocol_detail::set_neutral_controller_state(
197+
reply,
198+
packet_timer.has_value() ? *packet_timer : next_switch_pro_packet_timer()
199+
);
188200
auto acknowledgement = std::uint8_t {0x80};
189201
if (subcommand == 0x10U) {
190202
acknowledgement = 0x90;

tests/unit/test_report.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,15 @@ TEST(ReportTest, PacksSwitchProReport) {
205205
}
206206
}
207207

208-
TEST(ReportTest, AdvancesSwitchProPacketTimerForEachImuBatch) {
208+
TEST(ReportTest, AdvancesSwitchProPacketTimerForEachInputReport) {
209209
const auto profile = lvh::profiles::switch_pro();
210210

211211
const auto first = lvh::reports::pack_input_report(profile, {});
212212
const auto second = lvh::reports::pack_input_report(profile, {});
213213

214214
ASSERT_EQ(first.size(), profile.input_report_size);
215215
ASSERT_EQ(second.size(), profile.input_report_size);
216-
EXPECT_EQ(second[1], static_cast<std::uint8_t>(first[1] + 3U));
216+
EXPECT_EQ(second[1], static_cast<std::uint8_t>(first[1] + 1U));
217217
}
218218

219219
TEST(ReportTest, PacksSwitchProMotionInEveryImuSample) {

tests/unit/test_windows_consumers.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,7 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
858858
ASSERT_TRUE(player_lights_reply.has_value()) << "No Switch player-light acknowledgement reached the HID client";
859859
ASSERT_GE(player_lights_reply->size(), 15U);
860860
EXPECT_EQ(player_lights_reply->at(0), 0x21U);
861-
EXPECT_EQ(player_lights_reply->at(1), 0x08U);
861+
EXPECT_EQ(player_lights_reply->at(1), 0x00U);
862862
EXPECT_EQ(player_lights_reply->at(13), 0x80U);
863863
EXPECT_EQ(player_lights_reply->at(14), 0x30U);
864864

@@ -885,6 +885,7 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
885885
ASSERT_TRUE(input.has_value()) << "No native Switch 0x30 input report reached the HID client";
886886
ASSERT_EQ(input->size(), profile.input_report_size);
887887
EXPECT_EQ(input->at(0), 0x30U);
888+
EXPECT_EQ(input->at(1), static_cast<std::uint8_t>(player_lights_reply->at(1) + 1U));
888889
EXPECT_EQ(input->at(3), 0x08U); // Nintendo A in the native right-button byte.
889890
EXPECT_EQ(input->at(4), 0x14U); // R3 and Home.
890891
EXPECT_EQ(input->at(5), 0x08U); // D-pad left.
@@ -914,7 +915,7 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
914915
const auto next_input = read_hid_report_with_timeout(reader.get(), hid_interface->input_report_size, 5s);
915916
ASSERT_TRUE(next_input.has_value()) << "No second native Switch motion report reached the HID client";
916917
ASSERT_EQ(next_input->size(), profile.input_report_size);
917-
EXPECT_EQ(next_input->at(1), static_cast<std::uint8_t>(input->at(1) + 3U));
918+
EXPECT_EQ(next_input->at(1), static_cast<std::uint8_t>(input->at(1) + 1U));
918919
EXPECT_NE(next_input->at(19), input->at(19));
919920
ASSERT_TRUE(created.adapter->close().ok());
920921
}

tests/unit/test_windows_driver_protocol.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ TEST_F(WindowsDriverProtocolTest, SwitchProRepliesToUsbStatusAndHandshakeCommand
140140
TEST_F(WindowsDriverProtocolTest, SwitchProAcknowledgesInitializationSubcommands) {
141141
for (const auto subcommand : {0x03U, 0x30U, 0x38U, 0x40U, 0x41U, 0x48U}) {
142142
const auto report = switch_output_report(0x01, static_cast<std::uint8_t>(subcommand));
143-
const auto reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(report);
143+
const auto reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(report, 0x42U);
144144
ASSERT_TRUE(reply.has_value());
145145
EXPECT_EQ(reply->at(0), 0x21);
146-
EXPECT_EQ(reply->at(1), 0x07);
146+
EXPECT_EQ(reply->at(1), 0x42);
147147
EXPECT_EQ(reply->at(2), 0x81);
148148
EXPECT_EQ(reply->at(13), 0x80);
149149
EXPECT_EQ(reply->at(14), subcommand);

0 commit comments

Comments
 (0)