#include "controller_input.h" #include "model.h" #include "input/bluepad32_input_backend.h" #include "input/switch2_mouse_capture.h" #include "platform/pico/bootsel_pairing_button.h" #include "parser/uni_hid_parser_switch2.h" #include "pico/stdlib.h" #include #include #include #include static uint64_t now; static uint32_t stage; static BootselPairingButtonEvent next_button_event = BootselPairingButtonEvent::kNone; static unsigned pairing_requests, clear_requests, button_polls; BootselPairingButtonEvent bootsel_pairing_button_task() { ++button_polls; const auto event = next_button_event; next_button_event = BootselPairingButtonEvent::kNone; return event; } void bluepad32_input_backend_open_pairing_window() { ++pairing_requests; } uint32_t bluepad32_input_backend_clear_pairings() { ++clear_requests; return 1; } static const uint8_t source_address[] = {0x98,0xe2,0x55,7,0xdf,0}; static const uint8_t other_address[] = {0x98,0xe2,0x55,7,0xdf,1}; static constexpr uint16_t other_product_id = SWITCH2_PROBE_JOYCON_LEFT ? 0x2066 : 0x2067; static constexpr uint8_t other_report_id = SWITCH2_PROBE_JOYCON_LEFT ? 8 : 7; void system_clock_initialize() {} void bluepad32_input_backend_init() { stage = 1; } void controller_profile_runtime_reset() {} void bluepad32_input_backend_start() { stage = 2; } void bluepad32_input_backend_diagnostics(Bluepad32BackendDiagnostics* out) { *out = {}; out->initialization_stage = stage; } absolute_time_t make_timeout_time_ms(uint32_t timeout) { return now + timeout; } absolute_time_t get_absolute_time() { return now; } uint32_t to_ms_since_boot(absolute_time_t value) { return static_cast(value); } bool time_reached(absolute_time_t deadline) { return now >= deadline; } void sleep_ms(uint32_t milliseconds) { now += milliseconds; } using NativeReport = std::array; static NativeReport native_report(uint8_t counter, uint8_t motion_length, int16_t x = 1, int16_t y = -2) { NativeReport report{}; // Deliberately opaque, nonzero bytes, including NFC and reserved fields. // The model-specific length declares 30/40 packed motion bytes; do not // decode them or normalize the unused tail of a 30-byte sample. for (size_t i = 0; i < report.size(); ++i) report[i] = static_cast((i * 37 + counter) % 255 + 1); report[0] = counter; report[1] = 0x93; report[2] = 0x12; report[3] = 0xd1; report[4] = 0xe7; report[5] = 0x23; report[6] = 0x81; report[7] = 0x45; report[8] = 0x38; report[9] = static_cast(x); report[10] = static_cast(x) >> 8; report[11] = static_cast(y); report[12] = static_cast(y) >> 8; report[13] = 0x1b; report[PROBE_IMU_LENGTH_OFFSET] = motion_length; return report; } static void emit(const NativeReport& report, const uint8_t* address = source_address, uint16_t product_id = PROBE_JOYCON_PID, uint8_t report_id = PROBE_NATIVE_REPORT_ID, uint16_t length = 63) { assert(length <= report.size()); switch_pico_switch2_mouse_report(product_id, address, report_id, report.data(), length, static_cast(now)); } static void disconnect(const uint8_t* address = source_address, uint16_t product_id = PROBE_JOYCON_PID) { switch_pico_switch2_mouse_report(product_id, address, 0, nullptr, 0, static_cast(now)); } static probe_controller_input poll(uint32_t timestamp = static_cast(now), uint8_t instance = 0) { probe_controller_input input{}; probe_controller_input_poll(instance, timestamp, &input); return input; } static uint32_t expect_report(const NativeReport& expected, uint32_t timestamp = static_cast(now), uint8_t instance = 0) { NativeReport actual; actual.fill(0xa5); const uint32_t serial = probe_controller_input_peek_native_report(instance, timestamp, actual.data()); assert(serial != 0 && actual == expected); return serial; } static void expect_empty(uint8_t instance = 0) { NativeReport actual; actual.fill(0xa5); const auto untouched = actual; assert(probe_controller_input_peek_native_report(instance, static_cast(now), actual.data()) == 0); assert(actual == untouched); } static void expect_inactive(const probe_controller_input& input) { assert(!input.active && input.mouse_epoch == 0); assert(input.buttons[0] == 0 && input.buttons[1] == 0); assert(input.stick[0] == 0 && input.stick[1] == 0 && input.stick[2] == 0); assert(input.native_status == 0 && input.mouse_surface == 0); assert(input.mouse_total_x == 0 && input.mouse_total_y == 0); } static void test_startup_pairing_and_stream_gate() { next_button_event = BootselPairingButtonEvent::kOpenPairing; assert(!probe_controller_input_pairing_task() && button_polls == 0 && pairing_requests == 0); probe_controller_input_set_native_stream(0, true); expect_empty(); assert(!probe_controller_input_commit_native_report(0, 1)); expect_inactive(poll()); probe_controller_input_clock_init(); probe_controller_input_init(); // Even an enable request after init must not open the pre-flash-ready gate. probe_controller_input_set_native_stream(0, true); const auto report = native_report(0x31, 30, -6, 9); emit(report); expect_empty(); assert(probe_controller_input_start()); expect_empty(); assert(probe_controller_input_pairing_task() && pairing_requests == 1); assert(!probe_controller_input_pairing_task()); next_button_event = BootselPairingButtonEvent::kClearPairings; assert(!probe_controller_input_pairing_task() && clear_requests == 0 && pairing_requests == 1); next_button_event = BootselPairingButtonEvent::kOpenPairing; assert(probe_controller_input_pairing_task() && pairing_requests == 2 && clear_requests == 0); const auto input = poll(); assert(input.active && input.buttons[0] == 0x12 && input.buttons[1] == 0xd1); assert(input.stick[0] == 0x23 && input.stick[1] == 0x81 && input.stick[2] == 0x45); assert(input.native_status == 0x38 && input.mouse_surface == 0x1b); assert(input.mouse_total_x == -6 && input.mouse_total_y == 9); probe_controller_input_set_native_stream(0, true); expect_empty(); // Enabling never replays the latest input or raw ring. emit(report); const uint32_t pending = expect_report(report); probe_controller_input_set_native_stream(0, false); assert(!probe_controller_input_commit_native_report(0, pending)); emit(report); // Selected input continues updating while native USB is gated. assert(poll().active); expect_empty(); probe_controller_input_set_native_stream(0, true); expect_empty(); emit(report); const uint32_t resumed = expect_report(report); assert(resumed > pending); assert(probe_controller_input_commit_native_report(0, resumed)); expect_empty(); } static void test_opaque_fidelity_order_and_retry() { now = 100; const auto first = native_report(0xfe, 30, -6, 9); const auto repeated = native_report(0xff, 40, -32768, 32767); const auto last = native_report(0x00, 30, 1, -2); emit(first); const uint32_t first_serial = expect_report(first); // A failed USB submission simply does not commit. New arrivals must not // overwrite that retry, combine deltas, or collapse identical packets. ++now; emit(repeated); ++now; emit(repeated); ++now; emit(last); const uint32_t last_serial = poll().serial; assert(last_serial > first_serial); assert(!probe_controller_input_commit_native_report(0, last_serial)); assert(!probe_controller_input_commit_native_report(0, 0)); probe_controller_input_set_native_stream(0, true); assert(expect_report(first) == first_serial); assert(expect_report(first) == first_serial); assert(probe_controller_input_commit_native_report(0, first_serial)); assert(!probe_controller_input_commit_native_report(0, first_serial)); const uint32_t second_serial = expect_report(repeated); assert(second_serial > first_serial); assert(probe_controller_input_commit_native_report(0, second_serial)); const uint32_t third_serial = expect_report(repeated); assert(third_serial > second_serial); assert(!probe_controller_input_commit_native_report(0, second_serial)); assert(expect_report(repeated) == third_serial); assert(probe_controller_input_commit_native_report(0, third_serial)); assert(expect_report(last) == last_serial); assert(probe_controller_input_commit_native_report(0, last_serial)); expect_empty(); assert(!probe_controller_input_commit_native_report(0, last_serial)); expect_empty(); // No cached duplicate report when the source has not advanced. } static void test_selected_source_isolation_and_reconnect() { now = 200; const auto first = native_report(0x41, 30, -17, 19); const auto second = native_report(0x42, 40, 31, -37); const auto unrelated = native_report(0x99, 40, 300, 300); emit(first); const auto selected = poll(); const uint32_t first_serial = expect_report(first); assert(first_serial == selected.serial); for (unsigned i = 0; i < 30; ++i) emit(unrelated, other_address); emit(unrelated, source_address, other_product_id, other_report_id); emit(unrelated, source_address, PROBE_JOYCON_PID, other_report_id); emit(unrelated, source_address, PROBE_JOYCON_PID, 5); emit(unrelated, source_address, PROBE_JOYCON_PID, 0xc0, 12); emit(unrelated, source_address, PROBE_JOYCON_PID, PROBE_NATIVE_REPORT_ID, 62); emit(unrelated, source_address, PROBE_JOYCON_PID, 0, 1); // Not a teardown. uint8_t oversized[64]; memcpy(oversized, unrelated.data(), unrelated.size()); oversized[63] = 0x5a; switch_pico_switch2_mouse_report(PROBE_JOYCON_PID, source_address, PROBE_NATIVE_REPORT_ID, oversized, sizeof(oversized), static_cast(now)); disconnect(other_address); disconnect(source_address, other_product_id); const auto isolated = poll(); assert(isolated.active && isolated.serial == selected.serial); assert(isolated.mouse_epoch == selected.mouse_epoch); assert(isolated.mouse_total_x == selected.mouse_total_x && isolated.mouse_total_y == selected.mouse_total_y); assert(expect_report(first) == first_serial); ++now; emit(second); const uint32_t second_serial = poll().serial; assert(probe_controller_input_commit_native_report(0, first_serial)); assert(expect_report(second) == second_serial); assert(probe_controller_input_commit_native_report(0, second_serial)); expect_empty(); // Unrelated ring entries neither evict nor enter the FIFO. emit(first); const uint32_t disconnected_serial = expect_report(first); disconnect(); ++now; emit(second); // Disconnect and reconnect both occur between polls. const auto reconnected = poll(); assert(reconnected.active && reconnected.mouse_epoch != selected.mouse_epoch); assert(reconnected.mouse_total_x == 31 && reconnected.mouse_total_y == -37); assert(!probe_controller_input_commit_native_report(0, disconnected_serial)); assert(expect_report(second) == reconnected.serial); assert(reconnected.serial > disconnected_serial); assert(probe_controller_input_commit_native_report(0, reconnected.serial)); expect_empty(); emit(first); const uint32_t pending = expect_report(first); disconnect(); for (unsigned i = 0; i < 30; ++i) emit(unrelated, other_address); expect_inactive(poll()); expect_empty(); assert(!probe_controller_input_commit_native_report(0, pending)); ++now; emit(second); const auto resumed = poll(); assert(resumed.active && resumed.mouse_epoch != reconnected.mouse_epoch); const uint32_t resumed_serial = expect_report(second); assert(resumed_serial > pending); assert(probe_controller_input_commit_native_report(0, resumed_serial)); emit(first); const uint32_t old_source = expect_report(first); emit(unrelated, other_address); switch2_mouse_capture_select_input(0, other_address, PROBE_JOYCON_PID); expect_inactive(poll()); expect_empty(); assert(!probe_controller_input_commit_native_report(0, old_source)); probe_controller_input_set_native_stream(0, true); expect_empty(); // Selection cannot revive the other peer's raw history. emit(first); expect_empty(); emit(unrelated, other_address); const uint32_t new_source = expect_report(unrelated); assert(new_source > old_source); switch2_mouse_capture_select_input(0, source_address, PROBE_JOYCON_PID); expect_inactive(poll()); probe_controller_input_set_native_stream(0, true); expect_empty(); assert(!probe_controller_input_commit_native_report(0, new_source)); emit(second); const uint32_t restored = expect_report(second); assert(restored > new_source); assert(probe_controller_input_commit_native_report(0, restored)); } static void test_side_switch_and_sample_ownership() { now = 500; const auto first = native_report(0x71, 30); auto opposite = native_report(0x72, 40); opposite[SWITCH2_PROBE_JOYCON_LEFT ? 15 : 14] = 40; emit(first); const uint32_t old_packet = expect_report(first); assert(poll().active); uint64_t old_cue = 0; assert(probe_controller_input_play_sample(0, 3, &old_cue) && old_cue != 0); uint64_t taken = 0; uint8_t sample = 0; // Unrelated callers cannot take a cue, even with a timestamp that would // otherwise expire it. Ownership is checked before mutating its lifetime. assert(!switch_pico_switch2_sample_take(other_product_id, source_address, now + 2000, &sample, &taken)); assert(!switch_pico_switch2_sample_take(PROBE_JOYCON_PID, other_address, now + 2000, &sample, &taken)); assert(probe_controller_input_sample_result(0, old_cue, now) == 0); assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address, old_cue, 1, now)); // Not dispatched. assert(switch_pico_switch2_sample_take(PROBE_JOYCON_PID, source_address, now, &sample, &taken)); assert(taken == old_cue && sample == 3); switch2_mouse_capture_select_input(0, source_address, 0x2069); // Invalid PID. switch2_mouse_capture_select_input(0, nullptr, PROBE_JOYCON_PID); assert(expect_report(first) == old_packet && poll().active); assert(probe_controller_input_sample_result(0, old_cue, now) == 0); // Same address, different side is still a new source. Native and cue // tokens from the prior selection cannot acknowledge or consume it. switch2_mouse_capture_select_input(0, source_address, other_product_id); expect_empty(); expect_inactive(poll()); assert(!probe_controller_input_commit_native_report(0, old_packet)); assert(probe_controller_input_sample_result(0, old_cue, now) == -1); uint64_t new_cue = 0; assert(!probe_controller_input_play_sample(0, 4, &new_cue)); emit(first); emit(opposite, source_address, other_product_id, PROBE_NATIVE_REPORT_ID); expect_inactive(poll()); emit(opposite, source_address, other_product_id, other_report_id); assert(poll().active); expect_empty(); // Selection disabled native output even for valid input. probe_controller_input_set_native_stream(0, true); expect_empty(); emit(opposite, source_address, other_product_id, other_report_id); const uint32_t new_packet = expect_report(opposite); assert(new_packet > old_packet); assert(probe_controller_input_play_sample(0, 4, &new_cue) && new_cue > old_cue); assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address, old_cue, 1, now + 2000)); assert(switch_pico_switch2_sample_take(other_product_id, source_address, now, &sample, &taken)); assert(sample == 4 && taken == new_cue); assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address, new_cue, 1, now + 2000)); assert(!switch_pico_switch2_sample_result(other_product_id, other_address, new_cue, -1, now + 2000)); assert(!switch_pico_switch2_sample_result(other_product_id, source_address, old_cue, 1, now + 2000)); assert(probe_controller_input_sample_result(0, old_cue, now + 2000) == -1); disconnect(source_address); assert(poll().active && expect_report(opposite) == new_packet); assert(probe_controller_input_sample_result(0, new_cue, now) == 0); assert(switch_pico_switch2_sample_result(other_product_id, source_address, new_cue, 1, now)); disconnect(source_address, other_product_id); expect_inactive(poll()); expect_empty(); assert(!probe_controller_input_commit_native_report(0, new_packet)); assert(probe_controller_input_sample_result(0, new_cue, now) == -1); emit(opposite, source_address, other_product_id, other_report_id); assert(probe_controller_input_play_sample(0, 5, &new_cue) && new_cue > old_cue); old_cue = new_cue; switch2_mouse_capture_select_input(0, other_address, other_product_id); expect_inactive(poll()); assert(probe_controller_input_sample_result(0, old_cue, now) == -1); assert(!probe_controller_input_play_sample(0, 6, &new_cue)); emit(opposite, other_address, other_product_id, other_report_id); assert(probe_controller_input_play_sample(0, 6, &new_cue) && new_cue > old_cue); assert(switch_pico_switch2_sample_take(other_product_id, other_address, now, &sample, &taken)); assert(taken == new_cue && sample == 6); assert(!switch_pico_switch2_sample_result(other_product_id, source_address, old_cue, 1, now)); assert(switch_pico_switch2_sample_result(other_product_id, other_address, new_cue, 1, now)); assert(probe_controller_input_sample_result(0, new_cue, now) == 1); assert(probe_controller_input_sample_result(0, new_cue, now) == -1); switch2_mouse_capture_select_input(0, source_address, PROBE_JOYCON_PID); probe_controller_input_set_native_stream(0, true); expect_empty(); } static void test_bounded_overflow() { now = 1000; const auto first = native_report(0x50, 30); emit(first); const uint32_t old_serial = expect_report(first); // The 32-entry contract bounds backlog independently of the diagnostic ring. for (unsigned i = 1; i < 32; ++i) { ++now; emit(native_report(static_cast(0x50 + i), 40)); } assert(expect_report(first) == old_serial); const auto newest = native_report(0xbb, 30, -101, 103); ++now; emit(newest); assert(!probe_controller_input_commit_native_report(0, old_serial)); const uint32_t newest_serial = expect_report(newest); assert(newest_serial > old_serial); const auto following = native_report(0xbc, 40, 107, -109); ++now; emit(following); assert(expect_report(newest) == newest_serial); assert(probe_controller_input_commit_native_report(0, newest_serial)); const uint32_t following_serial = expect_report(following); assert(following_serial > newest_serial); assert(probe_controller_input_commit_native_report(0, following_serial)); expect_empty(); // Overflow discarded all prior history, not merely its head. } static void test_continuous_state_coalescing_preserves_events_and_borrowed_head() { now = 1500; disconnect(); probe_controller_input_set_native_stream(0, true); const auto first = native_report(0x80, 30, 0, 0); auto newest = first; emit(first); for (unsigned i = 1; i <= 24; ++i) { now += 8; newest[0] = static_cast(0x80 + i); newest[5] = static_cast(first[5] + i); newest[PROBE_IMU_LENGTH_OFFSET + 1] = static_cast(i); emit(newest); } // No 192ms history of analog/IMU-only updates is replayed to a slow consumer. assert(probe_controller_input_commit_native_report(0, expect_report(newest))); expect_empty(); emit(first); const uint32_t borrowed = expect_report(first); for (unsigned i = 0; i < 6; ++i) { now += 8; ++newest[0]; ++newest[6]; emit(newest); } assert(expect_report(first) == borrowed); assert(probe_controller_input_commit_native_report(0, borrowed)); assert(probe_controller_input_commit_native_report(0, expect_report(newest))); expect_empty(); auto pressed = first; pressed[2] ^= 1; auto last_pressed = pressed; ++last_pressed[0]; ++last_pressed[5]; auto released = last_pressed; released[2] = first[2]; auto surface = released; surface[13] ^= 0x10; auto opaque_tail = surface; opaque_tail.back() ^= 0x80; auto other_format = opaque_tail; other_format[PROBE_IMU_LENGTH_OFFSET] = 40; emit(first); emit(pressed); emit(last_pressed); emit(released); emit(surface); emit(opaque_tail); emit(other_format); for (const auto& expected : {first, last_pressed, released, surface, opaque_tail, other_format}) assert(probe_controller_input_commit_native_report(0, expect_report(expected))); expect_empty(); auto mouse = first; mouse[9] = 7; emit(first); emit(mouse); emit(first); for (const auto& expected : {first, mouse, first}) assert(probe_controller_input_commit_native_report(0, expect_report(expected))); expect_empty(); } static void test_expiry_and_wrapping_clock() { now = 2000; const auto first = native_report(0x61, 30); const auto fresh = native_report(0x62, 40); emit(first); const uint32_t expired = expect_report(first); now += 499; assert(poll().active && expect_report(first) == expired); ++now; emit(fresh, other_address); // Wrong-source traffic cannot refresh the timeout. expect_inactive(poll()); expect_empty(); assert(!probe_controller_input_commit_native_report(0, expired)); ++now; emit(first); const uint32_t stale_head = expect_report(first); now += 499; emit(fresh); assert(expect_report(first) == stale_head); ++now; assert(poll().active); // Latest source is fresh, but its queued head is not. expect_empty(); assert(!probe_controller_input_commit_native_report(0, stale_head)); emit(fresh); const uint32_t resumed = expect_report(fresh); assert(resumed > stale_head); assert(probe_controller_input_commit_native_report(0, resumed)); expect_empty(); now = static_cast(UINT32_MAX) - 100; emit(first); const uint32_t wrapped = expect_report(first); // The producer can timestamp input one millisecond after the caller samples // its clock; a signed age must accept this race rather than expire the input. const uint32_t before_capture = static_cast(now) - 1; assert(poll(before_capture).active); assert(expect_report(first, before_capture) == wrapped); now += 499; // Cross the uint32 millisecond rollover with a fresh packet. assert(poll().active && expect_report(first) == wrapped); ++now; expect_inactive(poll()); expect_empty(); assert(!probe_controller_input_commit_native_report(0, wrapped)); ++now; emit(fresh); assert(poll().active); const uint32_t after_wrap = expect_report(fresh); assert(after_wrap > wrapped); assert(probe_controller_input_commit_native_report(0, after_wrap)); expect_empty(); } #if SWITCH2_PROBE_COMPOSITE static void test_simultaneous_sources() { const uint8_t left_address[] = {0x98,0xe2,0x55,7,0xe9,0xd3}; now = 10000; disconnect(); const auto right = native_report(0x31, 30, 7, -9); auto left = native_report(0x62, 40, -13, 17); left[probe_model_imu_length_offset(1)] = 40; probe_controller_input_set_native_stream(0, true); probe_controller_input_set_native_stream(1, true); expect_empty(0); expect_empty(1); emit(right); const uint32_t r0 = expect_report(right); emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); const uint32_t l0 = expect_report(left, now, 1); emit(right); const uint32_t r1 = poll().serial; emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); const auto li = poll(now, 1); const auto ri = poll(); assert(r0 < l0 && l0 < r1 && r1 < li.serial); assert(ri.mouse_epoch == r0 && li.mouse_epoch == l0); assert(ri.mouse_total_x == 14 && ri.mouse_total_y == -18); assert(li.mouse_total_x == -26 && li.mouse_total_y == 34); // R is already owned: selecting it for L must not duplicate or steal it. switch2_mouse_capture_select_input(1, source_address, probe_model_pid(0)); assert(expect_report(right) == r0); assert(expect_report(left, now, 1) == l0); assert(!probe_controller_input_commit_native_report(1, r0)); assert(!probe_controller_input_commit_native_report(0, l0)); assert(probe_controller_input_commit_native_report(1, l0)); assert(expect_report(left, now, 1) == li.serial); assert(probe_controller_input_commit_native_report(1, li.serial)); expect_empty(1); assert(expect_report(right) == r0); // L consumption never moves stalled R. // R overflow drops only its own backlog; L's retry remains byte-identical. emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); const uint32_t left_retry = expect_report(left, now, 1); for (unsigned i = 0; i < 31; ++i) emit(right); assert(!probe_controller_input_commit_native_report(0, r0)); assert(expect_report(left, now, 1) == left_retry); const uint32_t r2 = expect_report(right); assert(r2 > left_retry); uint64_t rcue, lcue, taken; uint8_t sample; assert(probe_controller_input_play_sample(0, 3, &rcue)); assert(probe_controller_input_play_sample(1, 5, &lcue) && lcue > rcue); assert(switch_pico_switch2_sample_take(probe_model_pid(0), source_address, now, &sample, &taken)); assert(sample == 3 && taken == rcue); assert(switch_pico_switch2_sample_take(probe_model_pid(1), left_address, now, &sample, &taken)); assert(sample == 5 && taken == lcue); assert(!switch_pico_switch2_sample_result(probe_model_pid(1), left_address, rcue, 1, now + 2000)); assert(!switch_pico_switch2_sample_result(probe_model_pid(0), source_address, lcue, -1, now + 2000)); assert(probe_controller_input_sample_result(0, lcue, now + 2000) == -1); assert(probe_controller_input_sample_result(1, rcue, now + 2000) == -1); assert(probe_controller_input_sample_result(0, rcue, now) == 0); assert(probe_controller_input_sample_result(1, lcue, now) == 0); probe_controller_input_set_native_stream(0, false); probe_controller_input_cancel_sample(0); assert(probe_controller_input_sample_result(0, rcue, now) == -1); assert(probe_controller_input_sample_result(1, lcue, now) == 0); assert(expect_report(left, now, 1) == left_retry); assert(switch_pico_switch2_sample_result(probe_model_pid(1), left_address, lcue, 1, now)); assert(probe_controller_input_sample_result(1, lcue, now) == 1); assert(probe_controller_input_sample_result(1, lcue, now) == -1); // R selection/disconnect cannot revoke L's pending packet or cue. assert(probe_controller_input_play_sample(1, 6, &lcue)); switch2_mouse_capture_select_input(0, other_address, probe_model_pid(0)); disconnect(source_address, probe_model_pid(0)); assert(expect_report(left, now, 1) == left_retry); assert(probe_controller_input_sample_result(1, lcue, now) == 0); assert(poll(now, 1).mouse_epoch == l0); switch2_mouse_capture_select_input(0, source_address, probe_model_pid(0)); probe_controller_input_set_native_stream(0, true); emit(right); const uint32_t right_retry = expect_report(right); assert(probe_controller_input_play_sample(0, 7, &rcue) && rcue > lcue); disconnect(left_address, probe_model_pid(1)); expect_empty(1); expect_inactive(poll(now, 1)); assert(probe_controller_input_sample_result(1, lcue, now) == -1); assert(expect_report(right) == right_retry); assert(probe_controller_input_sample_result(0, rcue, now) == 0); assert(switch_pico_switch2_sample_take(probe_model_pid(0), source_address, now, &sample, &taken)); assert(sample == 7 && taken == rcue); assert(switch_pico_switch2_sample_result(probe_model_pid(0), source_address, rcue, 1, now)); assert(probe_controller_input_sample_result(0, rcue, now) == 1); // Reconnection creates a distinct L epoch and does not replay its old queue. emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); const auto resumed_left = poll(now, 1); assert(resumed_left.mouse_epoch != l0 && resumed_left.mouse_total_x == -13); assert(!probe_controller_input_commit_native_report(1, left_retry)); assert(probe_controller_input_commit_native_report(1, expect_report(left, now, 1))); assert(probe_controller_input_commit_native_report(0, right_retry)); expect_empty(0); expect_empty(1); // A refreshed L packet cannot refresh R's independent source deadline. now += 499; emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); ++now; expect_inactive(poll()); assert(poll(now, 1).active); assert(probe_controller_input_commit_native_report(1, expect_report(left, now, 1))); expect_empty(0); expect_empty(1); } #endif int main() { test_startup_pairing_and_stream_gate(); test_opaque_fidelity_order_and_retry(); test_selected_source_isolation_and_reconnect(); test_side_switch_and_sample_ownership(); test_bounded_overflow(); test_continuous_state_coalescing_preserves_events_and_borrowed_head(); test_expiry_and_wrapping_clock(); #if SWITCH2_PROBE_COMPOSITE test_simultaneous_sources(); #endif puts("Native packet fidelity, FIFO retry/order, source barriers, overflow, expiry and pairing passed"); }