fix: use sustainable native haptics cadence for mixed controllers

This commit is contained in:
Joey Yakimowich-Payne 2026-09-05 22:34:20 -06:00
commit 71fcf5bb94
12 changed files with 52 additions and 13 deletions

View file

@ -62,6 +62,18 @@ option(SWITCH_PICO_HCI_CREDIT_BATCH
if(SWITCH_PICO_HD_RUMBLE)
set(SWITCH_PICO_HAPTICS_EXPERIMENT ON)
endif()
set(SWITCH_PICO_HD_PACKET_FRAMES "64" CACHE STRING
"Native gameplay frames per packet: 64 standard, 32 single-controller experiment")
set_property(CACHE SWITCH_PICO_HD_PACKET_FRAMES PROPERTY STRINGS 64 32)
if(NOT SWITCH_PICO_HD_PACKET_FRAMES MATCHES "^(32|64)$")
message(FATAL_ERROR "SWITCH_PICO_HD_PACKET_FRAMES must be 32 or 64")
endif()
if(SWITCH_PICO_HD_PACKET_FRAMES STREQUAL "32")
if(NOT SWITCH_PICO_HAPTICS_EXPERIMENT OR NOT SWITCH_PICO_CYW43_PACKET_READ
OR NOT SWITCH_PICO_HCI_CREDIT_BATCH OR SWITCH_PICO_SYS_CLOCK_MHZ LESS 300)
message(FATAL_ERROR "32-frame qualification requires native transport, packet reads, credit batching and at least 300 MHz")
endif()
endif()
set(SWITCH_PICO_INPUT_BACKEND "UART" CACHE STRING "Controller input backend")
set_property(CACHE SWITCH_PICO_INPUT_BACKEND PROPERTY STRINGS UART BLUEPAD32)
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "UART"
@ -288,6 +300,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
${SWITCH_PICO_SOURCE_DIR}/input/haptics_transport_probe.cpp)
target_compile_definitions(switch-pico PRIVATE
SWITCH_PICO_HAPTICS_EXPERIMENT=1
SWITCH_PICO_HD_PACKET_FRAMES=${SWITCH_PICO_HD_PACKET_FRAMES}
SWITCH_PICO_HAPTICS_EXPERIMENT_RAM=$<BOOL:${SWITCH_PICO_HAPTICS_EXPERIMENT_RAM}>)
target_compile_definitions(bluepad32 PRIVATE
SWITCH_PICO_HAPTICS_EXPERIMENT=1)

View file

@ -29,7 +29,7 @@ Sources:
1. AIO/XInput defaults enable `SWITCH_PICO_HAPTICS_EXPERIMENT`, `SWITCH_PICO_HD_RUMBLE`, packet-level CYW43 reads and bounded HCI credit batching at 300 MHz/1.3 V. UART is unchanged. Preserve wake identity, pairing storage and USB modes. Incoming flow control and FIFO capacities remain unchanged; the controller's advertised outgoing capacity is eight ACL packets on this hardware.
2. One selected Sony DualSense/DualSense Edge, Bluetooth Classic, sufficient negotiated MTU. Auto-arm chooses the first eligible ready controller, not necessarily slot 0, and later controllers do not steal an active stream. The fixture requires explicit start. Idle native output remains silent; other models and unselected slots use their controller-specific compatibility paths.
3. Report 0x32 plus A2 remains a 143-byte L2CAP SDU. The first report selects native mode with sized state block 0x90/63 and one silent 0x92/64 haptic block. Subsequent reports use compact controls `{0x91,3,0x62,16,counter}`. Qualified gameplay carries one 64-byte block (32 stereo frames, descriptor 0x92); the fixture and unoptimized builds carry two blocks (64 frames, descriptor 0xd2). The counter advances by the number of blocks. Padding and Bluetooth CRC remain deterministic. No speaker, microphone, USB audio endpoint, Opus or resampler.
3. Report 0x32 plus A2 remains a 143-byte L2CAP SDU. The first report selects native mode with sized state block 0x90/63 and one silent 0x92/64 haptic block. Subsequent reports use compact controls `{0x91,3,0x62,16,counter}`. Standard gameplay and the fixture carry two blocks (64 stereo frames, descriptor 0xd2). Explicit `SWITCH_PICO_HD_PACKET_FRAMES=32` carries one 64-byte block (descriptor 0x92) for single-controller qualification only. The counter advances by the number of blocks. Padding and Bluetooth CRC remain deterministic. No speaker, microphone, USB audio endpoint, Opus or resampler.
4. At 3 kHz, 32/64 stereo frames require 93.75/46.875 reports/s. Absolute rational deadlines preserve fractional time and skip obsolete packets after stalls rather than burst-replaying them. Timer wakeups account for SDK +1 tick. Can-send permission and audio deadlines remain separate; flags are armed before requests and synchronous callbacks cannot recursively generate a stream.
5. The deterministic fixture remains a finite 288-report / 6.144-second sequence: 48 priming intervals, four cycles of left 100 Hz / silence / right 200 Hz / silence (12 reports = 256 ms per phase), then 48 trailing-silence reports. Its peak remains 32/127. Gameplay is continuous, has no one-second priming pattern, and uses timestamped Switch commands instead. Stop restores compatibility output; disconnect cancels without stale-pointer use.
6. No historical PCM FIFO. Generate only the current due block when transmission is permitted; bounded control mailbox across cores. Record packet counts, skipped blocks, failed sends, synchronous callbacks, generation cost, send gaps, lateness, request wait and first-tone timestamps. HCI submission is not physical actuator onset.
@ -46,6 +46,7 @@ cmake -S . -B build-hd-rumble -DPICO_BOARD=pico2_w \
-DSWITCH_PICO_HD_RUMBLE=ON \
-DSWITCH_PICO_SYS_CLOCK_MHZ=300 -DSWITCH_PICO_OVERCLOCK_MV=1300 \
-DSWITCH_PICO_CYW43_PACKET_READ=ON -DSWITCH_PICO_HCI_CREDIT_BATCH=ON \
-DSWITCH_PICO_HD_PACKET_FRAMES=64 \
-DSWITCH_PICO_HAPTICS_EXPERIMENT_RAM=ON -DSWITCH_PICO_LOG=OFF
cmake --build build-hd-rumble
```
@ -56,7 +57,7 @@ The decoder preserves each actuator's one-to-three ordered substeps and frequenc
The synthesizer has independent left/right low/high phase accumulators. Frequencies are `40 * 2^(index/32)` and `80 * 2^(index/32)` Hz. Each Switch command occupies an 8 ms window, split into 24/12/8 PCM samples per substep for counts 1/2/3. New reports supersede unplayed old substeps; identical compressed words hold final state rather than replaying deltas. Each Switch-updated side expires after 50 ms, matching the existing conservative timeout policy.
One report interval of causal lookback is 10.667 ms in the qualified 32-frame path and 21.333 ms in the 64-frame path. Fixed 16-entry cross-core and synthesis histories contain commands, not PCM. Overflow is counted and obsolete sample intervals are not replayed. XInput holds use a distinct persistent command: strong/low magnitude drives the left 160 Hz band, weak/high drives the right 320 Hz band, until a new command or zero stop. They do not fake refreshes to evade the Switch watchdog. Retained XInput state is seeded once per native run, including manual re-arming after compatibility output.
Standard gameplay uses 21.333 ms causal lookback; the explicit 32-frame experiment uses 10.667 ms. Fixed 16-entry cross-core and synthesis histories contain commands, not PCM. Overflow is counted and obsolete sample intervals are not replayed. XInput holds use a distinct persistent command: strong/low magnitude drives the left 160 Hz band, weak/high magnitude drives the right 320 Hz band, until a new command or zero stop. They do not fake refreshes to evade the 50 ms Switch watchdog. Retained XInput state is seeded once per native run, including manual re-arming after compatibility output.
Native gameplay uses balanced **2x low/high gain after profile scaling**, followed by a gentle **0.8-power curve** on the combined amplitude. This lifts quiet and medium effects while retaining their low/high ratio. The curve is a 257-entry lookup with integer interpolation, not per-sample floating-point math. Combined weights are capped at 65535 to avoid overflow and clipping. Zero remains zero. The amplitude curve does not alter carrier frequencies or local-confirmation gain; packet timing follows the transport configuration above. This response replaced the initial 1.5x and low-band-only experiments after user comparison.
@ -287,3 +288,26 @@ Pico/UART builds. The profile catalog was migrated and all sixteen stored
profiles were compared with the pre-migration backup; the temporary editor
profile and name were restored. No configuration, bond, or wake-identity reset
was part of the transport work.
### Mixed-controller cadence limit
Final testing with a Switch Pro plus a DualSense and continuous USB motion
reads changed the cadence decision. Rumble commands targeted only the
DualSense; the Pro supplied concurrent input traffic, not a Pro rumble
qualification. The 32-frame stream received all 2,050
host commands but skipped **80 audio slots in 16.6 s**. Maximum permission
wait reached **17,180 us**, exceeding its 10,667 us interval, with all eight
outgoing credits observed in use. CPU clock remained 300 MHz/1.3 V.
The standard build therefore uses **64 frames / 46.875 reports per second**,
without reverting its CPU or transport improvements. The same 2,050-command
mixed-controller comparison passed with zero drops/skips/send failures and
778 audio reports. Its worst observed report gap was 26,588 us. A subsequent
roughly 65-second mixed-controller stress run received all **8,194 commands**
and submitted **3,082 audio reports**, with **zero drops, skipped audio slots,
or send failures**. It processed 41,738 input reports and its worst observed
audio report gap was 26,655 us.
The 32-frame path remains an explicit single-controller experiment and is
covered by the same native protocol/lifecycle tests; it is not advertised as
sustainable for mixed/four-controller operation.

View file

@ -329,7 +329,7 @@ The standard AIO and XInput builds now use **300 MHz at 1.3 V**, packet-level CY
In Switch mode, that stream preserves decoded left/right, low/high-band HD commands. In XInput mode, strong/low magnitude drives the left 160 Hz carrier and weak/high drives the right 320 Hz carrier; these commands stay active until changed or stopped. XInput does not supply Nintendo frequency/substep detail. USB reset, unmount, and suspend stop held host rumble. Auto-mode XInput additionally reboots to Switch probe after unmount, by the existing one-attachment policy; manual XInput is exempt.
Qualified optimized gameplay uses **32 stereo frames at 3 kHz** per Bluetooth report (93.75 reports/s), with 10.667 ms causal lookback. Unoptimized builds and the deterministic fixture retain 64-frame packets. Native streaming continues silence while idle to avoid mode churn. This is not a measured physical actuator-onset bound; details and the accepted reference pattern are in [HAPTICS_EXPERIMENT.md](HAPTICS_EXPERIMENT.md).
Standard native gameplay uses **64 stereo frames at 3 kHz** per Bluetooth report (46.875 reports/s), with 21.333 ms causal lookback. The 32-frame mode passed single-controller tests but skipped audio slots under mixed Pro/DualSense load, so it is an explicit experiment: `SWITCH_PICO_HD_PACKET_FRAMES=32` requires the optimized transport and at least 300 MHz. It uses 93.75 reports/s and 10.667 ms lookback but is not the mixed-controller default. Native streaming continues silence while idle; no physical actuator-onset bound is claimed. See [HAPTICS_EXPERIMENT.md](HAPTICS_EXPERIMENT.md).
400 MHz is an explicit experiment: use `SWITCH_PICO_SYS_CLOCK_MHZ=400` and `SWITCH_PICO_OVERCLOCK_MV=1400`. This board did not boot at 400 MHz/1.3 V; 1.4 V booted and passed a short run but did not outperform 300 MHz in the comparison. USB stays at 48 MHz and flash/radio bus dividers remain bounded. UART builds are unchanged; a stock-clock AIO build is an explicit recovery/compatibility option, not the normal default.
@ -728,15 +728,15 @@ linked binary, not from the larger debug-bearing ELF or UF2 transport file:
| Resource | Used or reserved | Device capacity |
|---|---:|---:|
| Executable flash image | 747,024 bytes | 4 MiB |
| Executable flash image | 746,568 bytes | 4 MiB |
| Indexed profile arenas | 256 KiB | 4 MiB flash |
| Adapter configuration | 8 KiB | 4 MiB flash |
| BTstack bonds | 8 KiB | 4 MiB flash |
| RP2350 terminal sector | 4 KiB | 4 MiB flash |
| Allocated/reserved SRAM, including heap and stacks | 131,144 bytes | 520 KiB |
| Allocated/reserved SRAM, including heap and stacks | 130,784 bytes | 520 KiB |
The executable plus persistent reservations consume 1,029,648 bytes of flash,
leaving 3,164,656 bytes. Allocated SRAM sections leave 401,336 bytes of link-time
The executable plus persistent reservations consume 1,029,192 bytes of flash,
leaving 3,165,112 bytes. Allocated SRAM sections leave 401,696 bytes of link-time
headroom; this is not a runtime heap high-water measurement. Core 0 has a
4 KiB stack, and Core 1 uses a dedicated 16 KiB stack in main SRAM for nested
catalog migration/compaction rather than overflowing its 4 KiB scratch bank.

View file

@ -20,7 +20,7 @@ Start with a genuine original Switch Pro Controller, then original standalone Jo
### Important timing qualification caveat
Do not generalize single-controller DualSense results to mixed-controller loads. A 32-frame/93.75-packet-per-second DualSense run passed roughly 65 seconds with one controller, but a later Switch Pro + DualSense test with continuous USB motion reads recorded **80 skipped audio slots over 16.6 seconds**, despite receiving all 2,050 USB commands with no command drops or send failures. Maximum permission wait was 17,180 us and the eight outgoing ACL credits were observed exhausted. Final cadence qualification is active work; do not assume 32-frame PCM is universally sustainable or independently change it as part of this Nintendo backend task.
Do not generalize single-controller DualSense results to mixed-controller loads. A 32-frame/93.75-packet-per-second run passed roughly 65 seconds with one controller, but a later Switch Pro + DualSense test with continuous USB motion reads recorded **80 skipped audio slots over 16.6 seconds**, despite receiving all 2,050 USB commands with no command drops or send failures. Maximum permission wait was 17,180 us and the eight outgoing ACL credits were observed exhausted. The standard native cadence is consequently **64 frames / 46.875 packets per second** at the same 300 MHz/1.3 V, with 32 frames an explicit experiment. Preserve the current cadence choice and coordinate before changing it as part of this Nintendo backend task.
The Nintendo path should not need PCM packets at all. Its small native commands have a different bandwidth budget, which still needs real multi-controller measurement.

View file

@ -409,6 +409,7 @@ def build(
f"-DSWITCH_PICO_HD_RUMBLE={native}",
f"-DSWITCH_PICO_SYS_CLOCK_MHZ={300 if native == 'ON' else 150}",
"-DSWITCH_PICO_OVERCLOCK_MV=1300",
"-DSWITCH_PICO_HD_PACKET_FRAMES=64",
f"-DSWITCH_PICO_CYW43_PACKET_READ={native}",
f"-DSWITCH_PICO_HCI_CREDIT_BATCH={native}",
]

Binary file not shown.

Binary file not shown.

View file

@ -17,12 +17,11 @@
namespace {
#if defined(SWITCH_PICO_CYW43_PACKET_READ) && defined(SWITCH_PICO_HCI_CREDIT_BATCH) && \
defined(SWITCH_PICO_SYS_CLOCK_MHZ) && SWITCH_PICO_SYS_CLOCK_MHZ >= 300
constexpr uint32_t kGameplayFrames = 32;
#else
constexpr uint32_t kGameplayFrames = 64;
#ifndef SWITCH_PICO_HD_PACKET_FRAMES
#define SWITCH_PICO_HD_PACKET_FRAMES 64
#endif
constexpr uint32_t kGameplayFrames = SWITCH_PICO_HD_PACKET_FRAMES;
static_assert(kGameplayFrames == 32 || kGameplayFrames == 64);
constexpr uint32_t kPacketDenominator = 3;
constexpr uint32_t kPackets = 288;
constexpr uint32_t kPrimingPackets = 48;

View file

@ -67,6 +67,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
"-DSWITCH_PICO_CYW43_PACKET_READ=1",
"-DSWITCH_PICO_HCI_CREDIT_BATCH=1",
"-DSWITCH_PICO_SYS_CLOCK_MHZ=300",
"-DSWITCH_PICO_HD_PACKET_FRAMES=32",
]
)
command.extend(

View file

@ -32,6 +32,7 @@ def test_haptics_experiment_native(
"-DSWITCH_PICO_CYW43_PACKET_READ=1",
"-DSWITCH_PICO_HCI_CREDIT_BATCH=1",
"-DSWITCH_PICO_SYS_CLOCK_MHZ=300",
"-DSWITCH_PICO_HD_PACKET_FRAMES=32",
]
if short_packets
else []