feat: coordinate native rumble with deadline-aware scheduling

This commit is contained in:
Joey Yakimowich-Payne 2026-09-06 15:12:06 -06:00
commit dcc97e05ba
28 changed files with 1079 additions and 106 deletions

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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);

View 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;
}

View 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);

View file

@ -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;
} }

View file

@ -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,

View file

@ -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;
} }
} }

View file

@ -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);

View file

@ -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);

View file

@ -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;
}; };

View file

@ -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();

View 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");
}

View file

@ -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);

View file

@ -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;

View file

@ -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;
} }

View file

@ -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"

View file

@ -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",

View 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)

View file

@ -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)