switch-pico/src/firmware/input/bluepad32_input_backend.cpp
Joey Yakimowich-Payne d424fe0f4a fix(native-usb): service masked completions and trace startup stalls
Keep native backend locking IRQ-permitting and service USB hardware from SRAM during BOOTSEL sampling. Add bounded input/control flight recording, root IN and EP0 snapshots, and masked-window handoff coverage. Record PC qualification and the remaining Switch stability limitations.
2026-09-14 13:55:35 -06:00

5097 lines
193 KiB
C++

#include "input/bluepad32_input_backend.h"
#include "bluetooth_transport_config.h"
#include "input/controller_hotkey_config.h"
#include "input/switch2_wake.h"
#ifdef SWITCH_PICO_WII_IR
#include "input/wii_ir_pointer.h"
#include "parser/uni_hid_parser_wii_ir.h"
#endif
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
#include "input/switch_native_output.h"
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
#include "input/haptics_experiment.h"
#endif
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
#include "input/native_output_scheduler.h"
#endif
#include "configuration/configuration_service.h"
#include "profile/profile_service.h"
#include <limits.h>
#include <math.h>
#include <stddef.h>
#include <string.h>
#include <btstack_run_loop.h>
#if SWITCH2_PROBE_HUB
#include <pico/async_context.h>
#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
#include "usb/native_hub/native_hub_trace.h"
#endif
#endif
#include <pico/critical_section.h>
#include <pico/cyw43_arch.h>
#include <pico/flash.h>
#if !SWITCH2_PROBE_HUB
#include <pico/multicore.h>
#endif
#include <pico/stdlib.h>
#include <uni.h>
extern "C" {
#include "parser/uni_hid_parser_wii.h"
#if SWITCH2_BRIDGE_FULL_INPUT
#include "parser/uni_hid_parser_ds5.h"
#include "parser/uni_hid_parser_native_motion.h"
#endif
}
#include "parser/uni_hid_parser_switch2.h"
#include "parser/uni_switch2_haptics.h"
#include "parser/uni_switch2_pairing.h"
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
#include "adapter/adapter_usb_mode.h"
#endif
#if SWITCH2_PROBE_HUB && !PICO_CYW43_ARCH_POLL
#error "Native hub Bluetooth requires pico_cyw43_arch_poll on Core 0"
#endif
#if SWITCH2_PROBE_HUB && !PICO_FLASH_ASSUME_CORE1_SAFE
#error "Native hub flash writes require its IRQ-disabled SRAM-only Core 1 transport"
#endif
namespace {
constexpr int32_t kAxisMinimum = -512;
constexpr int32_t kAxisMaximum = 511;
constexpr int32_t kTriggerMaximum = 1023;
constexpr int32_t kTriggerFullScaleMinimum = 1020;
constexpr uint16_t kSwitchHostRumbleDurationMs = 50;
// XInput vibration is stateful and remains active until XInputSetState sends
// a new magnitude.
constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX;
constexpr uint32_t kRumblePollIntervalMs = 5;
constexpr uint32_t kConfigurationPollIntervalMs = 50;
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
constexpr uint32_t kDefaultPairingWindowDurationMs =
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT * 1000u;
constexpr uint32_t kPairingResetFeedbackDurationMs = 2000;
// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds.
constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900;
// LE units are 1.25 ms. All Switch 2 links request the 7.5 ms minimum.
constexpr uint16_t kSwitch2FastInterval = 6;
constexpr uint32_t kSwitch2IntervalSettleMs = 1000;
constexpr uint8_t kAllBlePairingMethods =
SM_STK_GENERATION_METHOD_JUST_WORKS |
SM_STK_GENERATION_METHOD_OOB |
SM_STK_GENERATION_METHOD_PASSKEY |
SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON;
constexpr uint16_t kProfileFeedbackPhaseDurationMs = 75;
constexpr uint8_t kProfileFeedbackWeakMagnitude = UINT8_MAX;
constexpr uint8_t kProfileFeedbackStrongMagnitude = UINT8_MAX;
constexpr uint32_t kJoyConGestureHoldMs = 2000;
constexpr uint32_t kJoyConGestureFreshMs = 250;
constexpr uint16_t kJoyConGestureFeedbackMs = 75;
#ifdef SWITCH_PICO_WII_IR_GYRO
constexpr uint32_t kWiiAimChordHoldUs = 2000000;
constexpr uint32_t kWiiAimChordFreshUs = 150000;
constexpr uint16_t kWiiAimChordButtons = 0x0002 | 0x0001;
#endif
// One initial indication can be followed by one committed switch before the
// BTstack timer drains the queue. Profile commits are rate-limited well beyond
// the longest feedback sequence.
constexpr uint8_t kProfileFeedbackQueueCapacity = 2;
constexpr SwitchRgbColor kProfileLightbarPalette[CONTROLLER_PROFILE_COUNT] = {
{0x00, 0x55, 0xff},
{0x00, 0xcc, 0x66},
{0xff, 0xaa, 0x00},
{0xcc, 0x33, 0xff},
{0xff, 0x44, 0x44},
{0x00, 0xdd, 0xdd},
{0xff, 0x66, 0xbb},
{0xcc, 0xff, 0x33},
};
constexpr bool kDefaultMotionEnabled =
SWITCH_MOTION_DEFAULT_ENABLED != 0;
constexpr uint16_t kMotionDisabledFeedbackDurationMs =
SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS;
constexpr uint8_t kMotionDisabledFeedbackWeakMagnitude =
SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE;
constexpr uint8_t kMotionDisabledFeedbackStrongMagnitude =
SWITCH_MOTION_DISABLED_FEEDBACK_STRONG_MAGNITUDE;
constexpr uint16_t kMotionEnabledFeedbackDurationMs =
SWITCH_MOTION_ENABLED_FEEDBACK_DURATION_MS;
constexpr uint8_t kMotionEnabledFeedbackWeakMagnitude =
SWITCH_MOTION_ENABLED_FEEDBACK_WEAK_MAGNITUDE;
constexpr uint8_t kMotionEnabledFeedbackStrongMagnitude =
SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE;
static_assert(kProfileFeedbackPhaseDurationMs == 75);
static_assert(CONTROLLER_PROFILE_COUNT == 8);
static_assert(kMotionDisabledFeedbackDurationMs > 0);
static_assert(kMotionEnabledFeedbackDurationMs > 0);
static_assert(kSlotCount == 4);
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount);
enum class ConnectionStatus {
Initializing,
Scanning,
Connecting,
Ready,
};
enum class ConnectionPolicyState {
Uninitialized,
Open,
Passive,
Paused,
FailedClosed,
};
struct RumbleEnvelope {
uint8_t slot;
uint32_t connection_generation;
ControllerRumbleOutput rumble;
uint16_t duration_ms;
uint32_t received_ms = 0;
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
uint64_t received_us = 0;
#endif
};
constexpr uint8_t kSwitch2IngressCapacity = 16;
struct Switch2HostCommand {
RumbleEnvelope envelope;
uint32_t generation;
uint8_t accepted_halves;
};
struct Switch2Ingress {
Switch2HostCommand commands[kSwitch2IngressCapacity];
uint32_t generation;
uint8_t host_mode;
uint8_t head;
uint8_t count;
bool reset_pending;
};
struct FeedbackEnvelope {
uint32_t connection_generation;
uint16_t duration_ms;
uint8_t weak_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;
};
struct WiiOrientationRequest {
ControllerIdentity identity;
uint32_t connection_generation;
bool vertical;
};
#ifdef SWITCH_PICO_WII_IR_GYRO
struct WiiAimSource {
uint32_t sequence;
uint32_t last_report_us;
uint32_t started_us;
bool have_sequence;
bool infrared;
bool holding;
bool masked;
bool latched;
bool reposition_masked;
};
#endif
#ifdef SWITCH2_BRIDGE_WII_INPUT
struct WiiMotionIngress {
uint32_t received_us = 0;
uint32_t accel_sequence = 0;
uint32_t gyro_sequence = 0;
uint32_t nunchuk_sequence = 0;
uint32_t accel_received_us = 0;
uint32_t gyro_received_us = 0;
int32_t accel_q13[3]{};
int32_t gyro_q10[3]{};
bool accel_valid = false;
bool gyro_valid = false;
};
struct WiiCue {
uint64_t token = 0;
uint32_t connection_generation = 0;
uint32_t requested_ms = 0;
uint32_t started_ms = 0;
uint8_t slot = 0xff;
uint8_t sample_id = 0;
uint8_t dispatched_phase = 0xff;
int result = -1;
bool consumed = false;
bool active = false;
bool stop_pending = false;
bool in_flight = false;
};
// Wii has a fixed-strength ERM motor: "soft"/"strong" are approximated only
// through pulse length, not invented HD frequencies or amplitude control.
struct WiiCuePattern {
uint16_t phases_ms[7]; // Alternating on/gap, starting and ending on.
uint8_t count;
};
constexpr WiiCuePattern kWiiCuePatterns[8] = {
{{0}, 0},
{{1000}, 1},
{{100, 180, 100, 180, 100, 180, 100}, 7},
{{25, 90, 25}, 3},
{{100, 140, 100}, 3},
{{70, 120, 70}, 3},
{{60}, 1},
{{120}, 1},
};
constexpr uint32_t kWiiCueDeadlineMs = 2000;
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
struct NativeGamepadIngress {
uint32_t received_us = 0;
uint32_t accel_sequence = 0;
uint32_t gyro_sequence = 0;
uint32_t accel_received_us = 0;
uint32_t gyro_received_us = 0;
bool has_report = false;
bool accel_valid = false;
bool gyro_valid = false;
int32_t accel_q13[3]{};
int32_t gyro_q10[3]{};
};
// Physical parser counters survive logical pairing/reselection epochs. They
// are retired only with the Bluetooth connection, never by a snapshot getter.
struct NativeGamepadReportIngress {
uni_hid_device_t* device = nullptr;
uint32_t report_sequence = 0;
uint32_t accel_sequence = 0;
uint32_t gyro_sequence = 0;
};
struct NativeGamepadCue {
uint64_t token = 0;
uint32_t connection_generation = 0;
uint32_t requested_ms = 0;
uint32_t started_ms = 0;
uint8_t slot = 0xff;
uint8_t sample_id = 0;
int result = -1;
bool active = false;
bool consumed = false;
bool in_flight = false;
};
struct NativeGamepadMotorOutput {
uint32_t deadline_ms = 0;
uint8_t magnitude[2]{};
bool owned = false;
};
// Same bounded pulse vocabulary as Wii, with source-driver motor magnitudes.
// These are compatibility-vibration approximations, not uploaded HD waveforms.
struct NativeGamepadCuePattern {
uint16_t phases_ms[7];
uint8_t count;
uint8_t magnitude;
};
constexpr NativeGamepadCuePattern kNativeGamepadCuePatterns[8] = {
{{0}, 0, 0},
{{1000}, 1, 160},
{{100, 180, 100, 180, 100, 180, 100}, 7, 200},
{{25, 90, 25}, 3, 96},
{{100, 140, 100}, 3, 220},
{{70, 120, 70}, 3, 160},
{{60}, 1, 96},
{{120}, 1, 220},
};
constexpr uint32_t kNativeGamepadCueDeadlineMs = 2000;
#endif
// Security Manager identity events arrive before Bluepad32 publishes a ready
// device. Retain only the four live handle/address associations so a BLE RPA
// is never promoted to a stable identity on its own.
struct BleIdentityMapping {
bool used;
hci_con_handle_t connection_handle;
bd_addr_t connection_address;
uint8_t identity_address_type;
bd_addr_t identity_address;
};
struct BackendSlot {
ControllerState state;
WiiAccelerometerSample accelerometer{};
WiiAccelerometerSample nunchuk_accelerometer{};
uint16_t pre_hotkey_button_mask;
ControllerIdentity identity;
// Non-null with active=false is a connected device still becoming ready.
uni_hid_device_t* device;
// A pair occupies one logical profile owner and output, but still consumes
// two Bluepad32 physical device indices. device is always the left half.
uni_hid_device_t* companion;
uni_gamepad_t gamepad;
uni_gamepad_t companion_gamepad;
uint8_t extra_buttons;
uint8_t companion_extra_buttons;
uint32_t state_generation;
uint32_t connection_generation;
bool active;
bool wii_orientation_pending;
WiiOrientationRequest pending_wii_orientation;
#ifdef SWITCH2_BRIDGE_WII_INPUT
WiiMotionIngress wii_motion;
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
NativeGamepadIngress native_motion;
NativeGamepadMotorOutput native_output;
#endif
#ifdef SWITCH_PICO_WII_IR_GYRO
WiiAimSource wii_aim;
#endif
bool rumble_pending;
bool motion_enabled;
bool feedback_pending;
uint32_t feedback_until_ms;
uint8_t pending_profile_feedback_count;
RumbleEnvelope pending_rumble;
bool retained_host_rumble_valid;
RumbleEnvelope retained_host_rumble;
Switch2Ingress switch2_ingress;
FeedbackEnvelope pending_feedback;
ProfileFeedbackEnvelope
pending_profile_feedback[kProfileFeedbackQueueCapacity];
ProfileFeedbackSequence profile_feedback;
};
#if SWITCH2_PROBE_HUB
spin_lock_t* g_state_lock;
static __force_inline void backend_state_lock_enter() {
// Hub Bluetooth is polled on Core0, and USB IRQs only enqueue transport
// events. Neither IRQs nor Core1 may enter backend state. Keep USB IRQs
// serviceable while holding this lock; preserve any caller-owned masking.
if (get_core_num() != 0 || __get_current_exception() != 0) {
panic("Native hub backend state requires Core0 foreground");
}
spin_lock_unsafe_blocking(g_state_lock);
}
#define backend_state_lock_exit() spin_unlock_unsafe(g_state_lock)
#else
critical_section_t g_state_lock;
#define backend_state_lock_enter() critical_section_enter_blocking(&g_state_lock)
#define backend_state_lock_exit() critical_section_exit(&g_state_lock)
#endif
#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
static uint32_t g_state_lock_trace_parent;
static __force_inline void trace_state_lock_enter(uint32_t line) {
backend_state_lock_enter();
g_state_lock_trace_parent = native_hub_trace_phase(
NATIVE_HUB_TRACE_PHASE_BACKEND_LOCK | line);
}
static __force_inline void trace_state_lock_exit() {
native_hub_trace_phase(g_state_lock_trace_parent);
backend_state_lock_exit();
}
#define state_lock_enter() trace_state_lock_enter(__LINE__)
#define state_lock_exit() trace_state_lock_exit()
#else
#define state_lock_enter() backend_state_lock_enter()
#define state_lock_exit() backend_state_lock_exit()
#endif
uni_hid_device_t* g_retired_devices[kSlotCount]{};
BackendSlot g_slots[kSlotCount];
ControllerMacroCapture g_macro_capture;
#if !SWITCH2_PROBE_HUB
// Catalog migration/compaction needs more than the 4 KiB scratch bank.
// Supply a dedicated static stack in main SRAM rather than overflowing it.
alignas(8) uint32_t g_core1_stack[4096];
#else
bool g_poll_ready = false;
#endif
BleIdentityMapping g_ble_identity_mappings[kSlotCount]{};
// These acknowledgement generations and request producers are only used by
// Core 0. Requests are transferred under the cross-core state lock.
uint32_t g_consumed_generation[kSlotCount]{};
uint32_t g_last_snapshot_generation[kSlotCount]{};
bool g_pairing_window_requested = false;
uint32_t g_clear_pairings_requested_token = 0;
uint32_t g_clear_pairings_in_progress_token = 0;
uint32_t g_next_clear_pairings_request_token = 1;
bool g_pairing_snapshot_requested = false;
bool g_initialized = false;
bool g_started = false;
#ifdef SWITCH2_BRIDGE_WII_INPUT
bool g_wii_source_selected = false;
uint8_t g_wii_source_address[6]{};
Bluepad32WiiBridgeSnapshot g_wii_snapshot{};
WiiCue g_wii_cue{};
uint64_t g_next_wii_cue_token = 1;
// All native Wii state shares the backend lock. Only Core 1 dereferences a
// parser or dispatches transport; Core 0 sees a copied, published snapshot.
bool is_selected_wii(const BackendSlot& slot) {
return g_wii_source_selected && slot.active && slot.device != nullptr &&
slot.companion == nullptr &&
slot.device->controller_type == CONTROLLER_TYPE_WiiController &&
memcmp(slot.device->conn.btaddr, g_wii_source_address, 6) == 0;
}
void retire_wii_motion(WiiMotionIngress& motion) {
// Keep the observed counters across logical epochs: a cached parser sample
// must not acquire a new receipt timestamp after orientation/reselection.
const uint32_t accel_sequence = motion.accel_sequence;
const uint32_t gyro_sequence = motion.gyro_sequence;
const uint32_t nunchuk_sequence = motion.nunchuk_sequence;
motion = {};
motion.accel_sequence = accel_sequence;
motion.gyro_sequence = gyro_sequence;
motion.nunchuk_sequence = nunchuk_sequence;
}
void cancel_wii_cue_locked() {
g_wii_cue.stop_pending = g_wii_cue.stop_pending ||
g_wii_cue.active || g_wii_cue.in_flight;
g_wii_cue.active = false;
g_wii_cue.result = -1;
}
void retire_wii_slot(uint8_t slot_index) {
if (g_wii_snapshot.slot == slot_index) g_wii_snapshot = {};
if (g_wii_cue.slot == slot_index) {
// The connection is gone or its owner is being reset on Core 1.
// Never send a deferred stop into a replacement generation.
g_wii_cue = {};
}
}
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
bool g_native_explicit_address = false;
uint8_t g_native_address[6]{};
uint8_t g_native_slot = 0xff;
uint32_t g_native_generation = 0;
Bluepad32NativeGamepadSnapshot g_native_snapshot{};
NativeGamepadCue g_native_cues[2]{};
uint64_t g_next_native_token = 1;
uni_hid_device_t* g_native_pending_devices[kSlotCount]{};
NativeGamepadReportIngress g_native_reports[kSlotCount]{};
bool native_device_allowed(const uni_hid_device_t* device) {
if (device == nullptr || !uni_hid_device_is_gamepad(device)) return false;
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
return device->controller_type == CONTROLLER_TYPE_PS5Controller &&
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report;
#else
return true;
#endif
}
bool native_address_matches(const uni_hid_device_t* device) {
return device != nullptr && memcmp(device->conn.btaddr, g_native_address, 6) == 0;
}
bool eligible_native_gamepad(const BackendSlot& slot) {
return slot.active && native_device_allowed(slot.device) &&
(slot.companion == nullptr || native_device_allowed(slot.companion)) &&
(!g_native_explicit_address || native_address_matches(slot.device) ||
native_address_matches(slot.companion));
}
uni_hid_device_t* native_rumble_target(const BackendSlot& slot, uint8_t side) {
// Paired Joy-Cons have a real left owner and right companion. A solo pad's
// driver owns its motor topology, including mono OR/max mixing.
return slot.companion != nullptr && side == 0 ? slot.companion : slot.device;
}
bool native_rumble_capable(const BackendSlot& slot, uint8_t side) {
const uni_hid_device_t* device = native_rumble_target(slot, side);
return device != nullptr && device->report_parser.play_dual_rumble != nullptr;
}
void cancel_native_cue_locked(NativeGamepadCue& cue) {
cue.active = false;
cue.result = -1;
// The slot's last motor output remains owned until the timer replaces it.
}
void refresh_native_source_locked(bool reselection = false) {
uint8_t selected = 0xff;
for (uint8_t index = 0; index < kSlotCount; ++index) {
if (!eligible_native_gamepad(g_slots[index])) continue;
if (selected != 0xff) {
selected = 0xff; // Never blend or choose by connection order.
break;
}
selected = index;
}
if (!reselection && selected == g_native_slot &&
(selected == 0xff ||
g_slots[selected].connection_generation == g_native_generation)) return;
for (NativeGamepadCue& cue : g_native_cues) cancel_native_cue_locked(cue);
g_native_snapshot = {};
g_native_slot = selected;
g_native_generation = 0;
if (selected != 0xff) {
BackendSlot& slot = g_slots[selected];
g_macro_capture.disconnect(selected, slot.connection_generation, time_us_32());
// A missed inactive snapshot must still retire the adapter's old epoch.
g_native_generation = ++slot.connection_generation;
++slot.state_generation;
slot.native_motion = {};
}
}
void retire_native_slot(uint8_t index) {
if (g_native_slot == index) {
g_native_slot = 0xff;
g_native_generation = 0;
g_native_snapshot = {};
}
for (NativeGamepadCue& cue : g_native_cues)
if (cue.slot == index) cue = {};
g_slots[index].native_motion = {};
g_slots[index].native_output = {};
}
bool native_cue_current(const NativeGamepadCue& cue) {
return cue.slot < kSlotCount && cue.slot == g_native_slot &&
cue.connection_generation == g_native_generation &&
cue.connection_generation == g_slots[cue.slot].connection_generation;
}
#endif
// These fields are only read or written by the BTstack execution context.
btstack_timer_source_t g_rumble_timer{};
btstack_timer_source_t g_configuration_timer{};
ConnectionStatus g_connection_status = ConnectionStatus::Initializing;
btstack_packet_callback_registration_t g_pairing_event_callback{};
btstack_packet_callback_registration_t g_identity_event_callback{};
ConnectionPolicyState g_connection_policy_state =
ConnectionPolicyState::Uninitialized;
bool g_background_scan_active = false;
struct Switch2IntervalRequest {
hci_con_handle_t handle = HCI_CON_HANDLE_INVALID;
uint16_t interval = 0;
uint32_t requested_ms = 0;
};
Switch2IntervalRequest g_switch2_interval_requests[kSlotCount]{};
JoyConMode g_joycon_mode = JoyConMode::kPaired;
bool g_joycon_reconcile_requested = false;
// Live-link hints only: splitting two pairs must not exchange their members
// when the next Paired preference is applied.
struct JoyConPairHint {
uni_hid_device_t* mate = nullptr;
uint8_t owner_slot = 0;
};
JoyConPairHint g_joycon_pair_hints[kSlotCount]{};
enum class JoyConGroupingOverride : uint8_t {
Default,
Individual,
Paired,
};
struct JoyConConnectionOverride {
JoyConGroupingOverride mode = JoyConGroupingOverride::Default;
uni_hid_device_t* mate = nullptr;
};
JoyConConnectionOverride g_joycon_overrides[kSlotCount]{};
// BTstack physical-link state survives logical slot moves. Raw reports stay in
// BackendSlot; only the derived logical view consumes the reserved buttons.
struct JoyConGesture {
uni_hid_device_t* device = nullptr;
uint32_t last_report_ms = 0;
uint32_t started_ms = 0;
uint8_t participants = 0;
bool held = false;
bool released = true;
bool masked = false;
bool blocked = false;
bool joining = false;
};
JoyConGesture g_joycon_gestures[kSlotCount]{};
uint32_t g_pairing_window_deadline_ms = 0;
uint32_t g_pairing_window_duration_ms =
kDefaultPairingWindowDurationMs;
uint32_t g_pairing_reset_feedback_deadline_ms = 0;
uint16_t g_status_led_tick = 0;
bool g_pairing_window_open = false;
bool g_status_led_on = false;
Bluepad32PairingSnapshot g_pairing_snapshot{};
uint32_t g_initialization_stage = 0;
uint32_t g_rumble_timer_ticks = 0;
uint32_t g_configuration_timer_ticks = 0;
uint32_t g_controller_reports = 0;
uint32_t g_host_rumble_requests = 0;
uint32_t g_local_feedback_requests = 0;
uint32_t g_rumble_dispatches = 0;
uint32_t g_switch2_ingress_drops = 0;
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
uint32_t g_seeded_native_run_id = 0;
#endif
uint16_t host_rumble_duration_ms() {
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
return kXInputHostRumbleDurationMs;
}
#endif
return kSwitchHostRumbleDurationMs;
}
uint8_t switch2_host_mode() {
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
return static_cast<uint8_t>(adapter_host_probe_mode());
#else
return 0;
#endif
}
ControllerState make_neutral_state() {
return controller_neutral_state();
}
bool valid_slot(uint8_t slot) {
return slot < kSlotCount;
}
bool has_free_slot() {
state_lock_enter();
unsigned physical_count = 0;
for (const BackendSlot& slot : g_slots) {
physical_count += slot.device != nullptr;
physical_count += slot.companion != nullptr;
}
#if SWITCH2_BRIDGE_FULL_INPUT
for (const auto* pending : g_native_pending_devices) physical_count += pending != nullptr;
#endif
state_lock_exit();
return physical_count < kSlotCount;
}
bool has_active_controller() {
state_lock_enter();
bool active_controller = false;
for (const BackendSlot& slot : g_slots) {
active_controller = active_controller || slot.active;
}
state_lock_exit();
return active_controller;
}
int physical_index_for_device(const uni_hid_device_t* device) {
if (device == nullptr) {
return -1;
}
const int index = uni_hid_device_get_idx_for_instance(device);
return index >= 0 && index < kSlotCount ? index : -1;
}
int slot_for_device(const uni_hid_device_t* device) {
if (device != nullptr) {
for (uint8_t index = 0; index < kSlotCount; ++index) {
if (g_slots[index].device == device ||
g_slots[index].companion == device) {
return index;
}
}
}
return -1;
}
// Called under the state lock. Bluepad32's index is a transport resource, not
// an output index once two Joy-Cons merge. Never evict an unrelated output.
int reserve_device_slot(uni_hid_device_t* device) {
const int physical_index = physical_index_for_device(device);
if (physical_index < 0) {
return -1;
}
const int tracked = slot_for_device(device);
if (g_retired_devices[physical_index] == device) {
#if SWITCH2_BRIDGE_FULL_INPUT
return -1;
#else
if (uni_hid_parser_switch2_is_ble_device(device)) return -1;
#endif
}
if (tracked >= 0) {
return tracked;
}
for (const BackendSlot& slot : g_slots) {
if ((slot.device != nullptr &&
physical_index_for_device(slot.device) == physical_index) ||
(slot.companion != nullptr &&
physical_index_for_device(slot.companion) == physical_index)) {
return -1;
}
}
#if !SWITCH2_BRIDGE_FULL_INPUT
if (g_slots[physical_index].device == nullptr) {
return physical_index;
}
#endif
for (uint8_t index = 0; index < kSlotCount; ++index) {
if (g_slots[index].device == nullptr) {
return index;
}
}
return -1;
}
int joycon_side(const uni_hid_device_t* device) {
if (!uni_hid_parser_switch2_is_ble_device(device)) {
return 0;
}
if (device->product_id == UNI_SW2_JOYCON_L_PID) {
return -1;
}
return device->product_id == UNI_SW2_JOYCON_R_PID ? 1 : 0;
}
bool joycon_default_pairing_allowed(const uni_hid_device_t* device) {
const int index = physical_index_for_device(device);
return g_joycon_mode == JoyConMode::kPaired && index >= 0 &&
g_joycon_overrides[index].mode == JoyConGroupingOverride::Default;
}
// Include the entire old attempt when another held solo makes selection
// ambiguous. None of its members may retry until all have released.
void block_joycon_gesture(uint8_t participants) {
for (uint8_t pass = 0; pass < kSlotCount; ++pass) {
for (uint8_t index = 0; index < kSlotCount; ++index) {
if (participants & (1u << index)) {
participants |= g_joycon_gestures[index].participants;
}
}
}
for (uint8_t index = 0; index < kSlotCount; ++index) {
if (!(participants & (1u << index))) continue;
g_joycon_gestures[index].participants = participants;
g_joycon_gestures[index].blocked = true;
}
}
bool joycon_gesture_live(uint8_t index) {
const JoyConGesture& gesture = g_joycon_gestures[index];
const int slot = slot_for_device(gesture.device);
return gesture.device != nullptr && slot >= 0 && g_slots[slot].active &&
physical_index_for_device(gesture.device) == index &&
joycon_side(gesture.device) != 0;
}
void arm_joycon_gesture(uint8_t left, uint8_t right, uint32_t now_ms,
bool joining) {
JoyConGesture& l = g_joycon_gestures[left];
JoyConGesture& r = g_joycon_gestures[right];
if (!l.held || !r.held || l.blocked || r.blocked ||
l.participants != (1u << left) ||
r.participants != (1u << right)) return;
l.participants = r.participants = (1u << left) | (1u << right);
l.started_ms = r.started_ms = now_ms;
l.joining = r.joining = joining;
}
// Caller holds the state lock. Reports perform admission; the timer also
// expires attempts, but elapsed cached input alone can never complete a hold.
void refresh_joycon_gestures(uint32_t now_ms) {
for (uint8_t index = 0; index < kSlotCount; ++index) {
JoyConGesture& gesture = g_joycon_gestures[index];
if (gesture.participants == 0 || gesture.blocked) continue;
if (!joycon_gesture_live(index) || !gesture.held ||
now_ms - gesture.last_report_ms > kJoyConGestureFreshMs) {
block_joycon_gesture(gesture.participants);
}
}
for (JoyConGesture& gesture : g_joycon_gestures) {
if (gesture.participants == 0) continue;
bool released = true;
for (uint8_t index = 0; index < kSlotCount; ++index) {
if ((gesture.participants & (1u << index)) &&
!g_joycon_gestures[index].released) released = false;
}
if (!released) continue;
const uint8_t participants = gesture.participants;
for (uint8_t index = 0; index < kSlotCount; ++index) {
if (!(participants & (1u << index))) continue;
g_joycon_gestures[index].participants = 0;
g_joycon_gestures[index].blocked = false;
}
}
int left = -1;
int right = -1;
uint8_t solos = 0;
bool ambiguous = false;
for (const BackendSlot& slot : g_slots) {
if (!slot.active || joycon_side(slot.device) == 0) continue;
const int index = physical_index_for_device(slot.device);
if (index < 0) continue;
if (slot.companion != nullptr) {
const int mate = physical_index_for_device(slot.companion);
if (mate >= 0) arm_joycon_gesture(index, mate, now_ms, false);
continue;
}
const JoyConGesture& gesture = g_joycon_gestures[index];
if (gesture.device != slot.device || !gesture.held || gesture.blocked) continue;
solos |= 1u << index;
int& side = joycon_side(slot.device) < 0 ? left : right;
ambiguous = ambiguous || side >= 0;
side = index;
}
if (ambiguous) {
block_joycon_gesture(solos);
} else if (left >= 0 && right >= 0) {
arm_joycon_gesture(left, right, now_ms, true);
}
}
void observe_joycon_gesture(uni_hid_device_t* device,
const uni_gamepad_t& raw, uint32_t now_ms) {
const int side = joycon_side(device);
const int index = physical_index_for_device(device);
if (side == 0 || index < 0) return;
// Expire before updating the timestamp: a returning stale report must not
// hide a gap, even when no timer ran during it.
refresh_joycon_gestures(now_ms);
JoyConGesture& gesture = g_joycon_gestures[index];
gesture.device = device;
const bool trigger = (raw.buttons &
(side < 0 ? BUTTON_TRIGGER_L : BUTTON_TRIGGER_R)) != 0;
const bool menu = (raw.misc_buttons &
(side < 0 ? MISC_BUTTON_SELECT : MISC_BUTTON_START)) != 0;
gesture.last_report_ms = now_ms;
gesture.held = trigger && menu;
gesture.released = !trigger && !menu;
if (gesture.held) {
gesture.masked = true;
if (gesture.participants == 0) gesture.participants = 1u << index;
} else if (gesture.released) {
gesture.masked = false;
}
refresh_joycon_gestures(now_ms);
}
bool joycon_gesture_masked(const uni_hid_device_t* device) {
const int index = physical_index_for_device(device);
return index >= 0 && g_joycon_gestures[index].device == device &&
g_joycon_gestures[index].masked;
}
void mask_joycon_gesture(uni_gamepad_t& gamepad,
const uni_hid_device_t* device) {
if (!joycon_gesture_masked(device)) return;
if (joycon_side(device) < 0) {
gamepad.buttons &= ~BUTTON_TRIGGER_L;
gamepad.misc_buttons &= ~MISC_BUTTON_SELECT;
gamepad.brake = 0;
} else {
gamepad.buttons &= ~BUTTON_TRIGGER_R;
gamepad.misc_buttons &= ~MISC_BUTTON_START;
gamepad.throttle = 0;
}
}
bool waiting_for_joycon_mate(int side = 0) {
state_lock_enter();
unsigned physical_count = 0;
bool pending = false;
unsigned left_count = 0;
unsigned right_count = 0;
for (const BackendSlot& slot : g_slots) {
physical_count += slot.device != nullptr;
physical_count += slot.companion != nullptr;
pending = pending || (slot.device != nullptr && !slot.active);
// An explicit solo choice is complete, not a request for another
// default-paired mate. Individual defaults retain balanced reconnects.
if (slot.active && slot.companion == nullptr &&
(g_joycon_mode == JoyConMode::kIndividual ||
joycon_default_pairing_allowed(slot.device))) {
const int candidate_side = joycon_side(slot.device);
left_count += candidate_side < 0;
right_count += candidate_side > 0;
}
}
state_lock_exit();
// Individual players still reconnect their remembered opposite half, but
// a balanced set is complete even though no logical pair was created.
const bool missing_left = g_joycon_mode == JoyConMode::kIndividual
? right_count > left_count
: right_count != 0;
const bool missing_right = g_joycon_mode == JoyConMode::kIndividual
? left_count > right_count
: left_count != 0;
return physical_count < kSlotCount && !pending &&
((side <= 0 && missing_left) || (side >= 0 && missing_right));
}
void stop_background_scan() {
if (!SWITCH_PICO_ENABLE_BLE) {
return;
}
if (g_background_scan_active) {
// Direct LE scans do not update Bluepad32's aggregate scanning flag.
uni_bt_le_scan_stop();
g_background_scan_active = false;
}
}
// BTstack only. Reconcile every ready physical Switch 2 link to the fast interval,
// independently of player grouping, controller count, or Classic connections.
void apply_radio_connection_policy() {
if (!SWITCH_PICO_ENABLE_BLE) {
return;
}
uni_hid_device_t* ready[kSlotCount]{};
for (const BackendSlot& slot : g_slots) {
uni_hid_device_t* targets[] = {slot.device, slot.companion};
for (uni_hid_device_t* target : targets) {
const int index = physical_index_for_device(target);
if (index < 0) continue;
const auto type = gap_get_connection_type(target->conn.handle);
if (type == GAP_CONNECTION_LE &&
uni_hid_parser_switch2_is_ble_device(target)) {
// The parser requests its initial interval during setup.
// Do not race that request by changing a pending device here.
if (slot.active) ready[index] = target;
}
}
}
const uint32_t now_ms = btstack_run_loop_get_time_ms();
for (uint8_t index = 0; index < kSlotCount; ++index) {
auto& request = g_switch2_interval_requests[index];
if (ready[index] == nullptr) {
request = {};
continue;
}
const auto handle = ready[index]->conn.handle;
if (request.handle != handle) request = {};
const uint16_t actual = gap_le_connection_interval(handle);
if (request.interval != 0 && actual != request.interval &&
now_ms - request.requested_ms < kSwitch2IntervalSettleMs) {
// Let an accepted asynchronous update settle before retrying it.
// API success alone does not prove that negotiation completed.
continue;
}
request.interval = 0;
if (actual == kSwitch2FastInterval) continue;
gap_update_connection_parameters(
handle, kSwitch2FastInterval, kSwitch2FastInterval, 0, 600);
// Reconcile negotiated state on the configuration timer. Rejected or
// incomplete requests are retried at most once per second per link.
request = {handle, kSwitch2FastInterval, now_ms};
}
}
// Caller holds the state lock. Only the BTstack context resets parser state.
void clear_switch2_ingress(BackendSlot& slot) {
Switch2Ingress& ingress = slot.switch2_ingress;
__atomic_add_fetch(&g_switch2_ingress_drops, ingress.count, __ATOMIC_RELAXED);
ingress.head = 0;
ingress.count = 0;
++ingress.generation;
ingress.reset_pending = true;
}
void reset_switch2_outputs(BackendSlot& slot) {
uni_hid_device_t* targets[] = {slot.device, slot.companion};
for (uni_hid_device_t* target : targets) {
if (uni_hid_parser_switch2_is_ble_device(target)) {
uni_hid_parser_switch2_reset_haptics(target);
}
}
slot.switch2_ingress.reset_pending = false;
}
bool switch2_has_hd(const ControllerRumbleOutput& rumble) {
return rumble.hd.actuators[0].sample_count != 0 ||
rumble.hd.actuators[1].sample_count != 0;
}
bool switch2_host_stop(const ControllerRumbleOutput& rumble) {
if (!switch2_has_hd(rumble)) {
return (rumble.low_frequency_magnitude | rumble.high_frequency_magnitude) == 0;
}
for (const SwitchHapticsActuatorFrame& side : rumble.hd.actuators) {
if (side.sample_count == 0 || side.sample_count > 3) return false;
for (uint8_t index = 0; index < side.sample_count; ++index) {
if (side.samples[index].low_amplitude_q15 != 0 ||
side.samples[index].high_amplitude_q15 != 0) return false;
}
}
return true;
}
void encode_switch2_side(uni_switch2_haptics_side_t& output,
const SwitchHapticsActuatorFrame& input) {
output.count = input.sample_count;
for (uint8_t index = 0; index < input.sample_count; ++index) {
const SwitchHapticsSample& sample = input.samples[index];
uni_switch2_haptics_encode_sample(
output.samples[index], sample.low_frequency_index,
sample.high_frequency_index, sample.low_amplitude_q15,
sample.high_amplitude_q15);
}
}
uni_switch2_haptics_frame_t switch2_physical_frame(
const ControllerRumbleOutput& rumble, const uni_hid_device_t* target,
bool paired) {
uni_switch2_haptics_frame_t frame{};
const SwitchHapticsActuatorFrame& left = rumble.hd.actuators[0];
const SwitchHapticsActuatorFrame& right = rumble.hd.actuators[1];
const int side = joycon_side(target);
if (side == 0) {
encode_switch2_side(frame.sides[0], left);
encode_switch2_side(frame.sides[1], right);
} else if (paired) {
encode_switch2_side(frame.sides[0], side < 0 ? left : right);
} else {
// Mono chooses each band's louder source independently. A short side
// holds its final substep; an absent side contributes no update.
frame.sides[0].count =
left.sample_count > right.sample_count ? left.sample_count : right.sample_count;
for (uint8_t index = 0; index < frame.sides[0].count; ++index) {
const SwitchHapticsSample* l = left.sample_count == 0 ? nullptr :
&left.samples[index < left.sample_count ? index : left.sample_count - 1];
const SwitchHapticsSample* r = right.sample_count == 0 ? nullptr :
&right.samples[index < right.sample_count ? index : right.sample_count - 1];
const SwitchHapticsSample* low = !r || (l && l->low_amplitude_q15 >= r->low_amplitude_q15) ? l : r;
const SwitchHapticsSample* high = !r || (l && l->high_amplitude_q15 >= r->high_amplitude_q15) ? l : r;
uni_switch2_haptics_encode_sample(
frame.sides[0].samples[index], low->low_frequency_index,
high->high_frequency_index, low->low_amplitude_q15,
high->high_amplitude_q15);
}
}
return frame;
}
void drain_switch2_ingress(uint8_t slot_index, uint32_t now_ms) {
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
if (!slot.active || !uni_hid_parser_switch2_is_ble_device(slot.device)) {
state_lock_exit();
return;
}
Switch2Ingress& ingress = slot.switch2_ingress;
const uint16_t duration_ms = host_rumble_duration_ms();
const uint8_t host_mode = switch2_host_mode();
if (ingress.host_mode != host_mode) {
clear_switch2_ingress(slot);
ingress.host_mode = host_mode;
}
if (ingress.reset_pending) reset_switch2_outputs(slot);
for (uint8_t budget = 0; budget < kSwitch2IngressCapacity && ingress.count != 0; ++budget) {
Switch2HostCommand& command = ingress.commands[ingress.head];
const RumbleEnvelope& envelope = command.envelope;
const bool hd = switch2_has_hd(envelope.rumble);
const bool stop = switch2_host_stop(envelope.rumble);
const bool stale = command.generation != ingress.generation ||
envelope.connection_generation != slot.connection_generation ||
envelope.duration_ms != duration_ms ||
(!stop && (hd || duration_ms != kXInputHostRumbleDurationMs) &&
static_cast<uint32_t>(now_ms - envelope.received_ms) >= UNI_SWITCH2_HAPTICS_WATCHDOG_MS);
const bool invalid = envelope.rumble.hd.actuators[0].sample_count > 3 ||
envelope.rumble.hd.actuators[1].sample_count > 3;
if (stale || invalid) {
__atomic_add_fetch(&g_switch2_ingress_drops, 1, __ATOMIC_RELAXED);
} else {
uni_hid_device_t* targets[] = {slot.device, slot.companion};
const uint8_t target_mask = slot.companion == nullptr ? 1 : 3;
for (uint8_t half = 0; half < 2; ++half) {
const uint8_t bit = 1u << half;
if (!(target_mask & bit) || (command.accepted_halves & bit)) continue;
bool accepted;
if (hd) {
const uni_switch2_haptics_frame_t frame =
switch2_physical_frame(envelope.rumble, targets[half], slot.companion != nullptr);
if (frame.sides[0].count == 0 && frame.sides[1].count == 0) {
command.accepted_halves |= bit;
continue;
}
accepted = uni_hid_parser_switch2_queue_haptics(
targets[half], &frame, envelope.received_ms);
} else {
accepted = uni_hid_parser_switch2_queue_rumble(
targets[half], envelope.rumble.high_frequency_magnitude,
envelope.rumble.low_frequency_magnitude,
stop ? 0 : envelope.duration_ms, envelope.received_ms);
}
if (accepted) {
command.accepted_halves |= bit;
__atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
}
}
if (command.accepted_halves != target_mask) break;
}
ingress.head = (ingress.head + 1u) % kSwitch2IngressCapacity;
--ingress.count;
}
state_lock_exit();
}
bool addresses_equal(const bd_addr_t first, const bd_addr_t second) {
return memcmp(first, second, sizeof(bd_addr_t)) == 0;
}
BleIdentityMapping* find_ble_identity_mapping(
hci_con_handle_t connection_handle,
const bd_addr_t connection_address) {
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
if (mapping.used &&
mapping.connection_handle == connection_handle &&
addresses_equal(mapping.connection_address,
connection_address)) {
return &mapping;
}
}
return nullptr;
}
BleIdentityMapping* find_ble_identity_mapping_for_handle(
hci_con_handle_t connection_handle) {
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
if (mapping.used &&
mapping.connection_handle == connection_handle) {
return &mapping;
}
}
return nullptr;
}
BleIdentityMapping* reserve_ble_identity_mapping(
hci_con_handle_t connection_handle) {
BleIdentityMapping* available = nullptr;
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
if (mapping.used &&
mapping.connection_handle == connection_handle) {
return &mapping;
}
if (!mapping.used && available == nullptr) {
available = &mapping;
}
}
return available;
}
ControllerIdentity make_ble_identity(
const BleIdentityMapping& mapping, const uni_hid_device_t* device) {
ControllerIdentity identity{};
identity.stable = true;
identity.transport = ControllerTransport::kBle;
identity.address_type = mapping.identity_address_type;
memcpy(identity.address, mapping.identity_address,
sizeof(identity.address));
identity.vendor_id = device->vendor_id;
identity.product_id = device->product_id;
return identity;
}
ControllerIdentity identity_for_device(const uni_hid_device_t* device) {
if (device == nullptr) {
return controller_identity_global();
}
switch (gap_get_connection_type(device->conn.handle)) {
case GAP_CONNECTION_ACL: {
ControllerIdentity identity{};
identity.stable = true;
identity.transport = ControllerTransport::kClassic;
identity.address_type = BD_ADDR_TYPE_UNKNOWN;
memcpy(identity.address, device->conn.btaddr,
sizeof(identity.address));
identity.vendor_id = device->vendor_id;
identity.product_id = device->product_id;
return identity;
}
case GAP_CONNECTION_LE: {
const BleIdentityMapping* mapping = find_ble_identity_mapping(
device->conn.handle, device->conn.btaddr);
if (mapping != nullptr) {
return make_ble_identity(*mapping, device);
}
uint8_t address_type = BD_ADDR_TYPE_UNKNOWN;
if (uni_hid_parser_switch2_identity_address_type(
device, &address_type)) {
BleIdentityMapping proprietary{};
proprietary.identity_address_type = address_type;
memcpy(proprietary.identity_address, device->conn.btaddr,
sizeof(proprietary.identity_address));
return make_ble_identity(proprietary, device);
}
break;
}
case GAP_CONNECTION_INVALID:
case GAP_CONNECTION_SCO:
break;
}
return controller_identity_global();
}
void publish_ble_identity(const BleIdentityMapping& mapping) {
ControllerIdentity observed_identity{};
bool observe_identity = false;
bool joycon_identity_changed = false;
state_lock_enter();
for (BackendSlot& slot : g_slots) {
if (slot.device != nullptr && slot.companion == nullptr &&
gap_get_connection_type(slot.device->conn.handle) ==
GAP_CONNECTION_LE &&
slot.device->conn.handle == mapping.connection_handle &&
addresses_equal(slot.device->conn.btaddr,
mapping.connection_address)) {
const ControllerIdentity identity = make_ble_identity(mapping, slot.device);
joycon_identity_changed = slot.active && joycon_side(slot.device) != 0 &&
!controller_identity_equal(slot.identity, identity);
slot.identity = identity;
if (slot.active) {
observed_identity = slot.identity;
observe_identity = true;
}
}
}
state_lock_exit();
if (observe_identity) {
profile_service_observe_identity_on_storage_core(
observed_identity);
if (joycon_identity_changed && g_joycon_mode == JoyConMode::kPaired) {
g_joycon_reconcile_requested = true;
}
}
}
void record_ble_identity(hci_con_handle_t connection_handle,
const bd_addr_t connection_address,
uint8_t identity_address_type,
const bd_addr_t identity_address) {
BleIdentityMapping* mapping =
reserve_ble_identity_mapping(connection_handle);
if (mapping == nullptr) {
return;
}
*mapping = {};
mapping->used = true;
mapping->connection_handle = connection_handle;
memcpy(mapping->connection_address, connection_address,
sizeof(mapping->connection_address));
mapping->identity_address_type = identity_address_type;
memcpy(mapping->identity_address, identity_address,
sizeof(mapping->identity_address));
publish_ble_identity(*mapping);
}
void clear_ble_identity_for_handle(hci_con_handle_t connection_handle) {
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
if (mapping.used &&
mapping.connection_handle == connection_handle) {
mapping = {};
}
}
state_lock_enter();
for (BackendSlot& slot : g_slots) {
if (slot.device != nullptr && slot.companion == nullptr &&
gap_get_connection_type(slot.device->conn.handle) ==
GAP_CONNECTION_LE &&
slot.device->conn.handle == connection_handle) {
slot.identity = controller_identity_global();
}
}
state_lock_exit();
}
void clear_ble_identity_for_device(const uni_hid_device_t* device) {
if (device == nullptr) {
return;
}
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
if (mapping.used &&
mapping.connection_handle == device->conn.handle &&
addresses_equal(mapping.connection_address,
device->conn.btaddr)) {
mapping = {};
}
}
}
void connection_address_for_handle(hci_con_handle_t connection_handle,
const bd_addr_t fallback,
bd_addr_t output) {
const uni_hid_device_t* device =
uni_hid_device_get_instance_for_connection_handle(
connection_handle);
if (device != nullptr) {
memcpy(output, device->conn.btaddr, sizeof(bd_addr_t));
return;
}
const BleIdentityMapping* mapping =
find_ble_identity_mapping_for_handle(connection_handle);
if (mapping != nullptr) {
memcpy(output, mapping->connection_address, sizeof(bd_addr_t));
return;
}
memcpy(output, fallback, sizeof(bd_addr_t));
}
void apply_slot_lighting(uint8_t slot_index, uni_hid_device_t* device) {
const SwitchRgbColor color =
switch_pro_get_slot_light_color(slot_index);
if (device->report_parser.set_lightbar_color != nullptr) {
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 << 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));
}
}
bool lighting_target_is_current(
uint8_t slot_index, uint32_t connection_generation,
const uni_hid_device_t* device) {
state_lock_enter();
const bool current =
device != nullptr && slot_index < kSlotCount &&
g_slots[slot_index].active &&
(g_slots[slot_index].device == device ||
g_slots[slot_index].companion == device) &&
g_slots[slot_index].connection_generation ==
connection_generation;
state_lock_exit();
return current;
}
Bluepad32ControllerLayout controller_layout(const BackendSlot& slot) {
if (!slot.active || slot.device == nullptr)
return Bluepad32ControllerLayout::kUnspecified;
const int side = joycon_side(slot.device);
if (side != 0) {
return slot.companion != nullptr
? Bluepad32ControllerLayout::kJoyCon2MergedPair
: side < 0 ? Bluepad32ControllerLayout::kJoyCon2LeftSolo
: Bluepad32ControllerLayout::kJoyCon2RightSolo;
}
switch (slot.device->controller_subtype) {
case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL:
case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL:
return Bluepad32ControllerLayout::kWiiHorizontal;
case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL:
return Bluepad32ControllerLayout::kWiiVertical;
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK:
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL:
return Bluepad32ControllerLayout::kWiiNunchuk;
default:
return Bluepad32ControllerLayout::kUnspecified;
}
}
ConnectionStatus compute_connection_status() {
state_lock_enter();
bool all_ready = true;
bool any_connecting = false;
unsigned physical_count = 0;
for (const BackendSlot& slot : g_slots) {
const bool has_device = slot.device != nullptr;
physical_count += has_device;
physical_count += slot.companion != nullptr;
all_ready = all_ready && (!has_device || slot.active);
any_connecting = any_connecting || (!slot.active && has_device);
}
#if SWITCH2_BRIDGE_FULL_INPUT
for (const auto* pending : g_native_pending_devices) {
if (pending != nullptr) {
++physical_count;
all_ready = false;
any_connecting = true;
}
}
#endif
state_lock_exit();
if (all_ready && physical_count == kSlotCount) {
return ConnectionStatus::Ready;
}
return any_connecting ? ConnectionStatus::Connecting
: ConnectionStatus::Scanning;
}
void publish_device_state(uint8_t slot, uni_hid_device_t* device,
uint16_t pre_hotkey_button_mask,
const ControllerState& state) {
state_lock_enter();
BackendSlot& target = g_slots[slot];
if (target.active && target.device == device) {
target.state = state;
target.pre_hotkey_button_mask = pre_hotkey_button_mask;
#ifdef SWITCH_PICO_WII_IR_GYRO
if (device->controller_type == CONTROLLER_TYPE_WiiController) {
const ControllerMotionSample sample =
state.motion_sample_count != 0
? state.motion_samples[0] : ControllerMotionSample{};
wii_ir_gyro_update_motion(
slot, target.connection_generation, target.motion_enabled, sample);
if (!target.motion_enabled) {
target.state.motion_sample_count = 0;
}
}
#endif
++target.state_generation;
#ifdef SWITCH2_BRIDGE_WII_INPUT
if (is_selected_wii(target)) {
const WiiMotionIngress& motion = target.wii_motion;
g_wii_snapshot.slot = slot;
g_wii_snapshot.controller = {
target.active, target.connection_generation, target.identity,
target.pre_hotkey_button_mask, target.state,
target.accelerometer, target.nunchuk_accelerometer};
g_wii_snapshot.layout = controller_layout(target);
g_wii_snapshot.state_generation = target.state_generation;
g_wii_snapshot.received_us = motion.received_us;
g_wii_snapshot.battery = device->controller.battery;
g_wii_snapshot.accel_valid = motion.accel_valid;
g_wii_snapshot.gyro_valid = motion.gyro_valid;
g_wii_snapshot.accel_sequence = motion.accel_sequence;
g_wii_snapshot.gyro_sequence = motion.gyro_sequence;
g_wii_snapshot.accel_received_us = motion.accel_received_us;
g_wii_snapshot.gyro_received_us = motion.gyro_received_us;
memcpy(g_wii_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13));
memcpy(g_wii_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10));
}
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
if (slot == g_native_slot && target.native_motion.has_report &&
target.connection_generation == g_native_generation) {
const NativeGamepadIngress& motion = target.native_motion;
g_native_snapshot.slot = slot;
g_native_snapshot.controller = {
target.active, target.connection_generation, target.identity,
target.pre_hotkey_button_mask, target.state,
target.accelerometer, target.nunchuk_accelerometer};
g_native_snapshot.state_generation = target.state_generation;
g_native_snapshot.received_us = motion.received_us;
g_native_snapshot.battery = device->controller.battery;
g_native_snapshot.track_stationary_bias =
device->controller_type == CONTROLLER_TYPE_WiiController;
g_native_snapshot.accel_valid = motion.accel_valid;
g_native_snapshot.gyro_valid = motion.gyro_valid;
g_native_snapshot.accel_sequence = motion.accel_sequence;
g_native_snapshot.gyro_sequence = motion.gyro_sequence;
g_native_snapshot.accel_received_us = motion.accel_received_us;
g_native_snapshot.gyro_received_us = motion.gyro_received_us;
memcpy(g_native_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13));
memcpy(g_native_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10));
}
#endif
g_macro_capture.observe(slot, target.connection_generation,
time_us_32(), target.state);
}
state_lock_exit();
}
void publish_all_neutral() {
state_lock_enter();
#ifdef SWITCH_PICO_WII_IR
wii_ir_pointer_reset();
#endif
for (BackendSlot& slot : g_slots) {
#if SWITCH2_BRIDGE_FULL_INPUT
retire_native_slot(static_cast<uint8_t>(&slot - g_slots));
#endif
#ifdef SWITCH2_BRIDGE_WII_INPUT
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
retire_wii_motion(slot.wii_motion);
#endif
clear_switch2_ingress(slot);
reset_switch2_outputs(slot);
slot.state = make_neutral_state();
slot.accelerometer = {};
slot.nunchuk_accelerometer = {};
slot.pre_hotkey_button_mask = 0;
slot.identity = controller_identity_global();
slot.device = nullptr;
slot.companion = nullptr;
slot.gamepad = {};
slot.companion_gamepad = {};
slot.extra_buttons = 0;
slot.companion_extra_buttons = 0;
slot.active = false;
slot.wii_orientation_pending = false;
slot.pending_wii_orientation = {};
#ifdef SWITCH_PICO_WII_IR_GYRO
slot.wii_aim = {};
#endif
slot.rumble_pending = false;
slot.retained_host_rumble_valid = false;
slot.retained_host_rumble = {};
slot.feedback_pending = false;
slot.feedback_until_ms = 0;
slot.pending_profile_feedback_count = 0;
for (ProfileFeedbackEnvelope& feedback :
slot.pending_profile_feedback) {
feedback = {};
}
slot.profile_feedback = {};
++slot.state_generation;
++slot.connection_generation;
}
for (uint8_t index = 0; index < kSlotCount; ++index) {
g_joycon_gestures[index] = {};
g_joycon_overrides[index] = {};
g_joycon_pair_hints[index] = {};
}
state_lock_exit();
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
mapping = {};
}
g_connection_status = ConnectionStatus::Initializing;
stop_background_scan();
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
g_pairing_window_open = false;
g_status_led_tick = 0;
}
constexpr int32_t clamp_axis(int32_t value) {
if (value < kAxisMinimum) {
return kAxisMinimum;
}
if (value > kAxisMaximum) {
return kAxisMaximum;
}
return value;
}
constexpr int16_t scale_axis(int32_t value) {
value = clamp_axis(value);
if (value <= 0) {
return static_cast<int16_t>(
(static_cast<int64_t>(value) * -INT16_MIN) /
-kAxisMinimum);
}
return static_cast<int16_t>(
(static_cast<int64_t>(value) * INT16_MAX) /
kAxisMaximum);
}
constexpr uint16_t scale_trigger(int32_t value) {
if (value <= 0) {
return 0;
}
if (value >= kTriggerFullScaleMinimum) {
return UINT16_MAX;
}
return static_cast<uint16_t>(
(static_cast<int64_t>(value) * UINT16_MAX) /
kTriggerMaximum);
}
constexpr int16_t clamp_int16(int64_t value) {
if (value < INT16_MIN) {
return INT16_MIN;
}
if (value > INT16_MAX) {
return INT16_MAX;
}
return static_cast<int16_t>(value);
}
constexpr int64_t divide_round_nearest(int64_t numerator,
int64_t denominator) {
if (numerator >= 0) {
return (numerator + denominator / 2) / denominator;
}
return -((-numerator + denominator / 2) / denominator);
}
constexpr int16_t convert_accel(int64_t q13_value) {
return clamp_int16(q13_value / 2);
}
constexpr int16_t convert_gyro(int64_t q10_value) {
constexpr int64_t kNumeratorScale = 13371;
constexpr int64_t kDenominator = 1024 * 936;
return clamp_int16(divide_round_nearest(q10_value * kNumeratorScale, kDenominator));
}
static_assert(scale_axis(-512) == INT16_MIN);
static_assert(scale_axis(0) == 0);
static_assert(scale_axis(511) == INT16_MAX);
static_assert(scale_trigger(0) == 0);
static_assert(scale_trigger(1016) == 65086);
static_assert(scale_trigger(1020) == UINT16_MAX);
static_assert(scale_trigger(1023) == UINT16_MAX);
static_assert(convert_accel(8192) == 4096);
static_assert(convert_accel(-8192) == -4096);
static_assert(convert_gyro(1024) == 14);
static_assert(convert_gyro(-1024) == -14);
bool has_motion(const uni_gamepad_t& gamepad) {
for (size_t i = 0; i < 3; ++i) {
if (gamepad.accel[i] != 0 || gamepad.gyro[i] != 0) {
return true;
}
}
return false;
}
constexpr uint16_t logical_button_bit(
ControllerProfileLogicalButton button) {
return static_cast<uint16_t>(
1u << static_cast<uint8_t>(button));
}
bool wake_chord_rising_edge(uint8_t slot, uni_hid_device_t* device,
uint16_t button_mask) {
const uint16_t chord =
logical_button_bit(ControllerProfileLogicalButton::kLeftShoulder) |
logical_button_bit(ControllerProfileLogicalButton::kRightShoulder) |
logical_button_bit(ControllerProfileLogicalButton::kSystem);
state_lock_enter();
const BackendSlot& previous = g_slots[slot];
const bool rising =
previous.active && previous.device == device &&
(button_mask & chord) == chord &&
(previous.pre_hotkey_button_mask & chord) != chord;
state_lock_exit();
return rising;
}
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));
}
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,
bool motion_enabled,
uint16_t button_mask) {
ControllerState state = make_neutral_state();
state.dpad_up =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kDpadUp)) != 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. Persistent
// profile mappings are the only button remapping layer.
state.button_south =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kSouth)) != 0;
state.button_east =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kEast)) != 0;
state.button_west =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kWest)) != 0;
state.button_north =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kNorth)) != 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 = scale_trigger(gamepad.brake);
if (gamepad.brake == 0 &&
(gamepad.buttons & BUTTON_TRIGGER_L) != 0) {
state.left_trigger = UINT16_MAX;
}
state.right_trigger = scale_trigger(gamepad.throttle);
if (gamepad.throttle == 0 &&
(gamepad.buttons & BUTTON_TRIGGER_R) != 0) {
state.right_trigger = UINT16_MAX;
}
state.button_left_stick =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kLeftStick)) != 0;
state.button_right_stick =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kRightStick)) != 0;
state.button_select =
(button_mask & logical_button_bit(
ControllerProfileLogicalButton::kSelect)) != 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_y = scale_axis(gamepad.axis_y);
state.right_stick_x = scale_axis(gamepad.axis_rx);
state.right_stick_y = scale_axis(gamepad.axis_ry);
if (motion_enabled && has_motion(gamepad)) {
// Dependency patches normalize both arrays to SDL3 PlayStation axes.
ControllerMotionSample sample{};
sample.accel_x = convert_accel(-static_cast<int64_t>(gamepad.accel[2]));
sample.accel_y = convert_accel(-static_cast<int64_t>(gamepad.accel[0]));
sample.accel_z = convert_accel(gamepad.accel[1]);
sample.gyro_x = convert_gyro(-static_cast<int64_t>(gamepad.gyro[2]));
sample.gyro_y = convert_gyro(-static_cast<int64_t>(gamepad.gyro[0]));
sample.gyro_z = convert_gyro(gamepad.gyro[1]);
state.motion_sample_count = 3;
for (ControllerMotionSample& destination : state.motion_samples) {
destination = sample;
}
}
return state;
}
int32_t negate_motion_axis(int32_t value) {
return value == INT32_MIN ? INT32_MAX : -value;
}
void rotate_solo_joycon(uni_gamepad_t& gamepad, int side,
uint8_t extras) {
const uint32_t buttons = gamepad.buttons;
if (side < 0) {
const int32_t x = gamepad.axis_x;
gamepad.axis_x = clamp_axis(gamepad.axis_y);
gamepad.axis_y = clamp_axis(-clamp_axis(x));
gamepad.buttons &= ~(BUTTON_A | BUTTON_B | BUTTON_X | BUTTON_Y);
gamepad.buttons |=
((gamepad.dpad & DPAD_LEFT) ? uint32_t{BUTTON_A} : 0u) |
((gamepad.dpad & DPAD_DOWN) ? uint32_t{BUTTON_B} : 0u) |
((gamepad.dpad & DPAD_UP) ? uint32_t{BUTTON_X} : 0u) |
((gamepad.dpad & DPAD_RIGHT) ? uint32_t{BUTTON_Y} : 0u) |
((extras & UNI_SW2_BUTTON_LEFT_SL) ? uint32_t{BUTTON_SHOULDER_L} : 0u) |
((extras & UNI_SW2_BUTTON_LEFT_SR) ? uint32_t{BUTTON_SHOULDER_R} : 0u);
} else {
gamepad.axis_x = clamp_axis(-clamp_axis(gamepad.axis_ry));
gamepad.axis_y = clamp_axis(gamepad.axis_rx);
gamepad.buttons &=
~(BUTTON_A | BUTTON_B | BUTTON_X | BUTTON_Y | BUTTON_THUMB_R);
gamepad.buttons |=
((buttons & BUTTON_B) ? uint32_t{BUTTON_A} : 0u) |
((buttons & BUTTON_Y) ? uint32_t{BUTTON_B} : 0u) |
((buttons & BUTTON_A) ? uint32_t{BUTTON_X} : 0u) |
((buttons & BUTTON_X) ? uint32_t{BUTTON_Y} : 0u) |
((buttons & BUTTON_THUMB_R) ? uint32_t{BUTTON_THUMB_L} : 0u) |
((extras & UNI_SW2_BUTTON_RIGHT_SL) ? uint32_t{BUTTON_SHOULDER_L} : 0u) |
((extras & UNI_SW2_BUTTON_RIGHT_SR) ? uint32_t{BUTTON_SHOULDER_R} : 0u);
}
gamepad.dpad = 0;
gamepad.axis_rx = 0;
gamepad.axis_ry = 0;
int32_t* motion_axes[] = {gamepad.accel, gamepad.gyro};
for (int32_t* axes : motion_axes) {
const int32_t x = axes[0];
axes[0] = side < 0 ? negate_motion_axis(axes[1]) : axes[1];
axes[1] = side < 0 ? x : negate_motion_axis(x);
}
}
#ifdef SWITCH_PICO_WII_IR_GYRO
uint32_t wii_aim_chord_button_mask(const uni_hid_device_t* device) {
if (device == nullptr ||
device->controller_type != CONTROLLER_TYPE_WiiController) {
return 0;
}
switch (device->controller_subtype) {
case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL:
case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL:
return BUTTON_A | BUTTON_B;
case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL:
return BUTTON_X | BUTTON_Y;
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK:
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL:
return BUTTON_SHOULDER_L | BUTTON_SHOULDER_R;
default:
return 0;
}
}
#endif
uni_gamepad_t logical_gamepad(const BackendSlot& slot) {
uni_gamepad_t gamepad = slot.gamepad;
mask_joycon_gesture(gamepad, slot.device);
#ifdef SWITCH_PICO_WII_IR_GYRO
if (slot.wii_aim.masked) {
gamepad.buttons &= ~wii_aim_chord_button_mask(slot.device);
}
if (slot.wii_aim.reposition_masked) {
gamepad.buttons &= ~(BUTTON_X | BUTTON_SHOULDER_L);
}
#endif
if (slot.companion != nullptr) {
const uni_gamepad_t& right = slot.companion_gamepad;
gamepad.dpad |= right.dpad;
const bool masked = joycon_gesture_masked(slot.companion);
gamepad.buttons |=
right.buttons & ~(masked ? uint32_t{BUTTON_TRIGGER_R} : 0u);
gamepad.misc_buttons |=
right.misc_buttons & ~(masked ? uint32_t{MISC_BUTTON_START} : 0u);
gamepad.axis_rx = right.axis_rx;
gamepad.axis_ry = right.axis_ry;
gamepad.throttle = masked ? 0 : right.throttle;
// The right half is the sole aim source. A left report must not
// republish an already consumed right-hand motion sample.
memcpy(gamepad.accel, right.accel, sizeof(gamepad.accel));
memcpy(gamepad.gyro, right.gyro, sizeof(gamepad.gyro));
} else {
const int side = joycon_side(slot.device);
if (side != 0) {
rotate_solo_joycon(gamepad, side, slot.extra_buttons);
}
}
return gamepad;
}
void refresh_topology_input(BackendSlot& slot) {
const uni_gamepad_t gamepad = logical_gamepad(slot);
slot.pre_hotkey_button_mask = logical_button_mask(gamepad);
// Topology changes release the lost half immediately; motion stays neutral
// until a fresh report from the newly selected source arrives.
slot.state = map_gamepad(gamepad, false, slot.pre_hotkey_button_mask);
slot.state.extra_buttons =
slot.extra_buttons | slot.companion_extra_buttons;
}
struct HotkeyDecision {
bool motion_enabled;
};
void queue_local_feedback(BackendSlot& slot, uint16_t duration_ms,
uint8_t weak_magnitude,
uint8_t strong_magnitude) {
slot.feedback_pending = true;
slot.pending_feedback = {
slot.connection_generation, duration_ms, weak_magnitude,
strong_magnitude};
__atomic_add_fetch(&g_local_feedback_requests, 1, __ATOMIC_RELAXED);
}
void queue_profile_feedback(BackendSlot& slot,
const ProfileFeedbackEnvelope& feedback) {
if (slot.pending_profile_feedback_count < kProfileFeedbackQueueCapacity) {
slot.pending_profile_feedback[
slot.pending_profile_feedback_count++] = feedback;
} else {
slot.pending_profile_feedback[kProfileFeedbackQueueCapacity - 1u] =
feedback;
}
}
#ifdef SWITCH_PICO_WII_IR_GYRO
void observe_wii_aim_chord(BackendSlot& slot, uni_hid_device_t* device,
const uni_gamepad_t& gamepad,
const uni_wii_ir_snapshot_t* infrared,
uint32_t now_us) {
const uint32_t mapped_buttons = wii_aim_chord_button_mask(device);
if (mapped_buttons == 0) return;
WiiAimSource& aim = slot.wii_aim;
const uint8_t slot_index = static_cast<uint8_t>(&slot - g_slots);
if (mapped_buttons == (BUTTON_SHOULDER_L | BUTTON_SHOULDER_R)) {
const uint32_t controls = BUTTON_X | BUTTON_SHOULDER_L; // Nunchuk C + 1.
const uint32_t pressed = gamepad.buttons & controls;
if (aim.infrared && pressed == controls) {
aim.reposition_masked = true;
} else if (pressed == 0) {
aim.reposition_masked = false;
}
} else {
aim.reposition_masked = false;
}
// Expire before accepting a returning packet. Cached snapshots cannot
// extend or complete a hold, and a gap requires a full release to retry.
// A transport gap cancels the gesture, not the user's selected source.
// The pointer's freshness guard stops IR output without falling back.
if (aim.have_sequence &&
now_us - aim.last_report_us >= kWiiAimChordFreshUs) {
aim.holding = false;
aim.latched = aim.latched || aim.masked;
}
const uint32_t pressed_buttons = gamepad.buttons & mapped_buttons;
if (pressed_buttons == mapped_buttons) {
aim.masked = true;
} else {
aim.holding = false;
if (pressed_buttons == 0) {
aim.masked = false;
aim.latched = false;
} else {
aim.latched = aim.latched || aim.masked;
}
}
if (infrared == nullptr ||
(aim.have_sequence && infrared->sequence == aim.sequence) ||
(!aim.have_sequence && infrared->sequence == 0)) {
return;
}
aim.have_sequence = true;
aim.sequence = infrared->sequence;
aim.last_report_us = now_us;
const uint16_t buttons = infrared->buttons & kWiiAimChordButtons;
if (buttons != kWiiAimChordButtons || pressed_buttons != mapped_buttons) {
aim.holding = false;
aim.latched = aim.latched || aim.masked;
return;
}
if (aim.latched) return;
if (!aim.holding) {
aim.started_us = now_us;
aim.holding = true;
return;
}
if (now_us - aim.started_us < kWiiAimChordHoldUs) return;
aim.latched = true;
if (!wii_ir_gyro_select(
slot_index, slot.connection_generation, !aim.infrared)) {
return;
}
aim.infrared = !aim.infrared;
queue_profile_feedback(
slot, {slot.connection_generation,
static_cast<uint8_t>(aim.infrared ? 2 : 1),
ControllerProfileConfirmationPolicy::kRumble});
__atomic_add_fetch(&g_local_feedback_requests, 1, __ATOMIC_RELAXED);
}
#endif
void reset_slot_hotkeys(BackendSlot& slot) {
g_macro_capture.disconnect(static_cast<uint8_t>(&slot - g_slots),
slot.connection_generation, time_us_32());
slot.wii_orientation_pending = false;
slot.pending_wii_orientation = {};
#if SWITCH2_BRIDGE_FULL_INPUT
retire_native_slot(static_cast<uint8_t>(&slot - g_slots));
#endif
#ifdef SWITCH2_BRIDGE_WII_INPUT
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
retire_wii_motion(slot.wii_motion);
#endif
#ifdef SWITCH_PICO_WII_IR_GYRO
slot.wii_aim = {};
#endif
slot.motion_enabled = kDefaultMotionEnabled;
slot.pre_hotkey_button_mask = 0;
slot.accelerometer = {};
slot.nunchuk_accelerometer = {};
slot.feedback_pending = false;
slot.feedback_until_ms = 0;
slot.pending_feedback = {};
slot.pending_profile_feedback_count = 0;
for (ProfileFeedbackEnvelope& feedback :
slot.pending_profile_feedback) {
feedback = {};
}
slot.profile_feedback = {};
slot.retained_host_rumble_valid = false;
slot.retained_host_rumble = {};
}
void invalidate_slot(BackendSlot& slot) {
#ifdef SWITCH_PICO_WII_IR
wii_ir_pointer_disconnect(static_cast<uint8_t>(&slot - g_slots));
#endif
clear_switch2_ingress(slot);
reset_switch2_outputs(slot);
reset_slot_hotkeys(slot);
slot.rumble_pending = false;
slot.pending_rumble = {};
slot.state = make_neutral_state();
++slot.state_generation;
++slot.connection_generation;
}
void release_slot(BackendSlot& slot) {
invalidate_slot(slot);
slot.identity = controller_identity_global();
slot.device = nullptr;
slot.companion = nullptr;
slot.gamepad = {};
slot.companion_gamepad = {};
slot.extra_buttons = 0;
slot.companion_extra_buttons = 0;
slot.active = false;
}
bool is_solo_wii_remote(const BackendSlot& slot) {
if (!slot.active || slot.device == nullptr || slot.companion != nullptr ||
slot.device->controller_type != CONTROLLER_TYPE_WiiController) {
return false;
}
switch (slot.device->controller_subtype) {
case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL:
case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL:
case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL:
return true;
default:
return false;
}
}
HotkeyDecision update_controller_hotkeys(
uint8_t slot_index, uni_hid_device_t* device) {
HotkeyDecision decision{kDefaultMotionEnabled};
state_lock_enter();
const BackendSlot& slot = g_slots[slot_index];
if (slot.active && slot.device == device) {
decision.motion_enabled = slot.motion_enabled;
}
state_lock_exit();
return decision;
}
bool pairing_window_active_at(uint32_t now_ms) {
return g_pairing_window_open &&
static_cast<int32_t>(now_ms - g_pairing_window_deadline_ms) < 0;
}
void handle_btstack_event(uint8_t packet_type, uint16_t channel,
uint8_t* packet, uint16_t size) {
(void)channel;
if (packet_type != HCI_EVENT_PACKET || packet == nullptr || size < 2) {
return;
}
bd_addr_t address{};
bd_addr_t identity_address{};
bd_addr_t connection_address{};
hci_con_handle_t connection_handle = 0;
const bool pairing_open =
pairing_window_active_at(btstack_run_loop_get_time_ms());
switch (hci_event_packet_get_type(packet)) {
case SM_EVENT_IDENTITY_RESOLVING_STARTED:
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
clear_ble_identity_for_handle(
sm_event_identity_resolving_started_get_handle(packet));
}
break;
case SM_EVENT_IDENTITY_RESOLVING_FAILED:
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
clear_ble_identity_for_handle(
sm_event_identity_resolving_failed_get_handle(packet));
}
break;
case SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED:
if (SWITCH_PICO_ENABLE_BLE && size >= 20) {
connection_handle =
sm_event_identity_resolving_succeeded_get_handle(packet);
sm_event_identity_resolving_succeeded_get_address(
packet, connection_address);
sm_event_identity_resolving_succeeded_get_identity_address(
packet, identity_address);
record_ble_identity(
connection_handle, connection_address,
sm_event_identity_resolving_succeeded_get_identity_addr_type(
packet),
identity_address);
}
break;
case SM_EVENT_IDENTITY_CREATED:
if (SWITCH_PICO_ENABLE_BLE && size >= 20) {
connection_handle =
sm_event_identity_created_get_handle(packet);
sm_event_identity_created_get_address(packet, address);
sm_event_identity_created_get_identity_address(
packet, identity_address);
connection_address_for_handle(
connection_handle, address, connection_address);
record_ble_identity(
connection_handle, connection_address,
sm_event_identity_created_get_identity_addr_type(packet),
identity_address);
}
break;
case SM_EVENT_REENCRYPTION_STARTED:
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
connection_handle =
sm_event_reencryption_started_get_handle(packet);
sm_event_reencryption_started_get_address(
packet, identity_address);
connection_address_for_handle(
connection_handle, identity_address,
connection_address);
record_ble_identity(
connection_handle, connection_address,
sm_event_reencryption_started_get_addr_type(packet),
identity_address);
}
break;
case SM_EVENT_REENCRYPTION_COMPLETE:
if (SWITCH_PICO_ENABLE_BLE && size >= 12) {
connection_handle =
sm_event_reencryption_complete_get_handle(packet);
if (sm_event_reencryption_complete_get_status(packet) ==
ERROR_CODE_SUCCESS) {
sm_event_reencryption_complete_get_address(
packet, identity_address);
connection_address_for_handle(
connection_handle, identity_address,
connection_address);
record_ble_identity(
connection_handle, connection_address,
sm_event_reencryption_complete_get_addr_type(packet),
identity_address);
} else {
clear_ble_identity_for_handle(connection_handle);
}
}
break;
case HCI_EVENT_USER_CONFIRMATION_REQUEST:
if (!SWITCH_PICO_ENABLE_CLASSIC || size < 8) {
break;
}
hci_event_user_confirmation_request_get_bd_addr(packet, address);
if (pairing_open) {
gap_ssp_confirmation_response(address);
} else {
gap_ssp_confirmation_negative(address);
}
break;
case HCI_EVENT_USER_PASSKEY_REQUEST:
if (!SWITCH_PICO_ENABLE_CLASSIC || size < 8) {
break;
}
hci_event_user_passkey_request_get_bd_addr(packet, address);
if (pairing_open) {
gap_ssp_passkey_response(address, 0);
} else {
gap_ssp_passkey_negative(address);
}
break;
default:
break;
}
}
bool update_pairing_window(uint32_t now_ms) {
state_lock_enter();
const bool requested = g_pairing_window_requested;
g_pairing_window_requested = false;
state_lock_exit();
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
return false;
}
if (requested) {
ConfigurationServiceSnapshot configuration{};
configuration_service_snapshot(&configuration);
g_pairing_window_duration_ms =
static_cast<uint32_t>(
configuration.configuration.pairing_window_seconds) *
1000u;
g_pairing_window_open = true;
g_pairing_window_deadline_ms =
now_ms + g_pairing_window_duration_ms;
gap_set_bondable_mode(true);
if (SWITCH_PICO_ENABLE_BLE) {
sm_set_accepted_stk_generation_methods(kAllBlePairingMethods);
}
g_status_led_tick = 0;
return true;
}
if (g_pairing_window_open && !pairing_window_active_at(now_ms)) {
g_pairing_window_open = false;
if (SWITCH_PICO_ENABLE_BLE) {
sm_set_accepted_stk_generation_methods(0);
}
g_status_led_tick = 0;
gap_set_bondable_mode(false);
return true;
}
return false;
}
void append_pairing_record(
Bluepad32PairingSnapshot& snapshot,
Bluepad32PairingTransport transport, uint8_t address_type,
const bd_addr_t address) {
if (snapshot.record_count >= BLUEPAD32_PAIRING_RECORD_CAPACITY) {
snapshot.overflow = true;
return;
}
Bluepad32PairingRecord& record =
snapshot.records[snapshot.record_count++];
record.transport = transport;
record.address_type = address_type;
memcpy(record.address, address, sizeof(record.address));
}
void refresh_pairing_snapshot() {
Bluepad32PairingSnapshot snapshot{};
snapshot.status = Bluepad32PairingSnapshotStatus::kReady;
btstack_link_key_iterator_t iterator{};
if (gap_link_key_iterator_init(&iterator)) {
bd_addr_t address{};
link_key_t link_key{};
link_key_type_t link_key_type{};
while (gap_link_key_iterator_get_next(
&iterator, address, link_key, &link_key_type)) {
append_pairing_record(
snapshot, Bluepad32PairingTransport::kClassic,
BD_ADDR_TYPE_UNKNOWN, address);
}
gap_link_key_iterator_done(&iterator);
}
for (int index = 0; index < le_device_db_max_count(); ++index) {
int address_type = BD_ADDR_TYPE_UNKNOWN;
bd_addr_t address{};
le_device_db_info(index, &address_type, address, nullptr);
if (address_type == BD_ADDR_TYPE_UNKNOWN) {
continue;
}
append_pairing_record(
snapshot, Bluepad32PairingTransport::kBle,
static_cast<uint8_t>(address_type), address);
}
for (uint8_t index = 0; index < UNI_SWITCH2_PAIRING_CAPACITY; ++index) {
uint8_t address_type = BD_ADDR_TYPE_UNKNOWN;
bd_addr_t address{};
if (uni_switch2_pairing_get(index, &address_type, address)) {
append_pairing_record(
snapshot, Bluepad32PairingTransport::kBle, address_type, address);
}
}
state_lock_enter();
if (g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kFailed) {
snapshot.status = Bluepad32PairingSnapshotStatus::kFailed;
}
snapshot.generation = g_pairing_snapshot.generation + 1;
snapshot.completed_clear_pairings_token =
g_pairing_snapshot.completed_clear_pairings_token;
g_pairing_snapshot = snapshot;
g_pairing_snapshot_requested = false;
state_lock_exit();
}
void process_pairing_snapshot_request() {
state_lock_enter();
const bool requested = g_pairing_snapshot_requested;
state_lock_exit();
if (requested) {
refresh_pairing_snapshot();
}
}
void apply_connection_policy();
void recompute_connection_status();
void process_clear_pairings(uint32_t now_ms) {
uni_hid_device_t* devices[kSlotCount]{};
uint8_t device_count = 0;
state_lock_enter();
const uint32_t request_token =
g_clear_pairings_requested_token;
if (request_token != 0) {
g_clear_pairings_requested_token = 0;
g_clear_pairings_in_progress_token = request_token;
}
if (request_token != 0) {
g_pairing_window_requested = false;
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
BackendSlot& slot = g_slots[slot_index];
if (slot.device != nullptr) {
g_retired_devices[physical_index_for_device(slot.device)] =
slot.device;
devices[device_count++] = slot.device;
}
if (slot.companion != nullptr) {
g_retired_devices[physical_index_for_device(slot.companion)] =
slot.companion;
devices[device_count++] = slot.companion;
}
release_slot(slot);
g_joycon_gestures[slot_index] = {};
g_joycon_overrides[slot_index] = {};
g_joycon_pair_hints[slot_index] = {};
}
}
state_lock_exit();
if (request_token == 0) {
return;
}
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
mapping = {};
}
g_pairing_window_open = false;
gap_set_bondable_mode(false);
if (SWITCH_PICO_ENABLE_BLE) {
sm_set_accepted_stk_generation_methods(0);
}
const bool proprietary_cleared = uni_switch2_pairing_clear();
uni_bt_del_keys_unsafe();
for (uni_hid_device_t* device : devices) {
if (device != nullptr) {
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
switch_native_output_detach(device);
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
haptics_experiment_detach(device);
#endif
uni_hid_device_disconnect(device);
}
}
refresh_pairing_snapshot();
if (!proprietary_cleared) {
stop_background_scan();
uni_bt_stop_scanning_unsafe();
uni_bt_allow_incoming_connections(false);
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
state_lock_enter();
g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kFailed;
g_clear_pairings_in_progress_token = 0;
g_clear_pairings_requested_token = 0;
g_pairing_window_requested = false;
state_lock_exit();
return;
}
g_connection_status = ConnectionStatus::Scanning;
g_status_led_tick = 0;
g_pairing_reset_feedback_deadline_ms =
now_ms + kPairingResetFeedbackDurationMs;
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
}
apply_connection_policy();
state_lock_enter();
g_pairing_snapshot.completed_clear_pairings_token =
request_token;
g_clear_pairings_in_progress_token = 0;
state_lock_exit();
}
void apply_connection_policy() {
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
return;
}
apply_radio_connection_policy();
const bool free_slot = has_free_slot();
const bool active_controller = has_active_controller();
const bool pairing_open =
pairing_window_active_at(btstack_run_loop_get_time_ms());
const bool active_scan =
free_slot && (!active_controller || pairing_open);
const bool background_scan = SWITCH_PICO_ENABLE_BLE &&
free_slot && !active_scan && waiting_for_joycon_mate();
const ConnectionPolicyState desired_state =
!free_slot
? ConnectionPolicyState::Paused
: (active_scan ? ConnectionPolicyState::Open
: ConnectionPolicyState::Passive);
if (g_connection_policy_state == desired_state &&
g_background_scan_active == background_scan) {
return;
}
// Leave low-duty LE explicitly before the aggregate stop, which otherwise
// does nothing when its own scanning flag is already clear.
stop_background_scan();
uni_bt_stop_scanning_unsafe();
if (!free_slot) {
uni_bt_allow_incoming_connections(false);
g_connection_policy_state = ConnectionPolicyState::Paused;
return;
}
// Passive mode permits incoming Classic reconnects, with LE discovery
// limited to a remembered opposite half for a ready solo Joy-Con2.
uni_bt_allow_incoming_connections(SWITCH_PICO_ENABLE_CLASSIC != 0);
if (active_scan) {
if (SWITCH_PICO_ENABLE_BLE) {
uni_bt_le_set_background_scan(false);
}
uni_bt_start_scanning_and_autoconnect_unsafe();
g_connection_policy_state = ConnectionPolicyState::Open;
} else {
g_connection_policy_state = ConnectionPolicyState::Passive;
if (background_scan) {
uni_bt_le_set_background_scan(true);
uni_bt_le_scan_start();
g_background_scan_active = true;
}
}
}
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() {
++g_status_led_tick;
const uint32_t now_ms = btstack_run_loop_get_time_ms();
bool profile_led_override = false;
bool profile_led_on = false;
state_lock_enter();
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;
}
}
state_lock_exit();
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) {
led_on = (g_status_led_tick % 20) < 10;
} else if (pairing_window_active_at(now_ms)) {
const uint16_t phase = g_status_led_tick % 200;
led_on = phase < 20 || (phase >= 40 && phase < 60);
} else if (g_connection_status == ConnectionStatus::Connecting) {
led_on = (g_status_led_tick % 40) < 20;
} else if (g_connection_status == ConnectionStatus::Initializing ||
has_active_controller()) {
led_on = true;
} else {
led_on = (g_status_led_tick % 200) < 100;
}
if (led_on != g_status_led_on) {
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on);
g_status_led_on = led_on;
}
}
void apply_joycon_configuration(const ConfigurationServiceSnapshot& configuration);
void process_joycon_gestures(uint32_t now_ms);
void process_configuration_timer(btstack_timer_source_t* timer) {
__atomic_add_fetch(
&g_configuration_timer_ticks, 1, __ATOMIC_RELAXED);
btstack_run_loop_set_timer(timer, kConfigurationPollIntervalMs);
btstack_run_loop_add_timer(timer);
const uint32_t now_ms = btstack_run_loop_get_time_ms();
apply_radio_connection_policy();
configuration_service_task_on_storage_core(now_ms);
profile_service_task_on_storage_core(now_ms);
ConfigurationServiceSnapshot configuration{};
configuration_service_snapshot(&configuration);
apply_joycon_configuration(configuration);
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
if (configuration.state == ConfigurationServiceState::kReady) {
uint8_t previously_owned = 0;
for (uint8_t i = 0; i < kSlotCount; ++i)
if (switch_native_output_owns(g_slots[i].device)) previously_owned |= 1u << i;
switch_native_output_configure(configuration.configuration, configuration.generation);
for (uint8_t i = 0; i < kSlotCount; ++i) {
if ((previously_owned & (1u << i)) || !switch_native_output_owns(g_slots[i].device))
continue;
RumbleEnvelope retained{};
state_lock_enter();
const BackendSlot& current = g_slots[i];
retained = current.pending_rumble;
const bool valid = current.active && retained.slot == i &&
retained.connection_generation == current.connection_generation &&
retained.duration_ms == host_rumble_duration_ms();
state_lock_exit();
if (valid) switch_native_output_submit(i, retained.connection_generation,
retained.received_us, retained.rumble,
retained.duration_ms == kXInputHostRumbleDurationMs);
}
}
#endif
}
void dispatch_rumble(uni_hid_device_t* device, uint16_t duration_ms,
uint8_t weak, uint8_t strong) {
#if SWITCH2_BRIDGE_FULL_INPUT
if (device->controller_type == CONTROLLER_TYPE_PS5Controller &&
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) {
// Local feedback shares the bounded writer. Its stale compatibility
// packets must not remain queued to overtake a later native cue.
uni_hid_parser_ds5_bridge_rumble(
device, duration_ms > 1000 ? 1000 : duration_ms, weak, strong);
return;
}
#endif
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
if (switch_native_output_feedback(device, strong, weak, duration_ms)) return;
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
if (haptics_experiment_feedback(device, strong, weak, duration_ms)) {
return;
}
#endif
device->report_parser.play_dual_rumble(device, 0, duration_ms, weak, strong);
}
#ifdef SWITCH2_BRIDGE_WII_INPUT
struct WiiCueDispatch {
uni_hid_device_t* device = nullptr;
uint64_t token = 0;
uint32_t connection_generation = 0;
uint8_t slot = 0xff;
uint8_t phase = 0;
uint16_t duration_ms = 0;
bool cancellation_stop = false;
};
bool wii_cue_target_current() {
return g_wii_cue.slot < kSlotCount &&
is_selected_wii(g_slots[g_wii_cue.slot]) &&
g_slots[g_wii_cue.slot].connection_generation ==
g_wii_cue.connection_generation;
}
void restore_wii_host_rumble(BackendSlot& slot) {
if (slot.retained_host_rumble_valid) {
slot.pending_rumble = slot.retained_host_rumble;
slot.rumble_pending = true;
}
}
// Called under the backend lock, after profile/local arbitration. A waiting
// cue yields to local feedback; a playing cue is canceled rather than replayed
// after an interruption. A finite driver timer bounds even a stalled poller.
bool prepare_wii_cue(uint8_t slot_index, uint32_t now_ms,
bool local_active, bool local_dispatch,
WiiCueDispatch* output) {
WiiCue& cue = g_wii_cue;
if (cue.slot != slot_index) return false;
BackendSlot& slot = g_slots[slot_index];
if (!wii_cue_target_current() ||
slot.device->report_parser.play_dual_rumble == nullptr) {
cue = {};
return false;
}
if ((cue.result == 0 &&
static_cast<uint32_t>(now_ms - cue.requested_ms) >= kWiiCueDeadlineMs) ||
(cue.active &&
static_cast<uint32_t>(now_ms - cue.started_ms) >= kWiiCueDeadlineMs)) {
cancel_wii_cue_locked();
}
if (local_active || local_dispatch) {
if (cue.active) cancel_wii_cue_locked();
// A local rumble command replaces our finite pulse; a later stop must
// not cut that higher-priority feedback short.
if (local_dispatch) cue.stop_pending = false;
return cue.result == 0 || cue.active || cue.stop_pending;
}
if (cue.in_flight) return true;
if (cue.result != 0 && !cue.active && !cue.stop_pending) return false;
uint8_t phase = 0;
uint16_t duration_ms = 0;
if (!cue.stop_pending && cue.sample_id != 0) {
const WiiCuePattern& pattern = kWiiCuePatterns[cue.sample_id];
uint32_t elapsed = cue.active ? now_ms - cue.started_ms : 0;
while (phase < pattern.count && elapsed >= pattern.phases_ms[phase]) {
elapsed -= pattern.phases_ms[phase++];
}
if (phase == pattern.count) {
cue.active = false;
restore_wii_host_rumble(slot);
return false;
}
// The driver's duration timer supplies the gaps. Skip missed phases,
// never replay a burst of old pulses to catch up after a scheduling gap.
if ((phase & 1u) != 0 || phase == cue.dispatched_phase) return true;
duration_ms = static_cast<uint16_t>(pattern.phases_ms[phase] - elapsed);
}
*output = {slot.device, cue.token, cue.connection_generation,
slot_index, phase, duration_ms, cue.stop_pending};
cue.in_flight = true;
return true;
}
void dispatch_wii_cue(const WiiCueDispatch& command) {
if (command.device == nullptr) return;
state_lock_enter();
const bool current = g_wii_cue.token == command.token &&
g_wii_cue.in_flight && wii_cue_target_current() &&
g_slots[command.slot].device == command.device &&
(command.cancellation_stop ? g_wii_cue.stop_pending
: g_wii_cue.result != -1);
state_lock_exit();
// Lifecycle/parser callbacks are serialized on Core 1. The readiness check
// excludes the Wii void hook's early-return path during topology setup.
const bool dispatched = current &&
command.device->report_parser.play_dual_rumble != nullptr &&
uni_hid_parser_wii_rumble_ready(command.device);
if (dispatched) {
__atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
command.device->report_parser.play_dual_rumble(
command.device, 0, command.duration_ms, UINT8_MAX, UINT8_MAX);
}
state_lock_enter();
WiiCue& cue = g_wii_cue;
if (cue.token == command.token && cue.connection_generation == command.connection_generation) {
cue.in_flight = false;
if (!wii_cue_target_current()) {
cue = {};
} else if (command.cancellation_stop) {
if (dispatched) {
cue.stop_pending = false;
restore_wii_host_rumble(g_slots[command.slot]);
}
} else if (cue.result != -1 && dispatched) {
const uint32_t now_ms = btstack_run_loop_get_time_ms();
if (cue.result == 0 && now_ms - cue.requested_ms >= kWiiCueDeadlineMs) {
cue.stop_pending = command.duration_ms != 0;
cancel_wii_cue_locked();
} else {
if (cue.result == 0) {
cue.started_ms = now_ms;
cue.result = 1; // Driver dispatch, not an application ACK.
}
cue.active = cue.sample_id != 0;
cue.dispatched_phase = command.phase;
if (!cue.active) restore_wii_host_rumble(g_slots[command.slot]);
}
}
}
state_lock_exit();
}
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
struct NativeGamepadCueDispatch {
uni_hid_device_t* device = nullptr;
uni_hid_device_t* companion = nullptr;
uint32_t connection_generation = 0;
uint32_t prepared_ms = 0;
uint64_t token[2]{};
uint16_t duration_ms = 0;
uint8_t magnitude[2]{};
uint8_t slot = 0xff;
};
// Source drivers use a shared finite timer (or one per paired half). Recompute
// both contributions at each boundary; the shortest ON remainder protects a
// mono actuator too. Absolute timelines skip missed pulses, never queue them.
bool prepare_native_cues(uint8_t index, uint32_t now_ms,
bool local_active, bool local_dispatch,
NativeGamepadCueDispatch* command) {
BackendSlot& slot = g_slots[index];
NativeGamepadMotorOutput& previous = slot.native_output;
bool busy = false;
bool pending = false;
uint16_t duration = UINT16_MAX;
uint8_t magnitude[2]{};
for (uint8_t side = 0; side < 2; ++side) {
NativeGamepadCue& cue = g_native_cues[side];
if (cue.slot != index) continue;
if (!native_cue_current(cue) ||
(cue.result == 0 && now_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs) ||
(cue.active && now_ms - cue.started_ms >= kNativeGamepadCueDeadlineMs))
cancel_native_cue_locked(cue);
if (local_active || local_dispatch) {
if (cue.active) cancel_native_cue_locked(cue);
busy |= cue.result == 0;
continue;
}
if (cue.in_flight) return true;
if (cue.result != 0 && !cue.active) continue;
command->token[side] = cue.token;
pending |= cue.result == 0;
if (cue.sample_id == 0) continue;
const NativeGamepadCuePattern& pattern = kNativeGamepadCuePatterns[cue.sample_id];
uint32_t elapsed = cue.active ? now_ms - cue.started_ms : 0;
uint8_t phase = 0;
while (phase < pattern.count && elapsed >= pattern.phases_ms[phase])
elapsed -= pattern.phases_ms[phase++];
if (phase == pattern.count) {
cue.active = false;
command->token[side] = 0;
continue;
}
busy = true;
if ((phase & 1u) == 0) {
magnitude[side] = pattern.magnitude;
const uint16_t remaining = static_cast<uint16_t>(pattern.phases_ms[phase] - elapsed);
if (remaining < duration) duration = remaining;
}
}
if (local_active || local_dispatch) {
// Higher-priority feedback replaces our finite timer; do not stop it.
if (local_dispatch) previous = {};
return busy || previous.owned;
}
if (duration == UINT16_MAX) duration = 0;
const uint32_t deadline = duration == 0 ? 0 : now_ms + duration;
const bool changed = magnitude[0] != previous.magnitude[0] ||
magnitude[1] != previous.magnitude[1] ||
(duration != 0 && deadline != previous.deadline_ms);
if (!pending && !changed) return busy || previous.owned;
if (!slot.active || slot.device == nullptr ||
(!native_rumble_capable(slot, 0) && !native_rumble_capable(slot, 1))) {
for (NativeGamepadCue& cue : g_native_cues)
if (cue.slot == index) cancel_native_cue_locked(cue);
previous = {};
return false;
}
command->device = slot.device;
command->companion = slot.companion;
command->connection_generation = slot.connection_generation;
command->prepared_ms = now_ms;
command->duration_ms = duration;
command->magnitude[0] = magnitude[0];
command->magnitude[1] = magnitude[1];
command->slot = index;
for (uint8_t side = 0; side < 2; ++side)
if (command->token[side] != 0) g_native_cues[side].in_flight = true;
return true;
}
bool submit_native_rumble(uni_hid_device_t* device, uint16_t duration,
uint8_t right, uint8_t left) {
if (device == nullptr || device->report_parser.play_dual_rumble == nullptr)
return false;
if (device->controller_type == CONTROLLER_TYPE_PS5Controller &&
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) {
return uni_hid_parser_ds5_bridge_rumble(device, duration, right, left);
}
// Existing finite-duration dispatch is the strongest observable result
// most drivers expose. It is not transport acceptance or a remote ACK.
dispatch_rumble(device, duration, right, left);
return true;
}
void dispatch_native_cues(const NativeGamepadCueDispatch& command) {
if (command.device == nullptr) return;
BackendSlot& slot = g_slots[command.slot];
const bool paired = command.companion != nullptr;
bool submitted[2]{};
uint32_t dispatch_ms = btstack_run_loop_get_time_ms();
for (uint8_t target = 0; target < (paired ? 2 : 1); ++target) {
state_lock_enter();
bool current = slot.active && slot.device == command.device &&
slot.companion == command.companion &&
slot.connection_generation == command.connection_generation;
for (uint8_t side = 0; side < 2; ++side) {
if (paired && side != target) continue;
const NativeGamepadCue& cue = g_native_cues[side];
if (command.token[side] != 0)
current &= cue.token == command.token[side] && cue.in_flight &&
cue.result != -1 && native_cue_current(cue);
}
state_lock_exit();
// No backend lock crosses a driver call. Recheck every real target:
// cancel/reselection or a stall during R dispatch must not send stale L.
dispatch_ms = btstack_run_loop_get_time_ms();
const uint32_t delay = dispatch_ms - command.prepared_ms;
const uint16_t duration = command.duration_ms > delay
? static_cast<uint16_t>(command.duration_ms - delay) : 0;
current &= delay < kRumblePollIntervalMs &&
(command.duration_ms == 0 || duration != 0);
if (!current) continue;
uni_hid_device_t* device = paired && target == 0
? command.companion : command.device;
const uint8_t right = command.magnitude[paired ? target : 0];
const uint8_t left = command.magnitude[paired ? target : 1];
if (!submit_native_rumble(
device, (right | left) == 0 ? 0 : duration, right, left)) continue;
__atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
if (paired) submitted[target] = true;
else submitted[0] = submitted[1] = true;
state_lock_enter();
if (slot.active && slot.device == command.device &&
slot.companion == command.companion) {
// Retain each actual submission even if USB canceled/reselected
// during its driver call, or the other half cannot be submitted.
NativeGamepadMotorOutput& output = slot.native_output;
if (paired) output.magnitude[target] = command.magnitude[target];
else {
output.magnitude[0] = command.magnitude[0];
output.magnitude[1] = command.magnitude[1];
}
output.owned = (output.magnitude[0] | output.magnitude[1]) != 0;
output.deadline_ms = output.owned
? command.prepared_ms + command.duration_ms : 0;
}
state_lock_exit();
}
state_lock_enter();
for (uint8_t side = 0; side < 2; ++side) {
NativeGamepadCue& cue = g_native_cues[side];
if (command.token[side] == 0 || cue.token != command.token[side]) continue;
cue.in_flight = false;
if (!native_cue_current(cue) ||
(cue.result == 0 && dispatch_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs)) {
cancel_native_cue_locked(cue);
} else if (submitted[side] && cue.result == 0) {
cue.result = 1; // Accepted source submission, never a native ACK.
cue.started_ms = command.prepared_ms;
cue.active = cue.sample_id != 0;
}
}
state_lock_exit();
}
#endif
// Core 1 only. The mailbox carries values, never a parser pointer supplied by
// Core 0. Revalidate after lifecycle/topology work and before touching the parser.
void process_wii_orientation(uint8_t slot_index) {
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
if (!slot.wii_orientation_pending) {
state_lock_exit();
return;
}
const WiiOrientationRequest request = slot.pending_wii_orientation;
slot.wii_orientation_pending = false;
slot.pending_wii_orientation = {};
if (!is_solo_wii_remote(slot) ||
slot.connection_generation != request.connection_generation ||
!controller_identity_equal(slot.identity, request.identity)) {
state_lock_exit();
return;
}
uni_hid_device_t* device = slot.device;
// Subtype publication can lag set_mode during extension discovery. Even a
// reselection must reach the parser to replace a deferred opposite choice.
// A new logical epoch retires profile macros, hotkey holds, capture and
// feedback without touching this connection's identity or saved profiles.
invalidate_slot(slot);
slot.gamepad = {};
slot.extra_buttons = 0;
#if SWITCH2_BRIDGE_FULL_INPUT
refresh_native_source_locked();
#endif
state_lock_exit();
// The setter can synchronously re-enter the platform ready callback, so
// release the lock first. Lifecycle and parser callbacks share this core.
if (device->report_parser.play_dual_rumble != nullptr) {
dispatch_rumble(device, 0, 0, 0);
}
uni_hid_parser_wii_set_mode(
device, request.vertical ? WII_MODE_VERTICAL : WII_MODE_HORIZONTAL);
apply_slot_lighting(slot_index, device);
}
uint8_t xbox_trigger_magnitude(const SwitchHapticsActuatorFrame& frame) {
uint16_t peak = 0;
for (uint8_t i = 0; i < frame.sample_count && i < 3; ++i) {
if (frame.samples[i].high_amplitude_q15 > peak)
peak = frame.samples[i].high_amplitude_q15;
}
// Impulse triggers are amplitude-only ERMs, not HD actuators. Keep their
// extra response at half scale, including after profile amplification.
if (peak > 32767) peak = 32767;
return static_cast<uint8_t>((static_cast<uint32_t>(peak) * 127u) / 32767u);
}
void dispatch_host_rumble(uni_hid_device_t* device, uint16_t duration_ms,
const ControllerRumbleOutput& rumble) {
const uint8_t weak = rumble.high_frequency_magnitude;
const uint8_t strong = rumble.low_frequency_magnitude;
if (device->vendor_id == 0x045e &&
device->report_parser.play_dual_rumble ==
uni_hid_parser_xboxone_play_dual_rumble) {
const bool hd = rumble.hd.actuators[0].sample_count != 0 ||
rumble.hd.actuators[1].sample_count != 0;
const uint8_t left = hd ? xbox_trigger_magnitude(rumble.hd.actuators[0])
: weak / 2u;
const uint8_t right = hd ? xbox_trigger_magnitude(rumble.hd.actuators[1])
: weak / 2u;
const bool stop = (weak | strong | left | right) == 0;
xboxone_play_quad_rumble(device, 0, stop ? 0 : duration_ms,
left, right, weak, strong);
return;
}
dispatch_rumble(device, (weak | strong) == 0 ? 0 : duration_ms, weak, strong);
}
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
void seed_native_host_rumble() {
HapticsExperimentDiagnostics native;
haptics_experiment_snapshot(&native);
if (native.mode != 1 || native.slot >= kSlotCount ||
native.run_id == g_seeded_native_run_id ||
(native.state != HapticsExperimentState::kPending &&
native.state != HapticsExperimentState::kRunning)) {
return;
}
g_seeded_native_run_id = native.run_id;
RumbleEnvelope retained{};
state_lock_enter();
BackendSlot& slot = g_slots[native.slot];
const bool valid = slot.active && slot.device != nullptr &&
slot.retained_host_rumble_valid &&
slot.connection_generation == native.connection_generation &&
slot.retained_host_rumble.connection_generation ==
native.connection_generation &&
slot.retained_host_rumble.slot == native.slot &&
slot.retained_host_rumble.duration_ms ==
kXInputHostRumbleDurationMs;
if (valid) {
retained = slot.retained_host_rumble;
// Arming cancels compatibility output even when its mailbox was
// already consumed. Keep that held state available for the next Stop.
slot.pending_rumble = retained;
slot.rumble_pending = true;
}
state_lock_exit();
if (valid) {
// Replay once on arm, not on a watchdog cadence. The original timestamp
// keeps a raced newer USB command authoritative in the host timeline.
haptics_experiment_submit_rumble(
native.slot, native.connection_generation, retained.received_us,
retained.rumble.low_frequency_magnitude,
retained.rumble.high_frequency_magnitude);
}
}
#endif
void process_rumble_timer(btstack_timer_source_t* timer) {
__atomic_add_fetch(&g_rumble_timer_ticks, 1, __ATOMIC_RELAXED);
uint32_t now_ms = btstack_run_loop_get_time_ms();
process_clear_pairings(now_ms);
process_pairing_snapshot_request();
process_joycon_gestures(now_ms);
now_ms = btstack_run_loop_get_time_ms();
const bool wake_identity_ready =
switch2_wake_ready_for_connections();
if (g_connection_policy_state ==
ConnectionPolicyState::Uninitialized &&
wake_identity_ready) {
recompute_connection_status();
}
if (update_pairing_window(now_ms) && wake_identity_ready) {
apply_connection_policy();
}
const bool xinput_host_mode =
host_rumble_duration_ms() == kXInputHostRumbleDurationMs;
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
if (xinput_host_mode) seed_native_host_rumble();
haptics_experiment_poll();
#endif
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
process_wii_orientation(slot_index);
drain_switch2_ingress(slot_index, now_ms);
RumbleEnvelope envelope{};
FeedbackEnvelope feedback{};
ProfileFeedbackEnvelope profile_feedback{};
uni_hid_device_t* device = nullptr;
uni_hid_device_t* profile_lighting_device = nullptr;
uint32_t profile_lighting_generation = 0;
uni_hid_device_t* companion = nullptr;
uint32_t dispatch_generation = 0;
bool profile_lighting_dispatch = false;
bool profile_lighting_restore = false;
bool profile_rumble_dispatch = false;
bool feedback_dispatch = false;
bool host_dispatch = false;
#ifdef SWITCH2_BRIDGE_WII_INPUT
WiiCueDispatch wii_cue_dispatch{};
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
NativeGamepadCueDispatch native_dispatch{};
#endif
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
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 = {};
}
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
if (haptics_experiment_owns(slot.device) &&
!haptics_experiment_gameplay_owns(slot.device)) {
// Fixture/startup/restoration exclusively own output. Preserve
// stateful XInput requests until compatibility restoration ends.
if (!xinput_host_mode) slot.rumble_pending = false;
state_lock_exit();
continue;
}
#endif
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;
const bool completed_feedback_had_led =
slot.profile_feedback.led_enabled;
const uint32_t completed_feedback_generation =
slot.profile_feedback.connection_generation;
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_feedback_was_active &&
!slot.profile_feedback.active &&
completed_feedback_had_led && slot.active &&
slot.device != nullptr &&
completed_feedback_generation ==
slot.connection_generation) {
profile_lighting_device = slot.device;
profile_lighting_generation =
completed_feedback_generation;
profile_lighting_restore = 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.pending_profile_feedback_count != 0) {
profile_feedback = slot.pending_profile_feedback[0];
if (slot.pending_profile_feedback_count == 2) {
slot.pending_profile_feedback[0] =
slot.pending_profile_feedback[1];
}
--slot.pending_profile_feedback_count;
slot.pending_profile_feedback[
slot.pending_profile_feedback_count] = {};
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_device = slot.device;
profile_lighting_generation =
slot.profile_feedback.connection_generation;
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_dispatch =
slot.active && slot.device != nullptr &&
feedback.connection_generation ==
slot.connection_generation &&
slot.device->report_parser.play_dual_rumble != nullptr;
slot.feedback_pending = false;
if (feedback_dispatch) {
device = slot.device;
slot.feedback_until_ms =
now_ms + feedback.duration_ms;
}
}
const bool local_feedback_active =
slot.profile_feedback.active ||
static_cast<int32_t>(
now_ms - slot.feedback_until_ms) < 0;
#ifdef SWITCH2_BRIDGE_WII_INPUT
const bool wii_cue_owns_rumble = prepare_wii_cue(
slot_index, now_ms, local_feedback_active,
profile_rumble_dispatch || feedback_dispatch, &wii_cue_dispatch);
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
const bool native_owns_rumble = prepare_native_cues(
slot_index, now_ms, local_feedback_active,
profile_rumble_dispatch || feedback_dispatch, &native_dispatch);
#endif
if (!profile_rumble_dispatch && !feedback_dispatch &&
!local_feedback_active && slot.rumble_pending
#ifdef SWITCH2_BRIDGE_WII_INPUT
&& !wii_cue_owns_rumble
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
&& !native_owns_rumble
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
&& !(xinput_host_mode &&
haptics_experiment_gameplay_owns(slot.device))
#endif
) {
envelope = slot.pending_rumble;
slot.rumble_pending = false;
host_dispatch =
envelope.slot == slot_index && slot.active &&
envelope.duration_ms == host_rumble_duration_ms() &&
slot.device != nullptr &&
envelope.connection_generation ==
slot.connection_generation;
if (host_dispatch) {
device = slot.device;
}
}
companion = slot.companion;
dispatch_generation = slot.connection_generation;
state_lock_exit();
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
if (host_dispatch && switch_native_output_owns(device))
host_dispatch = false; // The timestamped native queue already owns this command.
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
if (host_dispatch && haptics_experiment_gameplay_owns(device)) {
// Switch commands have already entered the timestamped timeline.
// Consume their finite fallback, never turn it into a PCM overlay
// or emit compatibility reports while gameplay owns the device.
host_dispatch = false;
}
#endif
uni_hid_device_t* lighting_targets[] = {
profile_lighting_device, companion};
for (uni_hid_device_t* target : lighting_targets) {
if (!lighting_target_is_current(
slot_index, profile_lighting_generation, target)) {
continue;
}
if (profile_lighting_restore) {
apply_slot_lighting(slot_index, target);
}
if (profile_lighting_dispatch) {
apply_profile_lighting(
profile_feedback.active_profile_number, target);
}
}
uni_hid_device_t* rumble_targets[] = {device, companion};
for (uni_hid_device_t* target : rumble_targets) {
if (!lighting_target_is_current(
slot_index, dispatch_generation, target) ||
target->report_parser.play_dual_rumble == nullptr) {
continue;
}
if (profile_rumble_dispatch) {
__atomic_add_fetch(
&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
dispatch_rumble(
target, kProfileFeedbackPhaseDurationMs,
kProfileFeedbackWeakMagnitude, kProfileFeedbackStrongMagnitude);
} else if (feedback_dispatch) {
__atomic_add_fetch(
&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
dispatch_rumble(
target, feedback.duration_ms,
feedback.weak_magnitude, feedback.strong_magnitude);
} else if (host_dispatch) {
__atomic_add_fetch(
&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
dispatch_host_rumble(target, envelope.duration_ms, envelope.rumble);
}
}
#ifdef SWITCH2_BRIDGE_WII_INPUT
dispatch_wii_cue(wii_cue_dispatch);
#endif
#if SWITCH2_BRIDGE_FULL_INPUT
dispatch_native_cues(native_dispatch);
#endif
}
update_status_led();
btstack_run_loop_set_timer(timer, kRumblePollIntervalMs);
btstack_run_loop_add_timer(timer);
}
void recompute_connection_status() {
const uint32_t now_ms = btstack_run_loop_get_time_ms();
update_pairing_window(now_ms);
g_connection_status = compute_connection_status();
g_status_led_tick = 0;
apply_connection_policy();
}
void forget_joycon_pair_hint(uni_hid_device_t* device) {
const int physical_index = physical_index_for_device(device);
if (physical_index < 0) return;
for (JoyConPairHint& hint : g_joycon_pair_hints) {
if (hint.mate == device) hint = {};
}
g_joycon_pair_hints[physical_index] = {};
}
// Replacing an explicit association does not revoke its former partner's solo
// choice. Only the new current association participates in disconnect reset.
void set_joycon_override(uni_hid_device_t* left, uni_hid_device_t* right,
JoyConGroupingOverride mode) {
uni_hid_device_t* devices[] = {left, right};
for (uni_hid_device_t* device : devices) {
for (JoyConConnectionOverride& current : g_joycon_overrides) {
if (current.mate == device) current.mate = nullptr;
}
}
g_joycon_overrides[physical_index_for_device(left)] = {mode, right};
g_joycon_overrides[physical_index_for_device(right)] = {mode, left};
}
// Caller holds the state lock; used for disconnect and physical index reuse.
void reset_joycon_connection(uni_hid_device_t* device) {
const int index = physical_index_for_device(device);
if (index < 0) return;
bool changed = g_joycon_overrides[index].mode !=
JoyConGroupingOverride::Default;
for (JoyConConnectionOverride& current : g_joycon_overrides) {
if (current.mate == device) {
current = {};
changed = true;
}
}
g_joycon_overrides[index] = {};
if (changed) g_joycon_reconcile_requested = true;
block_joycon_gesture(g_joycon_gestures[index].participants);
g_joycon_gestures[index] = {};
for (JoyConGesture& gesture : g_joycon_gestures) {
gesture.participants &= ~(1u << index);
}
forget_joycon_pair_hint(device);
}
bool joycon_gesture_mature(uni_hid_device_t* first,
uni_hid_device_t* second, bool joining,
uint32_t now_ms) {
const int first_index = physical_index_for_device(first);
const int second_index = physical_index_for_device(second);
if (first_index < 0 || second_index < 0 || first_index == second_index ||
joycon_side(first) == 0 || joycon_side(first) != -joycon_side(second)) {
return false;
}
const uint8_t participants = (1u << first_index) | (1u << second_index);
const JoyConGesture& a = g_joycon_gestures[first_index];
const JoyConGesture& b = g_joycon_gestures[second_index];
if (a.device != first || b.device != second ||
a.started_ms != b.started_ms) return false;
const int indices[] = {first_index, second_index};
for (int index : indices) {
const JoyConGesture& gesture = g_joycon_gestures[index];
if (!joycon_gesture_live(index) ||
gesture.participants != participants ||
gesture.joining != joining || !gesture.held ||
now_ms - gesture.last_report_ms > kJoyConGestureFreshMs ||
static_cast<int32_t>(gesture.last_report_ms - gesture.started_ms) <
static_cast<int32_t>(kJoyConGestureHoldMs)) return false;
}
const int a_slot = slot_for_device(first);
const int b_slot = slot_for_device(second);
return joining
? a_slot != b_slot && g_slots[a_slot].companion == nullptr &&
g_slots[b_slot].companion == nullptr
: a_slot == b_slot && g_slots[a_slot].companion != nullptr;
}
// Caller holds the state lock. Prefer the last live pair's exact members,
// otherwise preserve the existing first-ready / lowest-slot admission order.
int joycon_partner_slot(uni_hid_device_t* device, int slot_index) {
const int side = joycon_side(device);
if (side == 0 || !joycon_default_pairing_allowed(device)) return -1;
const auto& hint = g_joycon_pair_hints[physical_index_for_device(device)];
int first = -1;
for (uint8_t index = 0; index < kSlotCount; ++index) {
const BackendSlot& candidate = g_slots[index];
if (index == slot_index || !candidate.active ||
candidate.companion != nullptr ||
!joycon_default_pairing_allowed(candidate.device) ||
joycon_side(candidate.device) != -side) continue;
if (candidate.device == hint.mate) return index;
const auto& candidate_hint =
g_joycon_pair_hints[physical_index_for_device(candidate.device)];
if (candidate_hint.mate != nullptr && candidate_hint.mate != device) continue;
if (first < 0) first = index;
}
return first;
}
void stop_joycon_output(uni_hid_device_t* device) {
if (device->report_parser.play_dual_rumble != nullptr) {
dispatch_rumble(device, 0, 0, 0);
}
}
// BTstack only; shared by ready admission, saved defaults and explicit gestures.
// Pair enrollment is atomic and idempotent, and always precedes topology
// changes with no cross-core input lock held during storage I/O.
bool merge_joycon_slots(int owner_index, int joining_index,
uni_hid_device_t* joining_device,
bool gesture = false) {
state_lock_enter();
BackendSlot& owner = g_slots[owner_index];
BackendSlot& joining = g_slots[joining_index];
uni_hid_device_t* const owner_device = owner.device;
const uint32_t owner_generation = owner.connection_generation;
const uint32_t joining_generation = joining.connection_generation;
const bool joining_active = joining.active;
const int side = joycon_side(joining_device);
const bool admission = gesture
? joining.active && joycon_gesture_mature(
owner_device, joining_device, true, btstack_run_loop_get_time_ms())
: joycon_default_pairing_allowed(owner_device) &&
joycon_default_pairing_allowed(joining_device);
const bool eligible = admission &&
owner.active && owner.companion == nullptr &&
joining.companion == nullptr && side != 0 &&
joycon_side(owner_device) == -side &&
reserve_device_slot(joining_device) == joining_index;
const ControllerIdentity owner_identity = identity_for_device(owner_device);
const ControllerIdentity joining_identity = identity_for_device(joining_device);
state_lock_exit();
ControllerIdentity pair_identity{};
if (!eligible ||
!controller_identity_make_joycon_pair(
side < 0 ? joining_identity : owner_identity,
side < 0 ? owner_identity : joining_identity, &pair_identity) ||
!profile_service_observe_joycon_pair_on_storage_core(pair_identity)) {
return false;
}
state_lock_enter();
const bool still_admitted = gesture
? joycon_gesture_mature(
owner_device, joining_device, true, btstack_run_loop_get_time_ms())
: joycon_default_pairing_allowed(owner_device) &&
joycon_default_pairing_allowed(joining_device);
if (!still_admitted ||
!owner.active || owner.device != owner_device ||
owner.companion != nullptr ||
owner.connection_generation != owner_generation ||
joining.active != joining_active || joining.companion != nullptr ||
joining.connection_generation != joining_generation ||
reserve_device_slot(joining_device) != joining_index) {
state_lock_exit();
return false;
}
invalidate_slot(owner);
invalidate_slot(joining);
// Clear both parser epochs and local motor feedback before publishing
// either the new pair or its neutral retired output.
stop_joycon_output(owner_device);
stop_joycon_output(joining_device);
if (side < 0) {
owner.companion = owner.device;
owner.companion_gamepad = owner.gamepad;
owner.companion_extra_buttons = owner.extra_buttons;
owner.device = joining_device;
owner.gamepad = joining.gamepad;
owner.extra_buttons = joining.extra_buttons;
} else {
owner.companion = joining_device;
owner.companion_gamepad = joining.gamepad;
owner.companion_extra_buttons = joining.extra_buttons;
}
owner.identity = pair_identity;
refresh_topology_input(owner);
joining.identity = controller_identity_global();
joining.device = nullptr;
joining.gamepad = {};
joining.extra_buttons = 0;
joining.active = false;
forget_joycon_pair_hint(owner.device);
forget_joycon_pair_hint(owner.companion);
if (gesture) {
set_joycon_override(owner.device, owner.companion,
JoyConGroupingOverride::Paired);
queue_local_feedback(owner, kJoyConGestureFeedbackMs,
kProfileFeedbackWeakMagnitude,
kProfileFeedbackStrongMagnitude);
}
g_joycon_pair_hints[physical_index_for_device(owner.device)] =
{owner.companion, static_cast<uint8_t>(owner_index)};
g_joycon_pair_hints[physical_index_for_device(owner.companion)] =
{owner.device, static_cast<uint8_t>(owner_index)};
#if SWITCH2_BRIDGE_FULL_INPUT
refresh_native_source_locked();
#endif
state_lock_exit();
apply_slot_lighting(static_cast<uint8_t>(owner_index), owner.device);
apply_slot_lighting(static_cast<uint8_t>(owner_index), owner.companion);
return true;
}
bool split_joycon_slot(uint8_t owner_index, bool gesture = false) {
state_lock_enter();
BackendSlot& owner = g_slots[owner_index];
const int physical_index = physical_index_for_device(owner.device);
if (!owner.active || owner.companion == nullptr || physical_index < 0 ||
(!gesture && g_joycon_overrides[physical_index].mode ==
JoyConGroupingOverride::Paired) ||
(gesture && !joycon_gesture_mature(
owner.device, owner.companion, false,
btstack_run_loop_get_time_ms()))) {
state_lock_exit();
return false;
}
// Keep the pair's left member at its existing player index. Prefer the
// right member's physical index, falling back to the lowest free output.
int right_index = physical_index_for_device(owner.companion);
if (right_index < 0 || g_slots[right_index].device != nullptr) {
right_index = -1;
for (uint8_t index = 0; index < kSlotCount; ++index) {
if (g_slots[index].device == nullptr) {
right_index = index;
break;
}
}
}
if (right_index < 0) {
state_lock_exit();
return false;
}
BackendSlot& right = g_slots[right_index];
ControllerIdentity left_identity{};
ControllerIdentity right_identity{};
// The enrolled owner is authoritative even if a later identity-resolution
// event has temporarily cleared a member's transport mapping.
if (!controller_identity_joycon_pair_members(
owner.identity, &left_identity, &right_identity)) {
state_lock_exit();
return false;
}
invalidate_slot(owner);
invalidate_slot(right);
stop_joycon_output(owner.device);
stop_joycon_output(owner.companion);
right.device = owner.companion;
right.identity = right_identity;
right.gamepad = owner.companion_gamepad;
right.extra_buttons = owner.companion_extra_buttons;
right.active = true;
owner.identity = left_identity;
owner.companion = nullptr;
owner.companion_gamepad = {};
owner.companion_extra_buttons = 0;
refresh_topology_input(owner);
refresh_topology_input(right);
if (gesture) {
set_joycon_override(owner.device, right.device,
JoyConGroupingOverride::Individual);
queue_local_feedback(owner, kJoyConGestureFeedbackMs,
kProfileFeedbackWeakMagnitude,
kProfileFeedbackStrongMagnitude);
queue_local_feedback(right, kJoyConGestureFeedbackMs,
kProfileFeedbackWeakMagnitude,
kProfileFeedbackStrongMagnitude);
}
#if SWITCH2_BRIDGE_FULL_INPUT
refresh_native_source_locked();
#endif
state_lock_exit();
apply_slot_lighting(owner_index, owner.device);
apply_slot_lighting(static_cast<uint8_t>(right_index), right.device);
return true;
}
void process_joycon_gestures(uint32_t now_ms) {
for (uint8_t index = 0; index < kSlotCount; ++index) {
now_ms = btstack_run_loop_get_time_ms();
state_lock_enter();
refresh_joycon_gestures(now_ms);
const JoyConGesture& gesture = g_joycon_gestures[index];
if (gesture.blocked || joycon_side(gesture.device) >= 0) {
state_lock_exit();
continue;
}
uni_hid_device_t* right = nullptr;
for (uint8_t mate = 0; mate < kSlotCount; ++mate) {
if (mate != index && (gesture.participants & (1u << mate))) {
right = g_joycon_gestures[mate].device;
}
}
const bool joining = gesture.joining;
const int owner_slot = slot_for_device(gesture.device);
const int right_slot = slot_for_device(right);
const bool mature = joycon_gesture_mature(
gesture.device, right, joining, now_ms);
if (mature) block_joycon_gesture(gesture.participants);
state_lock_exit();
if (!mature) continue;
// Latch success AND failure before any enrollment I/O. A failed seed
// must not retry at the timer cadence or undo either participant.
// Player-slot ownership is independent of physical handedness.
// Keep the lower occupied slot, even when the left half owns the higher one.
const bool keep_left_slot = owner_slot < right_slot;
const bool changed = joining
? merge_joycon_slots(
keep_left_slot ? owner_slot : right_slot,
keep_left_slot ? right_slot : owner_slot,
keep_left_slot ? right : gesture.device, true)
: split_joycon_slot(static_cast<uint8_t>(owner_slot), true);
if (changed) recompute_connection_status();
}
}
void apply_joycon_configuration(const ConfigurationServiceSnapshot& configuration) {
if (configuration.state != ConfigurationServiceState::kReady) return;
const JoyConMode requested = configuration.configuration.joycon_mode;
if (requested != g_joycon_mode) {
state_lock_enter();
for (uint8_t index = 0; index < kSlotCount; ++index) {
g_joycon_overrides[index] = {};
block_joycon_gesture(g_joycon_gestures[index].participants);
}
g_joycon_mode = requested;
g_joycon_reconcile_requested = true;
state_lock_exit();
}
if (!g_joycon_reconcile_requested) return;
g_joycon_reconcile_requested = false;
if (g_joycon_mode == JoyConMode::kIndividual) {
for (uint8_t index = 0; index < kSlotCount; ++index) {
split_joycon_slot(index);
}
} else {
uint8_t attempted = 0;
for (uint8_t index = 0; index < kSlotCount; ++index) {
state_lock_enter();
const BackendSlot& slot = g_slots[index];
int partner = slot.active && slot.companion == nullptr &&
!(attempted & (1u << index))
? joycon_partner_slot(slot.device, index) : -1;
if (partner < 0 || (attempted & (1u << partner))) {
state_lock_exit();
continue;
}
int owner = index;
int joining = partner;
const auto& hint =
g_joycon_pair_hints[physical_index_for_device(slot.device)];
if (hint.mate == g_slots[partner].device && hint.owner_slot == partner) {
owner = partner;
joining = index;
}
uni_hid_device_t* joining_device = g_slots[joining].device;
attempted |= (1u << index) | (1u << partner);
state_lock_exit();
// Failed seeds/invalid identities leave both live solos intact.
// Retry only on a mode change or fresh identity/ready event, never
// at the 50 ms poll cadence or for unrelated configuration edits.
merge_joycon_slots(owner, joining, joining_device);
}
}
recompute_connection_status();
}
void platform_init(int argc, const char** argv) {
(void)argc;
(void)argv;
}
void platform_on_init_complete() {
if (SWITCH_PICO_ENABLE_CLASSIC) {
gap_set_link_supervision_timeout(kClassicLinkSupervisionTimeout);
gap_ssp_set_auto_accept(false);
g_pairing_event_callback.callback = handle_btstack_event;
hci_add_event_handler(&g_pairing_event_callback);
}
gap_set_bondable_mode(false);
if (SWITCH_PICO_ENABLE_BLE) {
// Bluepad32 does not initialize SM in Classic-only mode.
sm_set_accepted_stk_generation_methods(0);
g_identity_event_callback.callback = handle_btstack_event;
sm_add_event_handler(&g_identity_event_callback);
}
switch2_wake_initialize();
refresh_pairing_snapshot();
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs);
btstack_run_loop_add_timer(&g_rumble_timer);
btstack_run_loop_set_timer_handler(
&g_configuration_timer, process_configuration_timer);
btstack_run_loop_set_timer(
&g_configuration_timer, kConfigurationPollIntervalMs);
btstack_run_loop_add_timer(&g_configuration_timer);
__atomic_store_n(&g_initialization_stage, 6, __ATOMIC_RELEASE);
ConfigurationServiceSnapshot configuration{};
configuration_service_snapshot(&configuration);
apply_joycon_configuration(configuration);
if (switch2_wake_ready_for_connections()) {
recompute_connection_status();
}
}
bool device_transport_enabled(const uni_hid_device_t* device) {
if (device == nullptr) {
return false;
}
if (SWITCH_PICO_ENABLE_BLE && SWITCH_PICO_ENABLE_CLASSIC) {
return true;
}
// GAP describes the actual live link. The protocol hint is also available
// before an outgoing connection has an HCI handle.
switch (gap_get_connection_type(device->conn.handle)) {
case GAP_CONNECTION_ACL:
return SWITCH_PICO_ENABLE_CLASSIC != 0;
case GAP_CONNECTION_LE:
return SWITCH_PICO_ENABLE_BLE != 0;
default:
break;
}
switch (device->conn.protocol) {
case UNI_BT_CONN_PROTOCOL_BR_EDR:
return SWITCH_PICO_ENABLE_CLASSIC != 0;
case UNI_BT_CONN_PROTOCOL_BLE:
return SWITCH_PICO_ENABLE_BLE != 0;
default:
// Preserve mixed-mode admission; single-transport builds cannot
// safely admit a connection whose transport is still unknown.
return SWITCH_PICO_ENABLE_BLE && SWITCH_PICO_ENABLE_CLASSIC;
}
}
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
uint16_t cod, uint8_t rssi) {
(void)name;
(void)cod;
(void)rssi;
if (!has_free_slot()) {
return UNI_ERROR_IGNORE_DEVICE;
}
const uni_hid_device_t* candidate =
(g_connection_policy_state == ConnectionPolicyState::Passive ||
!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_ENABLE_CLASSIC)
? uni_hid_device_get_instance_for_address(addr)
: nullptr;
if (candidate != nullptr && !device_transport_enabled(candidate)) {
return UNI_ERROR_IGNORE_DEVICE;
}
// First discovery can precede device creation, so the transport-specific
// Bluepad32 discovery handlers must enforce the mode before this callback.
if (g_connection_policy_state == ConnectionPolicyState::Open) {
return UNI_ERROR_SUCCESS;
}
if (!SWITCH_PICO_ENABLE_BLE ||
g_connection_policy_state != ConnectionPolicyState::Passive) {
return UNI_ERROR_IGNORE_DEVICE;
}
const int side = joycon_side(candidate);
uint8_t address_type = BD_ADDR_TYPE_UNKNOWN;
return side != 0 && waiting_for_joycon_mate(side) &&
uni_hid_parser_switch2_identity_address_type(
candidate, &address_type) &&
uni_switch2_pairing_known(address_type, addr)
? UNI_ERROR_SUCCESS
: UNI_ERROR_IGNORE_DEVICE;
}
void platform_on_device_connected(uni_hid_device_t* device) {
if (device == nullptr) {
return;
}
if (!device_transport_enabled(device) ||
(g_connection_policy_state != ConnectionPolicyState::Open &&
g_connection_policy_state != ConnectionPolicyState::Passive)) {
uni_hid_device_disconnect(device);
return;
}
#if SWITCH2_BRIDGE_FULL_INPUT
// Classification completes after connection. Reserve transport capacity,
// not a player/color slot, until a supported source reaches ready.
const int pending_index = physical_index_for_device(device);
if (pending_index < 0) {
uni_hid_device_disconnect(device);
return;
}
state_lock_enter();
g_retired_devices[pending_index] = nullptr;
g_switch2_interval_requests[pending_index] = {};
if (slot_for_device(device) < 0) {
g_native_reports[pending_index] = {};
reset_joycon_connection(device);
}
if (slot_for_device(device) < 0) g_native_pending_devices[pending_index] = device;
state_lock_exit();
recompute_connection_status();
#else
const ControllerIdentity connection_identity = identity_for_device(device);
state_lock_enter();
const int physical_index = physical_index_for_device(device);
if (physical_index >= 0) {
g_retired_devices[physical_index] = nullptr;
g_switch2_interval_requests[physical_index] = {};
if (slot_for_device(device) < 0) reset_joycon_connection(device);
}
const int slot_index = reserve_device_slot(device);
if (slot_index >= 0) {
BackendSlot& slot = g_slots[slot_index];
if (slot.device == nullptr) {
slot.device = device;
slot.identity = connection_identity;
slot.rumble_pending = false;
reset_slot_hotkeys(slot);
}
}
state_lock_exit();
if (slot_index >= 0) {
recompute_connection_status();
} else {
uni_hid_device_disconnect(device);
}
#endif
}
void platform_on_device_disconnected(uni_hid_device_t* device) {
#if SWITCH2_BRIDGE_FULL_INPUT
const int pending_index = physical_index_for_device(device);
if (pending_index >= 0) {
state_lock_enter();
if (g_native_pending_devices[pending_index] == device)
g_native_pending_devices[pending_index] = nullptr;
g_retired_devices[pending_index] = device;
state_lock_exit();
}
#endif
const int slot_index = slot_for_device(device);
if (slot_index < 0) {
#if SWITCH2_BRIDGE_FULL_INPUT
recompute_connection_status();
#endif
return;
}
#if SWITCH2_BRIDGE_FULL_INPUT
// DS4/PSMove and other finite-rumble drivers keep timers in parser_data.
// Retire those timers before Bluepad32 reuses that memory. Call the real
// driver directly: a feedback scheduler must not defer this local teardown.
if (device->report_parser.play_dual_rumble != nullptr)
device->report_parser.play_dual_rumble(device, 0, 0, 0, 0);
#endif
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
switch_native_output_detach(device);
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
haptics_experiment_detach(device);
#endif
uni_hid_device_t* survivor = nullptr;
ControllerIdentity survivor_identity{};
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
g_retired_devices[physical_index_for_device(device)] = device;
reset_joycon_connection(device);
if (slot.companion != nullptr) {
invalidate_slot(slot);
if (slot.device == device) {
slot.device = slot.companion;
slot.gamepad = slot.companion_gamepad;
slot.extra_buttons = slot.companion_extra_buttons;
}
slot.companion = nullptr;
slot.companion_gamepad = {};
slot.companion_extra_buttons = 0;
survivor = slot.device;
slot.identity = identity_for_device(survivor);
survivor_identity = slot.identity;
refresh_topology_input(slot);
} else {
release_slot(slot);
}
#if SWITCH2_BRIDGE_FULL_INPUT
refresh_native_source_locked();
#endif
state_lock_exit();
clear_ble_identity_for_device(device);
if (survivor != nullptr) {
if (survivor->report_parser.play_dual_rumble != nullptr) {
dispatch_rumble(survivor, 0, 0, 0);
}
apply_slot_lighting(static_cast<uint8_t>(slot_index), survivor);
if (survivor_identity.stable) {
profile_service_observe_identity_on_storage_core(survivor_identity);
}
}
// Losing a half frees transport capacity, not another logical player.
if (g_connection_policy_state != ConnectionPolicyState::FailedClosed) {
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
}
recompute_connection_status();
}
uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
if (!device_transport_enabled(device) ||
!uni_hid_device_is_gamepad(device)) {
return UNI_ERROR_INVALID_CONTROLLER;
}
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
return UNI_ERROR_NO_SLOTS;
}
bool became_active = false;
bool paired = false;
uint32_t lighting_generation = 0;
uni_hid_device_t* owner = device;
uni_hid_device_t* companion = nullptr;
ControllerIdentity connection_identity = identity_for_device(device);
state_lock_enter();
#if SWITCH2_BRIDGE_FULL_INPUT
if (!native_device_allowed(device)) {
state_lock_exit();
return UNI_ERROR_INVALID_CONTROLLER;
}
#endif
int slot_index = reserve_device_slot(device);
if (slot_index < 0) {
state_lock_exit();
return UNI_ERROR_NO_SLOTS;
}
#if SWITCH2_BRIDGE_FULL_INPUT
const int pending_index = physical_index_for_device(device);
if (g_native_pending_devices[pending_index] == device)
g_native_pending_devices[pending_index] = nullptr;
#endif
BackendSlot& pending = g_slots[slot_index];
if (!pending.active) {
const int partner_index = joycon_partner_slot(device, slot_index);
if (partner_index >= 0) {
state_lock_exit();
if (!merge_joycon_slots(partner_index, slot_index, device)) {
return UNI_ERROR_INIT_FAILED;
}
state_lock_enter();
slot_index = partner_index;
paired = true;
} else {
pending.identity = connection_identity;
pending.device = device;
pending.state = make_neutral_state();
pending.active = true;
pending.rumble_pending = false;
reset_slot_hotkeys(pending);
++pending.state_generation;
}
became_active = true;
}
#if SWITCH2_BRIDGE_FULL_INPUT
refresh_native_source_locked();
#endif
const BackendSlot& current = g_slots[slot_index];
owner = current.device;
companion = current.companion;
lighting_generation = current.connection_generation;
connection_identity = current.identity;
state_lock_exit();
if (became_active) {
apply_radio_connection_policy();
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
if (!paired) {
switch_native_output_attach(static_cast<uint8_t>(slot_index),
lighting_generation, device, connection_identity);
}
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
if (!paired && !uni_hid_parser_switch2_is_ble_device(device)) {
haptics_experiment_attach(
static_cast<uint8_t>(slot_index), lighting_generation, device);
}
#ifdef SWITCH_PICO_HD_RUMBLE
if (connection_identity.vendor_id == 0x054c &&
(connection_identity.product_id == 0x0ce6 ||
connection_identity.product_id == 0x0df2)) {
haptics_experiment_request(2, static_cast<uint8_t>(slot_index));
}
#endif
#endif
if (!paired && lighting_target_is_current(
static_cast<uint8_t>(slot_index), lighting_generation, owner)) {
apply_slot_lighting(static_cast<uint8_t>(slot_index), owner);
if (companion != nullptr) {
apply_slot_lighting(static_cast<uint8_t>(slot_index), companion);
}
}
if (!paired && connection_identity.stable) {
profile_service_observe_identity_on_storage_core(
connection_identity);
}
if (!paired && joycon_side(device) != 0 &&
g_joycon_mode == JoyConMode::kPaired) {
g_joycon_reconcile_requested = true;
}
}
recompute_connection_status();
return UNI_ERROR_SUCCESS;
}
void platform_on_controller_data(uni_hid_device_t* device,
uni_controller_t* controller) {
const int slot_index = slot_for_device(device);
if (slot_index < 0 || controller == nullptr ||
controller->klass != UNI_CONTROLLER_CLASS_GAMEPAD) {
return;
}
__atomic_add_fetch(&g_controller_reports, 1, __ATOMIC_RELAXED);
// Parser state belongs to this serialized Bluepad32 callback, not to
// g_state_lock. Read it before masking IRQs so USB completions can run
// while snapshot helpers execute (including cold XIP fetches).
#if SWITCH2_BRIDGE_FULL_INPUT
const int physical_index = physical_index_for_device(device);
if (physical_index < 0) return;
uni_native_motion_snapshot_t sensor{};
uni_hid_parser_native_motion_snapshot(device, &sensor);
#endif
const uint8_t extras = uni_hid_parser_switch2_extra_buttons(device);
const bool is_wii = device->controller_type == CONTROLLER_TYPE_WiiController;
int32_t acceleration[3];
uint32_t sequence = 0;
const bool have_acceleration = is_wii &&
uni_hid_parser_wii_accel_snapshot(device, acceleration, &sequence);
int32_t nunchuk_acceleration[3];
uint32_t nunchuk_sequence = 0;
const bool have_nunchuk_acceleration = is_wii &&
uni_hid_parser_wii_nunchuk_accel_snapshot(
device, nunchuk_acceleration, &nunchuk_sequence);
#ifdef SWITCH2_BRIDGE_WII_INPUT
int32_t gyro[3];
uint32_t gyro_sequence = 0;
const bool have_gyro = is_wii &&
uni_hid_parser_wii_gyro_snapshot(device, gyro, &gyro_sequence);
#endif
const uint32_t report_ms = btstack_run_loop_get_time_ms();
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
if (!slot.active) {
state_lock_exit();
return;
}
#if SWITCH2_BRIDGE_FULL_INPUT
NativeGamepadReportIngress& ingress = g_native_reports[physical_index];
if (ingress.device != device) ingress = {device, 0, 0, 0};
if (sensor.report_tracked &&
(!sensor.report_valid || sensor.report_sequence == ingress.report_sequence)) {
state_lock_exit();
return;
}
ingress.report_sequence = sensor.report_sequence;
NativeGamepadIngress& motion = slot.native_motion;
motion.has_report = true;
motion.received_us = time_us_32();
// Read only the reporting physical device. The right half is the existing
// pair's motion owner; left controls must not refresh or invalidate its IMU.
if (slot.companion == nullptr || slot.companion == device) {
if (!sensor.accel_valid) motion.accel_valid = false;
else if (sensor.accel_sequence != ingress.accel_sequence) {
motion.accel_valid = true;
motion.accel_sequence = sensor.accel_sequence;
motion.accel_received_us = motion.received_us;
memcpy(motion.accel_q13, sensor.accel_q13, sizeof(motion.accel_q13));
}
if (!sensor.gyro_valid) motion.gyro_valid = false;
else if (sensor.gyro_sequence != ingress.gyro_sequence) {
motion.gyro_valid = true;
motion.gyro_sequence = sensor.gyro_sequence;
motion.gyro_received_us = motion.received_us;
memcpy(motion.gyro_q10, sensor.gyro_q10, sizeof(motion.gyro_q10));
}
}
if (sensor.accel_valid) ingress.accel_sequence = sensor.accel_sequence;
if (sensor.gyro_valid) ingress.gyro_sequence = sensor.gyro_sequence;
#endif
#ifdef SWITCH_PICO_WII_IR
uni_wii_ir_snapshot_t infrared{};
const bool have_infrared =
#ifdef SWITCH2_BRIDGE_WII_INPUT
is_selected_wii(slot) && slot.device == device &&
#endif
uni_hid_parser_wii_ir_snapshot(device, &infrared);
#ifdef SWITCH_PICO_WII_IR_GYRO
observe_wii_aim_chord(
slot, device, controller->gamepad,
have_infrared ? &infrared : nullptr, time_us_32());
#endif
#endif
if (slot.companion == device) {
slot.companion_gamepad = controller->gamepad;
slot.companion_extra_buttons = extras;
} else {
slot.gamepad = controller->gamepad;
slot.extra_buttons = extras;
}
observe_joycon_gesture(
device, controller->gamepad, report_ms);
if (is_wii) {
#ifdef SWITCH2_BRIDGE_WII_INPUT
WiiMotionIngress& motion = slot.wii_motion;
motion.received_us = time_us_32();
#endif
if (!have_acceleration) {
slot.accelerometer = {};
#ifdef SWITCH2_BRIDGE_WII_INPUT
motion.accel_valid = false;
#endif
} else if (
#ifdef SWITCH2_BRIDGE_WII_INPUT
sequence != motion.accel_sequence
#else
!slot.accelerometer.valid || sequence != slot.accelerometer.sequence
#endif
) {
slot.accelerometer = {
convert_accel(-static_cast<int64_t>(acceleration[2])),
convert_accel(-static_cast<int64_t>(acceleration[0])),
convert_accel(acceleration[1]), sequence,
report_ms, true};
}
#ifdef SWITCH2_BRIDGE_WII_INPUT
if (slot.accelerometer.valid && sequence != motion.accel_sequence) {
motion.accel_sequence = sequence;
motion.accel_received_us = motion.received_us;
motion.accel_valid = true;
memcpy(motion.accel_q13, acceleration, sizeof(motion.accel_q13));
}
if (!have_gyro) {
motion.gyro_valid = false;
} else if (gyro_sequence != motion.gyro_sequence) {
motion.gyro_sequence = gyro_sequence;
motion.gyro_received_us = motion.received_us;
motion.gyro_valid = true;
memcpy(motion.gyro_q10, gyro, sizeof(motion.gyro_q10));
}
#endif
if (!have_nunchuk_acceleration) {
slot.nunchuk_accelerometer = {};
} else if (
#ifdef SWITCH2_BRIDGE_WII_INPUT
nunchuk_sequence != motion.nunchuk_sequence
#else
!slot.nunchuk_accelerometer.valid ||
nunchuk_sequence != slot.nunchuk_accelerometer.sequence
#endif
) {
slot.nunchuk_accelerometer = {
convert_accel(-static_cast<int64_t>(nunchuk_acceleration[2])),
convert_accel(-static_cast<int64_t>(nunchuk_acceleration[0])),
convert_accel(nunchuk_acceleration[1]), nunchuk_sequence,
report_ms, true};
#ifdef SWITCH2_BRIDGE_WII_INPUT
motion.nunchuk_sequence = nunchuk_sequence;
#endif
}
}
#ifdef SWITCH_PICO_WII_IR
if (have_infrared) {
const bool nunchuk_c =
(device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK ||
device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL) &&
(controller->gamepad.buttons & BUTTON_X) != 0;
float gravity_roll = 0.0f;
bool gravity_valid = false;
#if SWITCH2_BRIDGE_WII_INPUT
const WiiMotionIngress& motion = slot.wii_motion;
if (motion.accel_valid && time_us_32() - motion.accel_received_us < 150000) {
const float x = static_cast<float>(motion.accel_q13[0]) / 8192.0f;
const float y = static_cast<float>(motion.accel_q13[1]) / 8192.0f;
const float z = static_cast<float>(motion.accel_q13[2]) / 8192.0f;
const float magnitude = x * x + y * y + z * z;
gravity_valid = magnitude >= 0.85f * 0.85f && magnitude <= 1.15f * 1.15f &&
x * x + y * y >= 0.25f;
if (gravity_valid) gravity_roll = atan2f(-x, y);
}
#endif
wii_ir_pointer_observe(static_cast<uint8_t>(slot_index),
slot.connection_generation, infrared.sequence,
infrared.buttons, infrared.x, infrared.y,
infrared.valid_mask, nunchuk_c, gravity_roll, gravity_valid);
}
#endif
const uni_gamepad_t gamepad = logical_gamepad(slot);
uni_hid_device_t* owner = slot.device;
const bool fresh_motion =
slot.companion == nullptr || slot.companion == device;
const uint8_t merged_extras =
slot.extra_buttons | slot.companion_extra_buttons;
state_lock_exit();
const uint16_t pre_hotkey_button_mask = logical_button_mask(gamepad);
if (wake_chord_rising_edge(
static_cast<uint8_t>(slot_index), owner, pre_hotkey_button_mask)) {
switch2_wake_request();
}
const HotkeyDecision hotkeys = update_controller_hotkeys(
static_cast<uint8_t>(slot_index), owner);
ControllerState state = map_gamepad(
gamepad, hotkeys.motion_enabled && fresh_motion, pre_hotkey_button_mask);
state.extra_buttons = merged_extras;
publish_device_state(
static_cast<uint8_t>(slot_index), owner, pre_hotkey_button_mask, state);
}
const uni_property_t* platform_get_property(uni_property_idx_t index) {
(void)index;
return nullptr;
}
void platform_on_oob_event(uni_platform_oob_event_t event, void* data) {
(void)event;
(void)data;
}
uni_platform* get_platform() {
static uni_platform platform = {
"Switch Pico",
platform_init,
platform_on_init_complete,
platform_on_device_discovered,
platform_on_device_connected,
platform_on_device_disconnected,
platform_on_device_ready,
nullptr,
platform_on_controller_data,
platform_get_property,
platform_on_oob_event,
nullptr,
nullptr,
};
return &platform;
}
#if !SWITCH2_PROBE_HUB
[[noreturn]] void halt_wireless_backend() {
publish_all_neutral();
while (true) {
tight_loop_contents();
}
}
#endif
// Called on the Bluetooth/storage owner after flash-safe registration.
bool initialize_wireless_backend() {
__atomic_store_n(&g_initialization_stage, 2, __ATOMIC_RELEASE);
configuration_service_initialize_on_storage_core();
profile_service_initialize_on_storage_core();
ConfigurationServiceSnapshot configuration{};
configuration_service_snapshot(&configuration);
if (configuration.state == ConfigurationServiceState::kReady) {
// Load before uni_init can deliver even the first ready callback.
g_joycon_mode = configuration.configuration.joycon_mode;
}
__atomic_store_n(&g_initialization_stage, 3, __ATOMIC_RELEASE);
if (cyw43_arch_init() != 0) {
return false;
}
__atomic_store_n(&g_initialization_stage, 4, __ATOMIC_RELEASE);
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true);
g_status_led_on = true;
uni_platform_set_custom(get_platform());
if (uni_init(0, nullptr) != 0) {
return false;
}
__atomic_store_n(&g_initialization_stage, 5, __ATOMIC_RELEASE);
return true;
}
#if !SWITCH2_PROBE_HUB
[[noreturn]] void core1_main() {
if (!flash_safe_execute_core_init() || !initialize_wireless_backend()) {
halt_wireless_backend();
}
btstack_run_loop_execute();
while (true) {
tight_loop_contents();
}
}
#endif
} // namespace
extern "C" bool switch_pico_switch2_pairing_allowed(void) {
return SWITCH_PICO_ENABLE_BLE && g_initialized &&
pairing_window_active_at(btstack_run_loop_get_time_ms());
}
extern "C" void __real_sm_request_pairing(hci_con_handle_t handle);
extern "C" void __wrap_sm_request_pairing(hci_con_handle_t handle) {
uni_hid_device_t* device =
uni_hid_device_get_instance_for_connection_handle(handle);
if (!SWITCH_PICO_ENABLE_BLE) {
if (device != nullptr) {
uni_hid_device_disconnect(device);
}
return;
}
if (uni_hid_parser_switch2_is_ble_device(device)) {
// GATT's implicit authentication retry must not enter standard SMP for
// this proprietary protocol. Retain storage until HCI teardown.
uni_hid_device_disconnect(device);
return;
}
__real_sm_request_pairing(handle);
}
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
extern "C" bool uni_platform_on_l2cap_can_send_now(
uni_hid_device_t* device, uint16_t cid) {
bool block_generic = false;
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
block_generic = haptics_experiment_blocks_generic(device);
#endif
const bool consumed = native_output_scheduler_on_can_send_now(device, cid);
return consumed || block_generic;
}
#endif
bool bluepad32_input_backend_capture_start(
uint8_t slot, uint32_t connection_generation, const CaptureOptions& options) {
if (!g_initialized || slot >= kSlotCount) return false;
state_lock_enter();
const BackendSlot& current = g_slots[slot];
const bool accepted = current.active &&
current.connection_generation == connection_generation &&
g_macro_capture.start(slot, connection_generation, options,
time_us_32(), current.state);
state_lock_exit();
return accepted;
}
bool bluepad32_input_backend_capture_stop(uint32_t run_id) {
if (!g_initialized || run_id == 0) return false;
state_lock_enter();
const bool matches = run_id == g_macro_capture.run_id();
if (matches) g_macro_capture.stop(time_us_32());
state_lock_exit();
return matches;
}
bool bluepad32_input_backend_capture_page(
uint32_t run_id, uint16_t first_index, Bluepad32CaptureSnapshot* output) {
if (!g_initialized || output == nullptr) return false;
state_lock_enter();
g_macro_capture.tick(time_us_32());
if ((run_id != 0 && run_id != g_macro_capture.run_id()) ||
first_index > g_macro_capture.event_count()) {
state_lock_exit();
return false;
}
*output = {};
output->run_id = g_macro_capture.run_id();
output->connection_generation = g_macro_capture.generation();
output->elapsed_us = g_macro_capture.elapsed_us(time_us_32());
output->slot = g_macro_capture.slot();
output->state = g_macro_capture.state();
output->options = g_macro_capture.options();
output->total_events = g_macro_capture.event_count();
output->first_index = first_index;
const uint16_t remaining = output->total_events - first_index;
output->event_count = remaining < BLUEPAD32_CAPTURE_PAGE_EVENTS
? remaining : BLUEPAD32_CAPTURE_PAGE_EVENTS;
for (uint8_t index = 0; index < output->event_count; ++index) {
g_macro_capture.event(first_index + index, &output->events[index]);
}
state_lock_exit();
return true;
}
void bluepad32_input_backend_init() {
if (g_initialized) {
return;
}
#if SWITCH2_PROBE_HUB
g_state_lock = spin_lock_init(spin_lock_claim_unused(true));
#else
critical_section_init(&g_state_lock);
#endif
#ifdef SWITCH_PICO_WII_IR
wii_ir_pointer_init();
#endif
configuration_service_prepare();
profile_service_prepare();
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
native_output_scheduler_prepare();
#endif
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
switch_native_output_prepare();
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
haptics_experiment_prepare();
#endif
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
BackendSlot& slot = g_slots[slot_index];
slot = {};
slot.state = make_neutral_state();
slot.identity = controller_identity_global();
slot.pending_rumble.slot = slot_index;
reset_slot_hotkeys(slot);
g_consumed_generation[slot_index] = 0;
g_last_snapshot_generation[slot_index] = 0;
g_ble_identity_mappings[slot_index] = {};
g_joycon_pair_hints[slot_index] = {};
g_joycon_gestures[slot_index] = {};
g_joycon_overrides[slot_index] = {};
#if SWITCH2_BRIDGE_FULL_INPUT
g_native_pending_devices[slot_index] = nullptr;
g_native_reports[slot_index] = {};
#endif
}
g_joycon_mode = JoyConMode::kPaired;
g_joycon_reconcile_requested = false;
g_pairing_window_requested = false;
g_pairing_snapshot_requested = false;
g_pairing_snapshot = {};
g_pairing_snapshot.status =
Bluepad32PairingSnapshotStatus::kPending;
g_clear_pairings_requested_token = 0;
g_clear_pairings_in_progress_token = 0;
g_next_clear_pairings_request_token = 1;
g_connection_status = ConnectionStatus::Initializing;
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
g_background_scan_active = false;
g_pairing_window_deadline_ms = 0;
g_pairing_window_duration_ms =
kDefaultPairingWindowDurationMs;
g_pairing_reset_feedback_deadline_ms = 0;
g_pairing_window_open = false;
g_initialized = true;
__atomic_store_n(&g_initialization_stage, 1, __ATOMIC_RELEASE);
__atomic_store_n(&g_rumble_timer_ticks, 0, __ATOMIC_RELAXED);
__atomic_store_n(&g_configuration_timer_ticks, 0, __ATOMIC_RELAXED);
__atomic_store_n(&g_controller_reports, 0, __ATOMIC_RELAXED);
__atomic_store_n(&g_host_rumble_requests, 0, __ATOMIC_RELAXED);
__atomic_store_n(&g_local_feedback_requests, 0, __ATOMIC_RELAXED);
__atomic_store_n(&g_rumble_dispatches, 0, __ATOMIC_RELAXED);
}
void bluepad32_input_backend_start() {
#if SWITCH2_PROBE_HUB
if (get_core_num() != 0) {
panic("native hub Bluetooth must start on Core 0");
}
#endif
if (!g_initialized) {
bluepad32_input_backend_init();
}
if (g_started) {
return;
}
// Non-hub builds register both cores before BTstack can write flash.
// Hub builds keep Core 1 in IRQ-disabled SRAM code, so the SDK's
// PICO_FLASH_ASSUME_CORE1_SAFE path only disables Core 0 interrupts.
if (!flash_safe_execute_core_init()) {
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
return;
}
g_started = true;
#if SWITCH2_PROBE_HUB
g_poll_ready = initialize_wireless_backend();
if (!g_poll_ready) {
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
publish_all_neutral();
}
#else
multicore_launch_core1_with_stack(
core1_main, g_core1_stack, sizeof(g_core1_stack));
#endif
}
void bluepad32_input_backend_poll() {
#if SWITCH2_PROBE_HUB
if (!g_poll_ready) return;
async_context_t* context = cyw43_arch_async_context();
// The SDK checks both the owning core and non-IRQ context. Polling invokes
// the existing BTstack workers/timers; no second scheduler or wait loop.
async_context_lock_check(context);
async_context_poll(context);
#endif
}
void bluepad32_input_backend_open_pairing_window() {
if (!g_initialized) {
bluepad32_input_backend_init();
}
state_lock_enter();
g_pairing_window_requested = true;
state_lock_exit();
}
uint32_t bluepad32_input_backend_clear_pairings() {
if (!g_initialized) {
bluepad32_input_backend_init();
}
state_lock_enter();
uint32_t request_token = g_clear_pairings_requested_token;
if (request_token == 0) {
request_token = g_clear_pairings_in_progress_token;
}
if (request_token == 0) {
request_token = g_next_clear_pairings_request_token;
g_next_clear_pairings_request_token =
request_token == UINT32_MAX ? 1 : request_token + 1;
g_clear_pairings_requested_token = request_token;
g_pairing_snapshot.status =
Bluepad32PairingSnapshotStatus::kPending;
}
state_lock_exit();
return request_token;
}
void bluepad32_input_backend_request_pairing_snapshot() {
if (!g_initialized) {
bluepad32_input_backend_init();
}
state_lock_enter();
g_pairing_snapshot_requested = true;
if (g_pairing_snapshot.status != Bluepad32PairingSnapshotStatus::kFailed) {
g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kPending;
}
state_lock_exit();
}
void bluepad32_input_backend_pairing_snapshot(
Bluepad32PairingSnapshot* out) {
if (out == nullptr) {
return;
}
if (!g_initialized) {
bluepad32_input_backend_init();
}
state_lock_enter();
*out = g_pairing_snapshot;
state_lock_exit();
}
void bluepad32_input_backend_diagnostics(
Bluepad32BackendDiagnostics* out) {
if (out == nullptr) {
return;
}
*out = {};
out->initialization_stage =
__atomic_load_n(&g_initialization_stage, __ATOMIC_ACQUIRE);
out->rumble_timer_ticks =
__atomic_load_n(&g_rumble_timer_ticks, __ATOMIC_RELAXED);
out->configuration_timer_ticks =
__atomic_load_n(&g_configuration_timer_ticks, __ATOMIC_RELAXED);
out->controller_reports =
__atomic_load_n(&g_controller_reports, __ATOMIC_RELAXED);
out->host_rumble_requests =
__atomic_load_n(&g_host_rumble_requests, __ATOMIC_RELAXED);
out->local_feedback_requests =
__atomic_load_n(&g_local_feedback_requests, __ATOMIC_RELAXED);
out->rumble_dispatches =
__atomic_load_n(&g_rumble_dispatches, __ATOMIC_RELAXED);
out->switch2_ingress_drops =
__atomic_load_n(&g_switch2_ingress_drops, __ATOMIC_RELAXED);
out->switch2_output_drops = uni_hid_parser_switch2_haptics_dropped();
state_lock_enter();
for (const BackendSlot& slot : g_slots) {
if (slot.active) {
++out->active_slots;
}
if (slot.active && slot.device != nullptr &&
slot.device->report_parser.play_dual_rumble != nullptr) {
++out->rumble_capable_slots;
}
if (slot.feedback_pending || slot.profile_feedback.active ||
slot.pending_profile_feedback_count != 0) {
++out->feedback_pending_slots;
}
if (slot.rumble_pending || slot.switch2_ingress.count != 0) {
++out->rumble_pending_slots;
}
}
state_lock_exit();
}
void bluepad32_input_backend_snapshot(uint8_t slot_index,
Bluepad32SlotSnapshot* out) {
if (out == nullptr) {
return;
}
*out = {};
if (!valid_slot(slot_index) || !g_initialized) {
return;
}
state_lock_enter();
const BackendSlot& slot = g_slots[slot_index];
out->active = slot.active;
out->connection_generation = slot.connection_generation;
out->identity = slot.identity;
out->pre_hotkey_button_mask =
slot.pre_hotkey_button_mask;
out->state = slot.state;
out->accelerometer = slot.accelerometer;
out->nunchuk_accelerometer = slot.nunchuk_accelerometer;
const uint32_t state_generation = slot.state_generation;
state_lock_exit();
if (state_generation == g_consumed_generation[slot_index]) {
out->state.motion_sample_count = 0;
}
g_last_snapshot_generation[slot_index] = state_generation;
}
#if SWITCH2_BRIDGE_FULL_INPUT
void bluepad32_input_backend_select_native_source(const uint8_t address[6]) {
if (!g_initialized) return;
state_lock_enter();
g_native_explicit_address = address != nullptr;
if (address != nullptr) memcpy(g_native_address, address, 6);
else memset(g_native_address, 0, sizeof(g_native_address));
refresh_native_source_locked(true);
state_lock_exit();
}
void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* output) {
if (output == nullptr) return;
*output = {};
if (!g_initialized) return;
state_lock_enter();
*output = g_native_snapshot;
state_lock_exit();
}
bool bluepad32_input_backend_native_sample_request(
uint8_t instance, uint8_t sample_id, uint64_t* token) {
if (token == nullptr) return false;
*token = 0;
if (!g_initialized || instance >= 2 || sample_id >= 8) return false;
state_lock_enter();
NativeGamepadCue& cue = g_native_cues[instance];
const uint8_t index = g_native_slot;
const bool accepted = index < kSlotCount && g_next_native_token != 0 &&
native_rumble_capable(g_slots[index], instance) &&
!cue.in_flight && (sample_id == 0 || (cue.result != 0 && !cue.active));
if (accepted) {
cue = {};
cue.token = g_next_native_token++;
cue.slot = index;
cue.connection_generation = g_native_generation;
cue.requested_ms = btstack_run_loop_get_time_ms();
cue.sample_id = sample_id;
cue.result = 0;
*token = cue.token;
}
state_lock_exit();
return accepted;
}
int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t token) {
if (!g_initialized || instance >= 2 || token == 0) return -1;
state_lock_enter();
NativeGamepadCue& cue = g_native_cues[instance];
int result = -1;
if (cue.token == token && !cue.consumed) {
if (!native_cue_current(cue) ||
(cue.result == 0 &&
btstack_run_loop_get_time_ms() - cue.requested_ms >= kNativeGamepadCueDeadlineMs))
cancel_native_cue_locked(cue);
result = cue.result;
if (result != 0) cue.consumed = true;
}
state_lock_exit();
return result;
}
void bluepad32_input_backend_native_sample_cancel(uint8_t instance) {
if (!g_initialized || instance >= 2) return;
state_lock_enter();
cancel_native_cue_locked(g_native_cues[instance]);
state_lock_exit();
}
#endif
#ifdef SWITCH2_BRIDGE_WII_INPUT
void bluepad32_input_backend_select_wii_source(const uint8_t address[6]) {
// Selection is configuration, not a live Core 0 parser mutation.
if (!g_initialized || g_started) return;
state_lock_enter();
g_wii_source_selected = address != nullptr;
if (address != nullptr) memcpy(g_wii_source_address, address, 6);
else memset(g_wii_source_address, 0, sizeof(g_wii_source_address));
g_wii_snapshot = {};
g_wii_cue = {};
for (BackendSlot& slot : g_slots) retire_wii_motion(slot.wii_motion);
#ifdef SWITCH_PICO_WII_IR
// Lock order: backend exclusive, then pointer striped.
wii_ir_pointer_reset();
#endif
state_lock_exit();
}
void bluepad32_input_backend_wii_snapshot(Bluepad32WiiBridgeSnapshot* output) {
if (output == nullptr) return;
*output = {};
if (!g_initialized) return;
state_lock_enter();
const uint8_t slot = g_wii_snapshot.slot;
if (slot < kSlotCount && is_selected_wii(g_slots[slot]) &&
g_slots[slot].connection_generation ==
g_wii_snapshot.controller.connection_generation) {
*output = g_wii_snapshot;
}
state_lock_exit();
}
bool bluepad32_input_backend_wii_sample_request(uint8_t sample_id, uint64_t* token) {
if (token == nullptr) return false;
*token = 0;
if (!g_initialized || sample_id >= 8) return false;
state_lock_enter();
bool accepted = false;
// Do not overwrite a command already being dispatched. Sample zero can
// replace a queued/running pattern; ordinary cues serialize until it ends.
if (g_next_wii_cue_token != 0 && !g_wii_cue.in_flight &&
(sample_id == 0 || (g_wii_cue.result != 0 &&
!g_wii_cue.active && !g_wii_cue.stop_pending))) {
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
const BackendSlot& slot = g_slots[slot_index];
if (!is_selected_wii(slot) ||
slot.device->report_parser.play_dual_rumble == nullptr) continue;
g_wii_cue = {};
g_wii_cue.token = g_next_wii_cue_token++;
g_wii_cue.slot = slot_index;
g_wii_cue.connection_generation = slot.connection_generation;
g_wii_cue.sample_id = sample_id;
g_wii_cue.requested_ms = btstack_run_loop_get_time_ms();
g_wii_cue.result = 0;
*token = g_wii_cue.token;
accepted = true;
break;
}
}
state_lock_exit();
return accepted;
}
int bluepad32_input_backend_wii_sample_result(uint64_t token) {
if (!g_initialized || token == 0) return -1;
state_lock_enter();
int result = -1;
if (g_wii_cue.token == token && !g_wii_cue.consumed) {
if (!wii_cue_target_current() ||
g_slots[g_wii_cue.slot].device->report_parser.play_dual_rumble == nullptr ||
(g_wii_cue.result == 0 &&
btstack_run_loop_get_time_ms() - g_wii_cue.requested_ms >= kWiiCueDeadlineMs)) {
cancel_wii_cue_locked();
}
result = g_wii_cue.result;
if (result != 0) g_wii_cue.consumed = true;
}
state_lock_exit();
return result;
}
void bluepad32_input_backend_wii_sample_cancel() {
if (!g_initialized) return;
state_lock_enter();
cancel_wii_cue_locked();
state_lock_exit();
}
#endif
void bluepad32_input_backend_playtest_snapshot(
uint8_t slot_index, Bluepad32PlaytestSnapshot* out) {
if (out == nullptr) {
return;
}
*out = {};
if (!valid_slot(slot_index) || !g_initialized) {
return;
}
state_lock_enter();
const BackendSlot& slot = g_slots[slot_index];
out->active = slot.active;
out->connection_generation = slot.connection_generation;
out->state_generation = slot.state_generation;
out->identity = slot.identity;
out->physical_button_mask = slot.pre_hotkey_button_mask;
out->state = slot.state;
if (slot.device != nullptr) {
out->battery = slot.device->controller.battery;
out->capabilities =
(slot.device->report_parser.play_dual_rumble != nullptr ? 1u : 0u) |
(slot.device->report_parser.set_lightbar_color != nullptr ? 2u : 0u) |
(slot.device->report_parser.set_player_leds != nullptr ? 4u : 0u) |
(slot.state.motion_sample_count != 0 ? 8u : 0u);
out->controller_layout = controller_layout(slot);
}
state_lock_exit();
}
bool bluepad32_input_backend_toggle_motion(
uint8_t slot_index, uint32_t connection_generation) {
if (!g_initialized || !valid_slot(slot_index)) {
return false;
}
bool toggled = false;
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
if (slot.active &&
slot.connection_generation == connection_generation) {
slot.motion_enabled = !slot.motion_enabled;
#ifdef SWITCH_PICO_WII_IR_GYRO
if (slot.device != nullptr &&
slot.device->controller_type == CONTROLLER_TYPE_WiiController) {
const ControllerMotionSample sample =
slot.state.motion_sample_count != 0
? slot.state.motion_samples[0] : ControllerMotionSample{};
wii_ir_gyro_update_motion(
slot_index, connection_generation, slot.motion_enabled, sample);
if (!slot.motion_enabled) {
slot.state.motion_sample_count = 0;
}
}
#endif
if (slot.motion_enabled) {
queue_local_feedback(
slot, kMotionEnabledFeedbackDurationMs,
kMotionEnabledFeedbackWeakMagnitude,
kMotionEnabledFeedbackStrongMagnitude);
} else {
queue_local_feedback(
slot, kMotionDisabledFeedbackDurationMs,
kMotionDisabledFeedbackWeakMagnitude,
kMotionDisabledFeedbackStrongMagnitude);
}
toggled = true;
}
state_lock_exit();
return toggled;
}
void bluepad32_input_backend_report_sent(uint8_t slot_index) {
if (!g_initialized || !valid_slot(slot_index)) {
return;
}
g_consumed_generation[slot_index] = g_last_snapshot_generation[slot_index];
}
void bluepad32_input_backend_queue_rumble(
uint8_t slot_index, const ControllerRumbleOutput& rumble) {
if (!g_initialized || !valid_slot(slot_index)) {
return;
}
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
const uint64_t received_us = time_us_64();
uint32_t native_generation = 0;
bool native_candidate = false;
#endif
const uint16_t duration_ms = host_rumble_duration_ms();
const uint32_t received_ms = btstack_run_loop_get_time_ms();
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
if (slot.active && slot.device != nullptr) {
if (uni_hid_parser_switch2_is_ble_device(slot.device)) {
Switch2Ingress& ingress = slot.switch2_ingress;
const uint8_t host_mode = switch2_host_mode();
if (ingress.host_mode != host_mode) {
clear_switch2_ingress(slot);
ingress.host_mode = host_mode;
}
if (switch2_host_stop(rumble)) {
// Host stop is a barrier, not a local-feedback cancellation.
ingress.head = 0;
ingress.count = 0;
} else if (ingress.count == kSwitch2IngressCapacity) {
ingress.head = (ingress.head + 1u) % kSwitch2IngressCapacity;
--ingress.count;
__atomic_add_fetch(&g_switch2_ingress_drops, 1, __ATOMIC_RELAXED);
}
Switch2HostCommand& command =
ingress.commands[(ingress.head + ingress.count) % kSwitch2IngressCapacity];
command = {};
command.envelope.slot = slot_index;
command.envelope.connection_generation = slot.connection_generation;
command.envelope.rumble = rumble;
command.envelope.duration_ms = duration_ms;
command.envelope.received_ms = received_ms;
command.generation = ingress.generation;
++ingress.count;
__atomic_add_fetch(&g_host_rumble_requests, 1, __ATOMIC_RELAXED);
state_lock_exit();
return;
}
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
native_generation = slot.connection_generation;
native_candidate = true;
#endif
const RumbleEnvelope envelope{
slot_index, slot.connection_generation, rumble,
duration_ms, received_ms
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
, received_us
#endif
};
slot.pending_rumble = envelope;
slot.rumble_pending = true;
__atomic_add_fetch(
&g_host_rumble_requests, 1, __ATOMIC_RELAXED);
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;
}
}
state_lock_exit();
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
if (native_candidate)
switch_native_output_submit(slot_index, native_generation, received_us, rumble,
duration_ms == kXInputHostRumbleDurationMs);
#endif
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
if (native_candidate) {
if (duration_ms == kXInputHostRumbleDurationMs) {
haptics_experiment_submit_rumble(
slot_index, native_generation, received_us,
rumble.low_frequency_magnitude, rumble.high_frequency_magnitude);
} else {
haptics_experiment_submit(
slot_index, native_generation, received_us, rumble.hd);
}
}
#endif
}
bool bluepad32_input_backend_set_wii_orientation(
const ControllerIdentity& identity, uint32_t connection_generation,
bool vertical) {
if (!g_initialized || !identity.stable ||
controller_identity_is_global(identity)) {
return false;
}
bool queued = false;
state_lock_enter();
for (BackendSlot& slot : g_slots) {
if (!is_solo_wii_remote(slot) ||
slot.connection_generation != connection_generation ||
!controller_identity_equal(slot.identity, identity)) {
continue;
}
slot.pending_wii_orientation = {identity, connection_generation, vertical};
slot.wii_orientation_pending = true;
queued = true;
break;
}
state_lock_exit();
return queued;
}
bool bluepad32_input_backend_identify(
const ControllerIdentity& identity) {
if (!g_initialized || !identity.stable ||
controller_identity_is_global(identity)) {
return false;
}
bool queued = false;
state_lock_enter();
for (BackendSlot& slot : g_slots) {
if (!slot.active ||
!controller_identity_equal(slot.identity, identity)) {
continue;
}
const ProfileFeedbackEnvelope feedback{
slot.connection_generation, 1,
ControllerProfileConfirmationPolicy::kRumbleAndLed};
queue_profile_feedback(slot, feedback);
queued = true;
break;
}
state_lock_exit();
return queued;
}
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;
}
state_lock_enter();
BackendSlot& slot = g_slots[slot_index];
if (slot.active && slot.device != nullptr &&
slot.connection_generation == connection_generation) {
const ProfileFeedbackEnvelope feedback{
connection_generation, active_profile_number, policy};
queue_profile_feedback(slot, feedback);
}
state_lock_exit();
}