From d424fe0f4a96fda70b7a93195b7d160e8a7dfeef Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 14 Sep 2026 13:55:35 -0600 Subject: [PATCH] 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. --- README.md | 140 +++++- .../input/bluepad32_input_backend.cpp | 405 ++++++++++-------- .../platform/pico/bootsel_pairing_button.cpp | 9 + src/firmware/usb/native_hub/native_hub.c | 367 +++++++++++++++- src/firmware/usb/native_hub/native_hub.h | 5 + .../usb/native_hub/native_hub_trace.h | 34 ++ .../usb/usb_configuration_management.cpp | 6 +- tests/native_hub_management_test.cpp | 40 ++ tests/native_hub_stubs/hardware_stub.h | 1 + tools/pico_usb_address_probe/router.c | 36 +- tools/switch2_usb_probe/main.c | 26 ++ tools/switch2_usb_probe/probe_build.cmake | 10 +- 12 files changed, 884 insertions(+), 195 deletions(-) create mode 100644 src/firmware/usb/native_hub/native_hub_trace.h diff --git a/README.md b/README.md index 44bb4c5..8bcd8c1 100644 --- a/README.md +++ b/README.md @@ -966,7 +966,7 @@ Select the Wii profile owner and its active profile. With the Nunchuk connected, Auto uses its live layout; while offline, choose **Preview · Wii Remote + Nunchuk**. The physical Nunchuk **C** is logical `west` and **Z** is logical `north`, not the unrelated Switch2 extra control named `c`. Both can target buttons or triggers. -For example, **Z → L**, **C → ZL**, with **Remote 2 → R**, makes **Z + 2** the +For example, **C → L**, **Z → ZL**, with **Remote 2 → R**, makes **C + 2** the physical L+R combination. Save changes to that Wii profile, not the global default or another controller's profile. @@ -977,11 +977,12 @@ cannot borrow child EP0 buffers; aborted/short/corrupt transfers and reset-stale status completions cannot dispatch profile writes. Valid status ACKs preceding a subsequent SETUP remain valid. -Qualification: 390 focused tests pass. The actual browser editor saved the -Wii C/Z example and read it back after a Pico reboot. All 80 stored profiles -were compared: only the two intended mappings changed; the other 79 profiles, -metadata and active selections were unchanged. The configuration/pairing flash -region matched the pre-update backup. Native R/L descriptors, EP0 identity, +Qualification: 390 focused tests pass. The earlier browser save/readback check +used the reverse C/Z example (**C → ZL, Z → L**); that test configuration must +not be mistaken for the requested **C → L, Z → ZL** mapping above. All 80 stored +profiles were compared: only those two mapping fields changed; the other 79 +profiles, metadata and active selections were unchanged. The configuration/pairing +flash region matched the pre-update backup. Native R/L descriptors, EP0 identity, initialization and bulk-isolation checks passed while editor traffic was active. No physical Switch L+R button press was claimed by that transport check. @@ -1047,8 +1048,131 @@ packets and 149,567 L packets, including 113,316 and 114,276 fresh IMU packets. USBmon measured overlapping child reads during 94.4% of the steady-state window with no failed read completions there and no capture drops. A subsequent 15-second mixed control/bulk/input check passed 57 rounds with no errors. -This confirms the PC starvation reproduction is corrected. The user's subsequent -Switch gameplay trial also stopped reproducing the L+R disconnect. +This confirms the PC starvation reproduction is corrected. An initial Switch +trial appeared successful, but a longer run subsequently disconnected after +roughly 236,000 R / 234,000 L input reports. Bluetooth reports and USB SOF +continued while input and addressed-token counters stopped, without USB error +flags. An additional right-side firmware-version request and endpoint-halt clear +were observed. Extended Switch stability remains unresolved. Concurrent HID +polling with interleaved version reads reproduced `EPROTO` on `0.78` after about +140 seconds (`c1/02`, interface 1, requested length 64). + +A synchronous shared-EP0-copy trial (`0.79`) also failed a version read after +about 17 seconds and recorded late bank switches. The trial was rejected and +the exact `0.78` image restored, with saved data, pairing inventory and profile +selections verified unchanged. A subsequent xHCI trace reproduced the failure +after 111 seconds and located USB transaction error completion code 4 on the +right controller's final OUT status-stage descriptor, not its data-stage +descriptor (`c0/02`, requested length 16). This narrows the missing handshake but +does not distinguish a missed status token, bank/IRQ handoff delay, or malformed +response. No further timing change is qualified by these results; failure-time +device-state or direct USB-wire capture is still needed. + +**Flight recorder (since diagnostic 0.83):** trace-enabled hub builds retain 64 completed +records in a 65-slot SRAM ring. They record OUT selections and rejected IN/SETUP +selections; 0.87 additionally records successful root-hub IN selections. The +failure hook runs only after the original selector rejects the token. Records +include PID, address/owner, physical EP0 buffer controls, selection reason, raw +SIE state, IRQ timestamps and the Core 0 execution phase. +`pre=0` marks unavailable pre-selection observations rather than inferred values. +Phase values identify main-loop work; `0x10000 | line` identifies a held backend +state-lock region by source line in `bluepad32_input_backend.cpp`. +These are execution breadcrumbs, not a sampled program counter. The phase tags +do not change lock scope, but their instrumentation adds overhead. + +After 200 ms without child IN completions following first input, the recorder +freezes and emits `HUB_FLIGHT_FREEZE`, `HUB_FLIGHT_CONTEXT`, `HUB_FLIGHT` and +`HUB_FLIGHT_END` over UART, at most one line per 50 ms. Since 0.87, a pending +control transfer with no generation/stage/position progress for 200 ms also +triggers a dump, even before input starts. `HUB_FLIGHT_CONTROL` adds the stalled +request, expected first data word and physical EP0 word/buffer state; records +also include the physical EP0 first word. Rearming requires the dump to finish +and fresh input or control progress. It never resets USB or changes selection +decisions. Logger-mask +measurements include instrumentation overhead, and hardware snapshots are not +atomic. Ordinary non-trace builds retain the 0.78 transport. + +The initial 0.80 recorder selected IN rather than OUT; those logs are not OUT +evidence. Independently encoded PID checks corrected this in 0.81. Its two mixed +test failures were located in the IN data stage, so 0.82 adds rejected-IN/SETUP +coverage. Ring retention, freeze/rearm and dispatch smoke checks, both native +builds and 14 transport regressions pass; the 0.82 recorder also passed an +on-device initialization/poll/quiet-dump smoke. This is diagnostic instrumentation, +not a qualified fix for the remaining control-transfer failure. + +The 0.83 concurrent-input/version-read capture failed after 15.8 seconds in the +left controller's OUT status stage (xHCI completion code 4). Four rejected tokens +spanned 30.7 microseconds with EP1 IN completion pending and unchanged USB IRQ +timestamps. All four carried `phase=00010fee`: the state-lock section beginning +at line 4078 of that build's `platform_on_controller_data()`. The phase identifies +the held critical section, not an individual instruction within it. + +The 0.84 trial moves native/Wii parser snapshot reads, extra-button reads and +report-time calculation outside that interrupt-masked section. Shared-state +updates remain locked; USB bank-selection and recovery behavior are unchanged. +The concurrent USB test nevertheless failed after 34.0 seconds in the right +controller's IN data stage (xHCI completion code 4, all 16 bytes outstanding). +The final rejected tokens still showed pending EP1 completion and the +controller-data lock, now at line 4106. This trial did not qualify. + +The 0.85 trial keeps a spin lock for native-hub backend state without changing +the caller's interrupt mask. Its entry point enforces Core 0 foreground access: +the hub uses the SDK's polled Bluetooth context, and USB IRQ/Core1 routing never +access backend state or dispatch protocol callbacks. Ordinary AIO retains its +interrupt-masking critical section. USB bank locking and intentionally masked +control-status commits are unchanged. A held backend phase tag therefore no +longer implies masked interrupts. Both native builds, AIO and eight focused +backend/transport test cases pass; on-device backend snapshots return real +Wii/Nunchuk motion. The concurrent test still failed after 43.3 seconds, now in +the protocol phase rather than under the backend lock: left IN data failed with +16 bytes outstanding and three rejected tokens spanning 18.35 microseconds. + +The 0.86 trial addresses another masked interval in that phase: the 100 ms +BOOTSEL poll's flash-safe QSPI-CS settling loop. Interrupts remain disabled and +the 1000-iteration loop is retained, but each iteration services pending native +USB hardware status through an SRAM-only helper. It queues events without +dispatching protocol callbacks or changing NVIC pending state. The IRQ's +variable-length SRAM copy no longer calls flash-backed `memcpy`; linked trace +and non-trace call graphs contain no flash-backed calls in the sampler/IRQ path. +Nine focused tests pass, including real transport-fixture bank handoff while +interrupts remain masked and deferred completion dispatch. Native trace, +native non-trace and AIO builds pass. + +**0.86 PC qualification:** the original concurrent R/L HID plus device-recipient +version-read reproduction passed 600 seconds: 2,400 control reads and +149,139 R / 149,204 L active-phase HID reads, with no reported errors. +The complete usbmon capture contains 601,718 records with zero capture drops. +Its steady-state window shows 93.98% simultaneous pending R/L reads, no HID +completion errors, and a maximum per-side completion gap below 8.07 ms. +The xHCI trace is a rolling buffer and lost older events; it is not complete-run +evidence. A subsequent 60-second matrix passed all 240 queries across both sides, +requested lengths 1/7/16/32/64 and device/interface-0/interface-1 recipients +(30 variants, eight queries each), while HID reads remained concurrent. +Captures are `usb-sram-0.86-A.*` and `usb-matrix-0.86-B.*` in the ignored native +gamepad build directory. + +**0.86 Switch qualification failed at startup:** the root hub and right child +configured at addresses 5 and 6, but neither native protocol initialized and the +left child never acquired an address. Bluetooth reports continued. The last +root transaction was `a3/00`, port 2, length 4, following the port-reset ACK: +generation 39 armed `11010000` with DATA1, but no data completion followed. +The root remained in DATA_IN at position 0/4. This is an enumeration failure, +not a passed gameplay test; the PC result does not qualify console operation. +Startup trace and state are preserved as `switch-0.86-enumeration-*` in the +ignored native gamepad build directory. The existing recorder requires prior +child input, so it did not produce a flight dump for this startup failure. + +The 0.87 enumeration recorder addresses that diagnostic blind spot, not the +underlying startup failure. Its pre-input timeout, root-IN recording, expected +versus physical data distinction, progress tracking and rearm smoke checks pass, +as do 11 transport/protocol regressions and both native plus AIO builds. Linked +critical paths remain in SRAM. Non-trace firmware remains at 0.86. + +A subsequent 0.87 Switch startup configured and initialized both native +controllers, and the user confirmed working input. The startup trace is retained +as `switch-0.87-startup-uart.txt` in the ignored build directory. Instrumentation +changes timing; this successful run does not establish the startup failure's +root cause or qualify extended Switch gameplay. For sensorless hardware, the checker supports `--input-only`: press real buttons and keep changing controls on both halves during the run. Neutral fallback diff --git a/src/firmware/input/bluepad32_input_backend.cpp b/src/firmware/input/bluepad32_input_backend.cpp index f15f2cc..45469c5 100644 --- a/src/firmware/input/bluepad32_input_backend.cpp +++ b/src/firmware/input/bluepad32_input_backend.cpp @@ -25,6 +25,9 @@ #include #if SWITCH2_PROBE_HUB #include +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) +#include "usb/native_hub/native_hub_trace.h" +#endif #endif #include #include @@ -379,7 +382,40 @@ struct BackendSlot { 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; @@ -639,7 +675,7 @@ bool valid_slot(uint8_t slot) { } bool has_free_slot() { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); unsigned physical_count = 0; for (const BackendSlot& slot : g_slots) { physical_count += slot.device != nullptr; @@ -648,17 +684,17 @@ bool has_free_slot() { #if SWITCH2_BRIDGE_FULL_INPUT for (const auto* pending : g_native_pending_devices) physical_count += pending != nullptr; #endif - critical_section_exit(&g_state_lock); + state_lock_exit(); return physical_count < kSlotCount; } bool has_active_controller() { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); bool active_controller = false; for (const BackendSlot& slot : g_slots) { active_controller = active_controller || slot.active; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return active_controller; } @@ -875,7 +911,7 @@ void mask_joycon_gesture(uni_gamepad_t& gamepad, } bool waiting_for_joycon_mate(int side = 0) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); unsigned physical_count = 0; bool pending = false; unsigned left_count = 0; @@ -894,7 +930,7 @@ bool waiting_for_joycon_mate(int side = 0) { right_count += candidate_side > 0; } } - critical_section_exit(&g_state_lock); + 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 @@ -1050,10 +1086,10 @@ uni_switch2_haptics_frame_t switch2_physical_frame( } void drain_switch2_ingress(uint8_t slot_index, uint32_t now_ms) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& slot = g_slots[slot_index]; if (!slot.active || !uni_hid_parser_switch2_is_ble_device(slot.device)) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return; } Switch2Ingress& ingress = slot.switch2_ingress; @@ -1110,7 +1146,7 @@ void drain_switch2_ingress(uint8_t slot_index, uint32_t now_ms) { ingress.head = (ingress.head + 1u) % kSwitch2IngressCapacity; --ingress.count; } - critical_section_exit(&g_state_lock); + state_lock_exit(); } bool addresses_equal(const bd_addr_t first, const bd_addr_t second) { @@ -1214,7 +1250,7 @@ void publish_ble_identity(const BleIdentityMapping& mapping) { ControllerIdentity observed_identity{}; bool observe_identity = false; bool joycon_identity_changed = false; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); for (BackendSlot& slot : g_slots) { if (slot.device != nullptr && slot.companion == nullptr && gap_get_connection_type(slot.device->conn.handle) == @@ -1232,7 +1268,7 @@ void publish_ble_identity(const BleIdentityMapping& mapping) { } } } - critical_section_exit(&g_state_lock); + state_lock_exit(); if (observe_identity) { profile_service_observe_identity_on_storage_core( observed_identity); @@ -1270,7 +1306,7 @@ void clear_ble_identity_for_handle(hci_con_handle_t connection_handle) { } } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); for (BackendSlot& slot : g_slots) { if (slot.device != nullptr && slot.companion == nullptr && gap_get_connection_type(slot.device->conn.handle) == @@ -1279,7 +1315,7 @@ void clear_ble_identity_for_handle(hci_con_handle_t connection_handle) { slot.identity = controller_identity_global(); } } - critical_section_exit(&g_state_lock); + state_lock_exit(); } void clear_ble_identity_for_device(const uni_hid_device_t* device) { @@ -1354,7 +1390,7 @@ void apply_profile_lighting( bool lighting_target_is_current( uint8_t slot_index, uint32_t connection_generation, const uni_hid_device_t* device) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const bool current = device != nullptr && slot_index < kSlotCount && g_slots[slot_index].active && @@ -1362,7 +1398,7 @@ bool lighting_target_is_current( g_slots[slot_index].companion == device) && g_slots[slot_index].connection_generation == connection_generation; - critical_section_exit(&g_state_lock); + state_lock_exit(); return current; } @@ -1393,7 +1429,7 @@ Bluepad32ControllerLayout controller_layout(const BackendSlot& slot) { ConnectionStatus compute_connection_status() { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); bool all_ready = true; bool any_connecting = false; unsigned physical_count = 0; @@ -1413,7 +1449,7 @@ ConnectionStatus compute_connection_status() { } } #endif - critical_section_exit(&g_state_lock); + state_lock_exit(); if (all_ready && physical_count == kSlotCount) { return ConnectionStatus::Ready; @@ -1427,7 +1463,7 @@ ConnectionStatus compute_connection_status() { void publish_device_state(uint8_t slot, uni_hid_device_t* device, uint16_t pre_hotkey_button_mask, const ControllerState& state) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& target = g_slots[slot]; if (target.active && target.device == device) { target.state = state; @@ -1494,11 +1530,11 @@ void publish_device_state(uint8_t slot, uni_hid_device_t* device, g_macro_capture.observe(slot, target.connection_generation, time_us_32(), target.state); } - critical_section_exit(&g_state_lock); + state_lock_exit(); } void publish_all_neutral() { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); #ifdef SWITCH_PICO_WII_IR wii_ir_pointer_reset(); #endif @@ -1548,7 +1584,7 @@ void publish_all_neutral() { g_joycon_overrides[index] = {}; g_joycon_pair_hints[index] = {}; } - critical_section_exit(&g_state_lock); + state_lock_exit(); for (BleIdentityMapping& mapping : g_ble_identity_mappings) { mapping = {}; } @@ -1652,13 +1688,13 @@ bool wake_chord_rising_edge(uint8_t slot, uni_hid_device_t* device, logical_button_bit(ControllerProfileLogicalButton::kLeftShoulder) | logical_button_bit(ControllerProfileLogicalButton::kRightShoulder) | logical_button_bit(ControllerProfileLogicalButton::kSystem); - critical_section_enter_blocking(&g_state_lock); + 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; - critical_section_exit(&g_state_lock); + state_lock_exit(); return rising; } @@ -2114,12 +2150,12 @@ bool is_solo_wii_remote(const BackendSlot& slot) { HotkeyDecision update_controller_hotkeys( uint8_t slot_index, uni_hid_device_t* device) { HotkeyDecision decision{kDefaultMotionEnabled}; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const BackendSlot& slot = g_slots[slot_index]; if (slot.active && slot.device == device) { decision.motion_enabled = slot.motion_enabled; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return decision; } @@ -2248,10 +2284,10 @@ void handle_btstack_event(uint8_t packet_type, uint16_t channel, } bool update_pairing_window(uint32_t now_ms) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const bool requested = g_pairing_window_requested; g_pairing_window_requested = false; - critical_section_exit(&g_state_lock); + state_lock_exit(); if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) { return false; } @@ -2337,7 +2373,7 @@ void refresh_pairing_snapshot() { } } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); if (g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kFailed) { snapshot.status = Bluepad32PairingSnapshotStatus::kFailed; } @@ -2346,13 +2382,13 @@ void refresh_pairing_snapshot() { g_pairing_snapshot.completed_clear_pairings_token; g_pairing_snapshot = snapshot; g_pairing_snapshot_requested = false; - critical_section_exit(&g_state_lock); + state_lock_exit(); } void process_pairing_snapshot_request() { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const bool requested = g_pairing_snapshot_requested; - critical_section_exit(&g_state_lock); + state_lock_exit(); if (requested) { refresh_pairing_snapshot(); } @@ -2364,7 +2400,7 @@ void recompute_connection_status(); void process_clear_pairings(uint32_t now_ms) { uni_hid_device_t* devices[kSlotCount]{}; uint8_t device_count = 0; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const uint32_t request_token = g_clear_pairings_requested_token; if (request_token != 0) { @@ -2391,7 +2427,7 @@ void process_clear_pairings(uint32_t now_ms) { g_joycon_pair_hints[slot_index] = {}; } } - critical_section_exit(&g_state_lock); + state_lock_exit(); if (request_token == 0) { return; } @@ -2423,12 +2459,12 @@ void process_clear_pairings(uint32_t now_ms) { uni_bt_stop_scanning_unsafe(); uni_bt_allow_incoming_connections(false); g_connection_policy_state = ConnectionPolicyState::FailedClosed; - critical_section_enter_blocking(&g_state_lock); + 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; - critical_section_exit(&g_state_lock); + state_lock_exit(); return; } @@ -2440,11 +2476,11 @@ void process_clear_pairings(uint32_t now_ms) { g_connection_policy_state = ConnectionPolicyState::Uninitialized; } apply_connection_policy(); - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); g_pairing_snapshot.completed_clear_pairings_token = request_token; g_clear_pairings_in_progress_token = 0; - critical_section_exit(&g_state_lock); + state_lock_exit(); } @@ -2535,7 +2571,7 @@ void update_status_led() { const uint32_t now_ms = btstack_run_loop_get_time_ms(); bool profile_led_override = false; bool profile_led_on = false; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); for (const BackendSlot& slot : g_slots) { if (slot.profile_feedback.active && slot.profile_feedback.led_enabled) { @@ -2544,7 +2580,7 @@ void update_status_led() { profile_led_on || slot.profile_feedback.on; } } - critical_section_exit(&g_state_lock); + state_lock_exit(); bool led_on = false; if (profile_led_override) { @@ -2595,13 +2631,13 @@ void process_configuration_timer(btstack_timer_source_t* timer) { if ((previously_owned & (1u << i)) || !switch_native_output_owns(g_slots[i].device)) continue; RumbleEnvelope retained{}; - critical_section_enter_blocking(&g_state_lock); + 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(); - critical_section_exit(&g_state_lock); + state_lock_exit(); if (valid) switch_native_output_submit(i, retained.connection_generation, retained.received_us, retained.rumble, retained.duration_ms == kXInputHostRumbleDurationMs); @@ -2713,13 +2749,13 @@ bool prepare_wii_cue(uint8_t slot_index, uint32_t now_ms, void dispatch_wii_cue(const WiiCueDispatch& command) { if (command.device == nullptr) return; - critical_section_enter_blocking(&g_state_lock); + 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); - critical_section_exit(&g_state_lock); + 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 && @@ -2730,7 +2766,7 @@ void dispatch_wii_cue(const WiiCueDispatch& command) { command.device->report_parser.play_dual_rumble( command.device, 0, command.duration_ms, UINT8_MAX, UINT8_MAX); } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); WiiCue& cue = g_wii_cue; if (cue.token == command.token && cue.connection_generation == command.connection_generation) { cue.in_flight = false; @@ -2757,7 +2793,7 @@ void dispatch_wii_cue(const WiiCueDispatch& command) { } } } - critical_section_exit(&g_state_lock); + state_lock_exit(); } #endif @@ -2871,7 +2907,7 @@ void dispatch_native_cues(const NativeGamepadCueDispatch& command) { bool submitted[2]{}; uint32_t dispatch_ms = btstack_run_loop_get_time_ms(); for (uint8_t target = 0; target < (paired ? 2 : 1); ++target) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); bool current = slot.active && slot.device == command.device && slot.companion == command.companion && slot.connection_generation == command.connection_generation; @@ -2882,7 +2918,7 @@ void dispatch_native_cues(const NativeGamepadCueDispatch& command) { current &= cue.token == command.token[side] && cue.in_flight && cue.result != -1 && native_cue_current(cue); } - critical_section_exit(&g_state_lock); + 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(); @@ -2902,7 +2938,7 @@ void dispatch_native_cues(const NativeGamepadCueDispatch& command) { if (paired) submitted[target] = true; else submitted[0] = submitted[1] = true; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); if (slot.active && slot.device == command.device && slot.companion == command.companion) { // Retain each actual submission even if USB canceled/reselected @@ -2917,9 +2953,9 @@ void dispatch_native_cues(const NativeGamepadCueDispatch& command) { output.deadline_ms = output.owned ? command.prepared_ms + command.duration_ms : 0; } - critical_section_exit(&g_state_lock); + state_lock_exit(); } - critical_section_enter_blocking(&g_state_lock); + 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; @@ -2933,17 +2969,17 @@ void dispatch_native_cues(const NativeGamepadCueDispatch& command) { cue.active = cue.sample_id != 0; } } - critical_section_exit(&g_state_lock); + 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) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& slot = g_slots[slot_index]; if (!slot.wii_orientation_pending) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return; } const WiiOrientationRequest request = slot.pending_wii_orientation; @@ -2952,7 +2988,7 @@ void process_wii_orientation(uint8_t slot_index) { if (!is_solo_wii_remote(slot) || slot.connection_generation != request.connection_generation || !controller_identity_equal(slot.identity, request.identity)) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return; } uni_hid_device_t* device = slot.device; @@ -2966,7 +3002,7 @@ void process_wii_orientation(uint8_t slot_index) { #if SWITCH2_BRIDGE_FULL_INPUT refresh_native_source_locked(); #endif - critical_section_exit(&g_state_lock); + 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. @@ -3023,7 +3059,7 @@ void seed_native_host_rumble() { } g_seeded_native_run_id = native.run_id; RumbleEnvelope retained{}; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& slot = g_slots[native.slot]; const bool valid = slot.active && slot.device != nullptr && slot.retained_host_rumble_valid && @@ -3040,7 +3076,7 @@ void seed_native_host_rumble() { slot.pending_rumble = retained; slot.rumble_pending = true; } - critical_section_exit(&g_state_lock); + 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. @@ -3100,7 +3136,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) { NativeGamepadCueDispatch native_dispatch{}; #endif - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& slot = g_slots[slot_index]; if (slot.retained_host_rumble_valid && (!xinput_host_mode || @@ -3119,7 +3155,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) { // Fixture/startup/restoration exclusively own output. Preserve // stateful XInput requests until compatibility restoration ends. if (!xinput_host_mode) slot.rumble_pending = false; - critical_section_exit(&g_state_lock); + state_lock_exit(); continue; } #endif @@ -3265,7 +3301,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) { } companion = slot.companion; dispatch_generation = slot.connection_generation; - critical_section_exit(&g_state_lock); + 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. @@ -3451,7 +3487,7 @@ void stop_joycon_output(uni_hid_device_t* device) { bool merge_joycon_slots(int owner_index, int joining_index, uni_hid_device_t* joining_device, bool gesture = false) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& owner = g_slots[owner_index]; BackendSlot& joining = g_slots[joining_index]; uni_hid_device_t* const owner_device = owner.device; @@ -3471,7 +3507,7 @@ bool merge_joycon_slots(int owner_index, int joining_index, 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); - critical_section_exit(&g_state_lock); + state_lock_exit(); ControllerIdentity pair_identity{}; if (!eligible || !controller_identity_make_joycon_pair( @@ -3481,7 +3517,7 @@ bool merge_joycon_slots(int owner_index, int joining_index, return false; } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const bool still_admitted = gesture ? joycon_gesture_mature( owner_device, joining_device, true, btstack_run_loop_get_time_ms()) @@ -3494,7 +3530,7 @@ bool merge_joycon_slots(int owner_index, int joining_index, joining.active != joining_active || joining.companion != nullptr || joining.connection_generation != joining_generation || reserve_device_slot(joining_device) != joining_index) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return false; } invalidate_slot(owner); @@ -3538,14 +3574,14 @@ bool merge_joycon_slots(int owner_index, int joining_index, #if SWITCH2_BRIDGE_FULL_INPUT refresh_native_source_locked(); #endif - critical_section_exit(&g_state_lock); + state_lock_exit(); apply_slot_lighting(static_cast(owner_index), owner.device); apply_slot_lighting(static_cast(owner_index), owner.companion); return true; } bool split_joycon_slot(uint8_t owner_index, bool gesture = false) { - critical_section_enter_blocking(&g_state_lock); + 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 || @@ -3554,7 +3590,7 @@ bool split_joycon_slot(uint8_t owner_index, bool gesture = false) { (gesture && !joycon_gesture_mature( owner.device, owner.companion, false, btstack_run_loop_get_time_ms()))) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return false; } // Keep the pair's left member at its existing player index. Prefer the @@ -3570,7 +3606,7 @@ bool split_joycon_slot(uint8_t owner_index, bool gesture = false) { } } if (right_index < 0) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return false; } BackendSlot& right = g_slots[right_index]; @@ -3580,7 +3616,7 @@ bool split_joycon_slot(uint8_t owner_index, bool gesture = false) { // event has temporarily cleared a member's transport mapping. if (!controller_identity_joycon_pair_members( owner.identity, &left_identity, &right_identity)) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return false; } invalidate_slot(owner); @@ -3611,7 +3647,7 @@ bool split_joycon_slot(uint8_t owner_index, bool gesture = false) { #if SWITCH2_BRIDGE_FULL_INPUT refresh_native_source_locked(); #endif - critical_section_exit(&g_state_lock); + state_lock_exit(); apply_slot_lighting(owner_index, owner.device); apply_slot_lighting(static_cast(right_index), right.device); return true; @@ -3620,11 +3656,11 @@ bool split_joycon_slot(uint8_t owner_index, bool gesture = false) { void process_joycon_gestures(uint32_t now_ms) { for (uint8_t index = 0; index < kSlotCount; ++index) { now_ms = btstack_run_loop_get_time_ms(); - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); refresh_joycon_gestures(now_ms); const JoyConGesture& gesture = g_joycon_gestures[index]; if (gesture.blocked || joycon_side(gesture.device) >= 0) { - critical_section_exit(&g_state_lock); + state_lock_exit(); continue; } uni_hid_device_t* right = nullptr; @@ -3639,7 +3675,7 @@ void process_joycon_gestures(uint32_t now_ms) { const bool mature = joycon_gesture_mature( gesture.device, right, joining, now_ms); if (mature) block_joycon_gesture(gesture.participants); - critical_section_exit(&g_state_lock); + 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. @@ -3660,14 +3696,14 @@ void apply_joycon_configuration(const ConfigurationServiceSnapshot& configuratio if (configuration.state != ConfigurationServiceState::kReady) return; const JoyConMode requested = configuration.configuration.joycon_mode; if (requested != g_joycon_mode) { - critical_section_enter_blocking(&g_state_lock); + 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; - critical_section_exit(&g_state_lock); + state_lock_exit(); } if (!g_joycon_reconcile_requested) return; g_joycon_reconcile_requested = false; @@ -3678,13 +3714,13 @@ void apply_joycon_configuration(const ConfigurationServiceSnapshot& configuratio } else { uint8_t attempted = 0; for (uint8_t index = 0; index < kSlotCount; ++index) { - critical_section_enter_blocking(&g_state_lock); + 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))) { - critical_section_exit(&g_state_lock); + state_lock_exit(); continue; } int owner = index; @@ -3697,7 +3733,7 @@ void apply_joycon_configuration(const ConfigurationServiceSnapshot& configuratio } uni_hid_device_t* joining_device = g_slots[joining].device; attempted |= (1u << index) | (1u << partner); - critical_section_exit(&g_state_lock); + 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. @@ -3827,7 +3863,7 @@ void platform_on_device_connected(uni_hid_device_t* device) { uni_hid_device_disconnect(device); return; } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); g_retired_devices[pending_index] = nullptr; g_switch2_interval_requests[pending_index] = {}; if (slot_for_device(device) < 0) { @@ -3835,11 +3871,11 @@ void platform_on_device_connected(uni_hid_device_t* device) { reset_joycon_connection(device); } if (slot_for_device(device) < 0) g_native_pending_devices[pending_index] = device; - critical_section_exit(&g_state_lock); + state_lock_exit(); recompute_connection_status(); #else const ControllerIdentity connection_identity = identity_for_device(device); - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const int physical_index = physical_index_for_device(device); if (physical_index >= 0) { g_retired_devices[physical_index] = nullptr; @@ -3856,7 +3892,7 @@ void platform_on_device_connected(uni_hid_device_t* device) { reset_slot_hotkeys(slot); } } - critical_section_exit(&g_state_lock); + state_lock_exit(); if (slot_index >= 0) { recompute_connection_status(); } else { @@ -3869,11 +3905,11 @@ 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) { - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); if (g_native_pending_devices[pending_index] == device) g_native_pending_devices[pending_index] = nullptr; g_retired_devices[pending_index] = device; - critical_section_exit(&g_state_lock); + state_lock_exit(); } #endif const int slot_index = slot_for_device(device); @@ -3898,7 +3934,7 @@ void platform_on_device_disconnected(uni_hid_device_t* device) { #endif uni_hid_device_t* survivor = nullptr; ControllerIdentity survivor_identity{}; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& slot = g_slots[slot_index]; g_retired_devices[physical_index_for_device(device)] = device; reset_joycon_connection(device); @@ -3922,7 +3958,7 @@ void platform_on_device_disconnected(uni_hid_device_t* device) { #if SWITCH2_BRIDGE_FULL_INPUT refresh_native_source_locked(); #endif - critical_section_exit(&g_state_lock); + state_lock_exit(); clear_ble_identity_for_device(device); if (survivor != nullptr) { if (survivor->report_parser.play_dual_rumble != nullptr) { @@ -3955,16 +3991,16 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) { uni_hid_device_t* owner = device; uni_hid_device_t* companion = nullptr; ControllerIdentity connection_identity = identity_for_device(device); - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); #if SWITCH2_BRIDGE_FULL_INPUT if (!native_device_allowed(device)) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return UNI_ERROR_INVALID_CONTROLLER; } #endif int slot_index = reserve_device_slot(device); if (slot_index < 0) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return UNI_ERROR_NO_SLOTS; } #if SWITCH2_BRIDGE_FULL_INPUT @@ -3976,11 +4012,11 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) { if (!pending.active) { const int partner_index = joycon_partner_slot(device, slot_index); if (partner_index >= 0) { - critical_section_exit(&g_state_lock); + state_lock_exit(); if (!merge_joycon_slots(partner_index, slot_index, device)) { return UNI_ERROR_INIT_FAILED; } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); slot_index = partner_index; paired = true; } else { @@ -4002,7 +4038,7 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) { companion = current.companion; lighting_generation = current.connection_generation; connection_identity = current.identity; - critical_section_exit(&g_state_lock); + state_lock_exit(); if (became_active) { apply_radio_connection_policy(); #ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE @@ -4055,25 +4091,46 @@ void platform_on_controller_data(uni_hid_device_t* device, } __atomic_add_fetch(&g_controller_reports, 1, __ATOMIC_RELAXED); - critical_section_enter_blocking(&g_state_lock); + // 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) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return; } #if SWITCH2_BRIDGE_FULL_INPUT - const int physical_index = physical_index_for_device(device); - if (physical_index < 0) { - critical_section_exit(&g_state_lock); - return; - } - uni_native_motion_snapshot_t sensor{}; - uni_hid_parser_native_motion_snapshot(device, &sensor); 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)) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return; } ingress.report_sequence = sensor.report_sequence; @@ -4114,7 +4171,6 @@ void platform_on_controller_data(uni_hid_device_t* device, have_infrared ? &infrared : nullptr, time_us_32()); #endif #endif - const uint8_t extras = uni_hid_parser_switch2_extra_buttons(device); if (slot.companion == device) { slot.companion_gamepad = controller->gamepad; slot.companion_extra_buttons = extras; @@ -4123,15 +4179,13 @@ void platform_on_controller_data(uni_hid_device_t* device, slot.extra_buttons = extras; } observe_joycon_gesture( - device, controller->gamepad, btstack_run_loop_get_time_ms()); - if (device->controller_type == CONTROLLER_TYPE_WiiController) { - int32_t acceleration[3]; - uint32_t sequence; + 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 (!uni_hid_parser_wii_accel_snapshot(device, acceleration, &sequence)) { + if (!have_acceleration) { slot.accelerometer = {}; #ifdef SWITCH2_BRIDGE_WII_INPUT motion.accel_valid = false; @@ -4147,7 +4201,7 @@ void platform_on_controller_data(uni_hid_device_t* device, convert_accel(-static_cast(acceleration[2])), convert_accel(-static_cast(acceleration[0])), convert_accel(acceleration[1]), sequence, - btstack_run_loop_get_time_ms(), true}; + report_ms, true}; } #ifdef SWITCH2_BRIDGE_WII_INPUT if (slot.accelerometer.valid && sequence != motion.accel_sequence) { @@ -4156,33 +4210,32 @@ void platform_on_controller_data(uni_hid_device_t* device, motion.accel_valid = true; memcpy(motion.accel_q13, acceleration, sizeof(motion.accel_q13)); } - int32_t gyro[3]; - if (!uni_hid_parser_wii_gyro_snapshot(device, gyro, &sequence)) { + if (!have_gyro) { motion.gyro_valid = false; - } else if (sequence != motion.gyro_sequence) { - motion.gyro_sequence = sequence; + } 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 (!uni_hid_parser_wii_nunchuk_accel_snapshot(device, acceleration, &sequence)) { + if (!have_nunchuk_acceleration) { slot.nunchuk_accelerometer = {}; } else if ( #ifdef SWITCH2_BRIDGE_WII_INPUT - sequence != motion.nunchuk_sequence + nunchuk_sequence != motion.nunchuk_sequence #else !slot.nunchuk_accelerometer.valid || - sequence != slot.nunchuk_accelerometer.sequence + nunchuk_sequence != slot.nunchuk_accelerometer.sequence #endif ) { slot.nunchuk_accelerometer = { - convert_accel(-static_cast(acceleration[2])), - convert_accel(-static_cast(acceleration[0])), - convert_accel(acceleration[1]), sequence, - btstack_run_loop_get_time_ms(), true}; + convert_accel(-static_cast(nunchuk_acceleration[2])), + convert_accel(-static_cast(nunchuk_acceleration[0])), + convert_accel(nunchuk_acceleration[1]), nunchuk_sequence, + report_ms, true}; #ifdef SWITCH2_BRIDGE_WII_INPUT - motion.nunchuk_sequence = sequence; + motion.nunchuk_sequence = nunchuk_sequence; #endif } } @@ -4218,7 +4271,7 @@ void platform_on_controller_data(uni_hid_device_t* device, slot.companion == nullptr || slot.companion == device; const uint8_t merged_extras = slot.extra_buttons | slot.companion_extra_buttons; - critical_section_exit(&g_state_lock); + state_lock_exit(); const uint16_t pre_hotkey_button_mask = logical_button_mask(gamepad); if (wake_chord_rising_edge( static_cast(slot_index), owner, pre_hotkey_button_mask)) { @@ -4352,33 +4405,33 @@ extern "C" bool uni_platform_on_l2cap_can_send_now( bool bluepad32_input_backend_capture_start( uint8_t slot, uint32_t connection_generation, const CaptureOptions& options) { if (!g_initialized || slot >= kSlotCount) return false; - critical_section_enter_blocking(&g_state_lock); + 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); - critical_section_exit(&g_state_lock); + state_lock_exit(); return accepted; } bool bluepad32_input_backend_capture_stop(uint32_t run_id) { if (!g_initialized || run_id == 0) return false; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const bool matches = run_id == g_macro_capture.run_id(); if (matches) g_macro_capture.stop(time_us_32()); - critical_section_exit(&g_state_lock); + 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; - critical_section_enter_blocking(&g_state_lock); + 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()) { - critical_section_exit(&g_state_lock); + state_lock_exit(); return false; } *output = {}; @@ -4396,7 +4449,7 @@ bool bluepad32_input_backend_capture_page( for (uint8_t index = 0; index < output->event_count; ++index) { g_macro_capture.event(first_index + index, &output->events[index]); } - critical_section_exit(&g_state_lock); + state_lock_exit(); return true; } @@ -4405,7 +4458,11 @@ void bluepad32_input_backend_init() { 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 @@ -4515,16 +4572,16 @@ void bluepad32_input_backend_open_pairing_window() { bluepad32_input_backend_init(); } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); g_pairing_window_requested = true; - critical_section_exit(&g_state_lock); + state_lock_exit(); } uint32_t bluepad32_input_backend_clear_pairings() { if (!g_initialized) { bluepad32_input_backend_init(); } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); uint32_t request_token = g_clear_pairings_requested_token; if (request_token == 0) { request_token = g_clear_pairings_in_progress_token; @@ -4537,7 +4594,7 @@ uint32_t bluepad32_input_backend_clear_pairings() { g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kPending; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return request_token; } @@ -4546,12 +4603,12 @@ void bluepad32_input_backend_request_pairing_snapshot() { bluepad32_input_backend_init(); } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); g_pairing_snapshot_requested = true; if (g_pairing_snapshot.status != Bluepad32PairingSnapshotStatus::kFailed) { g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kPending; } - critical_section_exit(&g_state_lock); + state_lock_exit(); } void bluepad32_input_backend_pairing_snapshot( @@ -4563,9 +4620,9 @@ void bluepad32_input_backend_pairing_snapshot( bluepad32_input_backend_init(); } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); *out = g_pairing_snapshot; - critical_section_exit(&g_state_lock); + state_lock_exit(); } @@ -4593,7 +4650,7 @@ void bluepad32_input_backend_diagnostics( __atomic_load_n(&g_switch2_ingress_drops, __ATOMIC_RELAXED); out->switch2_output_drops = uni_hid_parser_switch2_haptics_dropped(); - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); for (const BackendSlot& slot : g_slots) { if (slot.active) { ++out->active_slots; @@ -4610,7 +4667,7 @@ void bluepad32_input_backend_diagnostics( ++out->rumble_pending_slots; } } - critical_section_exit(&g_state_lock); + state_lock_exit(); } void bluepad32_input_backend_snapshot(uint8_t slot_index, @@ -4623,7 +4680,7 @@ void bluepad32_input_backend_snapshot(uint8_t slot_index, return; } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const BackendSlot& slot = g_slots[slot_index]; out->active = slot.active; out->connection_generation = slot.connection_generation; @@ -4634,7 +4691,7 @@ void bluepad32_input_backend_snapshot(uint8_t slot_index, out->accelerometer = slot.accelerometer; out->nunchuk_accelerometer = slot.nunchuk_accelerometer; const uint32_t state_generation = slot.state_generation; - critical_section_exit(&g_state_lock); + state_lock_exit(); if (state_generation == g_consumed_generation[slot_index]) { out->state.motion_sample_count = 0; @@ -4645,21 +4702,21 @@ void bluepad32_input_backend_snapshot(uint8_t slot_index, #if SWITCH2_BRIDGE_FULL_INPUT void bluepad32_input_backend_select_native_source(const uint8_t address[6]) { if (!g_initialized) return; - critical_section_enter_blocking(&g_state_lock); + 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); - critical_section_exit(&g_state_lock); + state_lock_exit(); } void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* output) { if (output == nullptr) return; *output = {}; if (!g_initialized) return; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); *output = g_native_snapshot; - critical_section_exit(&g_state_lock); + state_lock_exit(); } bool bluepad32_input_backend_native_sample_request( @@ -4667,7 +4724,7 @@ bool bluepad32_input_backend_native_sample_request( if (token == nullptr) return false; *token = 0; if (!g_initialized || instance >= 2 || sample_id >= 8) return false; - critical_section_enter_blocking(&g_state_lock); + 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 && @@ -4683,13 +4740,13 @@ bool bluepad32_input_backend_native_sample_request( cue.result = 0; *token = cue.token; } - critical_section_exit(&g_state_lock); + 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; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); NativeGamepadCue& cue = g_native_cues[instance]; int result = -1; if (cue.token == token && !cue.consumed) { @@ -4700,15 +4757,15 @@ int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t toke result = cue.result; if (result != 0) cue.consumed = true; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return result; } void bluepad32_input_backend_native_sample_cancel(uint8_t instance) { if (!g_initialized || instance >= 2) return; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); cancel_native_cue_locked(g_native_cues[instance]); - critical_section_exit(&g_state_lock); + state_lock_exit(); } #endif @@ -4716,7 +4773,7 @@ void bluepad32_input_backend_native_sample_cancel(uint8_t instance) { 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; - critical_section_enter_blocking(&g_state_lock); + 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)); @@ -4727,28 +4784,28 @@ void bluepad32_input_backend_select_wii_source(const uint8_t address[6]) { // Lock order: backend exclusive, then pointer striped. wii_ir_pointer_reset(); #endif - critical_section_exit(&g_state_lock); + state_lock_exit(); } void bluepad32_input_backend_wii_snapshot(Bluepad32WiiBridgeSnapshot* output) { if (output == nullptr) return; *output = {}; if (!g_initialized) return; - critical_section_enter_blocking(&g_state_lock); + 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; } - critical_section_exit(&g_state_lock); + 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; - critical_section_enter_blocking(&g_state_lock); + 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. @@ -4771,13 +4828,13 @@ bool bluepad32_input_backend_wii_sample_request(uint8_t sample_id, uint64_t* tok break; } } - critical_section_exit(&g_state_lock); + state_lock_exit(); return accepted; } int bluepad32_input_backend_wii_sample_result(uint64_t token) { if (!g_initialized || token == 0) return -1; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); int result = -1; if (g_wii_cue.token == token && !g_wii_cue.consumed) { if (!wii_cue_target_current() || @@ -4789,15 +4846,15 @@ int bluepad32_input_backend_wii_sample_result(uint64_t token) { result = g_wii_cue.result; if (result != 0) g_wii_cue.consumed = true; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return result; } void bluepad32_input_backend_wii_sample_cancel() { if (!g_initialized) return; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); cancel_wii_cue_locked(); - critical_section_exit(&g_state_lock); + state_lock_exit(); } #endif @@ -4811,7 +4868,7 @@ void bluepad32_input_backend_playtest_snapshot( return; } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); const BackendSlot& slot = g_slots[slot_index]; out->active = slot.active; out->connection_generation = slot.connection_generation; @@ -4828,7 +4885,7 @@ void bluepad32_input_backend_playtest_snapshot( (slot.state.motion_sample_count != 0 ? 8u : 0u); out->controller_layout = controller_layout(slot); } - critical_section_exit(&g_state_lock); + state_lock_exit(); } bool bluepad32_input_backend_toggle_motion( @@ -4837,7 +4894,7 @@ bool bluepad32_input_backend_toggle_motion( return false; } bool toggled = false; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& slot = g_slots[slot_index]; if (slot.active && slot.connection_generation == connection_generation) { @@ -4868,7 +4925,7 @@ bool bluepad32_input_backend_toggle_motion( } toggled = true; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return toggled; } @@ -4892,7 +4949,7 @@ void bluepad32_input_backend_queue_rumble( #endif const uint16_t duration_ms = host_rumble_duration_ms(); const uint32_t received_ms = btstack_run_loop_get_time_ms(); - critical_section_enter_blocking(&g_state_lock); + 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)) { @@ -4922,7 +4979,7 @@ void bluepad32_input_backend_queue_rumble( command.generation = ingress.generation; ++ingress.count; __atomic_add_fetch(&g_host_rumble_requests, 1, __ATOMIC_RELAXED); - critical_section_exit(&g_state_lock); + state_lock_exit(); return; } #if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE) @@ -4948,7 +5005,7 @@ void bluepad32_input_backend_queue_rumble( slot.retained_host_rumble_valid = false; } } - critical_section_exit(&g_state_lock); + state_lock_exit(); #ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE if (native_candidate) switch_native_output_submit(slot_index, native_generation, received_us, rumble, @@ -4976,7 +5033,7 @@ bool bluepad32_input_backend_set_wii_orientation( return false; } bool queued = false; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); for (BackendSlot& slot : g_slots) { if (!is_solo_wii_remote(slot) || slot.connection_generation != connection_generation || @@ -4988,7 +5045,7 @@ bool bluepad32_input_backend_set_wii_orientation( queued = true; break; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return queued; } @@ -4999,7 +5056,7 @@ bool bluepad32_input_backend_identify( return false; } bool queued = false; - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); for (BackendSlot& slot : g_slots) { if (!slot.active || !controller_identity_equal(slot.identity, identity)) { @@ -5012,7 +5069,7 @@ bool bluepad32_input_backend_identify( queued = true; break; } - critical_section_exit(&g_state_lock); + state_lock_exit(); return queued; } @@ -5028,7 +5085,7 @@ void bluepad32_input_backend_queue_profile_feedback( return; } - critical_section_enter_blocking(&g_state_lock); + state_lock_enter(); BackendSlot& slot = g_slots[slot_index]; if (slot.active && slot.device != nullptr && slot.connection_generation == connection_generation) { @@ -5036,5 +5093,5 @@ void bluepad32_input_backend_queue_profile_feedback( connection_generation, active_profile_number, policy}; queue_profile_feedback(slot, feedback); } - critical_section_exit(&g_state_lock); + state_lock_exit(); } diff --git a/src/firmware/platform/pico/bootsel_pairing_button.cpp b/src/firmware/platform/pico/bootsel_pairing_button.cpp index ef60cb1..c37d532 100644 --- a/src/firmware/platform/pico/bootsel_pairing_button.cpp +++ b/src/firmware/platform/pico/bootsel_pairing_button.cpp @@ -8,6 +8,9 @@ #if PICO_RP2350 #include "hardware/regs/sio.h" #endif +#if SWITCH2_PROBE_HUB +#include "usb/native_hub/native_hub.h" +#endif namespace { @@ -34,6 +37,12 @@ void __no_inline_not_in_flash_func(read_bootsel_callback)(void* parameter) { IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS); for (volatile uint32_t delay = 0; delay < 1000; ++delay) { +#if SWITCH2_PROBE_HUB + // Keep flash safety and the original settling delay, but do not leave + // USB completions pending throughout it. This helper is SRAM-only and + // dispatches no protocol/Bluetooth callbacks. + native_hub_service_pending_usb(); +#endif } #if PICO_RP2350 diff --git a/src/firmware/usb/native_hub/native_hub.c b/src/firmware/usb/native_hub/native_hub.c index e2bae45..56f99a0 100644 --- a/src/firmware/usb/native_hub/native_hub.c +++ b/src/firmware/usb/native_hub/native_hub.c @@ -101,6 +101,69 @@ static uint32_t token_hits[DEVICES], missed_lock, blocked_buffers, blocked_sie, static uint16_t root_string[64]; static char root_serial[48]; +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) +#define OUT_TRACE_SLOTS 65u +#define OUT_TRACE_RETAIN 64u +#define OUT_TRACE_STALL_US 200000u +#define OUT_TRACE_LINE_US 50000u + +enum { + OUT_TRACE_LOCK = 1u, + OUT_TRACE_BUFFERS = 2u, + OUT_TRACE_SETUP = 4u, + OUT_TRACE_GUARD = 8u, + OUT_TRACE_INVALID = 16u, +}; +typedef struct { + uint32_t cursor, cutoff, clock_before, clock_after, sof; + uint32_t address_before, address_after, in0, out0, buffers, sie, sm, ints; + uint32_t blocked_buffers, blocked_sie, missed_lock, irq_enter, irq_exit, core0_phase; + uint32_t ep0_word; + uint8_t address, owner, owner_before, owner_after, selected, reason, pid, before_valid; +} out_trace_record_t; +typedef struct { + uint32_t time_us, quiet_us, cursor, count, input[2], cycle, sof; + uint32_t address, owner, out0, buffers, sie, sm, ints, intr, inte; + uint32_t tx_error, rx_error, irq_enter, irq_exit, core0_phase; + uint32_t log_max_us, log_max_us_bytes, log_max_bytes, log_nested; + uint32_t control_slot, control_generation, control_stage, control_position, control_length; + uint32_t control_word, in0, ep0_word; + tusb_control_request_t control_request; +} out_trace_header_t; + +// All producer state is ordinary SRAM/BSS. Only Core1 writes the records. +// Cursor low 7 bits name the NEXT slot (0..64); upper bits count ring laps. +// Skipping unused low-bit values keeps each publication monotonic without a +// division on Core1, and slot adjacency survives uint32_t generation rollover. +// Freeze and cursor accesses below are SC (publication includes release, +// snapshot includes acquire). In their common total order, a producer's final +// freeze=false load precedes Core0's freeze=true store. Every earlier record +// has already been published, and at most that one iteration can still write. +// Core0 therefore reads only the 64 slots BEFORE its acquired cursor, never +// the possible in-flight 65th slot. Ordinary record reads/writes cannot race. +// No rearm occurs until the dump reader has finished; cursors are never reset. +static out_trace_record_t out_trace_records[OUT_TRACE_SLOTS]; +static uint32_t out_trace_cursor, out_trace_frozen; +static uint32_t out_trace_irq_enter, out_trace_irq_exit; +static uint32_t out_trace_last_missed_lock; // Core1, updated after every rejected selection. +static uint32_t out_trace_core0_phase; + +// Core0 alone owns logger metrics and dump/IN-progress state. IRQ code never +// logs, and Core1 does not read these fields, so they need no cross-core atomics. +static uint32_t out_trace_log_max_us, out_trace_log_max_us_bytes; +static uint32_t out_trace_log_max_bytes, out_trace_log_nested; +static out_trace_header_t out_trace_header; +static uint32_t out_trace_input[2], out_trace_last_input_us, out_trace_last_line_us; +static uint32_t out_trace_dump_line, out_trace_dump_slot; +static bool out_trace_seen_input, out_trace_dumping; +typedef struct { + uint32_t generation, since_us; + stage_t stage; + uint16_t position; +} control_trace_watch_t; +static control_trace_watch_t out_trace_controls[DEVICES]; +#endif + static const uint8_t hub_device[] = { 18,1,0x10,1,9,0,0,64,0x7e,5,0x68,0x20,0,1,1,2,3,1 }; @@ -220,6 +283,116 @@ bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owne spin_unlock_unsafe(bank_lock); return true; } + +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) +static __force_inline out_trace_record_t* out_trace_begin_record(void) { + if (__atomic_load_n(&out_trace_frozen,__ATOMIC_SEQ_CST)) return NULL; + const uint32_t cursor = __atomic_load_n(&out_trace_cursor,__ATOMIC_SEQ_CST); + out_trace_record_t* record = &out_trace_records[cursor & 127u]; + record->cursor = cursor; + return record; +} + +static __force_inline void out_trace_finish_record(out_trace_record_t* record) { + // Post-decision observations are sequential, NOT an atomic hardware image. + // Never inspect unlocked software control stages or shadow banks. + record->address_after = usb_hw->dev_addr_ctrl; + record->owner_after = active_device; + record->sof = usb_hw->sof_rd; + record->in0 = usb_dpram->ep_buf_ctrl[0].in; + record->out0 = usb_dpram->ep_buf_ctrl[0].out; + record->buffers = usb_hw->buf_status; + record->sie = usb_hw->sie_status; + record->sm = usb_hw->sm_state; + record->ints = usb_hw->ints; + record->missed_lock = missed_lock; + record->core0_phase = __atomic_load_n(&out_trace_core0_phase,__ATOMIC_ACQUIRE); + record->irq_enter = __atomic_load_n(&out_trace_irq_enter,__ATOMIC_ACQUIRE); + record->irq_exit = __atomic_load_n(&out_trace_irq_exit,__ATOMIC_ACQUIRE); + record->ep0_word = *(const volatile uint32_t*)usb_dpram->ep0_buf_a; + const uint32_t cursor = record->cursor; + const uint32_t next = (cursor & 127u) == OUT_TRACE_SLOTS-1u ? cursor+64u : cursor+1u; + __atomic_store_n(&out_trace_cursor,next,__ATOMIC_SEQ_CST); +} + +bool __no_inline_not_in_flash_func(native_hub_select_device_traced)( + uint8_t address, uint8_t owner, uint32_t cutoff, uint8_t pid) { + if (__atomic_load_n(&out_trace_frozen,__ATOMIC_SEQ_CST)) + return native_hub_select_device(address,owner,cutoff); + const uint32_t clock_before = sio_hw->mtime; + const uint32_t address_before = usb_hw->dev_addr_ctrl; + const uint8_t owner_before = active_device; + const bool selected = native_hub_select_device(address,owner,cutoff); + const uint32_t clock_after = sio_hw->mtime; + // The router records all failures through the post-rejection hook below. + if (!selected) return false; + out_trace_record_t* record = out_trace_begin_record(); + if (!record) return true; + record->cutoff = cutoff; + record->clock_before = clock_before; + record->clock_after = clock_after; + record->address = address; + record->owner = owner; + record->address_before = address_before; + record->owner_before = owner_before; + record->selected = true; + record->reason = 0; + record->pid = pid; + record->before_valid = true; + record->blocked_buffers = record->blocked_sie = 0; + out_trace_finish_record(record); + return true; +} + +void __no_inline_not_in_flash_func(native_hub_note_failed_select)( + uint8_t address, uint8_t owner, uint32_t cutoff, uint8_t pid) { + // Every rejection reaches this hook, even while frozen. A changed lock + // counter therefore identifies THIS attempt, not an older rejected token. + const bool lock_failed = missed_lock != out_trace_last_missed_lock; + out_trace_last_missed_lock = missed_lock; + const uint32_t clock_after = sio_hw->mtime; + out_trace_record_t* record = out_trace_begin_record(); + if (!record) return; + record->cutoff = cutoff; + record->clock_before = 0; + record->clock_after = clock_after; + record->address = address; + record->owner = owner; + record->address_before = 0; + record->owner_before = NONE; + record->selected = false; + record->pid = pid; + record->before_valid = false; // No added work before normal IN/SETUP selection. + record->blocked_buffers = record->blocked_sie = 0; + if (lock_failed) record->reason = OUT_TRACE_LOCK; + else if (owner >= DEVICES || bank_lock == NULL) record->reason = OUT_TRACE_INVALID; + else { + record->reason = OUT_TRACE_GUARD; + record->blocked_buffers = blocked_buffers; + record->blocked_sie = blocked_sie; + if (blocked_buffers) record->reason |= OUT_TRACE_BUFFERS; + if (blocked_sie & USB_SIE_STATUS_SETUP_REC_BITS) record->reason |= OUT_TRACE_SETUP; + } + out_trace_finish_record(record); +} + +uint32_t __no_inline_not_in_flash_func(native_hub_trace_phase)(uint32_t phase) { + // All setters run on Core0; IRQ instrumentation does not modify this tag. + const uint32_t previous = __atomic_load_n(&out_trace_core0_phase,__ATOMIC_RELAXED); + __atomic_store_n(&out_trace_core0_phase,phase,__ATOMIC_RELEASE); + return previous; +} + +void __no_inline_not_in_flash_func(native_hub_note_log_mask)( + uint32_t elapsed_us, uint32_t bytes, bool already_masked) { + if (elapsed_us > out_trace_log_max_us) { + out_trace_log_max_us = elapsed_us; + out_trace_log_max_us_bytes = bytes; + } + if (bytes > out_trace_log_max_bytes) out_trace_log_max_bytes = bytes; + if (already_masked) ++out_trace_log_nested; +} +#endif // Section placement alone permits inlining into the flash-backed task. Keep // bank-lock ownership independent of XIP instruction-cache refill latency. static void __no_inline_not_in_flash_func(restore_selected_bank)(void) { @@ -268,7 +441,12 @@ static __force_inline bool push_event(uint8_t device, uint8_t channel, uint8_t k devices[device].endpoint_generation[channel]) : 0; event->reset_generation = device < DEVICES ? devices[device].reset_generation : 0; if (kind == 2 && (channel & 1u) && channel != 1) copy_from_usb(event->data,data,length); - else if (length) memcpy(event->data,data,length); + else { + // SRAM sources may be unaligned. Volatile byte reads keep this copy + // inline instead of calling flash-backed memcpy during BOOTSEL sampling. + const volatile uint8_t* source = data; + for (uint16_t i = 0; i < length; ++i) event->data[i] = source[i]; + } __dmb(); event_head = next; return true; } @@ -370,6 +548,26 @@ static void __not_in_flash_func(usb_interrupt)(void) { spin_unlock(bank_lock, flags); } +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) +static void __no_inline_not_in_flash_func(usb_interrupt_traced)(void) { + __atomic_store_n(&out_trace_irq_enter,sio_hw->mtime,__ATOMIC_RELEASE); + usb_interrupt(); + __atomic_store_n(&out_trace_irq_exit,sio_hw->mtime,__ATOMIC_RELEASE); +} +#endif + +void __no_inline_not_in_flash_func(native_hub_service_pending_usb)(void) { + // BOOTSEL sampling keeps IRQs disabled while QSPI CSn is floated. Drain + // real hardware completions so Core1 can route the next device's token. + // Do not touch NVIC pending state: a later IRQ safely observes cleared flags. + if (!started || !usb_hw->ints) return; +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + usb_interrupt_traced(); +#else + usb_interrupt(); +#endif +} + static void stall(uint8_t slot) { uint32_t flags = spin_lock_blocking(bank_lock); if (devices[slot].control.generation != devices[slot].generation) { @@ -882,7 +1080,11 @@ bool native_hub_init(void) { while ((int32_t)(time_us_32()-deadline) < 0); if (!observer.ready) return false; publish_addresses(); probe_router_enable(true); +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + irq_set_exclusive_handler(USBCTRL_IRQ,usb_interrupt_traced); +#else irq_set_exclusive_handler(USBCTRL_IRQ,usb_interrupt); +#endif irq_set_priority(USBCTRL_IRQ,0); irq_set_enabled(USBCTRL_IRQ,true); hw_set_bits(&usb_hw->sie_ctrl,USB_SIE_CTRL_PULLUP_EN_BITS); @@ -890,6 +1092,166 @@ bool native_hub_init(void) { probe_debug_printf("[NATIVE_HUB] stock USB, SIO phase=%u, 240MHz; hub2068 R2066 L2067; isolated EP0/1/2 banks\n",NATIVE_HUB_SAMPLE_PHASE); return true; } + +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) +static void out_trace_freeze(uint32_t now, uint8_t control_slot) { + __atomic_store_n(&out_trace_frozen,1u,__ATOMIC_SEQ_CST); + out_trace_header_t* h = &out_trace_header; + h->cursor = __atomic_load_n(&out_trace_cursor,__ATOMIC_SEQ_CST); + h->count = h->cursor < OUT_TRACE_RETAIN ? h->cursor : OUT_TRACE_RETAIN; + h->time_us = now; + h->quiet_us = now - (control_slot < DEVICES + ? out_trace_controls[control_slot].since_us : out_trace_last_input_us); + h->input[0] = input_count[1]; + h->input[1] = input_count[2]; + h->cycle = sio_hw->mtime; + h->sof = usb_hw->sof_rd; + h->address = usb_hw->dev_addr_ctrl; + h->owner = active_device; + h->out0 = usb_dpram->ep_buf_ctrl[0].out; + h->buffers = usb_hw->buf_status; + h->sie = usb_hw->sie_status; + h->sm = usb_hw->sm_state; + h->ints = usb_hw->ints; + h->intr = usb_hw->intr; + h->inte = usb_hw->inte; + h->tx_error = usb_hw->ep_tx_error; + h->rx_error = usb_hw->ep_rx_error; + h->irq_enter = __atomic_load_n(&out_trace_irq_enter,__ATOMIC_ACQUIRE); + h->irq_exit = __atomic_load_n(&out_trace_irq_exit,__ATOMIC_ACQUIRE); + h->core0_phase = __atomic_load_n(&out_trace_core0_phase,__ATOMIC_ACQUIRE); + h->log_max_us = out_trace_log_max_us; + h->log_max_us_bytes = out_trace_log_max_us_bytes; + h->log_max_bytes = out_trace_log_max_bytes; + h->log_nested = out_trace_log_nested; + h->in0 = usb_dpram->ep_buf_ctrl[0].in; + h->ep0_word = *(const volatile uint32_t*)usb_dpram->ep0_buf_a; + h->control_slot = control_slot; + h->control_generation = h->control_stage = h->control_position = h->control_length = 0; + h->control_word = 0; + memset(&h->control_request,0,sizeof(h->control_request)); + if (control_slot < DEVICES) { + const control_t* c = &devices[control_slot].control; + h->control_generation = c->generation; + h->control_stage = c->stage; + h->control_position = c->position; + h->control_length = c->length; + h->control_request = c->request; + if (c->position < c->length) { + unsigned length = c->length-c->position; + if (length > sizeof(h->control_word)) length = sizeof(h->control_word); + memcpy(&h->control_word,c->data+c->position,length); + } + } + // Snapshot EVERYTHING printed in the header before enqueueing its first + // byte. Later lines must not accidentally describe the act of dumping. + out_trace_dump_slot = (h->cursor & 127u)+OUT_TRACE_SLOTS-h->count; + if (out_trace_dump_slot >= OUT_TRACE_SLOTS) out_trace_dump_slot -= OUT_TRACE_SLOTS; + out_trace_dump_line = 0; + out_trace_last_line_us = now-OUT_TRACE_LINE_US; + out_trace_dumping = true; +} + +static bool out_trace_task(uint32_t now) { + const bool progress = input_count[1] != out_trace_input[0] || + input_count[2] != out_trace_input[1]; + if (progress) { + out_trace_input[0] = input_count[1]; + out_trace_input[1] = input_count[2]; + out_trace_last_input_us = now; + out_trace_seen_input = true; + } + bool control_progress = false; + uint8_t stalled_control = NONE; + for (uint8_t slot = 0; slot < DEVICES; ++slot) { + const control_t* c = &devices[slot].control; + control_trace_watch_t* watch = &out_trace_controls[slot]; + const bool pending = c->stage == DATA_IN || c->stage == DATA_OUT || + c->stage == STATUS_IN || c->stage == STATUS_OUT; + const bool changed = c->generation != watch->generation || + c->stage != watch->stage || c->position != watch->position; + if (changed || !pending) { + watch->since_us = now; + watch->generation = c->generation; + watch->stage = c->stage; + watch->position = c->position; + } + control_progress |= changed; + if (pending && (uint32_t)(now-watch->since_us) >= OUT_TRACE_STALL_US && + stalled_control == NONE) stalled_control = slot; + } + if (__atomic_load_n(&out_trace_frozen,__ATOMIC_SEQ_CST)) { + if (!out_trace_dumping) { + // A fresh completion after the dump permits diagnostic rearm. + // Never reset the producer cursor or touch controller/protocol state. + if (progress || control_progress) __atomic_store_n(&out_trace_frozen,0u,__ATOMIC_SEQ_CST); + return false; + } + } else if (stalled_control != NONE || (out_trace_seen_input && + (uint32_t)(now-out_trace_last_input_us) >= OUT_TRACE_STALL_US)) { + out_trace_freeze(now,stalled_control); + } else return false; + + // At most one <512-byte logger line per 50ms, below UART line capacity; + // no ring copying, waiting for Core1, masking, or formatting on Core1/IRQ. + if ((uint32_t)(now-out_trace_last_line_us) < OUT_TRACE_LINE_US) return true; + out_trace_last_line_us = now; + const out_trace_header_t* h = &out_trace_header; + if (out_trace_dump_line == 0) { + probe_debug_printf("[HUB_FLIGHT_FREEZE] us=%"PRIu32" quiet=%"PRIu32 + " next=%08"PRIx32" n=%"PRIu32" in=%"PRIu32"/%"PRIu32 + " cycle=%08"PRIx32" sof=%08"PRIx32" addr=%08"PRIx32" owner=%"PRIu32 + " out0=%08"PRIx32" bs=%08"PRIx32" sie=%08"PRIx32" sm=%08"PRIx32 + " ints=%08"PRIx32" intr=%08"PRIx32" inte=%08"PRIx32"\n", + h->time_us,h->quiet_us,h->cursor,h->count,h->input[0],h->input[1], + h->cycle,h->sof,h->address,h->owner,h->out0,h->buffers,h->sie,h->sm, + h->ints,h->intr,h->inte); + } else if (out_trace_dump_line == 1) { + probe_debug_printf("[HUB_FLIGHT_CONTEXT] next=%08"PRIx32" irq=%08"PRIx32"/%08"PRIx32 + " phase=%08"PRIx32" txerr=%08"PRIx32" rxerr=%08"PRIx32" log_us=%"PRIu32 + " bytes_at_max=%"PRIu32" max_bytes=%"PRIu32" nested=%"PRIu32"\n", + h->cursor,h->irq_enter,h->irq_exit,h->core0_phase,h->tx_error,h->rx_error, + h->log_max_us,h->log_max_us_bytes,h->log_max_bytes,h->log_nested); + } else if (out_trace_dump_line == 2) { + probe_debug_printf("[HUB_FLIGHT_CONTROL] slot=%"PRIu32" gen=%"PRIu32 + " stage=%"PRIu32" pos=%"PRIu32"/%"PRIu32 + " setup=%02x/%02x v=%04x i=%04x n=%u" + " expected=%08"PRIx32" ep0=%08"PRIx32" in0=%08"PRIx32"\n", + h->control_slot,h->control_generation,h->control_stage, + h->control_position,h->control_length, + h->control_request.bmRequestType,h->control_request.bRequest, + h->control_request.wValue,h->control_request.wIndex,h->control_request.wLength, + h->control_word,h->ep0_word,h->in0); + } else if (out_trace_dump_line < h->count+3u) { + const out_trace_record_t* r = &out_trace_records[out_trace_dump_slot]; + // Hex except owner/ok/lock; req=address/owner, addr/owner=before/after. + // pre=0 means before-clock/address/owner are unavailable; all HW/IRQ fields are POST. + // reason bits: 01 lock, 02 saved BUF_STATUS, 04 saved SETUP_REC, + // 08 original guard, 10 invalid selector arguments; 00 means selected. + // Guard snapshots can race hardware; absence of 02/04 alone does not + // prove cutoff was the cause. Keep raw cutoff and clocks for analysis. + probe_debug_printf("[HUB_FLIGHT] n=%08"PRIx32" pid=%02x pre=%u cutoff=%08"PRIx32 + " clock=%08"PRIx32"/%08"PRIx32" sof=%08"PRIx32 + " req=%02x/%u addr=%08"PRIx32"/%08"PRIx32" owner=%u/%u ok=%u why=%02x" + " in0=%08"PRIx32" out0=%08"PRIx32" bs=%08"PRIx32" sie=%08"PRIx32" sm=%08"PRIx32 + " ints=%08"PRIx32" block=%08"PRIx32"/%08"PRIx32 + " lock=%"PRIu32" irq=%08"PRIx32"/%08"PRIx32" phase=%08"PRIx32 + " ep0=%08"PRIx32"\n", + r->cursor,r->pid,r->before_valid,r->cutoff,r->clock_before,r->clock_after,r->sof, + r->address,r->owner,r->address_before,r->address_after, + r->owner_before,r->owner_after,r->selected,r->reason, + r->in0,r->out0,r->buffers,r->sie,r->sm,r->ints,r->blocked_buffers, + r->blocked_sie,r->missed_lock,r->irq_enter,r->irq_exit,r->core0_phase,r->ep0_word); + if (++out_trace_dump_slot == OUT_TRACE_SLOTS) out_trace_dump_slot = 0; + } else { + probe_debug_printf("[HUB_FLIGHT_END] next=%08"PRIx32" n=%"PRIu32"\n",h->cursor,h->count); + out_trace_dumping = false; + } + ++out_trace_dump_line; + return true; +} +#endif + void native_hub_task(void) { if (!started) return; if (failed) { @@ -926,6 +1288,9 @@ void native_hub_task(void) { } } static uint32_t last_log; +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + if (out_trace_task(now)) last_log = now; +#endif if ((uint32_t)(now-last_log) >= 1000000u) { last_log = now; probe_debug_printf("[NATIVE_HUB] addr=%u/%u/%u cfg=%u/%u/%u setup=%"PRIu32"/%"PRIu32"/%"PRIu32 diff --git a/src/firmware/usb/native_hub/native_hub.h b/src/firmware/usb/native_hub/native_hub.h index f2fdd78..05b8fbc 100644 --- a/src/firmware/usb/native_hub/native_hub.h +++ b/src/firmware/usb/native_hub/native_hub.h @@ -3,6 +3,7 @@ #include #include #include "tusb.h" +#include "native_hub_trace.h" #ifdef __cplusplus extern "C" { @@ -15,6 +16,9 @@ extern "C" { void native_hub_startup_guard(void); bool native_hub_init(void); void native_hub_task(void); +// Core0, with IRQs disabled and bank_lock not held: service hardware status +// while flash is unavailable. SRAM-only; queues events, never runs callbacks. +void native_hub_service_pending_usb(void); bool native_hub_mounted(uint8_t instance); bool native_hub_suspended(uint8_t instance); bool native_hub_hid_ready(uint8_t instance); @@ -31,6 +35,7 @@ bool native_hub_control_xfer(uint8_t device_slot, bool native_hub_control_status(uint8_t device_slot, const tusb_control_request_t* request); + // Supplied by the existing native Joy-Con protocol engine. Each returned // descriptor is the standalone model, with interfaces 0/1 and EPs 1/2. const uint8_t* native_joycon_device_descriptor(uint8_t instance); diff --git a/src/firmware/usb/native_hub/native_hub_trace.h b/src/firmware/usb/native_hub/native_hub_trace.h new file mode 100644 index 0000000..c09a6be --- /dev/null +++ b/src/firmware/usb/native_hub/native_hub_trace.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) +#ifdef __cplusplus +extern "C" { +#endif + +// Core1: selected token wrapper; the original selector owns every hardware decision. +bool native_hub_select_device_traced(uint8_t address, uint8_t owner, uint32_t cutoff, uint8_t pid); +// Core1: call after EVERY failed selection, including OUT and while frozen. +// Observes the completed decision; never retries or changes the bank. +void native_hub_note_failed_select(uint8_t address, uint8_t owner, uint32_t cutoff, uint8_t pid); +// Core0: call only AFTER restoring the logger's saved interrupt state. +void native_hub_note_log_mask(uint32_t elapsed_us, uint32_t bytes, bool already_masked); +// Core0 execution tags. Backend lock tags include the source line. +enum { + NATIVE_HUB_TRACE_PHASE_NONE = 0, + NATIVE_HUB_TRACE_PHASE_LOG_COPY = 1, + NATIVE_HUB_TRACE_PHASE_RADIO_POLL = 2, + NATIVE_HUB_TRACE_PHASE_USB_TASK = 3, + NATIVE_HUB_TRACE_PHASE_LOG_DRAIN = 4, + NATIVE_HUB_TRACE_PHASE_PROTOCOL = 5, + NATIVE_HUB_TRACE_PHASE_SLEEP = 6, + NATIVE_HUB_TRACE_PHASE_BACKEND_LOCK = 0x10000, +}; +uint32_t native_hub_trace_phase(uint32_t phase); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/firmware/usb/usb_configuration_management.cpp b/src/firmware/usb/usb_configuration_management.cpp index 5baf453..8e8f118 100644 --- a/src/firmware/usb/usb_configuration_management.cpp +++ b/src/firmware/usb/usb_configuration_management.cpp @@ -192,7 +192,11 @@ size_t encode_transaction(uint8_t* output, size_t output_size) { size_t encode_info(uint8_t* output, size_t output_size) { uint8_t payload[8] = { #if SWITCH2_PROBE_HUB - 0, 78, 0, 2, +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + 0, 87, 0, 2, +#else + 0, 86, 0, 2, +#endif kNativeHubActiveMode, USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE | USB_OUTPUT_CAPABILITY_MOTION, diff --git a/tests/native_hub_management_test.cpp b/tests/native_hub_management_test.cpp index be4946f..0cb384a 100644 --- a/tests/native_hub_management_test.cpp +++ b/tests/native_hub_management_test.cpp @@ -20,6 +20,7 @@ void native_test_bus_reset(bool); void native_test_hold_abort(bool); bool native_test_select(uint8_t); bool native_test_private_in(uint8_t, uint8_t, uint8_t*, uint16_t*); +extern uint32_t native_test_interrupt_mask; } namespace { @@ -332,6 +333,44 @@ void test_private_transmit_survives_round_robin_tokens() { native_test_drain(); native_test_initialize(); } +void test_masked_irq_completion_handoff() { + native_test_initialize(); + tusb_control_request_t configuration{}; + configuration.bRequest = TUSB_REQ_SET_CONFIGURATION; + configuration.wValue = 1; + for (uint8_t slot : {1, 2}) { + require(native_test_setup(slot, &configuration, true), "child configuration failed"); + acknowledge(slot); + } + const uint8_t payloads[2][3] = {{0x12, 0x34, 0x56}, {0x78, 0x9a, 0xbc}}; + for (uint8_t instance : {0, 1}) + require(native_hub_hid_report(instance, 8, payloads[instance], 3), + "could not queue masked-window HID packet"); + uint8_t packet[64]; + uint16_t length = 0; + native_test_interrupt_mask = 1; + require(native_test_private_in(1, 0x81, packet, &length), + "first controller did not complete during masked window"); + require(!native_test_select(2), + "pending completion must prevent overwriting the active bank"); + native_hub_service_pending_usb(); + require(native_test_interrupt_mask == 1, + "SRAM service must preserve the caller's interrupt mask"); + require(native_test_private_in(2, 0x81, packet, &length) && length == 4 && + packet[0] == 8 && std::memcmp(packet + 1, payloads[1], 3) == 0, + "SRAM service did not permit the other controller's real packet"); + native_hub_service_pending_usb(); + require(!native_hub_hid_ready(0) && !native_hub_hid_ready(1), + "SRAM service must defer protocol callbacks to foreground dispatch"); + native_test_interrupt_mask = 0; + native_test_drain(); + require(native_hub_hid_ready(0) && native_hub_hid_ready(1), + "deferred completions did not release both controller queues"); + require(!native_test_private_in(1, 0x81, packet, &length), + "later IRQ dispatch duplicated a serviced completion"); + native_test_initialize(); +} + void test_private_bootsel() { const auto bytes = envelope(Operation::kBootselReboot, {}); @@ -430,6 +469,7 @@ int main() { test_pending_control_buffer_ownership(); test_read_ack_allows_usb_progress(); test_private_transmit_survives_round_robin_tokens(); + test_masked_irq_completion_handoff(); test_private_bootsel(); std::cout << "native root management packet and persistence regressions passed\n"; } diff --git a/tests/native_hub_stubs/hardware_stub.h b/tests/native_hub_stubs/hardware_stub.h index 62325f1..c822a8a 100644 --- a/tests/native_hub_stubs/hardware_stub.h +++ b/tests/native_hub_stubs/hardware_stub.h @@ -41,6 +41,7 @@ typedef struct { volatile uint32_t pwr, main_ctrl, sie_ctrl, ep_nak_stall_status; volatile uint32_t ep_tx_error, ep_rx_error; volatile uint32_t abort, abort_done; + volatile uint32_t sm_state, sof_rd, intr, dev_sm_watchdog, nak_poll; } usb_hw_t; typedef struct { volatile uint32_t in, out; } usb_pair_t; typedef struct { diff --git a/tools/pico_usb_address_probe/router.c b/tools/pico_usb_address_probe/router.c index a80a3a1..4cdb421 100644 --- a/tools/pico_usb_address_probe/router.c +++ b/tools/pico_usb_address_probe/router.c @@ -9,6 +9,9 @@ #if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB extern bool native_hub_select_device(uint8_t address, uint8_t owner, uint32_t cutoff); +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) +#include "usb/native_hub/native_hub_trace.h" +#endif #endif #if !PICO_RP2350 || defined(__riscv) @@ -30,6 +33,10 @@ extern bool native_hub_select_device(uint8_t address, uint8_t owner, uint32_t cu #define PID_OUT 0xe1u #define PID_IN 0x69u #define PID_SETUP 0x2du +// NRZI SYNC+PID words, with K=2 and J=1 packed into two-bit samples. +#define TOKEN_OUT_SIGNATURE 0xaa66a666u +#define TOKEN_IN_SIGNATURE 0x95a6a666u +#define TOKEN_SETUP_SIGNATURE 0x9a56a666u #define NO_READER 2u #define SETUP_SEQUENCE_MASK 0x3fffffffu #define SETUP_SLOT_SHIFT 30u @@ -175,9 +182,9 @@ static void build_table(routing_table* table, void probe_router_init(uint32_t system_clock_hz) { // Explicit SRAM data: Core1 must never fetch flash during durable saves. - token_words[6] = 0xaa66a666u; - token_words[10] = 0x95a6a666u; - token_words[5] = 0x9a56a666u; + token_words[6] = TOKEN_OUT_SIGNATURE; + token_words[10] = TOKEN_IN_SIGNATURE; + token_words[5] = TOKEN_SETUP_SIGNATURE; const uint8_t addresses[PROBE_ROUTER_SLOTS] = {0u, PROBE_ROUTER_UNASSIGNED, PROBE_ROUTER_UNASSIGNED}; memset(&counters, 0, sizeof(counters)); @@ -295,7 +302,7 @@ static __force_inline bool sample_line(uint32_t* deadline, uint32_t* line) { } static __force_inline void route_header(const routing_table* table, uint32_t address, - bool setup, uint32_t initial_address, + uint32_t signature, uint32_t initial_address, uint32_t cutoff, raw_packet* packet) { // TinyUSB clears SETUP_REC only AFTER copying the hardware-validated SETUP // into its event callback. Until then, preserve both address and owner. @@ -306,8 +313,21 @@ static __force_inline void route_header(const routing_table* table, uint32_t add return; #if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB if (atomic_read(&enabled) != 0u) { - if (!native_hub_select_device((uint8_t)address, table->owner[address], cutoff)) +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + const uint8_t pid = signature == TOKEN_OUT_SIGNATURE ? PID_OUT : + signature == TOKEN_IN_SIGNATURE ? PID_IN : PID_SETUP; + const bool selected = (pid == PID_OUT || (pid == PID_IN && table->owner[address] == 0)) + ? native_hub_select_device_traced((uint8_t)address, table->owner[address], cutoff, pid) + : native_hub_select_device((uint8_t)address, table->owner[address], cutoff); +#else + const bool selected = native_hub_select_device((uint8_t)address, table->owner[address], cutoff); +#endif + if (!selected) { +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_note_failed_select((uint8_t)address, table->owner[address], cutoff, pid); +#endif return; + } if (initial_address != address) ++packet->retargets; } #else @@ -324,7 +344,7 @@ static __force_inline void route_header(const routing_table* table, uint32_t add // Candidate observations qualify calibration only. Runtime ownership // comes from the hardware address frozen by SETUP_REC. A missed software // candidate must not reject a correctly addressed, hardware-accepted SETUP. - if (setup) + if (signature == TOKEN_SETUP_SIGNATURE) publish_setup(table->owner[address]); } @@ -438,7 +458,7 @@ edge: if ((base) + (bit) == 19u && decoder != NULL) { \ uint8_t candidate = early_decoder[word1 & 0xffu]; \ if (candidate < 128u) \ - route_header(table, candidate, word0 == 0x9a56a666u, initial_address, \ + route_header(table, candidate, word0, initial_address, \ deadline + 11u * FS_BIT_CYCLES, &result); \ } \ } while (0) @@ -465,7 +485,7 @@ edge: address_wire = (address_wire | (address_wire >> 1u)) & 0x3333u; \ address_wire = (address_wire | (address_wire >> 2u)) & 0x0f0fu; \ address_wire = (address_wire | (address_wire >> 4u)) & 0xffu; \ - route_header(table, decoder[address_wire], word0 == 0x9a56a666u, \ + route_header(table, decoder[address_wire], word0, \ initial_address, deadline + 7u * FS_BIT_CYCLES, &result); \ if (result.late) { result.count = (base) + (bit) + 1u; goto done; } \ } \ diff --git a/tools/switch2_usb_probe/main.c b/tools/switch2_usb_probe/main.c index 93b7da2..c5a028c 100644 --- a/tools/switch2_usb_probe/main.c +++ b/tools/switch2_usb_probe/main.c @@ -124,6 +124,10 @@ int probe_debug_printf(const char* format, ...) { if (result <= 0) return result; const size_t size = (size_t)result < sizeof(message) ? (size_t)result : sizeof(message) - 1; const uint32_t interrupts = save_and_disable_interrupts(); +#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + const uint32_t mask_started = time_us_32(); + const uint32_t trace_parent = native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_LOG_COPY); +#endif if (LOG_CAPACITY - (log_written - log_read) >= size) { for (size_t i = 0; i < size; ++i) log_bytes[(log_written + i) % LOG_CAPACITY] = message[i]; @@ -132,7 +136,14 @@ int probe_debug_printf(const char* format, ...) { } else { log_dropped += (uint32_t)result; } +#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_trace_phase(trace_parent); + const uint32_t mask_elapsed = time_us_32() - mask_started; +#endif restore_interrupts(interrupts); +#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_note_log_mask(mask_elapsed, (uint32_t)size, interrupts != 0); +#endif return result; } @@ -847,12 +858,24 @@ int main(void) { uint32_t last_heartbeat = 0; while (true) { #if SWITCH2_PROBE_HUB +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_RADIO_POLL); +#endif probe_controller_input_task(); +#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_USB_TASK); +#endif native_hub_task(); #else tud_task(); +#endif +#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_LOG_DRAIN); #endif drain_log(); +#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_PROTOCOL); +#endif const uint32_t now = to_ms_since_boot(get_absolute_time()); #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE probe_bootsel_task(now); @@ -917,6 +940,9 @@ int main(void) { } #endif } +#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_SLEEP); +#endif sleep_us(100); } } diff --git a/tools/switch2_usb_probe/probe_build.cmake b/tools/switch2_usb_probe/probe_build.cmake index c7365a0..d6d8472 100644 --- a/tools/switch2_usb_probe/probe_build.cmake +++ b/tools/switch2_usb_probe/probe_build.cmake @@ -369,12 +369,16 @@ function(switch2_usb_probe_configure target) endif() if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT) if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) - pico_set_program_version(${target} "0.78-native-tx-bank-trace") + pico_set_program_version(${target} "0.87-native-enumeration-flight") else() - pico_set_program_version(${target} "0.78-native-gamepad") + pico_set_program_version(${target} "0.86-native-flash-service") endif() elseif(SWITCH2_PROBE_HUB) - pico_set_program_version(${target} "0.78-native-hub-tx-bank") + if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + pico_set_program_version(${target} "0.87-native-hub-enumeration-flight") + else() + pico_set_program_version(${target} "0.86-native-hub-flash-service") + endif() elseif(SWITCH2_PROBE_JOIN_CHORD_GATE) if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) pico_set_program_version(${target} "0.37-pair-chord-trace")