switch-pico/tests/controller_profile_runtime_test.cpp

1673 lines
74 KiB
C++

#include "profile/controller_profile_runtime.h"
#include "core/controller_identity.h"
#include "profile/controller_profile.h"
#include "profile/profile_service.h"
#include <array>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <cmath>
namespace {
struct FakeProfileRow {
ControllerIdentity identity{};
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;
ConfigurationTransactionStatus activation_result =
ConfigurationTransactionStatus::kPending;
uint8_t last_motion_toggle_slot = 0;
uint32_t last_motion_toggle_connection_generation = 0;
unsigned motion_toggle_count = 0;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
ControllerIdentity make_identity(uint8_t value) {
ControllerIdentity identity{};
identity.stable = true;
identity.transport = ControllerTransport::kClassic;
identity.address[0] = value;
identity.address[5] = static_cast<uint8_t>(value + 0x40u);
identity.vendor_id = static_cast<uint16_t>(0x1000u + value);
identity.product_id = static_cast<uint16_t>(0x2000u + value);
return identity;
}
void prepare_profiles() {
database_generation = 7;
configuration_reset_generation = 3;
active_snapshot_count = 0;
activation_attempts = {};
activation_attempt_count = 0;
activation_busy_attempts = 0;
activation_result = ConfigurationTransactionStatus::kPending;
last_motion_toggle_slot = 0;
last_motion_toggle_connection_generation = 0;
motion_toggle_count = 0;
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
FakeProfileRow& row = rows[slot];
row = {};
row.identity = make_identity(static_cast<uint8_t>(slot + 1u));
for (uint8_t profile_index = 0;
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
row.profiles[profile_index] =
controller_profile_default(row.identity, profile_index);
}
row.profiles[0].triggers[0].digital_threshold =
static_cast<uint16_t>(1000u + slot);
row.profiles[0].triggers[1].digital_threshold =
static_cast<uint16_t>(5000u + slot);
row.profiles[0].confirmation_policy =
slot == 0 ? ControllerProfileConfirmationPolicy::kLed
: ControllerProfileConfirmationPolicy::kRumble;
}
rows[0].profiles[0].strong_rumble_scale = 0;
rows[0].profiles[0].weak_rumble_scale = UINT8_MAX;
rows[1].profiles[0].strong_rumble_scale = UINT8_MAX;
rows[1].profiles[0].weak_rumble_scale = 0;
controller_profile_runtime_reset();
}
Bluepad32SlotSnapshot make_snapshot(uint8_t slot,
uint32_t connection_generation = 1) {
Bluepad32SlotSnapshot snapshot{};
snapshot.active = true;
snapshot.connection_generation = connection_generation;
snapshot.identity = rows[slot].identity;
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;
}
ControllerProfileRuntimeProfileChangeEvent
take_initial_profile_indication(uint8_t slot, bool* available) {
ControllerProfileRuntimeProfileChangeEvent event{};
*available =
controller_profile_runtime_take_initial_profile_indication(
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,
uint32_t now_ms = 0,
AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe) {
return controller_profile_runtime_transform(slot, snapshot, now_ms,
output_mode);
}
bool motion_equal(const ControllerState& first,
const ControllerState& second) {
return first.motion_sample_count == second.motion_sample_count &&
std::memcmp(first.motion_samples, second.motion_samples,
sizeof(first.motion_samples)) == 0;
}
void test_four_slot_cache_and_unchanged_generation() {
prepare_profiles();
std::array<Bluepad32SlotSnapshot,
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
snapshots{};
std::array<ControllerProfileTransformResult,
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
transformed{};
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
snapshots[slot] = make_snapshot(slot);
transformed[slot] =
runtime_transform(slot, snapshots[slot]);
require(transformed[slot].left_trigger_digital_threshold ==
static_cast<uint16_t>(1000u + slot) &&
transformed[slot].right_trigger_digital_threshold ==
static_cast<uint16_t>(5000u + slot),
"a slot did not receive its own cached profile");
}
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
"initial slot loads did not fetch exactly one profile each");
rows[0].profiles[0].triggers[0].digital_threshold = 65000;
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
transformed[slot] =
runtime_transform(slot, snapshots[slot]);
}
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
"unchanged generations copied profiles on the report path");
require(transformed[0].left_trigger_digital_threshold == 1000,
"an unchanged generation bypassed the slot cache");
snapshots[2].connection_generation = 2;
transformed[2] =
runtime_transform(2, snapshots[2]);
require(active_snapshot_count ==
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 1 &&
transformed[2].left_trigger_digital_threshold == 1002,
"one slot connection generation did not refresh in isolation");
snapshots[3].identity = rows[1].identity;
transformed[3] =
runtime_transform(3, snapshots[3]);
require(active_snapshot_count ==
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 2 &&
transformed[3].left_trigger_digital_threshold == 1001,
"an exact identity change did not refresh only its slot");
}
void test_activation_disconnect_and_default_preservation() {
prepare_profiles();
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
snapshot.state.button_south = true;
ControllerProfileTransformResult transformed =
runtime_transform(0, snapshot);
require(transformed.state.button_south &&
transformed.left_trigger_digital_threshold == 1000,
"initial active profile was not applied");
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].triggers[0].digital_threshold = 12345;
++database_generation;
transformed = runtime_transform(0, snapshot);
require(!transformed.state.button_south && transformed.state.button_north &&
transformed.left_trigger_digital_threshold == 12345,
"profile activation generation did not refresh the slot cache");
const unsigned reads_before_disconnect = active_snapshot_count;
snapshot.active = false;
transformed = runtime_transform(0, snapshot);
require(!transformed.state.button_south && !transformed.state.button_north &&
transformed.left_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
active_snapshot_count == reads_before_disconnect,
"disconnect did not neutralize output without fetching a profile");
transformed = runtime_transform(0, snapshot);
require(active_snapshot_count == reads_before_disconnect,
"an unchanged inactive slot repeatedly cleared or fetched state");
snapshot.active = true;
transformed = runtime_transform(0, snapshot);
require(active_snapshot_count == reads_before_disconnect + 1 &&
transformed.state.button_north,
"reconnection did not reload a cleared slot cache");
Bluepad32SlotSnapshot default_snapshot = make_snapshot(2);
rows[2].profiles[0] =
controller_profile_default(rows[2].identity, 0);
++database_generation;
default_snapshot.state.button_east = true;
default_snapshot.state.dpad_left = true;
default_snapshot.state.left_trigger = 32123;
default_snapshot.state.right_trigger = 54321;
default_snapshot.state.left_stick_x = -12345;
default_snapshot.state.right_stick_y = 23456;
default_snapshot.state.motion_sample_count = 2;
default_snapshot.state.motion_samples[0] = {1, 2, 3, 4, 5, 6};
default_snapshot.state.motion_samples[1] = {-1, -2, -3, -4, -5, -6};
transformed = runtime_transform(2, default_snapshot);
require(transformed.state.button_east && transformed.state.dpad_left &&
transformed.state.left_trigger == 32123 &&
transformed.state.right_trigger == 54321 &&
transformed.state.left_stick_x == -12345 &&
transformed.state.right_stick_y == 23456 &&
motion_equal(transformed.state, default_snapshot.state) &&
transformed.left_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
transformed.right_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
"default runtime profile changed serializer input or motion");
}
void test_analog_thresholds_rumble_and_local_confirmation() {
prepare_profiles();
Bluepad32SlotSnapshot first = make_snapshot(0);
Bluepad32SlotSnapshot second = make_snapshot(1);
first.state.left_trigger = second.state.left_trigger = 23456;
first.state.right_trigger = second.state.right_trigger = 45678;
const ControllerProfileTransformResult first_output =
runtime_transform(0, first);
const ControllerProfileTransformResult second_output =
runtime_transform(1, second);
require(first_output.state.left_trigger ==
second_output.state.left_trigger &&
first_output.state.right_trigger ==
second_output.state.right_trigger &&
first_output.left_trigger_digital_threshold !=
second_output.left_trigger_digital_threshold,
"digital thresholds changed XInput-visible analog trigger state");
const ControllerRumbleOutput host{91, 73};
const ControllerRumbleOutput first_host =
controller_profile_runtime_scale_host_rumble(0, first, host);
const ControllerRumbleOutput second_host =
controller_profile_runtime_scale_host_rumble(1, second, host);
require(first_host.low_frequency_magnitude == 0 &&
first_host.high_frequency_magnitude == 73 &&
second_host.low_frequency_magnitude == 91 &&
second_host.high_frequency_magnitude == 0,
"host rumble was not scaled through each slot profile");
const ControllerProfileRuntimeLocalConfirmation confirmation =
controller_profile_runtime_local_confirmation(0, first, host);
require(confirmation.rumble.low_frequency_magnitude == 91 &&
confirmation.rumble.high_frequency_magnitude == 73 &&
confirmation.policy ==
ControllerProfileConfirmationPolicy::kLed,
"local confirmation was scaled or lost its profile policy");
}
void test_initial_profile_indication_once_per_connection() {
prepare_profiles();
std::array<Bluepad32SlotSnapshot,
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
snapshots{};
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
rows[slot].active_profile = slot;
rows[slot].profiles[slot].confirmation_policy =
ControllerProfileConfirmationPolicy::kRumbleAndLed;
snapshots[slot] = make_snapshot(slot);
(void)runtime_transform(slot, snapshots[slot]);
}
constexpr uint8_t kTakeOrder[CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT] = {
2, 0, 3, 1};
for (const uint8_t slot : kTakeOrder) {
bool available = false;
const ControllerProfileRuntimeProfileChangeEvent event =
take_initial_profile_indication(slot, &available);
require(available &&
event.connection_generation == 1 &&
event.database_generation == database_generation &&
event.active_profile_number == slot + 1u &&
event.policy ==
ControllerProfileConfirmationPolicy::kLed,
"initial profile 1..4 indication was not isolated, "
"LED-only, or generation-bound");
(void)take_initial_profile_indication(slot, &available);
require(!available,
"initial profile indication repeated without a new "
"connection");
(void)take_profile_change(slot, &available);
require(!available,
"initial profile resolution published switch feedback");
}
++database_generation;
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
(void)runtime_transform(slot, snapshots[slot]);
bool available = true;
(void)take_initial_profile_indication(slot, &available);
require(!available,
"database refresh repeated initial profile indication");
}
rows[2].identity = controller_identity_global();
snapshots[0].identity = controller_identity_global();
snapshots[0].connection_generation = 2;
(void)runtime_transform(0, snapshots[0]);
bool available = false;
const ControllerProfileRuntimeProfileChangeEvent unresolved_event =
take_initial_profile_indication(0, &available);
require(available &&
unresolved_event.connection_generation == 2 &&
unresolved_event.active_profile_number == 3,
"first committed unresolved-identity profile was not "
"indicated");
snapshots[0].identity = rows[1].identity;
(void)runtime_transform(0, snapshots[0]);
(void)take_initial_profile_indication(0, &available);
require(!available,
"identity promotion repeated initial profile indication");
snapshots[0].connection_generation = 3;
(void)runtime_transform(0, snapshots[0]);
const ControllerProfileRuntimeProfileChangeEvent reconnect_event =
take_initial_profile_indication(0, &available);
require(available &&
reconnect_event.connection_generation == 3 &&
reconnect_event.active_profile_number == 2 &&
reconnect_event.policy ==
ControllerProfileConfirmationPolicy::kLed,
"true reconnection did not publish one fresh LED-only "
"profile indication");
rows[1].profiles[1].confirmation_policy =
ControllerProfileConfirmationPolicy::kRumble;
snapshots[1].connection_generation = 2;
(void)runtime_transform(1, snapshots[1]);
(void)take_initial_profile_indication(1, &available);
require(!available,
"rumble-only initial policy disturbed controller feedback");
rows[3].profiles[3].confirmation_policy =
ControllerProfileConfirmationPolicy::kNone;
snapshots[3].connection_generation = 2;
(void)runtime_transform(3, snapshots[3]);
(void)take_initial_profile_indication(3, &available);
require(!available,
"disabled initial policy disturbed controller feedback");
}
void test_default_switching_retry_commit_and_feedback() {
prepare_profiles();
ControllerProfile& initial_profile = rows[0].profiles[0];
initial_profile.macros[0].trigger_mask =
logical_button_bit(
ControllerProfileLogicalButton::kLeftShoulder);
initial_profile.macros[0].first_step = 0;
initial_profile.macros[0].step_count = 1;
initial_profile.macro_step_count = 1;
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);
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->macros[0].trigger_mask =
logical_button_bit(
ControllerProfileLogicalButton::kDpadUp);
profile->macros[0].first_step = 0;
profile->macros[0].step_count = 1;
profile->macro_step_count = 1;
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);
}
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 = 7;
rows[0].profiles[7].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 8 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 8 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 test_profile_motion_toggle_supports_trigger_chords() {
prepare_profiles();
const uint32_t custom_chord =
logical_button_bit(ControllerProfileLogicalButton::kSouth) |
(1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL);
rows[0].profiles[0].motion_toggle_chord = custom_chord;
Bluepad32SlotSnapshot snapshot = make_snapshot(0, 9);
(void)runtime_transform(0, snapshot, 0);
require(motion_toggle_count == 0,
"profile refresh toggled motion without a chord");
apply_button_mask(
logical_button_bit(ControllerProfileLogicalButton::kSouth),
&snapshot);
snapshot.state.left_trigger = UINT16_MAX;
ControllerProfileTransformResult output =
runtime_transform(0, snapshot, 1);
require(motion_toggle_count == 1 &&
last_motion_toggle_slot == 0 &&
last_motion_toggle_connection_generation == 9 &&
controller_profile_extract_button_mask(output.state) == 0 &&
output.state.left_trigger == 0,
"trigger-backed motion chord did not toggle and consume inputs");
(void)runtime_transform(0, snapshot, 2);
require(motion_toggle_count == 1,
"held trigger-backed motion chord toggled twice");
snapshot = make_snapshot(0, 9);
(void)runtime_transform(0, snapshot, 3);
apply_button_mask(
logical_button_bit(ControllerProfileLogicalButton::kSouth),
&snapshot);
snapshot.state.left_trigger = UINT16_MAX;
(void)runtime_transform(0, snapshot, 4);
require(motion_toggle_count == 2,
"released trigger-backed motion chord did not re-arm");
}
void configure_synthetic_profile(uint8_t slot) {
ControllerProfile& profile = rows[slot].profiles[0];
profile = controller_profile_default(rows[slot].identity, 0);
profile.macros[0].trigger_mask =
logical_button_bit(ControllerProfileLogicalButton::kSouth);
profile.macros[0].cancel_control =
static_cast<uint8_t>(ControllerProfileLogicalButton::kCapture);
profile.macros[0].first_step = 0;
profile.macros[0].step_count = 1;
profile.macro_step_count = 1;
profile.macro_steps[0].override_flags =
kControllerProfileOverrideButtons;
profile.macro_steps[0].duration_ms = 1000;
profile.macro_steps[0].output_button_mask = static_cast<uint16_t>(
1u << static_cast<uint8_t>(
ControllerProfileLogicalButton::kNorth));
profile.turbo_modes[static_cast<uint8_t>(
ControllerProfileLogicalButton::kEast)] =
ControllerProfileTurboMode::kAutoBurst;
}
void start_macro(Bluepad32SlotSnapshot* snapshot, uint32_t now_ms,
AdapterUsbMode mode = AdapterUsbMode::kSwitchProbe) {
snapshot->state = controller_neutral_state();
(void)runtime_transform(0, *snapshot, now_ms, mode);
snapshot->state.button_south = true;
const ControllerProfileTransformResult started =
runtime_transform(0, *snapshot, now_ms + 1u, mode);
require(started.state.button_north && !started.state.button_south,
"runtime fixture did not start its macro");
snapshot->state = controller_neutral_state();
const ControllerProfileTransformResult held =
runtime_transform(0, *snapshot, now_ms + 2u, mode);
require(held.state.button_north,
"runtime fixture macro did not remain active");
}
void start_auto_burst(Bluepad32SlotSnapshot* snapshot, uint32_t now_ms,
AdapterUsbMode mode =
AdapterUsbMode::kSwitchProbe) {
snapshot->state = controller_neutral_state();
(void)runtime_transform(0, *snapshot, now_ms, mode);
snapshot->state.button_east = true;
const ControllerProfileTransformResult started =
runtime_transform(0, *snapshot, now_ms + 1u, mode);
require(started.state.button_east,
"runtime fixture did not start Auto Burst");
snapshot->state = controller_neutral_state();
const ControllerProfileTransformResult latched =
runtime_transform(0, *snapshot, now_ms + 2u, mode);
require(latched.state.button_east,
"runtime fixture Auto Burst did not latch");
}
void require_no_synthetic_output(
const ControllerProfileTransformResult& output,
const char* message) {
require(!output.state.button_north && !output.state.button_east &&
!output.state.button_south &&
!output.state.button_capture,
message);
}
void test_all_runtime_cancellation_causes() {
prepare_profiles();
configure_synthetic_profile(0);
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
start_macro(&snapshot, 10);
snapshot.active = false;
ControllerProfileTransformResult output =
runtime_transform(0, snapshot, 13);
require_no_synthetic_output(
output, "disconnect did not cancel synthetic output");
snapshot.active = true;
output = runtime_transform(0, snapshot, 14);
require_no_synthetic_output(
output, "reconnection restored stale synthetic output");
prepare_profiles();
configure_synthetic_profile(0);
snapshot = make_snapshot(0);
start_macro(&snapshot, 20);
++snapshot.connection_generation;
output = runtime_transform(0, snapshot, 23);
require_no_synthetic_output(
output, "connection replacement did not cancel before output");
prepare_profiles();
configure_synthetic_profile(0);
snapshot = make_snapshot(0);
start_macro(&snapshot, 30);
rows[0].active_profile = 1;
++database_generation;
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);
snapshot = make_snapshot(0);
start_auto_burst(&snapshot, 40);
output = runtime_transform(0, snapshot, 43,
AdapterUsbMode::kXInput);
require_no_synthetic_output(
output, "output-mode change did not cancel Auto Burst");
output = runtime_transform(0, snapshot, 44,
AdapterUsbMode::kXInput);
require_no_synthetic_output(
output, "output-mode cancellation left a stuck output");
prepare_profiles();
configure_synthetic_profile(0);
snapshot = make_snapshot(0);
start_auto_burst(&snapshot, 50);
++configuration_reset_generation;
output = runtime_transform(0, snapshot, 53);
require_no_synthetic_output(
output, "configuration reset did not cancel Auto Burst");
output = runtime_transform(0, snapshot, 54);
require_no_synthetic_output(
output, "configuration reset cancellation left a stuck output");
}
void test_runtime_slot_synthetic_isolation() {
prepare_profiles();
configure_synthetic_profile(0);
configure_synthetic_profile(1);
Bluepad32SlotSnapshot first = make_snapshot(0);
Bluepad32SlotSnapshot second = make_snapshot(1);
(void)runtime_transform(0, first, 0);
(void)runtime_transform(1, second, 0);
first.state.button_east = true;
second.state.button_east = true;
require(runtime_transform(0, first, 1).state.button_east &&
runtime_transform(1, second, 1).state.button_east,
"runtime slots did not activate independently");
first.state = controller_neutral_state();
second.state = controller_neutral_state();
require(runtime_transform(0, first, 2).state.button_east &&
runtime_transform(1, second, 2).state.button_east,
"runtime Auto Burst state did not remain isolated");
first.state.button_capture = true;
const ControllerProfileTransformResult cancelled =
runtime_transform(0, first, 3);
const ControllerProfileTransformResult untouched =
runtime_transform(1, second, 3);
require_no_synthetic_output(
cancelled, "slot-local configured cancel did not clear its output");
require(untouched.state.button_east,
"slot-local configured cancel affected another slot");
}
void test_shortcut_arbitration_latching_and_commit_feedback() {
prepare_profiles();
ControllerProfile& profile = rows[0].profiles[0];
constexpr uint16_t modifier = 1u << 9;
constexpr uint16_t chord = modifier | 1u;
profile.shortcuts.modifier = 9;
profile.shortcuts.selectors[2] = 0;
profile.shortcuts.selectors[3] = 1;
profile.switching_chord = chord;
profile.motion_toggle_chord = chord;
profile.shift.mode = ControllerProfileShiftMode::kToggle;
profile.shift.modifier = 9;
profile.shift.button_map[2] = 3;
profile.macros[0] = {chord, CONTROLLER_PROFILE_NO_BUTTON, 0, 1};
profile.macro_step_count = 1;
profile.macro_steps[0] = {kControllerProfileOverrideButtons, 100, 8};
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
(void)runtime_transform(0, snapshot, 0);
apply_button_mask(chord | 2u, &snapshot);
auto output = runtime_transform(0, snapshot, 1);
require(activation_attempt_count == 0 && motion_toggle_count == 0 &&
controller_profile_extract_button_mask(output.state) == 0,
"ambiguous direct selectors triggered a lower-priority action");
apply_button_mask(chord, &snapshot);
output = runtime_transform(0, snapshot, 2);
require(activation_attempt_count == 0 &&
controller_profile_extract_button_mask(output.state) == 0,
"ambiguous chord chose a target when one selector was released");
apply_button_mask(modifier, &snapshot);
(void)runtime_transform(0, snapshot, 3);
apply_button_mask(chord | 4u, &snapshot);
output = runtime_transform(0, snapshot, 4);
require(activation_attempt_count == 1 &&
activation_attempts[0].profile_index == 2 &&
motion_toggle_count == 0 && output.state.button_west &&
!output.state.button_north && !output.state.button_south &&
!output.state.button_capture,
"direct shortcut did not outrank cycle, motion, Shift and macro");
bool available = true;
(void)take_profile_change(0, &available);
require(!available, "accepted but uncommitted shortcut produced feedback");
ControllerProfile& committed = rows[0].profiles[2];
committed.shortcuts.modifier = 9;
committed.shortcuts.selectors[4] = 0;
committed.button_map[2] = 3;
committed.confirmation_policy = ControllerProfileConfirmationPolicy::kLed;
rows[0].active_profile = 2;
++database_generation;
output = runtime_transform(0, snapshot, 5);
const auto event = take_profile_change(0, &available);
require(available && event.active_profile_number == 3 &&
event.policy == ControllerProfileConfirmationPolicy::kLed &&
activation_attempt_count == 1 && output.state.button_north &&
!output.state.button_capture && !output.state.button_south,
"committed shortcut redirected a held chord or lost commit feedback");
(void)take_profile_change(0, &available);
require(!available, "shortcut published duplicate commit feedback");
apply_button_mask(modifier, &snapshot);
(void)runtime_transform(0, snapshot, 6);
activation_result = ConfigurationTransactionStatus::kStorageError;
apply_button_mask(chord, &snapshot);
output = runtime_transform(0, snapshot, 7);
(void)runtime_transform(0, snapshot, 8);
(void)take_profile_change(0, &available);
require(activation_attempt_count == 2 &&
activation_attempts[1].profile_index == 4 && !available &&
controller_profile_extract_button_mask(output.state) == 0,
"failed activation retried, leaked its chord, or produced feedback");
prepare_profiles();
rows[0].profiles[0].shortcuts.modifier = 9;
rows[0].profiles[0].shortcuts.selectors[0] = 0;
snapshot = make_snapshot(0);
(void)runtime_transform(0, snapshot, 0);
apply_button_mask(chord, &snapshot);
output = runtime_transform(0, snapshot, 1);
(void)take_profile_change(0, &available);
require(activation_attempt_count == 0 && !available &&
controller_profile_extract_button_mask(output.state) == 0,
"already-active direct target was not a consumed no-op");
}
void test_busy_shortcut_target_and_generation_isolation() {
prepare_profiles();
constexpr uint16_t chord = (1u << 9) | 1u;
rows[0].profiles[0].shortcuts.modifier = 9;
rows[0].profiles[0].shortcuts.selectors[3] = 0;
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
(void)runtime_transform(0, snapshot, 0);
activation_busy_attempts = 3;
apply_button_mask(chord, &snapshot);
(void)runtime_transform(0, snapshot, 1);
rows[0].active_profile = 1;
rows[0].profiles[1].shortcuts.modifier = 9;
rows[0].profiles[1].shortcuts.selectors[6] = 0;
++database_generation;
(void)runtime_transform(0, snapshot, 2);
require(activation_attempt_count == 2 &&
activation_attempts[1].profile_index == 3 &&
activation_attempts[0].transaction_id ==
activation_attempts[1].transaction_id,
"profile refresh changed a busy shortcut's latched target");
++snapshot.connection_generation;
auto output = runtime_transform(0, snapshot, 3);
require(activation_attempt_count == 2 &&
controller_profile_extract_button_mask(output.state) == 0,
"new connection generation inherited or retriggered held activation");
apply_button_mask(0, &snapshot);
(void)runtime_transform(0, snapshot, 4);
apply_button_mask(chord, &snapshot);
(void)runtime_transform(0, snapshot, 5);
require(activation_attempt_count == 3 &&
activation_attempts[2].profile_index == 6 &&
activation_attempts[2].transaction_id !=
activation_attempts[0].transaction_id,
"new generation could not rearm with its own target");
output = runtime_transform(0, snapshot, 6, AdapterUsbMode::kXInput);
(void)runtime_transform(0, snapshot, 7, AdapterUsbMode::kXInput);
require(activation_attempt_count == 3 &&
controller_profile_extract_button_mask(output.state) == 0,
"output-mode reset leaked a pending activation retry");
}
void test_cycle_motion_arbitration_and_held_refresh() {
prepare_profiles();
constexpr uint16_t chord = (1u << 9) | 1u;
rows[0].profiles[0].switching_chord = chord;
rows[0].profiles[0].motion_toggle_chord = chord;
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
(void)runtime_transform(0, snapshot, 0);
apply_button_mask(chord, &snapshot);
auto output = runtime_transform(0, snapshot, 1);
require(activation_attempt_count == 1 && motion_toggle_count == 0 &&
controller_profile_extract_button_mask(output.state) == 0,
"one cycle chord also toggled motion");
apply_button_mask(1u, &snapshot);
(void)runtime_transform(0, snapshot, 2);
apply_button_mask(chord, &snapshot);
(void)runtime_transform(0, snapshot, 2);
require(activation_attempt_count == 2 && motion_toggle_count == 0,
"re-completing a released cycle chord did not rearm");
apply_button_mask(1u, &snapshot);
(void)runtime_transform(0, snapshot, 2);
rows[0].profiles[0].motion_toggle_chord = 1u;
rows[0].profiles[0].switching_chord = 2u;
++database_generation;
output = runtime_transform(0, snapshot, 3);
require(motion_toggle_count == 0 && !output.state.button_south,
"partial held chord became a new action after profile refresh");
apply_button_mask(0, &snapshot);
(void)runtime_transform(0, snapshot, 4);
apply_button_mask(1u, &snapshot);
output = runtime_transform(0, snapshot, 5);
require(motion_toggle_count == 1 && !output.state.button_south,
"motion did not rearm after the higher-priority chord released");
++database_generation;
output = runtime_transform(0, snapshot, 6);
require(motion_toggle_count == 1 && !output.state.button_south,
"profile refresh phantom-toggled held motion chord");
}
void test_shift_slot_and_context_resets() {
prepare_profiles();
std::array<Bluepad32SlotSnapshot, 4> snapshots{};
constexpr uint8_t outputs[4] = {1, 2, 3, 12};
for (uint8_t slot = 0; slot < 4; ++slot) {
auto& profile = rows[slot].profiles[0];
profile.shift.mode = ControllerProfileShiftMode::kToggle;
profile.shift.modifier = 9;
profile.shift.button_map[0] = outputs[slot];
snapshots[slot] = make_snapshot(slot);
(void)runtime_transform(slot, snapshots[slot], 0);
apply_button_mask((1u << 9) | 1u, &snapshots[slot]);
const auto output = runtime_transform(slot, snapshots[slot], 1);
require(controller_profile_extract_button_mask(output.state) ==
(1u << outputs[slot]),
"Shift activation was not physical, consumed and slot-local");
}
++snapshots[0].connection_generation;
auto output = runtime_transform(0, snapshots[0], 2);
require(output.state.button_south && !output.state.button_capture,
"connection reset phantom-toggled held Shift");
output = runtime_transform(1, snapshots[1], 2);
require(output.state.button_west && !output.state.button_south,
"one slot's Shift reset disturbed another slot");
output = runtime_transform(1, snapshots[1], 3, AdapterUsbMode::kXInput);
require(output.state.button_south && !output.state.button_west,
"mode reset retained or retriggered toggle Shift");
rows[2].profiles[1].shift = rows[2].profiles[0].shift;
rows[2].active_profile = 1;
++database_generation;
output = runtime_transform(2, snapshots[2], 4);
require(output.state.button_south && !output.state.button_north &&
!output.state.button_capture,
"profile activation retained or phantom-toggled Shift");
output = runtime_transform(3, snapshots[3], 4);
require(output.state.button_south && !output.state.dpad_up,
"database refresh did not reset the remaining Shift slot");
apply_button_mask(1u, &snapshots[3]);
(void)runtime_transform(3, snapshots[3], 5);
apply_button_mask((1u << 9) | 1u, &snapshots[3]);
output = runtime_transform(3, snapshots[3], 6);
require(output.state.dpad_up && !output.state.button_south,
"Shift could not rearm after reset and physical release");
++configuration_reset_generation;
output = runtime_transform(3, snapshots[3], 7);
require(output.state.button_south && !output.state.dpad_up,
"configuration reset retained Shift toggle state");
}
void test_held_synthetic_sources_and_disconnect_rearming() {
prepare_profiles();
auto& profile = rows[0].profiles[0];
profile.macros[0] = {1u, 9, 0, 1};
profile.macros[0].mode = ControllerProfileMacroMode::kToggle;
profile.macro_step_count = 1;
profile.macro_steps[0].override_flags = kControllerProfileOverrideLeftStick;
profile.macro_steps[0].duration_ms = 10;
profile.macro_steps[0].left_stick_x = 12345;
profile.turbo_modes[1] = ControllerProfileTurboMode::kBurst;
profile.turbo_modes[2] = ControllerProfileTurboMode::kAutoBurst;
profile.shortcuts.modifier = 9;
profile.shortcuts.selectors[2] = 3;
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
(void)runtime_transform(0, snapshot, 0);
apply_button_mask(7u, &snapshot);
auto output = runtime_transform(0, snapshot, 1);
require(output.state.left_stick_x == 12345 &&
output.state.button_east && output.state.button_west &&
!output.state.button_south,
"independent looping macro and Burst sources did not start");
++database_generation;
output = runtime_transform(0, snapshot, 2);
require(output.state.left_stick_x == 0 && output.state.button_south &&
!output.state.button_east && !output.state.button_west,
"refresh phantom-restarted a held edge-triggered synthetic source");
apply_button_mask(0, &snapshot);
(void)runtime_transform(0, snapshot, 3);
apply_button_mask(7u, &snapshot);
output = runtime_transform(0, snapshot, 4);
require(output.state.left_stick_x == 12345 &&
output.state.button_east && output.state.button_west,
"synthetic sources could not rearm after refresh and release");
snapshot.active = false;
(void)runtime_transform(0, snapshot, 5);
snapshot.active = true;
apply_button_mask((1u << 9) | 8u, &snapshot);
output = runtime_transform(0, snapshot, 6);
require(activation_attempt_count == 0 &&
controller_profile_extract_button_mask(output.state) == 0 &&
output.state.left_stick_x == 0,
"reconnect treated an already-held shortcut as a new command");
apply_button_mask(0, &snapshot);
(void)runtime_transform(0, snapshot, 7);
apply_button_mask((1u << 9) | 8u, &snapshot);
(void)runtime_transform(0, snapshot, 8);
require(activation_attempt_count == 1 &&
activation_attempts[0].profile_index == 2,
"reconnected shortcut did not rearm on physical release");
}
void test_shortcut_selector_rollover_without_modifier_release() {
prepare_profiles();
auto& profile = rows[0].profiles[0];
profile.shortcuts.modifier = 9;
profile.shortcuts.selectors[1] = 0;
profile.shortcuts.selectors[2] = 1;
auto snapshot = make_snapshot(0);
(void)runtime_transform(0, snapshot, 0);
apply_button_mask((1u << 9) | 1u, &snapshot);
(void)runtime_transform(0, snapshot, 1);
apply_button_mask((1u << 9) | 2u, &snapshot);
const auto output = runtime_transform(0, snapshot, 2);
require(activation_attempt_count == 2 &&
activation_attempts[0].profile_index == 1 &&
activation_attempts[1].profile_index == 2 &&
controller_profile_extract_button_mask(output.state) == 0,
"swapping shortcut selectors while holding the modifier lost the new action");
}
void test_extra_hotkeys_consume_mappings_and_rearm() {
prepare_profiles();
auto& profile = rows[0].profiles[0];
profile.switching_chord = (1u << 18) | (1u << 24);
profile.motion_toggle_chord = (1u << 19) | (1u << 16);
profile.shortcuts.modifier = 20;
profile.shortcuts.selectors[7] = 0;
profile.extra_button_map[0] = 3;
profile.extra_button_map[1] = 1;
profile.extra_button_map[2] = 2;
profile.extra_button_map[6] = 17;
Bluepad32SlotSnapshot snapshot = make_snapshot(0, 9);
(void)runtime_transform(0, snapshot, 0);
snapshot.state.extra_buttons = 1;
auto output = runtime_transform(0, snapshot, 1);
require(activation_attempt_count == 0 && output.state.button_north,
"partial extra switching chord was swallowed or activated");
snapshot.state.extra_buttons = 0x41;
output = runtime_transform(0, snapshot, 2);
require(activation_attempt_count == 1 &&
activation_attempts[0].profile_index == 1 &&
controller_profile_extract_button_mask(output.state) == 0 &&
output.state.right_trigger == 0 && output.state.extra_buttons == 0,
"extra switching chord did not select the next profile and consume mappings");
(void)runtime_transform(0, snapshot, 3);
require(activation_attempt_count == 1, "held extra switching chord retriggered");
snapshot.state = controller_neutral_state();
(void)runtime_transform(0, snapshot, 4);
snapshot.state.extra_buttons = 2;
snapshot.state.left_trigger = UINT16_MAX;
output = runtime_transform(0, snapshot, 5);
require(motion_toggle_count == 1 && last_motion_toggle_slot == 0 &&
last_motion_toggle_connection_generation == 9 &&
!output.state.button_east && output.state.left_trigger == 0,
"extra/trigger motion chord lost its logical slot or leaked mapped inputs");
(void)runtime_transform(0, snapshot, 6);
require(motion_toggle_count == 1, "held extra motion chord retriggered");
snapshot.state = controller_neutral_state();
(void)runtime_transform(0, snapshot, 7);
apply_button_mask(1, &snapshot);
snapshot.state.extra_buttons = 4;
output = runtime_transform(0, snapshot, 8);
require(activation_attempt_count == 2 &&
activation_attempts[1].profile_index == 7 &&
controller_profile_extract_button_mask(output.state) == 0,
"extra shortcut modifier failed to select profile eight or leaked inputs");
apply_button_mask(0, &snapshot);
(void)runtime_transform(0, snapshot, 9);
snapshot.state.extra_buttons = 0x41;
(void)runtime_transform(0, snapshot, 10);
require(activation_attempt_count == 3 &&
activation_attempts[2].profile_index == 1,
"released extra switching chord did not rearm");
}
void test_accelerometer_swing_requires_evidence_and_settle() {
WiiSwingDetector detector;
WiiAccelerometerSample sample{};
sample.valid = true;
uint32_t now = UINT32_MAX - 100u;
auto feed = [&](int16_t x, int16_t y, int16_t z) {
now += 10;
sample = {x, y, z, sample.sequence + 1, now, true};
return detector.update(sample, now, 1, true);
};
for (int i = 0; i < 20; ++i)
require(!feed(0, 0, 4096), "resting gravity armed a swing output");
for (int degrees = 0; degrees <= 90; degrees += 3) {
const double angle = degrees * 0.017453292519943;
require(!feed(static_cast<int16_t>(4096 * std::sin(angle)), 0,
static_cast<int16_t>(4096 * std::cos(angle))),
"ordinary rotation of gravity triggered a sword swing");
}
for (int i = 0; i < 40; ++i) feed(4096, 0, 0);
require(!feed(4096, 10000, 0), "a single acceleration spike triggered a swing");
for (int i = 0; i < 5; ++i) {
now += 2;
require(!detector.update(sample, now, 1, true),
"repeated reads of one sample accumulated swing evidence");
}
require(feed(4096, 10000, 0), "sustained acceleration did not produce a swing");
for (int i = 0; i < 50; ++i) {
const bool pressed = feed(4096, 10000, 0);
if (i >= 8) require(!pressed, "continuous shaking retriggered without settling");
}
for (int i = 0; i < 35; ++i)
require(!feed(4096, 0, 0), "settling generated a second button pulse");
require(!feed(4096, -10000, 0) && feed(4096, -10000, 0),
"settled detector did not accept an opposite-direction swing");
require(!detector.update(sample, now + 151, 1, true),
"stale acceleration retained a button press");
}
void test_swing_output_isolated_from_motion_remaps_and_profiles() {
prepare_profiles();
rows[0].profiles[0].swing.button = 2; // Final logical west / Switch Y.
rows[0].profiles[0].button_map[2] = 1;
rows[1].profiles[0].swing.button = 2;
auto snapshot = make_snapshot(0);
snapshot.state.button_south = true;
snapshot.pre_hotkey_button_mask = 1;
uint32_t now = 0;
auto feed = [&](int16_t x) {
now += 10;
snapshot.accelerometer = {x, 0, 4096, snapshot.accelerometer.sequence + 1, now, true};
return runtime_transform(0, snapshot, now);
};
for (int i = 0; i < 20; ++i) feed(0);
feed(10000);
auto output = feed(10000);
require(output.state.button_west && output.state.button_south &&
!output.state.button_east && output.state.motion_sample_count == 0,
"accelerometer swing required gyro output, remapped its target, or lost physical input");
require(!runtime_transform(1, make_snapshot(1), now).state.button_west,
"a swing leaked to another controller slot");
snapshot.state.button_west = true;
rows[0].profiles[0].button_map[2] = 2;
++database_generation;
output = runtime_transform(0, snapshot, now + 1);
require(output.state.button_west, "gesture cancellation released a physical target button");
snapshot.state.button_west = false;
rows[0].profiles[0].swing.button = 3;
++database_generation;
output = runtime_transform(0, snapshot, now + 2);
require(!output.state.button_west && !output.state.button_north,
"profile refresh replayed a swing onto old or new target");
}
void test_swing_modifier_release_cancels_and_requires_fresh_settle() {
prepare_profiles();
rows[0].profiles[0].swing = {2, 1, 0};
auto snapshot = make_snapshot(0);
uint32_t now = 0;
auto feed = [&](int16_t x, bool held) {
now += 10;
apply_button_mask(held ? 1 : 0, &snapshot);
snapshot.accelerometer = {x, 0, 4096, snapshot.accelerometer.sequence + 1, now, true};
return runtime_transform(0, snapshot, now);
};
for (int i = 0; i < 20; ++i) feed(0, true);
feed(10000, true);
require(feed(10000, true).state.button_west, "held modifier failed to allow a swing");
require(!feed(10000, false).state.button_west, "modifier release retained swing output");
require(!feed(10000, true).state.button_west, "repressing modifier during motion retriggered");
for (int i = 0; i < 60; ++i) feed(0, true);
feed(10000, true);
require(feed(10000, true).state.button_west, "settling after modifier release did not rearm");
snapshot.active = false;
require(!runtime_transform(0, snapshot, now + 1).state.button_west,
"disconnect retained gesture output");
}
void test_back_and_forth_swings_do_not_require_full_stops() {
prepare_profiles();
rows[0].profiles[0].swing.button = 2;
auto snapshot = make_snapshot(0);
uint32_t now = UINT32_MAX - 100u;
unsigned presses = 0;
bool held = false;
auto feed = [&](int16_t force) {
now += 10;
snapshot.accelerometer = {force, 0, 4096, snapshot.accelerometer.sequence + 1, now, true};
const bool pressed = runtime_transform(0, snapshot, now).state.button_west;
if (pressed && !held) ++presses;
held = pressed;
};
for (int i = 0; i < 20; ++i) feed(0);
for (int stroke = 0; stroke < 6; ++stroke) {
const int direction = stroke % 2 == 0 ? 1 : -1;
for (int i = 0; i < 24; ++i) feed(direction * 10000);
// An 80ms lower-force transition, not a 120ms full stop.
for (int i = 0; i < 8; ++i) feed(direction * 2000);
require(presses == static_cast<unsigned>(stroke + 1),
"each back-and-forth stroke must press once without coming to rest");
}
}
void test_early_rebound_cannot_become_a_delayed_swing() {
WiiSwingDetector detector;
WiiAccelerometerSample sample{};
uint32_t now = 0;
unsigned presses = 0;
bool held = false;
auto feed = [&](int16_t force) {
now += 10;
sample = {force, 0, 4096, sample.sequence + 1, now, true};
const bool pressed = detector.update(sample, now, 1, true);
if (pressed && !held) ++presses;
held = pressed;
};
for (int i = 0; i < 20; ++i) feed(0);
for (int i = 0; i < 3; ++i) feed(10000);
require(presses == 1, "initial swing must trigger");
for (int i = 0; i < 5; ++i) feed(0);
for (int i = 0; i < 40; ++i) feed(-10000);
require(presses == 1,
"early rebound must not fire after cooldown while its force remains high");
for (int i = 0; i < 5; ++i) feed(0);
for (int i = 0; i < 3; ++i) feed(10000);
require(presses == 2, "a new stroke after cooldown and release must trigger");
}
void test_combined_swings_have_priority_at_window_boundary() {
for (bool nunchuk_first : {false, true}) {
for (unsigned delay : {0u, 100u, 110u}) {
WiiSwingGestures gestures;
uint32_t now = UINT32_MAX - 100u;
uint32_t sequence = 0;
unsigned counts[3]{};
auto feed = [&](int16_t first, int16_t second) {
now += 10;
++sequence;
WiiAccelerometerSample remote{
nunchuk_first ? second : first, 0, 4096, sequence, now, true};
WiiAccelerometerSample nunchuk{
nunchuk_first ? first : second, 0, 4096, sequence, now, true};
auto result = gestures.update(remote, nunchuk, now, 1, 1, 7, 100);
for (unsigned i = 0; i < 3; ++i)
if (result.started & (1u << i)) ++counts[i];
return result;
};
for (unsigned i = 0; i < 20; ++i) feed(0, 0);
for (unsigned step = 0; step < 40; ++step) {
auto result = feed(10000, step * 10 >= delay ? 10000 : 0);
if (delay <= 100)
require((result.active & 3u) == 0, "combined gesture leaked individual buttons");
}
if (delay <= 100)
require(counts[2] == 1 && counts[0] == 0 && counts[1] == 0,
"simultaneous/in-window strokes must start only the combined action");
else
require(counts[2] == 0 && counts[0] == 1 && counts[1] == 1,
"out-of-window strokes must remain two individual actions");
}
}
}
void test_combined_pending_cancels_on_detach_and_modifier_release() {
for (bool detach : {false, true}) {
WiiSwingGestures gestures;
uint32_t now = 0, sequence = 0;
auto feed = [&](int16_t force, uint8_t allowed, bool nunchuk_valid) {
now += 10;
++sequence;
WiiAccelerometerSample remote{force, 0, 4096, sequence, now, true};
WiiAccelerometerSample nunchuk{0, 0, 4096, sequence, now, nunchuk_valid};
return gestures.update(remote, nunchuk, now, 1, 1, allowed, 100);
};
for (unsigned i = 0; i < 20; ++i) feed(0, 7, true);
feed(10000, 7, true);
require(feed(10000, 7, true).started == 0, "single must wait while combined is eligible");
for (unsigned i = 0; i < 20; ++i)
require(feed(10000, detach ? 7 : 3, !detach).started == 0,
"detaching or releasing combined modifier replayed a pending action");
}
WiiSwingGestures remote_only;
WiiAccelerometerSample missing{};
WiiAccelerometerSample sample{0, 0, 4096, 0, 0, true};
for (unsigned i = 0; i < 20; ++i) {
sample.timestamp_ms += 10;
++sample.sequence;
remote_only.update(sample, missing, sample.timestamp_ms, 1, 1, 7, 100);
}
sample.x = 10000;
++sample.sequence;
sample.timestamp_ms += 10;
remote_only.update(sample, missing, sample.timestamp_ms, 1, 1, 7, 100);
++sample.sequence;
sample.timestamp_ms += 10;
require(remote_only.update(sample, missing, sample.timestamp_ms, 1, 1, 7, 100).started == 1,
"absent Nunchuk must not delay the Remote gesture");
}
void test_nunchuk_button_and_combined_macro_runtime() {
prepare_profiles();
auto& profile = rows[0].profiles[0];
profile.swing.button = 2;
profile.nunchuk_swing.button = 1;
profile.combined_swing.macro = 0;
profile.macros[0].step_count = 1;
profile.macros[0].mode = ControllerProfileMacroMode::kWhileHeld;
profile.macros[0].trigger_mask = 1; // Not held during the combined gesture.
profile.macro_step_count = 1;
profile.macro_steps[0].override_flags = kControllerProfileOverrideButtons;
profile.macro_steps[0].duration_ms = 100;
profile.macro_steps[0].output_button_mask = 8;
for (unsigned i = 1; i < CONTROLLER_PROFILE_MACRO_COUNT; ++i)
profile.macros[i].first_step = 1;
auto snapshot = make_snapshot(0);
uint32_t now = 0, sequence = 0;
auto feed = [&](int16_t remote, int16_t nunchuk) {
now += 10;
++sequence;
snapshot.accelerometer = {remote, 0, 4096, sequence, now, true};
snapshot.nunchuk_accelerometer = {nunchuk, 0, 4096, sequence, now, true};
return runtime_transform(0, snapshot, now);
};
for (unsigned i = 0; i < 20; ++i) feed(0, 0);
feed(10000, 10000);
auto result = feed(10000, 10000);
require(result.state.button_north && !result.state.button_west && !result.state.button_east,
"combined gesture must start its macro instead of both individual buttons");
for (unsigned i = 0; i < 8; ++i)
require(feed(0, 0).state.button_north, "gesture macro stopped when physical trigger was absent");
feed(0, 0);
require(!feed(0, 0).state.button_north, "gesture macro must finish exactly one cycle");
for (unsigned i = 0; i < 30; ++i) feed(0, 0);
feed(0, 10000);
require(!feed(0, 10000).state.button_east, "Nunchuk alone must wait for combination window");
for (unsigned i = 0; i < 10; ++i) feed(0, 10000);
result = feed(0, 10000);
require(result.state.button_east && !result.state.button_west && !result.state.button_north,
"Nunchuk-only gesture must produce its configured button after the window");
}
void test_gesture_macros_rearm_during_continuous_swings() {
prepare_profiles();
auto& profile = rows[0].profiles[0];
profile.swing.macro = 0;
profile.macros[0].step_count = 1;
profile.macros[0].mode = ControllerProfileMacroMode::kRepeat;
profile.macros[0].repeat_count = 5;
profile.macro_step_count = 1;
profile.macro_steps[0].override_flags = kControllerProfileOverrideButtons;
profile.macro_steps[0].duration_ms = 100;
profile.macro_steps[0].output_button_mask = 8;
auto snapshot = make_snapshot(0);
uint32_t now = 0;
unsigned starts = 0;
bool held = false;
auto feed = [&](int16_t force) {
now += 10;
snapshot.accelerometer = {force, 0, 4096, snapshot.accelerometer.sequence + 1, now, true};
const bool pressed = runtime_transform(0, snapshot, now).state.button_north;
if (pressed && !held) ++starts;
held = pressed;
};
for (unsigned i = 0; i < 20; ++i) feed(0);
for (unsigned stroke = 0; stroke < 4; ++stroke) {
const int direction = stroke % 2 ? -1 : 1;
for (unsigned i = 0; i < 24; ++i) feed(direction * 10000);
for (unsigned i = 0; i < 8; ++i) feed(direction * 2000);
require(starts == stroke + 1 && !held,
"gesture macro must play once per stroke without requiring a full stop");
}
}
struct CombinedConfirmationRig {
WiiSwingGestures gestures;
uint32_t now = 1000, sequence = 0;
unsigned counts[3]{};
CombinedConfirmationRig() {
for (unsigned i = 0; i < 20; ++i) feed(0, 0);
}
WiiSwingGestureResult feed(int16_t remote_force, int16_t nunchuk_force,
uint8_t allowed = 7) {
now += 10;
++sequence;
WiiAccelerometerSample remote{remote_force, 0, 4096, sequence, now, true};
WiiAccelerometerSample nunchuk{nunchuk_force, 0, 4096, sequence, now, true};
const auto result = gestures.update(remote, nunchuk, now, 1, 1, allowed, 100);
for (unsigned i = 0; i < 3; ++i)
if (result.started & (1u << i)) ++counts[i];
return result;
}
};
void test_combined_accepts_smaller_motion_before_or_after_full_swing() {
for (bool weak_nunchuk : {false, true}) {
for (int delay : {-60, 0, 60}) {
CombinedConfirmationRig rig;
for (int step = 0; step < 40; ++step) {
const int strong_start = delay < 0 ? -delay : 0;
const int weak_start = delay > 0 ? delay : 0;
const int16_t strong = step * 10 >= strong_start ? 10000 : 0;
const int16_t weak = step * 10 >= weak_start ? 4500 : 0;
const auto result = rig.feed(weak_nunchuk ? strong : weak,
weak_nunchuk ? weak : strong);
require((result.active & 3) == 0, "confirmed combined motion leaked a single action");
}
require(rig.counts[2] == 1 && rig.counts[0] == 0 && rig.counts[1] == 0,
"one full swing plus smaller companion motion must combine in either order");
}
}
}
void test_confirmation_does_not_weaken_individuals_or_accept_noise() {
for (uint8_t allowed : {uint8_t{3}, uint8_t{7}}) {
CombinedConfirmationRig rig;
for (unsigned i = 0; i < 40; ++i) rig.feed(4500, 4500, allowed);
require(rig.counts[0] == 0 && rig.counts[1] == 0 && rig.counts[2] == 0,
"two subthreshold movements must not generate any action");
}
for (bool spike : {false, true}) {
CombinedConfirmationRig rig;
for (unsigned i = 0; i < 40; ++i)
rig.feed(10000, spike && i == 2 ? 4500 : 0);
require(rig.counts[0] == 1 && rig.counts[1] == 0 && rig.counts[2] == 0,
"a stationary companion or one noisy sample must not confirm a combined swing");
}
}
void test_consumed_confirmation_suppresses_late_full_swing() {
CombinedConfirmationRig rig;
for (unsigned i = 0; i < 45; ++i)
rig.feed(10000, i < 8 ? 4500 : 10000);
require(rig.counts[2] == 1 && rig.counts[0] == 0 && rig.counts[1] == 0,
"a confirmation growing into a full swing must not leak a later individual action");
}
void test_expired_or_discarded_confirmation_cannot_be_reused() {
for (bool discard : {false, true}) {
CombinedConfirmationRig rig;
for (unsigned i = 0; i < 3; ++i) rig.feed(0, 4500);
if (discard) rig.gestures.discard_actions();
else for (unsigned i = 0; i < 12; ++i) rig.feed(0, 0);
for (unsigned i = 0; i < 40; ++i) rig.feed(10000, 0);
require(rig.counts[0] == 1 && rig.counts[1] == 0 && rig.counts[2] == 0,
"expired or macro-discarded evidence must not confirm a later swing");
}
}
} // namespace
bool bluepad32_input_backend_toggle_motion(
uint8_t slot, uint32_t connection_generation) {
last_motion_toggle_slot = slot;
last_motion_toggle_connection_generation = connection_generation;
++motion_toggle_count;
return true;
}
uint32_t profile_service_database_generation() {
return 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 activation_result;
}
void profile_service_active_profile_snapshot(
const ControllerIdentity& identity,
ProfileServiceActiveProfileSnapshot* output) {
if (output == nullptr) {
return;
}
++active_snapshot_count;
*output = {};
output->metadata.state = ProfileServiceState::kReady;
output->metadata.generation = database_generation;
for (const FakeProfileRow& row : rows) {
if (!controller_identity_equal(row.identity, identity)) {
continue;
}
output->valid = true;
output->profile_index = row.active_profile;
output->profile = row.profiles[row.active_profile];
return;
}
}
int main() {
test_four_slot_cache_and_unchanged_generation();
test_activation_disconnect_and_default_preservation();
test_analog_thresholds_rumble_and_local_confirmation();
test_initial_profile_indication_once_per_connection();
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_profile_motion_toggle_supports_trigger_chords();
test_switching_slot_isolation();
test_all_runtime_cancellation_causes();
test_runtime_slot_synthetic_isolation();
test_shortcut_arbitration_latching_and_commit_feedback();
test_busy_shortcut_target_and_generation_isolation();
test_cycle_motion_arbitration_and_held_refresh();
test_shift_slot_and_context_resets();
test_held_synthetic_sources_and_disconnect_rearming();
test_shortcut_selector_rollover_without_modifier_release();
test_extra_hotkeys_consume_mappings_and_rearm();
test_accelerometer_swing_requires_evidence_and_settle();
test_swing_output_isolated_from_motion_remaps_and_profiles();
test_swing_modifier_release_cancels_and_requires_fresh_settle();
test_back_and_forth_swings_do_not_require_full_stops();
test_early_rebound_cannot_become_a_delayed_swing();
test_combined_swings_have_priority_at_window_boundary();
test_combined_pending_cancels_on_detach_and_modifier_release();
test_nunchuk_button_and_combined_macro_runtime();
test_gesture_macros_rearm_during_continuous_swings();
test_combined_accepts_smaller_motion_before_or_after_full_swing();
test_confirmation_does_not_weaken_individuals_or_accept_noise();
test_consumed_confirmation_suppresses_late_full_swing();
test_expired_or_discarded_confirmation_cannot_be_reused();
return 0;
}