Add persistent profile switching feedback
This commit is contained in:
parent
114266836d
commit
045edc984e
12 changed files with 1573 additions and 198 deletions
|
|
@ -1039,7 +1039,8 @@ void test_independent_lifecycle() {
|
|||
"unstable replacement must not be enrolled for profiles");
|
||||
|
||||
g_slots[3].pending_rumble = {
|
||||
3, disconnected_generation, ControllerRumbleOutput{77, 88}};
|
||||
3, disconnected_generation, ControllerRumbleOutput{77, 88},
|
||||
kXInputHostRumbleDurationMs};
|
||||
g_slots[3].rumble_pending = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_three_replacement.rumble_calls == 0,
|
||||
|
|
@ -1273,102 +1274,437 @@ void test_slot_lighting() {
|
|||
"controller without RGB support did not receive its slot LED");
|
||||
}
|
||||
|
||||
void require_south_button_mapping(const ControllerState& state,
|
||||
bool swapped,
|
||||
const char* message) {
|
||||
require(state.button_east == swapped && state.button_south == !swapped &&
|
||||
!state.button_north && !state.button_west,
|
||||
message);
|
||||
}
|
||||
|
||||
void test_abxy_hotkey() {
|
||||
void test_profile_chord_remains_raw() {
|
||||
start_pairing_backend();
|
||||
uni_hid_device_t slot_zero = device(0);
|
||||
uni_hid_device_t slot_one = device(1);
|
||||
require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS &&
|
||||
platform_on_device_ready(&slot_one) == UNI_ERROR_SUCCESS,
|
||||
"ABXY test controllers did not become ready");
|
||||
uni_hid_device_t controller = device(0);
|
||||
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
|
||||
"raw profile chord controller did not become ready");
|
||||
|
||||
uni_controller_t input{};
|
||||
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
input.gamepad.buttons = BUTTON_A;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
ControllerState snapshot{};
|
||||
require(read_controller_state(0, &snapshot),
|
||||
"slot 0 ABXY state was not published");
|
||||
require_south_button_mapping(
|
||||
snapshot, kDefaultSwapAbxy,
|
||||
"slot 0 did not start in the configured ABXY layout");
|
||||
|
||||
input.gamepad.buttons =
|
||||
kAbxyHotkeyButtonMask | BUTTON_A;
|
||||
input.gamepad.misc_buttons = kAbxyHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
require(read_controller_state(0, &snapshot),
|
||||
"toggled slot 0 state was not published");
|
||||
require_south_button_mapping(
|
||||
snapshot, !kDefaultSwapAbxy,
|
||||
"hotkey did not toggle slot 0 ABXY mapping");
|
||||
require(!snapshot.button_left_shoulder && !snapshot.button_right_shoulder &&
|
||||
!snapshot.button_select && !snapshot.button_start,
|
||||
"hotkey chord leaked into the Switch report");
|
||||
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{0x11, 0x22});
|
||||
BUTTON_A | BUTTON_SHOULDER_L | BUTTON_SHOULDER_R;
|
||||
input.gamepad.misc_buttons =
|
||||
MISC_BUTTON_SELECT | MISC_BUTTON_START;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
ControllerState snapshot{};
|
||||
require(read_controller_state(0, &snapshot) &&
|
||||
snapshot.button_south && !snapshot.button_east &&
|
||||
snapshot.button_left_shoulder &&
|
||||
snapshot.button_right_shoulder &&
|
||||
snapshot.button_select && snapshot.button_start,
|
||||
"Core 1 suppressed the profile chord or mutated ABXY mapping");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_zero.rumble_calls == 1 &&
|
||||
slot_zero.last_high == kAbxyFeedbackWeakMagnitude &&
|
||||
slot_zero.last_low == kAbxyFeedbackStrongMagnitude &&
|
||||
slot_zero.last_high == UINT8_MAX &&
|
||||
slot_zero.last_low == UINT8_MAX &&
|
||||
g_slots[0].rumble_pending,
|
||||
"ABXY confirmation was not full-strength or did not take priority");
|
||||
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(g_slots[0].swap_abxy == !kDefaultSwapAbxy &&
|
||||
slot_zero.rumble_calls == 1,
|
||||
"held hotkey toggled or rumbled more than once");
|
||||
|
||||
input.gamepad = {};
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
input.gamepad.buttons = kAbxyHotkeyButtonMask | BUTTON_A;
|
||||
input.gamepad.misc_buttons = kAbxyHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(g_slots[0].swap_abxy == kDefaultSwapAbxy &&
|
||||
slot_zero.rumble_calls == 2,
|
||||
"released hotkey did not re-arm for a second toggle");
|
||||
|
||||
now_ms = kAbxyFeedbackDurationMs - 1;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_zero.rumble_calls == 2 && g_slots[0].rumble_pending,
|
||||
"host rumble interrupted ABXY confirmation");
|
||||
now_ms = kAbxyFeedbackDurationMs;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_zero.rumble_calls == 3 &&
|
||||
slot_zero.last_high == 0x22 &&
|
||||
slot_zero.last_low == 0x11 &&
|
||||
!g_slots[0].rumble_pending,
|
||||
"deferred host rumble did not resume after confirmation");
|
||||
|
||||
uni_controller_t peer_input{};
|
||||
peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
peer_input.gamepad.buttons = BUTTON_A;
|
||||
platform_on_controller_data(&slot_one, &peer_input);
|
||||
require(read_controller_state(1, &snapshot),
|
||||
"slot 1 ABXY state was not published");
|
||||
require_south_button_mapping(
|
||||
snapshot, kDefaultSwapAbxy,
|
||||
"slot 0 hotkey changed slot 1 layout");
|
||||
|
||||
platform_on_device_disconnected(&slot_zero);
|
||||
uni_hid_device_t replacement = device(0);
|
||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
||||
g_slots[0].swap_abxy == kDefaultSwapAbxy &&
|
||||
!g_slots[0].abxy_hotkey_latched &&
|
||||
require(controller.rumble_calls == 0 &&
|
||||
!g_slots[0].feedback_pending,
|
||||
"disconnect did not reset slot 0 hotkey state");
|
||||
"legacy ABXY chord still produced local feedback");
|
||||
}
|
||||
|
||||
void test_profile_feedback_scheduler() {
|
||||
start_pairing_backend();
|
||||
uni_hid_device_t devices[kSlotCount] = {
|
||||
device(0), device(1), device(2), device(3)};
|
||||
devices[0].report_parser.set_lightbar_color = set_lightbar;
|
||||
devices[1].report_parser.set_player_leds = set_player_leds;
|
||||
devices[2].report_parser.set_lightbar_color = set_lightbar;
|
||||
devices[3].report_parser.set_player_leds = set_player_leds;
|
||||
|
||||
uint32_t generations[kSlotCount]{};
|
||||
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(platform_on_device_ready(&devices[slot]) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"profile feedback controller did not become ready");
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(slot, &snapshot);
|
||||
generations[slot] = snapshot.connection_generation;
|
||||
require(devices[slot].rumble_calls == 0 &&
|
||||
!g_slots[slot].profile_feedback.active &&
|
||||
!g_slots[slot].profile_feedback_pending,
|
||||
"initial controller/profile load scheduled confirmation feedback");
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
slot, generations[slot], static_cast<uint8_t>(slot + 1u),
|
||||
ControllerProfileConfirmationPolicy::kRumble);
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
slot, ControllerRumbleOutput{
|
||||
static_cast<uint8_t>(0x10u + slot),
|
||||
static_cast<uint8_t>(0x20u + slot)});
|
||||
}
|
||||
|
||||
now_ms = 0;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(devices[slot].rumble_calls == 1 &&
|
||||
devices[slot].last_rumble_duration_ms ==
|
||||
kProfileFeedbackPhaseDurationMs &&
|
||||
devices[slot].last_high == UINT8_MAX &&
|
||||
devices[slot].last_low == UINT8_MAX &&
|
||||
g_slots[slot].rumble_pending &&
|
||||
g_slots[slot].profile_feedback.active,
|
||||
"profile pulse sequence did not start at full strength");
|
||||
}
|
||||
|
||||
now_ms = 74;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 75;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
for (const uni_hid_device_t& controller : devices) {
|
||||
require(controller.rumble_calls == 1,
|
||||
"profile pulse did not retain a 75 ms on phase");
|
||||
}
|
||||
|
||||
now_ms = 149;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 150;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[0].rumble_calls == 2 &&
|
||||
devices[0].last_high == 0x20 &&
|
||||
devices[0].last_low == 0x10 &&
|
||||
!g_slots[0].rumble_pending,
|
||||
"one-pulse confirmation did not defer host rumble through its off phase");
|
||||
for (uint8_t slot = 1; slot < kSlotCount; ++slot) {
|
||||
require(devices[slot].rumble_calls == 2 &&
|
||||
devices[slot].last_high == UINT8_MAX &&
|
||||
g_slots[slot].rumble_pending,
|
||||
"second profile pulse did not start after 75 ms off");
|
||||
}
|
||||
|
||||
now_ms = 225;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 300;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[1].rumble_calls == 3 &&
|
||||
devices[1].last_high == 0x21 &&
|
||||
!g_slots[1].rumble_pending &&
|
||||
devices[2].rumble_calls == 3 &&
|
||||
devices[3].rumble_calls == 3,
|
||||
"two/three/four-pulse sequences diverged at 300 ms");
|
||||
|
||||
now_ms = 375;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 450;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[2].rumble_calls == 4 &&
|
||||
devices[2].last_high == 0x22 &&
|
||||
!g_slots[2].rumble_pending &&
|
||||
devices[3].rumble_calls == 4 &&
|
||||
devices[3].last_high == UINT8_MAX,
|
||||
"three/four-pulse sequences diverged at 450 ms");
|
||||
|
||||
now_ms = 525;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 600;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[3].rumble_calls == 5 &&
|
||||
devices[3].last_high == 0x23 &&
|
||||
!g_slots[3].rumble_pending &&
|
||||
devices[0].lightbar_calls == 1 &&
|
||||
devices[1].player_led_calls == 1 &&
|
||||
devices[2].lightbar_calls == 1 &&
|
||||
devices[3].player_led_calls == 1,
|
||||
"four-pulse confirmation did not release host rumble at 600 ms");
|
||||
|
||||
const int slot_zero_lightbar_calls = devices[0].lightbar_calls;
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
0, generations[0], 2,
|
||||
ControllerProfileConfirmationPolicy::kNone);
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{0x31, 0x41});
|
||||
now_ms = 700;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[0].rumble_calls == 3 &&
|
||||
devices[0].last_high == 0x41 &&
|
||||
devices[0].lightbar_calls ==
|
||||
slot_zero_lightbar_calls &&
|
||||
!g_slots[0].profile_feedback.active,
|
||||
"none policy scheduled profile rumble or lighting");
|
||||
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
0, generations[0], 2,
|
||||
ControllerProfileConfirmationPolicy::kLed);
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{0x32, 0x42});
|
||||
now_ms = 800;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[0].rumble_calls == 3 &&
|
||||
devices[0].lightbar_calls ==
|
||||
slot_zero_lightbar_calls + 1 &&
|
||||
devices[0].lightbar_red ==
|
||||
kProfileLightbarPalette[1].red &&
|
||||
devices[0].lightbar_green ==
|
||||
kProfileLightbarPalette[1].green &&
|
||||
devices[0].lightbar_blue ==
|
||||
kProfileLightbarPalette[1].blue &&
|
||||
observed_status_led_on &&
|
||||
g_slots[0].rumble_pending,
|
||||
"LED policy did not set persistent profile color and first onboard blink");
|
||||
now_ms = 875;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!observed_status_led_on &&
|
||||
devices[0].rumble_calls == 3,
|
||||
"onboard profile blink did not enter its 75 ms off phase");
|
||||
now_ms = 950;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(observed_status_led_on,
|
||||
"second onboard profile blink did not start");
|
||||
now_ms = 1025;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!observed_status_led_on &&
|
||||
g_slots[0].rumble_pending,
|
||||
"host rumble interrupted the final onboard off phase");
|
||||
now_ms = 1100;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[0].rumble_calls == 4 &&
|
||||
devices[0].last_high == 0x42 &&
|
||||
!g_slots[0].rumble_pending,
|
||||
"LED-only sequence did not release deferred host rumble");
|
||||
|
||||
const int slot_one_player_led_calls =
|
||||
devices[1].player_led_calls;
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
1, generations[1], 3,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||
now_ms = 1200;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[1].rumble_calls == 4 &&
|
||||
devices[1].last_high == UINT8_MAX &&
|
||||
devices[1].player_led_calls ==
|
||||
slot_one_player_led_calls + 1 &&
|
||||
devices[1].player_leds == 0x07 &&
|
||||
observed_status_led_on,
|
||||
"combined policy did not drive rumble, onboard LED, and player LEDs");
|
||||
for (uint32_t deadline = 1275; deadline <= 1650;
|
||||
deadline += 75) {
|
||||
now_ms = deadline;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
}
|
||||
require(devices[1].rumble_calls ==
|
||||
(host_rumble_duration_ms() ==
|
||||
kXInputHostRumbleDurationMs
|
||||
? 7
|
||||
: 6) &&
|
||||
!g_slots[1].profile_feedback.active,
|
||||
"combined three-pulse sequence did not terminate");
|
||||
|
||||
const int old_rumble_calls = devices[2].rumble_calls;
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
2, generations[2], 4,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||
platform_on_device_disconnected(&devices[2]);
|
||||
uni_hid_device_t replacement = device(2);
|
||||
replacement.report_parser.set_lightbar_color = set_lightbar;
|
||||
require(platform_on_device_ready(&replacement) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"replacement feedback controller did not become ready");
|
||||
now_ms = 1700;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[2].rumble_calls == old_rumble_calls &&
|
||||
replacement.rumble_calls == 0 &&
|
||||
replacement.lightbar_calls == 1 &&
|
||||
!g_slots[2].profile_feedback.active &&
|
||||
!g_slots[2].profile_feedback_pending,
|
||||
"slot replacement accepted stale queued profile feedback");
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
2, generations[2], 4,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||
now_ms = 1705;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(replacement.rumble_calls == 0 &&
|
||||
replacement.lightbar_calls == 1,
|
||||
"stale generation feedback reached a replacement controller");
|
||||
|
||||
const int slot_three_rumble_calls = devices[3].rumble_calls;
|
||||
const int slot_three_player_led_calls =
|
||||
devices[3].player_led_calls;
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
3, generations[3], 1,
|
||||
ControllerProfileConfirmationPolicy::kLed);
|
||||
now_ms = 1800;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(observed_status_led_on &&
|
||||
g_slots[3].profile_feedback.pulses_started == 1 &&
|
||||
devices[3].player_led_calls ==
|
||||
slot_three_player_led_calls + 1 &&
|
||||
devices[3].player_leds == 0x01,
|
||||
"one-blink LED profile indication did not start");
|
||||
now_ms = 1875;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!observed_status_led_on,
|
||||
"one-blink LED profile indication did not turn off");
|
||||
now_ms = 1950;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!g_slots[3].profile_feedback.active &&
|
||||
devices[3].rumble_calls ==
|
||||
slot_three_rumble_calls,
|
||||
"one-blink LED-only profile indication did not terminate cleanly");
|
||||
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
3, generations[3], 4,
|
||||
ControllerProfileConfirmationPolicy::kLed);
|
||||
now_ms = 2000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[3].player_leds == 0x0f,
|
||||
"profile 4 did not persist four player LEDs");
|
||||
for (uint8_t pulse = 1; pulse <= 4; ++pulse) {
|
||||
require(observed_status_led_on &&
|
||||
g_slots[3].profile_feedback.on &&
|
||||
g_slots[3].profile_feedback.pulses_started ==
|
||||
pulse,
|
||||
"four-blink LED sequence missed an on phase");
|
||||
now_ms = static_cast<uint32_t>(
|
||||
2075u + static_cast<uint32_t>(pulse - 1u) * 150u);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!observed_status_led_on &&
|
||||
!g_slots[3].profile_feedback.on,
|
||||
"four-blink LED sequence missed an off phase");
|
||||
if (pulse != 4) {
|
||||
now_ms += 75;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
}
|
||||
}
|
||||
now_ms = 2600;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!g_slots[3].profile_feedback.active &&
|
||||
devices[3].rumble_calls ==
|
||||
slot_three_rumble_calls,
|
||||
"four-blink LED-only profile indication did not terminate");
|
||||
}
|
||||
|
||||
void test_stateful_host_rumble_restore() {
|
||||
start_pairing_backend();
|
||||
uni_hid_device_t devices[kSlotCount] = {
|
||||
device(0), device(1), device(2), device(3)};
|
||||
uint32_t generations[kSlotCount]{};
|
||||
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(platform_on_device_ready(&devices[slot]) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"rumble restore controller did not become ready");
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(slot, &snapshot);
|
||||
generations[slot] = snapshot.connection_generation;
|
||||
}
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
test_adapter_mode = AdapterUsbMode::kXInput;
|
||||
const ControllerRumbleOutput desired[kSlotCount] = {
|
||||
{0x11, 0x21}, {0x12, 0x22}, {0x13, 0x23}, {0x14, 0x24}};
|
||||
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||
bluepad32_input_backend_queue_rumble(slot, desired[slot]);
|
||||
}
|
||||
now_ms = 0;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(devices[slot].rumble_calls == 1 &&
|
||||
devices[slot].last_rumble_duration_ms ==
|
||||
kXInputHostRumbleDurationMs &&
|
||||
g_slots[slot].retained_host_rumble_valid,
|
||||
"initial XInput rumble was not dispatched and retained");
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
slot, generations[slot], static_cast<uint8_t>(slot + 1u),
|
||||
ControllerProfileConfirmationPolicy::kRumble);
|
||||
}
|
||||
now_ms = 10;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
for (now_ms = 85; now_ms <= 610; now_ms += 75) {
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
}
|
||||
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(devices[slot].rumble_calls ==
|
||||
static_cast<int>(slot + 3u) &&
|
||||
devices[slot].last_low ==
|
||||
desired[slot].low_frequency_magnitude &&
|
||||
devices[slot].last_high ==
|
||||
desired[slot].high_frequency_magnitude &&
|
||||
devices[slot].last_rumble_duration_ms ==
|
||||
kXInputHostRumbleDurationMs &&
|
||||
!g_slots[slot].profile_feedback.active,
|
||||
"XInput rumble did not resume after its profile pulse count");
|
||||
}
|
||||
|
||||
const int stop_calls_before = devices[0].rumble_calls;
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{0x51, 0x61});
|
||||
now_ms = 700;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
0, generations[0], 2,
|
||||
ControllerProfileConfirmationPolicy::kRumble);
|
||||
now_ms = 705;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{0, 0});
|
||||
for (now_ms = 780; now_ms <= 1005; now_ms += 75) {
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
}
|
||||
require(devices[0].rumble_calls == stop_calls_before + 4 &&
|
||||
devices[0].last_rumble_duration_ms == 0 &&
|
||||
devices[0].last_low == 0 &&
|
||||
devices[0].last_high == 0 &&
|
||||
g_slots[0].retained_host_rumble_valid,
|
||||
"newest XInput stop did not win during profile pulses");
|
||||
|
||||
test_adapter_mode = AdapterUsbMode::kSwitchProbe;
|
||||
const int switch_calls_before = devices[1].rumble_calls;
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
1, ControllerRumbleOutput{0x31, 0x41});
|
||||
now_ms = 1100;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!g_slots[1].retained_host_rumble_valid,
|
||||
"Switch rumble retained stale XInput desired state");
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
1, generations[1], 1,
|
||||
ControllerProfileConfirmationPolicy::kRumble);
|
||||
now_ms = 1105;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 1180;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 1255;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[1].rumble_calls == switch_calls_before + 2 &&
|
||||
devices[1].last_rumble_duration_ms ==
|
||||
kProfileFeedbackPhaseDurationMs,
|
||||
"finite Switch rumble resumed after profile feedback");
|
||||
|
||||
test_adapter_mode = AdapterUsbMode::kXInput;
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
2, ControllerRumbleOutput{0x71, 0x81});
|
||||
now_ms = 1300;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
2, generations[2], 3,
|
||||
ControllerProfileConfirmationPolicy::kRumble);
|
||||
now_ms = 1305;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
const int old_device_calls = devices[2].rumble_calls;
|
||||
platform_on_device_disconnected(&devices[2]);
|
||||
uni_hid_device_t replacement = device(2);
|
||||
require(platform_on_device_ready(&replacement) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"rumble restore replacement did not become ready");
|
||||
now_ms = 2000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[2].rumble_calls == old_device_calls &&
|
||||
replacement.rumble_calls == 0 &&
|
||||
!g_slots[2].retained_host_rumble_valid &&
|
||||
!g_slots[2].profile_feedback.active,
|
||||
"stale XInput rumble resumed on a replacement connection");
|
||||
#else
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{0x31, 0x41});
|
||||
now_ms = 0;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
0, generations[0], 1,
|
||||
ControllerProfileConfirmationPolicy::kRumble);
|
||||
now_ms = 5;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 80;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 155;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[0].rumble_calls == 2 &&
|
||||
!g_slots[0].retained_host_rumble_valid,
|
||||
"bounded Switch rumble resumed after profile feedback");
|
||||
#endif
|
||||
}
|
||||
|
||||
void test_motion_hotkey() {
|
||||
|
|
@ -1393,12 +1729,17 @@ void test_motion_hotkey() {
|
|||
input.gamepad.buttons = kMotionHotkeyButtonMask;
|
||||
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
Bluepad32SlotSnapshot backend_snapshot{};
|
||||
bluepad32_input_backend_snapshot(0, &backend_snapshot);
|
||||
require(read_controller_state(0, &snapshot) &&
|
||||
backend_snapshot.pre_hotkey_button_mask ==
|
||||
kMotionHotkeyLogicalButtonMask &&
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 0 : 3) &&
|
||||
!snapshot.dpad_up && !snapshot.button_right_shoulder &&
|
||||
!snapshot.button_start,
|
||||
"motion chord did not toggle motion or suppress its inputs");
|
||||
"motion chord was not published pre-hotkey or suppressed "
|
||||
"from normal output");
|
||||
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_zero.rumble_calls == 1 &&
|
||||
|
|
@ -1452,6 +1793,7 @@ void test_motion_hotkey() {
|
|||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
||||
g_slots[0].motion_enabled == kDefaultMotionEnabled &&
|
||||
!g_slots[0].motion_hotkey_latched &&
|
||||
g_slots[0].pre_hotkey_button_mask == 0 &&
|
||||
!g_slots[0].feedback_pending,
|
||||
"disconnect did not reset slot 0 motion hotkey state");
|
||||
}
|
||||
|
|
@ -1641,8 +1983,12 @@ int main(int argc, char** argv) {
|
|||
test_pairing_window_policy();
|
||||
} else if (scenario == "slot-lighting") {
|
||||
test_slot_lighting();
|
||||
} else if (scenario == "abxy-hotkey") {
|
||||
test_abxy_hotkey();
|
||||
} else if (scenario == "stateful-rumble") {
|
||||
test_stateful_host_rumble_restore();
|
||||
} else if (scenario == "profile-chord-raw") {
|
||||
test_profile_chord_remains_raw();
|
||||
} else if (scenario == "profile-feedback") {
|
||||
test_profile_feedback_scheduler();
|
||||
} else if (scenario == "motion-hotkey") {
|
||||
test_motion_hotkey();
|
||||
} else if (scenario == "analog-state") {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,20 @@ struct FakeProfileRow {
|
|||
uint8_t active_profile = 0;
|
||||
ControllerProfile profiles[CONTROLLER_PROFILE_COUNT]{};
|
||||
};
|
||||
struct ActivationAttempt {
|
||||
uint32_t transaction_id = 0;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
};
|
||||
|
||||
|
||||
std::array<FakeProfileRow, CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT> rows{};
|
||||
uint32_t database_generation = 7;
|
||||
uint32_t configuration_reset_generation = 3;
|
||||
unsigned active_snapshot_count = 0;
|
||||
std::array<ActivationAttempt, 32> activation_attempts{};
|
||||
size_t activation_attempt_count = 0;
|
||||
unsigned activation_busy_attempts = 0;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
|
|
@ -44,6 +53,9 @@ void prepare_profiles() {
|
|||
database_generation = 7;
|
||||
configuration_reset_generation = 3;
|
||||
active_snapshot_count = 0;
|
||||
activation_attempts = {};
|
||||
activation_attempt_count = 0;
|
||||
activation_busy_attempts = 0;
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
FakeProfileRow& row = rows[slot];
|
||||
|
|
@ -78,6 +90,31 @@ Bluepad32SlotSnapshot make_snapshot(uint8_t slot,
|
|||
snapshot.state = controller_neutral_state();
|
||||
return snapshot;
|
||||
}
|
||||
void apply_button_masks(uint16_t pre_hotkey_mask,
|
||||
uint16_t output_mask,
|
||||
Bluepad32SlotSnapshot* snapshot) {
|
||||
snapshot->pre_hotkey_button_mask = pre_hotkey_mask;
|
||||
snapshot->state = controller_neutral_state();
|
||||
controller_profile_apply_button_mask(output_mask, &snapshot->state);
|
||||
}
|
||||
void apply_button_mask(uint16_t mask, Bluepad32SlotSnapshot* snapshot) {
|
||||
apply_button_masks(mask, mask, snapshot);
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeProfileChangeEvent take_profile_change(
|
||||
uint8_t slot, bool* available) {
|
||||
ControllerProfileRuntimeProfileChangeEvent event{};
|
||||
*available =
|
||||
controller_profile_runtime_take_profile_change(slot, &event);
|
||||
return event;
|
||||
}
|
||||
|
||||
constexpr uint16_t logical_button_bit(
|
||||
ControllerProfileLogicalButton button) {
|
||||
return static_cast<uint16_t>(
|
||||
1u << static_cast<uint8_t>(button));
|
||||
}
|
||||
|
||||
|
||||
ControllerProfileTransformResult runtime_transform(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
|
|
@ -248,6 +285,292 @@ void test_analog_thresholds_rumble_and_local_confirmation() {
|
|||
"local confirmation was scaled or lost its profile policy");
|
||||
}
|
||||
|
||||
void test_default_switching_retry_commit_and_feedback() {
|
||||
prepare_profiles();
|
||||
ControllerProfile& initial_profile = rows[0].profiles[0];
|
||||
initial_profile.macro_trigger = static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kLeftShoulder);
|
||||
initial_profile.macro_step_count = 2;
|
||||
initial_profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
initial_profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
initial_profile.macro_steps[0].duration_ms = 1000;
|
||||
initial_profile.macro_steps[0].output_button_mask =
|
||||
logical_button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
initial_profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 0);
|
||||
bool event_available = true;
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"initial profile load published confirmation feedback");
|
||||
|
||||
activation_busy_attempts = 2;
|
||||
const uint16_t held_mask = static_cast<uint16_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD |
|
||||
logical_button_bit(ControllerProfileLogicalButton::kSouth));
|
||||
apply_button_mask(held_mask, &snapshot);
|
||||
ControllerProfileTransformResult output =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
require(activation_attempt_count == 1 &&
|
||||
rows[0].active_profile == 0 &&
|
||||
output.state.button_south &&
|
||||
!output.state.button_left_shoulder &&
|
||||
!output.state.button_right_shoulder &&
|
||||
!output.state.button_select &&
|
||||
!output.state.button_start &&
|
||||
!output.state.button_north,
|
||||
"default chord was not consumed before a busy activation and synthetic trigger");
|
||||
output = runtime_transform(0, snapshot, 2);
|
||||
output = runtime_transform(0, snapshot, 3);
|
||||
require(activation_attempt_count == 3 &&
|
||||
activation_attempts[0].transaction_id ==
|
||||
activation_attempts[1].transaction_id &&
|
||||
activation_attempts[1].transaction_id ==
|
||||
activation_attempts[2].transaction_id &&
|
||||
(activation_attempts[0].transaction_id & 0x80000000u) != 0 &&
|
||||
activation_attempts[0].transaction_id != 0 &&
|
||||
activation_attempts[0].profile_index == 1 &&
|
||||
output.state.button_south && !output.state.button_north,
|
||||
"held busy chord did not retry one internal activation transaction");
|
||||
|
||||
(void)runtime_transform(0, snapshot, 4);
|
||||
require(activation_attempt_count == 3,
|
||||
"accepted activation repeated while the chord remained held");
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"activation feedback was published before storage commit");
|
||||
|
||||
rows[0].active_profile = 1;
|
||||
rows[0].profiles[1].button_map[
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth)] =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
|
||||
rows[0].profiles[1].confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kLed;
|
||||
++database_generation;
|
||||
output = runtime_transform(0, snapshot, 5);
|
||||
const ControllerProfileRuntimeProfileChangeEvent event =
|
||||
take_profile_change(0, &event_available);
|
||||
require(event_available && event.connection_generation == 1 &&
|
||||
event.database_generation == database_generation &&
|
||||
event.active_profile_number == 2 &&
|
||||
event.policy == ControllerProfileConfirmationPolicy::kLed &&
|
||||
output.state.button_north &&
|
||||
!output.state.button_south &&
|
||||
!output.state.button_left_shoulder &&
|
||||
activation_attempt_count == 3,
|
||||
"committed activation did not cancel, transform, and publish exactly one event");
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"committed profile change feedback was published twice");
|
||||
|
||||
apply_button_mask(0, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 6);
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshot);
|
||||
(void)runtime_transform(0, snapshot, 7);
|
||||
require(activation_attempt_count == 4 &&
|
||||
activation_attempts[3].profile_index == 2 &&
|
||||
activation_attempts[3].transaction_id !=
|
||||
activation_attempts[0].transaction_id &&
|
||||
(activation_attempts[3].transaction_id & 0x80000000u) != 0,
|
||||
"release did not re-arm one activation for the next profile");
|
||||
}
|
||||
|
||||
void test_identity_promotion_preserves_held_switching() {
|
||||
prepare_profiles();
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
snapshot.identity = controller_identity_global();
|
||||
apply_button_mask(
|
||||
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD, &snapshot);
|
||||
|
||||
const ControllerProfileTransformResult unresolved =
|
||||
runtime_transform(0, snapshot, 0);
|
||||
require(activation_attempt_count == 1 &&
|
||||
controller_identity_is_global(
|
||||
activation_attempts[0].identity) &&
|
||||
unresolved.left_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"unresolved BLE identity did not begin one activation");
|
||||
|
||||
snapshot.identity = rows[0].identity;
|
||||
const ControllerProfileTransformResult promoted =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
bool event_available = true;
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(activation_attempt_count == 1 &&
|
||||
promoted.left_trigger_digital_threshold == 1000 &&
|
||||
!event_available,
|
||||
"identity-only BLE promotion re-armed a held activation or "
|
||||
"failed to refresh its profile");
|
||||
|
||||
(void)runtime_transform(0, snapshot, 2);
|
||||
require(activation_attempt_count == 1,
|
||||
"promoted BLE identity repeated an accepted held activation");
|
||||
|
||||
apply_button_mask(0, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 3);
|
||||
apply_button_mask(
|
||||
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 4);
|
||||
require(activation_attempt_count == 2 &&
|
||||
controller_identity_equal(
|
||||
activation_attempts[1].identity,
|
||||
rows[0].identity),
|
||||
"physical release did not re-arm the promoted identity");
|
||||
}
|
||||
|
||||
void configure_motion_suppression_probe(ControllerProfile* profile) {
|
||||
profile->macro_trigger = static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kDpadUp);
|
||||
profile->macro_step_count = 2;
|
||||
profile->macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile->macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile->macro_steps[0].duration_ms = 1000;
|
||||
profile->macro_steps[0].output_button_mask =
|
||||
logical_button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
profile->macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
}
|
||||
|
||||
void test_switching_uses_pre_hotkey_buttons_only() {
|
||||
constexpr uint16_t kMotionHotkeyLogicalMask =
|
||||
static_cast<uint16_t>(
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kDpadUp) |
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kRightShoulder) |
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kStart));
|
||||
constexpr uint16_t kCustomChord =
|
||||
static_cast<uint16_t>(
|
||||
kMotionHotkeyLogicalMask |
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kSouth));
|
||||
|
||||
for (uint8_t custom = 0; custom < 2; ++custom) {
|
||||
prepare_profiles();
|
||||
ControllerProfile& profile = rows[0].profiles[0];
|
||||
configure_motion_suppression_probe(&profile);
|
||||
const uint16_t switching_chord =
|
||||
custom != 0 ? kCustomChord
|
||||
: CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD;
|
||||
profile.switching_chord = custom != 0 ? kCustomChord : 0;
|
||||
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 0);
|
||||
const uint16_t pre_hotkey_mask = static_cast<uint16_t>(
|
||||
switching_chord | kMotionHotkeyLogicalMask);
|
||||
const uint16_t output_mask = static_cast<uint16_t>(
|
||||
pre_hotkey_mask & ~kMotionHotkeyLogicalMask);
|
||||
apply_button_masks(
|
||||
pre_hotkey_mask, output_mask, &snapshot);
|
||||
ControllerProfileTransformResult output =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
require(activation_attempt_count == 1 &&
|
||||
!output.state.dpad_up &&
|
||||
!output.state.button_right_shoulder &&
|
||||
!output.state.button_start &&
|
||||
!output.state.button_left_shoulder &&
|
||||
!output.state.button_select &&
|
||||
!output.state.button_south &&
|
||||
!output.state.button_north,
|
||||
custom != 0
|
||||
? "custom chord hidden by motion suppression"
|
||||
: "default chord hidden by motion suppression");
|
||||
|
||||
output = runtime_transform(0, snapshot, 2);
|
||||
require(activation_attempt_count == 1 &&
|
||||
!output.state.button_north,
|
||||
"held pre-hotkey chord repeated activation or leaked "
|
||||
"into synthetic input");
|
||||
}
|
||||
}
|
||||
|
||||
void test_custom_switching_chord_and_wrap() {
|
||||
prepare_profiles();
|
||||
constexpr uint16_t kCustomChord =
|
||||
static_cast<uint16_t>(
|
||||
logical_button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||
logical_button_bit(ControllerProfileLogicalButton::kCapture));
|
||||
rows[0].profiles[0].switching_chord = kCustomChord;
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 0);
|
||||
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshot);
|
||||
ControllerProfileTransformResult output =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
require(activation_attempt_count == 0 &&
|
||||
output.state.button_left_shoulder &&
|
||||
output.state.button_right_shoulder &&
|
||||
output.state.button_select && output.state.button_start,
|
||||
"nonzero switching chord did not replace the default");
|
||||
|
||||
apply_button_mask(kCustomChord, &snapshot);
|
||||
output = runtime_transform(0, snapshot, 2);
|
||||
require(activation_attempt_count == 1 &&
|
||||
activation_attempts[0].profile_index == 1 &&
|
||||
!output.state.button_south &&
|
||||
!output.state.button_capture,
|
||||
"custom switching chord was not consumed or activated");
|
||||
|
||||
prepare_profiles();
|
||||
rows[0].active_profile = 3;
|
||||
rows[0].profiles[3].switching_chord = kCustomChord;
|
||||
snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 10);
|
||||
bool event_available = true;
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"initial profile 4 load published a change event");
|
||||
apply_button_mask(kCustomChord, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 11);
|
||||
require(activation_attempt_count == 1 &&
|
||||
activation_attempts[0].profile_index == 0,
|
||||
"profile switching did not wrap profile 4 to profile 1");
|
||||
}
|
||||
void test_switching_slot_isolation() {
|
||||
prepare_profiles();
|
||||
std::array<Bluepad32SlotSnapshot,
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||
snapshots{};
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
snapshots[slot] = make_snapshot(slot);
|
||||
(void)runtime_transform(slot, snapshots[slot], 0);
|
||||
}
|
||||
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshots[2]);
|
||||
(void)runtime_transform(2, snapshots[2], 1);
|
||||
snapshots[0].state.button_south = true;
|
||||
const ControllerProfileTransformResult untouched =
|
||||
runtime_transform(0, snapshots[0], 1);
|
||||
require(activation_attempt_count == 1 &&
|
||||
controller_identity_equal(
|
||||
activation_attempts[0].identity,
|
||||
rows[2].identity) &&
|
||||
untouched.state.button_south,
|
||||
"one slot's switching chord affected another slot");
|
||||
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshots[3]);
|
||||
(void)runtime_transform(2, snapshots[2], 2);
|
||||
(void)runtime_transform(3, snapshots[3], 2);
|
||||
require(activation_attempt_count == 2 &&
|
||||
controller_identity_equal(
|
||||
activation_attempts[1].identity,
|
||||
rows[3].identity),
|
||||
"held switching state was shared between controller slots");
|
||||
}
|
||||
|
||||
|
||||
void configure_synthetic_profile(uint8_t slot) {
|
||||
ControllerProfile& profile = rows[slot].profiles[0];
|
||||
profile = controller_profile_default(rows[slot].identity, 0);
|
||||
|
|
@ -347,6 +670,12 @@ void test_all_runtime_cancellation_causes() {
|
|||
output = runtime_transform(0, snapshot, 33);
|
||||
require_no_synthetic_output(
|
||||
output, "profile/database generation change did not cancel");
|
||||
bool event_available = false;
|
||||
const ControllerProfileRuntimeProfileChangeEvent cancellation_event =
|
||||
take_profile_change(0, &event_available);
|
||||
require(event_available &&
|
||||
cancellation_event.active_profile_number == 2,
|
||||
"profile commit feedback was not observed with cancellation");
|
||||
|
||||
prepare_profiles();
|
||||
configure_synthetic_profile(0);
|
||||
|
|
@ -413,6 +742,20 @@ uint32_t profile_service_database_generation() {
|
|||
uint32_t configuration_service_reset_generation() {
|
||||
return configuration_reset_generation;
|
||||
}
|
||||
ConfigurationTransactionStatus profile_service_activate_internal(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
require(activation_attempt_count < activation_attempts.size(),
|
||||
"activation attempt fixture overflow");
|
||||
activation_attempts[activation_attempt_count++] = {
|
||||
transaction_id, identity, profile_index};
|
||||
if (activation_busy_attempts != 0) {
|
||||
--activation_busy_attempts;
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity,
|
||||
|
|
@ -439,6 +782,11 @@ int main() {
|
|||
test_four_slot_cache_and_unchanged_generation();
|
||||
test_activation_disconnect_and_default_preservation();
|
||||
test_analog_thresholds_rumble_and_local_confirmation();
|
||||
test_default_switching_retry_commit_and_feedback();
|
||||
test_identity_promotion_preserves_held_switching();
|
||||
test_switching_uses_pre_hotkey_buttons_only();
|
||||
test_custom_switching_chord_and_wrap();
|
||||
test_switching_slot_isolation();
|
||||
test_all_runtime_cancellation_causes();
|
||||
test_runtime_slot_synthetic_isolation();
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -211,6 +211,100 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
|
|||
"activation did not publish profile, index, and generation together");
|
||||
}
|
||||
|
||||
void test_host_and_controller_mutations_are_serialized() {
|
||||
const ControllerIdentity identity = controller_identity_global();
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(identity, 1);
|
||||
profile.weak_rumble_scale = 23;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"serialization fixture profile did not encode");
|
||||
|
||||
constexpr uint32_t kHostTransactionId = 0x11223344;
|
||||
constexpr uint32_t kInternalTransactionId = 0x80000019;
|
||||
require(profile_service_begin(
|
||||
kHostTransactionId, identity, 1,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"host write did not acquire the profile mutation boundary");
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"controller activation raced a receiving host write");
|
||||
ProfileServiceTransactionSnapshot snapshot =
|
||||
transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"busy controller activation replaced the host transaction");
|
||||
|
||||
require(profile_service_append(
|
||||
kHostTransactionId, 0, encoded, sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kHostTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host write did not reach pending after controller contention");
|
||||
profile_service_task_on_storage_core(3000);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"serialized host write did not commit");
|
||||
|
||||
constexpr uint32_t kHostActivationTransactionId = 0x22334455;
|
||||
require(profile_service_activate(
|
||||
kHostActivationTransactionId, identity, 0) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host activation did not acquire the released boundary");
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"controller activation raced a pending host activation");
|
||||
snapshot = transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending &&
|
||||
active_profile_snapshot(identity).profile_index == 3,
|
||||
"pending host activation was replaced or leaked before commit");
|
||||
profile_service_task_on_storage_core(4000);
|
||||
require(active_profile_snapshot(identity).profile_index == 0,
|
||||
"serialized host activation did not commit");
|
||||
require(profile_service_activate_internal(
|
||||
0x19, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"internal activation admitted a transaction without the high bit");
|
||||
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"controller activation did not acquire the released boundary");
|
||||
require(profile_service_begin(
|
||||
0x55667788, identity, 0,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"host write raced a pending controller activation");
|
||||
snapshot = transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
active_profile_snapshot(identity).profile_index == 0,
|
||||
"pending controller activation replaced host-visible status or leaked before commit");
|
||||
|
||||
profile_service_task_on_storage_core(5000);
|
||||
const ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(identity);
|
||||
snapshot = transaction_snapshot();
|
||||
require(active.valid && active.profile_index == 2 &&
|
||||
snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"controller activation did not publish while preserving host-visible status");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io() {
|
||||
|
|
@ -219,5 +313,6 @@ ProfileStorageIo pico_profile_storage_io() {
|
|||
|
||||
int main() {
|
||||
test_pending_commands_are_not_decoded_as_profile_writes();
|
||||
test_host_and_controller_mutations_are_serialized();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"lifecycle",
|
||||
"pairing-policy",
|
||||
"slot-lighting",
|
||||
"abxy-hotkey",
|
||||
"profile-chord-raw",
|
||||
"profile-feedback",
|
||||
"stateful-rumble",
|
||||
"motion-hotkey",
|
||||
"analog-state",
|
||||
"rumble-mode",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue