#include #include #include #include #include "controller_input.h" #include "input/bluepad32_input_backend.h" #include "model.h" #include "pico/stdlib.h" #include "platform/pico/bootsel_pairing_button.h" #include "platform/pico/system_clock.h" #include "profile/controller_profile_runtime.h" #include "profile/profile_service.h" namespace { uint64_t now_us = 1000000; uint32_t stage; Bluepad32NativeGamepadSnapshot sources[BLUEPAD32_NATIVE_PAIR_COUNT]; ControllerProfile profiles[BLUEPAD32_NATIVE_PAIR_COUNT]; // Existing single-pair scenarios exercise PairA in both executable configurations. Bluepad32NativeGamepadSnapshot& source = sources[0]; ControllerProfile& profile = profiles[0]; bool selected[BLUEPAD32_NATIVE_PAIR_COUNT]; uint64_t cue_tokens[PROBE_CONTROLLER_COUNT]; uint64_t next_cue_token; uint32_t profile_generation = 1; bool alternating_shortcuts[BLUEPAD32_NATIVE_PAIR_COUNT]; bool shortcut_phases[BLUEPAD32_NATIVE_PAIR_COUNT]; bool& alternating_shortcut = alternating_shortcuts[0]; bool latching_shortcuts[BLUEPAD32_NATIVE_PAIR_COUNT]; struct SlotShortcut { bool active = false; bool latched = false; uint32_t connection_generation = 0; }; SlotShortcut slot_shortcuts[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]; probe_controller_input controls[PROBE_CONTROLLER_COUNT]; uint8_t reports[PROBE_CONTROLLER_COUNT][63]; uint8_t source_pair(uint8_t slot) { for (uint8_t pair_index = 0; pair_index < BLUEPAD32_NATIVE_PAIR_COUNT; ++pair_index) if (sources[pair_index].controller.active && sources[pair_index].slot == slot) return pair_index; assert(false); return 0; } } // namespace uint32_t time_us_32() { return static_cast(now_us); } absolute_time_t get_absolute_time() { return now_us; } uint32_t to_ms_since_boot(absolute_time_t time) { return static_cast(time / 1000); } void system_clock_initialize() {} extern "C" int probe_debug_printf(const char*, ...) { return 0; } BootselPairingButtonEvent bootsel_pairing_button_task() { return BootselPairingButtonEvent::kNone; } void bluepad32_input_backend_init() { stage = 1; } void bluepad32_input_backend_start() { stage = 2; } void bluepad32_input_backend_poll() {} void bluepad32_input_backend_diagnostics(Bluepad32BackendDiagnostics* out) { *out = {}; out->initialization_stage = stage; } void bluepad32_input_backend_open_pairing_window() {} void bluepad32_input_backend_select_native_source(uint8_t pair_index, const uint8_t*) { assert(pair_index < BLUEPAD32_NATIVE_PAIR_COUNT); selected[pair_index] = true; } void bluepad32_input_backend_native_snapshot(uint8_t pair_index, Bluepad32NativeGamepadSnapshot* out) { assert(pair_index < BLUEPAD32_NATIVE_PAIR_COUNT); *out = selected[pair_index] ? sources[pair_index] : Bluepad32NativeGamepadSnapshot{}; } bool bluepad32_input_backend_native_sample_request(uint8_t instance, uint8_t, uint64_t* token) { if (instance >= PROBE_CONTROLLER_COUNT || !sources[instance / 2].controller.active || !token) return false; *token = cue_tokens[instance] = ++next_cue_token; return true; } int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t token) { return instance < PROBE_CONTROLLER_COUNT && token && cue_tokens[instance] == token ? 1 : -1; } void bluepad32_input_backend_native_sample_cancel(uint8_t instance) { assert(instance < PROBE_CONTROLLER_COUNT); cue_tokens[instance] = 0; } bool bluepad32_input_backend_native_rumble_submit(uint8_t, const NativeHapticsActuatorFrame*) { assert(false && "gameplay motor dispatch belongs to the native backend fixture"); return false; } void bluepad32_input_backend_native_rumble_cancel(uint8_t) { assert(false && "gameplay motor cancellation belongs to the native backend fixture"); } void bluepad32_input_backend_queue_profile_feedback(uint8_t, uint32_t, uint8_t, ControllerProfileConfirmationPolicy) {} void controller_profile_runtime_reset() { for (ControllerProfile& value : profiles) value = controller_profile_default(controller_identity_global(), 0); } uint32_t profile_service_database_generation() { return profile_generation; } bool controller_profile_runtime_take_initial_profile_indication(uint8_t, ControllerProfileRuntimeProfileChangeEvent*) { return false; } bool controller_profile_runtime_take_profile_change(uint8_t, ControllerProfileRuntimeProfileChangeEvent*) { return false; } ControllerProfileTransformResult controller_profile_runtime_transform( uint8_t slot, const Bluepad32SlotSnapshot& input, uint32_t, AdapterUsbMode) { if (!input.active) { slot_shortcuts[slot] = {}; return {}; } const uint8_t pair_index = source_pair(slot); auto result = controller_profile_transform(input.state, profiles[pair_index]); if (alternating_shortcuts[pair_index]) { // Model a runtime synthetic transition spanning the two halves. Two // evaluations for one paired report would expose contradictory states. shortcut_phases[pair_index] = !shortcut_phases[pair_index]; result.state.button_system = result.state.button_capture = shortcut_phases[pair_index]; } if (latching_shortcuts[pair_index]) { // Model a macro/Shift latch owned by a runtime SLOT, not a USB pair. auto& shortcut = slot_shortcuts[slot]; if (!shortcut.active || shortcut.connection_generation != input.connection_generation) { shortcut = {}; shortcut.active = true; shortcut.connection_generation = input.connection_generation; } if (input.state.button_select) shortcut.latched = true; result.state.button_system = result.state.button_capture = shortcut.latched; } return result; } namespace { uint32_t now_ms() { return to_ms_since_boot(now_us); } void put_pair(uint8_t* out, uint16_t x, uint16_t y) { out[0] = static_cast(x); out[1] = static_cast((x >> 8) | (y << 4)); out[2] = static_cast(y >> 4); } void calibrate(uint8_t instance, uint16_t x, uint16_t y, uint16_t px, uint16_t py, uint16_t nx, uint16_t ny) { uint8_t record[9]; put_pair(record, x, y); put_pair(record + 3, px, py); put_pair(record + 6, nx, ny); probe_controller_input_set_full_stick_calibration(instance, record); } uint16_t stick_x(uint8_t instance) { return reports[instance][5] | ((reports[instance][6] & 15u) << 8); } uint16_t stick_y(uint8_t instance) { return (reports[instance][6] >> 4) | (reports[instance][7] << 4); } uint8_t imu_length(uint8_t instance) { return reports[instance][probe_model_imu_length_offset(instance)]; } uint32_t bits(const uint8_t* bytes, unsigned offset, unsigned count) { uint32_t value = 0; for (unsigned i = 0; i < count; ++i) value |= uint32_t((bytes[(offset + i) / 8] >> ((offset + i) % 8)) & 1) << i; return value; } void quaternion(uint8_t instance, double out[4]) { const uint8_t* imu = reports[instance] + probe_model_imu_data_offset(instance); assert(imu_length(instance) == 30); const unsigned largest = bits(imu, 32, 3); assert(largest < 4); double ratios[3], norm = 1; for (unsigned i = 0; i < 3; ++i) { ratios[i] = bits(imu, 35 + 31 * i, 31) / 1073741824.0 - 1; norm += ratios[i] * ratios[i]; } out[largest] = 1 / sqrt(norm); for (unsigned i = 0; i < 3; ++i) out[(largest + i + 1) & 3] = ratios[i] * out[largest]; } void publish_at_current_time(uint8_t pair_index, bool motion) { Bluepad32NativeGamepadSnapshot& snapshot = sources[pair_index]; snapshot.received_us = time_us_32(); ++snapshot.state_generation; if (motion) { snapshot.accel_received_us = snapshot.gyro_received_us = time_us_32(); ++snapshot.accel_sequence; ++snapshot.gyro_sequence; } } void publish(bool motion = true, uint8_t pair_index = 0) { now_us += 4000; publish_at_current_time(pair_index, motion); } uint32_t peek(uint8_t instance) { probe_controller_input_poll(instance, now_ms(), &controls[instance]); return probe_controller_input_peek_native_report(instance, now_ms(), reports[instance]); } void consume(uint8_t instance) { const uint32_t token = peek(instance); assert(token && probe_controller_input_commit_native_report(instance, token)); } void pair(uint8_t pair_index = 0) { consume(pair_index * 2); consume(pair_index * 2 + 1); } void no_mouse_or_rails() { for (unsigned i = 0; i < 2; ++i) { assert((reports[i][3] & 0xc0) == 0); assert(reports[i][9] == 0 && reports[i][10] == 0 && reports[i][11] == 0 && reports[i][12] == 0); assert(reports[i][13] == 0xff); } } void mapped_halves_and_calibration() { source.slot = 2; source.controller.active = true; source.controller.connection_generation = 7; source.controller.identity = controller_identity_global(); source.battery = 128; publish(); // Neither an absent source nor an uncalibrated child masquerades as active. assert(!peek(0) && !controls[0].active); calibrate(0, 2000, 2100, 1500, 1400, 1600, 1700); assert(peek(0)); assert(!peek(1) && !controls[1].active); calibrate(1, 1800, 1900, 1700, 1800, 1400, 1500); pair(); assert(stick_x(0) == 2000 && stick_y(0) == 2100); assert(stick_x(1) == 1800 && stick_y(1) == 1900); assert(reports[0][1] == 0x15 && reports[1][1] == 0x15); // Measured half battery, USB powered, not charging. // Actual profile transforms can move controls across native children. profile.button_map[static_cast(ControllerProfileLogicalButton::kSouth)] = static_cast(ControllerProfileLogicalButton::kDpadRight); profile.button_map[static_cast(ControllerProfileLogicalButton::kDpadLeft)] = static_cast(ControllerProfileLogicalButton::kEast); profile.triggers[0].digital_threshold = 20000; profile.triggers[1].digital_threshold = 30000; ControllerState& state = source.controller.state; state.button_south = state.dpad_left = true; state.button_left_shoulder = state.button_right_shoulder = true; state.button_select = state.button_start = true; state.button_left_stick = state.button_right_stick = true; state.button_system = state.button_capture = true; state.left_trigger = 19999; state.right_trigger = 30000; state.right_stick_x = INT16_MAX; state.left_stick_y = INT16_MIN; publish(); pair(); assert(reports[0][2] == 0xf2 && reports[1][2] == 0xd2); assert(reports[0][3] == 1 && reports[1][3] == 1); assert(stick_x(0) == 3500 && stick_y(0) == 2100); assert(stick_x(1) == 1800 && stick_y(1) == 3700); no_mouse_or_rails(); state = {}; state.button_west = state.button_north = true; state.dpad_up = state.dpad_down = true; state.left_trigger = 20000; state.right_stick_x = INT16_MIN; state.left_stick_y = INT16_MAX; publish(); pair(); assert(reports[0][2] == 0x0c && reports[1][2] == 0x29); assert(stick_x(0) == 400 && stick_y(1) == 400); // A malformed calibration may not spill a 12-bit axis into its neighbor. const uint32_t left_pending = peek(1); calibrate(0, 2000, 2100, 3000, 1400, 1600, 1700); assert(!peek(0)); assert(probe_controller_input_commit_native_report(1, left_pending)); calibrate(0, 2000, 2100, 1500, 1400, 1600, 1700); state = {}; profile = controller_profile_default(controller_identity_global(), 0); alternating_shortcut = true; for (unsigned i = 0; i < 4; ++i) { publish(); pair(); assert(reports[0][3] == reports[1][3]); } alternating_shortcut = false; } void independent_backpressure_and_resets() { publish(); const uint32_t blocked_left = peek(1); const uint32_t right = peek(0); uint8_t saved[63]; memcpy(saved, reports[0], sizeof(saved)); assert(peek(0) == right && memcmp(saved, reports[0], sizeof(saved)) == 0); assert(!probe_controller_input_commit_native_report(1, right)); assert(probe_controller_input_commit_native_report(0, right)); assert(!probe_controller_input_commit_native_report(0, right)); assert(probe_controller_input_commit_native_report(1, blocked_left)); publish(); const uint32_t obsolete = peek(1); for (unsigned i = 0; i < 40; ++i) { source.controller.state.dpad_down = (i & 1) != 0; source.controller.state.button_east = (i & 1) != 0; publish(); consume(0); } const uint32_t latest = peek(1); assert(latest != obsolete && reports[1][2] == 1); assert(!probe_controller_input_commit_native_report(1, obsolete)); probe_controller_input_set_native_stream(0, false); assert(!peek(0)); assert(probe_controller_input_commit_native_report(1, latest)); probe_controller_input_set_native_stream(0, true); const uint32_t right_pending = peek(0); probe_controller_input_set_native_stream(1, false); assert(probe_controller_input_commit_native_report(0, right_pending)); probe_controller_input_set_native_stream(1, true); source.controller.state = {}; } void real_motion_admission_and_loss() { source.accel_valid = source.gyro_valid = true; source.accel_q13[1] = 8192; // SDL face-up gravity -> native +Z, no mouse mounting. // These values have already passed the DS5 factory-calibration path. // Even a controller rotating at connection must not wait for stationary bias estimation. source.gyro_q10[0] = 0; source.gyro_q10[1] = 90 * 1024; source.gyro_q10[2] = 0; publish(); pair(); assert(imu_length(0) == 30 && imu_length(1) == 30); source.gyro_q10[1] = 0; publish(); pair(); // Polling and fresh button packets cannot create additional IMU samples. for (unsigned i = 0; i < 420; ++i) { publish(false); pair(); assert(controls[0].active && controls[1].active); assert(imu_length(0) == 0 && imu_length(1) == 0); } publish(); pair(); // Fresh factory-calibrated data recovers without another settling delay. assert(imu_length(0) == 30 && imu_length(1) == 30); const uint8_t* right_imu = reports[0] + probe_model_imu_data_offset(0); const uint8_t* left_imu = reports[1] + probe_model_imu_data_offset(1); assert(memcmp(right_imu, left_imu, 30) == 0); assert(bits(right_imu, 128, 32) == 0 && bits(right_imu, 160, 32) == 0); assert(bits(right_imu, 192, 32) == (1u << 28)); double initial[4]; quaternion(0, initial); // A new controls packet with no new IMU cannot emit the old sample again. source.controller.state.button_east = true; publish(false); pair(); assert(reports[0][2] == 2 && imu_length(0) == 0 && imu_length(1) == 0); // A blocked child's motion is not consumed by the other child's endpoint. publish(); consume(0); const uint32_t left_pending = peek(1); assert(imu_length(1) == 30); consume(0); assert(imu_length(0) == 0); probe_controller_input_set_native_stream(0, false); assert(probe_controller_input_commit_native_report(1, left_pending)); probe_controller_input_set_native_stream(0, true); publish(); pair(); assert(imu_length(0) == 30 && imu_length(1) == 30); // No shared recalibration on USB reset. // One second of genuine 90dps yaw advances the same rigid orientation once, // not twice because two virtual endpoints happen to consume it. source.gyro_q10[1] += 90 * 1024; for (unsigned i = 0; i < 250; ++i) { publish(); pair(); } double turned[4]; quaternion(0, turned); double dot = 0; for (unsigned i = 0; i < 4; ++i) dot += initial[i] * turned[i]; assert(fabs(fabs(dot) - sqrt(.5)) < .015); quaternion(1, initial); for (unsigned i = 0; i < 4; ++i) assert(fabs(initial[i] - turned[i]) < 1e-8); source.gyro_q10[1] -= 90 * 1024; publish(); const uint32_t obsolete = peek(0); source.accel_valid = source.gyro_valid = false; assert(!probe_controller_input_commit_native_report(0, obsolete)); publish(false); pair(); assert(controls[0].active && reports[0][2] == 2 && imu_length(0) == 0 && imu_length(1) == 0); source.accel_valid = source.gyro_valid = true; publish(); pair(); assert(imu_length(0) == 30); for (unsigned i = 0; i < 38; ++i) { publish(false); pair(); } assert(controls[0].active && reports[0][2] == 2 && imu_length(0) == 0 && imu_length(1) == 0); publish(); const uint32_t old_right = peek(0), old_left = peek(1); // Even a reconnect whose teardown was missed retires both USB identities. ++source.controller.connection_generation; source.accel_valid = source.gyro_valid = false; // New epochs require fresh reports. assert(!probe_controller_input_commit_native_report(0, old_right)); assert(!probe_controller_input_commit_native_report(1, old_left)); pair(); assert(controls[0].active && imu_length(0) == 0 && imu_length(1) == 0); source.controller.active = false; memset(reports[0], 0x5a, 63); assert(!peek(0) && !controls[0].active); for (uint8_t byte : reports[0]) assert(byte == 0x5a); assert(!peek(1) && !controls[1].active); source.controller.active = true; ++source.controller.connection_generation; publish(); pair(); now_us += 500000; assert(!peek(0) && !peek(1)); assert(!controls[0].active && !controls[1].active); } void selected_motion_target_keeps_both_control_halves() { source = {}; source.slot = 0; source.controller.active = true; source.controller.connection_generation = 99; source.controller.state.button_south = true; source.controller.state.dpad_up = true; source.accel_valid = source.gyro_valid = true; source.accel_q13[1] = 8192; profile = controller_profile_default(controller_identity_global(), 0); calibrate(0, 2048, 2048, 2047, 2047, 2048, 2048); calibrate(1, 2048, 2048, 2047, 2047, 2048, 2048); publish(); pair(); for (uint8_t instance = 0; instance < 2; ++instance) { assert(controls[instance].active); const bool enabled = (SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) != 0; assert(imu_length(instance) == (enabled ? 30 : 0)); } assert(reports[0][2] == 0x01 && reports[1][2] == 0x08); source.gyro_q10[1] = 90 * 1024; publish(); pair(); assert(imu_length(0) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & 1) ? 30 : 0)); assert(imu_length(1) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & 2) ? 30 : 0)); no_mouse_or_rails(); } void wii_bias_and_independent_sensor_freshness() { ++source.controller.connection_generation; source.track_stationary_bias = true; source.gyro_q10[1] = 2 * 1024; publish(); pair(); assert(controls[0].active && controls[1].active); assert(reports[0][2] == 0x01 && reports[1][2] == 0x08); for (uint8_t instance = 0; instance < 2; ++instance) assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0)); for (unsigned i = 0; i < 400; ++i) { publish(); pair(); } for (uint8_t instance = 0; instance < 2; ++instance) { assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0)); } // Accelerometer-only reports must not refresh a stalled MotionPlus stream. for (unsigned i = 0; i < 38; ++i) { publish(false); source.accel_received_us = time_us_32(); ++source.accel_sequence; pair(); } assert(controls[0].active && controls[1].active); assert(imu_length(0) == 0 && imu_length(1) == 0); source.gyro_valid = false; publish(); pair(); assert(reports[0][2] == 0x01 && reports[1][2] == 0x08); assert(imu_length(0) == 0 && imu_length(1) == 0); // Fresh Wii sensors recover immediately, without borrowing the old bias. source.gyro_valid = true; publish(); pair(); for (uint8_t instance = 0; instance < 2; ++instance) assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0)); // A factory-calibrated source switching policy must initialize immediately. source.track_stationary_bias = false; publish(); pair(); for (uint8_t instance = 0; instance < 2; ++instance) { assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0)); } } void nunchuk_buttons_map_to_native_left_shoulders() { ++source.controller.connection_generation; source.controller.state = {}; source.accel_valid = source.gyro_valid = false; source.battery = 0; profile = controller_profile_default(controller_identity_global(), 0); // The real Wii parser maps Nunchuk C to west and Z to north. These are // ordinary profile inputs, not the unrelated Switch2 extra "C" control. profile.button_map[static_cast(ControllerProfileLogicalButton::kWest)] = CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL; profile.button_map[static_cast(ControllerProfileLogicalButton::kNorth)] = static_cast(ControllerProfileLogicalButton::kLeftShoulder); source.controller.state.button_west = true; // C -> ZL. publish(false); pair(); assert(reports[0][2] == 0 && reports[1][2] == 0x20); assert(reports[0][1] == 0x01 && reports[1][1] == 0x01); source.controller.state.button_west = false; source.controller.state.button_north = true; // Z -> L. publish(false); pair(); assert(reports[0][2] == 0 && reports[1][2] == 0x10); source.controller.state.button_right_shoulder = true; // Remote 2 -> R. publish(false); pair(); assert(reports[0][2] == 0x10 && reports[1][2] == 0x10); // Real L+R across the pair. source.controller.state.button_west = true; publish(false); pair(); assert(reports[0][2] == 0x10 && reports[1][2] == 0x30); source.controller.state = {}; publish(false); pair(); assert(reports[0][2] == 0 && reports[1][2] == 0); // No sticky synthetic chord. no_mouse_or_rails(); } void inactive_child(uint8_t instance) { memset(reports[instance], 0x5a, sizeof(reports[instance])); assert(!peek(instance) && !controls[instance].active); assert(controls[instance].buttons[0] == 0 && controls[instance].buttons[1] == 0); for (uint8_t byte : controls[instance].stick) assert(byte == 0); for (uint8_t byte : reports[instance]) assert(byte == 0x5a); } void solo_controls_and_explicit_rails() { source.accel_valid = source.gyro_valid = false; calibrate(0, 2000, 2100, 1500, 1400, 1600, 1700); calibrate(1, 1800, 1900, 1700, 1800, 1400, 1500); bool ControllerState::* const faces[] = { &ControllerState::button_south, &ControllerState::button_east, &ControllerState::button_west, &ControllerState::button_north}; const uint8_t face_bits[2][4] = {{0x02, 0x08, 0x01, 0x04}, {0x04, 0x01, 0x08, 0x02}}; ControllerState& state = source.controller.state; for (uint8_t instance = 0; instance < 2; ++instance) { const bool left = instance == 1; profile = controller_profile_default(controller_identity_global(), 0); profile.native_joycon_layout = left ? ControllerProfileNativeJoyconLayout::kLeftSolo : ControllerProfileNativeJoyconLayout::kRightSolo; ++profile_generation; for (unsigned face = 0; face < 4; ++face) { state = {}; state.*faces[face] = true; publish(false); consume(instance); assert(reports[instance][2] == face_bits[instance][face]); inactive_child(instance ^ 1); } // Dpad is not silently merged into the four solo face actions. state = {}; state.dpad_up = state.dpad_down = state.dpad_left = state.dpad_right = true; publish(false); consume(instance); assert(reports[instance][2] == 0); profile.button_map[static_cast(ControllerProfileLogicalButton::kDpadRight)] = static_cast(ControllerProfileLogicalButton::kSouth); ++profile_generation; consume(instance); assert(reports[instance][2] == face_bits[instance][0]); state = {}; state.button_select = state.button_start = state.button_system = state.button_capture = true; state.button_left_stick = true; publish(false); consume(instance); assert(reports[instance][2] == 0xc0 && reports[instance][3] == 1); state = {}; state.button_right_stick = true; state.right_stick_x = INT16_MIN; publish(false); consume(instance); assert(reports[instance][2] == 0); assert(stick_x(instance) == (left ? 1800 : 2000)); assert(stick_y(instance) == (left ? 1900 : 2100)); // A live swap selects the physical right stick AND click, without a // physical publication or a second swap in the native routing layer. profile.swap_sticks = true; ++profile_generation; consume(instance); assert(reports[instance][2] == 0x80); assert(stick_x(instance) == (left ? 1800 : 2000)); assert(stick_y(instance) == (left ? 3700 : 400)); state.button_right_stick = false; state.button_left_stick = true; state.right_stick_x = 0; state.right_stick_y = INT16_MIN; publish(false); consume(instance); assert(reports[instance][2] == 0); assert(stick_x(instance) == (left ? 3500 : 400)); assert(stick_y(instance) == (left ? 1900 : 2100)); state = {}; state.button_left_shoulder = true; state.left_trigger = UINT16_MAX; publish(false); consume(instance); assert(reports[instance][2] == (left ? 0x30 : 0)); assert(reports[instance][3] == 0); state = {}; state.button_right_shoulder = true; state.right_trigger = UINT16_MAX; publish(false); consume(instance); assert(reports[instance][2] == (left ? 0 : 0x30)); assert(reports[instance][3] == 0); const uint8_t sl = left ? CONTROLLER_PROFILE_LEFT_SL_OUTPUT : CONTROLLER_PROFILE_RIGHT_SL_OUTPUT; const unsigned shoulder_l = static_cast(ControllerProfileLogicalButton::kLeftShoulder); const unsigned shoulder_r = static_cast(ControllerProfileLogicalButton::kRightShoulder); profile.button_map[shoulder_l] = sl; profile.button_map[shoulder_r] = sl + 1; ++profile_generation; state = {}; state.button_left_shoulder = true; publish(false); consume(instance); assert(reports[instance][2] == 0 && reports[instance][3] == 0x80); state.button_right_shoulder = true; publish(false); consume(instance); assert(reports[instance][2] == 0 && reports[instance][3] == 0xc0); state.button_left_shoulder = false; publish(false); consume(instance); assert(reports[instance][2] == 0 && reports[instance][3] == 0x40); // Mapping to the other child's rails never creates a selected-side chord. profile.button_map[shoulder_l] = left ? CONTROLLER_PROFILE_RIGHT_SL_OUTPUT : CONTROLLER_PROFILE_LEFT_SL_OUTPUT; profile.button_map[shoulder_r] = left ? CONTROLLER_PROFILE_RIGHT_SR_OUTPUT : CONTROLLER_PROFILE_LEFT_SR_OUTPUT; ++profile_generation; state.button_left_shoulder = true; publish(false); consume(instance); assert(reports[instance][2] == 0 && reports[instance][3] == 0); inactive_child(instance ^ 1); // A mapped analog source reaches the same rail wire bit only at its // transformed digital threshold; unmapped physical extras cannot leak. profile.triggers[0].output = sl; profile.triggers[0].digital_threshold = 20000; ++profile_generation; state = {}; state.extra_buttons = 0x7f; state.left_trigger = 19999; publish(false); consume(instance); assert(reports[instance][2] == 0 && reports[instance][3] == 0); state.left_trigger = 20000; publish(false); consume(instance); assert(reports[instance][2] == 0 && reports[instance][3] == 0x80); } } void profile_changes_retire_tokens_without_source_publication() { profile = controller_profile_default(controller_identity_global(), 0); ++profile_generation; source.controller.state = {}; source.controller.state.button_south = source.controller.state.dpad_left = true; source.accel_valid = source.gyro_valid = false; publish(false); const uint32_t paired_right = peek(0), paired_left = peek(1); profile.native_joycon_layout = ControllerProfileNativeJoyconLayout::kRightSolo; ++profile_generation; assert(!probe_controller_input_commit_native_report(0, paired_right)); assert(!probe_controller_input_commit_native_report(1, paired_left)); consume(0); assert(reports[0][2] == 0x02); inactive_child(1); const uint32_t solo_right = peek(0); profile.native_joycon_layout = ControllerProfileNativeJoyconLayout::kLeftSolo; ++profile_generation; assert(!probe_controller_input_commit_native_report(0, solo_right)); consume(1); assert(reports[1][2] == 0x04); inactive_child(0); const uint32_t solo_left = peek(1); // Even an ordinary mapping edit in the same layout retires old reports. profile.button_map[static_cast(ControllerProfileLogicalButton::kSouth)] = static_cast(ControllerProfileLogicalButton::kNorth); ++profile_generation; assert(!probe_controller_input_commit_native_report(1, solo_left)); consume(1); assert(reports[1][2] == 0x02); profile.native_joycon_layout = ControllerProfileNativeJoyconLayout::kPaired; ++profile_generation; pair(); assert(reports[0][2] == 0x08 && reports[1][2] == 0x04); } void digital_dpad_reaches_the_mapped_left_stick() { source.accel_valid = source.gyro_valid = false; source.controller.active = true; calibrate(0, 2048, 2048, 1000, 1000, 1000, 1000); calibrate(1, 2048, 2048, 1000, 1000, 1000, 1000); profile = controller_profile_default(controller_identity_global(), 0); profile.button_map[12] = CONTROLLER_PROFILE_LEFT_STICK_UP_OUTPUT; profile.button_map[13] = CONTROLLER_PROFILE_LEFT_STICK_DOWN_OUTPUT; profile.button_map[14] = CONTROLLER_PROFILE_LEFT_STICK_LEFT_OUTPUT; profile.button_map[15] = CONTROLLER_PROFILE_LEFT_STICK_RIGHT_OUTPUT; ++profile_generation; ControllerState& state = source.controller.state; state = {}; state.dpad_up = true; publish(false); pair(); assert(stick_x(1) == 2048 && stick_y(1) == 3048); assert(stick_x(0) == 2048 && stick_y(0) == 2048); assert(reports[0][2] == 0 && reports[1][2] == 0); profile.native_joycon_layout = ControllerProfileNativeJoyconLayout::kRightSolo; ++profile_generation; consume(0); assert(stick_x(0) == 1048 && stick_y(0) == 2048); inactive_child(1); state.dpad_right = true; publish(false); consume(0); assert(stick_x(0) == 1341 && stick_y(0) == 2755); profile.native_joycon_layout = ControllerProfileNativeJoyconLayout::kLeftSolo; ++profile_generation; consume(1); assert(stick_x(1) == 2755 && stick_y(1) == 1341); inactive_child(0); // Digital directions still target mapped LEFT after swapping. Physical // left movement now belongs to mapped right and must not block them. profile.swap_sticks = true; ++profile_generation; state.dpad_up = false; state.left_stick_x = INT16_MAX; publish(false); consume(1); assert(stick_x(1) == 2048 && stick_y(1) == 1048); state.right_stick_y = INT16_MAX; publish(false); consume(1); assert(stick_x(1) == 1048 && stick_y(1) == 2048); // Releasing all inputs cannot leave a generated stick or click held. state = {}; publish(false); consume(1); assert(stick_x(1) == 2048 && stick_y(1) == 2048 && reports[1][2] == 0); } void solo_motion_rotates_coherently_and_resets_frame() { profile = controller_profile_default(controller_identity_global(), 0); source.controller.state = {}; ++source.controller.connection_generation; source.track_stationary_bias = false; source.accel_valid = source.gyro_valid = true; source.accel_q13[0] = 2048; source.accel_q13[1] = 4096; source.accel_q13[2] = -4096; // Parallel acceleration/rate vectors turn about reference gravity. A // one-sided or sign-inconsistent rotation cannot preserve this motion. source.gyro_q10[0] = 30 * 1024; source.gyro_q10[1] = 60 * 1024; source.gyro_q10[2] = -60 * 1024; const ControllerProfileNativeJoyconLayout layouts[] = { ControllerProfileNativeJoyconLayout::kPaired, ControllerProfileNativeJoyconLayout::kLeftSolo, ControllerProfileNativeJoyconLayout::kRightSolo, ControllerProfileNativeJoyconLayout::kPaired}; const int32_t body_accel[4][3] = { {2048, 4096, 4096}, {4096, 4096, -2048}, {-4096, 4096, 2048}, {2048, 4096, 4096}}; uint32_t previous_token = 0; uint8_t previous_instance = 0; publish(); for (unsigned layout = 0; layout < 4; ++layout) { profile.native_joycon_layout = layouts[layout]; ++profile_generation; if (previous_token) assert(!probe_controller_input_commit_native_report(previous_instance, previous_token)); const uint8_t instance = layout == 1 ? 1 : layout == 2 ? 0 : (SWITCH2_BRIDGE_IMU_TARGET_MASK & 1) ? 0 : 1; consume(instance); if (layout == 1 || layout == 2) inactive_child(instance ^ 1); if (!(SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance))) { assert(imu_length(instance) == 0); continue; } assert(imu_length(instance) == 30); const uint8_t* imu = reports[instance] + probe_model_imu_data_offset(instance); assert(bits(imu, 12, 12) == 1); // No committed timestamp from the old frame. for (unsigned axis = 0; axis < 3; ++axis) assert(bits(imu, 128 + axis * 32, 32) == static_cast(body_accel[layout][axis] * 32768)); double initial[4]; quaternion(instance, initial); const double w = sqrt((1.0 + body_accel[layout][2] / 6144.0) / 2.0); const double expected[4] = { w, body_accel[layout][1] / (12288.0 * w), -body_accel[layout][0] / (12288.0 * w), 0}; double dot = 0; for (unsigned i = 0; i < 4; ++i) dot += initial[i] * expected[i]; assert(fabs(fabs(dot) - 1.0) < 1e-6); for (unsigned sample = 0; sample < 250; ++sample) { publish(); consume(instance); } double turned[4]; quaternion(instance, turned); const double half = sqrt(.5); const double expected_turn[4] = { half * initial[0], half * (initial[1] - initial[2]), half * (initial[1] + initial[2]), half * initial[0]}; dot = 0; for (unsigned i = 0; i < 4; ++i) dot += turned[i] * expected_turn[i]; assert(fabs(fabs(dot) - 1.0) < 1e-5); consume(instance); assert(imu_length(instance) == 0); // No repeated sensor provenance. publish(); previous_token = peek(instance); previous_instance = instance; assert(previous_token && imu_length(instance) == 30); // Next layout uses this exact fresh source sample, not a new publication. } } #if PROBE_CONTROLLER_COUNT == 4 void publish_both(bool motion = true) { now_us += 4000; publish_at_current_time(0, motion); publish_at_current_time(1, motion); } void prepare_two_sources(bool motion) { for (uint8_t pair_index = 0; pair_index < BLUEPAD32_NATIVE_PAIR_COUNT; ++pair_index) { auto& snapshot = sources[pair_index]; const uint32_t connection_generation = snapshot.controller.connection_generation + 1; snapshot = {}; snapshot.slot = pair_index; snapshot.controller.active = true; snapshot.controller.connection_generation = connection_generation; snapshot.controller.identity = controller_identity_global(); snapshot.accel_valid = snapshot.gyro_valid = motion; snapshot.accel_q13[1] = 8192; profiles[pair_index] = controller_profile_default(controller_identity_global(), 0); alternating_shortcuts[pair_index] = false; } ++profile_generation; for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { probe_controller_input_set_native_stream(instance, true); calibrate(instance, 2048, 2048, 1000, 1000, 1000, 1000); } publish_both(motion); pair(0); pair(1); } void two_pair_controls_and_profile_coherence() { prepare_two_sources(false); auto& a = sources[0].controller.state; auto& b = sources[1].controller.state; a.button_south = a.dpad_up = true; a.button_left_shoulder = a.button_right_shoulder = true; b.button_east = b.dpad_down = true; sources[0].battery = 255; sources[1].battery = 0; publish_both(false); pair(0); pair(1); assert(reports[0][2] == 0x11 && reports[1][2] == 0x18); assert(reports[2][2] == 0x02 && reports[3][2] == 0x01); assert(reports[0][1] == 0x25 && reports[1][1] == 0x25); assert(reports[2][1] == 0x01 && reports[3][1] == 0x01); a.button_left_shoulder = a.button_right_shoulder = false; b.button_left_shoulder = b.button_right_shoulder = true; publish_both(false); pair(1); pair(0); assert(reports[0][2] == 0x01 && reports[1][2] == 0x08); assert(reports[2][2] == 0x12 && reports[3][2] == 0x11); // Real L+R only on PairB. // Digital mapped-left movement after swapping feeds only A's solo frame. // B independently inverts its physical left stick, then swaps it to right. a = {}; b = {}; a.dpad_up = a.button_south = true; a.left_stick_x = INT16_MAX; profiles[0].button_map[12] = CONTROLLER_PROFILE_LEFT_STICK_UP_OUTPUT; profiles[0].native_joycon_layout = ControllerProfileNativeJoyconLayout::kRightSolo; profiles[0].swap_sticks = true; b.dpad_down = true; b.left_stick_y = INT16_MAX; profiles[1].sticks[0].invert_y = true; profiles[1].swap_sticks = true; ++profile_generation; publish_both(false); consume(0); pair(1); inactive_child(1); assert(reports[0][2] == 0x02 && stick_x(0) == 1048 && stick_y(0) == 2048); assert(reports[2][2] == 0 && stick_x(2) == 2048 && stick_y(2) == 3048); assert(reports[3][2] == 0x01 && stick_x(3) == 2048 && stick_y(3) == 2048); profiles[0].native_joycon_layout = ControllerProfileNativeJoyconLayout::kLeftSolo; ++profile_generation; pair(1); consume(1); inactive_child(0); assert(reports[1][2] == 0x04 && stick_x(1) == 3048 && stick_y(1) == 2048); assert(reports[2][2] == 0 && stick_y(2) == 3048 && reports[3][2] == 0x01); // A and B may select different solo sides without neutralizing each other. profiles[1].native_joycon_layout = ControllerProfileNativeJoyconLayout::kRightSolo; b.right_stick_x = INT16_MAX; ++profile_generation; publish_both(false); consume(2); consume(1); inactive_child(0); inactive_child(3); assert(stick_x(1) == 3048 && stick_y(1) == 2048); assert(stick_x(2) == 2048 && stick_y(2) == 3048); a = {}; b = {}; profiles[0] = profiles[1] = controller_profile_default(controller_identity_global(), 0); ++profile_generation; alternating_shortcuts[0] = alternating_shortcuts[1] = true; shortcut_phases[0] = false; shortcut_phases[1] = true; for (unsigned round = 0; round < 4; ++round) { publish_both(false); consume(0); consume(2); consume(1); consume(3); assert(reports[0][3] == reports[1][3] && reports[2][3] == reports[3][3]); assert(reports[0][3] != reports[2][3]); } const uint8_t a_before = reports[0][3], b_before = reports[2][3]; // A's same-millisecond publication must re-evaluate A, not B; alternating // slot-local transitions make both duplicate and missing evaluations visible. publish_at_current_time(0, false); consume(0); consume(2); consume(1); consume(3); assert(reports[0][3] != a_before && reports[0][3] == reports[1][3]); assert(reports[2][3] == b_before && reports[2][3] == reports[3][3]); alternating_shortcuts[0] = alternating_shortcuts[1] = false; } void two_pair_transport_and_disconnect_isolation() { prepare_two_sources(true); sources[0].controller.state.button_south = true; sources[1].controller.state.button_north = true; publish_both(); uint32_t pending[PROBE_CONTROLLER_COUNT]; uint64_t cues[PROBE_CONTROLLER_COUNT]; for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { pending[instance] = peek(instance); assert(pending[instance]); assert(bluepad32_input_backend_native_sample_request(instance, 1, &cues[instance])); } assert(!probe_controller_input_commit_native_report(0, pending[2])); uint8_t saved_b[2][63]; memcpy(saved_b, reports + 2, sizeof(saved_b)); sources[0].controller.active = false; // Recheck the actual owning source, even before any poll sees its loss. assert(!probe_controller_input_commit_native_report(0, pending[0])); assert(!probe_controller_input_commit_native_report(1, pending[1])); inactive_child(0); inactive_child(1); for (uint8_t instance = 0; instance < 2; ++instance) assert(bluepad32_input_backend_native_sample_result(instance, cues[instance]) == -1); for (uint8_t instance = 2; instance < 4; ++instance) { assert(bluepad32_input_backend_native_sample_result(instance, cues[instance]) == 1); assert(peek(instance) == pending[instance]); assert(memcmp(saved_b[instance - 2], reports[instance], 63) == 0); assert(probe_controller_input_commit_native_report(instance, pending[instance])); } const uint32_t b_pending = peek(2); sources[0].controller.active = true; ++sources[0].controller.connection_generation; publish(true, 0); pair(0); assert(reports[0][2] == 0x01 && reports[2][2] == 0x08); assert(!probe_controller_input_commit_native_report(0, pending[0])); assert(probe_controller_input_commit_native_report(2, b_pending)); // Repeated updates on three endpoints must neither consume a blocked // endpoint's counter nor starve the other source's two endpoints. publish_both(); const uint32_t blocked_left = peek(1); const uint8_t left_counter = reports[1][0]; const uint32_t blocked_b = peek(2); const uint8_t b_counter = reports[2][0]; for (unsigned update = 0; update < 40; ++update) { sources[1].controller.state.button_east = (update & 1u) != 0; publish_both(); consume(0); pair(1); } assert(!probe_controller_input_commit_native_report(1, blocked_left)); assert(!probe_controller_input_commit_native_report(2, blocked_b)); consume(1); assert(reports[1][0] == left_counter); assert(reports[2][0] == static_cast(b_counter + 39)); assert(reports[2][2] == 0x0a); probe_controller_input_set_native_stream(0, false); assert(!peek(0)); publish_both(); const uint32_t left_pending = peek(1); pair(1); assert(probe_controller_input_commit_native_report(1, left_pending)); probe_controller_input_set_native_stream(0, true); consume(0); assert(reports[0][2] == 0x01); // USB suspension also remains child-local on PairB. publish_both(); const uint32_t a_pending = peek(0), b_left_pending = peek(3); probe_controller_input_set_native_stream(2, false); assert(!peek(2)); assert(probe_controller_input_commit_native_report(0, a_pending)); assert(probe_controller_input_commit_native_report(3, b_left_pending)); assert(bluepad32_input_backend_native_sample_result(2, cues[2]) == -1); assert(bluepad32_input_backend_native_sample_result(3, cues[3]) == 1); probe_controller_input_set_native_stream(2, true); const uint32_t expires = peek(0); // B stays live while A's queued report expires, then A's source times out. for (unsigned update = 0; update < 26; ++update) { publish(true, 1); pair(1); } assert(!probe_controller_input_commit_native_report(0, expires)); for (unsigned update = 0; update < 100; ++update) { publish(true, 1); pair(1); } const uint32_t surviving_b = peek(2); inactive_child(0); inactive_child(1); assert(probe_controller_input_commit_native_report(2, surviving_b)); assert(controls[2].active && controls[3].active && reports[2][2] == 0x0a); } void two_pair_motion_provenance_and_resets() { prepare_two_sources(true); const uint8_t side = (SWITCH2_BRIDGE_IMU_TARGET_MASK & 1) ? 0 : 1; const uint8_t a_imu = side, b_imu = 2 + side; sources[0].gyro_q10[1] = 90 * 1024; sources[1].gyro_q10[1] = -45 * 1024; for (unsigned sample = 0; sample < 250; ++sample) { publish_both(); consume(0); consume(2); consume(1); consume(3); } for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { const bool enabled = (SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << (instance & 1u))) != 0; assert(imu_length(instance) == (enabled ? 30 : 0)); } double a[4], b[4]; quaternion(a_imu, a); quaternion(b_imu, b); assert(fabs(fabs(a[0]) - sqrt(.5)) < .015); assert(fabs(fabs(b[0]) - cos(3.141592653589793 / 8)) < .015); assert(a[3] * b[3] < 0); // Opposite physical yaw cannot share one integrator. if (SWITCH2_BRIDGE_IMU_TARGET_MASK == 3) { assert(memcmp(reports[0] + probe_model_imu_data_offset(0), reports[1] + probe_model_imu_data_offset(1), 30) == 0); assert(memcmp(reports[2] + probe_model_imu_data_offset(2), reports[3] + probe_model_imu_data_offset(3), 30) == 0); } sources[0].gyro_q10[1] = sources[1].gyro_q10[1] = 0; publish_both(); pair(0); pair(1); quaternion(b_imu, b); const uint8_t* b_block = reports[b_imu] + probe_model_imu_data_offset(b_imu); const uint32_t b_ticks = bits(b_block, 0, 12); publish(false, 1); pair(1); publish(true, 0); pair(0); pair(1); assert(imu_length(2) == 0 && imu_length(3) == 0); // A cannot manufacture a B sample. const uint32_t pending_a = peek(a_imu); sources[0].controller.active = false; inactive_child(0); inactive_child(1); sources[0].controller.active = true; ++sources[0].controller.connection_generation; publish(true, 0); pair(0); assert(!probe_controller_input_commit_native_report(a_imu, pending_a)); quaternion(a_imu, a); assert(fabs(fabs(a[0]) - 1) < 1e-6); // Only A reconnects at identity heading. publish(true, 1); pair(1); double after[4]; quaternion(b_imu, after); for (unsigned axis = 0; axis < 4; ++axis) assert(fabs(after[axis] - b[axis]) < 1e-6); b_block = reports[b_imu] + probe_model_imu_data_offset(b_imu); assert(bits(b_block, 12, 12) == ((bits(b_block, 0, 12) - b_ticks) & 0xfffu)); // Reframing A to solo must not reset B's heading or in-flight motion. profiles[0].native_joycon_layout = ControllerProfileNativeJoyconLayout::kLeftSolo; ++profile_generation; publish_both(); const uint32_t b_pending = peek(b_imu); uint8_t saved[63]; memcpy(saved, reports[b_imu], sizeof(saved)); consume(1); inactive_child(0); assert(peek(b_imu) == b_pending && memcmp(saved, reports[b_imu], sizeof(saved)) == 0); assert(probe_controller_input_commit_native_report(b_imu, b_pending)); quaternion(b_imu, after); for (unsigned axis = 0; axis < 4; ++axis) assert(fabs(after[axis] - b[axis]) < 1e-6); } void recycled_slot_preserves_the_new_pairs_runtime() { prepare_two_sources(false); const uint32_t old_a = peek(0); // A disconnects without another poll. B reconnects into A's recycled // physical slot and starts a held synthetic action before A sees its loss. sources[0].controller.active = false; sources[1].slot = sources[0].slot; ++sources[1].controller.connection_generation; sources[1].controller.state.button_select = true; latching_shortcuts[1] = true; publish(false, 1); pair(1); assert(reports[2][3] == 1 && reports[3][3] == 1); sources[1].controller.state.button_select = false; publish(false, 1); pair(1); const uint32_t pending_b = peek(2); inactive_child(0); inactive_child(1); assert(!probe_controller_input_commit_native_report(0, old_a)); assert(probe_controller_input_commit_native_report(2, pending_b)); // The next evaluation exposes accidental inactive-transform retirement; // checking only the already-cached report would miss that runtime reset. publish(false, 1); pair(1); assert(reports[2][3] == 1 && reports[3][3] == 1); sources[0].slot = 1; sources[0].controller.active = true; ++sources[0].controller.connection_generation; publish(false, 0); pair(0); publish(false, 1); pair(1); assert(reports[2][3] == 1 && reports[3][3] == 1); latching_shortcuts[1] = false; } #endif } // namespace int main() { assert(!probe_controller_input_peek_native_report(0, now_ms(), reports[0])); probe_controller_input_init(); assert(probe_controller_input_start()); for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { probe_controller_input_set_native_stream(instance, true); assert(!peek(instance)); } mapped_halves_and_calibration(); independent_backpressure_and_resets(); if (SWITCH2_BRIDGE_IMU_TARGET_MASK == 3) real_motion_admission_and_loss(); selected_motion_target_keeps_both_control_halves(); wii_bias_and_independent_sensor_freshness(); nunchuk_buttons_map_to_native_left_shoulders(); solo_controls_and_explicit_rails(); profile_changes_retire_tokens_without_source_publication(); digital_dpad_reaches_the_mapped_left_stick(); solo_motion_rotates_coherently_and_resets_frame(); #if PROBE_CONTROLLER_COUNT == 4 two_pair_controls_and_profile_coherence(); two_pair_transport_and_disconnect_isolation(); two_pair_motion_provenance_and_resets(); recycled_slot_preserves_the_new_pairs_runtime(); #endif return 0; }