switch-pico/tests/native_gamepad_backend_test.cpp
Joey Yakimowich-Payne 19358a0fff feat(wake): add USB-triggered Switch 2 wake with Windows interface support
Queue correlated, volatile wake requests for the BTstack owner and expose
read-only completion state. Keep the controller chord, captured identity,
pairings, profiles, HD settings and existing wake burst unchanged.

Add Python API and wake CLI with bounded waits, explicit failure outcomes
and no automatic broadcast retries. Avoid importing SDL just to obtain
fixed sensor IDs so USB automation has clean output and no SDL dependency.

Windows libusb cannot control hub roots. Expose only INFO/WAKE on each
native child's existing vendor Interface 1 with isolated control state;
retain hub/HID drivers, descriptors and identities. Add safe sibling-aware
Windows discovery and a packaged Windows libusb runtime. Document manual
WinUSB binding to Interface 1 only and migrate examples to find_wake_pico.

Validate with 713 tests, 10 firmware/probe builds, Windows x64 wheel
resolution on Python 3.9/3.11, and a real child-interface wake burst on
Linux (2.03 s, no controller, no failures). Device/configuration descriptors,
serials and persistent state matched before/after. Physical Windows
hardware/driver operation remains unverified on this Linux workstation.
2026-09-19 20:19:46 -06:00

1679 lines
90 KiB
C++

// Reuse the backend's transport/storage fixture; these scenarios exercise only
// the native gamepad contract, not a second implementation of its scheduler.
#include <uni.h>
extern "C" bool uni_hid_parser_wii_rumble_ready(uni_hid_device_t*);
#define profile_service_active_profile_snapshot fixture_active_profile_snapshot
#define main backend_fixture_main
#include "bluepad32_backend_lifecycle_test.cpp"
#undef main
#undef profile_service_active_profile_snapshot
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
#include "input/switch_hd_rumble_synth.h"
#endif
namespace {
void (*during_profile_resolution)() = nullptr;
bool native_wii_ready = true;
}
void profile_service_active_profile_snapshot(
const ControllerIdentity& identity, ProfileServiceActiveProfileSnapshot* output) {
require(state_lock_depth == 0, "profile service callbacks must not hold the backend lock");
if (during_profile_resolution) during_profile_resolution();
fixture_active_profile_snapshot(identity, output);
}
extern "C" bool uni_hid_parser_wii_rumble_ready(uni_hid_device_t*) {
require(state_lock_depth == 0, "Wii readiness must not hold the backend lock");
return native_wii_ready;
}
namespace {
// Test data only: construct the raw native frame whose conventional peak is
// the requested old magnitude. Production has no magnitude-only ingress.
bool submit_native_magnitudes(uint8_t instance, const uint8_t* magnitudes, uint8_t count) {
if (magnitudes == nullptr)
return bluepad32_input_backend_native_rumble_submit(instance, nullptr);
NativeHapticsActuatorFrame frame{};
frame.sample_count = count;
if (count <= 3)
for (uint8_t sample = 0; sample < count; ++sample)
frame.samples[sample] = {
385, 481, static_cast<uint16_t>((uint32_t{magnitudes[sample]} * 1023u + 127u) / 255u), 0};
return bluepad32_input_backend_native_rumble_submit(instance, &frame);
}
struct SensorFixture {
uni_hid_device_t* device = nullptr;
uni_native_motion_snapshot_t metadata{};
bool valid = false;
};
SensorFixture sensors[4];
void (*during_dualsense_dispatch)() = nullptr;
bool dualsense_transport_available = true;
void observe_dualsense_rumble(uni_hid_device_t* target, uint16_t delay,
uint16_t duration, uint8_t right, uint8_t left) {
require(state_lock_depth == 0, "DS5 driver dispatch must not hold the shared state lock");
play_rumble(target, delay, duration, right, left);
if (during_dualsense_dispatch) during_dualsense_dispatch();
}
uni_hid_device_t dualsense(int index) {
auto result = device(index, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
result.vendor_id = 0x054c;
result.product_id = index == 0 ? 0x0ce6 : 0x0df2;
result.controller_type = CONTROLLER_TYPE_PS5Controller;
result.report_parser.parse_input_report = uni_hid_parser_ds5_parse_input_report;
result.report_parser.play_dual_rumble = observe_dualsense_rumble;
return result;
}
void report_dualsense(uni_hid_device_t& pad, bool fresh_motion = true) {
SensorFixture& sensor = sensors[pad.idx];
sensor.device = &pad;
sensor.valid = true;
sensor.metadata.report_tracked = true;
sensor.metadata.report_valid = true;
++sensor.metadata.report_sequence;
if (fresh_motion) {
++sensor.metadata.accel_sequence;
++sensor.metadata.gyro_sequence;
}
sensor.metadata.accel_valid = fresh_motion;
sensor.metadata.gyro_valid = fresh_motion;
sensor.metadata.accel_q13[1] = 8193;
sensor.metadata.gyro_q10[2] = -123456;
pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
pad.controller.gamepad.buttons = BUTTON_A | BUTTON_SHOULDER_L;
// Parser init_report may clear legacy fields; native precision comes from
// the independent sensor provider rather than cached controller values.
pad.controller.gamepad.accel[1] = 0;
pad.controller.gamepad.gyro[2] = 0;
pad.controller.battery = 176;
platform_on_controller_data(&pad, &pad.controller);
}
Bluepad32NativeGamepadSnapshot bridge_snapshot(uint8_t pair = 0) {
Bluepad32NativeGamepadSnapshot result{};
bluepad32_input_backend_native_snapshot(pair, &result);
return result;
}
void source_isolation() {
start_pairing_backend();
auto ordinary = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
ordinary.vendor_id = 0x054c;
ordinary.product_id = 0x0ce6;
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
ordinary.gamepad = false;
#endif
require(platform_on_device_ready(&ordinary) == UNI_ERROR_INVALID_CONTROLLER,
"an ineligible controller must not enter dedicated output slots");
bluepad32_input_backend_select_native_source(0, ordinary.conn.btaddr);
require(!bridge_snapshot().controller.active, "an ineligible device cannot become the native source");
platform_on_device_disconnected(&ordinary);
bluepad32_input_backend_select_native_source(0, nullptr);
auto first = dualsense(0);
auto second = dualsense(1);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS, "first DS5 must connect");
now_ms = 100;
report_dualsense(first);
const auto initial = bridge_snapshot();
require(initial.slot == 0 && initial.controller.active && initial.controller.state.button_south &&
initial.battery == 176 && initial.accel_valid && initial.gyro_valid &&
!initial.track_stationary_bias && initial.accel_q13[1] == 8193 &&
initial.gyro_q10[2] == -123456 && initial.gyro_received_us == 100000,
"native snapshot must preserve coherent physical controls and calibrated precision");
now_ms = 120;
platform_on_controller_data(&first, &first.controller);
bluepad32_input_backend_report_sent(0);
auto snapshot = bridge_snapshot();
require(snapshot.state_generation == initial.state_generation && snapshot.received_us == 100000 &&
snapshot.accel_sequence == initial.accel_sequence &&
snapshot.gyro_sequence == initial.gyro_sequence,
"polling/cached callbacks and USB consumption must not freshen input or motion");
report_dualsense(first, false);
snapshot = bridge_snapshot();
require(snapshot.received_us == 120000 && snapshot.gyro_received_us == 100000 &&
!snapshot.accel_valid && !snapshot.gyro_valid,
"a controls-only admission must not refresh a duplicate sensor timestamp");
sensors[0].valid = false;
first.controller.gamepad.buttons = 0;
platform_on_controller_data(&first, &first.controller);
require(bridge_snapshot().controller.state.button_south,
"malformed parser input must not publish an invented button release");
uint64_t old_token;
require(bluepad32_input_backend_native_sample_request(0, 1, &old_token), "first source cue must queue");
require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge must connect");
report_dualsense(second);
require(!bridge_snapshot().controller.active &&
bluepad32_input_backend_native_sample_result(0, old_token) == -1,
"auto ambiguity must fail closed and retire source work immediately");
platform_on_device_disconnected(&second);
platform_on_controller_data(&first, &first.controller);
require(!bridge_snapshot().controller.active, "returning to a source cannot resurrect cached state");
report_dualsense(first);
snapshot = bridge_snapshot();
require(snapshot.controller.active && snapshot.controller.connection_generation != initial.controller.connection_generation,
"a missed ambiguous interval still needs a new adapter epoch");
platform_on_device_connected(&second);
require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge reconnect must succeed");
bluepad32_input_backend_select_native_source(0, first.conn.btaddr);
report_dualsense(first);
report_dualsense(second);
require(bridge_snapshot().slot == 0, "explicit source must ignore another live PS5");
platform_on_device_disconnected(&first);
require(!bridge_snapshot().controller.active, "disconnect must not migrate an explicit source");
}
void stable_logical_slot() {
start_pairing_backend();
auto unrelated = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
unrelated.gamepad = false;
#endif
auto pad = dualsense(1);
pad.report_parser.set_lightbar_color = set_lightbar;
// An earlier, still-unclassified transport connection must not reserve player 1.
platform_on_device_connected(&unrelated);
platform_on_device_connected(&pad);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must complete setup");
now_ms = 100;
report_dualsense(pad);
const auto first = bridge_snapshot();
const auto color = switch_pro_get_slot_light_color(0);
require(first.controller.active && first.slot == 0 &&
pad.lightbar_red == color.red && pad.lightbar_green == color.green &&
pad.lightbar_blue == color.blue,
"a transport-index-1 DualSense must own logical slot 0 and its lightbar color");
require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER,
"dedicated DualSense mode must reject an unrelated ready controller");
platform_on_device_disconnected(&unrelated);
platform_on_device_connected(&unrelated);
require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER,
"remembered unrelated reconnects must remain outside logical slots");
platform_on_device_disconnected(&unrelated);
require(bridge_snapshot().slot == 0 &&
bridge_snapshot().controller.connection_generation == first.controller.connection_generation &&
pad.lightbar_calls == 1,
"unrelated connection churn must not rebind or recolor the active DualSense");
platform_on_device_disconnected(&pad);
auto reconnected = dualsense(2);
memcpy(reconnected.conn.btaddr, pad.conn.btaddr, sizeof(pad.conn.btaddr));
reconnected.report_parser.set_lightbar_color = set_lightbar;
platform_on_device_connected(&reconnected);
require(platform_on_device_ready(&reconnected) == UNI_ERROR_SUCCESS, "DS5 reconnect must succeed");
now_ms = 200;
report_dualsense(reconnected);
const auto next = bridge_snapshot();
require(next.controller.active && next.slot == 0 &&
controller_identity_equal(next.controller.identity, first.controller.identity) &&
next.controller.connection_generation != first.controller.connection_generation &&
reconnected.lightbar_red == color.red && reconnected.lightbar_green == color.green &&
reconnected.lightbar_blue == color.blue,
"reusing another Bluetooth index must preserve identity and the first logical slot");
auto aborted = dualsense(3);
platform_on_device_connected(&aborted);
platform_on_device_disconnected(&aborted);
require(platform_on_device_ready(&aborted) == UNI_ERROR_NO_SLOTS,
"a late ready callback must not resurrect an unassigned disconnected transport");
require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0,
"an aborted second setup must not disturb the active source");
auto pending0 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
auto pending1 = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
auto pending3 = device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
platform_on_device_connected(&pending0);
platform_on_device_connected(&pending1);
platform_on_device_connected(&pending3);
require(!incoming_connections && !scanning_enabled,
"unclassified transports still consume physical connection capacity");
platform_on_device_disconnected(&pending0);
require(incoming_connections && bridge_snapshot().slot == 0 &&
bridge_snapshot().controller.connection_generation == next.controller.connection_generation,
"freeing pending transport capacity must not move the logical source");
platform_on_device_disconnected(&pending1);
platform_on_device_disconnected(&pending3);
}
void cue_lifetime() {
start_pairing_backend();
auto pad = dualsense(0);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect");
uint64_t right, left, stop;
require(bluepad32_input_backend_native_sample_request(0, 6, &right) &&
bluepad32_input_backend_native_sample_request(1, 1, &left) && right != left &&
bluepad32_input_backend_native_sample_result(0, right) == 0 && pad.rumble_calls == 0,
"independent acceptance is not driver completion");
dualsense_transport_available = false;
process_rumble_timer(&g_rumble_timer);
require(bluepad32_input_backend_native_sample_result(0, right) == 0 && pad.rumble_calls == 0,
"a busy source driver must not count as dispatch completion");
dualsense_transport_available = true;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 96 && pad.last_low == 160 && pad.last_rumble_duration_ms == 60 &&
bluepad32_input_backend_native_sample_result(0, right) == 1 &&
bluepad32_input_backend_native_sample_result(1, right) == -1 &&
bluepad32_input_backend_native_sample_result(1, left) == 1,
"combined output must route R weak/right and L strong/left with the shortest safe timer");
now_ms = 60;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 0 && pad.last_low == 160 && pad.last_rumble_duration_ms == 940,
"ending right must preserve only the left pulse's original remaining lifetime");
require(bluepad32_input_backend_native_sample_request(0, 3, &right), "right can restart independently");
process_rumble_timer(&g_rumble_timer);
now_ms = 85;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 0 && pad.last_low == 160, "right gap must not stop the left motor");
now_ms = 175;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 96 && pad.last_low == 160, "later pulse must resume after its gap");
require(bluepad32_input_backend_native_sample_request(1, 0, &stop), "left stop must replace only left");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 96 && pad.last_low == 0 &&
bluepad32_input_backend_native_sample_result(1, stop) == 1 &&
bluepad32_input_backend_native_sample_result(1, left) == -1,
"a side stop needs actual dispatch and cannot stop its sibling");
now_ms = 200;
process_rumble_timer(&g_rumble_timer);
require(pad.last_rumble_duration_ms == 0, "final pulse expiry must release both motors");
const int stopped = pad.rumble_calls;
now_ms = 5000;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == stopped, "expired pulses must never replay after a stall");
require(bluepad32_input_backend_native_sample_request(0, 1, &right), "pending timeout cue must queue");
now_ms += 2000;
process_rumble_timer(&g_rumble_timer);
require(bluepad32_input_backend_native_sample_result(0, right) == -1 && pad.rumble_calls == stopped,
"an undispatched expired cue must fail without producing a late pulse");
}
void cue_races() {
start_pairing_backend();
auto pad = dualsense(0);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect");
uint64_t token;
require(bluepad32_input_backend_native_sample_request(0, 1, &token), "race cue must queue");
during_dualsense_dispatch = [] { bluepad32_input_backend_native_sample_cancel(0); };
process_rumble_timer(&g_rumble_timer);
during_dualsense_dispatch = nullptr;
require(bluepad32_input_backend_native_sample_result(0, token) == -1,
"cancellation during dispatch must defeat a late completion");
process_rumble_timer(&g_rumble_timer);
require(pad.last_rumble_duration_ms == 0, "in-flight cancellation must retain a bounded stop obligation");
require(bluepad32_input_backend_native_sample_request(1, 1, &token), "reselection race must queue");
during_dualsense_dispatch = [] { bluepad32_input_backend_select_native_source(0, nullptr); };
process_rumble_timer(&g_rumble_timer);
during_dualsense_dispatch = nullptr;
require(bluepad32_input_backend_native_sample_result(1, token) == -1,
"reselection must retire an in-flight token even for the same physical source");
process_rumble_timer(&g_rumble_timer);
require(pad.last_rumble_duration_ms == 0, "reselection cannot orphan the just-dispatched motor");
require(bluepad32_input_backend_native_sample_request(0, 1, &token), "disconnect race must queue");
platform_on_device_disconnected(&pad);
auto replacement = dualsense(0);
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement must connect");
process_rumble_timer(&g_rumble_timer);
require(bluepad32_input_backend_native_sample_result(0, token) == -1 && replacement.rumble_calls == 0,
"old tokens and deferred stops must never enter a replacement connection");
}
void report_gamepad(uni_hid_device_t& pad) {
pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
platform_on_controller_data(&pad, &pad.controller);
}
SensorFixture& motion_fixture(uni_hid_device_t& pad, bool tracked = true) {
SensorFixture& fixture = sensors[pad.idx];
fixture = {};
fixture.device = &pad;
fixture.valid = true;
fixture.metadata.report_tracked = tracked;
fixture.metadata.report_valid = true;
fixture.metadata.report_sequence = 1;
fixture.metadata.accel_sequence = 1;
fixture.metadata.gyro_sequence = 1;
fixture.metadata.accel_valid = true;
fixture.metadata.gyro_valid = true;
fixture.metadata.accel_q13[1] = 8193;
fixture.metadata.gyro_q10[2] = -123456;
return fixture;
}
void sensorless_admission() {
start_pairing_backend();
auto xbox = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
xbox.controller_type = CONTROLLER_TYPE_XBoxOneController;
xbox.report_parser.play_dual_rumble = nullptr;
platform_on_device_connected(&xbox);
require(platform_on_device_ready(&xbox) == UNI_ERROR_SUCCESS,
"normal gamepad admission must not require a motion parser");
xbox.controller.gamepad.buttons = BUTTON_A;
xbox.controller.gamepad.accel[1] = 8192;
xbox.controller.gamepad.gyro[0] = 65536;
now_ms = 100;
report_gamepad(xbox);
const auto initial = bridge_snapshot();
uint64_t cue = 99;
require(initial.controller.active && initial.slot == 0 &&
initial.controller.state.button_south && !initial.accel_valid &&
!initial.gyro_valid && !initial.track_stationary_bias &&
initial.accel_sequence == 0 && initial.gyro_sequence == 0 &&
!bluepad32_input_backend_native_sample_request(0, 1, &cue) && cue == 0,
"sensorless controls remain live without invented IMU or rumble capability");
auto generic = device(0, true, UNI_BT_CONN_PROTOCOL_BLE);
platform_on_device_connected(&generic);
require(platform_on_device_ready(&generic) == UNI_ERROR_SUCCESS,
"unknown-family normal AIO gamepads must not face a native brand whitelist");
report_gamepad(generic);
require(!bridge_snapshot().controller.active, "two logical gamepads are ambiguous");
bluepad32_input_backend_select_native_source(0, xbox.conn.btaddr);
now_ms = 110;
report_gamepad(xbox);
require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0 &&
controller_identity_equal(bridge_snapshot().controller.identity, initial.controller.identity),
"explicit sensorless selection retains its real identity and logical slot");
platform_on_device_disconnected(&xbox);
report_gamepad(generic);
require(!bridge_snapshot().controller.active, "explicit selection cannot migrate on disconnect");
bluepad32_input_backend_select_native_source(0, nullptr);
generic.controller.gamepad.buttons = BUTTON_B;
report_gamepad(generic);
require(bridge_snapshot().controller.active && bridge_snapshot().controller.state.button_east &&
!bridge_snapshot().controller.state.button_south,
"unique sensorless reselection must publish only the new source's controls");
}
void independent_motion() {
start_pairing_backend();
auto ds4 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
ds4.controller_type = CONTROLLER_TYPE_PS4Controller;
require(platform_on_device_ready(&ds4) == UNI_ERROR_SUCCESS, "DS4 controls must be admitted");
auto& ds = motion_fixture(ds4).metadata;
ds4.controller.gamepad.buttons = BUTTON_A;
now_ms = 100;
report_gamepad(ds4);
const auto first = bridge_snapshot();
require(first.accel_valid && first.gyro_valid && !first.track_stationary_bias &&
first.accel_q13[1] == 8193 && first.gyro_q10[2] == -123456,
"calibrated DS4 motion retains precision without Wii background correction");
now_ms = 110;
++ds.report_sequence;
++ds.accel_sequence;
report_gamepad(ds4);
require(bridge_snapshot().accel_received_us == 110000 &&
bridge_snapshot().gyro_received_us == 100000 && bridge_snapshot().gyro_valid,
"acceleration ingress must not refresh an unchanged gyro");
now_ms = 120;
ds.report_valid = false;
ds4.controller.gamepad.buttons = 0;
report_gamepad(ds4);
require(bridge_snapshot().controller.state.button_south &&
bridge_snapshot().received_us == 110000,
"a tracked partial/malformed report cannot publish phantom releases");
ds.report_valid = true;
++ds.report_sequence;
ds.gyro_valid = false;
report_gamepad(ds4);
require(!bridge_snapshot().gyro_valid && bridge_snapshot().accel_valid,
"gyro capability loss must not suppress working acceleration or controls");
bluepad32_input_backend_select_native_source(0, nullptr);
++ds.report_sequence;
ds.gyro_valid = true;
report_gamepad(ds4);
require(bridge_snapshot().controller.active && !bridge_snapshot().accel_valid &&
!bridge_snapshot().gyro_valid,
"reselection cannot rehabilitate cached sensor sequences");
++ds.report_sequence;
++ds.gyro_sequence;
report_gamepad(ds4);
require(bridge_snapshot().gyro_valid && !bridge_snapshot().accel_valid,
"each sensor must earn freshness separately in a new source epoch");
platform_on_device_disconnected(&ds4);
auto pro = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
pro.controller_type = CONTROLLER_TYPE_SwitchProController;
require(platform_on_device_ready(&pro) == UNI_ERROR_SUCCESS, "Switch Pro controls must be admitted");
auto& sw = motion_fixture(pro).metadata;
sw.accel_valid = sw.gyro_valid = false;
pro.controller.gamepad.buttons = BUTTON_B;
report_gamepad(pro);
require(bridge_snapshot().controller.state.button_east && !bridge_snapshot().gyro_valid,
"Switch controls must not wait for motion calibration");
++sw.report_sequence;
++sw.accel_sequence;
++sw.gyro_sequence;
sw.accel_valid = sw.gyro_valid = true;
report_gamepad(pro);
require(bridge_snapshot().accel_valid && bridge_snapshot().gyro_valid &&
!bridge_snapshot().track_stationary_bias,
"validated Switch motion does not enable Wii background correction");
platform_on_device_disconnected(&pro);
auto remote = wii_device(2);
require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii controls must be admitted");
auto& wii = motion_fixture(remote, false).metadata;
wii.gyro_valid = false;
remote.controller.gamepad.buttons = BUTTON_A;
now_ms = 200;
report_gamepad(remote);
require(bridge_snapshot().controller.state.button_south && bridge_snapshot().accel_valid &&
!bridge_snapshot().gyro_valid && bridge_snapshot().track_stationary_bias,
"Wii without MotionPlus remains acceleration-capable, not a fabricated full IMU");
now_ms = 210;
++wii.gyro_sequence;
wii.gyro_valid = true;
report_gamepad(remote);
require(bridge_snapshot().gyro_valid && bridge_snapshot().gyro_received_us == 210000 &&
bridge_snapshot().accel_received_us == 200000 &&
bridge_snapshot().track_stationary_bias,
"Wii MotionPlus ingress must retain independent acceleration age and Wii bias policy");
}
void paired_source() {
start_pairing_backend();
auto left = switch2_device(2, UNI_SW2_JOYCON_L_PID);
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
ready_switch2(left);
ready_switch2(right);
auto& r = motion_fixture(right).metadata;
auto& l = motion_fixture(left).metadata;
right.controller.gamepad.buttons = BUTTON_A;
left.controller.gamepad.dpad = DPAD_LEFT;
now_ms = 100;
report_gamepad(right);
const auto initial = bridge_snapshot();
require_pair_owner(initial.controller.identity, left, right);
require(initial.controller.active && initial.slot == 0 && initial.gyro_valid &&
!slot_snapshot(1).active && !slot_snapshot(2).active,
"an existing Joy-Con pair is one logical native source, not auto ambiguity");
now_ms = 110;
l.gyro_q10[2] = 654321;
report_gamepad(left);
require(bridge_snapshot().controller.state.button_south &&
bridge_snapshot().controller.state.dpad_left &&
bridge_snapshot().received_us == 110000 &&
bridge_snapshot().gyro_received_us == 100000 &&
bridge_snapshot().gyro_q10[2] == initial.gyro_q10[2],
"left controls merge without refreshing or replacing the right motion owner");
bluepad32_input_backend_select_native_source(0, right.conn.btaddr);
++l.report_sequence;
report_gamepad(left);
require(bridge_snapshot().controller.active && !bridge_snapshot().gyro_valid,
"either member address selects the logical pair, not that member's cached IMU");
++r.report_sequence;
report_gamepad(right);
require(!bridge_snapshot().gyro_valid, "reselection cannot freshen the right parser cache");
++r.report_sequence;
++r.accel_sequence;
++r.gyro_sequence;
report_gamepad(right);
require(bridge_snapshot().gyro_valid, "new right samples restore paired motion");
uint64_t rc, lc, stop;
require(bluepad32_input_backend_native_sample_request(0, 6, &rc) &&
bluepad32_input_backend_native_sample_request(1, 1, &lc),
"both real halves expose their own driver capability");
process_rumble_timer(&g_rumble_timer);
require(right.last_high == 96 && right.last_low == 96 &&
left.last_high == 160 && left.last_low == 160 &&
bluepad32_input_backend_native_sample_result(0, rc) == 1 &&
bluepad32_input_backend_native_sample_result(1, lc) == 1,
"paired cues must dispatch only each half's contribution to its real driver");
now_ms = 120;
require(bluepad32_input_backend_native_sample_request(0, 0, &stop), "right stop must queue");
process_rumble_timer(&g_rumble_timer);
require(right.last_rumble_duration_ms == 0 && left.last_rumble_duration_ms == 990 &&
bluepad32_input_backend_native_sample_result(0, stop) == 1,
"stopping one paired side preserves the other side's original finite deadline");
bluepad32_input_backend_select_native_source(0, nullptr);
set_runtime_joycon_mode(JoyConMode::kIndividual);
require(!bridge_snapshot().controller.active &&
bluepad32_input_backend_native_sample_result(1, lc) == -1,
"live split retires the pair immediately and fails auto selection closed");
bluepad32_input_backend_select_native_source(0, right.conn.btaddr);
++r.report_sequence;
++r.accel_sequence;
++r.gyro_sequence;
report_gamepad(right);
require(bridge_snapshot().controller.active &&
controller_identity_equal(bridge_snapshot().controller.identity, identity_for_device(&right)),
"explicit selection after a split retains the real surviving member identity");
}
void pair_cue_races() {
start_pairing_backend();
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
ready_switch2(left);
ready_switch2(right);
left.report_parser.play_dual_rumble = nullptr;
uint64_t rc, lc;
require(!bluepad32_input_backend_native_sample_request(1, 1, &lc) && lc == 0,
"a paired half without a rumble driver cannot borrow its sibling's capability");
left.report_parser.play_dual_rumble = play_rumble;
right.report_parser.play_dual_rumble = [](
uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) {
require(state_lock_depth == 0, "paired driver dispatch must not hold the shared lock");
play_rumble(pad, delay, duration, weak, strong);
bluepad32_input_backend_native_sample_cancel(1);
};
require(bluepad32_input_backend_native_sample_request(0, 1, &rc) &&
bluepad32_input_backend_native_sample_request(1, 1, &lc), "paired race cues must queue");
const int before = left.rumble_calls;
process_rumble_timer(&g_rumble_timer);
require(left.rumble_calls == before && right.last_rumble_duration_ms == 1000 &&
bluepad32_input_backend_native_sample_result(0, rc) == 1 &&
bluepad32_input_backend_native_sample_result(1, lc) == -1,
"canceling L during R dispatch must not submit stale L or retire the accepted R cue");
bluepad32_input_backend_native_sample_cancel(0);
process_rumble_timer(&g_rumble_timer);
const int before_stall = left.rumble_calls;
require(right.last_rumble_duration_ms == 0, "partial pair submission retains a real stop obligation");
right.report_parser.play_dual_rumble = [](
uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) {
play_rumble(pad, delay, duration, weak, strong);
now_ms += 2000;
bluepad32_input_backend_select_native_source(0, nullptr);
};
require(bluepad32_input_backend_native_sample_request(0, 1, &rc) &&
bluepad32_input_backend_native_sample_request(1, 1, &lc), "reselection race cues must queue");
process_rumble_timer(&g_rumble_timer);
right.report_parser.play_dual_rumble = play_rumble;
require(left.rumble_calls == before_stall &&
bluepad32_input_backend_native_sample_result(0, rc) == -1 &&
bluepad32_input_backend_native_sample_result(1, lc) == -1,
"reselection and a dispatch stall cannot submit the old epoch's remaining half");
process_rumble_timer(&g_rumble_timer);
require(right.last_rumble_duration_ms == 0, "reselection cannot orphan a partial pair output");
}
uint8_t mono_magnitude = 0;
void observe_mono_rumble(uni_hid_device_t* pad, uint16_t delay, uint16_t duration,
uint8_t weak, uint8_t strong) {
require(state_lock_depth == 0 && delay == 0, "mono driver dispatch is immediate and outside the lock");
mono_magnitude = duration == 0 ? 0 : (weak > strong ? weak : strong);
play_rumble(pad, delay, duration, mono_magnitude, 0);
}
void mono_rumble() {
start_pairing_backend();
auto move = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
move.controller_type = CONTROLLER_TYPE_PSMoveController;
move.report_parser.play_dual_rumble = observe_mono_rumble;
require(platform_on_device_ready(&move) == UNI_ERROR_SUCCESS, "mono-rumble gamepad must connect");
uint64_t right, left;
require(bluepad32_input_backend_native_sample_request(0, 1, &right) &&
bluepad32_input_backend_native_sample_request(1, 7, &left),
"mono devices accept independent logical contributions, not separate actuators");
process_rumble_timer(&g_rumble_timer);
require(mono_magnitude == 220 && move.last_rumble_duration_ms == 120,
"one mono actuator combines both contributions with the shortest finite boundary");
bluepad32_input_backend_native_sample_cancel(1);
now_ms = 40;
process_rumble_timer(&g_rumble_timer);
require(mono_magnitude == 160 && move.last_rumble_duration_ms == 960 &&
bluepad32_input_backend_native_sample_result(1, left) == -1,
"canceling one mono contribution must preserve the other's remaining pulse");
now_ms = 1000;
process_rumble_timer(&g_rumble_timer);
require(mono_magnitude == 0, "the last mono contribution must stop at its original deadline");
require(bluepad32_input_backend_native_sample_request(0, 1, &right), "new mono cue must queue");
const int calls = move.rumble_calls;
now_ms += 2000;
process_rumble_timer(&g_rumble_timer);
require(move.rumble_calls == calls && bluepad32_input_backend_native_sample_result(0, right) == -1,
"expired undispatched mono work cannot replay after a timer stall");
require(bluepad32_input_backend_native_sample_request(1, 1, &left), "disconnect cue must queue");
process_rumble_timer(&g_rumble_timer);
require(mono_magnitude == 160, "disconnect regression must own a real active driver pulse");
platform_on_device_disconnected(&move);
require(mono_magnitude == 0 && move.last_rumble_duration_ms == 0,
"disconnect must retire the source driver's timer before parser memory reuse");
auto replacement = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
replacement.report_parser.play_dual_rumble = observe_mono_rumble;
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "new mono connection must succeed");
process_rumble_timer(&g_rumble_timer);
require(replacement.rumble_calls == 0 &&
bluepad32_input_backend_native_sample_result(1, left) == -1,
"retired mono work must never enter a replacement connection");
}
void gameplay_timeline() {
start_pairing_backend();
auto pad = dualsense(0);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "gameplay source must connect");
uint8_t right[] = {40, 80, 120};
const uint8_t left[] = {60, 180};
const uint8_t stop = 0;
require(submit_native_magnitudes(0, right, 3) &&
submit_native_magnitudes(1, left, 2),
"mixed sample counts must be accepted independently");
right[0] = 255;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 40 && pad.last_low == 60 && pad.last_rumble_duration_ms == 4,
"copied right/weak and left/strong samples share the earliest finite boundary");
now_ms = 5;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 80 && pad.last_low == 60 && pad.last_rumble_duration_ms == 1,
"5 ms polling skips elapsed time rather than replaying the first sample");
now_ms = 10;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 120 && pad.last_low == 180 && pad.last_rumble_duration_ms == 40,
"last samples hold only to their original receipt watchdog");
require(submit_native_magnitudes(0, &stop, 1),
"explicit zero magnitude must be a valid side stop");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 0 && pad.last_low == 180 && pad.last_rumble_duration_ms == 40,
"right stop must preserve the left contribution and deadline");
now_ms = 49;
const int calls = pad.rumble_calls;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == calls, "unchanged finite holds must not produce duplicate writes");
now_ms = 50;
process_rumble_timer(&g_rumble_timer);
require(pad.last_rumble_duration_ms == 0, "watchdog expiry must stop without further host packets");
now_ms = 500;
process_rumble_timer(&g_rumble_timer);
const int stopped = pad.rumble_calls;
const uint8_t delayed[] = {21, 42, 84};
require(submit_native_magnitudes(0, delayed, 3), "delayed block must queue");
now_ms = 520;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == stopped + 1 && pad.last_high == 84 &&
pad.last_rumble_duration_ms == 30,
"a stalled timer dispatches only the current sample with its remaining lifetime");
bluepad32_input_backend_native_rumble_cancel(0);
process_rumble_timer(&g_rumble_timer);
require(pad.last_rumble_duration_ms == 0, "explicit cancellation must stop its live hold");
now_ms = UINT32_MAX - 9u;
const uint8_t pulse = 99;
require(submit_native_magnitudes(1, &pulse, 1), "pre-wrap request must queue");
process_rumble_timer(&g_rumble_timer);
require(pad.last_low == 99 && pad.last_rumble_duration_ms == 50,
"finite host lifetime must remain valid before clock wrap");
now_ms = 39;
process_rumble_timer(&g_rumble_timer);
require(pad.last_low == 99, "wrap must not cause early watchdog expiry");
now_ms = 40;
process_rumble_timer(&g_rumble_timer);
require(pad.last_rumble_duration_ms == 0, "watchdog must expire exactly across clock wrap");
}
void gameplay_availability() {
start_pairing_backend();
auto pad = dualsense(0);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "busy gameplay source must connect");
const uint8_t old[] = {10, 20, 30};
const uint8_t newest[] = {70, 140};
require(submit_native_magnitudes(0, old, 3), "old block must queue");
dualsense_transport_available = false;
process_rumble_timer(&g_rumble_timer);
now_ms = 5;
require(submit_native_magnitudes(0, newest, 2), "busy source must retain a new block");
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == 0, "driver rejection cannot count as a successful output");
now_ms = 15;
dualsense_transport_available = true;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == 1 && pad.last_high == 140 && pad.last_rumble_duration_ms == 40,
"driver recovery must dispatch only the newest current phase, not old samples");
const uint8_t next = 210;
during_dualsense_dispatch = [] {
const uint8_t replacement = 33;
require(submit_native_magnitudes(0, &replacement, 1),
"gameplay must replace work while an earlier driver call is in flight");
};
require(submit_native_magnitudes(0, &next, 1), "dispatch race must queue");
process_rumble_timer(&g_rumble_timer);
during_dualsense_dispatch = nullptr;
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 33 && pad.last_rumble_duration_ms == 50,
"older dispatch completion cannot consume a newly accepted revision");
bluepad32_input_backend_native_rumble_cancel(0);
process_rumble_timer(&g_rumble_timer);
const int stopped = pad.rumble_calls;
dualsense_transport_available = false;
require(submit_native_magnitudes(0, old, 3), "stale block must queue");
now_ms += 50;
process_rumble_timer(&g_rumble_timer);
dualsense_transport_available = true;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == stopped, "expired rejected work must never replay after driver recovery");
}
void gameplay_priority() {
start_pairing_backend();
auto pad = dualsense(0);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "priority source must connect");
report_dualsense(pad);
const uint8_t game = 45;
uint64_t old_cue, new_cue;
require(bluepad32_input_backend_native_sample_request(0, 1, &old_cue) &&
submit_native_magnitudes(0, &game, 1),
"gameplay must replace a pending cue on the same side");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 45 &&
bluepad32_input_backend_native_sample_result(0, old_cue) == -1,
"replaced cue cannot dispatch or complete after gameplay");
require(bluepad32_input_backend_native_sample_request(0, 6, &new_cue),
"a new cue must replace live gameplay");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 96 && pad.last_rumble_duration_ms == 60 &&
bluepad32_input_backend_native_sample_result(0, new_cue) == 1,
"built-in cue completion must retain its driver-dispatch semantics");
require(submit_native_magnitudes(0, &game, 1) &&
submit_native_magnitudes(1, &game, 1),
"fresh gameplay must replace the playing cue");
process_rumble_timer(&g_rumble_timer);
const auto source = bridge_snapshot();
bluepad32_input_backend_queue_profile_feedback(source.slot,
source.controller.connection_generation, 1, ControllerProfileConfirmationPolicy::kRumble);
require(!submit_native_magnitudes(0, &game, 1),
"queued higher-priority feedback must not admit gameplay for later replay");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == UINT8_MAX && pad.last_low == UINT8_MAX &&
pad.last_rumble_duration_ms == 75, "profile feedback must own the only motor writer");
now_ms = 150;
process_rumble_timer(&g_rumble_timer);
const int after_feedback = pad.rumble_calls;
now_ms = 155;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == after_feedback, "profile completion must never resurrect interrupted gameplay");
require(submit_native_magnitudes(0, &game, 1), "fresh post-feedback gameplay must resume");
process_rumble_timer(&g_rumble_timer);
require(bluepad32_input_backend_toggle_motion(source.slot, source.controller.connection_generation),
"motion feedback must queue through the existing local-feedback path");
process_rumble_timer(&g_rumble_timer);
const int local_calls = pad.rumble_calls;
now_ms += 3000;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == local_calls, "local feedback must cancel rather than retain interrupted gameplay");
}
void gameplay_source_epochs() {
start_pairing_backend();
auto pad = dualsense(0);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "epoch source must connect");
const uint8_t game = 170;
require(!submit_native_magnitudes(0, nullptr, 1) &&
!submit_native_magnitudes(0, &game, 0) &&
!submit_native_magnitudes(0, &game, 4) &&
!submit_native_magnitudes(PROBE_CONTROLLER_COUNT, &game, 1),
"invalid gameplay frames must fail before dispatch");
pad.report_parser.play_dual_rumble = nullptr;
require(!submit_native_magnitudes(0, &game, 1),
"source without a rumble driver must reject gameplay");
pad.report_parser.play_dual_rumble = observe_dualsense_rumble;
require(submit_native_magnitudes(0, &game, 1), "epoch block must queue");
during_dualsense_dispatch = [] { bluepad32_input_backend_select_native_source(0, nullptr); };
process_rumble_timer(&g_rumble_timer);
during_dualsense_dispatch = nullptr;
process_rumble_timer(&g_rumble_timer);
require(pad.last_rumble_duration_ms == 0, "same-source reselection must stop an in-flight retired epoch");
require(submit_native_magnitudes(1, &game, 1), "disconnect block must queue");
process_rumble_timer(&g_rumble_timer);
platform_on_device_disconnected(&pad);
require(pad.last_rumble_duration_ms == 0 &&
!submit_native_magnitudes(0, &game, 1),
"disconnect must retire the driver's finite timer and refuse new work");
pad = dualsense(0);
platform_on_device_connected(&pad);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "same-address slot replacement must connect");
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == 0, "slot memory reuse cannot inherit old samples or stop obligations");
controller_profile_runtime_reset();
during_profile_resolution = [] { bluepad32_input_backend_select_native_source(0, nullptr); };
require(!submit_native_magnitudes(0, &game, 1),
"source generation must be rechecked after unlocked profile callbacks");
during_profile_resolution = nullptr;
process_rumble_timer(&g_rumble_timer);
require(pad.rumble_calls == 0, "a profile-resolution race cannot reach the replacement epoch");
}
void gameplay_profile_gain() {
start_pairing_backend();
initialize_runtime_profile_storage();
auto pad = dualsense(0);
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "profile source must connect");
const auto identity = identity_for_device(&pad);
auto profile = controller_profile_default(identity, 0);
profile.weak_rumble_scale = 128;
profile.strong_rumble_scale = 64;
require(runtime_profile_storage.set(identity, 0, profile) == ProfileStorageResult::kOk,
"profile must configure independent host motor gains");
const uint8_t maximum = 255;
const NativeHapticsActuatorFrame conventional{1, {{1023, 0, 512, 1023}}};
require(bluepad32_input_backend_native_rumble_submit(0, &conventional) &&
bluepad32_input_backend_native_rumble_submit(1, &conventional),
"conventional motors must ignore carrier codes they do not render");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 128 && pad.last_low == 64,
"right/weak and left/strong magnitudes must use their matching persisted profile gains");
profile.weak_rumble_scale = 0;
profile.strong_rumble_scale = 0;
require(runtime_profile_storage.set(identity, 0, profile) == ProfileStorageResult::kOk,
"profile mute must update its generation");
require(submit_native_magnitudes(0, &maximum, 1) &&
submit_native_magnitudes(1, &maximum, 1), "muted gameplay must queue");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 0 && pad.last_low == 0 && pad.last_rumble_duration_ms == 0,
"muting a profile must stop live host output rather than retaining unscaled samples");
uint64_t cue;
require(bluepad32_input_backend_native_sample_request(0, 7, &cue), "muted profile still permits local cues");
process_rumble_timer(&g_rumble_timer);
require(pad.last_high == 220 && bluepad32_input_backend_native_sample_result(0, cue) == 1,
"host gain must not scale built-in local confirmation cues");
}
void gameplay_two_pairs() {
start_pairing_backend();
auto first = dualsense(0);
auto second = dualsense(1);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "both gameplay pairs must connect");
const uint8_t values[] = {31, 62, 93, 124};
for (uint8_t instance = 0; instance < 4; ++instance)
require(submit_native_magnitudes(instance, values + instance, 1),
"each gameplay child must bind its own pair");
process_rumble_timer(&g_rumble_timer);
require(first.last_high == 31 && first.last_low == 62 &&
second.last_high == 93 && second.last_low == 124,
"four logical contributions must route to two independent physical sources");
bluepad32_input_backend_native_rumble_cancel(0);
now_ms = 10;
process_rumble_timer(&g_rumble_timer);
require(first.last_high == 0 && first.last_low == 62 && first.last_rumble_duration_ms == 40 &&
second.rumble_calls == 1, "side cancellation cannot dispatch into another pair");
bluepad32_input_backend_select_native_source(0, nullptr);
process_rumble_timer(&g_rumble_timer);
require(first.last_rumble_duration_ms == 0 && second.rumble_calls == 1,
"source reselection must retire only its own pair's output");
platform_on_device_disconnected(&first);
now_ms = 50;
process_rumble_timer(&g_rumble_timer);
require(second.last_rumble_duration_ms == 0 && second.rumble_calls == 2,
"the surviving pair must expire on its original independent watchdog");
}
void gameplay_paired_revision() {
start_pairing_backend();
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
ready_switch2(left);
ready_switch2(right);
right.report_parser.play_dual_rumble = [](
uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) {
play_rumble(pad, delay, duration, weak, strong);
const uint8_t fresh = 150;
require(submit_native_magnitudes(1, &fresh, 1),
"right dispatch may accept a newer left revision");
};
const uint8_t game = 75;
require(submit_native_magnitudes(0, &game, 1), "paired gameplay must queue");
const int previous_left = left.rumble_calls;
process_rumble_timer(&g_rumble_timer);
require(right.last_high == 75 && right.last_low == 75 && left.rumble_calls == previous_left,
"new left work must invalidate even a prepared zero-output command before left dispatch");
right.report_parser.play_dual_rumble = play_rumble;
process_rumble_timer(&g_rumble_timer);
require(left.last_high == 150 && left.last_low == 150,
"the newer paired-side revision must remain pending until its real driver dispatch");
const uint8_t stop = 0;
require(submit_native_magnitudes(0, &stop, 1), "paired right stop must queue");
process_rumble_timer(&g_rumble_timer);
require(right.last_rumble_duration_ms == 0 && left.last_high == 150 &&
left.last_rumble_duration_ms == 50, "paired stop must preserve only the live sibling contribution");
}
void gameplay_wii() {
start_pairing_backend();
auto remote = wii_device(0);
remote.report_parser.play_dual_rumble = observe_mono_rumble;
require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii GAMEPAD source must connect");
const uint8_t right = 70, left = 140;
require(submit_native_magnitudes(0, &right, 1) &&
submit_native_magnitudes(1, &left, 1), "Wii contributions must queue");
native_wii_ready = false;
process_rumble_timer(&g_rumble_timer);
require(remote.rumble_calls == 0, "Wii topology setup must not consume pending output");
native_wii_ready = true;
now_ms = 10;
process_rumble_timer(&g_rumble_timer);
require(mono_magnitude == 140 && remote.last_rumble_duration_ms == 40,
"Wii compatibility combines both contributions on its finite mono motor");
bluepad32_input_backend_native_rumble_cancel(1);
process_rumble_timer(&g_rumble_timer);
require(mono_magnitude == 70 && remote.last_rumble_duration_ms == 40,
"Wii side cancellation must not stop its sibling's mono contribution");
now_ms = 50;
process_rumble_timer(&g_rumble_timer);
require(mono_magnitude == 0, "Wii gameplay must stop at its original watchdog");
}
} // namespace
extern "C" void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t*, const uint8_t*, uint16_t) {}
extern "C" bool uni_hid_parser_ds5_bridge_rumble(
uni_hid_device_t* pad, uint16_t duration, uint8_t right, uint8_t left) {
if (!dualsense_transport_available) return false;
observe_dualsense_rumble(pad, 0, duration, right, left);
return true;
}
extern "C" bool uni_hid_parser_native_motion_snapshot(
uni_hid_device_t* pad, uni_native_motion_snapshot_t* out) {
*out = {};
for (const auto& fixture : sensors) {
if (fixture.device != pad) continue;
*out = fixture.metadata;
if (!fixture.valid) out->report_valid = false;
return fixture.valid;
}
// DS5 must be guarded even before its first good full report.
out->report_tracked =
pad->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report;
return false;
}
namespace {
void two_pair_sources() {
start_pairing_backend();
auto first = dualsense(2);
auto second = dualsense(0);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&second) == UNI_ERROR_SUCCESS,
"two independent physical pads must be admitted");
auto& a = motion_fixture(first).metadata;
auto& b = motion_fixture(second).metadata;
first.controller.gamepad.buttons = BUTTON_A | BUTTON_SHOULDER_L;
second.controller.gamepad.buttons = BUTTON_B | BUTTON_SHOULDER_R;
a.gyro_q10[2] = 10000;
b.gyro_q10[2] = -20000;
now_ms = 100;
report_gamepad(first);
now_ms = 110;
report_gamepad(second);
const auto initial_a = bridge_snapshot(0);
const auto initial_b = bridge_snapshot(1);
require(initial_a.controller.active && initial_b.controller.active &&
initial_a.slot != initial_b.slot &&
initial_a.controller.state.button_south && !initial_a.controller.state.button_east &&
initial_b.controller.state.button_east && !initial_b.controller.state.button_south &&
initial_a.gyro_q10[2] == 10000 && initial_b.gyro_q10[2] == -20000,
"each pair must publish only its own controls and calibrated motion");
initialize_runtime_profile_storage();
auto profile_a = controller_profile_default(initial_a.controller.identity, 2);
auto profile_b = controller_profile_default(initial_b.controller.identity, 5);
profile_a.confirmation_policy = profile_b.confirmation_policy = ControllerProfileConfirmationPolicy::kNone;
profile_a.button_map[static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth)] =
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
profile_b.button_map[static_cast<uint8_t>(ControllerProfileLogicalButton::kEast)] =
static_cast<uint8_t>(ControllerProfileLogicalButton::kWest);
require(runtime_profile_storage.set(initial_a.controller.identity, 2, profile_a) == ProfileStorageResult::kOk &&
runtime_profile_storage.activate(initial_a.controller.identity, 2) == ProfileStorageResult::kOk &&
runtime_profile_storage.set(initial_b.controller.identity, 5, profile_b) == ProfileStorageResult::kOk &&
runtime_profile_storage.activate(initial_b.controller.identity, 5) == ProfileStorageResult::kOk,
"independent identities must retain distinct active mapping banks");
controller_profile_runtime_reset();
const auto mapped_a = controller_profile_runtime_transform(
initial_a.slot, initial_a.controller, now_ms, AdapterUsbMode::kXInput);
const auto mapped_b = controller_profile_runtime_transform(
initial_b.slot, initial_b.controller, now_ms, AdapterUsbMode::kXInput);
require(mapped_a.state.button_north && !mapped_a.state.button_south &&
mapped_b.state.button_west && !mapped_b.state.button_east,
"each published source must use its own saved profile mapping");
now_ms = 120;
++a.report_sequence;
++a.accel_sequence;
first.controller.gamepad.buttons = BUTTON_X;
report_gamepad(first);
require(bridge_snapshot(0).controller.state.button_west &&
bridge_snapshot(0).accel_received_us == 120000 &&
bridge_snapshot(0).gyro_received_us == 100000 &&
bridge_snapshot(1).controller.state.button_east &&
bridge_snapshot(1).received_us == 110000 &&
bridge_snapshot(1).gyro_received_us == 110000,
"one source's input and independent sensor clocks must not freshen the other source");
require(bluepad32_input_backend_capture_start(
initial_b.slot, initial_b.controller.connection_generation, CaptureOptions{}),
"Pair B must be recordable while Pair A changes connections");
uint64_t old_a, live_b;
require(bluepad32_input_backend_native_sample_request(0, 1, &old_a) &&
bluepad32_input_backend_native_sample_request(3, 1, &live_b),
"both sources must accept independent pending feedback");
platform_on_device_disconnected(&first);
auto extra = dualsense(1);
require(platform_on_device_ready(&extra) == UNI_ERROR_SUCCESS, "third source may connect without assignment");
report_dualsense(extra);
require(!bridge_snapshot(0).controller.active &&
bridge_snapshot(1).controller.connection_generation == initial_b.controller.connection_generation &&
bluepad32_input_backend_native_sample_result(0, old_a) == -1 &&
bluepad32_input_backend_native_sample_result(3, live_b) == 0,
"a new third pad cannot steal a disconnected reservation or retire the independent pair");
auto reconnected = dualsense(3);
memcpy(reconnected.conn.btaddr, first.conn.btaddr, sizeof(first.conn.btaddr));
reconnected.product_id = first.product_id;
platform_on_device_connected(&reconnected);
require(platform_on_device_ready(&reconnected) == UNI_ERROR_SUCCESS, "reserved source must reconnect");
now_ms = 130;
report_dualsense(reconnected);
require(bridge_snapshot(0).controller.active &&
controller_identity_equal(bridge_snapshot(0).controller.identity, initial_a.controller.identity) &&
bridge_snapshot(0).slot != initial_a.slot &&
bridge_snapshot(1).slot == initial_b.slot &&
bridge_snapshot(1).controller.connection_generation == initial_b.controller.connection_generation,
"stable reservations must restore Pair A across physical and logical slot changes without moving Pair B");
++b.report_sequence;
second.controller.gamepad.buttons = BUTTON_Y;
report_gamepad(second);
Bluepad32CaptureSnapshot capture{};
require(bluepad32_input_backend_capture_page(0, 0, &capture) &&
capture.state == CaptureState::kRecording && capture.total_events == 2,
"Pair B capture must keep recording real changes across Pair A's disconnect and rebind");
process_rumble_timer(&g_rumble_timer);
require(extra.rumble_calls == 0 && reconnected.rumble_calls == 0 &&
second.last_high == 0 && second.last_low == 160 &&
bluepad32_input_backend_native_sample_result(3, live_b) == 1,
"pending Pair B work must reach only its original physical source after Pair A reconnects");
const auto restored = bridge_snapshot(0);
const auto remapped = controller_profile_runtime_transform(
restored.slot, restored.controller, now_ms, AdapterUsbMode::kXInput);
require(remapped.state.button_north && !remapped.state.button_south &&
runtime_profile_storage.find(initial_b.controller.identity)->active_profile == 5,
"reconnecting at another logical slot must preserve A's saved mapping and B's active profile");
}
void two_pair_cues() {
start_pairing_backend();
auto first = dualsense(0);
auto second = dualsense(1);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "both cue sources must connect");
report_dualsense(first);
report_dualsense(second);
const auto before_b = bridge_snapshot(1);
uint64_t ar, al, br, bl;
require(bluepad32_input_backend_native_sample_request(0, 6, &ar) &&
bluepad32_input_backend_native_sample_request(1, 7, &al) &&
bluepad32_input_backend_native_sample_request(2, 3, &br) &&
bluepad32_input_backend_native_sample_request(3, 1, &bl),
"all four virtual sides must accept independent cues");
process_rumble_timer(&g_rumble_timer);
require(first.last_high == 96 && first.last_low == 220 && first.last_rumble_duration_ms == 60 &&
second.last_high == 96 && second.last_low == 160 && second.last_rumble_duration_ms == 25 &&
bluepad32_input_backend_native_sample_result(2, ar) == -1 &&
bluepad32_input_backend_native_sample_result(0, br) == -1,
"R/L contributions and completion tokens must be scoped to their physical pair");
now_ms = 25;
process_rumble_timer(&g_rumble_timer);
require(second.last_high == 0 && second.last_low == 160 && second.last_rumble_duration_ms == 975 &&
first.last_high == 96 && first.last_low == 220,
"Pair B's pulse boundary must not replace Pair A's independently timed motors");
const uint8_t absent[6] = {0xee, 0, 0, 0, 0, 1};
bluepad32_input_backend_select_native_source(0, absent);
process_rumble_timer(&g_rumble_timer);
require(first.last_rumble_duration_ms == 0 && second.last_low == 160 &&
bluepad32_input_backend_native_sample_result(0, ar) == -1 &&
bluepad32_input_backend_native_sample_result(1, al) == -1 &&
bluepad32_input_backend_native_sample_result(2, br) == 1 &&
bluepad32_input_backend_native_sample_result(3, bl) == 1 &&
bridge_snapshot(1).controller.connection_generation == before_b.controller.connection_generation,
"disabling Pair A must stop only A and preserve B's accepted cues and input epoch");
bluepad32_input_backend_native_sample_cancel(2);
now_ms = 40;
process_rumble_timer(&g_rumble_timer);
bluepad32_input_backend_select_native_source(0, nullptr);
require(bluepad32_input_backend_native_sample_request(0, 1, &ar) &&
bluepad32_input_backend_native_sample_request(2, 6, &br),
"retired sides can accept fresh boot-unique work");
during_dualsense_dispatch = [] {
during_dualsense_dispatch = nullptr;
bluepad32_input_backend_select_native_source(0, nullptr);
};
process_rumble_timer(&g_rumble_timer);
require(bluepad32_input_backend_native_sample_result(0, ar) == -1 &&
bluepad32_input_backend_native_sample_result(2, br) == 1 &&
second.last_high == 96 && second.last_low == 160 && second.last_rumble_duration_ms == 60,
"reselection during A's driver call must reject stale A completion without retiring B's next dispatch");
process_rumble_timer(&g_rumble_timer);
require(first.last_rumble_duration_ms == 0 && second.last_high == 96,
"a raced A submission must be stopped without canceling B's physical timer");
platform_on_device_disconnected(&first);
now_ms = 100;
process_rumble_timer(&g_rumble_timer);
require(second.last_high == 0 && second.last_low == 160 && second.last_rumble_duration_ms == 900,
"B's remaining left pulse must retain its original deadline after A disconnects");
}
void explicit_precedence() {
start_pairing_backend();
auto first = dualsense(0);
auto second = dualsense(1);
bluepad32_input_backend_select_native_source(1, second.conn.btaddr);
require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "explicit Pair B may arrive first");
report_dualsense(second);
require(!bridge_snapshot(0).controller.active && bridge_snapshot(1).controller.active,
"automatic Pair A cannot borrow an explicitly reserved source");
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS, "independent automatic source must connect");
report_dualsense(first);
const auto initial_a = bridge_snapshot(0);
auto duplicate = dualsense(2);
memcpy(duplicate.conn.btaddr, second.conn.btaddr, sizeof(second.conn.btaddr));
duplicate.product_id = second.product_id;
require(platform_on_device_ready(&duplicate) == UNI_ERROR_SUCCESS, "ambiguous-address fixture must connect");
report_dualsense(duplicate);
require(!bridge_snapshot(1).controller.active &&
bridge_snapshot(0).controller.connection_generation == initial_a.controller.connection_generation,
"an ambiguous explicit address must fail only its affected pair closed");
platform_on_device_disconnected(&duplicate);
report_dualsense(second);
require(bridge_snapshot(1).controller.active, "the unique explicit match must resume after ambiguity clears");
bluepad32_input_backend_select_native_source(0, second.conn.btaddr);
report_dualsense(second);
require(!bridge_snapshot(0).controller.active && !bridge_snapshot(1).controller.active,
"two explicit selectors matching one logical pad must never broadcast it");
bluepad32_input_backend_select_native_source(0, nullptr);
report_dualsense(first);
report_dualsense(second);
require(controller_identity_equal(bridge_snapshot(0).controller.identity, identity_for_device(&first)) &&
controller_identity_equal(bridge_snapshot(1).controller.identity, identity_for_device(&second)),
"releasing an explicit conflict restores separate automatic and explicit sources");
}
void paired_explicit_conflict() {
start_pairing_backend();
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
bluepad32_input_backend_select_native_source(0, left.conn.btaddr);
bluepad32_input_backend_select_native_source(1, right.conn.btaddr);
ready_switch2(left);
uint64_t old;
require(bluepad32_input_backend_native_sample_request(0, 1, &old), "solo explicit source cue must queue");
ready_switch2(right);
motion_fixture(left);
motion_fixture(right);
report_gamepad(right);
uint64_t rejected;
require(!bridge_snapshot(0).controller.active && !bridge_snapshot(1).controller.active &&
bluepad32_input_backend_native_sample_result(0, old) == -1 &&
!bluepad32_input_backend_native_sample_request(2, 1, &rejected),
"paired physical halves matched by different explicit selectors must retire old work and fail both closed");
bluepad32_input_backend_select_native_source(1, nullptr);
++sensors[right.idx].metadata.report_sequence;
report_gamepad(right);
require(bridge_snapshot(0).controller.active && !bridge_snapshot(1).controller.active,
"an explicit logical pair reserves both halves against automatic assignment");
auto independent = dualsense(2);
require(platform_on_device_ready(&independent) == UNI_ERROR_SUCCESS, "independent second source must connect");
report_dualsense(independent);
const auto before_b = bridge_snapshot(1);
uint64_t rc, lc, bc;
require(bluepad32_input_backend_native_sample_request(0, 6, &rc) &&
bluepad32_input_backend_native_sample_request(1, 1, &lc) &&
bluepad32_input_backend_native_sample_request(3, 7, &bc),
"paired real halves and independent pad must accept separate feedback");
process_rumble_timer(&g_rumble_timer);
require(right.last_high == 96 && left.last_low == 160 && independent.last_low == 220,
"feedback must respect both logical pair and paired physical side");
set_runtime_joycon_mode(JoyConMode::kIndividual);
++sensors[left.idx].metadata.report_sequence;
++sensors[right.idx].metadata.report_sequence;
report_gamepad(left);
report_gamepad(right);
require(bridge_snapshot(0).controller.active &&
controller_identity_equal(bridge_snapshot(0).controller.identity, identity_for_device(&left)) &&
bridge_snapshot(1).controller.connection_generation == before_b.controller.connection_generation &&
bluepad32_input_backend_native_sample_result(0, rc) == -1 &&
bluepad32_input_backend_native_sample_result(1, lc) == -1 &&
bluepad32_input_backend_native_sample_result(3, bc) == 1,
"splitting a physical pair retires only its old cues and cannot duplicate its unselected member into Pair B");
}
void topology_reservations() {
start_pairing_backend();
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
ready_switch2(left);
ready_switch2(right);
motion_fixture(left);
motion_fixture(right);
report_gamepad(right);
auto independent = dualsense(2);
require(platform_on_device_ready(&independent) == UNI_ERROR_SUCCESS, "independent automatic source must connect");
report_dualsense(independent);
const auto before_b = bridge_snapshot(1);
const auto pair_identity = bridge_snapshot(0).controller.identity;
set_runtime_joycon_mode(JoyConMode::kIndividual);
++sensors[left.idx].metadata.report_sequence;
++sensors[right.idx].metadata.report_sequence;
report_gamepad(left);
report_gamepad(right);
require(!bridge_snapshot(0).controller.active &&
bridge_snapshot(1).controller.connection_generation == before_b.controller.connection_generation,
"a split remembered pair is ambiguous without moving the independent pair");
set_runtime_joycon_mode(JoyConMode::kPaired);
++sensors[right.idx].metadata.report_sequence;
report_gamepad(right);
require(bridge_snapshot(0).controller.active &&
controller_identity_equal(bridge_snapshot(0).controller.identity, pair_identity) &&
bridge_snapshot(1).controller.connection_generation == before_b.controller.connection_generation,
"remerging the same remembered members must restore only their reserved pair");
platform_on_device_disconnected(&independent);
set_runtime_joycon_mode(JoyConMode::kIndividual);
bluepad32_input_backend_select_native_source(0, left.conn.btaddr);
bluepad32_input_backend_select_native_source(1, right.conn.btaddr);
bluepad32_input_backend_select_native_source(0, nullptr);
bluepad32_input_backend_select_native_source(1, nullptr);
++sensors[left.idx].metadata.report_sequence;
++sensors[right.idx].metadata.report_sequence;
report_gamepad(left);
report_gamepad(right);
require(bridge_snapshot(0).controller.active && bridge_snapshot(1).controller.active,
"individually reserved physical halves must first own separate logical streams");
set_runtime_joycon_mode(JoyConMode::kPaired);
++sensors[right.idx].metadata.report_sequence;
report_gamepad(right);
require(!bridge_snapshot(0).controller.active && !bridge_snapshot(1).controller.active,
"merging two independently reserved sources must fail both closed rather than duplicate the merged pair");
set_runtime_joycon_mode(JoyConMode::kIndividual);
++sensors[left.idx].metadata.report_sequence;
++sensors[right.idx].metadata.report_sequence;
report_gamepad(left);
report_gamepad(right);
require(bridge_snapshot(0).controller.active && bridge_snapshot(1).controller.active &&
bridge_snapshot(0).slot != bridge_snapshot(1).slot,
"splitting conflicting members restores their previous independent reservations");
}
void stable_ble_reservation() {
start_pairing_backend();
auto first = device(0, true, UNI_BT_CONN_PROTOCOL_BLE);
auto independent = dualsense(1);
bluepad32_input_backend_select_native_source(1, independent.conn.btaddr);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&independent) == UNI_ERROR_SUCCESS,
"an unresolved BLE gamepad may connect beside an explicit stable source");
report_gamepad(first);
report_dualsense(independent);
const auto initial_b = bridge_snapshot(1);
require(!bridge_snapshot(0).controller.active && initial_b.controller.active,
"automatic reservations must not promote an unresolved BLE connection address to a stable identity");
const bd_addr_t identity = {0xc2, 0x10, 0x20, 0x30, 0x40, 0x50};
dispatch_identity_event(SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED, first,
BD_ADDR_TYPE_LE_RANDOM, identity);
first.controller.gamepad.buttons = BUTTON_A;
report_gamepad(first);
const auto initial_a = bridge_snapshot(0);
uint64_t old_a, live_b;
require(initial_a.controller.active && initial_a.controller.state.button_south &&
bluepad32_input_backend_native_sample_request(0, 1, &old_a) &&
bluepad32_input_backend_native_sample_request(3, 1, &live_b),
"resolved identity publication must activate its own stream and feedback without waiting for another connection");
dispatch_identity_event(SM_EVENT_IDENTITY_RESOLVING_STARTED, first,
BD_ADDR_TYPE_LE_RANDOM, identity);
require(!bridge_snapshot(0).controller.active &&
bridge_snapshot(1).controller.connection_generation == initial_b.controller.connection_generation &&
bluepad32_input_backend_native_sample_result(0, old_a) == -1 &&
bluepad32_input_backend_native_sample_result(3, live_b) == 0,
"identity loss must retire only the uncertain source's input and pending work");
platform_on_device_disconnected(&first);
auto reconnect = device(2, true, UNI_BT_CONN_PROTOCOL_BLE);
reconnect.vendor_id = first.vendor_id;
reconnect.product_id = first.product_id;
platform_on_device_connected(&reconnect);
require(platform_on_device_ready(&reconnect) == UNI_ERROR_SUCCESS, "BLE controller must reconnect at a different transport index");
report_gamepad(reconnect);
require(!bridge_snapshot(0).controller.active, "a fresh unresolved BLE address must not steal the remembered stable source");
dispatch_identity_event(SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED, reconnect,
BD_ADDR_TYPE_LE_RANDOM, identity);
reconnect.controller.gamepad.buttons = BUTTON_B;
report_gamepad(reconnect);
require(bridge_snapshot(0).controller.active && bridge_snapshot(0).controller.state.button_east &&
!bridge_snapshot(0).controller.state.button_south &&
controller_identity_equal(bridge_snapshot(0).controller.identity, initial_a.controller.identity) &&
bridge_snapshot(1).controller.connection_generation == initial_b.controller.connection_generation,
"resolving a new BLE connection address must recover the original pair reservation without reviving cached controls");
}
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
uni_hid_device_t hd_dualsense(int index) {
auto pad = dualsense(index);
pad.conn.connected = true;
pad.conn.interrupt_cid = static_cast<uint16_t>(0x80 + 2 * index);
return pad;
}
NativeHapticsActuatorFrame native_waveform(uint8_t count = 3) {
return {count, {{387, 484, 870, 321}, {411, 509, 439, 731}, {433, 538, 680, 511}}};
}
NativeHapticsActuatorFrame scale_native_fixture(
NativeHapticsActuatorFrame frame, uint8_t low, uint8_t high) {
for (uint8_t sample = 0; sample < frame.sample_count; ++sample) {
frame.samples[sample].low_amplitude = static_cast<uint16_t>(
(uint32_t{frame.samples[sample].low_amplitude} * low + 127u) / 255u);
frame.samples[sample].high_amplitude = static_cast<uint16_t>(
(uint32_t{frame.samples[sample].high_amplitude} * high + 127u) / 255u);
}
return frame;
}
void compare_native_packets(SwitchHdRumbleSynth& reference, size_t& cursor, uint16_t cid) {
while (cursor < native_packets.size()) {
const auto& packet = native_packets[cursor++];
require(packet.cid == cid, "PCM must target the selected physical interrupt CID");
if (packet.data[3] != 0x91) continue; // Audio setup has no PCM interval.
require(packet.data[12] == 0x92 && packet.data[13] == 64,
"native HD must retain the guarded 32-frame packet format");
uint8_t expected[64]{};
reference.render(uint64_t{packet.data[11]} * 32, 32, expected);
require(memcmp(expected, packet.data.data() + 14, sizeof(expected)) == 0,
"PCM must preserve native timing, both full-precision bands, profile gains and side identity");
}
}
void hd_second_pair(bool first_is_wii) {
start_pairing_backend();
initialize_runtime_profile_storage();
auto first = first_is_wii ? wii_device(0) : device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
auto selected = hd_dualsense(2);
selected.outgoing_buffer.queued = 1;
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS,
"mixed controllers must bind A and B while HD selects physical slot one");
const uint32_t generation = g_slots[1].connection_generation;
require(haptics_experiment_native_selected(1, generation),
"auto-arm pending ownership must include a nonzero physical slot");
const auto identity = identity_for_device(&selected);
auto profile = controller_profile_default(identity, 0);
profile.strong_rumble_scale = 64;
profile.weak_rumble_scale = 128;
require(runtime_profile_storage.set(identity, 0, profile) == ProfileStorageResult::kOk,
"HD profile gains must be persisted before host ingress");
auto right = native_waveform();
const auto left = native_waveform(2);
const auto scaled_right = scale_native_fixture(right, 64, 128);
now_ms = 100;
now_sub_ms_us = 137;
const uint64_t right_us = time_us_64();
during_profile_resolution = [] {
during_profile_resolution = nullptr;
now_ms += 7;
};
require(bluepad32_input_backend_native_rumble_submit(2, &right),
"pending selected HD must admit a full native frame before stream startup");
right.samples[0] = {}; // Submission must own its copy.
const uint64_t left_us = time_us_64();
require(bluepad32_input_backend_native_rumble_submit(3, &left),
"B left must independently update physical actuator zero");
const uint8_t conventional = 73;
require(submit_native_magnitudes(0, &conventional, 1),
"non-HD first pair must retain conventional output");
process_rumble_timer(&g_rumble_timer);
const int compatibility_calls = selected.rumble_calls;
require(first.last_high == conventional && selected.last_high == 0 && selected.last_low == 0,
"only unsupported first-pair gameplay may use compatibility reports");
selected.outgoing_buffer.queued = 0;
advance_native_backend(25);
HapticsExperimentDiagnostics status{};
haptics_experiment_snapshot(&status);
require(status.state == HapticsExperimentState::kRunning && status.slot == 1 &&
status.host_updates == 2 && selected.rumble_calls == compatibility_calls,
"pending native frames must reach the real selected PCM queue without double writes");
SwitchHdRumbleSynth reference;
reference.reset(status.start_us);
NativeHapticsFrame stereo{};
stereo.actuators[1] = scaled_right;
require(reference.push_native(stereo, right_us), "reference right timeline must accept original receipt");
stereo = {};
stereo.actuators[0] = scale_native_fixture(left, 64, 128);
require(reference.push_native(stereo, left_us), "reference left timeline must accept independent receipt");
size_t cursor = 0;
compare_native_packets(reference, cursor, selected.conn.interrupt_cid);
bluepad32_input_backend_native_rumble_cancel(0);
advance_native_backend(5);
compare_native_packets(reference, cursor, selected.conn.interrupt_cid);
bluepad32_input_backend_native_rumble_cancel(2);
reference.cancel_native(2);
advance_native_backend(10);
compare_native_packets(reference, cursor, selected.conn.interrupt_cid);
advance_native_backend(40);
compare_native_packets(reference, cursor, selected.conn.interrupt_cid);
require_native_channels(false, false);
require(selected.rumble_calls == compatibility_calls,
"watchdog and side cancellation must remain PCM-only");
auto invalid = native_waveform(1);
invalid.samples[0].low_frequency_code = 671;
require(!bluepad32_input_backend_native_rumble_submit(2, &invalid),
"malformed active native carriers cannot mutate the selected stream");
require(haptics_experiment_request(0, 1), "selected HD stream must accept stop");
require(!bluepad32_input_backend_native_rumble_submit(2, &left),
"selected stopping HD must reject rather than queue compatibility fallback");
process_rumble_timer(&g_rumble_timer);
require(haptics_experiment_native_selected(1, generation) &&
!bluepad32_input_backend_native_rumble_submit(3, &left) &&
selected.last_high == 0 && selected.last_low == 0,
"drain/restoration must reject host fallback while allowing the compatibility zero stop");
advance_native_backend(30);
require(selected.last_high == 0 && selected.last_low == 0,
"compatibility restoration cannot replay rejected native host work");
}
void hd_unselected_dualsense() {
start_pairing_backend();
auto selected = hd_dualsense(0);
auto unselected = hd_dualsense(1);
require(platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&unselected) == UNI_ERROR_SUCCESS,
"two DualSenses must retain the existing single selected HD policy");
advance_native_backend(30);
const uint8_t magnitude = 93;
require(submit_native_magnitudes(2, &magnitude, 1),
"unselected DualSense must accept conventional native gameplay");
process_rumble_timer(&g_rumble_timer);
require(unselected.rumble_calls == 1 && unselected.last_high == magnitude &&
last_native_cid == selected.conn.interrupt_cid,
"later DualSense must not steal HD or lose bounded compatibility");
require_native_channels(false, false);
}
void hd_cues() {
start_pairing_backend();
auto first = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
auto selected = hd_dualsense(1);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS,
"HD cue source must occupy the second pair");
advance_native_backend(30);
const int compatibility_calls = selected.rumble_calls;
uint64_t cue;
require(bluepad32_input_backend_native_sample_request(2, 1, &cue),
"right built-in cue must enter its bounded scheduler");
process_rumble_timer(&g_rumble_timer);
require(bluepad32_input_backend_native_sample_result(2, cue) == 0,
"overlay admission must not masquerade as source-driver completion");
advance_native_backend(20);
require_native_channels(false, true);
require(bluepad32_input_backend_native_sample_result(2, cue) == 1,
"successful later PCM packet submission must complete the built-in cue");
const auto left = native_waveform(1);
require(bluepad32_input_backend_native_rumble_submit(3, &left),
"left host waveform must coexist with a right local cue");
advance_native_backend(15);
require_native_channels(true, true);
bluepad32_input_backend_native_sample_cancel(2);
process_rumble_timer(&g_rumble_timer);
advance_native_backend(25); // Drain the 32-frame lookback and the next send interval.
require_native_channels(true, false);
const uint32_t generation = g_slots[1].connection_generation;
bluepad32_input_backend_queue_profile_feedback(
1, generation, 1, ControllerProfileConfirmationPolicy::kRumble);
advance_native_backend(20);
require_native_channels(true, true);
bluepad32_input_backend_native_rumble_cancel(2);
bluepad32_input_backend_native_rumble_cancel(3);
advance_native_backend(10);
require_native_channels(true, true);
advance_native_backend(250); // Finish both 75 ms confirmation pulses and their gap.
require_native_channels(false, false);
require(bluepad32_input_backend_native_sample_request(2, 3, &cue),
"short cue must enter the same real PCM scheduler");
process_rumble_timer(&g_rumble_timer);
now_ms += 100; // No transport callback covers the first 25 ms cue pulse.
advance_native_backend(12);
require(bluepad32_input_backend_native_sample_result(2, cue) == -1,
"a successful late packet beyond the admitted pulse must not falsely acknowledge that cue");
require(selected.rumble_calls == compatibility_calls,
"local cues, profile feedback and USB side cancellation must not emit compatibility writes");
}
void hd_reselection() {
start_pairing_backend();
auto first = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
auto selected = hd_dualsense(1);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS,
"HD epoch source must occupy the second pair");
advance_native_backend(30);
auto wave = native_waveform(1);
require(bluepad32_input_backend_native_rumble_submit(2, &wave) &&
bluepad32_input_backend_native_rumble_submit(3, &wave),
"both selected actuator timelines must start");
advance_native_backend(15);
HapticsExperimentDiagnostics original{};
haptics_experiment_snapshot(&original);
bluepad32_input_backend_select_native_source(0, first.conn.btaddr);
advance_native_backend(10);
require_native_channels(true, true);
require(haptics_experiment_native_selected(1, original.connection_generation),
"first-pair source epochs must not invalidate selected second-pair HD");
bluepad32_input_backend_select_native_source(1, selected.conn.btaddr);
require(!bluepad32_input_backend_native_rumble_submit(2, &wave),
"Core0 epoch migration must reject until BT attachment sync, not fall back");
advance_native_backend(20);
HapticsExperimentDiagnostics current{};
haptics_experiment_snapshot(&current);
require(current.run_id == original.run_id &&
current.connection_generation != original.connection_generation &&
haptics_experiment_native_selected(1, current.connection_generation),
"logical reselection must synchronize generation without losing selected HD");
require_native_channels(false, false);
NativeHapticsFrame stereo{};
stereo.actuators[1] = wave;
require(!haptics_experiment_submit_native(1, original.connection_generation, time_us_64(), stereo) &&
bluepad32_input_backend_native_rumble_submit(2, &wave),
"old attachment generation must reject while fresh host work resumes");
platform_on_device_disconnected(&selected);
auto replacement = hd_dualsense(2);
memcpy(replacement.conn.btaddr, selected.conn.btaddr, 6);
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS,
"replacement selected identity must reconnect at another physical index");
advance_native_backend(30);
require_native_channels(false, false);
require(!haptics_experiment_submit_native(1, current.connection_generation, time_us_64(), stereo),
"replacement must never inherit old-generation PCM work");
}
void hd_delayed_cue() {
start_pairing_backend();
auto first = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
auto selected = hd_dualsense(1);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS,
"delayed cue must use the selected second-pair PCM stream");
advance_native_backend(30);
now_ms = 43;
uint64_t token;
require(bluepad32_input_backend_native_sample_request(2, 3, &token),
"short native cue must be admitted");
NativeGamepadCueDispatch command{};
state_lock_enter();
const bool prepared = prepare_native_cues(1, now_ms, false, false, &command);
state_lock_exit();
require(prepared, "short cue must prepare for dispatch");
now_ms += 4;
dispatch_native_cues(command);
// The 25 ms pulse prepared at 43 ms actually runs [47,68), not [47,72).
// Skip transport until the successful PCM block covers [69,79.666) ms.
now_ms = 80;
advance_native_backend(1);
process_rumble_timer(&g_rumble_timer);
require_native_channels(false, false);
require(bluepad32_input_backend_native_sample_result(2, token) == -1,
"a packet after the shortened pulse must not acknowledge an undelivered cue");
}
void hd_admission_cancel() {
start_pairing_backend();
auto first = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
auto selected = hd_dualsense(1);
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS,
"cancellation race must use the selected second-pair PCM stream");
advance_native_backend(30);
const auto wave = native_waveform(1);
require(bluepad32_input_backend_native_rumble_submit(3, &wave),
"unrelated left waveform must start before the race");
advance_native_backend(15);
after_backend_state_unlock = [] {
static unsigned unlocks = 0;
if (++unlocks == 2) {
// Retire the right side immediately after the final backend check.
after_backend_state_unlock = nullptr;
bluepad32_input_backend_native_rumble_cancel(2);
}
};
require(bluepad32_input_backend_native_rumble_submit(2, &wave),
"the waveform can be accepted before its concurrent cancellation");
advance_native_backend(25);
require_native_channels(true, false);
bluepad32_input_backend_native_rumble_cancel(3);
bluepad32_input_backend_native_rumble_cancel(2);
require(bluepad32_input_backend_native_rumble_submit(2, &wave),
"a fresh waveform after cancellation returns must remain admissible");
advance_native_backend(25);
require_native_channels(false, true);
}
#endif
} // namespace
int main(int argc, char** argv) {
require(argc == 2, "scenario required");
const std::string scenario = argv[1];
if (scenario == "usb-wake") { test_usb_switch2_wake(); return 0; }
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
if (scenario == "hd-second-pair-wii") { hd_second_pair(true); return 0; }
if (scenario == "hd-second-pair-other") { hd_second_pair(false); return 0; }
if (scenario == "hd-unselected-dualsense") { hd_unselected_dualsense(); return 0; }
if (scenario == "hd-cues") { hd_cues(); return 0; }
if (scenario == "hd-reselection") { hd_reselection(); return 0; }
if (scenario == "hd-delayed-cue") { hd_delayed_cue(); return 0; }
if (scenario == "hd-admission-cancel") { hd_admission_cancel(); return 0; }
#endif
if (scenario == "source-isolation") source_isolation();
else if (scenario == "cue-lifetime") cue_lifetime();
else if (scenario == "cue-races") cue_races();
else if (scenario == "gameplay-timeline") gameplay_timeline();
else if (scenario == "gameplay-availability") gameplay_availability();
else if (scenario == "gameplay-priority") gameplay_priority();
else if (scenario == "gameplay-source-epochs") gameplay_source_epochs();
else if (scenario == "gameplay-profile-gain") gameplay_profile_gain();
else if (scenario == "gameplay-two-pairs") gameplay_two_pairs();
else if (scenario == "gameplay-paired-revision") gameplay_paired_revision();
else if (scenario == "gameplay-wii") gameplay_wii();
else if (scenario == "stable-logical-slot") stable_logical_slot();
else if (scenario == "sensorless-admission") sensorless_admission();
else if (scenario == "independent-motion") independent_motion();
else if (scenario == "paired-source") paired_source();
else if (scenario == "pair-cue-races") pair_cue_races();
else if (scenario == "mono-rumble") mono_rumble();
else if (scenario == "two-pair-sources") two_pair_sources();
else if (scenario == "two-pair-cues") two_pair_cues();
else if (scenario == "explicit-precedence") explicit_precedence();
else if (scenario == "paired-explicit-conflict") paired_explicit_conflict();
else if (scenario == "topology-reservations") topology_reservations();
else if (scenario == "stable-ble-reservation") stable_ble_reservation();
else require(false, "unknown native gamepad scenario");
return 0;
}