feat(native-usb): add 32-frame HD gameplay rumble and USB flight diagnostics

Preserve native frequency/amplitude timelines for DualSense PCM output,
remove the unsupported 64-frame path, and retain compatibility rumble for
other controllers. Use the validated 300 MHz sampling phase and restrict
non-bondable Classic discovery autoconnect to previously paired peers.

Buffer native-hub UART stdout and release USB IRQs around port-reset
callbacks. Add observer liveness and pre-SETUP root-response observations
without changing recovery behavior. Cover transport and haptics boundaries.

Record the user-accepted 0.108 trial: controls remained responsive and
rumble felt fine. Instrumentation changes timing; the disconnect root
cause and long-term reliability remain unqualified.

Validation: 624 tests, 11 affected firmware/probe builds, and on-device
concurrent USB and HD-auto-start checks. Private captures, generated
images, and unrelated working-tree files are intentionally excluded.
This commit is contained in:
Joey Yakimowich-Payne 2026-09-19 17:03:46 -06:00
commit 80b788099b
46 changed files with 2873 additions and 412 deletions

View file

@ -32,6 +32,7 @@ uni_platform* installed_platform = nullptr;
bool observed_status_led_on = false;
int observed_status_led_writes = 0;
uint32_t now_ms = 0;
uint32_t now_sub_ms_us = 0;
struct WiiAccelFixture {
uni_hid_device_t* device = nullptr;
int32_t acceleration[3]{};
@ -689,6 +690,8 @@ uint32_t time_us_32() {
return now_ms * 1000u;
}
uint64_t time_us_64() { return uint64_t{now_ms} * 1000 + now_sub_ms_us; }
void switch2_wake_initialize() {
++switch2_wake_initializations;
@ -711,6 +714,7 @@ void switch2_wake_diagnostics(Switch2WakeDiagnostics*) {
#include "core/controller_identity.cpp"
namespace {
unsigned state_lock_depth = 0;
void (*after_backend_state_unlock)() = nullptr;
void tracked_state_lock_enter(critical_section_t* lock) {
critical_section_enter_blocking(lock);
@ -721,6 +725,8 @@ void tracked_state_lock_exit(critical_section_t* lock) {
require(state_lock_depth != 0, "state lock exit must match an enter");
--state_lock_depth;
critical_section_exit(lock);
if (state_lock_depth == 0 && after_backend_state_unlock)
after_backend_state_unlock();
}
} // namespace
@ -769,26 +775,34 @@ namespace {
std::vector<btstack_timer_source_t*> native_timers;
std::array<uint8_t, 143> last_native_packet{};
uint16_t last_native_cid = 0;
struct NativePacketObservation {
uint64_t sent_us;
uint16_t cid;
std::array<uint8_t, 143> data;
};
std::vector<NativePacketObservation> native_packets;
}
void native_test_add_timer(btstack_timer_source_t* timer) {
require(state_lock_depth == 0, "HD timer scheduling must not hold the backend lock");
btstack_run_loop_remove_timer(timer);
timer->due_ms = uint64_t{now_ms} + timer->timeout_ms + 1;
native_timers.push_back(timer);
}
int btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
require(state_lock_depth == 0, "HD timer cancellation must not hold the backend lock");
const auto found = std::find(native_timers.begin(), native_timers.end(), timer);
if (found == native_timers.end()) return 0;
native_timers.erase(found);
return 1;
}
uint64_t time_us_64() { return uint64_t{now_ms} * 1000; }
uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t) { return 143; }
bool l2cap_can_send_packet_now(uint16_t) { return true; }
int hci_number_free_acl_slots_for_handle(uint16_t) { return 8; }
uint8_t l2cap_request_can_send_now_event(uint16_t cid) {
require(state_lock_depth == 0, "HD permission callbacks must not hold the backend lock");
for (const auto& slot : g_slots) {
if (slot.device != nullptr && slot.device->conn.interrupt_cid == cid) {
(void)uni_platform_on_l2cap_can_send_now(slot.device, cid);
@ -800,9 +814,11 @@ uint8_t l2cap_request_can_send_now_event(uint16_t cid) {
}
uint8_t l2cap_send(uint16_t cid, const uint8_t* data, uint16_t size) {
require(state_lock_depth == 0, "HD packet writes must not hold the backend lock");
require(size == last_native_packet.size(), "native report size changed");
std::copy(data, data + size, last_native_packet.begin());
last_native_cid = cid;
native_packets.push_back({time_us_64(), cid, last_native_packet});
return ERROR_CODE_SUCCESS;
}
@ -5351,12 +5367,11 @@ void require_native_channels(bool left, bool right) {
require(status.state == HapticsExperimentState::kRunning &&
status.mode == 1,
"stateful host rumble lost native gameplay ownership");
require(status.packet_frames == SWITCH_PICO_HD_PACKET_FRAMES,
"native gameplay ignored the configured packet frame count");
const unsigned sample_offset = status.packet_frames == 32 ? 14 : 10;
require(status.packet_frames == 32,
"native gameplay requires the supported 32-frame transport");
constexpr unsigned sample_offset = 14;
require(last_native_packet[3] == 0x91 &&
last_native_packet[sample_offset - 2] ==
(status.packet_frames == 32 ? 0x92 : 0xd2) &&
last_native_packet[sample_offset - 2] == 0x92 &&
last_native_packet[sample_offset - 1] == 64,
"stateful channel inspection requires a native PCM block");
unsigned active[2]{};