fix: reserve Classic reconnect airtime with two Switch 2 links
This commit is contained in:
parent
b0a896997b
commit
ed8f3a7c7f
9 changed files with 74 additions and 44 deletions
|
|
@ -819,13 +819,26 @@ from 197 host requests without queue drops; the user confirmed both sides.
|
|||
Normal GATT-client busy responses now retain queued output instead of
|
||||
disconnecting the controller.
|
||||
|
||||
Two Switch 2 BLE links plus Classic now select 30 ms Switch 2 connection
|
||||
intervals automatically; other topologies retain 7.5 ms. This trades Switch 2
|
||||
delivery latency for Classic airtime without changing HD encoding or bonds.
|
||||
Physical DualSense power-off/reconnect verified both interval transitions
|
||||
without losing the Joy-Con links. A 30-second production mixed-rumble run
|
||||
kept native DualSense streaming, with no send failures or ingress drops,
|
||||
14 Joy-Con output-stage discards and nine PCM skips. It is not lossless.
|
||||
Two or more Switch 2 BLE links now select 30 ms connection intervals even
|
||||
before Classic connects; a single Switch 2 link retains 7.5 ms. Paired and
|
||||
Individual Joy-Cons use the same physical-link policy. This trades Switch 2
|
||||
delivery latency for Classic paging and streaming airtime without changing
|
||||
HD encoding or bonds. The earlier policy waited for a Classic link and passed
|
||||
one physical power-off/reconnect check, but subsequent use exposed intermittent
|
||||
DualSense connection failures with two Joy-Cons already active. Regression
|
||||
coverage now requires airtime before Classic admission and after its departure.
|
||||
The user confirmed reconnects and actuator output/clean stops at 30 ms.
|
||||
A same-workload 15/30 ms comparison at 300 MHz rejected 15 ms for native
|
||||
haptics: it timed out around four seconds, with 111 PCM sends, 38 skips and
|
||||
one send failure. At 30 ms the full 30-second run retained all three links
|
||||
and native streaming: 1,384 PCM sends, 47 skips, no send failures or host-update/
|
||||
Switch 2 ingress drops, and seven Joy-Con output-stage drops. The isolated
|
||||
DualSense fixture delivered all 288 packets without skips. Release images keep
|
||||
30 ms; long-duration reliability remains unqualified. All 349 tests and four
|
||||
affected firmware builds passed for the preconnection policy.
|
||||
A prior 30-second production mixed-rumble run kept native DualSense streaming,
|
||||
with no send failures or ingress drops, 14 Joy-Con output-stage discards and
|
||||
nine PCM skips. It is not lossless.
|
||||
|
||||
The native-HD checkpoint passed 289 tests and all five firmware builds; its
|
||||
40 pre-existing profiles and metadata survived. Its inventory then contained
|
||||
|
|
|
|||
40
README.md
40
README.md
|
|
@ -481,13 +481,16 @@ results do **not** establish lossless arbitrary workloads or perceptual
|
|||
equivalence. All 40 profiles, names, active selections and adapter
|
||||
configuration were preserved during this upgrade.
|
||||
|
||||
**Automatic mixed-controller scheduling:** when at least two physical Switch 2
|
||||
BLE links coexist with a Classic Bluetooth link, the firmware requests a
|
||||
30 ms connection interval for the Switch 2 links. Otherwise it requests
|
||||
7.5 ms. A merged Joy-Con pair counts as two links. Unrelated BLE controllers
|
||||
**Automatic mixed-controller scheduling:** whenever at least two physical
|
||||
Switch 2 BLE links are present, the firmware requests a 30 ms connection
|
||||
interval for those links, even before a Classic Bluetooth controller connects.
|
||||
A single Switch 2 link uses 7.5 ms. A merged Joy-Con pair counts as two links;
|
||||
Individual mode uses the same physical-link policy. Unrelated BLE controllers
|
||||
are not retimed; bonds, profiles, HD encoding and the DualSense timeout stay
|
||||
unchanged. The policy runs during connection setup, before native DualSense
|
||||
attachment, and reconciles asynchronous negotiation and topology changes.
|
||||
unchanged. The policy reconciles asynchronous negotiation and topology changes.
|
||||
Waiting for a Classic connection before reserving its airtime left a reconnect
|
||||
timing hole: DualSense could fail to connect with both Joy-Cons already active.
|
||||
Keeping 30 ms after Classic disconnect also leaves airtime for its next attempt.
|
||||
|
||||
This trades additional Switch 2 input, gyro and rumble delivery latency for
|
||||
Classic radio time. Multiple reports can travel per connection event, so
|
||||
|
|
@ -497,13 +500,24 @@ both negotiated 30 ms and DualSense started without manual re-arming.
|
|||
A production-firmware 30-second simultaneous rumble run retained all three
|
||||
connections and native streaming: zero ingress drops or send failures,
|
||||
14 Joy-Con output-stage discards and nine DualSense PCM skips. This is not
|
||||
a lossless or long-duration qualification. Lifecycle regressions cover
|
||||
Classic arrival/departure, a missing half, handle reuse, late negotiation
|
||||
completion, clock wrap and transient request rejection.
|
||||
In the physical power-off/reconnect check, both Joy-Cons negotiated 7.5 ms
|
||||
after DualSense shut down, then 30 ms after it reconnected with PS. Their
|
||||
Bluetooth handles stayed unchanged; native DualSense streaming restarted
|
||||
automatically and sent 1,715 packets without skips at the observation point.
|
||||
a lossless or long-duration qualification. Lifecycle regressions cover airtime
|
||||
reservation before Classic arrival in Paired and Individual modes, Classic
|
||||
departure/reconnect, a missing half, handle reuse, late negotiation completion,
|
||||
clock wrap and transient request rejection.
|
||||
The earlier policy passed one physical DualSense power-off/reconnect check,
|
||||
but subsequent use exposed intermittent connection failures. The preconnection
|
||||
airtime regression fails under that policy and passes with the current one.
|
||||
The user confirmed reconnects and correct actuator output/stops with the
|
||||
preconnection 30 ms policy. A 15 ms trial also reconnected, but its native
|
||||
DualSense stream timed out even while sending silence. Under the same
|
||||
125 Hz-per-slot held-effect/silence workload at 300 MHz, 15 ms failed around
|
||||
four seconds (111 PCM packets sent, 38 skipped, one send failure); 30 ms
|
||||
completed 30 seconds (1,384 sent, 47 skipped, no send failures). All three
|
||||
controllers remained connected. The 30 ms run had zero host-update or Switch 2
|
||||
ingress drops and seven Joy-Con output-stage drops; its separate DualSense
|
||||
fixture delivered all 288 packets without skips. This supports keeping 30 ms,
|
||||
not a lossless or long-duration claim. The 15 ms trial is not the release default.
|
||||
The preconnection policy passed all 349 tests and four affected firmware builds.
|
||||
|
||||
### Rumble per controller
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -48,7 +48,7 @@ constexpr uint32_t kDefaultPairingWindowDurationMs =
|
|||
constexpr uint32_t kPairingResetFeedbackDurationMs = 2000;
|
||||
// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds.
|
||||
constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900;
|
||||
// LE units are 1.25 ms. Two fast Switch 2 links starve Classic native PCM.
|
||||
// LE units are 1.25 ms. Reserve airtime for Classic paging and native PCM.
|
||||
constexpr uint16_t kSwitch2FastInterval = 6;
|
||||
constexpr uint16_t kSwitch2MixedInterval = 24;
|
||||
constexpr uint32_t kSwitch2IntervalSettleMs = 1000;
|
||||
|
|
@ -591,11 +591,11 @@ void stop_background_scan() {
|
|||
g_background_scan_active = false;
|
||||
}
|
||||
}
|
||||
// Core 1 only. Count physical links, including Classic setup, rather than
|
||||
// logical players: a merged Joy-Con pair still consumes two LE connections.
|
||||
// Core 1 only. Count physical links, not logical players: a merged Joy-Con
|
||||
// pair still consumes two LE connections. Reserve Classic airtime before an
|
||||
// incoming connection exists; waiting for its ACL handle is too late for paging.
|
||||
void apply_radio_connection_policy() {
|
||||
unsigned switch2_links = 0;
|
||||
bool classic_link = false;
|
||||
uni_hid_device_t* ready[kSlotCount]{};
|
||||
for (const BackendSlot& slot : g_slots) {
|
||||
uni_hid_device_t* targets[] = {slot.device, slot.companion};
|
||||
|
|
@ -609,12 +609,10 @@ void apply_radio_connection_policy() {
|
|||
// The parser requests its initial interval during setup.
|
||||
// Do not race that request by changing a pending device here.
|
||||
if (slot.active) ready[index] = target;
|
||||
} else if (type == GAP_CONNECTION_ACL) {
|
||||
classic_link = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const uint16_t desired = switch2_links >= 2 && classic_link
|
||||
const uint16_t desired = switch2_links >= 2
|
||||
? kSwitch2MixedInterval
|
||||
: kSwitch2FastInterval;
|
||||
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
||||
|
|
|
|||
|
|
@ -3034,33 +3034,37 @@ void test_switch2_admission() {
|
|||
"Pro2 must preserve vertical normal controls and ingest remappable extras");
|
||||
}
|
||||
|
||||
void test_switch2_radio_policy() {
|
||||
void test_switch2_radio_policy(bool individual) {
|
||||
start_backend();
|
||||
if (individual) set_runtime_joycon_mode(JoyConMode::kIndividual);
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto other_ble = device(3, true, UNI_BT_CONN_PROTOCOL_BLE);
|
||||
ready_switch2(left);
|
||||
require(negotiated_intervals[left.conn.handle] == 6,
|
||||
"a single Switch2 link must retain fast scheduling");
|
||||
ready_switch2(right);
|
||||
require(incoming_connections &&
|
||||
negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24,
|
||||
"two Switch2 links must reserve Classic reconnect airtime before a Classic device exists");
|
||||
platform_on_device_connected(&other_ble);
|
||||
require(platform_on_device_ready(&other_ble) == UNI_ERROR_SUCCESS,
|
||||
"unrelated BLE controller must join");
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"a pair without Classic must retain fast intervals");
|
||||
platform_on_device_connected(&classic);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24 &&
|
||||
negotiated_intervals[other_ble.conn.handle] == 6,
|
||||
"Classic setup must relax both physical halves before native attachment, not unrelated BLE");
|
||||
"Classic setup must retain reconnect airtime without retiming unrelated BLE");
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic controller must complete setup alongside the pair");
|
||||
const auto pair = slot_snapshot(0);
|
||||
platform_on_device_disconnected(&classic);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6 &&
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24 &&
|
||||
slot_snapshot(0).connection_generation == pair.connection_generation,
|
||||
"Classic departure must restore fast intervals without rebinding the pair");
|
||||
"Classic departure must preserve reconnect airtime without rebinding the pair");
|
||||
platform_on_device_connected(&classic);
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic reconnect must complete");
|
||||
|
|
@ -3090,22 +3094,20 @@ void test_switch2_radio_settling() {
|
|||
"pending Switch2 setup must retain ownership of its initial interval");
|
||||
require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS,
|
||||
"second Switch2 connection must become ready");
|
||||
platform_on_device_disconnected(&classic);
|
||||
// The controller completes the old update after the topology reversed.
|
||||
platform_on_device_disconnected(&right);
|
||||
// The surviving controller completes the old update after losing its mate.
|
||||
for (auto* half : {&left, &right}) {
|
||||
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
||||
}
|
||||
now_ms += 50;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
for (auto* half : {&left, &right}) {
|
||||
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
||||
require(negotiated_intervals[half->conn.handle] == 6,
|
||||
"late mixed-mode completion must not strand a pair at slow intervals across clock wrap");
|
||||
}
|
||||
negotiated_intervals[left.conn.handle] = pending_intervals[left.conn.handle];
|
||||
require(negotiated_intervals[left.conn.handle] == 6,
|
||||
"late coexistence update must not strand a solo at slow intervals across clock wrap");
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
defer_interval_updates = false;
|
||||
reject_interval_updates = true;
|
||||
platform_on_device_connected(&classic);
|
||||
ready_switch2(right);
|
||||
const unsigned attempts = interval_requests[left.conn.handle];
|
||||
now_ms += 999;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
|
|
@ -5477,7 +5479,9 @@ int main(int argc, char** argv) {
|
|||
} else if (scenario == "switch2-admission") {
|
||||
test_switch2_admission();
|
||||
} else if (scenario == "switch2-radio-policy") {
|
||||
test_switch2_radio_policy();
|
||||
test_switch2_radio_policy(false);
|
||||
} else if (scenario == "switch2-radio-individual") {
|
||||
test_switch2_radio_policy(true);
|
||||
} else if (scenario == "switch2-radio-settling") {
|
||||
test_switch2_radio_settling();
|
||||
} else if (scenario == "switch2-mate-reconnect") {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"switch2-pair-replacement",
|
||||
"switch2-admission",
|
||||
"switch2-radio-policy",
|
||||
"switch2-radio-individual",
|
||||
"switch2-radio-settling",
|
||||
"switch2-mate-reconnect",
|
||||
"switch2-mate-pending",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue