diff --git a/docs/windows-driver.md b/docs/windows-driver.md index 075a0b5..738938f 100644 --- a/docs/windows-driver.md +++ b/docs/windows-driver.md @@ -72,6 +72,16 @@ submit/destroy requests include that token so stale or unrelated clients cannot control devices they did not create. Input reports are submitted through VHF, and HID output writes are normalized back to the C++ output callback path. +The driver owns the VHF input buffering policy instead of allowing VHF to build +the default HID report backlog. VHF readiness notifications permit one report at +a time; while a consumer is not ready, the driver replaces superseded axis, +trigger, motion, battery, and touch-position states with the newest report. +Button, D-pad, trigger-threshold, report-ID, and touch-contact lifecycle changes +remain ordered in a bounded transition queue. This keeps continuously moving +controls close to the latest submitted state while preserving ordinary button +press and release transitions. Profile initialization replies are prioritized +over pending controller states so the Switch Pro handshake remains responsive. + The driver rejects gamepad create, destroy, and broker-instance reset IOCTLs unless the requestor token contains the `NT SERVICE\libvirtualhid_broker` service SID. On the first boot after installation, before Windows applies a diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index ba2a47d..76e3b84 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,7 @@ #include "rotating_trace_log.hpp" #include "switch_pro_protocol.hpp" #include "unique_win32_handle.hpp" +#include "vhf_input_report_queue.hpp" #include "windows_device_identity.hpp" using VhfContext = PVOID; // NOSONAR(cpp:S5008): VHF callback ABI requires PVOID; client context narrows to DeviceRecord. @@ -69,6 +71,7 @@ EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL LvhEvtIoDeviceControl; EVT_WDF_OBJECT_CONTEXT_CLEANUP LvhEvtDeviceCleanup; EVT_WDF_REQUEST_CANCEL LvhEvtOutputReadCanceled; EVT_VHF_ASYNC_OPERATION LvhEvtVhfGetFeature; +EVT_VHF_READY_FOR_NEXT_READ_REPORT LvhEvtVhfReadyForNextReadReport; EVT_VHF_ASYNC_OPERATION LvhEvtVhfSetFeature; EVT_VHF_ASYNC_OPERATION LvhEvtVhfWriteReport; @@ -85,7 +88,17 @@ namespace { decltype(&::CloseServiceHandle)>; struct DeviceRecord { + explicit DeviceRecord(const LvhWindowsCreateGamepadRequest &create_request): + request {create_request}, + pending_input_reports { + create_request.gamepad_kind, + create_request.bus_type, + create_request.hardware_ids.report_id, + } { + } + std::mutex mutex; + std::condition_variable submissions_drained; std::uint64_t driver_device_id {}; WDFDEVICE owner_device {}; WDFFILEOBJECT owner_file {}; @@ -96,6 +109,11 @@ namespace { std::vector report_descriptor; std::wstring hardware_ids; lvh::detail::windows::GenericPidFeatureState generic_pid_feature_state; + lvh::detail::windows::VhfInputReportQueue pending_input_reports; + std::shared_ptr> in_flight_input_report; + std::size_t active_input_submissions {}; + bool vhf_ready_for_input_report {}; + bool shutting_down {}; }; struct PendingOutputRequest { @@ -208,6 +226,81 @@ namespace { return iter->second; } + struct VhfInputSubmission { + VHFHANDLE vhf_handle {}; + std::shared_ptr> report; + std::uint8_t report_id {}; + }; + + std::optional prepare_vhf_input_submission(DeviceRecord &record) { + std::lock_guard lock {record.mutex}; + if (record.shutting_down || record.vhf_handle == nullptr || !record.vhf_ready_for_input_report) { + return std::nullopt; + } + + auto pending = record.pending_input_reports.pop(); + if (!pending.has_value()) { + return std::nullopt; + } + + auto report = std::make_shared>(std::move(*pending)); + const auto configured_report_id = record.request.hardware_ids.report_id; + const auto report_id = configured_report_id == 0U || report->empty() ? configured_report_id : report->front(); + + record.vhf_ready_for_input_report = false; + record.in_flight_input_report = report; + ++record.active_input_submissions; + return VhfInputSubmission { + .vhf_handle = record.vhf_handle, + .report = std::move(report), + .report_id = report_id, + }; + } + + std::optional submit_next_vhf_input_report(DeviceRecord &record) { + auto submission = prepare_vhf_input_submission(record); + if (!submission.has_value()) { + return std::nullopt; + } + + HID_XFER_PACKET packet {}; + packet.reportBuffer = submission->report->data(); + packet.reportBufferLen = static_cast(submission->report->size()); + packet.reportId = submission->report_id; + const auto status = VhfReadReportSubmit(submission->vhf_handle, &packet); + + { + std::lock_guard lock {record.mutex}; + --record.active_input_submissions; + if (!NT_SUCCESS(status) && record.in_flight_input_report == submission->report) { + record.in_flight_input_report.reset(); + } + } + record.submissions_drained.notify_all(); + + if (!NT_SUCCESS(status)) { + trace_status("submit_next_vhf_input_report VhfReadReportSubmit", status); + } + return status; + } + + NTSTATUS queue_vhf_input_report(DeviceRecord &record, std::vector report) { + { + std::lock_guard lock {record.mutex}; + if (record.shutting_down || record.vhf_handle == nullptr) { + return STATUS_OBJECT_NAME_NOT_FOUND; + } + record.pending_input_reports.push(std::move(report)); + } + + if (const auto status = submit_next_vhf_input_report(record); status.has_value()) { + return *status; + } + + std::lock_guard lock {record.mutex}; + return record.shutting_down || record.vhf_handle == nullptr ? STATUS_OBJECT_NAME_NOT_FOUND : STATUS_SUCCESS; + } + bool complete_output_request( WDFREQUEST request, const LvhWindowsOutputReportEvent &event, @@ -305,9 +398,15 @@ namespace { VHFHANDLE vhf_handle = nullptr; WDFIOTARGET vhf_io_target = nullptr; { - std::lock_guard lock {record->mutex}; + std::unique_lock lock {record->mutex}; + record->shutting_down = true; vhf_handle = record->vhf_handle; record->vhf_handle = nullptr; + record->vhf_ready_for_input_report = false; + record->pending_input_reports.clear(); + record->submissions_drained.wait(lock, [record] { + return record->active_input_submissions == 0U; + }); vhf_io_target = record->vhf_io_target; record->vhf_io_target = nullptr; } @@ -317,6 +416,11 @@ namespace { VhfDelete(vhf_handle, TRUE); } + { + std::lock_guard lock {record->mutex}; + record->in_flight_input_report.reset(); + } + if (vhf_io_target != nullptr) { trace_status("delete_vhf_device WdfObjectDelete target"); WdfObjectDelete(vhf_io_target); @@ -438,6 +542,7 @@ namespace { vhf_config.VersionNumber = record->request.hardware_ids.device_version; vhf_config.HardwareIDsLength = static_cast(record->hardware_ids.size() * sizeof(wchar_t)); vhf_config.HardwareIDs = record->hardware_ids.data(); + vhf_config.EvtVhfReadyForNextReadReport = LvhEvtVhfReadyForNextReadReport; vhf_config.EvtVhfAsyncOperationGetFeature = LvhEvtVhfGetFeature; vhf_config.EvtVhfAsyncOperationSetFeature = LvhEvtVhfSetFeature; vhf_config.EvtVhfAsyncOperationWriteReport = LvhEvtVhfWriteReport; @@ -719,22 +824,7 @@ namespace { return; } - HID_XFER_PACKET packet {}; - packet.reportBuffer = reply->data(); - packet.reportBufferLen = static_cast(reply->size()); - packet.reportId = reply->front(); - - // Keep teardown from deleting the VHF handle while the reply is being - // submitted. delete_vhf_device() clears the handle under the same mutex - // before calling VhfDelete, so no new submission can start afterward. - std::lock_guard lock {record.mutex}; - if (record.vhf_handle == nullptr) { - return; - } - const auto submit_status = VhfReadReportSubmit(record.vhf_handle, &packet); - if (!NT_SUCCESS(submit_status)) { - trace_status("switch_pro_reply VhfReadReportSubmit", submit_status); - } + static_cast(queue_vhf_input_report(record, {reply->begin(), reply->end()})); } LvhWindowsOutputReportEvent make_output_event(DeviceRecord &record, const HID_XFER_PACKET &packet) { @@ -851,11 +941,10 @@ namespace { auto &state = driver_state(); const auto driver_device_id = state.next_driver_device_id.fetch_add(1); - auto record = std::make_shared(); + auto record = std::make_shared(*create_request); record->driver_device_id = driver_device_id; record->owner_device = device; record->owner_file = WdfRequestGetFileObject(request); - record->request = *create_request; status = generate_session_token(record->session_token); if (!NT_SUCCESS(status)) { trace_status("create_gamepad token failed", status); @@ -975,13 +1064,6 @@ namespace { return; } - std::lock_guard lock {record->mutex}; - if (record->vhf_handle == nullptr) { - trace_status("submit_input_report missing vhf"); - complete_request(request, STATUS_OBJECT_NAME_NOT_FOUND); - return; - } - auto report = make_vhf_input_payload(*record, *submit_request); if (report.empty()) { trace_status("submit_input_report invalid payload"); @@ -989,16 +1071,11 @@ namespace { return; } - HID_XFER_PACKET packet {}; - packet.reportBuffer = report.data(); - packet.reportBufferLen = static_cast(report.size()); - packet.reportId = record->request.hardware_ids.report_id; - - const auto submit_status = VhfReadReportSubmit(record->vhf_handle, &packet); - if (!NT_SUCCESS(submit_status)) { - trace_status("submit_input_report VhfReadReportSubmit", submit_status); + const auto queue_status = queue_vhf_input_report(*record, std::move(report)); + if (!NT_SUCCESS(queue_status)) { + trace_status("submit_input_report queue failed", queue_status); } - complete_request(request, submit_status); + complete_request(request, queue_status); } void handle_read_output_report_request(WDFREQUEST request) { @@ -1139,6 +1216,27 @@ void LvhEvtOutputReadCanceled(WDFREQUEST request) { } } +void LvhEvtVhfReadyForNextReadReport(VhfContext vhf_client_context) { + auto *record = static_cast(vhf_client_context); + if (record == nullptr) { + return; + } + + { + std::lock_guard lock {record->mutex}; + if (record->shutting_down || record->vhf_handle == nullptr) { + return; + } + + // This callback confirms that VHF no longer references the previously + // submitted buffer and grants permission for exactly one more submission. + record->in_flight_input_report.reset(); + record->vhf_ready_for_input_report = true; + } + + static_cast(submit_next_vhf_input_report(*record)); +} + void LvhEvtVhfGetFeature( VhfContext vhf_client_context, VHFOPERATIONHANDLE vhf_operation_handle, diff --git a/src/platform/windows/shared/vhf_input_report_queue.hpp b/src/platform/windows/shared/vhf_input_report_queue.hpp new file mode 100644 index 0000000..ee3d48e --- /dev/null +++ b/src/platform/windows/shared/vhf_input_report_queue.hpp @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC +// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0 + +/** + * @file src/platform/windows/shared/vhf_input_report_queue.hpp + * @brief Bounded Windows VHF input-report coalescing. + */ +#pragma once + +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "lvh_windows_protocol.h" + +namespace lvh::detail::windows { + + /** + * @brief Maximum number of discrete input-state transitions retained while VHF is not ready. + */ + inline constexpr std::size_t vhf_max_pending_input_reports = 32U; + + /** + * @brief A bounded queue that replaces superseded continuous gamepad states. + * + * Axis, trigger, motion, battery, and touch-position changes can safely replace + * the newest pending report when the report's discrete state is unchanged. + * Button, D-pad, trigger-threshold, report-ID, and touch-contact lifecycle + * transitions remain ordered so short presses and contacts are not silently + * coalesced away. + */ + class VhfInputReportQueue { + public: + VhfInputReportQueue(std::uint32_t gamepad_kind, std::uint32_t bus_type, std::uint8_t input_report_id): + gamepad_kind_ {gamepad_kind}, + bus_type_ {bus_type}, + input_report_id_ {input_report_id} { + } + + void push(std::vector report) { + if (is_protocol_report(report)) { + make_room(); + protocol_reports_.push_back(std::move(report)); + return; + } + + if (!reports_.empty() && same_discrete_state(reports_.back(), report)) { + reports_.back() = std::move(report); + return; + } + + make_room(); + reports_.push_back(std::move(report)); + } + + [[nodiscard]] std::optional> pop() { + if (!protocol_reports_.empty()) { + auto report = std::move(protocol_reports_.front()); + protocol_reports_.pop_front(); + return report; + } + if (reports_.empty()) { + return std::nullopt; + } + + auto report = std::move(reports_.front()); + reports_.pop_front(); + return report; + } + + void clear() { + protocol_reports_.clear(); + reports_.clear(); + } + + [[nodiscard]] bool empty() const noexcept { + return protocol_reports_.empty() && reports_.empty(); + } + + [[nodiscard]] std::size_t size() const noexcept { + return protocol_reports_.size() + reports_.size(); + } + + private: + [[nodiscard]] bool is_protocol_report(const std::vector &report) const noexcept { + return input_report_id_ != 0U && !report.empty() && report.front() != input_report_id_; + } + + void make_room() { + if (size() < vhf_max_pending_input_reports) { + return; + } + if (!reports_.empty()) { + reports_.pop_front(); + } else { + protocol_reports_.pop_front(); + } + } + + static bool equal_at( + const std::vector &left, + const std::vector &right, + std::initializer_list offsets + ) { + return std::ranges::all_of(offsets, [&](const auto offset) { + return offset < left.size() && offset < right.size() && left[offset] == right[offset]; + }); + } + + [[nodiscard]] bool same_discrete_state( + const std::vector &left, + const std::vector &right + ) const { + if (left.size() != right.size()) { + return false; + } + if ( + input_report_id_ != 0U && + (left.empty() || left.front() != input_report_id_ || right.front() != input_report_id_) + ) { + return false; + } + + if (gamepad_kind_ == LVH_WINDOWS_GAMEPAD_GENERIC) { + return equal_at(left, right, {0U, 1U, 2U}); + } + if (gamepad_kind_ == LVH_WINDOWS_GAMEPAD_XBOX_ONE || gamepad_kind_ == LVH_WINDOWS_GAMEPAD_XBOX_SERIES) { + return equal_at(left, right, {12U, 13U, 14U, 15U}); + } + if (gamepad_kind_ == LVH_WINDOWS_GAMEPAD_SWITCH_PRO) { + return equal_at(left, right, {0U, 3U, 4U, 5U}); + } + + const auto is_bluetooth = bus_type_ == LVH_WINDOWS_BUS_BLUETOOTH; + if (gamepad_kind_ == LVH_WINDOWS_GAMEPAD_DUALSHOCK4) { + const auto payload_offset = is_bluetooth ? 3U : 1U; + return equal_at( + left, + right, + { + 0U, + payload_offset + 4U, + payload_offset + 5U, + payload_offset + 6U, + payload_offset + 34U, + payload_offset + 38U, + } + ); + } + if (gamepad_kind_ == LVH_WINDOWS_GAMEPAD_DUALSENSE) { + const auto payload_offset = is_bluetooth ? 2U : 1U; + return equal_at( + left, + right, + { + 0U, + payload_offset + 7U, + payload_offset + 8U, + payload_offset + 9U, + payload_offset + 32U, + payload_offset + 36U, + } + ); + } + + return false; + } + + std::uint32_t gamepad_kind_; + std::uint32_t bus_type_; + std::uint8_t input_report_id_; + std::deque> protocol_reports_; + std::deque> reports_; + }; + +} // namespace lvh::detail::windows diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 729e3ae..03c9dd1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,6 +31,7 @@ set(LIBVIRTUALHID_TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_report.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_runtime.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_broker_validation.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_vhf_input_report_queue.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_protocol.cpp") if(TARGET virtualhid_control_model) diff --git a/tests/unit/test_windows_vhf_input_report_queue.cpp b/tests/unit/test_windows_vhf_input_report_queue.cpp new file mode 100644 index 0000000..716e7c9 --- /dev/null +++ b/tests/unit/test_windows_vhf_input_report_queue.cpp @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC +// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0 + +/** + * @file tests/unit/test_windows_vhf_input_report_queue.cpp + * @brief Tests for Windows VHF input-report coalescing. + */ + +// standard includes +#include +#include +#include + +// library includes +#include +#include +#include + +// local includes +#include "vhf_input_report_queue.hpp" + +namespace { + + using lvh::detail::windows::vhf_max_pending_input_reports; + using lvh::detail::windows::VhfInputReportQueue; + + std::vector make_report(std::size_t size, std::uint8_t report_id) { + auto report = std::vector(size, 0U); + if (report_id != 0U) { + report[0] = report_id; + } + return report; + } + + TEST(WindowsVhfInputReportQueueTest, CollapsesAnAxisBurstToItsLatestState) { + VhfInputReportQueue queue {LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 1U}; + auto latest = make_report(9U, 1U); + for (std::uint8_t axis = 0U; axis < 64U; ++axis) { + latest[3] = axis; + queue.push(latest); + } + + ASSERT_EQ(queue.size(), 1U); + EXPECT_EQ(queue.pop(), latest); + EXPECT_TRUE(queue.empty()); + } + + TEST(WindowsVhfInputReportQueueTest, PreservesButtonTransitionsWhileCoalescingTheirAxes) { + VhfInputReportQueue queue {LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 1U}; + auto neutral = make_report(9U, 1U); + neutral[3] = 10U; + auto pressed = neutral; + pressed[1] = 1U; + pressed[3] = 20U; + auto pressed_latest = pressed; + pressed_latest[3] = 30U; + auto released = pressed_latest; + released[1] = 0U; + released[3] = 40U; + + queue.push(neutral); + queue.push(pressed); + queue.push(pressed_latest); + queue.push(released); + + ASSERT_EQ(queue.size(), 3U); + EXPECT_EQ(queue.pop(), neutral); + EXPECT_EQ(queue.pop(), pressed_latest); + EXPECT_EQ(queue.pop(), released); + } + + struct ProfileCase { + lvh::DeviceProfile profile; + std::uint32_t gamepad_kind; + std::uint32_t bus_type; + bool supports_touch; + }; + + TEST(WindowsVhfInputReportQueueTest, RecognizesContinuousAndDiscreteStateForEveryVhfProfile) { + const std::vector profiles { + ProfileCase {lvh::profiles::generic_gamepad(), LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, false}, + ProfileCase {lvh::profiles::xbox_one(), LVH_WINDOWS_GAMEPAD_XBOX_ONE, LVH_WINDOWS_BUS_USB, false}, + ProfileCase {lvh::profiles::xbox_series(), LVH_WINDOWS_GAMEPAD_XBOX_SERIES, LVH_WINDOWS_BUS_USB, false}, + ProfileCase {lvh::profiles::switch_pro(), LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, false}, + ProfileCase {lvh::profiles::dualshock4_usb(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_USB, true}, + ProfileCase {lvh::profiles::dualshock4_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_BLUETOOTH, true}, + ProfileCase {lvh::profiles::dualsense_usb(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_USB, true}, + ProfileCase {lvh::profiles::dualsense_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_BLUETOOTH, true}, + }; + + for (const auto &test_case : profiles) { + SCOPED_TRACE(test_case.profile.name); + VhfInputReportQueue queue { + test_case.gamepad_kind, + test_case.bus_type, + test_case.profile.report_id, + }; + auto state = lvh::GamepadState {}; + const auto initial = lvh::reports::pack_input_report(test_case.profile, state); + state.left_stick.x = 0.75F; + const auto moved = lvh::reports::pack_input_report(test_case.profile, state); + state.buttons.set(lvh::GamepadButton::a); + const auto button_changed = lvh::reports::pack_input_report(test_case.profile, state); + + ASSERT_FALSE(initial.empty()); + ASSERT_FALSE(moved.empty()); + ASSERT_FALSE(button_changed.empty()); + + queue.push(initial); + queue.push(moved); + EXPECT_EQ(queue.size(), 1U); + queue.push(button_changed); + EXPECT_EQ(queue.size(), 2U); + + if (test_case.supports_touch) { + state.touchpad_contacts[0] = {.id = 7U, .active = true, .x = 0.25F, .y = 0.5F}; + const auto touch_started = lvh::reports::pack_input_report(test_case.profile, state); + state.touchpad_contacts[0].x = 0.75F; + const auto touch_moved = lvh::reports::pack_input_report(test_case.profile, state); + + queue.push(touch_started); + EXPECT_EQ(queue.size(), 3U); + queue.push(touch_moved); + EXPECT_EQ(queue.size(), 3U); + state.touchpad_contacts[0].active = false; + queue.push(lvh::reports::pack_input_report(test_case.profile, state)); + EXPECT_EQ(queue.size(), 4U); + } + } + } + + TEST(WindowsVhfInputReportQueueTest, CoalescesContinuousTriggerMovementAndPreservesNativeThresholdChanges) { + struct TriggerCase { + lvh::DeviceProfile profile; + std::uint32_t gamepad_kind; + std::uint32_t bus_type; + bool exposes_trigger_button; + }; + + const std::vector profiles { + TriggerCase {lvh::profiles::generic_gamepad(), LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, false}, + TriggerCase {lvh::profiles::xbox_one(), LVH_WINDOWS_GAMEPAD_XBOX_ONE, LVH_WINDOWS_BUS_USB, false}, + TriggerCase {lvh::profiles::xbox_series(), LVH_WINDOWS_GAMEPAD_XBOX_SERIES, LVH_WINDOWS_BUS_USB, false}, + TriggerCase {lvh::profiles::switch_pro(), LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, true}, + TriggerCase {lvh::profiles::dualshock4_usb(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_USB, true}, + TriggerCase {lvh::profiles::dualshock4_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_BLUETOOTH, true}, + TriggerCase {lvh::profiles::dualsense_usb(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_USB, true}, + TriggerCase {lvh::profiles::dualsense_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_BLUETOOTH, true}, + }; + + for (const auto &test_case : profiles) { + SCOPED_TRACE(test_case.profile.name); + VhfInputReportQueue queue { + test_case.gamepad_kind, + test_case.bus_type, + test_case.profile.report_id, + }; + auto state = lvh::GamepadState {}; + state.left_trigger = 0.1F; + queue.push(lvh::reports::pack_input_report(test_case.profile, state)); + state.left_trigger = 0.9F; + queue.push(lvh::reports::pack_input_report(test_case.profile, state)); + EXPECT_EQ(queue.size(), 1U); + + state.left_trigger = 0.0F; + queue.push(lvh::reports::pack_input_report(test_case.profile, state)); + EXPECT_EQ(queue.size(), test_case.exposes_trigger_button ? 2U : 1U); + } + } + + TEST(WindowsVhfInputReportQueueTest, PrioritizesSwitchProtocolRepliesWithoutCoalescingThem) { + VhfInputReportQueue queue {LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, 0x30U}; + auto controller_state = make_report(64U, 0x30U); + auto first_reply = make_report(64U, 0x21U); + auto second_reply = first_reply; + first_reply[15] = 1U; + second_reply[15] = 2U; + + queue.push(controller_state); + queue.push(first_reply); + queue.push(second_reply); + + ASSERT_EQ(queue.size(), 3U); + EXPECT_EQ(queue.pop(), first_reply); + EXPECT_EQ(queue.pop(), second_reply); + EXPECT_EQ(queue.pop(), controller_state); + } + + TEST(WindowsVhfInputReportQueueTest, BoundsSwitchProtocolReplyHistory) { + VhfInputReportQueue queue {LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, 0x30U}; + constexpr auto submitted_reports = vhf_max_pending_input_reports + 8U; + for (std::size_t index = 0U; index < submitted_reports; ++index) { + auto reply = make_report(64U, 0x21U); + reply[15] = static_cast(index); + queue.push(std::move(reply)); + } + + ASSERT_EQ(queue.size(), vhf_max_pending_input_reports); + const auto first = queue.pop(); + ASSERT_TRUE(first.has_value()); + EXPECT_EQ((*first)[15], 8U); + } + + TEST(WindowsVhfInputReportQueueTest, KeepsOnlyTheNewestBoundedTransitionHistory) { + VhfInputReportQueue queue {LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 1U}; + constexpr auto submitted_reports = vhf_max_pending_input_reports + 8U; + for (std::size_t index = 0U; index < submitted_reports; ++index) { + auto report = make_report(9U, 1U); + report[1] = static_cast(index % 2U); + report[3] = static_cast(index); + queue.push(std::move(report)); + } + + ASSERT_EQ(queue.size(), vhf_max_pending_input_reports); + const auto first = queue.pop(); + ASSERT_TRUE(first.has_value()); + EXPECT_EQ((*first)[3], 8U); + + auto last = first; + while (!queue.empty()) { + last = queue.pop(); + } + ASSERT_TRUE(last.has_value()); + EXPECT_EQ((*last)[3], submitted_reports - 1U); + } + + TEST(WindowsVhfInputReportQueueTest, KeepsMalformedAndUnknownReportsDistinctAndCanClearThem) { + VhfInputReportQueue malformed_queue {LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 0U}; + malformed_queue.push({}); + malformed_queue.push({}); + malformed_queue.push({0U}); + EXPECT_EQ(malformed_queue.size(), 3U); + malformed_queue.clear(); + EXPECT_TRUE(malformed_queue.empty()); + EXPECT_FALSE(malformed_queue.pop().has_value()); + + constexpr auto unknown_gamepad_kind = 0xFFFFFFFFU; + VhfInputReportQueue unknown_queue {unknown_gamepad_kind, LVH_WINDOWS_BUS_UNKNOWN, 0U}; + unknown_queue.push(make_report(9U, 0U)); + unknown_queue.push(make_report(9U, 0U)); + EXPECT_EQ(unknown_queue.size(), 2U); + } + +} // namespace