Fix XUSB binding after reconnect

This commit is contained in:
Joey Yakimowich-Payne 2026-09-03 12:46:44 -06:00
commit 1e36571770
11 changed files with 74 additions and 11 deletions

View file

@ -101,15 +101,28 @@ void adapter_host_probe_note_string_descriptor(uint8_t index) {
bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
tusb_control_request_t const *request) {
if (stage != CONTROL_STAGE_SETUP || request == nullptr ||
if (request == nullptr ||
request->bmRequestType_bit.direction != TUSB_DIR_IN ||
request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR ||
request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE) {
return false;
}
const bool status_request =
request->bRequest == kStatusRequest &&
request->wIndex == kStatusIndex;
const bool compatible_id_request =
request->bRequest == XInput::kMsVendorRequest &&
request->wIndex == XInput::kMsCompatIdIndex &&
(g_mode == AdapterUsbMode::kSwitchProbe ||
g_mode == AdapterUsbMode::kXInput);
if (!status_request && !compatible_id_request) {
return false;
}
if (stage != CONTROL_STAGE_SETUP) {
return true;
}
if (request->bRequest == kStatusRequest &&
request->wIndex == kStatusIndex) {
if (status_request) {
const uint32_t current_time = now_ms();
g_status_response[0] =
g_mode == AdapterUsbMode::kXInput ? 1 : 0;
@ -121,10 +134,6 @@ bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
sizeof(g_status_response));
}
if (request->bRequest != XInput::kMsVendorRequest ||
request->wIndex != XInput::kMsCompatIdIndex) {
return false;
}
if (g_mode == AdapterUsbMode::kSwitchProbe) {
g_probe.note_ms_compat_id_request(now_ms());

View file

@ -312,6 +312,13 @@ AdapterRequestedMode adapter_mode_controller_requested_mode() {
return g_requested_mode;
}
void adapter_mode_controller_on_usb_unmounted() {
if (g_requested_mode == AdapterRequestedMode::kAuto &&
adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
reboot_now();
}
}
void adapter_mode_controller_process_input(
uint8_t slot_index, bool active, uint32_t connection_generation,
uint16_t* pre_hotkey_button_mask, uint32_t now_ms,

View file

@ -16,6 +16,11 @@ constexpr uint16_t ADAPTER_MODE_CHORD_BUTTON_MASK =
void adapter_mode_controller_initialize_usb();
AdapterRequestedMode adapter_mode_controller_requested_mode();
// Auto mode's XInput selection is valid for one USB attachment. Reboot to the
// Switch probe after it unmounts so the next host attachment is detected
// afresh. Persistent manual XInput mode is intentionally unaffected.
void adapter_mode_controller_on_usb_unmounted();
// Observes the physical pre-hotkey mask and removes the mode chord from both
// that mask and the state before profile processing. Slot state is isolated by
// connection generation and all time comparisons are uint32-wrap safe.

View file

@ -63,7 +63,7 @@ bool valid_identity(const ControllerIdentity& identity) {
}
void refresh_list_locked() {
g_list = {};
g_list = ProfileServiceListSnapshot{};
g_list.metadata = g_metadata;
g_list.count = 1;
g_list.rows[0].identity = controller_identity_global();
@ -183,7 +183,7 @@ void profile_service_prepare() {
critical_section_init(&g_lock);
g_metadata = {};
__atomic_store_n(&g_published_generation, 0, __ATOMIC_RELAXED);
g_list = {};
g_list = ProfileServiceListSnapshot{};
g_selected = {};
g_active_profiles[0] = {};
g_active_profile_count = 0;

View file

@ -181,6 +181,11 @@ static void log_usb_state() {
if (mounted != g_last_mounted) {
g_last_mounted = mounted;
LOG_PRINTF("[USB] %s\n", mounted ? "mounted" : "unmounted");
#ifdef SWITCH_PICO_BLUEPAD32
if (!mounted) {
adapter_mode_controller_on_usb_unmounted();
}
#endif
}
#ifdef SWITCH_PICO_BLUEPAD32

View file

@ -115,6 +115,12 @@ void test_manual_modes_bypass_and_consume_probe_state() {
control_length) == 0 &&
alarm_count == 0,
"manual XInput did not serve XInput descriptors without probing");
require(adapter_host_probe_vendor_control(
0, CONTROL_STAGE_DATA, &request) &&
adapter_host_probe_vendor_control(
0, CONTROL_STAGE_ACK, &request) &&
control_count == 1,
"manual XInput did not retain the compatible-ID transfer");
reset_harness(kXInputBootMagic);
adapter_host_probe_init(AdapterRequestedMode::kDInput);
@ -154,6 +160,12 @@ void test_probe_transition_resets_before_watchdog() {
alarm_count == 1 && pending_alarm_delay_ms == 100 &&
reboot_count == 0,
"auto probe did not queue the exact delayed transition");
require(adapter_host_probe_vendor_control(
0, CONTROL_STAGE_DATA, &request) &&
adapter_host_probe_vendor_control(
0, CONTROL_STAGE_ACK, &request) &&
control_count == 1 && alarm_count == 1,
"auto probe did not retain the compatible-ID transfer");
pending_alarm(1, pending_alarm_user_data);
require(reset_count == 1 && reboot_count == 1 &&

View file

@ -532,6 +532,27 @@ void test_failed_recovery_never_reboots() {
"or failed recovery acknowledged, released ownership, or retried");
}
void test_auto_xinput_disconnect_reboots_to_probe() {
reset_harness(AdapterRequestedMode::kAuto);
probed_active_mode = AdapterUsbMode::kXInput;
adapter_mode_controller_on_usb_unmounted();
adapter_mode_controller_on_usb_unmounted();
require(runtime_reset_count == 1 && reboot_count == 1,
"Auto XInput unmount did not schedule exactly one probe reboot");
reset_harness(AdapterRequestedMode::kXInput);
probed_active_mode = AdapterUsbMode::kXInput;
adapter_mode_controller_on_usb_unmounted();
require(runtime_reset_count == 0 && reboot_count == 0,
"manual XInput mode rebooted after USB unmount");
reset_harness(AdapterRequestedMode::kAuto);
probed_active_mode = AdapterUsbMode::kSwitchProbe;
adapter_mode_controller_on_usb_unmounted();
require(runtime_reset_count == 0 && reboot_count == 0,
"Auto Switch probe rebooted after USB unmount");
}
} // namespace
void configuration_service_initialize_pre_usb() {
@ -659,5 +680,6 @@ int main() {
test_configuration_failure_and_correlated_reboot();
test_recovery_auto_wins_host_mode_interleaving();
test_failed_recovery_never_reboots();
test_auto_xinput_disconnect_reboots_to_probe();
return 0;
}

View file

@ -146,9 +146,12 @@ void test_microsoft_compatible_id_descriptor() {
"Microsoft function count mismatch");
for (uint8_t instance = 0;
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
const uint8_t* function =
&kMsCompatIdDescriptor[16 + instance * 24];
expect(function[0] == instance,
"Microsoft descriptor interface mismatch");
expect(function[1] == 1,
"XUSB function must own exactly one interface");
expect(std::memcmp(&function[2], "XUSB10", 6) == 0,
"XUSB10 compatible ID missing");
}

View file

@ -15,7 +15,7 @@ namespace XInput {
constexpr uint16_t kDevelopmentVendorId = 0xcafe;
constexpr uint16_t kDevelopmentProductId = 0x4010;
constexpr uint16_t kDevelopmentDeviceRevision = 0x0101;
constexpr uint16_t kDevelopmentDeviceRevision = 0x0105;
constexpr uint8_t kInterfaceDescriptorSize = 39;
constexpr uint16_t kConfigurationDescriptorSize =
9 + SWITCH_PICO_HID_INSTANCE_COUNT * kInterfaceDescriptorSize;