Add persistent profile switching feedback
This commit is contained in:
parent
114266836d
commit
045edc984e
12 changed files with 1573 additions and 198 deletions
|
|
@ -24,11 +24,9 @@ constexpr int32_t kAxisMinimum = -512;
|
||||||
constexpr int32_t kAxisMaximum = 511;
|
constexpr int32_t kAxisMaximum = 511;
|
||||||
constexpr int32_t kTriggerMaximum = 1023;
|
constexpr int32_t kTriggerMaximum = 1023;
|
||||||
constexpr uint16_t kSwitchHostRumbleDurationMs = 50;
|
constexpr uint16_t kSwitchHostRumbleDurationMs = 50;
|
||||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
|
||||||
// XInput vibration is stateful and remains active until XInputSetState sends
|
// XInput vibration is stateful and remains active until XInputSetState sends
|
||||||
// a new magnitude.
|
// a new magnitude.
|
||||||
constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX;
|
constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX;
|
||||||
#endif
|
|
||||||
constexpr uint32_t kRumblePollIntervalMs = 5;
|
constexpr uint32_t kRumblePollIntervalMs = 5;
|
||||||
constexpr uint32_t kConfigurationPollIntervalMs = 50;
|
constexpr uint32_t kConfigurationPollIntervalMs = 50;
|
||||||
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
|
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
|
||||||
|
|
@ -42,16 +40,15 @@ constexpr uint8_t kAllBlePairingMethods =
|
||||||
SM_STK_GENERATION_METHOD_OOB |
|
SM_STK_GENERATION_METHOD_OOB |
|
||||||
SM_STK_GENERATION_METHOD_PASSKEY |
|
SM_STK_GENERATION_METHOD_PASSKEY |
|
||||||
SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON;
|
SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON;
|
||||||
constexpr uint32_t kAbxyHotkeyButtonMask =
|
constexpr uint16_t kProfileFeedbackPhaseDurationMs = 75;
|
||||||
SWITCH_ABXY_HOTKEY_BUTTON_MASK;
|
constexpr uint8_t kProfileFeedbackWeakMagnitude = UINT8_MAX;
|
||||||
constexpr uint32_t kAbxyHotkeyMiscMask = SWITCH_ABXY_HOTKEY_MISC_MASK;
|
constexpr uint8_t kProfileFeedbackStrongMagnitude = UINT8_MAX;
|
||||||
constexpr bool kDefaultSwapAbxy = SWITCH_ABXY_DEFAULT_SWAPPED != 0;
|
constexpr SwitchRgbColor kProfileLightbarPalette[CONTROLLER_PROFILE_COUNT] = {
|
||||||
constexpr uint32_t kAbxyFeedbackDurationMs =
|
{0x00, 0x55, 0xff},
|
||||||
SWITCH_ABXY_FEEDBACK_DURATION_MS;
|
{0x00, 0xcc, 0x66},
|
||||||
constexpr uint8_t kAbxyFeedbackWeakMagnitude =
|
{0xff, 0xaa, 0x00},
|
||||||
SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE;
|
{0xcc, 0x33, 0xff},
|
||||||
constexpr uint8_t kAbxyFeedbackStrongMagnitude =
|
};
|
||||||
SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE;
|
|
||||||
constexpr uint32_t kMotionHotkeyDpadMask =
|
constexpr uint32_t kMotionHotkeyDpadMask =
|
||||||
SWITCH_MOTION_HOTKEY_DPAD_MASK;
|
SWITCH_MOTION_HOTKEY_DPAD_MASK;
|
||||||
constexpr uint32_t kMotionHotkeyButtonMask =
|
constexpr uint32_t kMotionHotkeyButtonMask =
|
||||||
|
|
@ -73,9 +70,8 @@ constexpr uint8_t kMotionEnabledFeedbackWeakMagnitude =
|
||||||
constexpr uint8_t kMotionEnabledFeedbackStrongMagnitude =
|
constexpr uint8_t kMotionEnabledFeedbackStrongMagnitude =
|
||||||
SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE;
|
SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE;
|
||||||
|
|
||||||
static_assert(kAbxyHotkeyButtonMask != 0);
|
static_assert(kProfileFeedbackPhaseDurationMs == 75);
|
||||||
static_assert(kAbxyHotkeyMiscMask != 0);
|
static_assert(CONTROLLER_PROFILE_COUNT == 4);
|
||||||
static_assert(kAbxyFeedbackDurationMs > 0);
|
|
||||||
static_assert(kMotionHotkeyDpadMask != 0);
|
static_assert(kMotionHotkeyDpadMask != 0);
|
||||||
static_assert(kMotionHotkeyButtonMask != 0);
|
static_assert(kMotionHotkeyButtonMask != 0);
|
||||||
static_assert(kMotionHotkeyMiscMask != 0);
|
static_assert(kMotionHotkeyMiscMask != 0);
|
||||||
|
|
@ -104,6 +100,7 @@ struct RumbleEnvelope {
|
||||||
uint8_t slot;
|
uint8_t slot;
|
||||||
uint32_t connection_generation;
|
uint32_t connection_generation;
|
||||||
ControllerRumbleOutput rumble;
|
ControllerRumbleOutput rumble;
|
||||||
|
uint16_t duration_ms;
|
||||||
};
|
};
|
||||||
struct FeedbackEnvelope {
|
struct FeedbackEnvelope {
|
||||||
uint32_t connection_generation;
|
uint32_t connection_generation;
|
||||||
|
|
@ -111,6 +108,23 @@ struct FeedbackEnvelope {
|
||||||
uint8_t weak_magnitude;
|
uint8_t weak_magnitude;
|
||||||
uint8_t strong_magnitude;
|
uint8_t strong_magnitude;
|
||||||
};
|
};
|
||||||
|
struct ProfileFeedbackEnvelope {
|
||||||
|
uint32_t connection_generation;
|
||||||
|
uint8_t active_profile_number;
|
||||||
|
ControllerProfileConfirmationPolicy policy;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProfileFeedbackSequence {
|
||||||
|
uint32_t connection_generation;
|
||||||
|
uint32_t phase_deadline_ms;
|
||||||
|
uint8_t pulse_count;
|
||||||
|
uint8_t pulses_started;
|
||||||
|
bool active;
|
||||||
|
bool on;
|
||||||
|
bool rumble_enabled;
|
||||||
|
bool led_enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Security Manager identity events arrive before Bluepad32 publishes a ready
|
// Security Manager identity events arrive before Bluepad32 publishes a ready
|
||||||
// device. Retain only the four live handle/address associations so a BLE RPA
|
// device. Retain only the four live handle/address associations so a BLE RPA
|
||||||
|
|
@ -126,6 +140,7 @@ struct BleIdentityMapping {
|
||||||
|
|
||||||
struct BackendSlot {
|
struct BackendSlot {
|
||||||
ControllerState state;
|
ControllerState state;
|
||||||
|
uint16_t pre_hotkey_button_mask;
|
||||||
ControllerIdentity identity;
|
ControllerIdentity identity;
|
||||||
// Non-null with active=false is a connected device still becoming ready.
|
// Non-null with active=false is a connected device still becoming ready.
|
||||||
uni_hid_device_t* device;
|
uni_hid_device_t* device;
|
||||||
|
|
@ -133,14 +148,17 @@ struct BackendSlot {
|
||||||
uint32_t connection_generation;
|
uint32_t connection_generation;
|
||||||
bool active;
|
bool active;
|
||||||
bool rumble_pending;
|
bool rumble_pending;
|
||||||
bool swap_abxy;
|
|
||||||
bool abxy_hotkey_latched;
|
|
||||||
bool motion_enabled;
|
bool motion_enabled;
|
||||||
bool motion_hotkey_latched;
|
bool motion_hotkey_latched;
|
||||||
bool feedback_pending;
|
bool feedback_pending;
|
||||||
uint32_t feedback_until_ms;
|
uint32_t feedback_until_ms;
|
||||||
|
bool profile_feedback_pending;
|
||||||
RumbleEnvelope pending_rumble;
|
RumbleEnvelope pending_rumble;
|
||||||
|
bool retained_host_rumble_valid;
|
||||||
|
RumbleEnvelope retained_host_rumble;
|
||||||
FeedbackEnvelope pending_feedback;
|
FeedbackEnvelope pending_feedback;
|
||||||
|
ProfileFeedbackEnvelope pending_profile_feedback;
|
||||||
|
ProfileFeedbackSequence profile_feedback;
|
||||||
};
|
};
|
||||||
|
|
||||||
critical_section_t g_state_lock;
|
critical_section_t g_state_lock;
|
||||||
|
|
@ -409,6 +427,31 @@ void apply_slot_lighting(uint8_t slot_index, uni_hid_device_t* device) {
|
||||||
device, static_cast<uint8_t>(1u << slot_index));
|
device, static_cast<uint8_t>(1u << slot_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bool valid_confirmation_policy(
|
||||||
|
ControllerProfileConfirmationPolicy policy) {
|
||||||
|
return static_cast<uint8_t>(policy) <=
|
||||||
|
static_cast<uint8_t>(
|
||||||
|
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_profile_lighting(
|
||||||
|
uint8_t active_profile_number, uni_hid_device_t* device) {
|
||||||
|
if (device == nullptr || active_profile_number == 0 ||
|
||||||
|
active_profile_number > CONTROLLER_PROFILE_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (device->report_parser.set_lightbar_color != nullptr) {
|
||||||
|
const SwitchRgbColor color =
|
||||||
|
kProfileLightbarPalette[active_profile_number - 1u];
|
||||||
|
device->report_parser.set_lightbar_color(
|
||||||
|
device, color.red, color.green, color.blue);
|
||||||
|
} else if (device->report_parser.set_player_leds != nullptr) {
|
||||||
|
device->report_parser.set_player_leds(
|
||||||
|
device, static_cast<uint8_t>(
|
||||||
|
(1u << active_profile_number) - 1u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ConnectionStatus compute_connection_status() {
|
ConnectionStatus compute_connection_status() {
|
||||||
|
|
@ -430,11 +473,13 @@ ConnectionStatus compute_connection_status() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
||||||
|
uint16_t pre_hotkey_button_mask,
|
||||||
const ControllerState& state) {
|
const ControllerState& state) {
|
||||||
critical_section_enter_blocking(&g_state_lock);
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
BackendSlot& target = g_slots[slot];
|
BackendSlot& target = g_slots[slot];
|
||||||
if (target.active && target.device == device) {
|
if (target.active && target.device == device) {
|
||||||
target.state = state;
|
target.state = state;
|
||||||
|
target.pre_hotkey_button_mask = pre_hotkey_button_mask;
|
||||||
++target.state_generation;
|
++target.state_generation;
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
|
|
@ -444,10 +489,18 @@ void publish_all_neutral() {
|
||||||
critical_section_enter_blocking(&g_state_lock);
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
for (BackendSlot& slot : g_slots) {
|
for (BackendSlot& slot : g_slots) {
|
||||||
slot.state = make_neutral_state();
|
slot.state = make_neutral_state();
|
||||||
|
slot.pre_hotkey_button_mask = 0;
|
||||||
slot.identity = controller_identity_global();
|
slot.identity = controller_identity_global();
|
||||||
slot.device = nullptr;
|
slot.device = nullptr;
|
||||||
slot.active = false;
|
slot.active = false;
|
||||||
slot.rumble_pending = false;
|
slot.rumble_pending = false;
|
||||||
|
slot.retained_host_rumble_valid = false;
|
||||||
|
slot.retained_host_rumble = {};
|
||||||
|
slot.feedback_pending = false;
|
||||||
|
slot.feedback_until_ms = 0;
|
||||||
|
slot.profile_feedback_pending = false;
|
||||||
|
slot.pending_profile_feedback = {};
|
||||||
|
slot.profile_feedback = {};
|
||||||
++slot.state_generation;
|
++slot.state_generation;
|
||||||
++slot.connection_generation;
|
++slot.connection_generation;
|
||||||
}
|
}
|
||||||
|
|
@ -541,32 +594,129 @@ bool has_motion(const uni_gamepad_t& gamepad) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
constexpr uint16_t logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton button) {
|
||||||
|
return static_cast<uint16_t>(
|
||||||
|
1u << static_cast<uint8_t>(button));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t logical_button_mask(
|
||||||
|
uint32_t dpad, uint32_t buttons, uint32_t misc_buttons) {
|
||||||
|
return static_cast<uint16_t>(
|
||||||
|
((buttons & BUTTON_A) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kSouth)
|
||||||
|
: 0u) |
|
||||||
|
((buttons & BUTTON_B) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kEast)
|
||||||
|
: 0u) |
|
||||||
|
((buttons & BUTTON_X) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kWest)
|
||||||
|
: 0u) |
|
||||||
|
((buttons & BUTTON_Y) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kNorth)
|
||||||
|
: 0u) |
|
||||||
|
((buttons & BUTTON_SHOULDER_L) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kLeftShoulder)
|
||||||
|
: 0u) |
|
||||||
|
((buttons & BUTTON_SHOULDER_R) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kRightShoulder)
|
||||||
|
: 0u) |
|
||||||
|
((misc_buttons & MISC_BUTTON_SELECT) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kSelect)
|
||||||
|
: 0u) |
|
||||||
|
((misc_buttons & MISC_BUTTON_START) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kStart)
|
||||||
|
: 0u) |
|
||||||
|
((misc_buttons & MISC_BUTTON_SYSTEM) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kSystem)
|
||||||
|
: 0u) |
|
||||||
|
((misc_buttons & MISC_BUTTON_CAPTURE) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kCapture)
|
||||||
|
: 0u) |
|
||||||
|
((buttons & BUTTON_THUMB_L) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kLeftStick)
|
||||||
|
: 0u) |
|
||||||
|
((buttons & BUTTON_THUMB_R) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kRightStick)
|
||||||
|
: 0u) |
|
||||||
|
((dpad & DPAD_UP) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadUp)
|
||||||
|
: 0u) |
|
||||||
|
((dpad & DPAD_DOWN) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadDown)
|
||||||
|
: 0u) |
|
||||||
|
((dpad & DPAD_LEFT) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadLeft)
|
||||||
|
: 0u) |
|
||||||
|
((dpad & DPAD_RIGHT) != 0
|
||||||
|
? logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadRight)
|
||||||
|
: 0u));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t kMotionHotkeyLogicalButtonMask =
|
||||||
|
logical_button_mask(
|
||||||
|
kMotionHotkeyDpadMask, kMotionHotkeyButtonMask,
|
||||||
|
kMotionHotkeyMiscMask);
|
||||||
|
|
||||||
|
uint16_t logical_button_mask(const uni_gamepad_t& gamepad) {
|
||||||
|
return logical_button_mask(
|
||||||
|
gamepad.dpad, gamepad.buttons, gamepad.misc_buttons);
|
||||||
|
}
|
||||||
|
|
||||||
ControllerState map_gamepad(const uni_gamepad_t& gamepad,
|
ControllerState map_gamepad(const uni_gamepad_t& gamepad,
|
||||||
bool swap_abxy,
|
bool motion_enabled,
|
||||||
bool motion_enabled) {
|
uint16_t button_mask) {
|
||||||
ControllerState state = make_neutral_state();
|
ControllerState state = make_neutral_state();
|
||||||
|
|
||||||
state.dpad_up = (gamepad.dpad & DPAD_UP) != 0;
|
state.dpad_up =
|
||||||
state.dpad_down = (gamepad.dpad & DPAD_DOWN) != 0;
|
(button_mask & logical_button_bit(
|
||||||
state.dpad_left = (gamepad.dpad & DPAD_LEFT) != 0;
|
ControllerProfileLogicalButton::kDpadUp)) != 0;
|
||||||
state.dpad_right = (gamepad.dpad & DPAD_RIGHT) != 0;
|
state.dpad_down =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadDown)) != 0;
|
||||||
|
state.dpad_left =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadLeft)) != 0;
|
||||||
|
state.dpad_right =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadRight)) != 0;
|
||||||
|
|
||||||
// Bluepad32's A/B/X/Y are positional: south/east/west/north.
|
// Bluepad32's A/B/X/Y are positional: south/east/west/north. Persistent
|
||||||
state.button_south = (gamepad.buttons & BUTTON_A) != 0;
|
// profile mappings are the only button remapping layer.
|
||||||
state.button_east = (gamepad.buttons & BUTTON_B) != 0;
|
state.button_south =
|
||||||
state.button_west = (gamepad.buttons & BUTTON_X) != 0;
|
(button_mask & logical_button_bit(
|
||||||
state.button_north = (gamepad.buttons & BUTTON_Y) != 0;
|
ControllerProfileLogicalButton::kSouth)) != 0;
|
||||||
if (swap_abxy) {
|
state.button_east =
|
||||||
bool temporary = state.button_east;
|
(button_mask & logical_button_bit(
|
||||||
state.button_east = state.button_south;
|
ControllerProfileLogicalButton::kEast)) != 0;
|
||||||
state.button_south = temporary;
|
state.button_west =
|
||||||
temporary = state.button_north;
|
(button_mask & logical_button_bit(
|
||||||
state.button_north = state.button_west;
|
ControllerProfileLogicalButton::kWest)) != 0;
|
||||||
state.button_west = temporary;
|
state.button_north =
|
||||||
}
|
(button_mask & logical_button_bit(
|
||||||
state.button_left_shoulder = (gamepad.buttons & BUTTON_SHOULDER_L) != 0;
|
ControllerProfileLogicalButton::kNorth)) != 0;
|
||||||
state.button_right_shoulder = (gamepad.buttons & BUTTON_SHOULDER_R) != 0;
|
state.button_left_shoulder =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kLeftShoulder)) != 0;
|
||||||
|
state.button_right_shoulder =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kRightShoulder)) != 0;
|
||||||
state.left_trigger =
|
state.left_trigger =
|
||||||
(gamepad.buttons & BUTTON_TRIGGER_L) != 0
|
(gamepad.buttons & BUTTON_TRIGGER_L) != 0
|
||||||
? UINT16_MAX
|
? UINT16_MAX
|
||||||
|
|
@ -575,13 +725,25 @@ ControllerState map_gamepad(const uni_gamepad_t& gamepad,
|
||||||
(gamepad.buttons & BUTTON_TRIGGER_R) != 0
|
(gamepad.buttons & BUTTON_TRIGGER_R) != 0
|
||||||
? UINT16_MAX
|
? UINT16_MAX
|
||||||
: scale_trigger(gamepad.throttle);
|
: scale_trigger(gamepad.throttle);
|
||||||
state.button_left_stick = (gamepad.buttons & BUTTON_THUMB_L) != 0;
|
state.button_left_stick =
|
||||||
state.button_right_stick = (gamepad.buttons & BUTTON_THUMB_R) != 0;
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kLeftStick)) != 0;
|
||||||
|
state.button_right_stick =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kRightStick)) != 0;
|
||||||
|
|
||||||
state.button_select = (gamepad.misc_buttons & MISC_BUTTON_SELECT) != 0;
|
state.button_select =
|
||||||
state.button_start = (gamepad.misc_buttons & MISC_BUTTON_START) != 0;
|
(button_mask & logical_button_bit(
|
||||||
state.button_system = (gamepad.misc_buttons & MISC_BUTTON_SYSTEM) != 0;
|
ControllerProfileLogicalButton::kSelect)) != 0;
|
||||||
state.button_capture = (gamepad.misc_buttons & MISC_BUTTON_CAPTURE) != 0;
|
state.button_start =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kStart)) != 0;
|
||||||
|
state.button_system =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kSystem)) != 0;
|
||||||
|
state.button_capture =
|
||||||
|
(button_mask & logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kCapture)) != 0;
|
||||||
|
|
||||||
state.left_stick_x = scale_axis(gamepad.axis_x);
|
state.left_stick_x = scale_axis(gamepad.axis_x);
|
||||||
state.left_stick_y = scale_axis(gamepad.axis_y);
|
state.left_stick_y = scale_axis(gamepad.axis_y);
|
||||||
|
|
@ -606,11 +768,11 @@ ControllerState map_gamepad(const uni_gamepad_t& gamepad,
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
struct HotkeyDecision {
|
struct HotkeyDecision {
|
||||||
bool swap_abxy;
|
|
||||||
bool motion_enabled;
|
bool motion_enabled;
|
||||||
uint32_t suppress_dpad;
|
uint32_t suppress_dpad;
|
||||||
uint32_t suppress_buttons;
|
uint32_t suppress_buttons;
|
||||||
uint32_t suppress_misc_buttons;
|
uint32_t suppress_misc_buttons;
|
||||||
|
uint16_t suppress_logical_buttons;
|
||||||
};
|
};
|
||||||
|
|
||||||
void queue_local_feedback(BackendSlot& slot, uint16_t duration_ms,
|
void queue_local_feedback(BackendSlot& slot, uint16_t duration_ms,
|
||||||
|
|
@ -622,24 +784,23 @@ void queue_local_feedback(BackendSlot& slot, uint16_t duration_ms,
|
||||||
strong_magnitude};
|
strong_magnitude};
|
||||||
}
|
}
|
||||||
void reset_slot_hotkeys(BackendSlot& slot) {
|
void reset_slot_hotkeys(BackendSlot& slot) {
|
||||||
slot.swap_abxy = kDefaultSwapAbxy;
|
|
||||||
slot.abxy_hotkey_latched = false;
|
|
||||||
slot.motion_enabled = kDefaultMotionEnabled;
|
slot.motion_enabled = kDefaultMotionEnabled;
|
||||||
slot.motion_hotkey_latched = false;
|
slot.motion_hotkey_latched = false;
|
||||||
|
slot.pre_hotkey_button_mask = 0;
|
||||||
slot.feedback_pending = false;
|
slot.feedback_pending = false;
|
||||||
slot.feedback_until_ms = 0;
|
slot.feedback_until_ms = 0;
|
||||||
slot.pending_feedback = {};
|
slot.pending_feedback = {};
|
||||||
|
slot.profile_feedback_pending = false;
|
||||||
|
slot.pending_profile_feedback = {};
|
||||||
|
slot.profile_feedback = {};
|
||||||
|
slot.retained_host_rumble_valid = false;
|
||||||
|
slot.retained_host_rumble = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
HotkeyDecision update_controller_hotkeys(
|
HotkeyDecision update_controller_hotkeys(
|
||||||
uint8_t slot_index, uni_hid_device_t* device,
|
uint8_t slot_index, uni_hid_device_t* device,
|
||||||
const uni_gamepad_t& gamepad) {
|
const uni_gamepad_t& gamepad) {
|
||||||
const bool abxy_pressed =
|
|
||||||
(gamepad.buttons & kAbxyHotkeyButtonMask) ==
|
|
||||||
kAbxyHotkeyButtonMask &&
|
|
||||||
(gamepad.misc_buttons & kAbxyHotkeyMiscMask) ==
|
|
||||||
kAbxyHotkeyMiscMask;
|
|
||||||
const bool motion_pressed =
|
const bool motion_pressed =
|
||||||
(gamepad.dpad & kMotionHotkeyDpadMask) ==
|
(gamepad.dpad & kMotionHotkeyDpadMask) ==
|
||||||
kMotionHotkeyDpadMask &&
|
kMotionHotkeyDpadMask &&
|
||||||
|
|
@ -648,18 +809,12 @@ HotkeyDecision update_controller_hotkeys(
|
||||||
(gamepad.misc_buttons & kMotionHotkeyMiscMask) ==
|
(gamepad.misc_buttons & kMotionHotkeyMiscMask) ==
|
||||||
kMotionHotkeyMiscMask;
|
kMotionHotkeyMiscMask;
|
||||||
HotkeyDecision decision{
|
HotkeyDecision decision{
|
||||||
kDefaultSwapAbxy, kDefaultMotionEnabled, 0, 0, 0};
|
kDefaultMotionEnabled, 0, 0, 0, 0};
|
||||||
|
|
||||||
critical_section_enter_blocking(&g_state_lock);
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
BackendSlot& slot = g_slots[slot_index];
|
BackendSlot& slot = g_slots[slot_index];
|
||||||
if (slot.active && slot.device == device) {
|
if (slot.active && slot.device == device) {
|
||||||
if (abxy_pressed && !slot.abxy_hotkey_latched) {
|
if (motion_pressed && !slot.motion_hotkey_latched) {
|
||||||
slot.swap_abxy = !slot.swap_abxy;
|
|
||||||
queue_local_feedback(
|
|
||||||
slot, static_cast<uint16_t>(kAbxyFeedbackDurationMs),
|
|
||||||
kAbxyFeedbackWeakMagnitude,
|
|
||||||
kAbxyFeedbackStrongMagnitude);
|
|
||||||
} else if (motion_pressed && !slot.motion_hotkey_latched) {
|
|
||||||
slot.motion_enabled = !slot.motion_enabled;
|
slot.motion_enabled = !slot.motion_enabled;
|
||||||
if (slot.motion_enabled) {
|
if (slot.motion_enabled) {
|
||||||
queue_local_feedback(
|
queue_local_feedback(
|
||||||
|
|
@ -673,18 +828,14 @@ HotkeyDecision update_controller_hotkeys(
|
||||||
kMotionDisabledFeedbackStrongMagnitude);
|
kMotionDisabledFeedbackStrongMagnitude);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slot.abxy_hotkey_latched = abxy_pressed;
|
|
||||||
slot.motion_hotkey_latched = motion_pressed;
|
slot.motion_hotkey_latched = motion_pressed;
|
||||||
decision.swap_abxy = slot.swap_abxy;
|
|
||||||
decision.motion_enabled = slot.motion_enabled;
|
decision.motion_enabled = slot.motion_enabled;
|
||||||
if (abxy_pressed) {
|
|
||||||
decision.suppress_buttons |= kAbxyHotkeyButtonMask;
|
|
||||||
decision.suppress_misc_buttons |= kAbxyHotkeyMiscMask;
|
|
||||||
}
|
|
||||||
if (motion_pressed) {
|
if (motion_pressed) {
|
||||||
decision.suppress_dpad |= kMotionHotkeyDpadMask;
|
decision.suppress_dpad |= kMotionHotkeyDpadMask;
|
||||||
decision.suppress_buttons |= kMotionHotkeyButtonMask;
|
decision.suppress_buttons |= kMotionHotkeyButtonMask;
|
||||||
decision.suppress_misc_buttons |= kMotionHotkeyMiscMask;
|
decision.suppress_misc_buttons |= kMotionHotkeyMiscMask;
|
||||||
|
decision.suppress_logical_buttons |=
|
||||||
|
kMotionHotkeyLogicalButtonMask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
|
|
@ -995,12 +1146,55 @@ void apply_connection_policy() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool deadline_reached(uint32_t now_ms, uint32_t deadline_ms) {
|
||||||
|
return static_cast<int32_t>(now_ms - deadline_ms) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool advance_profile_feedback(ProfileFeedbackSequence* sequence,
|
||||||
|
uint32_t now_ms) {
|
||||||
|
bool rumble_dispatch = false;
|
||||||
|
for (uint8_t transition = 0;
|
||||||
|
transition < CONTROLLER_PROFILE_COUNT * 2u &&
|
||||||
|
sequence->active &&
|
||||||
|
deadline_reached(now_ms, sequence->phase_deadline_ms);
|
||||||
|
++transition) {
|
||||||
|
sequence->phase_deadline_ms +=
|
||||||
|
kProfileFeedbackPhaseDurationMs;
|
||||||
|
if (sequence->on) {
|
||||||
|
sequence->on = false;
|
||||||
|
rumble_dispatch = false;
|
||||||
|
} else if (sequence->pulses_started >=
|
||||||
|
sequence->pulse_count) {
|
||||||
|
sequence->active = false;
|
||||||
|
} else {
|
||||||
|
sequence->on = true;
|
||||||
|
++sequence->pulses_started;
|
||||||
|
rumble_dispatch = sequence->rumble_enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rumble_dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
void update_status_led() {
|
void update_status_led() {
|
||||||
++g_status_led_tick;
|
++g_status_led_tick;
|
||||||
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
||||||
bool led_on = false;
|
bool profile_led_override = false;
|
||||||
|
bool profile_led_on = false;
|
||||||
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
|
for (const BackendSlot& slot : g_slots) {
|
||||||
|
if (slot.profile_feedback.active &&
|
||||||
|
slot.profile_feedback.led_enabled) {
|
||||||
|
profile_led_override = true;
|
||||||
|
profile_led_on =
|
||||||
|
profile_led_on || slot.profile_feedback.on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
critical_section_exit(&g_state_lock);
|
||||||
|
|
||||||
if (static_cast<int32_t>(
|
bool led_on = false;
|
||||||
|
if (profile_led_override) {
|
||||||
|
led_on = profile_led_on;
|
||||||
|
} else if (static_cast<int32_t>(
|
||||||
now_ms - g_pairing_reset_feedback_deadline_ms) < 0) {
|
now_ms - g_pairing_reset_feedback_deadline_ms) < 0) {
|
||||||
led_on = (g_status_led_tick % 20) < 10;
|
led_on = (g_status_led_tick % 20) < 10;
|
||||||
} else if (pairing_window_active_at(now_ms)) {
|
} else if (pairing_window_active_at(now_ms)) {
|
||||||
|
|
@ -1037,17 +1231,95 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
if (update_pairing_window(now_ms)) {
|
if (update_pairing_window(now_ms)) {
|
||||||
apply_connection_policy();
|
apply_connection_policy();
|
||||||
}
|
}
|
||||||
|
const bool xinput_host_mode =
|
||||||
|
host_rumble_duration_ms() == kXInputHostRumbleDurationMs;
|
||||||
|
|
||||||
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
||||||
RumbleEnvelope envelope{};
|
RumbleEnvelope envelope{};
|
||||||
FeedbackEnvelope feedback{};
|
FeedbackEnvelope feedback{};
|
||||||
|
ProfileFeedbackEnvelope profile_feedback{};
|
||||||
uni_hid_device_t* device = nullptr;
|
uni_hid_device_t* device = nullptr;
|
||||||
|
bool profile_lighting_dispatch = false;
|
||||||
|
bool profile_rumble_dispatch = false;
|
||||||
bool feedback_dispatch = false;
|
bool feedback_dispatch = false;
|
||||||
bool host_dispatch = false;
|
bool host_dispatch = false;
|
||||||
|
|
||||||
critical_section_enter_blocking(&g_state_lock);
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
BackendSlot& slot = g_slots[slot_index];
|
BackendSlot& slot = g_slots[slot_index];
|
||||||
if (slot.feedback_pending) {
|
if (slot.retained_host_rumble_valid &&
|
||||||
|
(!xinput_host_mode ||
|
||||||
|
slot.retained_host_rumble.duration_ms !=
|
||||||
|
kXInputHostRumbleDurationMs ||
|
||||||
|
slot.retained_host_rumble.slot != slot_index ||
|
||||||
|
slot.retained_host_rumble.connection_generation !=
|
||||||
|
slot.connection_generation ||
|
||||||
|
!slot.active || slot.device == nullptr)) {
|
||||||
|
slot.retained_host_rumble_valid = false;
|
||||||
|
slot.retained_host_rumble = {};
|
||||||
|
}
|
||||||
|
if (slot.profile_feedback.active &&
|
||||||
|
slot.profile_feedback.connection_generation !=
|
||||||
|
slot.connection_generation) {
|
||||||
|
slot.profile_feedback = {};
|
||||||
|
}
|
||||||
|
const bool profile_feedback_was_active =
|
||||||
|
slot.profile_feedback.active;
|
||||||
|
const bool completed_feedback_had_rumble =
|
||||||
|
slot.profile_feedback.rumble_enabled;
|
||||||
|
profile_rumble_dispatch =
|
||||||
|
advance_profile_feedback(&slot.profile_feedback, now_ms);
|
||||||
|
if (profile_feedback_was_active &&
|
||||||
|
!slot.profile_feedback.active &&
|
||||||
|
completed_feedback_had_rumble &&
|
||||||
|
slot.retained_host_rumble_valid) {
|
||||||
|
slot.pending_rumble = slot.retained_host_rumble;
|
||||||
|
slot.rumble_pending = true;
|
||||||
|
}
|
||||||
|
if (profile_rumble_dispatch) {
|
||||||
|
device = slot.device;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool feedback_active =
|
||||||
|
static_cast<int32_t>(now_ms - slot.feedback_until_ms) < 0;
|
||||||
|
if (!slot.profile_feedback.active && !feedback_active &&
|
||||||
|
slot.profile_feedback_pending) {
|
||||||
|
profile_feedback = slot.pending_profile_feedback;
|
||||||
|
slot.profile_feedback_pending = false;
|
||||||
|
const uint8_t policy =
|
||||||
|
static_cast<uint8_t>(profile_feedback.policy);
|
||||||
|
if (slot.active && slot.device != nullptr &&
|
||||||
|
profile_feedback.connection_generation ==
|
||||||
|
slot.connection_generation &&
|
||||||
|
profile_feedback.active_profile_number != 0 &&
|
||||||
|
profile_feedback.active_profile_number <=
|
||||||
|
CONTROLLER_PROFILE_COUNT &&
|
||||||
|
valid_confirmation_policy(profile_feedback.policy) &&
|
||||||
|
profile_feedback.policy !=
|
||||||
|
ControllerProfileConfirmationPolicy::kNone) {
|
||||||
|
slot.profile_feedback = {
|
||||||
|
slot.connection_generation,
|
||||||
|
now_ms + kProfileFeedbackPhaseDurationMs,
|
||||||
|
profile_feedback.active_profile_number,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
(policy & static_cast<uint8_t>(
|
||||||
|
ControllerProfileConfirmationPolicy::
|
||||||
|
kRumble)) != 0,
|
||||||
|
(policy & static_cast<uint8_t>(
|
||||||
|
ControllerProfileConfirmationPolicy::
|
||||||
|
kLed)) != 0,
|
||||||
|
};
|
||||||
|
device = slot.device;
|
||||||
|
profile_lighting_dispatch =
|
||||||
|
slot.profile_feedback.led_enabled;
|
||||||
|
profile_rumble_dispatch =
|
||||||
|
slot.profile_feedback.rumble_enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slot.profile_feedback.active &&
|
||||||
|
slot.feedback_pending) {
|
||||||
feedback = slot.pending_feedback;
|
feedback = slot.pending_feedback;
|
||||||
feedback_dispatch =
|
feedback_dispatch =
|
||||||
slot.active && slot.device != nullptr &&
|
slot.active && slot.device != nullptr &&
|
||||||
|
|
@ -1062,10 +1334,12 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool feedback_active =
|
const bool local_feedback_active =
|
||||||
static_cast<int32_t>(now_ms - slot.feedback_until_ms) < 0;
|
slot.profile_feedback.active ||
|
||||||
if (!feedback_dispatch && !feedback_active &&
|
static_cast<int32_t>(
|
||||||
slot.rumble_pending) {
|
now_ms - slot.feedback_until_ms) < 0;
|
||||||
|
if (!profile_rumble_dispatch && !feedback_dispatch &&
|
||||||
|
!local_feedback_active && slot.rumble_pending) {
|
||||||
envelope = slot.pending_rumble;
|
envelope = slot.pending_rumble;
|
||||||
slot.rumble_pending = false;
|
slot.rumble_pending = false;
|
||||||
host_dispatch =
|
host_dispatch =
|
||||||
|
|
@ -1079,7 +1353,17 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
|
|
||||||
if (feedback_dispatch) {
|
if (profile_lighting_dispatch) {
|
||||||
|
apply_profile_lighting(
|
||||||
|
profile_feedback.active_profile_number, device);
|
||||||
|
}
|
||||||
|
if (profile_rumble_dispatch && device != nullptr &&
|
||||||
|
device->report_parser.play_dual_rumble != nullptr) {
|
||||||
|
device->report_parser.play_dual_rumble(
|
||||||
|
device, 0, kProfileFeedbackPhaseDurationMs,
|
||||||
|
kProfileFeedbackWeakMagnitude,
|
||||||
|
kProfileFeedbackStrongMagnitude);
|
||||||
|
} else if (feedback_dispatch) {
|
||||||
device->report_parser.play_dual_rumble(
|
device->report_parser.play_dual_rumble(
|
||||||
device, 0, feedback.duration_ms,
|
device, 0, feedback.duration_ms,
|
||||||
feedback.weak_magnitude, feedback.strong_magnitude);
|
feedback.weak_magnitude, feedback.strong_magnitude);
|
||||||
|
|
@ -1089,7 +1373,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
envelope.rumble.low_frequency_magnitude == 0 &&
|
envelope.rumble.low_frequency_magnitude == 0 &&
|
||||||
envelope.rumble.high_frequency_magnitude == 0;
|
envelope.rumble.high_frequency_magnitude == 0;
|
||||||
device->report_parser.play_dual_rumble(
|
device->report_parser.play_dual_rumble(
|
||||||
device, 0, stop ? 0 : host_rumble_duration_ms(),
|
device, 0, stop ? 0 : envelope.duration_ms,
|
||||||
envelope.rumble.high_frequency_magnitude,
|
envelope.rumble.high_frequency_magnitude,
|
||||||
envelope.rumble.low_frequency_magnitude);
|
envelope.rumble.low_frequency_magnitude);
|
||||||
}
|
}
|
||||||
|
|
@ -1273,15 +1557,20 @@ void platform_on_controller_data(uni_hid_device_t* device,
|
||||||
}
|
}
|
||||||
|
|
||||||
uni_gamepad_t gamepad = controller->gamepad;
|
uni_gamepad_t gamepad = controller->gamepad;
|
||||||
|
const uint16_t pre_hotkey_button_mask =
|
||||||
|
logical_button_mask(gamepad);
|
||||||
const HotkeyDecision hotkeys = update_controller_hotkeys(
|
const HotkeyDecision hotkeys = update_controller_hotkeys(
|
||||||
static_cast<uint8_t>(slot_index), device, gamepad);
|
static_cast<uint8_t>(slot_index), device, gamepad);
|
||||||
gamepad.dpad &= ~hotkeys.suppress_dpad;
|
gamepad.dpad &= ~hotkeys.suppress_dpad;
|
||||||
gamepad.buttons &= ~hotkeys.suppress_buttons;
|
gamepad.buttons &= ~hotkeys.suppress_buttons;
|
||||||
gamepad.misc_buttons &= ~hotkeys.suppress_misc_buttons;
|
gamepad.misc_buttons &= ~hotkeys.suppress_misc_buttons;
|
||||||
|
const uint16_t output_button_mask = static_cast<uint16_t>(
|
||||||
|
pre_hotkey_button_mask & ~hotkeys.suppress_logical_buttons);
|
||||||
publish_device_state(
|
publish_device_state(
|
||||||
static_cast<uint8_t>(slot_index), device,
|
static_cast<uint8_t>(slot_index), device,
|
||||||
map_gamepad(gamepad, hotkeys.swap_abxy,
|
pre_hotkey_button_mask,
|
||||||
hotkeys.motion_enabled));
|
map_gamepad(
|
||||||
|
gamepad, hotkeys.motion_enabled, output_button_mask));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uni_property_t* platform_get_property(uni_property_idx_t index) {
|
const uni_property_t* platform_get_property(uni_property_idx_t index) {
|
||||||
|
|
@ -1462,6 +1751,8 @@ void bluepad32_input_backend_snapshot(uint8_t slot_index,
|
||||||
out->active = slot.active;
|
out->active = slot.active;
|
||||||
out->connection_generation = slot.connection_generation;
|
out->connection_generation = slot.connection_generation;
|
||||||
out->identity = slot.identity;
|
out->identity = slot.identity;
|
||||||
|
out->pre_hotkey_button_mask =
|
||||||
|
slot.pre_hotkey_button_mask;
|
||||||
out->state = slot.state;
|
out->state = slot.state;
|
||||||
const uint32_t state_generation = slot.state_generation;
|
const uint32_t state_generation = slot.state_generation;
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
|
|
@ -1485,11 +1776,45 @@ void bluepad32_input_backend_queue_rumble(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uint16_t duration_ms = host_rumble_duration_ms();
|
||||||
critical_section_enter_blocking(&g_state_lock);
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
BackendSlot& slot = g_slots[slot_index];
|
BackendSlot& slot = g_slots[slot_index];
|
||||||
if (slot.active && slot.device != nullptr) {
|
if (slot.active && slot.device != nullptr) {
|
||||||
slot.pending_rumble = {slot_index, slot.connection_generation, rumble};
|
const RumbleEnvelope envelope{
|
||||||
|
slot_index, slot.connection_generation, rumble,
|
||||||
|
duration_ms};
|
||||||
|
slot.pending_rumble = envelope;
|
||||||
slot.rumble_pending = true;
|
slot.rumble_pending = true;
|
||||||
|
if (duration_ms == kXInputHostRumbleDurationMs) {
|
||||||
|
slot.retained_host_rumble = envelope;
|
||||||
|
slot.retained_host_rumble_valid = true;
|
||||||
|
} else {
|
||||||
|
slot.retained_host_rumble = {};
|
||||||
|
slot.retained_host_rumble_valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
critical_section_exit(&g_state_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
uint8_t slot_index, uint32_t connection_generation,
|
||||||
|
uint8_t active_profile_number,
|
||||||
|
ControllerProfileConfirmationPolicy policy) {
|
||||||
|
if (!g_initialized || !valid_slot(slot_index) ||
|
||||||
|
active_profile_number == 0 ||
|
||||||
|
active_profile_number > CONTROLLER_PROFILE_COUNT ||
|
||||||
|
!valid_confirmation_policy(policy) ||
|
||||||
|
policy == ControllerProfileConfirmationPolicy::kNone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
|
BackendSlot& slot = g_slots[slot_index];
|
||||||
|
if (slot.active && slot.device != nullptr &&
|
||||||
|
slot.connection_generation == connection_generation) {
|
||||||
|
slot.pending_profile_feedback = {
|
||||||
|
connection_generation, active_profile_number, policy};
|
||||||
|
slot.profile_feedback_pending = true;
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include "controller_color.h"
|
#include "controller_color.h"
|
||||||
#include "controller_identity.h"
|
#include "controller_identity.h"
|
||||||
|
#include "controller_profile.h"
|
||||||
#include "controller_state.h"
|
#include "controller_state.h"
|
||||||
#include "switch_haptics.h"
|
#include "switch_haptics.h"
|
||||||
|
|
||||||
|
|
@ -37,6 +38,9 @@ struct Bluepad32SlotSnapshot {
|
||||||
bool active;
|
bool active;
|
||||||
uint32_t connection_generation;
|
uint32_t connection_generation;
|
||||||
ControllerIdentity identity;
|
ControllerIdentity identity;
|
||||||
|
// Physical logical-button state before backend hotkey consumption.
|
||||||
|
// Valid only for this snapshot's connection generation.
|
||||||
|
uint16_t pre_hotkey_button_mask;
|
||||||
ControllerState state;
|
ControllerState state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,3 +58,9 @@ void bluepad32_input_backend_pairing_snapshot(
|
||||||
void bluepad32_input_backend_report_sent(uint8_t slot);
|
void bluepad32_input_backend_report_sent(uint8_t slot);
|
||||||
void bluepad32_input_backend_queue_rumble(
|
void bluepad32_input_backend_queue_rumble(
|
||||||
uint8_t slot, const ControllerRumbleOutput& rumble);
|
uint8_t slot, const ControllerRumbleOutput& rumble);
|
||||||
|
// Schedule bounded local profile confirmation only for the matching live
|
||||||
|
// connection generation. Profile numbers are one-based (1..4).
|
||||||
|
void bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
uint8_t slot, uint32_t connection_generation,
|
||||||
|
uint8_t active_profile_number,
|
||||||
|
ControllerProfileConfirmationPolicy policy);
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,12 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// Bluepad32 button masks. Default chord: L + R + SELECT + START.
|
|
||||||
#define SWITCH_ABXY_HOTKEY_BUTTON_MASK \
|
|
||||||
(BUTTON_SHOULDER_L | BUTTON_SHOULDER_R)
|
|
||||||
#define SWITCH_ABXY_HOTKEY_MISC_MASK \
|
|
||||||
(MISC_BUTTON_SELECT | MISC_BUTTON_START)
|
|
||||||
|
|
||||||
// Motion toggle chord: D-pad Up + R + START / Options.
|
// Motion toggle chord: D-pad Up + R + START / Options.
|
||||||
#define SWITCH_MOTION_HOTKEY_DPAD_MASK DPAD_UP
|
#define SWITCH_MOTION_HOTKEY_DPAD_MASK DPAD_UP
|
||||||
#define SWITCH_MOTION_HOTKEY_BUTTON_MASK BUTTON_SHOULDER_R
|
#define SWITCH_MOTION_HOTKEY_BUTTON_MASK BUTTON_SHOULDER_R
|
||||||
#define SWITCH_MOTION_HOTKEY_MISC_MASK MISC_BUTTON_START
|
#define SWITCH_MOTION_HOTKEY_MISC_MASK MISC_BUTTON_START
|
||||||
#define SWITCH_MOTION_DEFAULT_ENABLED 1
|
#define SWITCH_MOTION_DEFAULT_ENABLED 1
|
||||||
|
|
||||||
// 0 starts each new connection in Nintendo positional layout; 1 starts swapped.
|
|
||||||
#define SWITCH_ABXY_DEFAULT_SWAPPED 0
|
|
||||||
|
|
||||||
// Local confirmation pulse sent only to the controller that toggled.
|
|
||||||
#define SWITCH_ABXY_FEEDBACK_DURATION_MS 120
|
|
||||||
#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0xFF
|
|
||||||
#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0xFF
|
|
||||||
|
|
||||||
// A longer pulse confirms disabled; a shorter pulse confirms enabled.
|
// A longer pulse confirms disabled; a shorter pulse confirms enabled.
|
||||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180
|
#define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180
|
||||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE 0xA0
|
#define SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE 0xA0
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,20 @@ struct ControllerProfileRuntimeContext {
|
||||||
ControllerIdentity identity{};
|
ControllerIdentity identity{};
|
||||||
uint32_t database_generation = 0;
|
uint32_t database_generation = 0;
|
||||||
uint8_t active_profile_index = 0;
|
uint8_t active_profile_index = 0;
|
||||||
|
bool profile_snapshot_valid = false;
|
||||||
ControllerProfile profile{};
|
ControllerProfile profile{};
|
||||||
ControllerSyntheticInputContext synthetic{};
|
ControllerSyntheticInputContext synthetic{};
|
||||||
bool runtime_generations_initialized = false;
|
bool runtime_generations_initialized = false;
|
||||||
AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe;
|
AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe;
|
||||||
uint32_t configuration_reset_generation = 0;
|
uint32_t configuration_reset_generation = 0;
|
||||||
|
bool switching_chord_held = false;
|
||||||
|
bool switching_chord_armed = true;
|
||||||
|
bool switching_activation_requested = false;
|
||||||
|
uint16_t held_switching_chord = 0;
|
||||||
|
uint8_t switching_target_profile_index = 0;
|
||||||
|
uint32_t switching_transaction_id = 0;
|
||||||
|
bool profile_change_pending = false;
|
||||||
|
ControllerProfileRuntimeProfileChangeEvent pending_profile_change{};
|
||||||
};
|
};
|
||||||
|
|
||||||
ControllerProfileRuntimeContext
|
ControllerProfileRuntimeContext
|
||||||
|
|
@ -25,6 +34,23 @@ ControllerProfileRuntimeContext
|
||||||
ControllerProfile g_default_profile{};
|
ControllerProfile g_default_profile{};
|
||||||
ControllerProfileTransformResult g_neutral_output{};
|
ControllerProfileTransformResult g_neutral_output{};
|
||||||
bool g_initialized = false;
|
bool g_initialized = false;
|
||||||
|
uint32_t g_next_activation_sequence = 1;
|
||||||
|
|
||||||
|
uint32_t next_activation_transaction_id() {
|
||||||
|
const uint32_t transaction_id =
|
||||||
|
0x80000000u | g_next_activation_sequence;
|
||||||
|
g_next_activation_sequence =
|
||||||
|
g_next_activation_sequence == 0x7fffffffu
|
||||||
|
? 1u
|
||||||
|
: g_next_activation_sequence + 1u;
|
||||||
|
return transaction_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t effective_switching_chord(const ControllerProfile& profile) {
|
||||||
|
return profile.switching_chord == 0
|
||||||
|
? CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD
|
||||||
|
: profile.switching_chord;
|
||||||
|
}
|
||||||
|
|
||||||
void initialize_defaults() {
|
void initialize_defaults() {
|
||||||
if (g_initialized) {
|
if (g_initialized) {
|
||||||
|
|
@ -52,6 +78,30 @@ void refresh_profile(ControllerProfileRuntimeContext* context,
|
||||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||||
|
|
||||||
|
const bool same_connection =
|
||||||
|
context->active &&
|
||||||
|
context->connection_generation == connection_generation;
|
||||||
|
const bool same_identity =
|
||||||
|
same_connection &&
|
||||||
|
controller_identity_equal(context->identity, identity);
|
||||||
|
const uint8_t previous_profile_index =
|
||||||
|
context->active_profile_index;
|
||||||
|
const bool previous_profile_valid =
|
||||||
|
context->profile_snapshot_valid;
|
||||||
|
if (!same_connection) {
|
||||||
|
context->runtime_generations_initialized = false;
|
||||||
|
context->switching_chord_held = false;
|
||||||
|
context->switching_chord_armed = true;
|
||||||
|
context->switching_activation_requested = false;
|
||||||
|
context->held_switching_chord = 0;
|
||||||
|
context->switching_target_profile_index = 0;
|
||||||
|
context->switching_transaction_id = 0;
|
||||||
|
context->profile_change_pending = false;
|
||||||
|
context->pending_profile_change = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
controller_synthetic_input_cancel(&context->synthetic,
|
||||||
|
current_input_button_mask);
|
||||||
context->active = true;
|
context->active = true;
|
||||||
context->connection_generation = connection_generation;
|
context->connection_generation = connection_generation;
|
||||||
context->identity = identity;
|
context->identity = identity;
|
||||||
|
|
@ -59,9 +109,25 @@ void refresh_profile(ControllerProfileRuntimeContext* context,
|
||||||
? snapshot.metadata.generation
|
? snapshot.metadata.generation
|
||||||
: observed_database_generation;
|
: observed_database_generation;
|
||||||
context->active_profile_index = snapshot.valid ? snapshot.profile_index : 0;
|
context->active_profile_index = snapshot.valid ? snapshot.profile_index : 0;
|
||||||
|
context->profile_snapshot_valid = snapshot.valid;
|
||||||
context->profile = snapshot.valid ? snapshot.profile : g_default_profile;
|
context->profile = snapshot.valid ? snapshot.profile : g_default_profile;
|
||||||
controller_synthetic_input_cancel(&context->synthetic,
|
if (context->switching_chord_held &&
|
||||||
current_input_button_mask);
|
!context->switching_activation_requested) {
|
||||||
|
context->switching_target_profile_index =
|
||||||
|
static_cast<uint8_t>(
|
||||||
|
(context->active_profile_index + 1u) %
|
||||||
|
CONTROLLER_PROFILE_COUNT);
|
||||||
|
}
|
||||||
|
if (same_identity && previous_profile_valid && snapshot.valid &&
|
||||||
|
previous_profile_index != context->active_profile_index) {
|
||||||
|
context->pending_profile_change = {
|
||||||
|
connection_generation,
|
||||||
|
context->database_generation,
|
||||||
|
static_cast<uint8_t>(context->active_profile_index + 1u),
|
||||||
|
controller_profile_confirmation_policy(context->profile),
|
||||||
|
};
|
||||||
|
context->profile_change_pending = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ControllerProfileRuntimeContext* update_context(
|
ControllerProfileRuntimeContext* update_context(
|
||||||
|
|
@ -91,7 +157,75 @@ ControllerProfileRuntimeContext* update_context(
|
||||||
}
|
}
|
||||||
return &context;
|
return &context;
|
||||||
}
|
}
|
||||||
|
void process_profile_switching(
|
||||||
|
ControllerProfileRuntimeContext* context,
|
||||||
|
const ControllerIdentity& identity,
|
||||||
|
uint16_t switching_input_button_mask,
|
||||||
|
uint16_t current_input_button_mask,
|
||||||
|
ControllerState* consumed_input) {
|
||||||
|
if (context == nullptr || consumed_input == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context->switching_chord_held) {
|
||||||
|
if ((switching_input_button_mask &
|
||||||
|
context->held_switching_chord) ==
|
||||||
|
context->held_switching_chord) {
|
||||||
|
controller_profile_apply_button_mask(
|
||||||
|
static_cast<uint16_t>(
|
||||||
|
current_input_button_mask &
|
||||||
|
~context->held_switching_chord),
|
||||||
|
consumed_input);
|
||||||
|
if (!context->switching_activation_requested) {
|
||||||
|
const ConfigurationTransactionStatus status =
|
||||||
|
profile_service_activate_internal(
|
||||||
|
context->switching_transaction_id, identity,
|
||||||
|
context->switching_target_profile_index);
|
||||||
|
if (status != ConfigurationTransactionStatus::kBusy) {
|
||||||
|
context->switching_activation_requested = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context->switching_chord_held = false;
|
||||||
|
context->switching_activation_requested = false;
|
||||||
|
context->held_switching_chord = 0;
|
||||||
|
context->switching_transaction_id = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t chord =
|
||||||
|
effective_switching_chord(context->profile);
|
||||||
|
const bool chord_fully_held =
|
||||||
|
(switching_input_button_mask & chord) == chord;
|
||||||
|
if (!chord_fully_held) {
|
||||||
|
context->switching_chord_armed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context->switching_chord_armed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context->switching_chord_armed = false;
|
||||||
|
context->switching_chord_held = true;
|
||||||
|
context->held_switching_chord = chord;
|
||||||
|
context->switching_target_profile_index =
|
||||||
|
static_cast<uint8_t>(
|
||||||
|
(context->active_profile_index + 1u) %
|
||||||
|
CONTROLLER_PROFILE_COUNT);
|
||||||
|
context->switching_transaction_id =
|
||||||
|
next_activation_transaction_id();
|
||||||
|
controller_profile_apply_button_mask(
|
||||||
|
static_cast<uint16_t>(
|
||||||
|
current_input_button_mask & ~context->held_switching_chord),
|
||||||
|
consumed_input);
|
||||||
|
const ConfigurationTransactionStatus status =
|
||||||
|
profile_service_activate_internal(
|
||||||
|
context->switching_transaction_id, identity,
|
||||||
|
context->switching_target_profile_index);
|
||||||
|
if (status != ConfigurationTransactionStatus::kBusy) {
|
||||||
|
context->switching_activation_requested = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
@ -101,6 +235,7 @@ void controller_profile_runtime_reset() {
|
||||||
for (ControllerProfileRuntimeContext& context : g_contexts) {
|
for (ControllerProfileRuntimeContext& context : g_contexts) {
|
||||||
clear_context(&context);
|
clear_context(&context);
|
||||||
}
|
}
|
||||||
|
g_next_activation_sequence = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||||
|
|
@ -112,6 +247,12 @@ ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||||
if (context == nullptr) {
|
if (context == nullptr) {
|
||||||
return g_neutral_output;
|
return g_neutral_output;
|
||||||
}
|
}
|
||||||
|
ControllerState consumed_input = snapshot.state;
|
||||||
|
const uint16_t current_input_button_mask =
|
||||||
|
controller_profile_extract_button_mask(snapshot.state);
|
||||||
|
process_profile_switching(
|
||||||
|
context, snapshot.identity, snapshot.pre_hotkey_button_mask,
|
||||||
|
current_input_button_mask, &consumed_input);
|
||||||
|
|
||||||
const uint32_t reset_generation =
|
const uint32_t reset_generation =
|
||||||
configuration_service_reset_generation();
|
configuration_service_reset_generation();
|
||||||
|
|
@ -124,12 +265,32 @@ ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||||
reset_generation) {
|
reset_generation) {
|
||||||
controller_synthetic_input_cancel(
|
controller_synthetic_input_cancel(
|
||||||
&context->synthetic,
|
&context->synthetic,
|
||||||
controller_profile_extract_button_mask(snapshot.state));
|
controller_profile_extract_button_mask(consumed_input));
|
||||||
context->output_mode = output_mode;
|
context->output_mode = output_mode;
|
||||||
context->configuration_reset_generation = reset_generation;
|
context->configuration_reset_generation = reset_generation;
|
||||||
}
|
}
|
||||||
return controller_synthetic_input_apply(
|
return controller_synthetic_input_apply(
|
||||||
&context->synthetic, snapshot.state, context->profile, now_ms);
|
&context->synthetic, consumed_input, context->profile, now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool controller_profile_runtime_take_profile_change(
|
||||||
|
uint8_t slot, ControllerProfileRuntimeProfileChangeEvent* output) {
|
||||||
|
if (output == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*output = {};
|
||||||
|
if (slot >= CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerProfileRuntimeContext& context = g_contexts[slot];
|
||||||
|
if (!context.profile_change_pending) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*output = context.pending_profile_change;
|
||||||
|
context.profile_change_pending = false;
|
||||||
|
context.pending_profile_change = {};
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,24 @@
|
||||||
#include "controller_profile_transform.h"
|
#include "controller_profile_transform.h"
|
||||||
|
|
||||||
constexpr uint8_t CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT = 4;
|
constexpr uint8_t CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT = 4;
|
||||||
|
constexpr uint16_t CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD =
|
||||||
|
static_cast<uint16_t>(
|
||||||
|
(1u << static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kLeftShoulder)) |
|
||||||
|
(1u << static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kRightShoulder)) |
|
||||||
|
(1u << static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kSelect)) |
|
||||||
|
(1u << static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kStart)));
|
||||||
|
|
||||||
|
struct ControllerProfileRuntimeProfileChangeEvent {
|
||||||
|
uint32_t connection_generation = 0;
|
||||||
|
uint32_t database_generation = 0;
|
||||||
|
uint8_t active_profile_number = 0;
|
||||||
|
ControllerProfileConfirmationPolicy policy =
|
||||||
|
ControllerProfileConfirmationPolicy::kNone;
|
||||||
|
};
|
||||||
|
|
||||||
struct ControllerProfileRuntimeLocalConfirmation {
|
struct ControllerProfileRuntimeLocalConfirmation {
|
||||||
ControllerRumbleOutput rumble{};
|
ControllerRumbleOutput rumble{};
|
||||||
|
|
@ -18,14 +36,21 @@ struct ControllerProfileRuntimeLocalConfirmation {
|
||||||
// Reset all four fixed slot caches to the default profile.
|
// Reset all four fixed slot caches to the default profile.
|
||||||
void controller_profile_runtime_reset();
|
void controller_profile_runtime_reset();
|
||||||
|
|
||||||
// Refresh a slot only when its connection key or database generation changes,
|
// Refresh a slot when its identity, connection generation, or database
|
||||||
// cancel synthetic state on every runtime invalidation, then apply the shared
|
// generation changes; identity-only promotion preserves a held switching
|
||||||
// transform and synthetic pipeline. Inactive snapshots return neutral output
|
// transaction. Consume pre-hotkey switching chords while transforming the
|
||||||
// and invalidate the slot immediately.
|
// backend-suppressed state through the shared synthetic pipeline. Inactive
|
||||||
|
// snapshots return neutral output and invalidate the slot.
|
||||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot, uint32_t now_ms,
|
uint8_t slot, const Bluepad32SlotSnapshot& snapshot, uint32_t now_ms,
|
||||||
AdapterUsbMode output_mode);
|
AdapterUsbMode output_mode);
|
||||||
|
|
||||||
|
// Take the single committed profile-index change observed by the slot. Initial
|
||||||
|
// profile loads, identity promotions, and connection replacements do not
|
||||||
|
// publish an event.
|
||||||
|
bool controller_profile_runtime_take_profile_change(
|
||||||
|
uint8_t slot, ControllerProfileRuntimeProfileChangeEvent* output);
|
||||||
|
|
||||||
// Refresh from the current slot snapshot and scale host-originated rumble.
|
// Refresh from the current slot snapshot and scale host-originated rumble.
|
||||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ enum class PendingCommandType : uint8_t {
|
||||||
|
|
||||||
struct PendingCommand {
|
struct PendingCommand {
|
||||||
PendingCommandType type = PendingCommandType::kNone;
|
PendingCommandType type = PendingCommandType::kNone;
|
||||||
|
uint32_t transaction_id = 0;
|
||||||
ControllerIdentity identity{};
|
ControllerIdentity identity{};
|
||||||
uint8_t profile_index = 0;
|
uint8_t profile_index = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -48,6 +49,7 @@ PublishedActiveProfile
|
||||||
uint8_t g_active_profile_count = 0;
|
uint8_t g_active_profile_count = 0;
|
||||||
ProfileTransaction g_transaction;
|
ProfileTransaction g_transaction;
|
||||||
PendingCommand g_command;
|
PendingCommand g_command;
|
||||||
|
PendingCommand g_internal_activation;
|
||||||
bool g_identity_dirty = false;
|
bool g_identity_dirty = false;
|
||||||
bool g_has_committed = false;
|
bool g_has_committed = false;
|
||||||
uint32_t g_last_commit_ms = 0;
|
uint32_t g_last_commit_ms = 0;
|
||||||
|
|
@ -159,6 +161,17 @@ void finish_mutation(ConfigurationTransactionStatus status,
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void finish_internal_activation(
|
||||||
|
ConfigurationTransactionStatus status) {
|
||||||
|
critical_section_enter_blocking(&g_lock);
|
||||||
|
g_internal_activation = {};
|
||||||
|
refresh_metadata_locked(
|
||||||
|
status == ConfigurationTransactionStatus::kStorageError
|
||||||
|
? ProfileServiceState::kStorageError
|
||||||
|
: ProfileServiceState::kReady);
|
||||||
|
critical_section_exit(&g_lock);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void profile_service_prepare() {
|
void profile_service_prepare() {
|
||||||
|
|
@ -176,6 +189,7 @@ void profile_service_prepare() {
|
||||||
g_selected.profile_index = 0;
|
g_selected.profile_index = 0;
|
||||||
g_transaction = {};
|
g_transaction = {};
|
||||||
g_command = {};
|
g_command = {};
|
||||||
|
g_internal_activation = {};
|
||||||
g_identity_dirty = false;
|
g_identity_dirty = false;
|
||||||
g_has_committed = false;
|
g_has_committed = false;
|
||||||
g_last_commit_ms = 0;
|
g_last_commit_ms = 0;
|
||||||
|
|
@ -226,6 +240,7 @@ bool profile_service_observe_identity_on_storage_core(
|
||||||
void profile_service_task_on_storage_core(uint32_t now_ms) {
|
void profile_service_task_on_storage_core(uint32_t now_ms) {
|
||||||
PendingCommand command{};
|
PendingCommand command{};
|
||||||
bool process_write = false;
|
bool process_write = false;
|
||||||
|
bool process_internal_activation = false;
|
||||||
bool process_identity = false;
|
bool process_identity = false;
|
||||||
ControllerIdentity write_identity{};
|
ControllerIdentity write_identity{};
|
||||||
uint8_t write_profile_index = 0;
|
uint8_t write_profile_index = 0;
|
||||||
|
|
@ -242,6 +257,10 @@ void profile_service_task_on_storage_core(uint32_t now_ms) {
|
||||||
write_profile_index = g_transaction.profile_index;
|
write_profile_index = g_transaction.profile_index;
|
||||||
memcpy(write_payload, g_transaction.payload,
|
memcpy(write_payload, g_transaction.payload,
|
||||||
sizeof(write_payload));
|
sizeof(write_payload));
|
||||||
|
} else if (g_internal_activation.type !=
|
||||||
|
PendingCommandType::kNone) {
|
||||||
|
command = g_internal_activation;
|
||||||
|
process_internal_activation = true;
|
||||||
} else if (g_identity_dirty) {
|
} else if (g_identity_dirty) {
|
||||||
process_identity = true;
|
process_identity = true;
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +268,7 @@ void profile_service_task_on_storage_core(uint32_t now_ms) {
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
|
|
||||||
if (!process_write && !process_identity &&
|
if (!process_write && !process_identity &&
|
||||||
|
!process_internal_activation &&
|
||||||
command.type == PendingCommandType::kNone) {
|
command.type == PendingCommandType::kNone) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -279,8 +299,13 @@ void profile_service_task_on_storage_core(uint32_t now_ms) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (database_result != ControllerProfileDatabaseResult::kOk) {
|
if (database_result != ControllerProfileDatabaseResult::kOk) {
|
||||||
finish_mutation(database_result_status(database_result),
|
const ConfigurationTransactionStatus error_status =
|
||||||
!process_write);
|
database_result_status(database_result);
|
||||||
|
if (process_internal_activation) {
|
||||||
|
finish_internal_activation(error_status);
|
||||||
|
} else {
|
||||||
|
finish_mutation(error_status, !process_write);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,8 +340,12 @@ void profile_service_task_on_storage_core(uint32_t now_ms) {
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
g_identity_dirty = false;
|
g_identity_dirty = false;
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
|
if (process_internal_activation) {
|
||||||
|
finish_internal_activation(status);
|
||||||
|
} else {
|
||||||
finish_mutation(status, !process_write);
|
finish_mutation(status, !process_write);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfigurationTransactionStatus profile_service_select(
|
ConfigurationTransactionStatus profile_service_select(
|
||||||
const ControllerIdentity& identity, uint8_t profile_index) {
|
const ControllerIdentity& identity, uint8_t profile_index) {
|
||||||
|
|
@ -362,6 +391,7 @@ ConfigurationTransactionStatus profile_service_begin(
|
||||||
}
|
}
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
if (g_command.type != PendingCommandType::kNone ||
|
if (g_command.type != PendingCommandType::kNone ||
|
||||||
|
g_internal_activation.type != PendingCommandType::kNone ||
|
||||||
g_transaction.snapshot.status ==
|
g_transaction.snapshot.status ==
|
||||||
ConfigurationTransactionStatus::kReceiving ||
|
ConfigurationTransactionStatus::kReceiving ||
|
||||||
g_transaction.snapshot.status ==
|
g_transaction.snapshot.status ==
|
||||||
|
|
@ -465,6 +495,7 @@ ConfigurationTransactionStatus profile_service_reset(
|
||||||
}
|
}
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
if (g_command.type != PendingCommandType::kNone ||
|
if (g_command.type != PendingCommandType::kNone ||
|
||||||
|
g_internal_activation.type != PendingCommandType::kNone ||
|
||||||
g_transaction.snapshot.status ==
|
g_transaction.snapshot.status ==
|
||||||
ConfigurationTransactionStatus::kReceiving ||
|
ConfigurationTransactionStatus::kReceiving ||
|
||||||
g_transaction.snapshot.status ==
|
g_transaction.snapshot.status ==
|
||||||
|
|
@ -485,6 +516,7 @@ ConfigurationTransactionStatus profile_service_reset(
|
||||||
return ConfigurationTransactionStatus::kMalformed;
|
return ConfigurationTransactionStatus::kMalformed;
|
||||||
}
|
}
|
||||||
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
||||||
|
g_command.transaction_id = transaction_id;
|
||||||
g_command.type = PendingCommandType::kReset;
|
g_command.type = PendingCommandType::kReset;
|
||||||
g_command.identity = identity;
|
g_command.identity = identity;
|
||||||
g_command.profile_index = profile_index;
|
g_command.profile_index = profile_index;
|
||||||
|
|
@ -500,6 +532,7 @@ ConfigurationTransactionStatus profile_service_activate(
|
||||||
}
|
}
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
if (g_command.type != PendingCommandType::kNone ||
|
if (g_command.type != PendingCommandType::kNone ||
|
||||||
|
g_internal_activation.type != PendingCommandType::kNone ||
|
||||||
g_transaction.snapshot.status ==
|
g_transaction.snapshot.status ==
|
||||||
ConfigurationTransactionStatus::kReceiving ||
|
ConfigurationTransactionStatus::kReceiving ||
|
||||||
g_transaction.snapshot.status ==
|
g_transaction.snapshot.status ==
|
||||||
|
|
@ -519,6 +552,7 @@ ConfigurationTransactionStatus profile_service_activate(
|
||||||
return ConfigurationTransactionStatus::kMalformed;
|
return ConfigurationTransactionStatus::kMalformed;
|
||||||
}
|
}
|
||||||
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
||||||
|
g_command.transaction_id = transaction_id;
|
||||||
g_command.type = PendingCommandType::kActivate;
|
g_command.type = PendingCommandType::kActivate;
|
||||||
g_command.identity = identity;
|
g_command.identity = identity;
|
||||||
g_command.profile_index = profile_index;
|
g_command.profile_index = profile_index;
|
||||||
|
|
@ -526,6 +560,36 @@ ConfigurationTransactionStatus profile_service_activate(
|
||||||
return ConfigurationTransactionStatus::kPending;
|
return ConfigurationTransactionStatus::kPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigurationTransactionStatus profile_service_activate_internal(
|
||||||
|
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||||
|
uint8_t profile_index) {
|
||||||
|
if (!g_prepared) {
|
||||||
|
profile_service_prepare();
|
||||||
|
}
|
||||||
|
critical_section_enter_blocking(&g_lock);
|
||||||
|
if (g_command.type != PendingCommandType::kNone ||
|
||||||
|
g_internal_activation.type != PendingCommandType::kNone ||
|
||||||
|
g_transaction.snapshot.status ==
|
||||||
|
ConfigurationTransactionStatus::kReceiving ||
|
||||||
|
g_transaction.snapshot.status ==
|
||||||
|
ConfigurationTransactionStatus::kPending) {
|
||||||
|
critical_section_exit(&g_lock);
|
||||||
|
return ConfigurationTransactionStatus::kBusy;
|
||||||
|
}
|
||||||
|
if ((transaction_id & 0x80000000u) == 0 ||
|
||||||
|
!valid_identity(identity) ||
|
||||||
|
profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||||
|
critical_section_exit(&g_lock);
|
||||||
|
return ConfigurationTransactionStatus::kMalformed;
|
||||||
|
}
|
||||||
|
g_internal_activation.type = PendingCommandType::kActivate;
|
||||||
|
g_internal_activation.transaction_id = transaction_id;
|
||||||
|
g_internal_activation.identity = identity;
|
||||||
|
g_internal_activation.profile_index = profile_index;
|
||||||
|
critical_section_exit(&g_lock);
|
||||||
|
return ConfigurationTransactionStatus::kPending;
|
||||||
|
}
|
||||||
|
|
||||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output) {
|
void profile_service_list_snapshot(ProfileServiceListSnapshot* output) {
|
||||||
if (output == nullptr) {
|
if (output == nullptr) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,11 @@ ConfigurationTransactionStatus profile_service_reset(
|
||||||
ConfigurationTransactionStatus profile_service_activate(
|
ConfigurationTransactionStatus profile_service_activate(
|
||||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||||
uint8_t profile_index);
|
uint8_t profile_index);
|
||||||
|
// Queue a controller-originated activation without replacing the host-visible
|
||||||
|
// transaction snapshot. transaction_id must be nonzero with its high bit set.
|
||||||
|
ConfigurationTransactionStatus profile_service_activate_internal(
|
||||||
|
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||||
|
uint8_t profile_index);
|
||||||
|
|
||||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output);
|
void profile_service_list_snapshot(ProfileServiceListSnapshot* output);
|
||||||
void profile_service_selected_snapshot(
|
void profile_service_selected_snapshot(
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,14 @@ int main() {
|
||||||
const ControllerProfileTransformResult transformed =
|
const ControllerProfileTransformResult transformed =
|
||||||
controller_profile_runtime_transform(
|
controller_profile_runtime_transform(
|
||||||
instance, snapshot, now_ms, output_mode);
|
instance, snapshot, now_ms, output_mode);
|
||||||
|
ControllerProfileRuntimeProfileChangeEvent profile_change{};
|
||||||
|
if (controller_profile_runtime_take_profile_change(
|
||||||
|
instance, &profile_change)) {
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
instance, profile_change.connection_generation,
|
||||||
|
profile_change.active_profile_number,
|
||||||
|
profile_change.policy);
|
||||||
|
}
|
||||||
g_user_states[instance] = transformed.state;
|
g_user_states[instance] = transformed.state;
|
||||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
bool sent = false;
|
bool sent = false;
|
||||||
|
|
|
||||||
|
|
@ -1039,7 +1039,8 @@ void test_independent_lifecycle() {
|
||||||
"unstable replacement must not be enrolled for profiles");
|
"unstable replacement must not be enrolled for profiles");
|
||||||
|
|
||||||
g_slots[3].pending_rumble = {
|
g_slots[3].pending_rumble = {
|
||||||
3, disconnected_generation, ControllerRumbleOutput{77, 88}};
|
3, disconnected_generation, ControllerRumbleOutput{77, 88},
|
||||||
|
kXInputHostRumbleDurationMs};
|
||||||
g_slots[3].rumble_pending = true;
|
g_slots[3].rumble_pending = true;
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
require(slot_three_replacement.rumble_calls == 0,
|
require(slot_three_replacement.rumble_calls == 0,
|
||||||
|
|
@ -1273,102 +1274,437 @@ void test_slot_lighting() {
|
||||||
"controller without RGB support did not receive its slot LED");
|
"controller without RGB support did not receive its slot LED");
|
||||||
}
|
}
|
||||||
|
|
||||||
void require_south_button_mapping(const ControllerState& state,
|
void test_profile_chord_remains_raw() {
|
||||||
bool swapped,
|
|
||||||
const char* message) {
|
|
||||||
require(state.button_east == swapped && state.button_south == !swapped &&
|
|
||||||
!state.button_north && !state.button_west,
|
|
||||||
message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_abxy_hotkey() {
|
|
||||||
start_pairing_backend();
|
start_pairing_backend();
|
||||||
uni_hid_device_t slot_zero = device(0);
|
uni_hid_device_t controller = device(0);
|
||||||
uni_hid_device_t slot_one = device(1);
|
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
|
||||||
require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS &&
|
"raw profile chord controller did not become ready");
|
||||||
platform_on_device_ready(&slot_one) == UNI_ERROR_SUCCESS,
|
|
||||||
"ABXY test controllers did not become ready");
|
|
||||||
|
|
||||||
uni_controller_t input{};
|
uni_controller_t input{};
|
||||||
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||||
input.gamepad.buttons = BUTTON_A;
|
|
||||||
platform_on_controller_data(&slot_zero, &input);
|
|
||||||
ControllerState snapshot{};
|
|
||||||
require(read_controller_state(0, &snapshot),
|
|
||||||
"slot 0 ABXY state was not published");
|
|
||||||
require_south_button_mapping(
|
|
||||||
snapshot, kDefaultSwapAbxy,
|
|
||||||
"slot 0 did not start in the configured ABXY layout");
|
|
||||||
|
|
||||||
input.gamepad.buttons =
|
input.gamepad.buttons =
|
||||||
kAbxyHotkeyButtonMask | BUTTON_A;
|
BUTTON_A | BUTTON_SHOULDER_L | BUTTON_SHOULDER_R;
|
||||||
input.gamepad.misc_buttons = kAbxyHotkeyMiscMask;
|
input.gamepad.misc_buttons =
|
||||||
platform_on_controller_data(&slot_zero, &input);
|
MISC_BUTTON_SELECT | MISC_BUTTON_START;
|
||||||
require(read_controller_state(0, &snapshot),
|
platform_on_controller_data(&controller, &input);
|
||||||
"toggled slot 0 state was not published");
|
ControllerState snapshot{};
|
||||||
require_south_button_mapping(
|
require(read_controller_state(0, &snapshot) &&
|
||||||
snapshot, !kDefaultSwapAbxy,
|
snapshot.button_south && !snapshot.button_east &&
|
||||||
"hotkey did not toggle slot 0 ABXY mapping");
|
snapshot.button_left_shoulder &&
|
||||||
require(!snapshot.button_left_shoulder && !snapshot.button_right_shoulder &&
|
snapshot.button_right_shoulder &&
|
||||||
!snapshot.button_select && !snapshot.button_start,
|
snapshot.button_select && snapshot.button_start,
|
||||||
"hotkey chord leaked into the Switch report");
|
"Core 1 suppressed the profile chord or mutated ABXY mapping");
|
||||||
|
|
||||||
bluepad32_input_backend_queue_rumble(
|
|
||||||
0, ControllerRumbleOutput{0x11, 0x22});
|
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
require(slot_zero.rumble_calls == 1 &&
|
require(controller.rumble_calls == 0 &&
|
||||||
slot_zero.last_high == kAbxyFeedbackWeakMagnitude &&
|
|
||||||
slot_zero.last_low == kAbxyFeedbackStrongMagnitude &&
|
|
||||||
slot_zero.last_high == UINT8_MAX &&
|
|
||||||
slot_zero.last_low == UINT8_MAX &&
|
|
||||||
g_slots[0].rumble_pending,
|
|
||||||
"ABXY confirmation was not full-strength or did not take priority");
|
|
||||||
|
|
||||||
platform_on_controller_data(&slot_zero, &input);
|
|
||||||
process_rumble_timer(&g_rumble_timer);
|
|
||||||
require(g_slots[0].swap_abxy == !kDefaultSwapAbxy &&
|
|
||||||
slot_zero.rumble_calls == 1,
|
|
||||||
"held hotkey toggled or rumbled more than once");
|
|
||||||
|
|
||||||
input.gamepad = {};
|
|
||||||
platform_on_controller_data(&slot_zero, &input);
|
|
||||||
input.gamepad.buttons = kAbxyHotkeyButtonMask | BUTTON_A;
|
|
||||||
input.gamepad.misc_buttons = kAbxyHotkeyMiscMask;
|
|
||||||
platform_on_controller_data(&slot_zero, &input);
|
|
||||||
process_rumble_timer(&g_rumble_timer);
|
|
||||||
require(g_slots[0].swap_abxy == kDefaultSwapAbxy &&
|
|
||||||
slot_zero.rumble_calls == 2,
|
|
||||||
"released hotkey did not re-arm for a second toggle");
|
|
||||||
|
|
||||||
now_ms = kAbxyFeedbackDurationMs - 1;
|
|
||||||
process_rumble_timer(&g_rumble_timer);
|
|
||||||
require(slot_zero.rumble_calls == 2 && g_slots[0].rumble_pending,
|
|
||||||
"host rumble interrupted ABXY confirmation");
|
|
||||||
now_ms = kAbxyFeedbackDurationMs;
|
|
||||||
process_rumble_timer(&g_rumble_timer);
|
|
||||||
require(slot_zero.rumble_calls == 3 &&
|
|
||||||
slot_zero.last_high == 0x22 &&
|
|
||||||
slot_zero.last_low == 0x11 &&
|
|
||||||
!g_slots[0].rumble_pending,
|
|
||||||
"deferred host rumble did not resume after confirmation");
|
|
||||||
|
|
||||||
uni_controller_t peer_input{};
|
|
||||||
peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
|
||||||
peer_input.gamepad.buttons = BUTTON_A;
|
|
||||||
platform_on_controller_data(&slot_one, &peer_input);
|
|
||||||
require(read_controller_state(1, &snapshot),
|
|
||||||
"slot 1 ABXY state was not published");
|
|
||||||
require_south_button_mapping(
|
|
||||||
snapshot, kDefaultSwapAbxy,
|
|
||||||
"slot 0 hotkey changed slot 1 layout");
|
|
||||||
|
|
||||||
platform_on_device_disconnected(&slot_zero);
|
|
||||||
uni_hid_device_t replacement = device(0);
|
|
||||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
|
||||||
g_slots[0].swap_abxy == kDefaultSwapAbxy &&
|
|
||||||
!g_slots[0].abxy_hotkey_latched &&
|
|
||||||
!g_slots[0].feedback_pending,
|
!g_slots[0].feedback_pending,
|
||||||
"disconnect did not reset slot 0 hotkey state");
|
"legacy ABXY chord still produced local feedback");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_profile_feedback_scheduler() {
|
||||||
|
start_pairing_backend();
|
||||||
|
uni_hid_device_t devices[kSlotCount] = {
|
||||||
|
device(0), device(1), device(2), device(3)};
|
||||||
|
devices[0].report_parser.set_lightbar_color = set_lightbar;
|
||||||
|
devices[1].report_parser.set_player_leds = set_player_leds;
|
||||||
|
devices[2].report_parser.set_lightbar_color = set_lightbar;
|
||||||
|
devices[3].report_parser.set_player_leds = set_player_leds;
|
||||||
|
|
||||||
|
uint32_t generations[kSlotCount]{};
|
||||||
|
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
require(platform_on_device_ready(&devices[slot]) ==
|
||||||
|
UNI_ERROR_SUCCESS,
|
||||||
|
"profile feedback controller did not become ready");
|
||||||
|
Bluepad32SlotSnapshot snapshot{};
|
||||||
|
bluepad32_input_backend_snapshot(slot, &snapshot);
|
||||||
|
generations[slot] = snapshot.connection_generation;
|
||||||
|
require(devices[slot].rumble_calls == 0 &&
|
||||||
|
!g_slots[slot].profile_feedback.active &&
|
||||||
|
!g_slots[slot].profile_feedback_pending,
|
||||||
|
"initial controller/profile load scheduled confirmation feedback");
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
slot, generations[slot], static_cast<uint8_t>(slot + 1u),
|
||||||
|
ControllerProfileConfirmationPolicy::kRumble);
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
slot, ControllerRumbleOutput{
|
||||||
|
static_cast<uint8_t>(0x10u + slot),
|
||||||
|
static_cast<uint8_t>(0x20u + slot)});
|
||||||
|
}
|
||||||
|
|
||||||
|
now_ms = 0;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
require(devices[slot].rumble_calls == 1 &&
|
||||||
|
devices[slot].last_rumble_duration_ms ==
|
||||||
|
kProfileFeedbackPhaseDurationMs &&
|
||||||
|
devices[slot].last_high == UINT8_MAX &&
|
||||||
|
devices[slot].last_low == UINT8_MAX &&
|
||||||
|
g_slots[slot].rumble_pending &&
|
||||||
|
g_slots[slot].profile_feedback.active,
|
||||||
|
"profile pulse sequence did not start at full strength");
|
||||||
|
}
|
||||||
|
|
||||||
|
now_ms = 74;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 75;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
for (const uni_hid_device_t& controller : devices) {
|
||||||
|
require(controller.rumble_calls == 1,
|
||||||
|
"profile pulse did not retain a 75 ms on phase");
|
||||||
|
}
|
||||||
|
|
||||||
|
now_ms = 149;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 150;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[0].rumble_calls == 2 &&
|
||||||
|
devices[0].last_high == 0x20 &&
|
||||||
|
devices[0].last_low == 0x10 &&
|
||||||
|
!g_slots[0].rumble_pending,
|
||||||
|
"one-pulse confirmation did not defer host rumble through its off phase");
|
||||||
|
for (uint8_t slot = 1; slot < kSlotCount; ++slot) {
|
||||||
|
require(devices[slot].rumble_calls == 2 &&
|
||||||
|
devices[slot].last_high == UINT8_MAX &&
|
||||||
|
g_slots[slot].rumble_pending,
|
||||||
|
"second profile pulse did not start after 75 ms off");
|
||||||
|
}
|
||||||
|
|
||||||
|
now_ms = 225;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 300;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[1].rumble_calls == 3 &&
|
||||||
|
devices[1].last_high == 0x21 &&
|
||||||
|
!g_slots[1].rumble_pending &&
|
||||||
|
devices[2].rumble_calls == 3 &&
|
||||||
|
devices[3].rumble_calls == 3,
|
||||||
|
"two/three/four-pulse sequences diverged at 300 ms");
|
||||||
|
|
||||||
|
now_ms = 375;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 450;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[2].rumble_calls == 4 &&
|
||||||
|
devices[2].last_high == 0x22 &&
|
||||||
|
!g_slots[2].rumble_pending &&
|
||||||
|
devices[3].rumble_calls == 4 &&
|
||||||
|
devices[3].last_high == UINT8_MAX,
|
||||||
|
"three/four-pulse sequences diverged at 450 ms");
|
||||||
|
|
||||||
|
now_ms = 525;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 600;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[3].rumble_calls == 5 &&
|
||||||
|
devices[3].last_high == 0x23 &&
|
||||||
|
!g_slots[3].rumble_pending &&
|
||||||
|
devices[0].lightbar_calls == 1 &&
|
||||||
|
devices[1].player_led_calls == 1 &&
|
||||||
|
devices[2].lightbar_calls == 1 &&
|
||||||
|
devices[3].player_led_calls == 1,
|
||||||
|
"four-pulse confirmation did not release host rumble at 600 ms");
|
||||||
|
|
||||||
|
const int slot_zero_lightbar_calls = devices[0].lightbar_calls;
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
0, generations[0], 2,
|
||||||
|
ControllerProfileConfirmationPolicy::kNone);
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
0, ControllerRumbleOutput{0x31, 0x41});
|
||||||
|
now_ms = 700;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[0].rumble_calls == 3 &&
|
||||||
|
devices[0].last_high == 0x41 &&
|
||||||
|
devices[0].lightbar_calls ==
|
||||||
|
slot_zero_lightbar_calls &&
|
||||||
|
!g_slots[0].profile_feedback.active,
|
||||||
|
"none policy scheduled profile rumble or lighting");
|
||||||
|
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
0, generations[0], 2,
|
||||||
|
ControllerProfileConfirmationPolicy::kLed);
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
0, ControllerRumbleOutput{0x32, 0x42});
|
||||||
|
now_ms = 800;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[0].rumble_calls == 3 &&
|
||||||
|
devices[0].lightbar_calls ==
|
||||||
|
slot_zero_lightbar_calls + 1 &&
|
||||||
|
devices[0].lightbar_red ==
|
||||||
|
kProfileLightbarPalette[1].red &&
|
||||||
|
devices[0].lightbar_green ==
|
||||||
|
kProfileLightbarPalette[1].green &&
|
||||||
|
devices[0].lightbar_blue ==
|
||||||
|
kProfileLightbarPalette[1].blue &&
|
||||||
|
observed_status_led_on &&
|
||||||
|
g_slots[0].rumble_pending,
|
||||||
|
"LED policy did not set persistent profile color and first onboard blink");
|
||||||
|
now_ms = 875;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!observed_status_led_on &&
|
||||||
|
devices[0].rumble_calls == 3,
|
||||||
|
"onboard profile blink did not enter its 75 ms off phase");
|
||||||
|
now_ms = 950;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(observed_status_led_on,
|
||||||
|
"second onboard profile blink did not start");
|
||||||
|
now_ms = 1025;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!observed_status_led_on &&
|
||||||
|
g_slots[0].rumble_pending,
|
||||||
|
"host rumble interrupted the final onboard off phase");
|
||||||
|
now_ms = 1100;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[0].rumble_calls == 4 &&
|
||||||
|
devices[0].last_high == 0x42 &&
|
||||||
|
!g_slots[0].rumble_pending,
|
||||||
|
"LED-only sequence did not release deferred host rumble");
|
||||||
|
|
||||||
|
const int slot_one_player_led_calls =
|
||||||
|
devices[1].player_led_calls;
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
1, generations[1], 3,
|
||||||
|
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||||
|
now_ms = 1200;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[1].rumble_calls == 4 &&
|
||||||
|
devices[1].last_high == UINT8_MAX &&
|
||||||
|
devices[1].player_led_calls ==
|
||||||
|
slot_one_player_led_calls + 1 &&
|
||||||
|
devices[1].player_leds == 0x07 &&
|
||||||
|
observed_status_led_on,
|
||||||
|
"combined policy did not drive rumble, onboard LED, and player LEDs");
|
||||||
|
for (uint32_t deadline = 1275; deadline <= 1650;
|
||||||
|
deadline += 75) {
|
||||||
|
now_ms = deadline;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
}
|
||||||
|
require(devices[1].rumble_calls ==
|
||||||
|
(host_rumble_duration_ms() ==
|
||||||
|
kXInputHostRumbleDurationMs
|
||||||
|
? 7
|
||||||
|
: 6) &&
|
||||||
|
!g_slots[1].profile_feedback.active,
|
||||||
|
"combined three-pulse sequence did not terminate");
|
||||||
|
|
||||||
|
const int old_rumble_calls = devices[2].rumble_calls;
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
2, generations[2], 4,
|
||||||
|
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||||
|
platform_on_device_disconnected(&devices[2]);
|
||||||
|
uni_hid_device_t replacement = device(2);
|
||||||
|
replacement.report_parser.set_lightbar_color = set_lightbar;
|
||||||
|
require(platform_on_device_ready(&replacement) ==
|
||||||
|
UNI_ERROR_SUCCESS,
|
||||||
|
"replacement feedback controller did not become ready");
|
||||||
|
now_ms = 1700;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[2].rumble_calls == old_rumble_calls &&
|
||||||
|
replacement.rumble_calls == 0 &&
|
||||||
|
replacement.lightbar_calls == 1 &&
|
||||||
|
!g_slots[2].profile_feedback.active &&
|
||||||
|
!g_slots[2].profile_feedback_pending,
|
||||||
|
"slot replacement accepted stale queued profile feedback");
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
2, generations[2], 4,
|
||||||
|
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||||
|
now_ms = 1705;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(replacement.rumble_calls == 0 &&
|
||||||
|
replacement.lightbar_calls == 1,
|
||||||
|
"stale generation feedback reached a replacement controller");
|
||||||
|
|
||||||
|
const int slot_three_rumble_calls = devices[3].rumble_calls;
|
||||||
|
const int slot_three_player_led_calls =
|
||||||
|
devices[3].player_led_calls;
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
3, generations[3], 1,
|
||||||
|
ControllerProfileConfirmationPolicy::kLed);
|
||||||
|
now_ms = 1800;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(observed_status_led_on &&
|
||||||
|
g_slots[3].profile_feedback.pulses_started == 1 &&
|
||||||
|
devices[3].player_led_calls ==
|
||||||
|
slot_three_player_led_calls + 1 &&
|
||||||
|
devices[3].player_leds == 0x01,
|
||||||
|
"one-blink LED profile indication did not start");
|
||||||
|
now_ms = 1875;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!observed_status_led_on,
|
||||||
|
"one-blink LED profile indication did not turn off");
|
||||||
|
now_ms = 1950;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!g_slots[3].profile_feedback.active &&
|
||||||
|
devices[3].rumble_calls ==
|
||||||
|
slot_three_rumble_calls,
|
||||||
|
"one-blink LED-only profile indication did not terminate cleanly");
|
||||||
|
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
3, generations[3], 4,
|
||||||
|
ControllerProfileConfirmationPolicy::kLed);
|
||||||
|
now_ms = 2000;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[3].player_leds == 0x0f,
|
||||||
|
"profile 4 did not persist four player LEDs");
|
||||||
|
for (uint8_t pulse = 1; pulse <= 4; ++pulse) {
|
||||||
|
require(observed_status_led_on &&
|
||||||
|
g_slots[3].profile_feedback.on &&
|
||||||
|
g_slots[3].profile_feedback.pulses_started ==
|
||||||
|
pulse,
|
||||||
|
"four-blink LED sequence missed an on phase");
|
||||||
|
now_ms = static_cast<uint32_t>(
|
||||||
|
2075u + static_cast<uint32_t>(pulse - 1u) * 150u);
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!observed_status_led_on &&
|
||||||
|
!g_slots[3].profile_feedback.on,
|
||||||
|
"four-blink LED sequence missed an off phase");
|
||||||
|
if (pulse != 4) {
|
||||||
|
now_ms += 75;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
now_ms = 2600;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!g_slots[3].profile_feedback.active &&
|
||||||
|
devices[3].rumble_calls ==
|
||||||
|
slot_three_rumble_calls,
|
||||||
|
"four-blink LED-only profile indication did not terminate");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_stateful_host_rumble_restore() {
|
||||||
|
start_pairing_backend();
|
||||||
|
uni_hid_device_t devices[kSlotCount] = {
|
||||||
|
device(0), device(1), device(2), device(3)};
|
||||||
|
uint32_t generations[kSlotCount]{};
|
||||||
|
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
require(platform_on_device_ready(&devices[slot]) ==
|
||||||
|
UNI_ERROR_SUCCESS,
|
||||||
|
"rumble restore controller did not become ready");
|
||||||
|
Bluepad32SlotSnapshot snapshot{};
|
||||||
|
bluepad32_input_backend_snapshot(slot, &snapshot);
|
||||||
|
generations[slot] = snapshot.connection_generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
test_adapter_mode = AdapterUsbMode::kXInput;
|
||||||
|
const ControllerRumbleOutput desired[kSlotCount] = {
|
||||||
|
{0x11, 0x21}, {0x12, 0x22}, {0x13, 0x23}, {0x14, 0x24}};
|
||||||
|
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
bluepad32_input_backend_queue_rumble(slot, desired[slot]);
|
||||||
|
}
|
||||||
|
now_ms = 0;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
require(devices[slot].rumble_calls == 1 &&
|
||||||
|
devices[slot].last_rumble_duration_ms ==
|
||||||
|
kXInputHostRumbleDurationMs &&
|
||||||
|
g_slots[slot].retained_host_rumble_valid,
|
||||||
|
"initial XInput rumble was not dispatched and retained");
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
slot, generations[slot], static_cast<uint8_t>(slot + 1u),
|
||||||
|
ControllerProfileConfirmationPolicy::kRumble);
|
||||||
|
}
|
||||||
|
now_ms = 10;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
for (now_ms = 85; now_ms <= 610; now_ms += 75) {
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
}
|
||||||
|
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
require(devices[slot].rumble_calls ==
|
||||||
|
static_cast<int>(slot + 3u) &&
|
||||||
|
devices[slot].last_low ==
|
||||||
|
desired[slot].low_frequency_magnitude &&
|
||||||
|
devices[slot].last_high ==
|
||||||
|
desired[slot].high_frequency_magnitude &&
|
||||||
|
devices[slot].last_rumble_duration_ms ==
|
||||||
|
kXInputHostRumbleDurationMs &&
|
||||||
|
!g_slots[slot].profile_feedback.active,
|
||||||
|
"XInput rumble did not resume after its profile pulse count");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int stop_calls_before = devices[0].rumble_calls;
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
0, ControllerRumbleOutput{0x51, 0x61});
|
||||||
|
now_ms = 700;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
0, generations[0], 2,
|
||||||
|
ControllerProfileConfirmationPolicy::kRumble);
|
||||||
|
now_ms = 705;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
0, ControllerRumbleOutput{0, 0});
|
||||||
|
for (now_ms = 780; now_ms <= 1005; now_ms += 75) {
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
}
|
||||||
|
require(devices[0].rumble_calls == stop_calls_before + 4 &&
|
||||||
|
devices[0].last_rumble_duration_ms == 0 &&
|
||||||
|
devices[0].last_low == 0 &&
|
||||||
|
devices[0].last_high == 0 &&
|
||||||
|
g_slots[0].retained_host_rumble_valid,
|
||||||
|
"newest XInput stop did not win during profile pulses");
|
||||||
|
|
||||||
|
test_adapter_mode = AdapterUsbMode::kSwitchProbe;
|
||||||
|
const int switch_calls_before = devices[1].rumble_calls;
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
1, ControllerRumbleOutput{0x31, 0x41});
|
||||||
|
now_ms = 1100;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!g_slots[1].retained_host_rumble_valid,
|
||||||
|
"Switch rumble retained stale XInput desired state");
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
1, generations[1], 1,
|
||||||
|
ControllerProfileConfirmationPolicy::kRumble);
|
||||||
|
now_ms = 1105;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 1180;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 1255;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[1].rumble_calls == switch_calls_before + 2 &&
|
||||||
|
devices[1].last_rumble_duration_ms ==
|
||||||
|
kProfileFeedbackPhaseDurationMs,
|
||||||
|
"finite Switch rumble resumed after profile feedback");
|
||||||
|
|
||||||
|
test_adapter_mode = AdapterUsbMode::kXInput;
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
2, ControllerRumbleOutput{0x71, 0x81});
|
||||||
|
now_ms = 1300;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
2, generations[2], 3,
|
||||||
|
ControllerProfileConfirmationPolicy::kRumble);
|
||||||
|
now_ms = 1305;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
const int old_device_calls = devices[2].rumble_calls;
|
||||||
|
platform_on_device_disconnected(&devices[2]);
|
||||||
|
uni_hid_device_t replacement = device(2);
|
||||||
|
require(platform_on_device_ready(&replacement) ==
|
||||||
|
UNI_ERROR_SUCCESS,
|
||||||
|
"rumble restore replacement did not become ready");
|
||||||
|
now_ms = 2000;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[2].rumble_calls == old_device_calls &&
|
||||||
|
replacement.rumble_calls == 0 &&
|
||||||
|
!g_slots[2].retained_host_rumble_valid &&
|
||||||
|
!g_slots[2].profile_feedback.active,
|
||||||
|
"stale XInput rumble resumed on a replacement connection");
|
||||||
|
#else
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
0, ControllerRumbleOutput{0x31, 0x41});
|
||||||
|
now_ms = 0;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
bluepad32_input_backend_queue_profile_feedback(
|
||||||
|
0, generations[0], 1,
|
||||||
|
ControllerProfileConfirmationPolicy::kRumble);
|
||||||
|
now_ms = 5;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 80;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
now_ms = 155;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(devices[0].rumble_calls == 2 &&
|
||||||
|
!g_slots[0].retained_host_rumble_valid,
|
||||||
|
"bounded Switch rumble resumed after profile feedback");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_motion_hotkey() {
|
void test_motion_hotkey() {
|
||||||
|
|
@ -1393,12 +1729,17 @@ void test_motion_hotkey() {
|
||||||
input.gamepad.buttons = kMotionHotkeyButtonMask;
|
input.gamepad.buttons = kMotionHotkeyButtonMask;
|
||||||
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||||
platform_on_controller_data(&slot_zero, &input);
|
platform_on_controller_data(&slot_zero, &input);
|
||||||
|
Bluepad32SlotSnapshot backend_snapshot{};
|
||||||
|
bluepad32_input_backend_snapshot(0, &backend_snapshot);
|
||||||
require(read_controller_state(0, &snapshot) &&
|
require(read_controller_state(0, &snapshot) &&
|
||||||
|
backend_snapshot.pre_hotkey_button_mask ==
|
||||||
|
kMotionHotkeyLogicalButtonMask &&
|
||||||
snapshot.motion_sample_count ==
|
snapshot.motion_sample_count ==
|
||||||
(kDefaultMotionEnabled ? 0 : 3) &&
|
(kDefaultMotionEnabled ? 0 : 3) &&
|
||||||
!snapshot.dpad_up && !snapshot.button_right_shoulder &&
|
!snapshot.dpad_up && !snapshot.button_right_shoulder &&
|
||||||
!snapshot.button_start,
|
!snapshot.button_start,
|
||||||
"motion chord did not toggle motion or suppress its inputs");
|
"motion chord was not published pre-hotkey or suppressed "
|
||||||
|
"from normal output");
|
||||||
|
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
require(slot_zero.rumble_calls == 1 &&
|
require(slot_zero.rumble_calls == 1 &&
|
||||||
|
|
@ -1452,6 +1793,7 @@ void test_motion_hotkey() {
|
||||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
||||||
g_slots[0].motion_enabled == kDefaultMotionEnabled &&
|
g_slots[0].motion_enabled == kDefaultMotionEnabled &&
|
||||||
!g_slots[0].motion_hotkey_latched &&
|
!g_slots[0].motion_hotkey_latched &&
|
||||||
|
g_slots[0].pre_hotkey_button_mask == 0 &&
|
||||||
!g_slots[0].feedback_pending,
|
!g_slots[0].feedback_pending,
|
||||||
"disconnect did not reset slot 0 motion hotkey state");
|
"disconnect did not reset slot 0 motion hotkey state");
|
||||||
}
|
}
|
||||||
|
|
@ -1641,8 +1983,12 @@ int main(int argc, char** argv) {
|
||||||
test_pairing_window_policy();
|
test_pairing_window_policy();
|
||||||
} else if (scenario == "slot-lighting") {
|
} else if (scenario == "slot-lighting") {
|
||||||
test_slot_lighting();
|
test_slot_lighting();
|
||||||
} else if (scenario == "abxy-hotkey") {
|
} else if (scenario == "stateful-rumble") {
|
||||||
test_abxy_hotkey();
|
test_stateful_host_rumble_restore();
|
||||||
|
} else if (scenario == "profile-chord-raw") {
|
||||||
|
test_profile_chord_remains_raw();
|
||||||
|
} else if (scenario == "profile-feedback") {
|
||||||
|
test_profile_feedback_scheduler();
|
||||||
} else if (scenario == "motion-hotkey") {
|
} else if (scenario == "motion-hotkey") {
|
||||||
test_motion_hotkey();
|
test_motion_hotkey();
|
||||||
} else if (scenario == "analog-state") {
|
} else if (scenario == "analog-state") {
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,20 @@ struct FakeProfileRow {
|
||||||
uint8_t active_profile = 0;
|
uint8_t active_profile = 0;
|
||||||
ControllerProfile profiles[CONTROLLER_PROFILE_COUNT]{};
|
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{};
|
std::array<FakeProfileRow, CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT> rows{};
|
||||||
uint32_t database_generation = 7;
|
uint32_t database_generation = 7;
|
||||||
uint32_t configuration_reset_generation = 3;
|
uint32_t configuration_reset_generation = 3;
|
||||||
unsigned active_snapshot_count = 0;
|
unsigned active_snapshot_count = 0;
|
||||||
|
std::array<ActivationAttempt, 32> activation_attempts{};
|
||||||
|
size_t activation_attempt_count = 0;
|
||||||
|
unsigned activation_busy_attempts = 0;
|
||||||
|
|
||||||
void require(bool condition, const char* message) {
|
void require(bool condition, const char* message) {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
|
|
@ -44,6 +53,9 @@ void prepare_profiles() {
|
||||||
database_generation = 7;
|
database_generation = 7;
|
||||||
configuration_reset_generation = 3;
|
configuration_reset_generation = 3;
|
||||||
active_snapshot_count = 0;
|
active_snapshot_count = 0;
|
||||||
|
activation_attempts = {};
|
||||||
|
activation_attempt_count = 0;
|
||||||
|
activation_busy_attempts = 0;
|
||||||
for (uint8_t slot = 0;
|
for (uint8_t slot = 0;
|
||||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||||
FakeProfileRow& row = rows[slot];
|
FakeProfileRow& row = rows[slot];
|
||||||
|
|
@ -78,6 +90,31 @@ Bluepad32SlotSnapshot make_snapshot(uint8_t slot,
|
||||||
snapshot.state = controller_neutral_state();
|
snapshot.state = controller_neutral_state();
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
void apply_button_masks(uint16_t pre_hotkey_mask,
|
||||||
|
uint16_t output_mask,
|
||||||
|
Bluepad32SlotSnapshot* snapshot) {
|
||||||
|
snapshot->pre_hotkey_button_mask = pre_hotkey_mask;
|
||||||
|
snapshot->state = controller_neutral_state();
|
||||||
|
controller_profile_apply_button_mask(output_mask, &snapshot->state);
|
||||||
|
}
|
||||||
|
void apply_button_mask(uint16_t mask, Bluepad32SlotSnapshot* snapshot) {
|
||||||
|
apply_button_masks(mask, mask, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerProfileRuntimeProfileChangeEvent take_profile_change(
|
||||||
|
uint8_t slot, bool* available) {
|
||||||
|
ControllerProfileRuntimeProfileChangeEvent event{};
|
||||||
|
*available =
|
||||||
|
controller_profile_runtime_take_profile_change(slot, &event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton button) {
|
||||||
|
return static_cast<uint16_t>(
|
||||||
|
1u << static_cast<uint8_t>(button));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ControllerProfileTransformResult runtime_transform(
|
ControllerProfileTransformResult runtime_transform(
|
||||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||||
|
|
@ -248,6 +285,292 @@ void test_analog_thresholds_rumble_and_local_confirmation() {
|
||||||
"local confirmation was scaled or lost its profile policy");
|
"local confirmation was scaled or lost its profile policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_default_switching_retry_commit_and_feedback() {
|
||||||
|
prepare_profiles();
|
||||||
|
ControllerProfile& initial_profile = rows[0].profiles[0];
|
||||||
|
initial_profile.macro_trigger = static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kLeftShoulder);
|
||||||
|
initial_profile.macro_step_count = 2;
|
||||||
|
initial_profile.macro_steps[0].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
initial_profile.macro_steps[0].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
initial_profile.macro_steps[0].duration_ms = 1000;
|
||||||
|
initial_profile.macro_steps[0].output_button_mask =
|
||||||
|
logical_button_bit(ControllerProfileLogicalButton::kNorth);
|
||||||
|
initial_profile.macro_steps[1].type =
|
||||||
|
ControllerProfileMacroStepType::kEnd;
|
||||||
|
|
||||||
|
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||||
|
(void)runtime_transform(0, snapshot, 0);
|
||||||
|
bool event_available = true;
|
||||||
|
(void)take_profile_change(0, &event_available);
|
||||||
|
require(!event_available,
|
||||||
|
"initial profile load published confirmation feedback");
|
||||||
|
|
||||||
|
activation_busy_attempts = 2;
|
||||||
|
const uint16_t held_mask = static_cast<uint16_t>(
|
||||||
|
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD |
|
||||||
|
logical_button_bit(ControllerProfileLogicalButton::kSouth));
|
||||||
|
apply_button_mask(held_mask, &snapshot);
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
runtime_transform(0, snapshot, 1);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
rows[0].active_profile == 0 &&
|
||||||
|
output.state.button_south &&
|
||||||
|
!output.state.button_left_shoulder &&
|
||||||
|
!output.state.button_right_shoulder &&
|
||||||
|
!output.state.button_select &&
|
||||||
|
!output.state.button_start &&
|
||||||
|
!output.state.button_north,
|
||||||
|
"default chord was not consumed before a busy activation and synthetic trigger");
|
||||||
|
output = runtime_transform(0, snapshot, 2);
|
||||||
|
output = runtime_transform(0, snapshot, 3);
|
||||||
|
require(activation_attempt_count == 3 &&
|
||||||
|
activation_attempts[0].transaction_id ==
|
||||||
|
activation_attempts[1].transaction_id &&
|
||||||
|
activation_attempts[1].transaction_id ==
|
||||||
|
activation_attempts[2].transaction_id &&
|
||||||
|
(activation_attempts[0].transaction_id & 0x80000000u) != 0 &&
|
||||||
|
activation_attempts[0].transaction_id != 0 &&
|
||||||
|
activation_attempts[0].profile_index == 1 &&
|
||||||
|
output.state.button_south && !output.state.button_north,
|
||||||
|
"held busy chord did not retry one internal activation transaction");
|
||||||
|
|
||||||
|
(void)runtime_transform(0, snapshot, 4);
|
||||||
|
require(activation_attempt_count == 3,
|
||||||
|
"accepted activation repeated while the chord remained held");
|
||||||
|
(void)take_profile_change(0, &event_available);
|
||||||
|
require(!event_available,
|
||||||
|
"activation feedback was published before storage commit");
|
||||||
|
|
||||||
|
rows[0].active_profile = 1;
|
||||||
|
rows[0].profiles[1].button_map[
|
||||||
|
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth)] =
|
||||||
|
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
|
||||||
|
rows[0].profiles[1].confirmation_policy =
|
||||||
|
ControllerProfileConfirmationPolicy::kLed;
|
||||||
|
++database_generation;
|
||||||
|
output = runtime_transform(0, snapshot, 5);
|
||||||
|
const ControllerProfileRuntimeProfileChangeEvent event =
|
||||||
|
take_profile_change(0, &event_available);
|
||||||
|
require(event_available && event.connection_generation == 1 &&
|
||||||
|
event.database_generation == database_generation &&
|
||||||
|
event.active_profile_number == 2 &&
|
||||||
|
event.policy == ControllerProfileConfirmationPolicy::kLed &&
|
||||||
|
output.state.button_north &&
|
||||||
|
!output.state.button_south &&
|
||||||
|
!output.state.button_left_shoulder &&
|
||||||
|
activation_attempt_count == 3,
|
||||||
|
"committed activation did not cancel, transform, and publish exactly one event");
|
||||||
|
(void)take_profile_change(0, &event_available);
|
||||||
|
require(!event_available,
|
||||||
|
"committed profile change feedback was published twice");
|
||||||
|
|
||||||
|
apply_button_mask(0, &snapshot);
|
||||||
|
(void)runtime_transform(0, snapshot, 6);
|
||||||
|
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||||
|
&snapshot);
|
||||||
|
(void)runtime_transform(0, snapshot, 7);
|
||||||
|
require(activation_attempt_count == 4 &&
|
||||||
|
activation_attempts[3].profile_index == 2 &&
|
||||||
|
activation_attempts[3].transaction_id !=
|
||||||
|
activation_attempts[0].transaction_id &&
|
||||||
|
(activation_attempts[3].transaction_id & 0x80000000u) != 0,
|
||||||
|
"release did not re-arm one activation for the next profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_identity_promotion_preserves_held_switching() {
|
||||||
|
prepare_profiles();
|
||||||
|
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||||
|
snapshot.identity = controller_identity_global();
|
||||||
|
apply_button_mask(
|
||||||
|
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD, &snapshot);
|
||||||
|
|
||||||
|
const ControllerProfileTransformResult unresolved =
|
||||||
|
runtime_transform(0, snapshot, 0);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
controller_identity_is_global(
|
||||||
|
activation_attempts[0].identity) &&
|
||||||
|
unresolved.left_trigger_digital_threshold ==
|
||||||
|
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||||
|
"unresolved BLE identity did not begin one activation");
|
||||||
|
|
||||||
|
snapshot.identity = rows[0].identity;
|
||||||
|
const ControllerProfileTransformResult promoted =
|
||||||
|
runtime_transform(0, snapshot, 1);
|
||||||
|
bool event_available = true;
|
||||||
|
(void)take_profile_change(0, &event_available);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
promoted.left_trigger_digital_threshold == 1000 &&
|
||||||
|
!event_available,
|
||||||
|
"identity-only BLE promotion re-armed a held activation or "
|
||||||
|
"failed to refresh its profile");
|
||||||
|
|
||||||
|
(void)runtime_transform(0, snapshot, 2);
|
||||||
|
require(activation_attempt_count == 1,
|
||||||
|
"promoted BLE identity repeated an accepted held activation");
|
||||||
|
|
||||||
|
apply_button_mask(0, &snapshot);
|
||||||
|
(void)runtime_transform(0, snapshot, 3);
|
||||||
|
apply_button_mask(
|
||||||
|
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD, &snapshot);
|
||||||
|
(void)runtime_transform(0, snapshot, 4);
|
||||||
|
require(activation_attempt_count == 2 &&
|
||||||
|
controller_identity_equal(
|
||||||
|
activation_attempts[1].identity,
|
||||||
|
rows[0].identity),
|
||||||
|
"physical release did not re-arm the promoted identity");
|
||||||
|
}
|
||||||
|
|
||||||
|
void configure_motion_suppression_probe(ControllerProfile* profile) {
|
||||||
|
profile->macro_trigger = static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kDpadUp);
|
||||||
|
profile->macro_step_count = 2;
|
||||||
|
profile->macro_steps[0].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile->macro_steps[0].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile->macro_steps[0].duration_ms = 1000;
|
||||||
|
profile->macro_steps[0].output_button_mask =
|
||||||
|
logical_button_bit(ControllerProfileLogicalButton::kNorth);
|
||||||
|
profile->macro_steps[1].type =
|
||||||
|
ControllerProfileMacroStepType::kEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_switching_uses_pre_hotkey_buttons_only() {
|
||||||
|
constexpr uint16_t kMotionHotkeyLogicalMask =
|
||||||
|
static_cast<uint16_t>(
|
||||||
|
logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kDpadUp) |
|
||||||
|
logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kRightShoulder) |
|
||||||
|
logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kStart));
|
||||||
|
constexpr uint16_t kCustomChord =
|
||||||
|
static_cast<uint16_t>(
|
||||||
|
kMotionHotkeyLogicalMask |
|
||||||
|
logical_button_bit(
|
||||||
|
ControllerProfileLogicalButton::kSouth));
|
||||||
|
|
||||||
|
for (uint8_t custom = 0; custom < 2; ++custom) {
|
||||||
|
prepare_profiles();
|
||||||
|
ControllerProfile& profile = rows[0].profiles[0];
|
||||||
|
configure_motion_suppression_probe(&profile);
|
||||||
|
const uint16_t switching_chord =
|
||||||
|
custom != 0 ? kCustomChord
|
||||||
|
: CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD;
|
||||||
|
profile.switching_chord = custom != 0 ? kCustomChord : 0;
|
||||||
|
|
||||||
|
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||||
|
(void)runtime_transform(0, snapshot, 0);
|
||||||
|
const uint16_t pre_hotkey_mask = static_cast<uint16_t>(
|
||||||
|
switching_chord | kMotionHotkeyLogicalMask);
|
||||||
|
const uint16_t output_mask = static_cast<uint16_t>(
|
||||||
|
pre_hotkey_mask & ~kMotionHotkeyLogicalMask);
|
||||||
|
apply_button_masks(
|
||||||
|
pre_hotkey_mask, output_mask, &snapshot);
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
runtime_transform(0, snapshot, 1);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
!output.state.dpad_up &&
|
||||||
|
!output.state.button_right_shoulder &&
|
||||||
|
!output.state.button_start &&
|
||||||
|
!output.state.button_left_shoulder &&
|
||||||
|
!output.state.button_select &&
|
||||||
|
!output.state.button_south &&
|
||||||
|
!output.state.button_north,
|
||||||
|
custom != 0
|
||||||
|
? "custom chord hidden by motion suppression"
|
||||||
|
: "default chord hidden by motion suppression");
|
||||||
|
|
||||||
|
output = runtime_transform(0, snapshot, 2);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
!output.state.button_north,
|
||||||
|
"held pre-hotkey chord repeated activation or leaked "
|
||||||
|
"into synthetic input");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_custom_switching_chord_and_wrap() {
|
||||||
|
prepare_profiles();
|
||||||
|
constexpr uint16_t kCustomChord =
|
||||||
|
static_cast<uint16_t>(
|
||||||
|
logical_button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||||
|
logical_button_bit(ControllerProfileLogicalButton::kCapture));
|
||||||
|
rows[0].profiles[0].switching_chord = kCustomChord;
|
||||||
|
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||||
|
(void)runtime_transform(0, snapshot, 0);
|
||||||
|
|
||||||
|
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||||
|
&snapshot);
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
runtime_transform(0, snapshot, 1);
|
||||||
|
require(activation_attempt_count == 0 &&
|
||||||
|
output.state.button_left_shoulder &&
|
||||||
|
output.state.button_right_shoulder &&
|
||||||
|
output.state.button_select && output.state.button_start,
|
||||||
|
"nonzero switching chord did not replace the default");
|
||||||
|
|
||||||
|
apply_button_mask(kCustomChord, &snapshot);
|
||||||
|
output = runtime_transform(0, snapshot, 2);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
activation_attempts[0].profile_index == 1 &&
|
||||||
|
!output.state.button_south &&
|
||||||
|
!output.state.button_capture,
|
||||||
|
"custom switching chord was not consumed or activated");
|
||||||
|
|
||||||
|
prepare_profiles();
|
||||||
|
rows[0].active_profile = 3;
|
||||||
|
rows[0].profiles[3].switching_chord = kCustomChord;
|
||||||
|
snapshot = make_snapshot(0);
|
||||||
|
(void)runtime_transform(0, snapshot, 10);
|
||||||
|
bool event_available = true;
|
||||||
|
(void)take_profile_change(0, &event_available);
|
||||||
|
require(!event_available,
|
||||||
|
"initial profile 4 load published a change event");
|
||||||
|
apply_button_mask(kCustomChord, &snapshot);
|
||||||
|
(void)runtime_transform(0, snapshot, 11);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
activation_attempts[0].profile_index == 0,
|
||||||
|
"profile switching did not wrap profile 4 to profile 1");
|
||||||
|
}
|
||||||
|
void test_switching_slot_isolation() {
|
||||||
|
prepare_profiles();
|
||||||
|
std::array<Bluepad32SlotSnapshot,
|
||||||
|
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||||
|
snapshots{};
|
||||||
|
for (uint8_t slot = 0;
|
||||||
|
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||||
|
snapshots[slot] = make_snapshot(slot);
|
||||||
|
(void)runtime_transform(slot, snapshots[slot], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||||
|
&snapshots[2]);
|
||||||
|
(void)runtime_transform(2, snapshots[2], 1);
|
||||||
|
snapshots[0].state.button_south = true;
|
||||||
|
const ControllerProfileTransformResult untouched =
|
||||||
|
runtime_transform(0, snapshots[0], 1);
|
||||||
|
require(activation_attempt_count == 1 &&
|
||||||
|
controller_identity_equal(
|
||||||
|
activation_attempts[0].identity,
|
||||||
|
rows[2].identity) &&
|
||||||
|
untouched.state.button_south,
|
||||||
|
"one slot's switching chord affected another slot");
|
||||||
|
|
||||||
|
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||||
|
&snapshots[3]);
|
||||||
|
(void)runtime_transform(2, snapshots[2], 2);
|
||||||
|
(void)runtime_transform(3, snapshots[3], 2);
|
||||||
|
require(activation_attempt_count == 2 &&
|
||||||
|
controller_identity_equal(
|
||||||
|
activation_attempts[1].identity,
|
||||||
|
rows[3].identity),
|
||||||
|
"held switching state was shared between controller slots");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void configure_synthetic_profile(uint8_t slot) {
|
void configure_synthetic_profile(uint8_t slot) {
|
||||||
ControllerProfile& profile = rows[slot].profiles[0];
|
ControllerProfile& profile = rows[slot].profiles[0];
|
||||||
profile = controller_profile_default(rows[slot].identity, 0);
|
profile = controller_profile_default(rows[slot].identity, 0);
|
||||||
|
|
@ -347,6 +670,12 @@ void test_all_runtime_cancellation_causes() {
|
||||||
output = runtime_transform(0, snapshot, 33);
|
output = runtime_transform(0, snapshot, 33);
|
||||||
require_no_synthetic_output(
|
require_no_synthetic_output(
|
||||||
output, "profile/database generation change did not cancel");
|
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();
|
prepare_profiles();
|
||||||
configure_synthetic_profile(0);
|
configure_synthetic_profile(0);
|
||||||
|
|
@ -413,6 +742,20 @@ uint32_t profile_service_database_generation() {
|
||||||
uint32_t configuration_service_reset_generation() {
|
uint32_t configuration_service_reset_generation() {
|
||||||
return configuration_reset_generation;
|
return configuration_reset_generation;
|
||||||
}
|
}
|
||||||
|
ConfigurationTransactionStatus profile_service_activate_internal(
|
||||||
|
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||||
|
uint8_t profile_index) {
|
||||||
|
require(activation_attempt_count < activation_attempts.size(),
|
||||||
|
"activation attempt fixture overflow");
|
||||||
|
activation_attempts[activation_attempt_count++] = {
|
||||||
|
transaction_id, identity, profile_index};
|
||||||
|
if (activation_busy_attempts != 0) {
|
||||||
|
--activation_busy_attempts;
|
||||||
|
return ConfigurationTransactionStatus::kBusy;
|
||||||
|
}
|
||||||
|
return ConfigurationTransactionStatus::kPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void profile_service_active_profile_snapshot(
|
void profile_service_active_profile_snapshot(
|
||||||
const ControllerIdentity& identity,
|
const ControllerIdentity& identity,
|
||||||
|
|
@ -439,6 +782,11 @@ int main() {
|
||||||
test_four_slot_cache_and_unchanged_generation();
|
test_four_slot_cache_and_unchanged_generation();
|
||||||
test_activation_disconnect_and_default_preservation();
|
test_activation_disconnect_and_default_preservation();
|
||||||
test_analog_thresholds_rumble_and_local_confirmation();
|
test_analog_thresholds_rumble_and_local_confirmation();
|
||||||
|
test_default_switching_retry_commit_and_feedback();
|
||||||
|
test_identity_promotion_preserves_held_switching();
|
||||||
|
test_switching_uses_pre_hotkey_buttons_only();
|
||||||
|
test_custom_switching_chord_and_wrap();
|
||||||
|
test_switching_slot_isolation();
|
||||||
test_all_runtime_cancellation_causes();
|
test_all_runtime_cancellation_causes();
|
||||||
test_runtime_slot_synthetic_isolation();
|
test_runtime_slot_synthetic_isolation();
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,100 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
|
||||||
"activation did not publish profile, index, and generation together");
|
"activation did not publish profile, index, and generation together");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_host_and_controller_mutations_are_serialized() {
|
||||||
|
const ControllerIdentity identity = controller_identity_global();
|
||||||
|
ControllerProfile profile =
|
||||||
|
controller_profile_default(identity, 1);
|
||||||
|
profile.weak_rumble_scale = 23;
|
||||||
|
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||||
|
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||||
|
"serialization fixture profile did not encode");
|
||||||
|
|
||||||
|
constexpr uint32_t kHostTransactionId = 0x11223344;
|
||||||
|
constexpr uint32_t kInternalTransactionId = 0x80000019;
|
||||||
|
require(profile_service_begin(
|
||||||
|
kHostTransactionId, identity, 1,
|
||||||
|
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||||
|
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||||
|
ConfigurationTransactionStatus::kReceiving,
|
||||||
|
"host write did not acquire the profile mutation boundary");
|
||||||
|
require(profile_service_activate_internal(
|
||||||
|
kInternalTransactionId, identity, 2) ==
|
||||||
|
ConfigurationTransactionStatus::kBusy,
|
||||||
|
"controller activation raced a receiving host write");
|
||||||
|
ProfileServiceTransactionSnapshot snapshot =
|
||||||
|
transaction_snapshot();
|
||||||
|
require(snapshot.transaction.transaction_id ==
|
||||||
|
kHostTransactionId &&
|
||||||
|
snapshot.transaction.status ==
|
||||||
|
ConfigurationTransactionStatus::kReceiving,
|
||||||
|
"busy controller activation replaced the host transaction");
|
||||||
|
|
||||||
|
require(profile_service_append(
|
||||||
|
kHostTransactionId, 0, encoded, sizeof(encoded)) ==
|
||||||
|
ConfigurationTransactionStatus::kReceiving &&
|
||||||
|
profile_service_commit(kHostTransactionId) ==
|
||||||
|
ConfigurationTransactionStatus::kPending,
|
||||||
|
"host write did not reach pending after controller contention");
|
||||||
|
profile_service_task_on_storage_core(3000);
|
||||||
|
require(transaction_snapshot().transaction.status ==
|
||||||
|
ConfigurationTransactionStatus::kCommitted,
|
||||||
|
"serialized host write did not commit");
|
||||||
|
|
||||||
|
constexpr uint32_t kHostActivationTransactionId = 0x22334455;
|
||||||
|
require(profile_service_activate(
|
||||||
|
kHostActivationTransactionId, identity, 0) ==
|
||||||
|
ConfigurationTransactionStatus::kPending,
|
||||||
|
"host activation did not acquire the released boundary");
|
||||||
|
require(profile_service_activate_internal(
|
||||||
|
kInternalTransactionId, identity, 2) ==
|
||||||
|
ConfigurationTransactionStatus::kBusy,
|
||||||
|
"controller activation raced a pending host activation");
|
||||||
|
snapshot = transaction_snapshot();
|
||||||
|
require(snapshot.transaction.transaction_id ==
|
||||||
|
kHostActivationTransactionId &&
|
||||||
|
snapshot.transaction.status ==
|
||||||
|
ConfigurationTransactionStatus::kPending &&
|
||||||
|
active_profile_snapshot(identity).profile_index == 3,
|
||||||
|
"pending host activation was replaced or leaked before commit");
|
||||||
|
profile_service_task_on_storage_core(4000);
|
||||||
|
require(active_profile_snapshot(identity).profile_index == 0,
|
||||||
|
"serialized host activation did not commit");
|
||||||
|
require(profile_service_activate_internal(
|
||||||
|
0x19, identity, 2) ==
|
||||||
|
ConfigurationTransactionStatus::kMalformed,
|
||||||
|
"internal activation admitted a transaction without the high bit");
|
||||||
|
|
||||||
|
require(profile_service_activate_internal(
|
||||||
|
kInternalTransactionId, identity, 2) ==
|
||||||
|
ConfigurationTransactionStatus::kPending,
|
||||||
|
"controller activation did not acquire the released boundary");
|
||||||
|
require(profile_service_begin(
|
||||||
|
0x55667788, identity, 0,
|
||||||
|
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||||
|
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||||
|
ConfigurationTransactionStatus::kBusy,
|
||||||
|
"host write raced a pending controller activation");
|
||||||
|
snapshot = transaction_snapshot();
|
||||||
|
require(snapshot.transaction.transaction_id ==
|
||||||
|
kHostActivationTransactionId &&
|
||||||
|
snapshot.transaction.status ==
|
||||||
|
ConfigurationTransactionStatus::kCommitted &&
|
||||||
|
active_profile_snapshot(identity).profile_index == 0,
|
||||||
|
"pending controller activation replaced host-visible status or leaked before commit");
|
||||||
|
|
||||||
|
profile_service_task_on_storage_core(5000);
|
||||||
|
const ProfileServiceActiveProfileSnapshot active =
|
||||||
|
active_profile_snapshot(identity);
|
||||||
|
snapshot = transaction_snapshot();
|
||||||
|
require(active.valid && active.profile_index == 2 &&
|
||||||
|
snapshot.transaction.transaction_id ==
|
||||||
|
kHostActivationTransactionId &&
|
||||||
|
snapshot.transaction.status ==
|
||||||
|
ConfigurationTransactionStatus::kCommitted,
|
||||||
|
"controller activation did not publish while preserving host-visible status");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ProfileStorageIo pico_profile_storage_io() {
|
ProfileStorageIo pico_profile_storage_io() {
|
||||||
|
|
@ -219,5 +313,6 @@ ProfileStorageIo pico_profile_storage_io() {
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
test_pending_commands_are_not_decoded_as_profile_writes();
|
test_pending_commands_are_not_decoded_as_profile_writes();
|
||||||
|
test_host_and_controller_mutations_are_serialized();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
"lifecycle",
|
"lifecycle",
|
||||||
"pairing-policy",
|
"pairing-policy",
|
||||||
"slot-lighting",
|
"slot-lighting",
|
||||||
"abxy-hotkey",
|
"profile-chord-raw",
|
||||||
|
"profile-feedback",
|
||||||
|
"stateful-rumble",
|
||||||
"motion-hotkey",
|
"motion-hotkey",
|
||||||
"analog-state",
|
"analog-state",
|
||||||
"rumble-mode",
|
"rumble-mode",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue