feat: coordinate native rumble with deadline-aware scheduling
This commit is contained in:
parent
be0f3579c6
commit
dcc97e05ba
28 changed files with 1079 additions and 106 deletions
|
|
@ -728,6 +728,9 @@ Current constraints:
|
||||||
payload at actual submission. Native ownership cancels compatibility timers.
|
payload at actual submission. Native ownership cancels compatibility timers.
|
||||||
- `switch_native_output.cpp` owns bounded generation-tagged queues, L2CAP
|
- `switch_native_output.cpp` owns bounded generation-tagged queues, L2CAP
|
||||||
permission-driven delivery, expiry, feedback/resume and held-state coalescing.
|
permission-driven delivery, expiry, feedback/resume and held-state coalescing.
|
||||||
|
- `native_output_scheduler.cpp` arbitrates both native writers with urgent-stop
|
||||||
|
precedence, earliest deadlines, rotating ties, periodic credit reservations,
|
||||||
|
and generation-bound grant completion. Radio power policy is unchanged.
|
||||||
- Adapter configuration schema 3 persists up to 16 explicit physical approvals.
|
- Adapter configuration schema 3 persists up to 16 explicit physical approvals.
|
||||||
Existing profile schema 6/catalog 2, bonds and wake identity are unchanged.
|
Existing profile schema 6/catalog 2, bonds and wake identity are unchanged.
|
||||||
- Joy-Cons are currently separate, horizontally mapped controllers in
|
- Joy-Cons are currently separate, horizontally mapped controllers in
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||||
${SWITCH_PICO_SOURCE_DIR}/core/controller_identity.cpp
|
${SWITCH_PICO_SOURCE_DIR}/core/controller_identity.cpp
|
||||||
${SWITCH_PICO_SOURCE_DIR}/input/bluepad32_input_backend.cpp
|
${SWITCH_PICO_SOURCE_DIR}/input/bluepad32_input_backend.cpp
|
||||||
${SWITCH_PICO_SOURCE_DIR}/input/switch_native_output.cpp
|
${SWITCH_PICO_SOURCE_DIR}/input/switch_native_output.cpp
|
||||||
|
${SWITCH_PICO_SOURCE_DIR}/input/native_output_scheduler.cpp
|
||||||
${SWITCH_PICO_SOURCE_DIR}/usb/switch/switch_native_haptics.cpp
|
${SWITCH_PICO_SOURCE_DIR}/usb/switch/switch_native_haptics.cpp
|
||||||
${SWITCH_PICO_SOURCE_DIR}/input/controller_macro_capture.cpp
|
${SWITCH_PICO_SOURCE_DIR}/input/controller_macro_capture.cpp
|
||||||
${SWITCH_PICO_SOURCE_DIR}/input/switch2_wake.cpp
|
${SWITCH_PICO_SOURCE_DIR}/input/switch2_wake.cpp
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -363,13 +363,23 @@ states refresh at 40 ms. XInput uses held low/left-160-Hz and high/right-320-Hz
|
||||||
effects until explicitly stopped. Standalone Joy-Cons downmix each band by
|
effects until explicitly stopped. Standalone Joy-Cons downmix each band by
|
||||||
dominant amplitude, choosing left on ties; logical Joy-Con pairing is not added.
|
dominant amplitude, choosing left on ties; logical Joy-Con pairing is not added.
|
||||||
|
|
||||||
|
Nintendo and DualSense native sends share a fixed-capacity **deadline-aware
|
||||||
|
host scheduler**. Real stop transitions take priority, pending packets use
|
||||||
|
earliest-deadline order with rotating ties, and earlier periodic deadlines
|
||||||
|
protect the last available controller ACL credit. Grants and releases are
|
||||||
|
connection-generation-bound; LED handoffs and reconnects do not retain stale
|
||||||
|
permissions. This schedules HCI submission, not the radio's on-air slots.
|
||||||
|
The standard firmware does **not** disable Bluetooth sniff/power-saving mode.
|
||||||
|
|
||||||
**Qualification is incomplete.** The optimized Pro-only build passed 1,025
|
**Qualification is incomplete.** The optimized Pro-only build passed 1,025
|
||||||
distinct commands at 125 Hz with no loss or congestion. Separate held-state
|
distinct commands at 125 Hz with no loss or congestion. Separate held-state
|
||||||
testing coalesced 505 of 513 commands into eight state changes plus refreshes,
|
testing coalesced 505 of 513 commands into eight state changes plus refreshes,
|
||||||
also without loss; the user confirmed both actuators, both bands and clean
|
also without loss; the user confirmed both actuators, both bands and clean
|
||||||
stops. Mixed held-effect traffic also passed at approximately 125 Hz per
|
stops. On the final deadline-scheduler build, a 16.6-second mixed held-effect
|
||||||
controller. Continuously changing both streams at that rate still loses Pro
|
test received 2,049 commands per controller with no Pro command drops and one
|
||||||
commands and DualSense audio slots; transport experiments remain unqualified.
|
DualSense audio-slot skip. This result was accepted for the current setup.
|
||||||
|
Continuously changing both streams at 125 Hz still loses Pro commands and
|
||||||
|
DualSense audio slots; lossless high-rate transport is not claimed.
|
||||||
Joy-Con hardware, four-controller operation, captured game effects and physical
|
Joy-Con hardware, four-controller operation, captured game effects and physical
|
||||||
actuator timing are not qualified by the native regression suite. See
|
actuator timing are not qualified by the native regression suite. See
|
||||||
[SWITCH_FAMILY_HD_RUMBLE_PLAN.md](SWITCH_FAMILY_HD_RUMBLE_PLAN.md) for the exact
|
[SWITCH_FAMILY_HD_RUMBLE_PLAN.md](SWITCH_FAMILY_HD_RUMBLE_PLAN.md) for the exact
|
||||||
|
|
@ -770,15 +780,15 @@ linked binary, not from the larger debug-bearing ELF or UF2 transport file:
|
||||||
|
|
||||||
| Resource | Used or reserved | Device capacity |
|
| Resource | Used or reserved | Device capacity |
|
||||||
|---|---:|---:|
|
|---|---:|---:|
|
||||||
| Executable flash image | 777,656 bytes | 4 MiB |
|
| Executable flash image | 786,864 bytes | 4 MiB |
|
||||||
| Indexed profile arenas | 256 KiB | 4 MiB flash |
|
| Indexed profile arenas | 256 KiB | 4 MiB flash |
|
||||||
| Adapter configuration | 8 KiB | 4 MiB flash |
|
| Adapter configuration | 8 KiB | 4 MiB flash |
|
||||||
| BTstack bonds | 8 KiB | 4 MiB flash |
|
| BTstack bonds | 8 KiB | 4 MiB flash |
|
||||||
| RP2350 terminal sector | 4 KiB | 4 MiB flash |
|
| RP2350 terminal sector | 4 KiB | 4 MiB flash |
|
||||||
| Allocated/reserved SRAM, including heap and stacks | 139,344 bytes | 520 KiB |
|
| Allocated/reserved SRAM, including heap and stacks | 139,616 bytes | 520 KiB |
|
||||||
|
|
||||||
The executable plus persistent reservations consume 1,060,280 bytes of flash,
|
The executable plus persistent reservations consume 1,069,488 bytes of flash,
|
||||||
leaving 3,134,024 bytes. Allocated SRAM sections leave 393,136 bytes of link-time
|
leaving 3,124,816 bytes. Allocated SRAM sections leave 392,864 bytes of link-time
|
||||||
headroom; this is not a runtime heap high-water measurement. Core 0 has a
|
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
|
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.
|
catalog migration/compaction rather than overflowing its 4 KiB scratch bank.
|
||||||
|
|
|
||||||
|
|
@ -143,18 +143,20 @@ prove nonzero-effect fidelity. Exit-sniff requests alone failed to keep the
|
||||||
Pro active; that unproven automatic-wake code has been removed. The no-sniff
|
Pro active; that unproven automatic-wake code has been removed. The no-sniff
|
||||||
policy is an experimental per-link setting, not a qualified production default.
|
policy is an experimental per-link setting, not a qualified production default.
|
||||||
|
|
||||||
The current diagnostic build supports **same-boot** receive bounds 1/2/4 and
|
The diagnostic builds supported same-boot receive bounds 1/2/4 and incoming
|
||||||
incoming-credit thresholds 2/3, retaining the existing credit timer and three
|
credit thresholds 2/3 while retaining the existing credit timer and three
|
||||||
advertised receive buffers. Runtime controls were exercised and returned to
|
advertised receive buffers. These experiments did not establish a lossless
|
||||||
baseline receive bound 1 / credit threshold 2; the radio workload matrix is
|
mixed high-rate configuration.
|
||||||
awaiting normal Home/PS reconnects after flashing. Native approval remains on.
|
|
||||||
|
|
||||||
Temporary `build-aio/link_probe.cpp`, `link_probe_config.h`,
|
Temporary `build-aio/link_probe.cpp`, `link_probe_config.h`,
|
||||||
`link_probe_credit.patch`, and `SWITCH_PICO_LINK_PROBE` CMake/management hooks
|
`link_probe_credit.patch`, and `SWITCH_PICO_LINK_PROBE` hooks were removed
|
||||||
are **uncommitted experiment code**, not packaged release firmware.
|
when packaging the deadline-aware scheduler. Production settings remain one
|
||||||
Operation `0x44`, diagnostic schema 2, returns 468 bytes containing link
|
received packet per service turn, two-credit batching, normal link policy,
|
||||||
snapshots, selected HCI command/mode events, disconnect reasons, and control
|
and 300 MHz/1.3 V. Native approvals remain enabled.
|
||||||
completion state. OUT payload `<HH>` supports:
|
|
||||||
|
For historical reproduction, diagnostic operation `0x44` (schema 2, 468-byte
|
||||||
|
payload) captured link snapshots, selected HCI events, disconnect reasons and
|
||||||
|
control completion state. Its OUT payload `<HH>` supported:
|
||||||
|
|
||||||
- connected Pro handle + policy 1 (role-switch only), 5 (normal role-switch
|
- connected Pro handle + policy 1 (role-switch only), 5 (normal role-switch
|
||||||
plus sniff), or `0xffff` (readback);
|
plus sniff), or `0xffff` (readback);
|
||||||
|
|
@ -194,8 +196,57 @@ DualSense stream was in can-send timeout. Original interval 24 / policy 5
|
||||||
were restored and the connected DualSense native stream was re-armed.
|
were restored and the connected DualSense native stream was re-armed.
|
||||||
Do not promote interval-only tuning from command acceptance.
|
Do not promote interval-only tuning from command acceptance.
|
||||||
|
|
||||||
Keep approvals/bonds/profiles unchanged. Remove diagnostic hooks and restore
|
Further interval/window tests with attempt 1 / timeout 0 also failed:
|
||||||
or qualify settings before publishing another production build.
|
7.5 ms completed 464 of 1,025 Pro commands, dropped 560 and skipped 76
|
||||||
|
DualSense audio slots; 5 ms completed 162 of 513 Pro commands, dropped 350
|
||||||
|
and skipped 52 slots. Each coalesced one command. Original timing was restored.
|
||||||
|
|
||||||
|
A host-side outstanding-packet quota protected DualSense only by sacrificing
|
||||||
|
Pro delivery. At quotas 8/4/2/1, Pro completed 224/184/97/50 of 513 commands,
|
||||||
|
with 22/8/0/0 DualSense skips. A repeated quota-2 trial completed 95 commands
|
||||||
|
with zero DualSense skips; the repeated quota-8 baseline completed 207 with
|
||||||
|
27 skips. Quotas were restored and that experimental gate was removed.
|
||||||
|
|
||||||
|
### Shared deadline-aware scheduler implementation
|
||||||
|
|
||||||
|
The user selected a deadline-aware host scheduler rather than making an
|
||||||
|
unqualified power-policy change permanent. `input/native_output_scheduler.*`
|
||||||
|
now coordinates Nintendo and DualSense native senders:
|
||||||
|
|
||||||
|
- Fixed four-client table, no payload/PCM FIFO or heap allocation.
|
||||||
|
- Urgent real stop transitions first; earliest pending deadlines next;
|
||||||
|
rotating equal-deadline ties; bounded waves of synchronous grants.
|
||||||
|
- The last free controller ACL credit is held for an earlier announced
|
||||||
|
periodic deadline. This is host admission, not reserved on-air time.
|
||||||
|
- Future-reservation changes do not dispatch synchronously. A regression
|
||||||
|
reproduced the former rollover race: announcing the next PCM interval
|
||||||
|
dispatched later Pro work before the already-due PCM request was registered.
|
||||||
|
- Every send/abort releases one grant, identified by connection generation.
|
||||||
|
A stale completion cannot release a replacement connection's grant.
|
||||||
|
- Generic LED FIFO handoffs preserve fixture/drain ownership, without
|
||||||
|
retaining a notification that prevents the next PCM request.
|
||||||
|
- Both writers retain their original packet formats, gains, command lifetimes
|
||||||
|
and generation/expiry rules. No Bluetooth power-policy commands are added.
|
||||||
|
|
||||||
|
Software verification: 261 repository tests pass, including 11 dedicated
|
||||||
|
arbiter scenarios and both native sender lifecycle suites. All five firmware
|
||||||
|
variants build. High-rate radio qualification remains a separate failure:
|
||||||
|
the corrected scheduler candidate completed 342/1,025 Pro commands, coalesced
|
||||||
|
one, dropped 682, and skipped 48 DualSense audio slots under a zero-amplitude
|
||||||
|
changing-command workload. Both connections stayed stable. A mixed held-effect
|
||||||
|
run completed 33 state changes, coalesced 992 holds and dropped none out of
|
||||||
|
1,025 Pro commands, with 197 reports including refreshes; DualSense accepted
|
||||||
|
all host commands and skipped one audio slot. These are measured limits, not
|
||||||
|
a claim that host arbitration solves the remaining mixed-transport limit.
|
||||||
|
|
||||||
|
Final packaged-build check: all 24 profiles, active indices, aliases and names
|
||||||
|
were preserved; adapter configuration remained generation 13 with Pro native
|
||||||
|
approval enabled. A 16.6-second mixed held-effect run received 2,049 commands
|
||||||
|
per controller, with zero Pro drops, 1,984 coalesced holds, stable connections,
|
||||||
|
and one DualSense audio-slot skip. The user accepted this result as sufficient
|
||||||
|
for the current setup. No further power-policy tuning was applied, and no
|
||||||
|
lossless high-rate, Joy-Con, four-controller or captured-game qualification is
|
||||||
|
implied by that acceptance.
|
||||||
|
|
||||||
Linux's current Nintendo driver also documents disconnect risk from excessive
|
Linux's current Nintendo driver also documents disconnect risk from excessive
|
||||||
output traffic and uses input-report-aware throttling. This is corroborating
|
output traffic and uses input-report-aware throttling. This is corroborating
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -7,6 +7,9 @@
|
||||||
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
||||||
#include "input/haptics_experiment.h"
|
#include "input/haptics_experiment.h"
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
||||||
|
#include "input/native_output_scheduler.h"
|
||||||
|
#endif
|
||||||
#include "configuration/configuration_service.h"
|
#include "configuration/configuration_service.h"
|
||||||
#include "profile/profile_service.h"
|
#include "profile/profile_service.h"
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
|
@ -1887,16 +1890,12 @@ uni_platform* get_platform() {
|
||||||
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
||||||
extern "C" bool uni_platform_on_l2cap_can_send_now(
|
extern "C" bool uni_platform_on_l2cap_can_send_now(
|
||||||
uni_hid_device_t* device, uint16_t cid) {
|
uni_hid_device_t* device, uint16_t cid) {
|
||||||
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
bool block_generic = false;
|
||||||
// Nintendo shares this event with queued LED/subcommand output. Let the
|
|
||||||
// normal queue run too; its parser refreshes payload/counter at submission.
|
|
||||||
if (switch_native_output_on_can_send_now(device, cid)) return false;
|
|
||||||
#endif
|
|
||||||
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
||||||
return haptics_experiment_on_can_send_now(device, cid);
|
block_generic = haptics_experiment_blocks_generic(device);
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
#endif
|
||||||
|
const bool consumed = native_output_scheduler_on_can_send_now(device, cid);
|
||||||
|
return consumed || block_generic;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
@ -1959,6 +1958,9 @@ void bluepad32_input_backend_init() {
|
||||||
critical_section_init(&g_state_lock);
|
critical_section_init(&g_state_lock);
|
||||||
configuration_service_prepare();
|
configuration_service_prepare();
|
||||||
profile_service_prepare();
|
profile_service_prepare();
|
||||||
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
||||||
|
native_output_scheduler_prepare();
|
||||||
|
#endif
|
||||||
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
||||||
switch_native_output_prepare();
|
switch_native_output_prepare();
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "input/haptics_experiment.h"
|
#include "input/haptics_experiment.h"
|
||||||
#include "input/haptics_transport_probe.h"
|
#include "input/haptics_transport_probe.h"
|
||||||
|
#include "input/native_output_scheduler.h"
|
||||||
#include "input/switch_hd_rumble_synth.h"
|
#include "input/switch_hd_rumble_synth.h"
|
||||||
|
|
||||||
#include <btstack.h>
|
#include <btstack.h>
|
||||||
|
|
@ -122,9 +123,10 @@ static const int8_t HAPTICS_DATA kSine[30] = {
|
||||||
|
|
||||||
void cadence_timer(btstack_timer_source_t*);
|
void cadence_timer(btstack_timer_source_t*);
|
||||||
void lifecycle_timer(btstack_timer_source_t*);
|
void lifecycle_timer(btstack_timer_source_t*);
|
||||||
void request_send();
|
void request_send(bool promote = false);
|
||||||
void restore_compatibility(HapticsExperimentState state);
|
void restore_compatibility(HapticsExperimentState state);
|
||||||
void start_stream();
|
void start_stream();
|
||||||
|
bool on_output_grant(uni_hid_device_t* device, uint16_t cid, uint32_t generation);
|
||||||
|
|
||||||
bool gameplay() {
|
bool gameplay() {
|
||||||
return g_diagnostics.mode == 1;
|
return g_diagnostics.mode == 1;
|
||||||
|
|
@ -256,8 +258,24 @@ void schedule_lifecycle(uint64_t deadline_us) {
|
||||||
schedule_timer(&g_lifecycle_timer, &g_lifecycle_armed, deadline_us);
|
schedule_timer(&g_lifecycle_timer, &g_lifecycle_armed, deadline_us);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void schedule_packet() {
|
||||||
|
const uint64_t due_us = packet_due(g_next_packet);
|
||||||
|
native_output_scheduler_reserve(g_connection.device,
|
||||||
|
g_connection.generation, due_us);
|
||||||
|
schedule_timer(&g_cadence_timer, &g_cadence_armed, due_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reserve_after_packet() {
|
||||||
|
const uint64_t due_us =
|
||||||
|
g_phase == Phase::kPattern && (gameplay() || g_next_packet + 1 < kPackets)
|
||||||
|
? packet_due(g_next_packet + 1) : UINT64_MAX;
|
||||||
|
native_output_scheduler_reserve(g_connection.device,
|
||||||
|
g_connection.generation, due_us);
|
||||||
|
}
|
||||||
|
|
||||||
void finish(HapticsExperimentState state, uint8_t error) {
|
void finish(HapticsExperimentState state, uint8_t error) {
|
||||||
haptics_transport_probe_end();
|
haptics_transport_probe_end();
|
||||||
|
native_output_scheduler_cancel(g_connection.device);
|
||||||
cancel_timer(&g_cadence_timer, &g_cadence_armed);
|
cancel_timer(&g_cadence_timer, &g_cadence_armed);
|
||||||
cancel_timer(&g_lifecycle_timer, &g_lifecycle_armed);
|
cancel_timer(&g_lifecycle_timer, &g_lifecycle_armed);
|
||||||
g_send_requested = false;
|
g_send_requested = false;
|
||||||
|
|
@ -283,8 +301,8 @@ void timeout_drain() {
|
||||||
}
|
}
|
||||||
++g_diagnostics.send_failures;
|
++g_diagnostics.send_failures;
|
||||||
g_diagnostics.last_error = kTimeout;
|
g_diagnostics.last_error = kTimeout;
|
||||||
// The outstanding BTstack notification cannot be canceled. Stop accepting
|
// Cancel native admission; an outstanding stack notification may still
|
||||||
// it as PCM permission; the normal FIFO may use it for compatibility output.
|
// arrive later and belongs to the ordinary compatibility FIFO.
|
||||||
g_send_requested = false;
|
g_send_requested = false;
|
||||||
restore_compatibility(HapticsExperimentState::kError);
|
restore_compatibility(HapticsExperimentState::kError);
|
||||||
}
|
}
|
||||||
|
|
@ -299,8 +317,10 @@ void begin_drain(HapticsExperimentState state, uint64_t deadline_us) {
|
||||||
schedule_lifecycle(deadline_us);
|
schedule_lifecycle(deadline_us);
|
||||||
if (time_us_64() >= deadline_us) {
|
if (time_us_64() >= deadline_us) {
|
||||||
timeout_drain();
|
timeout_drain();
|
||||||
} else if (!g_send_requested) {
|
} else {
|
||||||
request_send();
|
// Promote a pending tone in place: its eventual grant must send the
|
||||||
|
// urgent stop, without requesting a second stack notification.
|
||||||
|
request_send(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,6 +342,7 @@ void restore_compatibility(HapticsExperimentState state) {
|
||||||
g_accept_host = false;
|
g_accept_host = false;
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
cancel_timer(&g_cadence_timer, &g_cadence_armed);
|
cancel_timer(&g_cadence_timer, &g_cadence_armed);
|
||||||
|
native_output_scheduler_cancel(g_connection.device);
|
||||||
g_send_requested = false;
|
g_send_requested = false;
|
||||||
g_phase = Phase::kRestore;
|
g_phase = Phase::kRestore;
|
||||||
g_finish_state = g_diagnostics.send_failures != 0
|
g_finish_state = g_diagnostics.send_failures != 0
|
||||||
|
|
@ -343,20 +364,45 @@ void restore_compatibility(HapticsExperimentState state) {
|
||||||
publish();
|
publish();
|
||||||
}
|
}
|
||||||
|
|
||||||
void request_send() {
|
void request_send(bool promote) {
|
||||||
if (g_send_requested || g_request_in_progress ||
|
if ((g_send_requested && !promote) || g_request_in_progress ||
|
||||||
(g_phase != Phase::kPattern && g_phase != Phase::kDrain)) {
|
(g_phase != Phase::kPattern && g_phase != Phase::kDrain)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_request_us = time_us_64();
|
const uint64_t now_us = time_us_64();
|
||||||
|
const bool stopping = g_phase == Phase::kDrain;
|
||||||
|
if (!stopping) {
|
||||||
|
if (now_us >= g_end_us) {
|
||||||
|
end_pattern();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (now_us < packet_due(g_next_packet)) {
|
||||||
|
schedule_packet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint32_t current = static_cast<uint32_t>(
|
||||||
|
((now_us - g_start_us) * kPacketDenominator) / packet_numerator_us());
|
||||||
|
g_diagnostics.skipped_packets += current - g_next_packet;
|
||||||
|
g_next_packet = current;
|
||||||
|
reserve_after_packet();
|
||||||
|
}
|
||||||
|
if (!g_send_requested) {
|
||||||
|
g_request_us = now_us;
|
||||||
|
}
|
||||||
g_send_requested = true;
|
g_send_requested = true;
|
||||||
g_request_in_progress = true;
|
g_request_in_progress = true;
|
||||||
++g_diagnostics.can_send_requests;
|
++g_diagnostics.can_send_requests;
|
||||||
if (gameplay() && g_phase == Phase::kPattern) {
|
if (gameplay() && g_phase == Phase::kPattern) {
|
||||||
schedule_lifecycle(g_request_us + kDrainTimeoutUs);
|
schedule_lifecycle(g_request_us + kDrainTimeoutUs);
|
||||||
}
|
}
|
||||||
const uint8_t status =
|
const uint8_t status = native_output_scheduler_request(
|
||||||
l2cap_request_can_send_now_event(g_connection.cid);
|
g_connection.device, g_connection.generation,
|
||||||
|
stopping ? now_us : packet_due(g_next_packet), stopping, on_output_grant);
|
||||||
|
if (stopping && g_send_requested) {
|
||||||
|
// Clear cadence only after promotion: reserve() can pump a pending
|
||||||
|
// request synchronously, so the urgent bit must already be installed.
|
||||||
|
reserve_after_packet();
|
||||||
|
}
|
||||||
g_request_in_progress = false;
|
g_request_in_progress = false;
|
||||||
if (status == ERROR_CODE_SUCCESS && !g_send_requested) {
|
if (status == ERROR_CODE_SUCCESS && !g_send_requested) {
|
||||||
// The synchronous callback already published its result and armed the
|
// The synchronous callback already published its result and armed the
|
||||||
|
|
@ -380,8 +426,7 @@ void request_send() {
|
||||||
g_diagnostics.skipped_packets += current + 1 - g_next_packet;
|
g_diagnostics.skipped_packets += current + 1 - g_next_packet;
|
||||||
g_next_packet = current + 1;
|
g_next_packet = current + 1;
|
||||||
if (gameplay() || g_next_packet < kPackets) {
|
if (gameplay() || g_next_packet < kPackets) {
|
||||||
schedule_timer(&g_cadence_timer, &g_cadence_armed,
|
schedule_packet();
|
||||||
packet_due(g_next_packet));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (g_phase == Phase::kDrain) {
|
} else if (g_phase == Phase::kDrain) {
|
||||||
|
|
@ -505,8 +550,7 @@ void cadence_timer(btstack_timer_source_t*) {
|
||||||
if (now_us >= g_end_us) {
|
if (now_us >= g_end_us) {
|
||||||
end_pattern();
|
end_pattern();
|
||||||
} else if (now_us < packet_due(g_next_packet)) {
|
} else if (now_us < packet_due(g_next_packet)) {
|
||||||
schedule_timer(&g_cadence_timer, &g_cadence_armed,
|
schedule_packet();
|
||||||
packet_due(g_next_packet));
|
|
||||||
} else {
|
} else {
|
||||||
request_send();
|
request_send();
|
||||||
}
|
}
|
||||||
|
|
@ -753,6 +797,7 @@ void haptics_experiment_detach(uni_hid_device_t* device) {
|
||||||
if (device == nullptr) {
|
if (device == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
native_output_scheduler_cancel(device);
|
||||||
if (g_phase != Phase::kIdle && g_connection.device == device) {
|
if (g_phase != Phase::kIdle && g_connection.device == device) {
|
||||||
// Do not dereference the device or send restoration on a dead link.
|
// Do not dereference the device or send restoration on a dead link.
|
||||||
finish(HapticsExperimentState::kDisconnected, kConnection);
|
finish(HapticsExperimentState::kDisconnected, kConnection);
|
||||||
|
|
@ -824,11 +869,24 @@ bool haptics_experiment_feedback(uni_hid_device_t* device,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool haptics_experiment_blocks_generic(const uni_hid_device_t* device) {
|
||||||
|
return haptics_experiment_owns(device) &&
|
||||||
|
(g_phase == Phase::kDrain ||
|
||||||
|
(g_phase == Phase::kPattern && !gameplay()));
|
||||||
|
}
|
||||||
|
|
||||||
bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
namespace {
|
||||||
uint16_t cid) {
|
|
||||||
if (device != g_connection.device || !connection_current() ||
|
bool HAPTICS_HOT(on_output_grant)(uni_hid_device_t* device, uint16_t cid, uint32_t generation) {
|
||||||
|
if (!native_output_scheduler_granted(device)) {
|
||||||
|
return haptics_experiment_blocks_generic(device);
|
||||||
|
}
|
||||||
|
if (g_in_callback) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (device != g_connection.device || generation != g_connection.generation || !connection_current() ||
|
||||||
(g_phase != Phase::kPattern && g_phase != Phase::kDrain)) {
|
(g_phase != Phase::kPattern && g_phase != Phase::kDrain)) {
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (gameplay() && g_phase == Phase::kPattern && !g_in_callback &&
|
if (gameplay() && g_phase == Phase::kPattern && !g_in_callback &&
|
||||||
|
|
@ -840,12 +898,13 @@ bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
||||||
g_send_requested = false;
|
g_send_requested = false;
|
||||||
schedule_timer(&g_cadence_timer, &g_cadence_armed, time_us_64() + 1000);
|
schedule_timer(&g_cadence_timer, &g_cadence_armed, time_us_64() + 1000);
|
||||||
}
|
}
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// The generic FIFO is device-wide, not CID-specific. Consume control-CID
|
// Only the scheduler's current interrupt-CID grant is PCM permission.
|
||||||
// and unsolicited events too, without treating them as PCM permission.
|
if (cid != g_connection.cid || !g_send_requested) {
|
||||||
if (cid != g_connection.cid || !g_send_requested || g_in_callback) {
|
native_output_scheduler_complete(device, generation);
|
||||||
return true;
|
return haptics_experiment_blocks_generic(device);
|
||||||
}
|
}
|
||||||
g_send_requested = false;
|
g_send_requested = false;
|
||||||
g_in_callback = true;
|
g_in_callback = true;
|
||||||
|
|
@ -864,15 +923,16 @@ bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
||||||
schedule_lifecycle(g_end_us + kDrainTimeoutUs);
|
schedule_lifecycle(g_end_us + kDrainTimeoutUs);
|
||||||
}
|
}
|
||||||
if (g_phase == Phase::kDrain && now_us >= g_lifecycle_due_us) {
|
if (g_phase == Phase::kDrain && now_us >= g_lifecycle_due_us) {
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
timeout_drain();
|
timeout_drain();
|
||||||
g_in_callback = false;
|
g_in_callback = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (g_phase == Phase::kPattern && now_us < packet_due(g_next_packet)) {
|
if (g_phase == Phase::kPattern && now_us < packet_due(g_next_packet)) {
|
||||||
// Notifications are not reservations of credit for a future deadline.
|
// Notifications are not reservations of credit for a future deadline.
|
||||||
schedule_timer(&g_cadence_timer, &g_cadence_armed,
|
schedule_packet();
|
||||||
packet_due(g_next_packet));
|
|
||||||
g_in_callback = false;
|
g_in_callback = false;
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
publish();
|
publish();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -885,6 +945,7 @@ bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
||||||
g_next_packet = current;
|
g_next_packet = current;
|
||||||
due_us = packet_due(current);
|
due_us = packet_due(current);
|
||||||
}
|
}
|
||||||
|
reserve_after_packet();
|
||||||
uint8_t report[kReportBytes];
|
uint8_t report[kReportBytes];
|
||||||
const uint64_t generate_start_us = time_us_64();
|
const uint64_t generate_start_us = time_us_64();
|
||||||
const bool tone = generate_packet(report, g_next_packet, stopping);
|
const bool tone = generate_packet(report, g_next_packet, stopping);
|
||||||
|
|
@ -897,17 +958,27 @@ bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
||||||
if (!stopping && submit_us >= packet_due(g_next_packet + 1)) {
|
if (!stopping && submit_us >= packet_due(g_next_packet + 1)) {
|
||||||
// A flash/interrupt stall can occur during synthesis as well as before
|
// A flash/interrupt stall can occur during synthesis as well as before
|
||||||
// CAN_SEND_NOW. Never submit a now-obsolete tone after its phase ended.
|
// CAN_SEND_NOW. Never submit a now-obsolete tone after its phase ended.
|
||||||
++g_diagnostics.skipped_packets;
|
uint32_t next = static_cast<uint32_t>(
|
||||||
++g_next_packet;
|
((submit_us - g_start_us) * kPacketDenominator) /
|
||||||
|
packet_numerator_us()) + 1;
|
||||||
|
if (!gameplay() && next > kPackets) {
|
||||||
|
next = kPackets;
|
||||||
|
}
|
||||||
|
g_diagnostics.skipped_packets += next - g_next_packet;
|
||||||
|
g_next_packet = next;
|
||||||
if (gameplay() || g_next_packet < kPackets) {
|
if (gameplay() || g_next_packet < kPackets) {
|
||||||
schedule_timer(&g_cadence_timer, &g_cadence_armed,
|
schedule_packet();
|
||||||
packet_due(g_next_packet));
|
} else {
|
||||||
|
native_output_scheduler_reserve(device, g_connection.generation,
|
||||||
|
UINT64_MAX);
|
||||||
}
|
}
|
||||||
g_in_callback = false;
|
g_in_callback = false;
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
publish();
|
publish();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (stopping && submit_us >= g_lifecycle_due_us) {
|
if (stopping && submit_us >= g_lifecycle_due_us) {
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
timeout_drain();
|
timeout_drain();
|
||||||
g_in_callback = false;
|
g_in_callback = false;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -915,6 +986,7 @@ bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
||||||
const uint64_t send_started_us = time_us_64();
|
const uint64_t send_started_us = time_us_64();
|
||||||
const uint8_t status = l2cap_send(cid, report, sizeof(report));
|
const uint8_t status = l2cap_send(cid, report, sizeof(report));
|
||||||
const uint64_t send_returned_us = time_us_64();
|
const uint64_t send_returned_us = time_us_64();
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
haptics_transport_probe_send(
|
haptics_transport_probe_send(
|
||||||
static_cast<uint32_t>(send_returned_us - send_started_us),
|
static_cast<uint32_t>(send_returned_us - send_started_us),
|
||||||
static_cast<uint32_t>(send_returned_us),
|
static_cast<uint32_t>(send_returned_us),
|
||||||
|
|
@ -952,8 +1024,7 @@ bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
||||||
} else {
|
} else {
|
||||||
++g_next_packet;
|
++g_next_packet;
|
||||||
if (gameplay() || g_next_packet < kPackets) {
|
if (gameplay() || g_next_packet < kPackets) {
|
||||||
schedule_timer(&g_cadence_timer, &g_cadence_armed,
|
schedule_packet();
|
||||||
packet_due(g_next_packet));
|
|
||||||
}
|
}
|
||||||
if (gameplay()) {
|
if (gameplay()) {
|
||||||
schedule_lifecycle(packet_due(g_next_packet) + kDrainTimeoutUs);
|
schedule_lifecycle(packet_due(g_next_packet) + kDrainTimeoutUs);
|
||||||
|
|
@ -963,3 +1034,5 @@ bool HAPTICS_HOT(haptics_experiment_on_can_send_now)(uni_hid_device_t* device,
|
||||||
publish();
|
publish();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
|
||||||
|
|
@ -67,5 +67,5 @@ bool haptics_experiment_owns(const uni_hid_device_t* device);
|
||||||
bool haptics_experiment_gameplay_owns(const uni_hid_device_t* device);
|
bool haptics_experiment_gameplay_owns(const uni_hid_device_t* device);
|
||||||
bool haptics_experiment_feedback(uni_hid_device_t* device,
|
bool haptics_experiment_feedback(uni_hid_device_t* device,
|
||||||
uint8_t low, uint8_t high, uint16_t duration_ms);
|
uint8_t low, uint8_t high, uint16_t duration_ms);
|
||||||
bool haptics_experiment_on_can_send_now(uni_hid_device_t* device,
|
// Preserve fixture/drain exclusivity on unsolicited or control-CID events.
|
||||||
uint16_t cid);
|
bool haptics_experiment_blocks_generic(const uni_hid_device_t* device);
|
||||||
|
|
|
||||||
304
src/firmware/input/native_output_scheduler.cpp
Normal file
304
src/firmware/input/native_output_scheduler.cpp
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
#include "input/native_output_scheduler.h"
|
||||||
|
|
||||||
|
#include <btstack.h>
|
||||||
|
#include <pico/stdlib.h>
|
||||||
|
#include <uni.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint8_t kCapacity = 4;
|
||||||
|
constexpr uint8_t kNoClient = kCapacity;
|
||||||
|
constexpr uint8_t kNoCapacity = 0x07;
|
||||||
|
constexpr uint8_t kInvalidConnection = 0x02;
|
||||||
|
|
||||||
|
struct Client {
|
||||||
|
uni_hid_device_t* device = nullptr;
|
||||||
|
uint32_t generation = 0;
|
||||||
|
uint16_t cid = 0;
|
||||||
|
uint16_t handle = 0;
|
||||||
|
uint64_t deadline_us = 0;
|
||||||
|
uint64_t periodic_deadline_us = 0;
|
||||||
|
NativeOutputGrant callback = nullptr;
|
||||||
|
bool pending = false;
|
||||||
|
bool waiting = false;
|
||||||
|
bool urgent = false;
|
||||||
|
bool reserved = false;
|
||||||
|
bool generic_deferred = false;
|
||||||
|
uint8_t request_status = 0;
|
||||||
|
};
|
||||||
|
struct EventContext {
|
||||||
|
uni_hid_device_t* device;
|
||||||
|
uint16_t cid;
|
||||||
|
bool consumed = false;
|
||||||
|
EventContext* previous;
|
||||||
|
};
|
||||||
|
|
||||||
|
Client clients[kCapacity];
|
||||||
|
uint8_t granted = kNoClient;
|
||||||
|
uint8_t cursor = 0;
|
||||||
|
bool prepared = false;
|
||||||
|
bool pumping = false;
|
||||||
|
bool repump = false;
|
||||||
|
bool timer_armed = false;
|
||||||
|
btstack_timer_source_t retry_timer{};
|
||||||
|
EventContext* current_event = nullptr;
|
||||||
|
|
||||||
|
void pump();
|
||||||
|
|
||||||
|
bool current(const Client& client) {
|
||||||
|
return client.device != nullptr && client.device->conn.connected &&
|
||||||
|
client.device->conn.interrupt_cid == client.cid &&
|
||||||
|
client.device->conn.handle == client.handle && client.cid != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void retire(uint8_t index) {
|
||||||
|
if (granted == index) granted = kNoClient;
|
||||||
|
clients[index] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t find(uni_hid_device_t* device, uint32_t generation, bool allocate) {
|
||||||
|
uint8_t empty = kNoClient;
|
||||||
|
for (uint8_t i = 0; i < kCapacity; ++i) {
|
||||||
|
Client& client = clients[i];
|
||||||
|
if (client.device == device) {
|
||||||
|
if (client.generation != generation || !current(client)) retire(i);
|
||||||
|
else return i;
|
||||||
|
} else if (client.device != nullptr && !current(client)) {
|
||||||
|
retire(i);
|
||||||
|
}
|
||||||
|
if (client.device == nullptr && empty == kNoClient) empty = i;
|
||||||
|
}
|
||||||
|
if (!allocate || empty == kNoClient) return kNoClient;
|
||||||
|
clients[empty].device = device;
|
||||||
|
clients[empty].generation = generation;
|
||||||
|
clients[empty].cid = device->conn.interrupt_cid;
|
||||||
|
clients[empty].handle = device->conn.handle;
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool earlier(const Client& first, const Client& second) {
|
||||||
|
if (first.urgent != second.urgent) return first.urgent;
|
||||||
|
return first.deadline_us < second.deadline_us;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool generic_event_in_progress(const uni_hid_device_t* device) {
|
||||||
|
for (auto* event = current_event; event != nullptr; event = event->previous)
|
||||||
|
if (event->device == device) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool admission_allowed(uint8_t index) {
|
||||||
|
const Client& client = clients[index];
|
||||||
|
if (client.urgent) return true;
|
||||||
|
if (hci_number_free_acl_slots_for_handle(client.handle) != 1) return true;
|
||||||
|
for (uint8_t i = 0; i < kCapacity; ++i) {
|
||||||
|
if (i == index || !current(clients[i]) || !clients[i].reserved) continue;
|
||||||
|
const uint64_t deadline = clients[i].periodic_deadline_us;
|
||||||
|
if (deadline < client.deadline_us ||
|
||||||
|
(deadline == client.deadline_us &&
|
||||||
|
(i + kCapacity - cursor) % kCapacity < (index + kCapacity - cursor) % kCapacity))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void retry(btstack_timer_source_t*) {
|
||||||
|
timer_armed = false;
|
||||||
|
pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
void arm_retry(uint64_t due_us) {
|
||||||
|
if (timer_armed) btstack_run_loop_remove_timer(&retry_timer);
|
||||||
|
const uint64_t now_ms = time_us_64() / 1000;
|
||||||
|
const uint64_t due_ms = due_us / 1000;
|
||||||
|
const uint32_t delay = due_ms > now_ms + 1 ? due_ms - now_ms - 1 : 0;
|
||||||
|
btstack_run_loop_set_timer_handler(&retry_timer, retry);
|
||||||
|
btstack_run_loop_set_timer(&retry_timer, delay);
|
||||||
|
timer_armed = true;
|
||||||
|
btstack_run_loop_add_timer(&retry_timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pump() {
|
||||||
|
if (!prepared) return;
|
||||||
|
if (pumping) {
|
||||||
|
repump = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pumping = true;
|
||||||
|
if (timer_armed) {
|
||||||
|
btstack_run_loop_remove_timer(&retry_timer);
|
||||||
|
timer_armed = false;
|
||||||
|
}
|
||||||
|
uint64_t retry_at = UINT64_MAX;
|
||||||
|
bool exhausted = true;
|
||||||
|
// Bound synchronous callbacks/self-resubmission. A fresh loop turn services
|
||||||
|
// receive processing and timers before another bounded wave of grants.
|
||||||
|
for (uint8_t work = 0; work < kCapacity * 2; ++work) {
|
||||||
|
repump = false;
|
||||||
|
for (uint8_t i = 0; i < kCapacity; ++i)
|
||||||
|
if (clients[i].device != nullptr && !current(clients[i])) retire(i);
|
||||||
|
if (granted != kNoClient) {
|
||||||
|
exhausted = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t selected = kNoClient;
|
||||||
|
for (uint8_t offset = 0; offset < kCapacity; ++offset) {
|
||||||
|
const uint8_t i = (cursor + offset) % kCapacity;
|
||||||
|
const Client& client = clients[i];
|
||||||
|
if (!client.pending || !current(client) ||
|
||||||
|
!l2cap_can_send_packet_now(client.cid) || !admission_allowed(i)) continue;
|
||||||
|
if (selected == kNoClient || earlier(client, clients[selected])) selected = i;
|
||||||
|
}
|
||||||
|
if (selected != kNoClient) {
|
||||||
|
Client& client = clients[selected];
|
||||||
|
const auto callback = client.callback;
|
||||||
|
auto* device = client.device;
|
||||||
|
const uint16_t cid = client.cid;
|
||||||
|
const uint32_t generation = client.generation;
|
||||||
|
client.pending = false;
|
||||||
|
client.request_status = 0;
|
||||||
|
client.generic_deferred = false;
|
||||||
|
granted = selected;
|
||||||
|
cursor = (selected + 1) % kCapacity;
|
||||||
|
const bool consumed = callback(device, cid, generation);
|
||||||
|
for (auto* event = current_event; event != nullptr; event = event->previous)
|
||||||
|
if (event->device == device && event->cid == cid) event->consumed |= consumed;
|
||||||
|
const bool yield_generic = !consumed && client.device == device &&
|
||||||
|
client.generation == generation && client.cid == cid && current(client) &&
|
||||||
|
!uni_circular_buffer_is_empty(&device->outgoing_buffer) &&
|
||||||
|
!generic_event_in_progress(device);
|
||||||
|
if (yield_generic && granted == selected) client.generic_deferred = true;
|
||||||
|
if (yield_generic && granted == kNoClient && !client.waiting) {
|
||||||
|
client.waiting = true;
|
||||||
|
const uint8_t status = l2cap_request_can_send_now_event(cid);
|
||||||
|
if (client.device == device && client.generation == generation && status != 0)
|
||||||
|
client.waiting = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// No ready client can use the resource. Register one notification per
|
||||||
|
// pending CID; ready-state and deadline selection are rechecked when any
|
||||||
|
// callback arrives, rather than treating a callback as reserved airtime.
|
||||||
|
for (uint8_t offset = 0; offset < kCapacity; ++offset) {
|
||||||
|
const uint8_t i = (cursor + offset) % kCapacity;
|
||||||
|
Client& client = clients[i];
|
||||||
|
if (!client.pending || !current(client)) continue;
|
||||||
|
if (!admission_allowed(i)) {
|
||||||
|
for (uint8_t other = 0; other < kCapacity; ++other) {
|
||||||
|
if (other == i || !current(clients[other]) || !clients[other].reserved) continue;
|
||||||
|
const uint64_t due = clients[other].periodic_deadline_us;
|
||||||
|
const uint64_t next = due > time_us_64() ? due : time_us_64() + 1000;
|
||||||
|
if (next < retry_at) retry_at = next;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (client.waiting) continue;
|
||||||
|
const uint32_t generation = client.generation;
|
||||||
|
auto* device = client.device;
|
||||||
|
const uint16_t cid = client.cid;
|
||||||
|
client.waiting = true;
|
||||||
|
const uint8_t status = l2cap_request_can_send_now_event(cid);
|
||||||
|
if (client.device != device || client.generation != generation || client.cid != cid) continue;
|
||||||
|
if (status != 0) {
|
||||||
|
client.waiting = false;
|
||||||
|
client.pending = false;
|
||||||
|
client.request_status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!repump) {
|
||||||
|
exhausted = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exhausted && granted == kNoClient) retry_at = time_us_64() + 1000;
|
||||||
|
if (granted == kNoClient && retry_at != UINT64_MAX) arm_retry(retry_at);
|
||||||
|
pumping = false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void native_output_scheduler_prepare() {
|
||||||
|
if (prepared) return;
|
||||||
|
prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t native_output_scheduler_request(uni_hid_device_t* device,
|
||||||
|
uint32_t generation,
|
||||||
|
uint64_t deadline_us,
|
||||||
|
bool urgent_stop,
|
||||||
|
NativeOutputGrant callback) {
|
||||||
|
if (!prepared || device == nullptr || callback == nullptr || !device->conn.connected ||
|
||||||
|
device->conn.interrupt_cid == 0) return kInvalidConnection;
|
||||||
|
const uint8_t index = find(device, generation, true);
|
||||||
|
if (index == kNoClient) return kNoCapacity;
|
||||||
|
Client& client = clients[index];
|
||||||
|
client.deadline_us = deadline_us;
|
||||||
|
client.urgent = urgent_stop;
|
||||||
|
client.callback = callback;
|
||||||
|
client.pending = true;
|
||||||
|
client.request_status = 0;
|
||||||
|
pump();
|
||||||
|
return client.device == device && client.generation == generation ? client.request_status : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void native_output_scheduler_reserve(uni_hid_device_t* device,
|
||||||
|
uint32_t generation,
|
||||||
|
uint64_t deadline_us) {
|
||||||
|
if (!prepared || device == nullptr || !device->conn.connected || !device->conn.interrupt_cid) return;
|
||||||
|
const uint8_t index = find(device, generation, deadline_us != UINT64_MAX);
|
||||||
|
if (index == kNoClient) return;
|
||||||
|
clients[index].reserved = deadline_us != UINT64_MAX;
|
||||||
|
clients[index].periodic_deadline_us = deadline_us;
|
||||||
|
// Writers may announce the following interval immediately before requesting
|
||||||
|
// the current due packet. Never dispatch competing work in that gap.
|
||||||
|
// A standalone reservation release still wakes pending work next loop turn.
|
||||||
|
if (!pumping && granted == kNoClient) {
|
||||||
|
for (const auto& client : clients) {
|
||||||
|
if (client.pending && current(client) && l2cap_can_send_packet_now(client.cid)) {
|
||||||
|
arm_retry(time_us_64() + 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void native_output_scheduler_complete(uni_hid_device_t* device, uint32_t generation) {
|
||||||
|
const uint8_t index = granted;
|
||||||
|
const bool owned = index != kNoClient && clients[index].device == device &&
|
||||||
|
clients[index].generation == generation;
|
||||||
|
const bool wake_generic = owned && clients[index].generic_deferred &&
|
||||||
|
!generic_event_in_progress(device);
|
||||||
|
if (owned) clients[index].generic_deferred = false;
|
||||||
|
if (owned) granted = kNoClient;
|
||||||
|
pump();
|
||||||
|
if (wake_generic && clients[index].device == device && clients[index].generation == generation &&
|
||||||
|
current(clients[index]) && granted != index && !clients[index].waiting &&
|
||||||
|
!uni_circular_buffer_is_empty(&device->outgoing_buffer)) {
|
||||||
|
clients[index].waiting = true;
|
||||||
|
const uint8_t status = l2cap_request_can_send_now_event(clients[index].cid);
|
||||||
|
if (clients[index].device == device && clients[index].generation == generation && status != 0)
|
||||||
|
clients[index].waiting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void native_output_scheduler_cancel(uni_hid_device_t* device) {
|
||||||
|
if (!prepared || device == nullptr) return;
|
||||||
|
for (uint8_t i = 0; i < kCapacity; ++i)
|
||||||
|
if (clients[i].device == device) retire(i);
|
||||||
|
pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool native_output_scheduler_granted(const uni_hid_device_t* device) {
|
||||||
|
return granted != kNoClient && clients[granted].device == device && current(clients[granted]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool native_output_scheduler_on_can_send_now(uni_hid_device_t* device, uint16_t cid) {
|
||||||
|
if (!prepared || device == nullptr) return false;
|
||||||
|
for (auto& client : clients)
|
||||||
|
if (client.device == device && client.cid == cid && current(client)) client.waiting = false;
|
||||||
|
const bool leased = granted != kNoClient && clients[granted].device == device &&
|
||||||
|
current(clients[granted]);
|
||||||
|
EventContext event{device, cid, leased, current_event};
|
||||||
|
current_event = &event;
|
||||||
|
pump();
|
||||||
|
current_event = event.previous;
|
||||||
|
return event.consumed;
|
||||||
|
}
|
||||||
33
src/firmware/input/native_output_scheduler.h
Normal file
33
src/firmware/input/native_output_scheduler.h
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct uni_hid_device_s;
|
||||||
|
using NativeOutputGrant = bool (*)(uni_hid_device_s*, uint16_t, uint32_t);
|
||||||
|
|
||||||
|
// Fixed capacity: one native output client per physical controller, four total.
|
||||||
|
// prepare() precedes Core 1 launch; all other calls belong to BTstack/Core 1.
|
||||||
|
// Callbacks run without locks, at most one grant at a time. A callback's bool
|
||||||
|
// retains the existing generic-FIFO consumption convention (Sony true, Nintendo
|
||||||
|
// false). The grantee MUST complete() after its single send attempt or an abort;
|
||||||
|
// a grant must never be held across a timer/event-loop wait.
|
||||||
|
void native_output_scheduler_prepare();
|
||||||
|
uint8_t native_output_scheduler_request(uni_hid_device_s* device,
|
||||||
|
uint32_t generation,
|
||||||
|
uint64_t deadline_us,
|
||||||
|
bool urgent_stop,
|
||||||
|
NativeOutputGrant grant);
|
||||||
|
// Announce the next periodic deadline without requesting an early send. The
|
||||||
|
// arbiter leaves the last free controller ACL credit for an earlier announced
|
||||||
|
// deadline, with rotating ties, except urgent stops. UINT64_MAX clears this
|
||||||
|
// reservation without canceling a pending request. This is host admission, not
|
||||||
|
// an on-air reservation.
|
||||||
|
// This update never invokes a grant callback synchronously.
|
||||||
|
void native_output_scheduler_reserve(uni_hid_device_s* device,
|
||||||
|
uint32_t generation,
|
||||||
|
uint64_t deadline_us);
|
||||||
|
void native_output_scheduler_complete(uni_hid_device_s* device, uint32_t generation);
|
||||||
|
void native_output_scheduler_cancel(uni_hid_device_s* device);
|
||||||
|
bool native_output_scheduler_granted(const uni_hid_device_s* device);
|
||||||
|
bool native_output_scheduler_on_can_send_now(uni_hid_device_s* device,
|
||||||
|
uint16_t cid);
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "input/switch_native_output.h"
|
#include "input/switch_native_output.h"
|
||||||
|
#include "input/native_output_scheduler.h"
|
||||||
#include "usb/switch/switch_native_haptics.h"
|
#include "usb/switch/switch_native_haptics.h"
|
||||||
#include "usb/switch/switch_haptics_amplitudes.h"
|
#include "usb/switch/switch_haptics_amplitudes.h"
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ struct SharedSlot {
|
||||||
uint32_t generation = 0;
|
uint32_t generation = 0;
|
||||||
bool accepting = false;
|
bool accepting = false;
|
||||||
bool stop_pending = false;
|
bool stop_pending = false;
|
||||||
|
bool host_active = false;
|
||||||
bool lost = false;
|
bool lost = false;
|
||||||
uint8_t head = 0, count = 0;
|
uint8_t head = 0, count = 0;
|
||||||
Command queue[kCapacity]{};
|
Command queue[kCapacity]{};
|
||||||
|
|
@ -40,10 +42,13 @@ struct SharedSlot {
|
||||||
struct OutputSlot {
|
struct OutputSlot {
|
||||||
uni_hid_device_t* device = nullptr;
|
uni_hid_device_t* device = nullptr;
|
||||||
ControllerIdentity identity{};
|
ControllerIdentity identity{};
|
||||||
|
uint32_t generation = 0;
|
||||||
bool approved = false, active = false, mono = false;
|
bool approved = false, active = false, mono = false;
|
||||||
bool neutral_needed = false, dirty = false, host_valid = false;
|
bool neutral_needed = false, dirty = false, host_valid = false;
|
||||||
bool feedback_active = false, pending_host = false, pending_trimmed = false;
|
bool feedback_active = false, pending_host = false, pending_trimmed = false;
|
||||||
bool permission_requested = false, permission_granted = false;
|
bool permission_requested = false, processing = false, urgent_stop = false;
|
||||||
|
bool requested_urgent_stop = false;
|
||||||
|
uint64_t deadline_us = 0, requested_deadline_us = 0;
|
||||||
uint64_t feedback_until_us = 0, last_send_us = 0, retry_us = 0;
|
uint64_t feedback_until_us = 0, last_send_us = 0, retry_us = 0;
|
||||||
uint64_t pending_since_us = 0;
|
uint64_t pending_since_us = 0;
|
||||||
uint64_t pending_valid_until_us = UINT64_MAX;
|
uint64_t pending_valid_until_us = UINT64_MAX;
|
||||||
|
|
@ -66,6 +71,8 @@ btstack_data_source_t g_data_source{};
|
||||||
btstack_timer_source_t g_timer{};
|
btstack_timer_source_t g_timer{};
|
||||||
|
|
||||||
void poll();
|
void poll();
|
||||||
|
void process_slot(uint8_t slot);
|
||||||
|
bool granted(uni_hid_device_t* device, uint16_t cid, uint32_t generation);
|
||||||
void increment(uint32_t& value, uint32_t amount = 1) {
|
void increment(uint32_t& value, uint32_t amount = 1) {
|
||||||
value = amount > UINT32_MAX - value ? UINT32_MAX : value + amount;
|
value = amount > UINT32_MAX - value ? UINT32_MAX : value + amount;
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +148,23 @@ void ensure_runloop() {
|
||||||
btstack_run_loop_set_timer_handler(&g_timer, timer);
|
btstack_run_loop_set_timer_handler(&g_timer, timer);
|
||||||
g_runloop_ready = true;
|
g_runloop_ready = true;
|
||||||
}
|
}
|
||||||
|
uint64_t periodic_deadline(const OutputSlot& output) {
|
||||||
|
uint64_t deadline = UINT64_MAX;
|
||||||
|
if (output.host_valid || output.feedback_active)
|
||||||
|
deadline = output.last_send_us + kRefreshUs;
|
||||||
|
if (output.feedback_active && output.feedback_until_us < deadline)
|
||||||
|
deadline = output.feedback_until_us;
|
||||||
|
if (!output.feedback_active && output.host_valid && !output.host.stateful &&
|
||||||
|
output.host.received_us + kHostExpiryUs < deadline)
|
||||||
|
deadline = output.host.received_us + kHostExpiryUs;
|
||||||
|
return deadline;
|
||||||
|
}
|
||||||
|
bool ready(const OutputSlot& output, uint64_t now) {
|
||||||
|
return output.active && now >= output.retry_us &&
|
||||||
|
(output.neutral_needed || output.dirty || output.packets.count ||
|
||||||
|
((output.feedback_active || output.host_valid) &&
|
||||||
|
now >= output.last_send_us + kRefreshUs));
|
||||||
|
}
|
||||||
void schedule() {
|
void schedule() {
|
||||||
if (g_timer_armed) {
|
if (g_timer_armed) {
|
||||||
btstack_run_loop_remove_timer(&g_timer);
|
btstack_run_loop_remove_timer(&g_timer);
|
||||||
|
|
@ -150,6 +174,8 @@ void schedule() {
|
||||||
const uint64_t now = time_us_64();
|
const uint64_t now = time_us_64();
|
||||||
for (const auto& output : g_outputs) {
|
for (const auto& output : g_outputs) {
|
||||||
if (!output.active) continue;
|
if (!output.active) continue;
|
||||||
|
native_output_scheduler_reserve(output.device, output.generation,
|
||||||
|
periodic_deadline(output));
|
||||||
if ((output.neutral_needed || output.dirty || output.packets.count) &&
|
if ((output.neutral_needed || output.dirty || output.packets.count) &&
|
||||||
!output.permission_requested && output.retry_us < due) due = output.retry_us;
|
!output.permission_requested && output.retry_us < due) due = output.retry_us;
|
||||||
if (output.feedback_active && output.feedback_until_us < due)
|
if (output.feedback_active && output.feedback_until_us < due)
|
||||||
|
|
@ -157,7 +183,8 @@ void schedule() {
|
||||||
if (output.host_valid && !output.host.stateful &&
|
if (output.host_valid && !output.host.stateful &&
|
||||||
output.host.received_us + kHostExpiryUs < due)
|
output.host.received_us + kHostExpiryUs < due)
|
||||||
due = output.host.received_us + kHostExpiryUs;
|
due = output.host.received_us + kHostExpiryUs;
|
||||||
if ((output.feedback_active || output.host_valid) &&
|
if (!output.neutral_needed && !output.dirty && !output.packets.count &&
|
||||||
|
(output.feedback_active || output.host_valid) &&
|
||||||
output.last_send_us + kRefreshUs < due)
|
output.last_send_us + kRefreshUs < due)
|
||||||
due = output.last_send_us + kRefreshUs;
|
due = output.last_send_us + kRefreshUs;
|
||||||
}
|
}
|
||||||
|
|
@ -170,28 +197,36 @@ void schedule() {
|
||||||
}
|
}
|
||||||
bool permission(uint8_t slot) {
|
bool permission(uint8_t slot) {
|
||||||
OutputSlot& output = g_outputs[slot];
|
OutputSlot& output = g_outputs[slot];
|
||||||
if (output.permission_granted) return true;
|
if (native_output_scheduler_granted(output.device)) return true;
|
||||||
if (!output.permission_requested) {
|
if (!output.permission_requested ||
|
||||||
|
output.requested_deadline_us != output.deadline_us ||
|
||||||
|
output.requested_urgent_stop != output.urgent_stop) {
|
||||||
|
const bool waiting = output.permission_requested;
|
||||||
output.permission_requested = true; // Callback may be synchronous.
|
output.permission_requested = true; // Callback may be synchronous.
|
||||||
if (l2cap_request_can_send_now_event(output.device->conn.interrupt_cid) != 0) {
|
output.requested_deadline_us = output.deadline_us;
|
||||||
|
output.requested_urgent_stop = output.urgent_stop;
|
||||||
|
if (native_output_scheduler_request(output.device, output.generation,
|
||||||
|
output.deadline_us, output.urgent_stop, granted) != 0) {
|
||||||
output.permission_requested = false;
|
output.permission_requested = false;
|
||||||
output.retry_us = time_us_64() + 1000;
|
output.retry_us = time_us_64() + 1000;
|
||||||
}
|
}
|
||||||
if (!output.permission_granted) {
|
if (!waiting && !native_output_scheduler_granted(output.device)) {
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
increment(g_shared[slot].diagnostics.congested_attempts);
|
increment(g_shared[slot].diagnostics.congested_attempts);
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output.permission_granted;
|
return native_output_scheduler_granted(output.device);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool send(uint8_t slot, const uint8_t* bytes) {
|
bool send(uint8_t slot, const uint8_t* bytes) {
|
||||||
OutputSlot& output = g_outputs[slot];
|
OutputSlot& output = g_outputs[slot];
|
||||||
if (!permission(slot)) return false;
|
if (!permission(slot)) return false;
|
||||||
output.permission_granted = false;
|
auto* const device = output.device;
|
||||||
const bool sent = uni_hid_parser_switch_native_send(output.device, bytes);
|
const uint32_t generation = output.generation;
|
||||||
|
const bool sent = uni_hid_parser_switch_native_send(device, bytes);
|
||||||
const uint64_t now = time_us_64();
|
const uint64_t now = time_us_64();
|
||||||
|
if (output.device != device || output.generation != generation) return false;
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
auto& diagnostics = g_shared[slot].diagnostics;
|
auto& diagnostics = g_shared[slot].diagnostics;
|
||||||
if (sent) {
|
if (sent) {
|
||||||
|
|
@ -245,6 +280,10 @@ ControllerRumbleOutput current_host(const Command& command, uint64_t now,
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
void restore_compatibility(OutputSlot& output, uint64_t now) {
|
void restore_compatibility(OutputSlot& output, uint64_t now) {
|
||||||
|
if (native_output_scheduler_granted(output.device))
|
||||||
|
native_output_scheduler_complete(output.device, output.generation);
|
||||||
|
native_output_scheduler_cancel(output.device);
|
||||||
|
output.permission_requested = false;
|
||||||
uni_hid_parser_switch_native_release(output.device);
|
uni_hid_parser_switch_native_release(output.device);
|
||||||
output.active = false;
|
output.active = false;
|
||||||
if (output.device->report_parser.play_dual_rumble == nullptr) return;
|
if (output.device->report_parser.play_dual_rumble == nullptr) return;
|
||||||
|
|
@ -264,7 +303,7 @@ void restore_compatibility(OutputSlot& output, uint64_t now) {
|
||||||
output.feedback_active = false;
|
output.feedback_active = false;
|
||||||
output.dirty = false;
|
output.dirty = false;
|
||||||
}
|
}
|
||||||
void process_slot(uint8_t slot) {
|
void process_slot_step(uint8_t slot) {
|
||||||
OutputSlot& output = g_outputs[slot];
|
OutputSlot& output = g_outputs[slot];
|
||||||
if (!output.active) return;
|
if (!output.active) return;
|
||||||
const uint64_t now = time_us_64();
|
const uint64_t now = time_us_64();
|
||||||
|
|
@ -285,6 +324,7 @@ void process_slot(uint8_t slot) {
|
||||||
shared.count = shared.head = 0;
|
shared.count = shared.head = 0;
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
|
if (stop && output.host_valid && !output.feedback_active) output.urgent_stop = true;
|
||||||
if (have_new && !loss && !output.feedback_active) {
|
if (have_new && !loss && !output.feedback_active) {
|
||||||
const bool pending_hold = output.pending_host &&
|
const bool pending_hold = output.pending_host &&
|
||||||
same_hold(output.host, newest, false);
|
same_hold(output.host, newest, false);
|
||||||
|
|
@ -316,6 +356,8 @@ void process_slot(uint8_t slot) {
|
||||||
if (loss || stop || output.packets.count) reset_pending(output);
|
if (loss || stop || output.packets.count) reset_pending(output);
|
||||||
output.pending_host = true;
|
output.pending_host = true;
|
||||||
output.pending_since_us = newest.first_received_us;
|
output.pending_since_us = newest.first_received_us;
|
||||||
|
if (output.approved)
|
||||||
|
output.deadline_us = newest.first_received_us + kCommandWindowUs;
|
||||||
output.dirty = true;
|
output.dirty = true;
|
||||||
}
|
}
|
||||||
output.host = newest;
|
output.host = newest;
|
||||||
|
|
@ -329,12 +371,16 @@ void process_slot(uint8_t slot) {
|
||||||
if (!output.feedback_active) {
|
if (!output.feedback_active) {
|
||||||
reset_pending(output);
|
reset_pending(output);
|
||||||
output.dirty = true;
|
output.dirty = true;
|
||||||
|
output.deadline_us = output.host.received_us + kHostExpiryUs;
|
||||||
|
output.urgent_stop = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (output.feedback_active && now >= output.feedback_until_us) {
|
if (output.feedback_active && now >= output.feedback_until_us) {
|
||||||
output.feedback_active = false;
|
output.feedback_active = false;
|
||||||
reset_pending(output);
|
reset_pending(output);
|
||||||
output.dirty = true;
|
output.dirty = true;
|
||||||
|
output.deadline_us = output.feedback_until_us;
|
||||||
|
output.urgent_stop = !output.host_valid && !silent(output.feedback);
|
||||||
}
|
}
|
||||||
if (output.packets.count && now >= output.pending_valid_until_us) {
|
if (output.packets.count && now >= output.pending_valid_until_us) {
|
||||||
const bool host_command = output.pending_host;
|
const bool host_command = output.pending_host;
|
||||||
|
|
@ -347,6 +393,7 @@ void process_slot(uint8_t slot) {
|
||||||
if (!send(slot, kNeutral)) return;
|
if (!send(slot, kNeutral)) return;
|
||||||
output.encoder.reset();
|
output.encoder.reset();
|
||||||
output.neutral_needed = false;
|
output.neutral_needed = false;
|
||||||
|
output.urgent_stop = false;
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
increment(g_shared[slot].diagnostics.resynchronizations);
|
increment(g_shared[slot].diagnostics.resynchronizations);
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
|
|
@ -356,6 +403,8 @@ void process_slot(uint8_t slot) {
|
||||||
publish_flags(slot);
|
publish_flags(slot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A baseline consumes its own grant, even when a codec packet follows.
|
||||||
|
if (output.approved) return;
|
||||||
}
|
}
|
||||||
if (!output.approved) {
|
if (!output.approved) {
|
||||||
restore_compatibility(output, now);
|
restore_compatibility(output, now);
|
||||||
|
|
@ -371,6 +420,7 @@ void process_slot(uint8_t slot) {
|
||||||
((output.host_valid || output.feedback_active) && now >= output.last_send_us + kRefreshUs))) {
|
((output.host_valid || output.feedback_active) && now >= output.last_send_us + kRefreshUs))) {
|
||||||
// Request actual credit availability before mutating the encoder model.
|
// Request actual credit availability before mutating the encoder model.
|
||||||
// Timer polling misses short free-buffer windows behind HCI credit writes.
|
// Timer polling misses short free-buffer windows behind HCI credit writes.
|
||||||
|
if (!output.dirty) output.deadline_us = output.last_send_us + kRefreshUs;
|
||||||
if (!permission(slot)) return;
|
if (!permission(slot)) return;
|
||||||
ControllerRumbleOutput effective{};
|
ControllerRumbleOutput effective{};
|
||||||
output.pending_trimmed = false;
|
output.pending_trimmed = false;
|
||||||
|
|
@ -400,15 +450,52 @@ void process_slot(uint8_t slot) {
|
||||||
output.packet_index = 0;
|
output.packet_index = 0;
|
||||||
output.dirty = false;
|
output.dirty = false;
|
||||||
}
|
}
|
||||||
// Bounded legal schedule: at most baseline + one compressed command, never
|
// One legal wire packet per grant. The wrapper releases and reacquires
|
||||||
// drain an obsolete command backlog into the controller.
|
// through the shared arbiter before submitting a prepared tail.
|
||||||
for (uint8_t i = 0; i < 2 && output.packet_index < output.packets.count; ++i) {
|
if (output.packet_index < output.packets.count) {
|
||||||
if (!send(slot, output.packets.bytes[output.packet_index])) return;
|
if (!send(slot, output.packets.bytes[output.packet_index])) return;
|
||||||
++output.packet_index;
|
++output.packet_index;
|
||||||
}
|
}
|
||||||
if (output.packets.count && output.packet_index == output.packets.count) finish_command(slot);
|
if (output.packets.count && output.packet_index == output.packets.count) finish_command(slot);
|
||||||
publish_flags(slot);
|
publish_flags(slot);
|
||||||
}
|
}
|
||||||
|
void process_slot(uint8_t slot) {
|
||||||
|
OutputSlot& output = g_outputs[slot];
|
||||||
|
if (output.processing) return;
|
||||||
|
output.processing = true;
|
||||||
|
auto* const device = output.device;
|
||||||
|
const uint32_t generation = output.generation;
|
||||||
|
// A neutral barrier plus the codec's two-packet schedule is the maximum
|
||||||
|
// immediate work. Each attempt releases its token before another request.
|
||||||
|
for (uint8_t attempt = 0; attempt < 3; ++attempt) {
|
||||||
|
process_slot_step(slot);
|
||||||
|
if (output.device != device || output.generation != generation) return;
|
||||||
|
const bool had_grant = native_output_scheduler_granted(output.device);
|
||||||
|
if (had_grant) native_output_scheduler_complete(device, generation);
|
||||||
|
if (!had_grant || !ready(output, time_us_64())) break;
|
||||||
|
}
|
||||||
|
output.processing = false;
|
||||||
|
}
|
||||||
|
bool granted(uni_hid_device_t* device, uint16_t cid, uint32_t generation) {
|
||||||
|
if (!g_prepared || device == nullptr) return false;
|
||||||
|
for (uint8_t slot = 0; slot < kSlots; ++slot) {
|
||||||
|
auto& output = g_outputs[slot];
|
||||||
|
if (output.device != device || output.generation != generation || !output.active ||
|
||||||
|
device->conn.interrupt_cid != cid || !output.permission_requested ||
|
||||||
|
!native_output_scheduler_granted(device)) continue;
|
||||||
|
output.permission_requested = false;
|
||||||
|
output.retry_us = 0;
|
||||||
|
// Synchronous delivery resumes the caller's permission() immediately.
|
||||||
|
// A different slot can run here even while the outer poll is active.
|
||||||
|
if (!output.processing) {
|
||||||
|
process_slot(slot);
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
return false; // Nintendo's generic LED FIFO must still see the event.
|
||||||
|
}
|
||||||
|
native_output_scheduler_complete(device, generation); // Retired/no-send callback.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
void poll() {
|
void poll() {
|
||||||
if (g_polling) return;
|
if (g_polling) return;
|
||||||
g_polling = true;
|
g_polling = true;
|
||||||
|
|
@ -445,8 +532,11 @@ void switch_native_output_attach(uint8_t slot, uint32_t generation,
|
||||||
(type == 2 && identity.product_id == 0x2007))) return;
|
(type == 2 && identity.product_id == 0x2007))) return;
|
||||||
ensure_runloop();
|
ensure_runloop();
|
||||||
OutputSlot& output = g_outputs[slot];
|
OutputSlot& output = g_outputs[slot];
|
||||||
|
if (output.device) native_output_scheduler_cancel(output.device);
|
||||||
output = {};
|
output = {};
|
||||||
output.device = device;
|
output.device = device;
|
||||||
|
output.generation = generation;
|
||||||
|
output.deadline_us = time_us_64() + kCommandWindowUs;
|
||||||
output.identity = identity;
|
output.identity = identity;
|
||||||
output.mono = type != 3;
|
output.mono = type != 3;
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
|
|
@ -472,11 +562,13 @@ void switch_native_output_detach(uni_hid_device_t* device) {
|
||||||
for (uint8_t i = 0; i < kSlots; ++i) {
|
for (uint8_t i = 0; i < kSlots; ++i) {
|
||||||
if (g_outputs[i].device != device) continue;
|
if (g_outputs[i].device != device) continue;
|
||||||
// Parser teardown owns timer retirement; never transmit on a dead CID.
|
// Parser teardown owns timer retirement; never transmit on a dead CID.
|
||||||
|
native_output_scheduler_cancel(device);
|
||||||
g_outputs[i] = {};
|
g_outputs[i] = {};
|
||||||
critical_section_enter_blocking(&g_lock);
|
critical_section_enter_blocking(&g_lock);
|
||||||
g_shared[i].accepting = false;
|
g_shared[i].accepting = false;
|
||||||
g_shared[i].count = g_shared[i].head = 0;
|
g_shared[i].count = g_shared[i].head = 0;
|
||||||
g_shared[i].stop_pending = g_shared[i].lost = false;
|
g_shared[i].stop_pending = g_shared[i].lost = false;
|
||||||
|
g_shared[i].host_active = false;
|
||||||
g_shared[i].diagnostics.flags = 0;
|
g_shared[i].diagnostics.flags = 0;
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
}
|
}
|
||||||
|
|
@ -494,11 +586,17 @@ void switch_native_output_configure(const AdapterConfiguration& configuration,
|
||||||
const bool approved = adapter_configuration_native_switch_approved(configuration, output.identity);
|
const bool approved = adapter_configuration_native_switch_approved(configuration, output.identity);
|
||||||
if (approved == output.approved) continue;
|
if (approved == output.approved) continue;
|
||||||
output.approved = approved;
|
output.approved = approved;
|
||||||
|
native_output_scheduler_cancel(output.device);
|
||||||
|
output.permission_requested = false;
|
||||||
|
output.deadline_us = time_us_64() + kCommandWindowUs;
|
||||||
|
output.urgent_stop = !approved && output.active;
|
||||||
if (approved && !output.active && uni_hid_parser_switch_native_acquire(output.device)) {
|
if (approved && !output.active && uni_hid_parser_switch_native_acquire(output.device)) {
|
||||||
output.active = true;
|
output.active = true;
|
||||||
reset_pending(output);
|
reset_pending(output);
|
||||||
} else if (!approved && output.active) {
|
} else if (!approved && output.active) {
|
||||||
reset_pending(output);
|
reset_pending(output);
|
||||||
|
output.deadline_us = time_us_64();
|
||||||
|
output.urgent_stop = true;
|
||||||
}
|
}
|
||||||
publish_flags(i);
|
publish_flags(i);
|
||||||
}
|
}
|
||||||
|
|
@ -529,7 +627,9 @@ bool switch_native_output_submit(uint8_t slot, uint32_t generation,
|
||||||
shared.queue[(shared.head + shared.count) % kCapacity] = update;
|
shared.queue[(shared.head + shared.count) % kCapacity] = update;
|
||||||
++shared.count;
|
++shared.count;
|
||||||
}
|
}
|
||||||
shared.stop_pending = shared.stop_pending || silent(rumble);
|
const bool active = !silent(rumble);
|
||||||
|
shared.stop_pending = shared.stop_pending || (shared.host_active && !active);
|
||||||
|
shared.host_active = active;
|
||||||
increment(shared.diagnostics.received_commands);
|
increment(shared.diagnostics.received_commands);
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
|
|
@ -541,19 +641,6 @@ bool switch_native_output_submit(uint8_t slot, uint32_t generation,
|
||||||
}
|
}
|
||||||
return accepted;
|
return accepted;
|
||||||
}
|
}
|
||||||
bool switch_native_output_on_can_send_now(uni_hid_device_t* device, uint16_t cid) {
|
|
||||||
if (!g_prepared || device == nullptr) return false;
|
|
||||||
for (auto& output : g_outputs) {
|
|
||||||
if (output.device != device || !output.active ||
|
|
||||||
device->conn.interrupt_cid != cid || !output.permission_requested) continue;
|
|
||||||
output.permission_requested = false;
|
|
||||||
output.permission_granted = true;
|
|
||||||
output.retry_us = 0;
|
|
||||||
poll();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool switch_native_output_owns(const uni_hid_device_t* device) {
|
bool switch_native_output_owns(const uni_hid_device_t* device) {
|
||||||
if (!g_prepared || device == nullptr) return false;
|
if (!g_prepared || device == nullptr) return false;
|
||||||
|
|
@ -566,11 +653,18 @@ bool switch_native_output_feedback(uni_hid_device_t* device, uint8_t low,
|
||||||
if (!g_prepared || device == nullptr) return false;
|
if (!g_prepared || device == nullptr) return false;
|
||||||
for (auto& output : g_outputs) {
|
for (auto& output : g_outputs) {
|
||||||
if (output.device != device || !output.active) continue;
|
if (output.device != device || !output.active) continue;
|
||||||
|
const uint64_t now = time_us_64();
|
||||||
|
const bool stopped = output.feedback_active && !silent(output.feedback) &&
|
||||||
|
(duration_ms == 0 || (low == 0 && high == 0));
|
||||||
output.feedback = magnitudes(low, high);
|
output.feedback = magnitudes(low, high);
|
||||||
output.feedback_until_us = time_us_64() + uint64_t{duration_ms} * 1000;
|
output.feedback_until_us = now + uint64_t{duration_ms} * 1000;
|
||||||
output.feedback_active = duration_ms != 0;
|
output.feedback_active = duration_ms != 0;
|
||||||
reset_pending(output);
|
reset_pending(output);
|
||||||
output.dirty = true;
|
output.dirty = true;
|
||||||
|
output.deadline_us = now + kCommandWindowUs;
|
||||||
|
if (duration_ms && output.feedback_until_us < output.deadline_us)
|
||||||
|
output.deadline_us = output.feedback_until_us;
|
||||||
|
output.urgent_stop = stopped;
|
||||||
poll();
|
poll();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ bool switch_native_output_submit(uint8_t slot, uint32_t generation,
|
||||||
const ControllerRumbleOutput& rumble,
|
const ControllerRumbleOutput& rumble,
|
||||||
bool stateful);
|
bool stateful);
|
||||||
bool switch_native_output_owns(const uni_hid_device_s* device);
|
bool switch_native_output_owns(const uni_hid_device_s* device);
|
||||||
bool switch_native_output_on_can_send_now(uni_hid_device_s* device, uint16_t cid);
|
|
||||||
bool switch_native_output_feedback(uni_hid_device_s* device, uint8_t low,
|
bool switch_native_output_feedback(uni_hid_device_s* device, uint8_t low,
|
||||||
uint8_t high, uint16_t duration_ms);
|
uint8_t high, uint16_t duration_ms);
|
||||||
void switch_native_output_snapshot(uint8_t slot,
|
void switch_native_output_snapshot(uint8_t slot,
|
||||||
|
|
|
||||||
|
|
@ -499,11 +499,12 @@ int btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
|
||||||
|
|
||||||
uint64_t time_us_64() { return uint64_t{now_ms} * 1000; }
|
uint64_t time_us_64() { return uint64_t{now_ms} * 1000; }
|
||||||
uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t) { return 143; }
|
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) {
|
uint8_t l2cap_request_can_send_now_event(uint16_t cid) {
|
||||||
for (const auto& slot : g_slots) {
|
for (const auto& slot : g_slots) {
|
||||||
if (slot.device != nullptr && slot.device->conn.interrupt_cid == cid) {
|
if (slot.device != nullptr && slot.device->conn.interrupt_cid == cid) {
|
||||||
require(haptics_experiment_on_can_send_now(slot.device, cid),
|
(void)uni_platform_on_l2cap_can_send_now(slot.device, cid);
|
||||||
"native send permission was not consumed");
|
|
||||||
return ERROR_CODE_SUCCESS;
|
return ERROR_CODE_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,5 @@
|
||||||
uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t cid);
|
uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t cid);
|
||||||
uint8_t l2cap_request_can_send_now_event(uint16_t cid);
|
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);
|
uint8_t l2cap_send(uint16_t cid, const uint8_t* data, uint16_t size);
|
||||||
|
bool l2cap_can_send_packet_now(uint16_t cid);
|
||||||
|
int hci_number_free_acl_slots_for_handle(uint16_t handle);
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,7 @@ void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t timeout_
|
||||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer);
|
void btstack_run_loop_add_timer(btstack_timer_source_t* timer);
|
||||||
int btstack_run_loop_remove_timer(btstack_timer_source_t* timer);
|
int btstack_run_loop_remove_timer(btstack_timer_source_t* timer);
|
||||||
uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t cid);
|
uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t cid);
|
||||||
|
bool l2cap_can_send_packet_now(uint16_t cid);
|
||||||
|
int hci_number_free_acl_slots_for_handle(uint16_t handle);
|
||||||
uint8_t l2cap_request_can_send_now_event(uint16_t cid);
|
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);
|
uint8_t l2cap_send(uint16_t cid, const uint8_t* data, uint16_t size);
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,5 @@ struct uni_hid_device_s {
|
||||||
btstack_timer_source_t parser_timer;
|
btstack_timer_source_t parser_timer;
|
||||||
bool notification_pending = false;
|
bool notification_pending = false;
|
||||||
bool credit = true;
|
bool credit = true;
|
||||||
|
uint16_t native_ready_cid = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "input/haptics_experiment.h"
|
#include "input/haptics_experiment.h"
|
||||||
#include "input/haptics_transport_probe.h"
|
#include "input/haptics_transport_probe.h"
|
||||||
|
#include "input/native_output_scheduler.h"
|
||||||
#include "usb/switch/switch_haptics.h"
|
#include "usb/switch/switch_haptics.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -40,7 +41,6 @@ std::vector<Pcm> pcm;
|
||||||
std::vector<Generic> generic_sent;
|
std::vector<Generic> generic_sent;
|
||||||
std::vector<Generic> generic_queue;
|
std::vector<Generic> generic_queue;
|
||||||
Delivery delivery = Delivery::kImmediate;
|
Delivery delivery = Delivery::kImmediate;
|
||||||
uni_hid_device_t* permission = nullptr;
|
|
||||||
unsigned request_depth = 0;
|
unsigned request_depth = 0;
|
||||||
unsigned max_request_depth = 0;
|
unsigned max_request_depth = 0;
|
||||||
unsigned request_calls = 0;
|
unsigned request_calls = 0;
|
||||||
|
|
@ -51,6 +51,7 @@ unsigned fail_sends = 0;
|
||||||
uint32_t send_cost_us = 0;
|
uint32_t send_cost_us = 0;
|
||||||
uint32_t request_cost_us = 0;
|
uint32_t request_cost_us = 0;
|
||||||
bool reenter_send = false;
|
bool reenter_send = false;
|
||||||
|
bool detach_during_send = false;
|
||||||
|
|
||||||
void no_lock() {
|
void no_lock() {
|
||||||
assert(native_haptics_lock_depth == 0);
|
assert(native_haptics_lock_depth == 0);
|
||||||
|
|
@ -132,9 +133,11 @@ bool dispatch(uni_hid_device_t* device, uint16_t cid) {
|
||||||
no_lock();
|
no_lock();
|
||||||
if (cid == device->conn.interrupt_cid) {
|
if (cid == device->conn.interrupt_cid) {
|
||||||
device->notification_pending = false;
|
device->notification_pending = false;
|
||||||
|
device->native_ready_cid = cid;
|
||||||
}
|
}
|
||||||
permission = device->credit ? device : nullptr;
|
const bool exclusive = haptics_experiment_blocks_generic(device);
|
||||||
const bool consumed = haptics_experiment_on_can_send_now(device, cid);
|
const bool consumed =
|
||||||
|
native_output_scheduler_on_can_send_now(device, cid) || exclusive;
|
||||||
if (!consumed && device->credit) {
|
if (!consumed && device->credit) {
|
||||||
const auto it = std::find_if(generic_queue.begin(), generic_queue.end(),
|
const auto it = std::find_if(generic_queue.begin(), generic_queue.end(),
|
||||||
[device](const Generic& report) {
|
[device](const Generic& report) {
|
||||||
|
|
@ -146,9 +149,9 @@ bool dispatch(uni_hid_device_t* device, uint16_t cid) {
|
||||||
generic_sent.push_back(report);
|
generic_sent.push_back(report);
|
||||||
generic_queue.erase(it);
|
generic_queue.erase(it);
|
||||||
--device->outgoing_buffer.queued;
|
--device->outgoing_buffer.queued;
|
||||||
|
device->native_ready_cid = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
permission = nullptr;
|
|
||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,17 +184,18 @@ void reset(uint64_t at_us = 10000123) {
|
||||||
haptics_experiment_poll();
|
haptics_experiment_poll();
|
||||||
}
|
}
|
||||||
timers.clear();
|
timers.clear();
|
||||||
|
native_output_scheduler_prepare();
|
||||||
devices = {};
|
devices = {};
|
||||||
pcm.clear();
|
pcm.clear();
|
||||||
generic_sent.clear();
|
generic_sent.clear();
|
||||||
generic_queue.clear();
|
generic_queue.clear();
|
||||||
now_us = at_us;
|
now_us = at_us;
|
||||||
delivery = Delivery::kImmediate;
|
delivery = Delivery::kImmediate;
|
||||||
permission = nullptr;
|
|
||||||
request_depth = max_request_depth = request_calls = send_calls = timer_calls = 0;
|
request_depth = max_request_depth = request_calls = send_calls = timer_calls = 0;
|
||||||
fail_requests = fail_sends = 0;
|
fail_requests = fail_sends = 0;
|
||||||
send_cost_us = request_cost_us = 0;
|
send_cost_us = request_cost_us = 0;
|
||||||
reenter_send = false;
|
reenter_send = false;
|
||||||
|
detach_during_send = false;
|
||||||
for (unsigned slot = 0; slot < devices.size(); ++slot) {
|
for (unsigned slot = 0; slot < devices.size(); ++slot) {
|
||||||
auto& device = devices[slot];
|
auto& device = devices[slot];
|
||||||
device.vendor_id = 0x054c;
|
device.vendor_id = 0x054c;
|
||||||
|
|
@ -597,6 +601,48 @@ void timing_cost_reentrancy_and_wrap() {
|
||||||
assert(wrapped.elapsed_us >= 6147000 && wrapped.elapsed_us < 6148000);
|
assert(wrapped.elapsed_us >= 6147000 && wrapped.elapsed_us < 6148000);
|
||||||
assert(wrapped.max_send_gap_us <= 22000 && wrapped.sent_packets == 288);
|
assert(wrapped.max_send_gap_us <= 22000 && wrapped.sent_packets == 288);
|
||||||
}
|
}
|
||||||
|
void synchronous_teardown_releases_admission() {
|
||||||
|
reset();
|
||||||
|
detach_during_send = true;
|
||||||
|
assert(haptics_experiment_request(2, 0));
|
||||||
|
haptics_experiment_poll();
|
||||||
|
assert(snapshot().state == HapticsExperimentState::kDisconnected);
|
||||||
|
assert(!haptics_experiment_owns(&devices[0]));
|
||||||
|
assert(!native_output_scheduler_granted(&devices[0]) && timers.empty());
|
||||||
|
const size_t sent = pcm.size();
|
||||||
|
run_until(now_us + 100000);
|
||||||
|
assert(pcm.size() == sent);
|
||||||
|
|
||||||
|
detach_during_send = false;
|
||||||
|
haptics_experiment_attach(0, 101, &devices[0]);
|
||||||
|
assert(haptics_experiment_request(2, 0));
|
||||||
|
haptics_experiment_poll();
|
||||||
|
run_until(now_us + 50000);
|
||||||
|
assert(snapshot().state == HapticsExperimentState::kRunning);
|
||||||
|
assert(snapshot().connection_generation == 101);
|
||||||
|
assert(snapshot().sent_packets >= 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void gameplay_led_yield_releases_admission() {
|
||||||
|
reset();
|
||||||
|
assert(haptics_experiment_request(2, 0));
|
||||||
|
haptics_experiment_poll();
|
||||||
|
const uint64_t started = snapshot().start_us;
|
||||||
|
delivery = Delivery::kDeferred;
|
||||||
|
run_until(due(started, 1, snapshot().packet_frames) + 1000);
|
||||||
|
assert(devices[0].notification_pending);
|
||||||
|
devices[0].credit = false;
|
||||||
|
emit_generic(&devices[0], GenericKind::kLed);
|
||||||
|
devices[0].credit = true;
|
||||||
|
assert(!dispatch(&devices[0], devices[0].conn.interrupt_cid));
|
||||||
|
assert(pcm.size() == 1 && generic_queue.empty());
|
||||||
|
assert(generic_sent.size() == 1 && generic_sent[0].kind == GenericKind::kLed);
|
||||||
|
delivery = Delivery::kImmediate;
|
||||||
|
run_until(now_us + 2000);
|
||||||
|
assert(snapshot().state == HapticsExperimentState::kRunning);
|
||||||
|
assert(pcm.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
void gameplay_timeline_and_lifecycle() {
|
void gameplay_timeline_and_lifecycle() {
|
||||||
reset();
|
reset();
|
||||||
|
|
@ -830,6 +876,17 @@ uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t cid) {
|
||||||
return device_for_cid(cid)->remote_mtu;
|
return device_for_cid(cid)->remote_mtu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool l2cap_can_send_packet_now(uint16_t cid) {
|
||||||
|
no_lock();
|
||||||
|
const auto* device = device_for_cid(cid);
|
||||||
|
return device->credit && device->native_ready_cid == cid;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hci_number_free_acl_slots_for_handle(uint16_t handle) {
|
||||||
|
no_lock();
|
||||||
|
return handle < devices.size() && devices[handle].credit ? 4 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t uni_circular_buffer_is_empty(const uni_circular_buffer_t* buffer) {
|
uint8_t uni_circular_buffer_is_empty(const uni_circular_buffer_t* buffer) {
|
||||||
return buffer->queued == 0;
|
return buffer->queued == 0;
|
||||||
}
|
}
|
||||||
|
|
@ -857,13 +914,15 @@ 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) {
|
uint8_t l2cap_send(uint16_t cid, const uint8_t* data, uint16_t size) {
|
||||||
no_lock();
|
no_lock();
|
||||||
auto* device = device_for_cid(cid);
|
auto* device = device_for_cid(cid);
|
||||||
assert(permission == device && device->credit);
|
assert(device->native_ready_cid == cid && device->credit);
|
||||||
|
assert(native_output_scheduler_granted(device));
|
||||||
|
device->native_ready_cid = 0;
|
||||||
assert(size == 143);
|
assert(size == 143);
|
||||||
++send_calls;
|
++send_calls;
|
||||||
const uint64_t submitted_us = now_us;
|
const uint64_t submitted_us = now_us;
|
||||||
now_us += send_cost_us;
|
now_us += send_cost_us;
|
||||||
if (reenter_send) {
|
if (reenter_send) {
|
||||||
assert(haptics_experiment_on_can_send_now(device, cid));
|
assert(native_output_scheduler_on_can_send_now(device, cid));
|
||||||
const auto concurrent_snapshot = snapshot();
|
const auto concurrent_snapshot = snapshot();
|
||||||
assert(concurrent_snapshot.state == HapticsExperimentState::kPending ||
|
assert(concurrent_snapshot.state == HapticsExperimentState::kPending ||
|
||||||
concurrent_snapshot.state == HapticsExperimentState::kRunning);
|
concurrent_snapshot.state == HapticsExperimentState::kRunning);
|
||||||
|
|
@ -875,12 +934,16 @@ uint8_t l2cap_send(uint16_t cid, const uint8_t* data, uint16_t size) {
|
||||||
Pcm packet{submitted_us, cid, {}};
|
Pcm packet{submitted_us, cid, {}};
|
||||||
std::copy(data, data + size, packet.bytes.begin());
|
std::copy(data, data + size, packet.bytes.begin());
|
||||||
pcm.push_back(packet);
|
pcm.push_back(packet);
|
||||||
|
if (detach_during_send) {
|
||||||
|
haptics_experiment_detach(device);
|
||||||
|
}
|
||||||
return ERROR_CODE_SUCCESS;
|
return ERROR_CODE_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
assert(argc == 2);
|
assert(argc == 2);
|
||||||
haptics_experiment_prepare();
|
haptics_experiment_prepare();
|
||||||
|
native_output_scheduler_prepare();
|
||||||
assert(snapshot().state == HapticsExperimentState::kIdle);
|
assert(snapshot().state == HapticsExperimentState::kIdle);
|
||||||
nominal_run(argv[1]);
|
nominal_run(argv[1]);
|
||||||
stalled_deadlines();
|
stalled_deadlines();
|
||||||
|
|
@ -890,6 +953,8 @@ int main(int argc, char** argv) {
|
||||||
reconnect_and_pending_generation();
|
reconnect_and_pending_generation();
|
||||||
support_and_transport_errors();
|
support_and_transport_errors();
|
||||||
timing_cost_reentrancy_and_wrap();
|
timing_cost_reentrancy_and_wrap();
|
||||||
|
synchronous_teardown_releases_admission();
|
||||||
|
gameplay_led_yield_releases_admission();
|
||||||
gameplay_timeline_and_lifecycle();
|
gameplay_timeline_and_lifecycle();
|
||||||
gameplay_queued_start_and_command_overflow();
|
gameplay_queued_start_and_command_overflow();
|
||||||
stateful_rumble_prepare_feedback_and_zero();
|
stateful_rumble_prepare_feedback_and_zero();
|
||||||
|
|
|
||||||
187
tests/native_output_scheduler_test.cpp
Normal file
187
tests/native_output_scheduler_test.cpp
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#include "input/native_output_scheduler.h"
|
||||||
|
#include <btstack.h>
|
||||||
|
#include <uni.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
uint64_t now = 1000;
|
||||||
|
int credits = 0;
|
||||||
|
bool available = true, synchronous = false, hold = false;
|
||||||
|
uint8_t request_error = 0;
|
||||||
|
unsigned resubmit = 0;
|
||||||
|
std::vector<uint16_t> delivered;
|
||||||
|
std::vector<btstack_timer_source_t*> timers;
|
||||||
|
uni_hid_device_t devices[4];
|
||||||
|
void require(bool condition, const char* message) {
|
||||||
|
if (!condition) { std::cerr << message << '\n'; std::exit(1); }
|
||||||
|
}
|
||||||
|
bool send(uni_hid_device_t* device, uint16_t cid, uint32_t generation) {
|
||||||
|
require(native_output_scheduler_granted(device), "sender ran without its grant");
|
||||||
|
delivered.push_back(cid);
|
||||||
|
--credits;
|
||||||
|
if (!hold) native_output_scheduler_complete(device, generation);
|
||||||
|
if (resubmit && device == &devices[0]) {
|
||||||
|
--resubmit;
|
||||||
|
native_output_scheduler_request(device, 1, 1000, false, send);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool yield_generic(uni_hid_device_t* device, uint16_t cid, uint32_t generation) {
|
||||||
|
if (!uni_circular_buffer_is_empty(&device->outgoing_buffer)) {
|
||||||
|
native_output_scheduler_complete(device, generation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return send(device, cid, generation);
|
||||||
|
}
|
||||||
|
void notify(unsigned index) {
|
||||||
|
const bool consumed = native_output_scheduler_on_can_send_now(&devices[index], devices[index].conn.interrupt_cid);
|
||||||
|
if (!consumed && devices[index].outgoing_buffer.queued) {
|
||||||
|
--devices[index].outgoing_buffer.queued;
|
||||||
|
--credits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void request(unsigned index, uint64_t deadline, bool urgent = false) {
|
||||||
|
require(native_output_scheduler_request(&devices[index], 1, deadline, urgent, send) == 0,
|
||||||
|
"valid scheduler request rejected");
|
||||||
|
}
|
||||||
|
void edf() {
|
||||||
|
request(0, 3000); request(1, 1000); request(2, 2000);
|
||||||
|
require(delivered.empty(), "scheduler sent without transport credits");
|
||||||
|
credits = 3; notify(0);
|
||||||
|
require(delivered == std::vector<uint16_t>({65, 66, 64}), "deadline ordering followed callback/slot order");
|
||||||
|
}
|
||||||
|
void urgent() {
|
||||||
|
request(0, 1000); request(1, 90000, true);
|
||||||
|
credits = 2; notify(0);
|
||||||
|
require(delivered == std::vector<uint16_t>({65, 64}), "stop did not precede ordinary overdue work");
|
||||||
|
}
|
||||||
|
void reservation() {
|
||||||
|
native_output_scheduler_reserve(&devices[1], 1, 10000);
|
||||||
|
credits = 1;
|
||||||
|
request(0, 20000);
|
||||||
|
require(delivered.empty(), "future earlier stream lost the last available credit");
|
||||||
|
request(1, 10000);
|
||||||
|
require(delivered == std::vector<uint16_t>({65}), "reserved stream could not use its credit");
|
||||||
|
native_output_scheduler_reserve(&devices[1], 1, UINT64_MAX);
|
||||||
|
credits = 1; notify(0);
|
||||||
|
require(delivered == std::vector<uint16_t>({65, 64}), "retired reservation blocked remaining work");
|
||||||
|
}
|
||||||
|
void reservation_roll_forward() {
|
||||||
|
native_output_scheduler_reserve(&devices[1], 1, 10000);
|
||||||
|
request(0, 20000);
|
||||||
|
credits = 1;
|
||||||
|
// A periodic writer announces its next interval immediately before
|
||||||
|
// registering its already-due current packet.
|
||||||
|
native_output_scheduler_reserve(&devices[1], 1, 30000);
|
||||||
|
request(1, 10000);
|
||||||
|
require(delivered == std::vector<uint16_t>({65}),
|
||||||
|
"reservation update dispatched later work before the due request was registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
void reservation_tie() {
|
||||||
|
native_output_scheduler_reserve(&devices[0], 1, 10000);
|
||||||
|
native_output_scheduler_reserve(&devices[1], 1, 10000);
|
||||||
|
request(0, 10000); request(1, 10000);
|
||||||
|
credits = 1; notify(0);
|
||||||
|
require(delivered.size() == 1, "equal periodic reservations deadlocked");
|
||||||
|
credits = 1; notify(1);
|
||||||
|
require(delivered.size() == 2 && delivered[0] != delivered[1], "reservation ties starved a peer");
|
||||||
|
}
|
||||||
|
void grant_lifetime() {
|
||||||
|
credits = 2; hold = true;
|
||||||
|
request(0, 1000); request(1, 500);
|
||||||
|
require(delivered.size() == 1 && native_output_scheduler_granted(&devices[0]), "another client stole a live grant");
|
||||||
|
require(native_output_scheduler_on_can_send_now(&devices[0], 90), "reentrant generic event escaped live output ownership");
|
||||||
|
hold = false;
|
||||||
|
native_output_scheduler_complete(&devices[0], 1);
|
||||||
|
require(delivered == std::vector<uint16_t>({64, 65}), "grant completion did not unblock urgent peer");
|
||||||
|
}
|
||||||
|
void reuse() {
|
||||||
|
request(0, 1000);
|
||||||
|
native_output_scheduler_cancel(&devices[0]);
|
||||||
|
devices[0].conn.interrupt_cid = 80;
|
||||||
|
require(native_output_scheduler_request(&devices[0], 2, 2000, false, send) == 0, "new generation rejected");
|
||||||
|
credits = 1;
|
||||||
|
native_output_scheduler_on_can_send_now(&devices[0], 64);
|
||||||
|
require(delivered == std::vector<uint16_t>({80}), "old generation/CID reached sender");
|
||||||
|
}
|
||||||
|
void stale_completion() {
|
||||||
|
credits = 2;
|
||||||
|
hold = true;
|
||||||
|
request(0, 1000);
|
||||||
|
native_output_scheduler_cancel(&devices[0]);
|
||||||
|
require(native_output_scheduler_request(&devices[0], 2, 2000, false, send) == 0,
|
||||||
|
"replacement generation was rejected");
|
||||||
|
native_output_scheduler_complete(&devices[0], 1);
|
||||||
|
require(native_output_scheduler_granted(&devices[0]),
|
||||||
|
"old completion released the replacement generation's grant");
|
||||||
|
native_output_scheduler_complete(&devices[0], 2);
|
||||||
|
require(!native_output_scheduler_granted(&devices[0]), "current completion did not release its grant");
|
||||||
|
}
|
||||||
|
|
||||||
|
void bounded_reentry() {
|
||||||
|
credits = 100; resubmit = 50;
|
||||||
|
request(0, 1000);
|
||||||
|
require(delivered.size() <= 8 && !timers.empty(), "synchronous producer monopolized event loop");
|
||||||
|
const size_t before = delivered.size();
|
||||||
|
request(1, 1000);
|
||||||
|
require(delivered.size() > before && delivered[before] == 65, "equal deadline tie favored self-resubmitting client");
|
||||||
|
}
|
||||||
|
void generic() {
|
||||||
|
credits = 1; synchronous = true;
|
||||||
|
devices[0].outgoing_buffer.queued = 1;
|
||||||
|
require(native_output_scheduler_request(&devices[0], 1, 1000, false, yield_generic) == 0, "generic-yield request rejected");
|
||||||
|
require(delivered.empty() && devices[0].outgoing_buffer.queued == 0,
|
||||||
|
"generic FIFO yield stalled or emitted native output first");
|
||||||
|
credits = 1;
|
||||||
|
require(native_output_scheduler_request(&devices[0], 1, 2000, false, yield_generic) == 0, "native retry rejected");
|
||||||
|
require(delivered == std::vector<uint16_t>({64}), "native output did not resume after generic FIFO drain");
|
||||||
|
}
|
||||||
|
void error() {
|
||||||
|
available = false; request_error = 0x44;
|
||||||
|
require(native_output_scheduler_request(&devices[0], 1, 1000, false, send) == 0x44,
|
||||||
|
"immediate transport request error was hidden");
|
||||||
|
require(delivered.empty() && !native_output_scheduler_granted(&devices[0]), "failed request granted output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint64_t time_us_64() { return now; }
|
||||||
|
bool l2cap_can_send_packet_now(uint16_t) { return available && credits > 0; }
|
||||||
|
int hci_number_free_acl_slots_for_handle(uint16_t) { return credits; }
|
||||||
|
uint8_t l2cap_request_can_send_now_event(uint16_t cid) {
|
||||||
|
if (request_error) return request_error;
|
||||||
|
if (synchronous && available && credits > 0)
|
||||||
|
for (unsigned i = 0; i < 4; ++i) if (devices[i].conn.interrupt_cid == cid) notify(i);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer, void (*callback)(btstack_timer_source_t*)) { timer->handler = callback; }
|
||||||
|
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t delay) { timer->due_us = (now / 1000 + delay + 1) * 1000; }
|
||||||
|
void btstack_run_loop_add_timer(btstack_timer_source_t* timer) { timers.push_back(timer); }
|
||||||
|
bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
|
||||||
|
const auto found = std::find(timers.begin(), timers.end(), timer);
|
||||||
|
if (found == timers.end()) return false;
|
||||||
|
timers.erase(found); return true;
|
||||||
|
}
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
require(argc == 2, "scenario required");
|
||||||
|
for (uint16_t i = 0; i < 4; ++i) {
|
||||||
|
devices[i].conn.handle = 10 + i;
|
||||||
|
devices[i].conn.interrupt_cid = 64 + i;
|
||||||
|
}
|
||||||
|
native_output_scheduler_prepare();
|
||||||
|
if (!std::strcmp(argv[1], "edf")) edf();
|
||||||
|
else if (!std::strcmp(argv[1], "urgent")) urgent();
|
||||||
|
else if (!std::strcmp(argv[1], "reservation")) reservation();
|
||||||
|
else if (!std::strcmp(argv[1], "reservation-tie")) reservation_tie();
|
||||||
|
else if (!std::strcmp(argv[1], "reservation-roll-forward")) reservation_roll_forward();
|
||||||
|
else if (!std::strcmp(argv[1], "grant")) grant_lifetime();
|
||||||
|
else if (!std::strcmp(argv[1], "reuse")) reuse();
|
||||||
|
else if (!std::strcmp(argv[1], "stale-completion")) stale_completion();
|
||||||
|
else if (!std::strcmp(argv[1], "bounded")) bounded_reentry();
|
||||||
|
else if (!std::strcmp(argv[1], "generic")) generic();
|
||||||
|
else if (!std::strcmp(argv[1], "error")) error();
|
||||||
|
else require(false, "unknown scenario");
|
||||||
|
}
|
||||||
|
|
@ -28,4 +28,6 @@ void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer,
|
||||||
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t timeout_ms);
|
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t timeout_ms);
|
||||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer);
|
void btstack_run_loop_add_timer(btstack_timer_source_t* timer);
|
||||||
bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer);
|
bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer);
|
||||||
|
bool l2cap_can_send_packet_now(uint16_t cid);
|
||||||
|
int hci_number_free_acl_slots_for_handle(uint16_t handle);
|
||||||
uint8_t l2cap_request_can_send_now_event(uint16_t cid);
|
uint8_t l2cap_request_can_send_now_event(uint16_t cid);
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,24 @@ using uni_play_dual_rumble_t = void (*)(uni_hid_device_t*, uint16_t, uint16_t,
|
||||||
struct uni_report_parser_t {
|
struct uni_report_parser_t {
|
||||||
uni_play_dual_rumble_t play_dual_rumble = nullptr;
|
uni_play_dual_rumble_t play_dual_rumble = nullptr;
|
||||||
};
|
};
|
||||||
|
struct uni_circular_buffer_t {
|
||||||
|
unsigned queued = 0;
|
||||||
|
};
|
||||||
|
inline bool uni_circular_buffer_is_empty(const uni_circular_buffer_t* buffer) {
|
||||||
|
return buffer->queued == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Only the parser boundary is faked. Native sends either enter the byte sink
|
// Only the parser boundary is faked. Native sends either enter the byte sink
|
||||||
// synchronously or fail without retaining a packet; no hidden transmit queue.
|
// synchronously or fail without retaining a packet; no hidden transmit queue.
|
||||||
struct uni_hid_device_s {
|
struct uni_hid_device_s {
|
||||||
uni_report_parser_t report_parser{};
|
uni_report_parser_t report_parser{};
|
||||||
struct { uint16_t interrupt_cid = 0; } conn;
|
struct {
|
||||||
bool connected = true;
|
uint16_t interrupt_cid = 0;
|
||||||
|
uint16_t handle = 0;
|
||||||
|
bool connected = true;
|
||||||
|
} conn;
|
||||||
|
uni_circular_buffer_t outgoing_buffer{};
|
||||||
bool info_ready = true;
|
bool info_ready = true;
|
||||||
bool acquire_allowed = true;
|
bool acquire_allowed = true;
|
||||||
bool native_owned = false;
|
bool native_owned = false;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "input/switch_native_output.h"
|
#include "input/switch_native_output.h"
|
||||||
|
#include "input/native_output_scheduler.h"
|
||||||
|
|
||||||
#include <btstack.h>
|
#include <btstack.h>
|
||||||
#include <parser/uni_hid_parser_switch.h>
|
#include <parser/uni_hid_parser_switch.h>
|
||||||
|
|
@ -72,7 +73,8 @@ void run_until(uint64_t target_us) {
|
||||||
if (writable && !credit_event_only && !permission_requests.empty()) {
|
if (writable && !credit_event_only && !permission_requests.empty()) {
|
||||||
const auto cid = permission_requests.front();
|
const auto cid = permission_requests.front();
|
||||||
permission_requests.pop_front();
|
permission_requests.pop_front();
|
||||||
switch_native_output_on_can_send_now(radio_devices[cid], cid);
|
require(!native_output_scheduler_on_can_send_now(radio_devices[cid], cid),
|
||||||
|
"Nintendo arbitration swallowed the generic LED FIFO event");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (poll_requested) {
|
if (poll_requested) {
|
||||||
|
|
@ -86,6 +88,9 @@ void run_until(uint64_t target_us) {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
for (const auto& entry : radio_devices)
|
||||||
|
require(!native_output_scheduler_granted(entry.second),
|
||||||
|
"native owner held a grant across an event-loop wait");
|
||||||
const auto next = std::min_element(timers.begin(), timers.end(),
|
const auto next = std::min_element(timers.begin(), timers.end(),
|
||||||
[](const auto* a, const auto* b) { return a->due_us < b->due_us; });
|
[](const auto* a, const auto* b) { return a->due_us < b->due_us; });
|
||||||
if (next == timers.end() || (*next)->due_us > target_us) {
|
if (next == timers.end() || (*next)->due_us > target_us) {
|
||||||
|
|
@ -106,7 +111,7 @@ void advance_ms(uint32_t milliseconds) { run_until(now_us + uint64_t{millisecond
|
||||||
|
|
||||||
void conventional(uni_hid_device_t* device, uint16_t delay_ms,
|
void conventional(uni_hid_device_t* device, uint16_t delay_ms,
|
||||||
uint16_t duration_ms, uint8_t weak, uint8_t strong) {
|
uint16_t duration_ms, uint8_t weak, uint8_t strong) {
|
||||||
require(device->connected && !device->native_owned,
|
require(device->conn.connected && !device->native_owned,
|
||||||
"compatibility output ran before native ownership was released");
|
"compatibility output ran before native ownership was released");
|
||||||
compatibility.push_back({device, delay_ms, duration_ms, weak, strong, wire.size(), now_us});
|
compatibility.push_back({device, delay_ms, duration_ms, weak, strong, wire.size(), now_us});
|
||||||
}
|
}
|
||||||
|
|
@ -311,7 +316,7 @@ void test_generation() {
|
||||||
submit({255, 0}, true);
|
submit({255, 0}, true);
|
||||||
flush();
|
flush();
|
||||||
submit(three_steps());
|
submit(three_steps());
|
||||||
old.connected = false;
|
old.conn.connected = false;
|
||||||
const size_t old_count = frame_count(old);
|
const size_t old_count = frame_count(old);
|
||||||
switch_native_output_detach(&old);
|
switch_native_output_detach(&old);
|
||||||
switch_native_output_attach(0, kGeneration + 1, &replacement, identity());
|
switch_native_output_attach(0, kGeneration + 1, &replacement, identity());
|
||||||
|
|
@ -544,7 +549,8 @@ void test_credit_driven_delivery() {
|
||||||
const auto cid = permission_requests.front();
|
const auto cid = permission_requests.front();
|
||||||
permission_requests.pop_front();
|
permission_requests.pop_front();
|
||||||
in_credit_event = true;
|
in_credit_event = true;
|
||||||
require(switch_native_output_on_can_send_now(&target, cid), "credit event was ignored");
|
require(!native_output_scheduler_on_can_send_now(&target, cid),
|
||||||
|
"Nintendo arbitration swallowed the generic LED FIFO event");
|
||||||
in_credit_event = false;
|
in_credit_event = false;
|
||||||
expect_bytes(last_frame(target), kSeed, "credit window did not submit current native state");
|
expect_bytes(last_frame(target), kSeed, "credit window did not submit current native state");
|
||||||
}
|
}
|
||||||
|
|
@ -595,6 +601,61 @@ void test_pending_hold_preserves_initial_latency() {
|
||||||
diagnostics().max_latency_us == last_frame(target).submitted_us - first,
|
diagnostics().max_latency_us == last_frame(target).submitted_us - first,
|
||||||
"coalescing hid the initial wait or counted redundant commands as loss");
|
"coalescing hid the initial wait or counted redundant commands as loss");
|
||||||
}
|
}
|
||||||
|
void test_stop_priority(bool stopping) {
|
||||||
|
auto quiet = device();
|
||||||
|
auto periodic = device();
|
||||||
|
switch_native_output_configure(persisted({identity(), identity(2)}), 1);
|
||||||
|
switch_native_output_attach(0, kGeneration, &quiet, identity());
|
||||||
|
switch_native_output_attach(1, 21, &periodic, identity(2));
|
||||||
|
if (stopping) {
|
||||||
|
submit({255, 0}, true);
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
writable = false;
|
||||||
|
submit({0, 255}, true, 1, 21);
|
||||||
|
flush();
|
||||||
|
advance_ms(1);
|
||||||
|
submit({}, true); // A true stop only when the first controller was active.
|
||||||
|
flush();
|
||||||
|
const size_t before = wire.size();
|
||||||
|
writable = true;
|
||||||
|
advance_ms(1);
|
||||||
|
require(wire.size() >= before + 2,
|
||||||
|
"one controller's grant prevented the other pending output");
|
||||||
|
require(wire[before].device == (stopping ? &quiet : &periodic),
|
||||||
|
stopping ? "real stop failed to preempt an older vibration deadline"
|
||||||
|
: "already-silent host report hijacked urgent stop priority");
|
||||||
|
if (stopping) require(is_neutral(wire[before]), "urgent stop was not physical neutral");
|
||||||
|
expect_state(periodic, {}, {64, 64, 0, 17867});
|
||||||
|
require(!native_output_scheduler_granted(&quiet) &&
|
||||||
|
!native_output_scheduler_granted(&periodic),
|
||||||
|
"completed synchronous callbacks leaked a scheduler grant");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_reused_device_pending_credit() {
|
||||||
|
auto target = device();
|
||||||
|
attach_approved(target);
|
||||||
|
writable = false;
|
||||||
|
submit({255, 0}, true);
|
||||||
|
flush();
|
||||||
|
const uint16_t old_cid = target.conn.interrupt_cid;
|
||||||
|
switch_native_output_detach(&target);
|
||||||
|
target.native_owned = false; // Parser teardown, before its object is reused.
|
||||||
|
switch_native_output_attach(0, kGeneration + 1, &target, identity());
|
||||||
|
require(target.conn.interrupt_cid == old_cid, "fixture did not reuse the physical CID");
|
||||||
|
const size_t before = wire.size();
|
||||||
|
writable = true;
|
||||||
|
advance_ms(1);
|
||||||
|
require(wire.size() == before + 1 && is_neutral(wire.back()),
|
||||||
|
"stale credit replayed output across a reused device generation");
|
||||||
|
submit({0, 255}, true, 0, kGeneration + 1);
|
||||||
|
flush();
|
||||||
|
expect_state(target, {}, {64, 64, 0, 17867});
|
||||||
|
require(diagnostics().completed_commands == 1 &&
|
||||||
|
!native_output_scheduler_granted(&target),
|
||||||
|
"reconnect lost its new command or retained a stale grant");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
@ -637,9 +698,16 @@ bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool l2cap_can_send_packet_now(uint16_t) {
|
||||||
|
return writable && (!credit_event_only || in_credit_event);
|
||||||
|
}
|
||||||
|
int hci_number_free_acl_slots_for_handle(uint16_t) {
|
||||||
|
return writable ? 8 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t l2cap_request_can_send_now_event(uint16_t cid) {
|
uint8_t l2cap_request_can_send_now_event(uint16_t cid) {
|
||||||
if (writable && !credit_event_only)
|
if (writable && !credit_event_only)
|
||||||
switch_native_output_on_can_send_now(radio_devices[cid], cid);
|
native_output_scheduler_on_can_send_now(radio_devices[cid], cid);
|
||||||
else if (std::find(permission_requests.begin(), permission_requests.end(), cid) ==
|
else if (std::find(permission_requests.begin(), permission_requests.end(), cid) ==
|
||||||
permission_requests.end())
|
permission_requests.end())
|
||||||
permission_requests.push_back(cid);
|
permission_requests.push_back(cid);
|
||||||
|
|
@ -650,6 +718,7 @@ bool uni_hid_parser_switch_native_info(uni_hid_device_t* target, uint8_t* type,
|
||||||
uint8_t* firmware_hi, uint8_t* firmware_lo) {
|
uint8_t* firmware_hi, uint8_t* firmware_lo) {
|
||||||
if (!target || !target->info_ready) return false;
|
if (!target || !target->info_ready) return false;
|
||||||
if (!target->conn.interrupt_cid) target->conn.interrupt_cid = next_cid++;
|
if (!target->conn.interrupt_cid) target->conn.interrupt_cid = next_cid++;
|
||||||
|
target->conn.handle = target->conn.interrupt_cid;
|
||||||
radio_devices[target->conn.interrupt_cid] = target;
|
radio_devices[target->conn.interrupt_cid] = target;
|
||||||
if (type) *type = target->controller_type;
|
if (type) *type = target->controller_type;
|
||||||
if (firmware_hi) *firmware_hi = 5;
|
if (firmware_hi) *firmware_hi = 5;
|
||||||
|
|
@ -657,15 +726,17 @@ bool uni_hid_parser_switch_native_info(uni_hid_device_t* target, uint8_t* type,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool uni_hid_parser_switch_native_acquire(uni_hid_device_t* target) {
|
bool uni_hid_parser_switch_native_acquire(uni_hid_device_t* target) {
|
||||||
if (!target->connected || !target->info_ready || !target->acquire_allowed) return false;
|
if (!target->conn.connected || !target->info_ready || !target->acquire_allowed) return false;
|
||||||
require(!target->native_owned, "parser acquired twice without release");
|
require(!target->native_owned, "parser acquired twice without release");
|
||||||
target->native_owned = true;
|
target->native_owned = true;
|
||||||
++target->acquisitions;
|
++target->acquisitions;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool uni_hid_parser_switch_native_send(uni_hid_device_t* target, const uint8_t rumble[8]) {
|
bool uni_hid_parser_switch_native_send(uni_hid_device_t* target, const uint8_t rumble[8]) {
|
||||||
require(target && target->connected && target->native_owned,
|
require(target && target->conn.connected && target->native_owned,
|
||||||
"native send reached disconnected or unowned parser");
|
"native send reached disconnected or unowned parser");
|
||||||
|
require(native_output_scheduler_granted(target),
|
||||||
|
"native parser send bypassed the shared scheduler grant");
|
||||||
require(!credit_event_only || in_credit_event,
|
require(!credit_event_only || in_credit_event,
|
||||||
"native sender polled outside the notified credit window");
|
"native sender polled outside the notified credit window");
|
||||||
bool accepted = writable;
|
bool accepted = writable;
|
||||||
|
|
@ -680,7 +751,7 @@ bool uni_hid_parser_switch_native_send(uni_hid_device_t* target, const uint8_t r
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void uni_hid_parser_switch_native_release(uni_hid_device_t* target) {
|
void uni_hid_parser_switch_native_release(uni_hid_device_t* target) {
|
||||||
require(target && target->connected && target->native_owned,
|
require(target && target->conn.connected && target->native_owned,
|
||||||
"parser released while disconnected or already unowned");
|
"parser released while disconnected or already unowned");
|
||||||
target->native_owned = false;
|
target->native_owned = false;
|
||||||
++target->releases;
|
++target->releases;
|
||||||
|
|
@ -689,6 +760,7 @@ void uni_hid_parser_switch_native_release(uni_hid_device_t* target) {
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
require(argc == 2, "one regression scenario is required");
|
require(argc == 2, "one regression scenario is required");
|
||||||
scenario = argv[1];
|
scenario = argv[1];
|
||||||
|
native_output_scheduler_prepare();
|
||||||
switch_native_output_prepare();
|
switch_native_output_prepare();
|
||||||
if (std::strcmp(scenario, "approval") == 0) test_approval();
|
if (std::strcmp(scenario, "approval") == 0) test_approval();
|
||||||
else if (std::strcmp(scenario, "model-gate") == 0) test_model_gate();
|
else if (std::strcmp(scenario, "model-gate") == 0) test_model_gate();
|
||||||
|
|
@ -707,6 +779,9 @@ int main(int argc, char** argv) {
|
||||||
else if (std::strcmp(scenario, "credit-driven") == 0) test_credit_driven_delivery();
|
else if (std::strcmp(scenario, "credit-driven") == 0) test_credit_driven_delivery();
|
||||||
else if (std::strcmp(scenario, "held-state") == 0) test_held_state_coalescing();
|
else if (std::strcmp(scenario, "held-state") == 0) test_held_state_coalescing();
|
||||||
else if (std::strcmp(scenario, "pending-hold") == 0) test_pending_hold_preserves_initial_latency();
|
else if (std::strcmp(scenario, "pending-hold") == 0) test_pending_hold_preserves_initial_latency();
|
||||||
|
else if (std::strcmp(scenario, "silent-priority") == 0) test_stop_priority(false);
|
||||||
|
else if (std::strcmp(scenario, "stop-priority") == 0) test_stop_priority(true);
|
||||||
|
else if (std::strcmp(scenario, "reused-credit") == 0) test_reused_device_pending_credit();
|
||||||
else require(false, "unknown regression scenario");
|
else require(false, "unknown regression scenario");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,13 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
"-DSWITCH_PICO_HD_RUMBLE=1",
|
"-DSWITCH_PICO_HD_RUMBLE=1",
|
||||||
"-DSWITCH_PICO_HAPTICS_EXPERIMENT_RAM=0",
|
"-DSWITCH_PICO_HAPTICS_EXPERIMENT_RAM=0",
|
||||||
str(root / "src" / "firmware" / "input" / "haptics_experiment.cpp"),
|
str(root / "src" / "firmware" / "input" / "haptics_experiment.cpp"),
|
||||||
|
str(
|
||||||
|
root
|
||||||
|
/ "src"
|
||||||
|
/ "firmware"
|
||||||
|
/ "input"
|
||||||
|
/ "native_output_scheduler.cpp"
|
||||||
|
),
|
||||||
str(
|
str(
|
||||||
root
|
root
|
||||||
/ "src"
|
/ "src"
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ def test_haptics_experiment_native(
|
||||||
f"-I{root / 'src' / 'firmware'}",
|
f"-I{root / 'src' / 'firmware'}",
|
||||||
str(root / "tests" / "haptics_experiment_test.cpp"),
|
str(root / "tests" / "haptics_experiment_test.cpp"),
|
||||||
str(root / "src" / "firmware" / "input" / "haptics_experiment.cpp"),
|
str(root / "src" / "firmware" / "input" / "haptics_experiment.cpp"),
|
||||||
|
str(root / "src" / "firmware" / "input" / "native_output_scheduler.cpp"),
|
||||||
str(root / "src" / "firmware" / "input" / "switch_hd_rumble_synth.cpp"),
|
str(root / "src" / "firmware" / "input" / "switch_hd_rumble_synth.cpp"),
|
||||||
str(root / "src" / "firmware" / "usb" / "switch" / "switch_haptics.cpp"),
|
str(root / "src" / "firmware" / "usb" / "switch" / "switch_haptics.cpp"),
|
||||||
"-o",
|
"-o",
|
||||||
|
|
|
||||||
45
tests/test_native_output_scheduler_native.py
Normal file
45
tests/test_native_output_scheduler_native.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_output_scheduler_native(tmp_path: Path) -> None:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
compiler = shutil.which("c++") or shutil.which("g++")
|
||||||
|
assert compiler is not None, "a host C++ compiler is required"
|
||||||
|
executable = tmp_path / "native_output_scheduler_test"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
compiler,
|
||||||
|
"-std=c++17",
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
"-Werror",
|
||||||
|
"-pedantic",
|
||||||
|
f"-I{root / 'tests' / 'switch_native_output_native_stubs'}",
|
||||||
|
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||||
|
f"-I{root / 'src' / 'firmware'}",
|
||||||
|
str(root / "src" / "firmware" / "input" / "native_output_scheduler.cpp"),
|
||||||
|
str(root / "tests" / "native_output_scheduler_test.cpp"),
|
||||||
|
"-o",
|
||||||
|
str(executable),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
cwd=root,
|
||||||
|
)
|
||||||
|
for scenario in (
|
||||||
|
"edf",
|
||||||
|
"urgent",
|
||||||
|
"reservation",
|
||||||
|
"reservation-tie",
|
||||||
|
"reservation-roll-forward",
|
||||||
|
"grant",
|
||||||
|
"reuse",
|
||||||
|
"stale-completion",
|
||||||
|
"bounded",
|
||||||
|
"generic",
|
||||||
|
"error",
|
||||||
|
):
|
||||||
|
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||||
|
|
@ -23,6 +23,7 @@ def test_switch_native_output_native(tmp_path: Path) -> None:
|
||||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||||
f"-I{firmware}",
|
f"-I{firmware}",
|
||||||
str(firmware / "input" / "switch_native_output.cpp"),
|
str(firmware / "input" / "switch_native_output.cpp"),
|
||||||
|
str(firmware / "input" / "native_output_scheduler.cpp"),
|
||||||
str(firmware / "usb" / "switch" / "switch_native_haptics.cpp"),
|
str(firmware / "usb" / "switch" / "switch_native_haptics.cpp"),
|
||||||
str(firmware / "usb" / "switch" / "switch_haptics.cpp"),
|
str(firmware / "usb" / "switch" / "switch_haptics.cpp"),
|
||||||
str(firmware / "configuration" / "adapter_configuration.cpp"),
|
str(firmware / "configuration" / "adapter_configuration.cpp"),
|
||||||
|
|
@ -54,5 +55,8 @@ def test_switch_native_output_native(tmp_path: Path) -> None:
|
||||||
"credit-driven",
|
"credit-driven",
|
||||||
"held-state",
|
"held-state",
|
||||||
"pending-hold",
|
"pending-hold",
|
||||||
|
"silent-priority",
|
||||||
|
"stop-priority",
|
||||||
|
"reused-credit",
|
||||||
):
|
):
|
||||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue