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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/windows-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,10 @@ observed from physical Xbox Series USB and Xbox Wireless Adapter connections.
The VHF child preserves the native 17-byte GIP-shaped input report, and the
last byte carries battery strength for both Xbox One and Xbox Series. The report
parser accepts the native eight-byte four-motor Xbox payload when a consumer
delivers it. The Xbox 360 profile is rejected by the UMDF/VHF backend because a
delivers it. The Windows backend submits Xbox input only when the packed state
changes. State transitions still reach VHF, while raw HID consumers are not
asked to reinterpret the same unchanged Xbox state as fresh input. The Xbox 360
profile is rejected by the UMDF/VHF backend because a
real Xbox 360 controller is an XUSB device rather than a VHF HID gamepad.

DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
Expand Down
23 changes: 23 additions & 0 deletions src/platform/windows/windows_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,8 @@ namespace lvh::detail {

std::shared_ptr<WindowsBackendContext> context_;
std::shared_ptr<WindowsVhfDeviceState> state_;
std::mutex input_report_mutex_;
std::optional<std::vector<std::uint8_t>> last_input_report_;
std::condition_variable switch_pro_report_ready_;
std::mutex switch_pro_report_mutex_;
std::jthread switch_pro_report_thread_;
Expand Down Expand Up @@ -1368,6 +1370,8 @@ namespace lvh::detail {
) {
using enum ErrorCode;

auto suppress_unchanged_report = false;

{
std::lock_guard lock {state_->mutex_};
if (!state_->open) {
Expand All @@ -1386,6 +1390,25 @@ namespace lvh::detail {
if (state_->uses_generic_pid) {
return context_->submit_device_report(state_, windows::make_generic_windows_input_report(report));
}

suppress_unchanged_report =
state_->profile.gamepad_kind == GamepadProfileKind::xbox_one ||
state_->profile.gamepad_kind == GamepadProfileKind::xbox_series;
}

if (suppress_unchanged_report) {
// Xbox input reports have no sequence or timestamp field. Keep raw HID
// consumers transition-driven when a streaming host repeats a held state.
std::lock_guard lock {input_report_mutex_};
if (last_input_report_.has_value() && *last_input_report_ == report) {
return OperationStatus::success();
}

auto status = context_->submit_device_report(state_, report);
if (status.ok()) {
last_input_report_ = report;
}
return status;
}

return context_->submit_device_report(state_, report);
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ namespace lvh::detail::test {
std::vector<std::vector<std::uint8_t>> submitted_reports;
};

struct WindowsXboxInputDeduplicationResult {
OperationStatus create_status;
OperationStatus left_shoulder_status;
OperationStatus repeated_left_shoulder_status;
OperationStatus right_shoulder_status;
OperationStatus repeated_right_shoulder_status;
OperationStatus release_status;
OperationStatus close_status;
std::vector<std::vector<std::uint8_t>> submitted_reports;
};

struct WindowsBackendFailureResult {
OperationStatus invalid_argument_status;
OperationStatus unsupported_profile_status;
Expand Down Expand Up @@ -249,6 +260,7 @@ namespace lvh::detail::test {

WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle();
WindowsSwitchReportStreamResult windows_backend_switch_report_stream();
WindowsXboxInputDeduplicationResult windows_backend_xbox_input_deduplication(GamepadProfileKind kind);
WindowsPlayStationTransportResult windows_backend_playstation_transport();
WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering();
WindowsHidKeyboardResult windows_backend_hid_keyboard();
Expand Down
28 changes: 28 additions & 0 deletions tests/fixtures/windows_backend_test_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,34 @@ namespace lvh::detail {
return result;
}

WindowsXboxInputDeduplicationResult windows_backend_xbox_input_deduplication(GamepadProfileKind kind) {
WindowsXboxInputDeduplicationResult result;
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());

CreateGamepadOptions options;
options.profile = kind == GamepadProfileKind::xbox_one ? profiles::xbox_one() : profiles::xbox_series();
auto created = backend->create_gamepad(9, options);
result.create_status = created.status;
if (created) {
auto report = std::vector<std::uint8_t>(options.profile.input_report_size, 0U);
report[12] = 0x10U;
result.left_shoulder_status = created.gamepad->submit({}, report);
result.repeated_left_shoulder_status = created.gamepad->submit({}, report);

report[12] = 0x20U;
result.right_shoulder_status = created.gamepad->submit({}, report);
result.repeated_right_shoulder_status = created.gamepad->submit({}, report);

report[12] = 0U;
result.release_status = created.gamepad->submit({}, report);
result.close_status = created.gamepad->close();
result.submitted_reports = command_state->submit_reports();
}

return result;
}

WindowsHidMouseResult windows_backend_hid_mouse() {
using enum MouseEventKind;

Expand Down
19 changes: 19 additions & 0 deletions tests/unit/test_windows_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ TEST_F(WindowsBackendTest, SwitchReportsStreamAtTheNativeCadence) {
}));
}

TEST_F(WindowsBackendTest, XboxReportsOnlySubmitShoulderStateTransitions) {
for (const auto kind : {lvh::GamepadProfileKind::xbox_one, lvh::GamepadProfileKind::xbox_series}) {
SCOPED_TRACE(static_cast<int>(kind));
const auto result = lvh::detail::test::windows_backend_xbox_input_deduplication(kind);

expect_ok(result.create_status);
expect_ok(result.left_shoulder_status);
expect_ok(result.repeated_left_shoulder_status);
expect_ok(result.right_shoulder_status);
expect_ok(result.repeated_right_shoulder_status);
expect_ok(result.release_status);
expect_ok(result.close_status);
ASSERT_EQ(result.submitted_reports.size(), 3U);
EXPECT_EQ(result.submitted_reports[0][12], 0x10U);
EXPECT_EQ(result.submitted_reports[1][12], 0x20U);
EXPECT_EQ(result.submitted_reports[2][12], 0U);
}
}

TEST_F(WindowsBackendTest, PlayStationDefaultsUseEffectiveUsbProfiles) {
const auto result = lvh::detail::test::windows_backend_playstation_transport();
const auto dualshock4_usb = lvh::profiles::dualshock4_usb();
Expand Down
82 changes: 82 additions & 0 deletions tests/unit/test_windows_consumers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,88 @@ TEST_F(WindowsConsumerTest, NativeXboxPidRumbleWritesAreNormalized) {
ASSERT_TRUE(created.adapter->close().ok());
}

TEST_F(WindowsConsumerTest, XboxNativeInputSuppressesRepeatedHeldShoulderReports) {
lvh::RuntimeOptions runtime_options;
runtime_options.backend = lvh::BackendKind::platform_default;
auto runtime = lvh::Runtime::create(runtime_options);
ASSERT_NE(runtime, nullptr);
ASSERT_TRUE(runtime->capabilities().supports_gamepad)
<< "The installed libvirtualhid Windows driver is required for this integration test";

auto profile = lvh::profiles::xbox_one();
// Avoid a report descriptor cached for an older installed-driver identity.
profile.vendor_id = 0x1234U;
profile.product_id = 0x5670U;
profile.version = 0x0001U;
const auto previous_paths = current_gamepad_interface_paths();

lvh::CreateGamepadOptions options;
options.profile = profile;
options.metadata.stable_id = "native-shoulder-hold-test";
auto created = lvh::GamepadStateAdapter::create(*runtime, options);
ASSERT_TRUE(created) << created.status.message();

const auto hid_interface = wait_for_new_interface(previous_paths, profile.vendor_id, profile.product_id);
ASSERT_TRUE(hid_interface.has_value()) << "The VHF Xbox HID interface was not enumerated";
ASSERT_EQ(hid_interface->input_report_size, profile.input_report_size + 1U);

Handle reader {CreateFileW(
hid_interface->path.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
nullptr
)};
ASSERT_TRUE(reader) << "Unable to open the VHF Xbox HID interface: " << GetLastError();

lvh::GamepadState state;
state.buttons.set(lvh::GamepadButton::left_shoulder);
ASSERT_TRUE(created.adapter->set_state(state).ok());
const auto left_pressed = read_hid_report_matching(
reader.get(),
hid_interface->input_report_size,
5s,
[](const auto &report) {
return report.size() > 13U && (report[13] & 0x30U) == 0x10U;
}
);
ASSERT_TRUE(left_pressed.has_value()) << "No Xbox LB press reached the native HID client";

for (auto repeat = 0U; repeat < 32U; ++repeat) {
ASSERT_TRUE(created.adapter->set_state(state).ok());
}
EXPECT_FALSE(read_hid_report_with_timeout(reader.get(), hid_interface->input_report_size, 200ms).has_value())
<< "An unchanged held LB state was resubmitted to the native HID client";

state.buttons.reset(lvh::GamepadButton::left_shoulder);
state.buttons.set(lvh::GamepadButton::right_shoulder);
ASSERT_TRUE(created.adapter->set_state(state).ok());
const auto right_pressed = read_hid_report_matching(
reader.get(),
hid_interface->input_report_size,
5s,
[](const auto &report) {
return report.size() > 13U && (report[13] & 0x30U) == 0x20U;
}
);
ASSERT_TRUE(right_pressed.has_value()) << "No Xbox RB press reached the native HID client";

state.buttons.reset(lvh::GamepadButton::right_shoulder);
ASSERT_TRUE(created.adapter->set_state(state).ok());
const auto released = read_hid_report_matching(
reader.get(),
hid_interface->input_report_size,
5s,
[](const auto &report) {
return report.size() > 13U && (report[13] & 0x30U) == 0U;
}
);
ASSERT_TRUE(released.has_value()) << "No Xbox shoulder release reached the native HID client";
ASSERT_TRUE(created.adapter->close().ok());
}

TEST_F(WindowsConsumerTest, BatteryStateIsAvailableThroughGetInputReport) {
const std::array profiles {
lvh::profiles::xbox_one(),
Expand Down
Loading