Add sparse multi-macro profiles

This commit is contained in:
Joey Yakimowich-Payne 2026-09-04 16:22:20 -06:00
commit ce93f34d94
13 changed files with 1250 additions and 593 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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(