Add sparse multi-macro profiles
This commit is contained in:
parent
eb03d94183
commit
ce93f34d94
13 changed files with 1250 additions and 593 deletions
|
|
@ -396,19 +396,17 @@ void test_initial_profile_indication_once_per_connection() {
|
|||
void test_default_switching_retry_commit_and_feedback() {
|
||||
prepare_profiles();
|
||||
ControllerProfile& initial_profile = rows[0].profiles[0];
|
||||
initial_profile.macro_trigger_mask =
|
||||
initial_profile.macros[0].trigger_mask =
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kLeftShoulder);
|
||||
initial_profile.macro_step_count = 2;
|
||||
initial_profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
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);
|
||||
initial_profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 0);
|
||||
|
|
@ -533,19 +531,17 @@ void test_identity_promotion_preserves_held_switching() {
|
|||
}
|
||||
|
||||
void configure_motion_suppression_probe(ControllerProfile* profile) {
|
||||
profile->macro_trigger_mask =
|
||||
profile->macros[0].trigger_mask =
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kDpadUp);
|
||||
profile->macro_step_count = 2;
|
||||
profile->macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
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);
|
||||
profile->macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
}
|
||||
|
||||
void test_switching_uses_pre_hotkey_buttons_only() {
|
||||
|
|
@ -723,22 +719,19 @@ void test_profile_motion_toggle_supports_trigger_chords() {
|
|||
void configure_synthetic_profile(uint8_t slot) {
|
||||
ControllerProfile& profile = rows[slot].profiles[0];
|
||||
profile = controller_profile_default(rows[slot].identity, 0);
|
||||
profile.macro_trigger_mask =
|
||||
profile.macros[0].trigger_mask =
|
||||
logical_button_bit(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_cancel =
|
||||
profile.macros[0].cancel_control =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kCapture);
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
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.macro_steps[1] = {};
|
||||
profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
profile.turbo_modes[static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kEast)] =
|
||||
ControllerProfileTurboMode::kAutoBurst;
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ void test_profile_wire_schema() {
|
|||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"default profile did not encode");
|
||||
require(encoded[0] == 4 && encoded[1] == 0 &&
|
||||
require(encoded[0] == 5 && encoded[1] == 0 &&
|
||||
encoded[2] == 0 && encoded[3] == 1,
|
||||
"profile header is not little-endian v4/256");
|
||||
"profile header is not little-endian v5/256");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
require(encoded[4 + index] == index,
|
||||
|
|
@ -70,15 +70,13 @@ void test_profile_wire_schema() {
|
|||
encoded[71] == 0,
|
||||
"default trigger mappings are not identity");
|
||||
require(encoded[72] == 0xff && encoded[73] == 0xff &&
|
||||
encoded[74] == 3 && encoded[78] == 0 &&
|
||||
encoded[79] == 0 && encoded[80] == 1 &&
|
||||
encoded[81] == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"default rumble or macro encoding changed");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
require(encoded[100 + index * 19] == 1,
|
||||
"unused macro step is not canonical end");
|
||||
}
|
||||
encoded[74] == 3 && encoded[75] == 0 &&
|
||||
encoded[78] == 0 && encoded[79] == 0,
|
||||
"default rumble or action encoding changed");
|
||||
require(encoded[96] == 0 && encoded[97] == 0 &&
|
||||
encoded[98] == 0x7c && encoded[99] == 0 &&
|
||||
encoded[100] == 0 && encoded[101] == 0,
|
||||
"default sparse macro descriptor changed");
|
||||
|
||||
ControllerProfile decoded{};
|
||||
require(controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
|
|
@ -88,29 +86,50 @@ void test_profile_wire_schema() {
|
|||
(1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL) |
|
||||
(1u << static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kSouth));
|
||||
action_profile.macro_trigger_mask =
|
||||
(1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL) |
|
||||
(1u << CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL);
|
||||
action_profile.motion_toggle_chord =
|
||||
(1u << CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL);
|
||||
action_profile.macro_cancel =
|
||||
action_profile.macros[0].trigger_mask =
|
||||
(1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL) |
|
||||
(1u << CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL);
|
||||
action_profile.macros[0].cancel_control =
|
||||
CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL;
|
||||
action_profile.macros[0].first_step = 0;
|
||||
action_profile.macros[0].step_count = 1;
|
||||
action_profile.macro_step_count = 1;
|
||||
action_profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
for (uint8_t macro_index = 1;
|
||||
macro_index < CONTROLLER_PROFILE_MACRO_COUNT; ++macro_index) {
|
||||
action_profile.macros[macro_index].first_step = 1;
|
||||
}
|
||||
action_profile.macro_steps[0].duration_ms = 25;
|
||||
action_profile.macro_steps[0].output_button_mask = 1;
|
||||
uint8_t action_encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(
|
||||
action_profile, action_encoded, sizeof(action_encoded)) &&
|
||||
action_encoded[75] == 0x2d &&
|
||||
action_encoded[81] ==
|
||||
CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL &&
|
||||
controller_profile_decode(
|
||||
action_encoded, sizeof(action_encoded), &decoded) &&
|
||||
decoded.switching_chord ==
|
||||
action_profile, action_encoded, sizeof(action_encoded)),
|
||||
"sparse action profile did not encode");
|
||||
require(action_encoded[75] == 0x21,
|
||||
"sparse action extension encoding changed");
|
||||
require(action_encoded[96] == 0 &&
|
||||
action_encoded[97] == 0 &&
|
||||
action_encoded[98] == 0x43,
|
||||
"sparse macro binding encoding changed");
|
||||
require(action_encoded[100] == 1 &&
|
||||
action_encoded[101] == 5,
|
||||
"sparse macro step bounds changed");
|
||||
require(controller_profile_decode(
|
||||
action_encoded, sizeof(action_encoded), &decoded),
|
||||
"sparse action profile did not decode");
|
||||
require(decoded.switching_chord ==
|
||||
action_profile.switching_chord &&
|
||||
decoded.motion_toggle_chord ==
|
||||
action_profile.motion_toggle_chord &&
|
||||
decoded.macro_trigger_mask ==
|
||||
action_profile.macro_trigger_mask &&
|
||||
decoded.macro_cancel == action_profile.macro_cancel,
|
||||
"trigger-backed action masks did not round-trip");
|
||||
decoded.macros[0].trigger_mask ==
|
||||
action_profile.macros[0].trigger_mask &&
|
||||
decoded.macros[0].cancel_control ==
|
||||
action_profile.macros[0].cancel_control &&
|
||||
decoded.macros[0].step_count == 1,
|
||||
"sparse trigger-backed macro did not round-trip");
|
||||
encoded[252] = 1;
|
||||
require(!controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"nonzero reserved profile byte was accepted");
|
||||
|
|
@ -159,19 +178,18 @@ void test_profile_wire_schema() {
|
|||
require(!controller_profile_validate(invalid),
|
||||
"invalid Turbo mode was accepted");
|
||||
invalid = profile;
|
||||
invalid.macro_step_count = 2;
|
||||
invalid.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
invalid.macros[0].first_step = 0;
|
||||
invalid.macros[0].step_count = 1;
|
||||
invalid.macro_step_count = 1;
|
||||
invalid.macro_steps[0].duration_ms =
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS + 1;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"unbounded macro wait was accepted");
|
||||
invalid.macro_steps[0].duration_ms =
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS;
|
||||
invalid.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
invalid.macros[1].first_step = 0;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"macro without a final end was accepted");
|
||||
"noncanonical shared macro pool was accepted");
|
||||
}
|
||||
|
||||
void test_legacy_profile_migration() {
|
||||
|
|
@ -189,19 +207,9 @@ void test_legacy_profile_migration() {
|
|||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"migrated default profile did not encode");
|
||||
for (size_t index = 0; index < sizeof(encoded); ++index) {
|
||||
const bool migration_byte =
|
||||
index == 0 ||
|
||||
(index >= 58 && index < 60) ||
|
||||
index == 60 ||
|
||||
(index >= 68 && index < 70) ||
|
||||
index == 70 ||
|
||||
index == 78 || index == 79 || index == 81;
|
||||
if (!migration_byte) {
|
||||
require(encoded[index] == kLegacyDefaultProfile[index],
|
||||
"legacy default profile changed an unrelated byte");
|
||||
}
|
||||
}
|
||||
require(migrated.macros[0].step_count == 0 &&
|
||||
migrated.macro_step_count == 0,
|
||||
"legacy end marker was not removed during migration");
|
||||
require(encoded[0] == CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
encoded[58] ==
|
||||
static_cast<uint8_t>(
|
||||
|
|
@ -216,10 +224,8 @@ void test_legacy_profile_migration() {
|
|||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8) &&
|
||||
encoded[60] == CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL &&
|
||||
encoded[70] == CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL &&
|
||||
encoded[78] == 0 && encoded[79] == 0 &&
|
||||
encoded[81] == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"migrated default profile did not encode as v4");
|
||||
encoded[70] == CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL,
|
||||
"migrated default profile did not encode as v5");
|
||||
|
||||
require(controller_profile_decode(
|
||||
kLegacyNarrowRawRangeProfile,
|
||||
|
|
@ -236,20 +242,6 @@ void test_legacy_profile_migration() {
|
|||
"legacy narrow raw range or inherited threshold was not migrated");
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"migrated narrow-raw-range profile did not encode");
|
||||
for (size_t index = 0; index < sizeof(encoded); ++index) {
|
||||
const bool migration_byte =
|
||||
index == 0 ||
|
||||
(index >= 58 && index < 60) ||
|
||||
index == 60 ||
|
||||
(index >= 68 && index < 70) ||
|
||||
index == 70 ||
|
||||
index == 78 || index == 79 || index == 81;
|
||||
if (!migration_byte) {
|
||||
require(
|
||||
encoded[index] == kLegacyNarrowRawRangeProfile[index],
|
||||
"narrow-raw-range migration changed unrelated profile data");
|
||||
}
|
||||
}
|
||||
|
||||
require(controller_profile_decode(
|
||||
kLegacyCustomThresholdProfile,
|
||||
|
|
@ -267,58 +259,48 @@ void test_legacy_profile_migration() {
|
|||
"legacy custom-threshold profile did not re-encode");
|
||||
const uint8_t legacy_macro_trigger =
|
||||
kLegacyCustomThresholdProfile[78];
|
||||
const uint16_t expected_macro_trigger =
|
||||
const uint32_t expected_macro_trigger =
|
||||
legacy_macro_trigger < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT
|
||||
? static_cast<uint16_t>(1u << legacy_macro_trigger)
|
||||
? static_cast<uint32_t>(1u << legacy_macro_trigger)
|
||||
: 0;
|
||||
require(migrated.macro_trigger_mask == expected_macro_trigger &&
|
||||
migrated.macro_cancel ==
|
||||
require(migrated.macros[0].trigger_mask ==
|
||||
expected_macro_trigger &&
|
||||
migrated.macros[0].cancel_control ==
|
||||
kLegacyCustomThresholdProfile[79],
|
||||
"legacy macro trigger was not migrated to a chord");
|
||||
for (size_t index = 1; index < sizeof(encoded); ++index) {
|
||||
const bool migration_byte =
|
||||
index == 60 || index == 70 ||
|
||||
index == 78 || index == 79 || index == 81;
|
||||
if (!migration_byte) {
|
||||
require(encoded[index] ==
|
||||
kLegacyCustomThresholdProfile[index],
|
||||
"legacy custom-threshold profile changed data");
|
||||
}
|
||||
}
|
||||
"legacy macro trigger was not migrated to descriptor zero");
|
||||
|
||||
const ControllerProfile previous_source =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
uint8_t previous_encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(
|
||||
previous_source, previous_encoded,
|
||||
sizeof(previous_encoded)),
|
||||
"previous-schema fixture did not encode");
|
||||
memcpy(previous_encoded, kLegacyDefaultProfile,
|
||||
sizeof(previous_encoded));
|
||||
previous_encoded[0] = static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_TRIGGER_THRESHOLD_SCHEMA_VERSION);
|
||||
previous_encoded[60] = 0;
|
||||
previous_encoded[70] = 0;
|
||||
previous_encoded[58] = static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
previous_encoded[59] = static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8);
|
||||
previous_encoded[68] = static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
previous_encoded[69] = static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8);
|
||||
previous_encoded[78] = static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kSouth);
|
||||
previous_encoded[79] = static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kCapture);
|
||||
previous_encoded[81] = 0;
|
||||
previous_encoded[98] = 0;
|
||||
previous_encoded[99] = 0;
|
||||
require(controller_profile_decode(
|
||||
previous_encoded, sizeof(previous_encoded), &migrated) &&
|
||||
migrated.triggers[0].output ==
|
||||
CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL &&
|
||||
migrated.triggers[1].output ==
|
||||
CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL &&
|
||||
migrated.macro_trigger_mask ==
|
||||
static_cast<uint16_t>(
|
||||
migrated.macros[0].trigger_mask ==
|
||||
static_cast<uint32_t>(
|
||||
1u << static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kSouth)) &&
|
||||
migrated.macro_cancel ==
|
||||
migrated.macros[0].cancel_control ==
|
||||
static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kCapture) &&
|
||||
migrated.motion_toggle_chord == 0,
|
||||
"v2 profile controls did not migrate to v4");
|
||||
"v2 profile controls did not migrate to v5");
|
||||
|
||||
ControllerProfile current =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
|
|
|
|||
|
|
@ -39,36 +39,28 @@ ControllerProfile profile_with_macro(
|
|||
ControllerProfileLogicalButton::kCapture) {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
profile.macro_trigger_mask = button_bit(trigger);
|
||||
profile.macro_cancel = button_index(cancel);
|
||||
profile.macros[0].trigger_mask = button_bit(trigger);
|
||||
profile.macros[0].cancel_control = button_index(cancel);
|
||||
profile.macros[0].first_step = 0;
|
||||
return profile;
|
||||
}
|
||||
|
||||
void set_end(ControllerProfile* profile, uint8_t index) {
|
||||
profile->macro_steps[index] = {};
|
||||
profile->macro_steps[index].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
}
|
||||
|
||||
void test_immediate_press_release_dpad_and_explicit_end() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_step_count = 3;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_step_count = 2;
|
||||
profile.macros[0].step_count = 2;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 10;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth) |
|
||||
button_bit(ControllerProfileLogicalButton::kDpadUp);
|
||||
profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[1].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[1].duration_ms = 20;
|
||||
profile.macro_steps[1].output_button_mask = 0;
|
||||
set_end(&profile, 2);
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerState input =
|
||||
|
|
@ -103,20 +95,18 @@ void test_immediate_press_release_dpad_and_explicit_end() {
|
|||
void test_macro_trigger_chord_requires_every_button() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_trigger_mask =
|
||||
profile.macros[0].trigger_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||
(1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL);
|
||||
profile.macro_cancel =
|
||||
profile.macros[0].cancel_control =
|
||||
CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL;
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_step_count = 1;
|
||||
profile.macros[0].step_count = 1;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 100;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
set_end(&profile, 1);
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerState input =
|
||||
|
|
@ -151,9 +141,9 @@ void test_macro_trigger_chord_requires_every_button() {
|
|||
void test_optional_field_overrides_and_motion_preservation() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSelect);
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_step_count = 1;
|
||||
profile.macros[0].step_count = 1;
|
||||
ControllerProfileMacroStep& step = profile.macro_steps[0];
|
||||
step.type = ControllerProfileMacroStepType::kState;
|
||||
step.override_flags = kControllerProfileOverrideLeftStick |
|
||||
kControllerProfileOverrideRightStick |
|
||||
kControllerProfileOverrideLeftTrigger |
|
||||
|
|
@ -165,7 +155,6 @@ void test_optional_field_overrides_and_motion_preservation() {
|
|||
step.right_stick_y = INT16_MAX;
|
||||
step.left_trigger = 0;
|
||||
step.right_trigger = UINT16_MAX;
|
||||
set_end(&profile, 1);
|
||||
|
||||
ControllerState input = state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kSelect) |
|
||||
|
|
@ -203,29 +192,23 @@ void test_optional_field_overrides_and_motion_preservation() {
|
|||
void test_zero_max_wait_and_scheduled_catch_up() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_step_count = 4;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_step_count = 3;
|
||||
profile.macros[0].step_count = 3;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 0;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[1].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[1].duration_ms = CONTROLLER_PROFILE_MAX_WAIT_MS;
|
||||
profile.macro_steps[1].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kEast);
|
||||
profile.macro_steps[2].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[2].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[2].duration_ms = 0;
|
||||
profile.macro_steps[2].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kWest);
|
||||
set_end(&profile, 3);
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerProfileTransformResult output =
|
||||
|
|
@ -252,7 +235,8 @@ void test_zero_max_wait_and_scheduled_catch_up() {
|
|||
!context.macro_active,
|
||||
"zero-duration catch-up did not reach the explicit end");
|
||||
|
||||
profile.macro_step_count = 4;
|
||||
profile.macro_step_count = 3;
|
||||
profile.macros[0].step_count = 3;
|
||||
profile.macro_steps[0].duration_ms = 10;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
|
|
@ -277,11 +261,11 @@ void test_zero_max_wait_and_scheduled_catch_up() {
|
|||
controller_profile_extract_button_mask(output.state) == 0,
|
||||
"large time jump did not finish the bounded macro");
|
||||
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_step_count = 1;
|
||||
profile.macros[0].step_count = 1;
|
||||
profile.macro_steps[0].duration_ms = 10;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
set_end(&profile, 1);
|
||||
context = {};
|
||||
constexpr uint32_t macro_near_wrap = UINT32_MAX - 5u;
|
||||
output = controller_synthetic_input_apply(
|
||||
|
|
@ -305,15 +289,13 @@ void test_consumption_cancel_precedence_and_duplicate_contributors() {
|
|||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSelect,
|
||||
ControllerProfileLogicalButton::kCapture);
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_step_count = 1;
|
||||
profile.macros[0].step_count = 1;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 100;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kDpadLeft);
|
||||
set_end(&profile, 1);
|
||||
profile.button_map[button_index(ControllerProfileLogicalButton::kSouth)] =
|
||||
button_index(ControllerProfileLogicalButton::kNorth);
|
||||
profile.button_map[button_index(ControllerProfileLogicalButton::kEast)] =
|
||||
|
|
@ -469,6 +451,53 @@ void test_auto_burst_toggle_cancel_and_external_cancel() {
|
|||
"Auto Burst did not restart after release following cancellation");
|
||||
}
|
||||
|
||||
void test_multiple_macro_bindings_share_step_pool() {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
profile.macros[0] = {
|
||||
button_bit(ControllerProfileLogicalButton::kSouth),
|
||||
CONTROLLER_PROFILE_NO_BUTTON, 0, 1};
|
||||
profile.macros[1] = {
|
||||
button_bit(ControllerProfileLogicalButton::kEast),
|
||||
CONTROLLER_PROFILE_NO_BUTTON, 1, 1};
|
||||
profile.macros[2].first_step = 2;
|
||||
profile.macros[3].first_step = 2;
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 1;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
profile.macro_steps[1].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[1].duration_ms = 1;
|
||||
profile.macro_steps[1].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kWest);
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerProfileTransformResult output =
|
||||
controller_synthetic_input_apply(
|
||||
&context,
|
||||
state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kEast)),
|
||||
profile, 0);
|
||||
require(context.macro_active && context.macro_index == 1 &&
|
||||
has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"second macro descriptor did not execute its shared step");
|
||||
(void)controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 1);
|
||||
(void)controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 2);
|
||||
output = controller_synthetic_input_apply(
|
||||
&context,
|
||||
state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kSouth)),
|
||||
profile, 3);
|
||||
require(context.macro_active && context.macro_index == 0 &&
|
||||
has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||
"first macro descriptor did not execute independently");
|
||||
}
|
||||
|
||||
void test_four_contexts_are_isolated() {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
|
|
@ -507,6 +536,7 @@ int main() {
|
|||
test_zero_max_wait_and_scheduled_catch_up();
|
||||
test_consumption_cancel_precedence_and_duplicate_contributors();
|
||||
test_turbo_rate_release_and_uint32_wrap();
|
||||
test_multiple_macro_bindings_share_step_pool();
|
||||
test_auto_burst_toggle_cancel_and_external_cancel();
|
||||
test_four_contexts_are_isolated();
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -519,26 +519,70 @@ def custom_profile() -> config_manager.ControllerProfile:
|
|||
confirmation_policy=2,
|
||||
switching_chord=(1 << 6) | (1 << 16),
|
||||
motion_toggle_chord=(1 << 5) | (1 << 17),
|
||||
macro_trigger_mask=(1 << 0) | (1 << 16) | (1 << 17),
|
||||
macro_cancel=config_manager.LOGICAL_CONTROLS.index("right_trigger"),
|
||||
macro_steps=(
|
||||
config_manager.MacroStep(
|
||||
0,
|
||||
config_manager.MACRO_OVERRIDE_MASK,
|
||||
config_manager.PROFILE_MAXIMUM_WAIT_MS,
|
||||
(1 << 0) | (1 << 12),
|
||||
-32768,
|
||||
32767,
|
||||
-1000,
|
||||
1000,
|
||||
12345,
|
||||
54321,
|
||||
macros=(
|
||||
config_manager.ControllerMacro(
|
||||
(1 << 0) | (1 << 16) | (1 << 17),
|
||||
config_manager.LOGICAL_CONTROLS.index("right_trigger"),
|
||||
(
|
||||
config_manager.MacroStep(
|
||||
0,
|
||||
config_manager.MACRO_OVERRIDE_MASK,
|
||||
config_manager.PROFILE_MAXIMUM_WAIT_MS,
|
||||
(1 << 0) | (1 << 12),
|
||||
-32768,
|
||||
32767,
|
||||
-1000,
|
||||
1000,
|
||||
12345,
|
||||
54321,
|
||||
),
|
||||
),
|
||||
),
|
||||
config_manager.MacroStep.end(),
|
||||
config_manager.ControllerMacro(
|
||||
(1 << 1) | (1 << 2),
|
||||
config_manager.PROFILE_NONE_BUTTON,
|
||||
(
|
||||
config_manager.MacroStep(
|
||||
0, 1, 25, 1 << 3, 0, 0, 0, 0, 0, 0
|
||||
),
|
||||
),
|
||||
),
|
||||
config_manager.ControllerMacro.empty(),
|
||||
config_manager.ControllerMacro.empty(),
|
||||
),
|
||||
turbo_modes=(0, 1, 2) + (0,) * 13,
|
||||
)
|
||||
|
||||
def legacy_profile_wire(
|
||||
schema_version: int,
|
||||
macro_trigger: int = config_manager.PROFILE_NONE_BUTTON,
|
||||
macro_cancel: int = config_manager.PROFILE_NONE_BUTTON,
|
||||
) -> bytearray:
|
||||
profile = config_manager.ControllerProfile.default()
|
||||
payload = bytearray(config_manager.PROFILE_SIZE)
|
||||
struct.pack_into(
|
||||
"<HH", payload, 0, schema_version, config_manager.PROFILE_SIZE
|
||||
)
|
||||
payload[4:20] = bytes(range(len(config_manager.LOGICAL_BUTTONS)))
|
||||
payload[20:36] = profile.left_stick.to_bytes()
|
||||
payload[36:52] = profile.right_stick.to_bytes()
|
||||
payload[52:62] = profile.left_trigger.to_bytes()
|
||||
payload[62:72] = profile.right_trigger.to_bytes()
|
||||
if schema_version < config_manager.PROFILE_CONTROL_MAPPING_SCHEMA_VERSION:
|
||||
payload[60:62] = b"\x00\x00"
|
||||
payload[70:72] = b"\x00\x00"
|
||||
payload[72:75] = bytes((0xFF, 0xFF, 3))
|
||||
payload[78] = macro_trigger
|
||||
payload[79] = macro_cancel
|
||||
payload[80] = 1
|
||||
if schema_version >= config_manager.PROFILE_CONTROL_MAPPING_SCHEMA_VERSION:
|
||||
payload[81] = macro_cancel
|
||||
payload[79] = 0
|
||||
for index in range(config_manager.PROFILE_LEGACY_MACRO_STEP_CAPACITY):
|
||||
payload[100 + index * config_manager.PROFILE_MACRO_STEP_SIZE] = 1
|
||||
return payload
|
||||
|
||||
|
||||
|
||||
def test_response_validation() -> None:
|
||||
payload = make_response(config_manager.OP_INFO, b"12345678")
|
||||
|
|
@ -1120,33 +1164,25 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
config_manager.PROFILE_SCHEMA_VERSION,
|
||||
config_manager.PROFILE_SIZE,
|
||||
)
|
||||
assert encoded[75] == 0x2D
|
||||
assert encoded[75] == 0x21
|
||||
assert encoded[60:62] == bytes((17, 0))
|
||||
assert encoded[70:72] == bytes((3, 0))
|
||||
assert encoded[81] == config_manager.LOGICAL_CONTROLS.index("right_trigger")
|
||||
assert struct.unpack_from("<H", encoded, 98)[0] == (
|
||||
assert struct.unpack_from("<H", encoded, 78)[0] == (
|
||||
profile.motion_toggle_chord & 0xFFFF
|
||||
)
|
||||
assert encoded[96:102] == bytes((1, 0, 0x47, 0, 1, 17))
|
||||
assert encoded[102:108] == bytes((6, 0, 0x7C, 17, 1, 5))
|
||||
assert encoded[252:] == bytes(4)
|
||||
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
|
||||
|
||||
serialized = profile.to_json()
|
||||
assert serialized.startswith('{\n "schema_version": 4,\n "size": 256,')
|
||||
assert serialized.startswith('{\n "schema_version": 5,\n "size": 256,')
|
||||
decoded = config_manager.ControllerProfile.from_json(serialized)
|
||||
assert decoded == profile
|
||||
assert decoded.to_json() == serialized
|
||||
|
||||
legacy_default_wire = bytearray(default_wire)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
legacy_default_wire,
|
||||
0,
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION,
|
||||
legacy_default_wire = legacy_profile_wire(
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION
|
||||
)
|
||||
legacy_default_wire[60:62] = b"\x00\x00"
|
||||
legacy_default_wire[70:72] = b"\x00\x00"
|
||||
legacy_default_wire[78] = config_manager.PROFILE_NONE_BUTTON
|
||||
legacy_default_wire[79] = config_manager.PROFILE_NONE_BUTTON
|
||||
legacy_default_wire[81] = 0
|
||||
legacy_default_wire[98:100] = b"\x00\x00"
|
||||
struct.pack_into(
|
||||
|
|
@ -1198,26 +1234,18 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
== config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
|
||||
previous_wire = bytearray(default_wire)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
previous_wire,
|
||||
0,
|
||||
previous_wire = legacy_profile_wire(
|
||||
config_manager.PROFILE_TRIGGER_THRESHOLD_SCHEMA_VERSION,
|
||||
macro_trigger=0,
|
||||
macro_cancel=1,
|
||||
)
|
||||
previous_wire[60:62] = b"\x00\x00"
|
||||
previous_wire[70:72] = b"\x00\x00"
|
||||
previous_wire[78] = 0
|
||||
previous_wire[79] = 1
|
||||
previous_wire[81] = 0
|
||||
previous_wire[98:100] = b"\x00\x00"
|
||||
migrated_previous = config_manager.ControllerProfile.from_bytes(
|
||||
previous_wire
|
||||
)
|
||||
assert migrated_previous.left_trigger.output == 16
|
||||
assert migrated_previous.right_trigger.output == 17
|
||||
assert migrated_previous.macro_trigger_mask == 1
|
||||
assert migrated_previous.macro_cancel == 1
|
||||
assert migrated_previous.macros[0].trigger_mask == 1
|
||||
assert migrated_previous.macros[0].cancel_control == 1
|
||||
assert migrated_previous.motion_toggle_chord == 0
|
||||
assert migrated_previous.to_bytes()[0] == config_manager.PROFILE_SCHEMA_VERSION
|
||||
|
||||
|
|
@ -1228,7 +1256,12 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
del legacy_json_object["motion_toggle_chord"]
|
||||
del legacy_json_object["triggers"]["left"]["output"]
|
||||
del legacy_json_object["triggers"]["right"]["output"]
|
||||
legacy_json_object["macro"]["trigger"] = "south"
|
||||
legacy_json_object.pop("macros")
|
||||
legacy_json_object["macro"] = {
|
||||
"trigger": "south",
|
||||
"cancel": None,
|
||||
"steps": [config_manager.MacroStep.end().to_json_object()],
|
||||
}
|
||||
legacy_json_object["triggers"]["left"]["digital_threshold"] = (
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
|
|
@ -1247,10 +1280,50 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
assert migrated_json.left_trigger.upper_saturation == 40000
|
||||
assert migrated_json.left_trigger.output == 16
|
||||
assert migrated_json.right_trigger.output == 17
|
||||
assert migrated_json.macro_trigger_mask == 1
|
||||
assert migrated_json.macros[0].trigger_mask == 1
|
||||
assert migrated_json.motion_toggle_chord == 0
|
||||
|
||||
|
||||
def test_sparse_macro_capacity_boundaries() -> None:
|
||||
full_step = custom_profile().macros[0].steps[0].to_json_object()
|
||||
button_step = custom_profile().macros[1].steps[0].to_json_object()
|
||||
|
||||
exact = config_manager.ControllerProfile.default().to_json_object()
|
||||
exact["macros"][0]["trigger"] = ["south"]
|
||||
exact["macros"][0]["steps"] = [full_step] * 8
|
||||
exact_profile = config_manager.ControllerProfile.from_json_object(exact)
|
||||
assert sum(
|
||||
len(step.to_sparse_bytes())
|
||||
for macro in exact_profile.macros
|
||||
for step in macro.steps
|
||||
) == config_manager.PROFILE_MACRO_STREAM_SIZE
|
||||
assert (
|
||||
config_manager.ControllerProfile.from_bytes(exact_profile.to_bytes())
|
||||
== exact_profile
|
||||
)
|
||||
|
||||
sixteen = config_manager.ControllerProfile.default().to_json_object()
|
||||
for index, macro in enumerate(sixteen["macros"]):
|
||||
macro["trigger"] = [config_manager.LOGICAL_CONTROLS[index]]
|
||||
macro["steps"] = [button_step] * 4
|
||||
sixteen_profile = config_manager.ControllerProfile.from_json_object(
|
||||
sixteen
|
||||
)
|
||||
assert sum(len(macro.steps) for macro in sixteen_profile.macros) == 16
|
||||
assert len(sixteen_profile.to_bytes()) == config_manager.PROFILE_SIZE
|
||||
|
||||
overflow = config_manager.ControllerProfile.default().to_json_object()
|
||||
overflow["macros"][0]["trigger"] = ["south"]
|
||||
overflow["macros"][0]["steps"] = [full_step] * 8
|
||||
overflow["macros"][1]["trigger"] = ["east"]
|
||||
overflow["macros"][1]["steps"] = [full_step]
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="136-byte sparse stream",
|
||||
):
|
||||
config_manager.ControllerProfile.from_json_object(overflow)
|
||||
|
||||
|
||||
def test_trigger_threshold_uses_transformed_output_domain() -> None:
|
||||
for threshold in (0, 0xFFFF):
|
||||
trigger = config_manager.TriggerConfig(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue