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
|
Normal GATT-client busy responses now retain queued output instead of
|
||||||
disconnecting the controller.
|
disconnecting the controller.
|
||||||
|
|
||||||
Two Switch 2 BLE links plus Classic now select 30 ms Switch 2 connection
|
Two or more Switch 2 BLE links now select 30 ms connection intervals even
|
||||||
intervals automatically; other topologies retain 7.5 ms. This trades Switch 2
|
before Classic connects; a single Switch 2 link retains 7.5 ms. Paired and
|
||||||
delivery latency for Classic airtime without changing HD encoding or bonds.
|
Individual Joy-Cons use the same physical-link policy. This trades Switch 2
|
||||||
Physical DualSense power-off/reconnect verified both interval transitions
|
delivery latency for Classic paging and streaming airtime without changing
|
||||||
without losing the Joy-Con links. A 30-second production mixed-rumble run
|
HD encoding or bonds. The earlier policy waited for a Classic link and passed
|
||||||
kept native DualSense streaming, with no send failures or ingress drops,
|
one physical power-off/reconnect check, but subsequent use exposed intermittent
|
||||||
14 Joy-Con output-stage discards and nine PCM skips. It is not lossless.
|
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
|
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 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
|
equivalence. All 40 profiles, names, active selections and adapter
|
||||||
configuration were preserved during this upgrade.
|
configuration were preserved during this upgrade.
|
||||||
|
|
||||||
**Automatic mixed-controller scheduling:** when at least two physical Switch 2
|
**Automatic mixed-controller scheduling:** whenever at least two physical
|
||||||
BLE links coexist with a Classic Bluetooth link, the firmware requests a
|
Switch 2 BLE links are present, the firmware requests a 30 ms connection
|
||||||
30 ms connection interval for the Switch 2 links. Otherwise it requests
|
interval for those links, even before a Classic Bluetooth controller connects.
|
||||||
7.5 ms. A merged Joy-Con pair counts as two links. Unrelated BLE controllers
|
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
|
are not retimed; bonds, profiles, HD encoding and the DualSense timeout stay
|
||||||
unchanged. The policy runs during connection setup, before native DualSense
|
unchanged. The policy reconciles asynchronous negotiation and topology changes.
|
||||||
attachment, and 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
|
This trades additional Switch 2 input, gyro and rumble delivery latency for
|
||||||
Classic radio time. Multiple reports can travel per connection event, so
|
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
|
A production-firmware 30-second simultaneous rumble run retained all three
|
||||||
connections and native streaming: zero ingress drops or send failures,
|
connections and native streaming: zero ingress drops or send failures,
|
||||||
14 Joy-Con output-stage discards and nine DualSense PCM skips. This is not
|
14 Joy-Con output-stage discards and nine DualSense PCM skips. This is not
|
||||||
a lossless or long-duration qualification. Lifecycle regressions cover
|
a lossless or long-duration qualification. Lifecycle regressions cover airtime
|
||||||
Classic arrival/departure, a missing half, handle reuse, late negotiation
|
reservation before Classic arrival in Paired and Individual modes, Classic
|
||||||
completion, clock wrap and transient request rejection.
|
departure/reconnect, a missing half, handle reuse, late negotiation completion,
|
||||||
In the physical power-off/reconnect check, both Joy-Cons negotiated 7.5 ms
|
clock wrap and transient request rejection.
|
||||||
after DualSense shut down, then 30 ms after it reconnected with PS. Their
|
The earlier policy passed one physical DualSense power-off/reconnect check,
|
||||||
Bluetooth handles stayed unchanged; native DualSense streaming restarted
|
but subsequent use exposed intermittent connection failures. The preconnection
|
||||||
automatically and sent 1,715 packets without skips at the observation point.
|
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
|
### 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;
|
constexpr uint32_t kPairingResetFeedbackDurationMs = 2000;
|
||||||
// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds.
|
// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds.
|
||||||
constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900;
|
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 kSwitch2FastInterval = 6;
|
||||||
constexpr uint16_t kSwitch2MixedInterval = 24;
|
constexpr uint16_t kSwitch2MixedInterval = 24;
|
||||||
constexpr uint32_t kSwitch2IntervalSettleMs = 1000;
|
constexpr uint32_t kSwitch2IntervalSettleMs = 1000;
|
||||||
|
|
@ -591,11 +591,11 @@ void stop_background_scan() {
|
||||||
g_background_scan_active = false;
|
g_background_scan_active = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Core 1 only. Count physical links, including Classic setup, rather than
|
// Core 1 only. Count physical links, not logical players: a merged Joy-Con
|
||||||
// logical players: a merged Joy-Con pair still consumes two LE connections.
|
// 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() {
|
void apply_radio_connection_policy() {
|
||||||
unsigned switch2_links = 0;
|
unsigned switch2_links = 0;
|
||||||
bool classic_link = false;
|
|
||||||
uni_hid_device_t* ready[kSlotCount]{};
|
uni_hid_device_t* ready[kSlotCount]{};
|
||||||
for (const BackendSlot& slot : g_slots) {
|
for (const BackendSlot& slot : g_slots) {
|
||||||
uni_hid_device_t* targets[] = {slot.device, slot.companion};
|
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.
|
// The parser requests its initial interval during setup.
|
||||||
// Do not race that request by changing a pending device here.
|
// Do not race that request by changing a pending device here.
|
||||||
if (slot.active) ready[index] = target;
|
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
|
? kSwitch2MixedInterval
|
||||||
: kSwitch2FastInterval;
|
: kSwitch2FastInterval;
|
||||||
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
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");
|
"Pro2 must preserve vertical normal controls and ingest remappable extras");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_switch2_radio_policy() {
|
void test_switch2_radio_policy(bool individual) {
|
||||||
start_backend();
|
start_backend();
|
||||||
|
if (individual) set_runtime_joycon_mode(JoyConMode::kIndividual);
|
||||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||||
auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||||
auto other_ble = device(3, true, UNI_BT_CONN_PROTOCOL_BLE);
|
auto other_ble = device(3, true, UNI_BT_CONN_PROTOCOL_BLE);
|
||||||
ready_switch2(left);
|
ready_switch2(left);
|
||||||
|
require(negotiated_intervals[left.conn.handle] == 6,
|
||||||
|
"a single Switch2 link must retain fast scheduling");
|
||||||
ready_switch2(right);
|
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);
|
platform_on_device_connected(&other_ble);
|
||||||
require(platform_on_device_ready(&other_ble) == UNI_ERROR_SUCCESS,
|
require(platform_on_device_ready(&other_ble) == UNI_ERROR_SUCCESS,
|
||||||
"unrelated BLE controller must join");
|
"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);
|
platform_on_device_connected(&classic);
|
||||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||||
negotiated_intervals[right.conn.handle] == 24 &&
|
negotiated_intervals[right.conn.handle] == 24 &&
|
||||||
negotiated_intervals[other_ble.conn.handle] == 6,
|
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,
|
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||||
"Classic controller must complete setup alongside the pair");
|
"Classic controller must complete setup alongside the pair");
|
||||||
const auto pair = slot_snapshot(0);
|
const auto pair = slot_snapshot(0);
|
||||||
platform_on_device_disconnected(&classic);
|
platform_on_device_disconnected(&classic);
|
||||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||||
negotiated_intervals[right.conn.handle] == 6 &&
|
negotiated_intervals[right.conn.handle] == 24 &&
|
||||||
slot_snapshot(0).connection_generation == pair.connection_generation,
|
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);
|
platform_on_device_connected(&classic);
|
||||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||||
"Classic reconnect must complete");
|
"Classic reconnect must complete");
|
||||||
|
|
@ -3090,22 +3094,20 @@ void test_switch2_radio_settling() {
|
||||||
"pending Switch2 setup must retain ownership of its initial interval");
|
"pending Switch2 setup must retain ownership of its initial interval");
|
||||||
require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS,
|
require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS,
|
||||||
"second Switch2 connection must become ready");
|
"second Switch2 connection must become ready");
|
||||||
platform_on_device_disconnected(&classic);
|
platform_on_device_disconnected(&right);
|
||||||
// The controller completes the old update after the topology reversed.
|
// The surviving controller completes the old update after losing its mate.
|
||||||
for (auto* half : {&left, &right}) {
|
for (auto* half : {&left, &right}) {
|
||||||
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
||||||
}
|
}
|
||||||
now_ms += 50;
|
now_ms += 50;
|
||||||
process_configuration_timer(&g_configuration_timer);
|
process_configuration_timer(&g_configuration_timer);
|
||||||
for (auto* half : {&left, &right}) {
|
negotiated_intervals[left.conn.handle] = pending_intervals[left.conn.handle];
|
||||||
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
require(negotiated_intervals[left.conn.handle] == 6,
|
||||||
require(negotiated_intervals[half->conn.handle] == 6,
|
"late coexistence update must not strand a solo at slow intervals across clock wrap");
|
||||||
"late mixed-mode completion must not strand a pair at slow intervals across clock wrap");
|
|
||||||
}
|
|
||||||
process_configuration_timer(&g_configuration_timer);
|
process_configuration_timer(&g_configuration_timer);
|
||||||
defer_interval_updates = false;
|
defer_interval_updates = false;
|
||||||
reject_interval_updates = true;
|
reject_interval_updates = true;
|
||||||
platform_on_device_connected(&classic);
|
ready_switch2(right);
|
||||||
const unsigned attempts = interval_requests[left.conn.handle];
|
const unsigned attempts = interval_requests[left.conn.handle];
|
||||||
now_ms += 999;
|
now_ms += 999;
|
||||||
process_configuration_timer(&g_configuration_timer);
|
process_configuration_timer(&g_configuration_timer);
|
||||||
|
|
@ -5477,7 +5479,9 @@ int main(int argc, char** argv) {
|
||||||
} else if (scenario == "switch2-admission") {
|
} else if (scenario == "switch2-admission") {
|
||||||
test_switch2_admission();
|
test_switch2_admission();
|
||||||
} else if (scenario == "switch2-radio-policy") {
|
} 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") {
|
} else if (scenario == "switch2-radio-settling") {
|
||||||
test_switch2_radio_settling();
|
test_switch2_radio_settling();
|
||||||
} else if (scenario == "switch2-mate-reconnect") {
|
} else if (scenario == "switch2-mate-reconnect") {
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
"switch2-pair-replacement",
|
"switch2-pair-replacement",
|
||||||
"switch2-admission",
|
"switch2-admission",
|
||||||
"switch2-radio-policy",
|
"switch2-radio-policy",
|
||||||
|
"switch2-radio-individual",
|
||||||
"switch2-radio-settling",
|
"switch2-radio-settling",
|
||||||
"switch2-mate-reconnect",
|
"switch2-mate-reconnect",
|
||||||
"switch2-mate-pending",
|
"switch2-mate-pending",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue