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

@ -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; }
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) {
for (const auto& slot : g_slots) {
if (slot.device != nullptr && slot.device->conn.interrupt_cid == cid) {
require(haptics_experiment_on_can_send_now(slot.device, cid),
"native send permission was not consumed");
(void)uni_platform_on_l2cap_can_send_now(slot.device, cid);
return ERROR_CODE_SUCCESS;
}
}

View file

@ -6,3 +6,5 @@
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_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);
int btstack_run_loop_remove_timer(btstack_timer_source_t* timer);
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_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;
bool notification_pending = false;
bool credit = true;
uint16_t native_ready_cid = 0;
};

View file

@ -1,5 +1,6 @@
#include "input/haptics_experiment.h"
#include "input/haptics_transport_probe.h"
#include "input/native_output_scheduler.h"
#include "usb/switch/switch_haptics.h"
#include <algorithm>
@ -40,7 +41,6 @@ std::vector<Pcm> pcm;
std::vector<Generic> generic_sent;
std::vector<Generic> generic_queue;
Delivery delivery = Delivery::kImmediate;
uni_hid_device_t* permission = nullptr;
unsigned request_depth = 0;
unsigned max_request_depth = 0;
unsigned request_calls = 0;
@ -51,6 +51,7 @@ unsigned fail_sends = 0;
uint32_t send_cost_us = 0;
uint32_t request_cost_us = 0;
bool reenter_send = false;
bool detach_during_send = false;
void no_lock() {
assert(native_haptics_lock_depth == 0);
@ -132,9 +133,11 @@ bool dispatch(uni_hid_device_t* device, uint16_t cid) {
no_lock();
if (cid == device->conn.interrupt_cid) {
device->notification_pending = false;
device->native_ready_cid = cid;
}
permission = device->credit ? device : nullptr;
const bool consumed = haptics_experiment_on_can_send_now(device, cid);
const bool exclusive = haptics_experiment_blocks_generic(device);
const bool consumed =
native_output_scheduler_on_can_send_now(device, cid) || exclusive;
if (!consumed && device->credit) {
const auto it = std::find_if(generic_queue.begin(), generic_queue.end(),
[device](const Generic& report) {
@ -146,9 +149,9 @@ bool dispatch(uni_hid_device_t* device, uint16_t cid) {
generic_sent.push_back(report);
generic_queue.erase(it);
--device->outgoing_buffer.queued;
device->native_ready_cid = 0;
}
}
permission = nullptr;
return consumed;
}
@ -181,17 +184,18 @@ void reset(uint64_t at_us = 10000123) {
haptics_experiment_poll();
}
timers.clear();
native_output_scheduler_prepare();
devices = {};
pcm.clear();
generic_sent.clear();
generic_queue.clear();
now_us = at_us;
delivery = Delivery::kImmediate;
permission = nullptr;
request_depth = max_request_depth = request_calls = send_calls = timer_calls = 0;
fail_requests = fail_sends = 0;
send_cost_us = request_cost_us = 0;
reenter_send = false;
detach_during_send = false;
for (unsigned slot = 0; slot < devices.size(); ++slot) {
auto& device = devices[slot];
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.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() {
reset();
@ -830,6 +876,17 @@ uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t cid) {
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) {
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) {
no_lock();
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);
++send_calls;
const uint64_t submitted_us = now_us;
now_us += send_cost_us;
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();
assert(concurrent_snapshot.state == HapticsExperimentState::kPending ||
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, {}};
std::copy(data, data + size, packet.bytes.begin());
pcm.push_back(packet);
if (detach_during_send) {
haptics_experiment_detach(device);
}
return ERROR_CODE_SUCCESS;
}
int main(int argc, char** argv) {
assert(argc == 2);
haptics_experiment_prepare();
native_output_scheduler_prepare();
assert(snapshot().state == HapticsExperimentState::kIdle);
nominal_run(argv[1]);
stalled_deadlines();
@ -890,6 +953,8 @@ int main(int argc, char** argv) {
reconnect_and_pending_generation();
support_and_transport_errors();
timing_cost_reentrancy_and_wrap();
synchronous_teardown_releases_admission();
gameplay_led_yield_releases_admission();
gameplay_timeline_and_lifecycle();
gameplay_queued_start_and_command_overflow();
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_add_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);

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 {
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
// synchronously or fail without retaining a packet; no hidden transmit queue.
struct uni_hid_device_s {
uni_report_parser_t report_parser{};
struct { uint16_t interrupt_cid = 0; } conn;
bool connected = true;
struct {
uint16_t interrupt_cid = 0;
uint16_t handle = 0;
bool connected = true;
} conn;
uni_circular_buffer_t outgoing_buffer{};
bool info_ready = true;
bool acquire_allowed = true;
bool native_owned = false;

View file

@ -1,4 +1,5 @@
#include "input/switch_native_output.h"
#include "input/native_output_scheduler.h"
#include <btstack.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()) {
const auto cid = permission_requests.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;
}
if (poll_requested) {
@ -86,6 +88,9 @@ void run_until(uint64_t target_us) {
}
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* a, const auto* b) { return a->due_us < b->due_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,
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.push_back({device, delay_ms, duration_ms, weak, strong, wire.size(), now_us});
}
@ -311,7 +316,7 @@ void test_generation() {
submit({255, 0}, true);
flush();
submit(three_steps());
old.connected = false;
old.conn.connected = false;
const size_t old_count = frame_count(old);
switch_native_output_detach(&old);
switch_native_output_attach(0, kGeneration + 1, &replacement, identity());
@ -544,7 +549,8 @@ void test_credit_driven_delivery() {
const auto cid = permission_requests.front();
permission_requests.pop_front();
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;
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,
"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
@ -637,9 +698,16 @@ bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
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) {
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) ==
permission_requests.end())
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) {
if (!target || !target->info_ready) return false;
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;
if (type) *type = target->controller_type;
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;
}
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");
target->native_owned = true;
++target->acquisitions;
return true;
}
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");
require(native_output_scheduler_granted(target),
"native parser send bypassed the shared scheduler grant");
require(!credit_event_only || in_credit_event,
"native sender polled outside the notified credit window");
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;
}
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");
target->native_owned = false;
++target->releases;
@ -689,6 +760,7 @@ void uni_hid_parser_switch_native_release(uni_hid_device_t* target) {
int main(int argc, char** argv) {
require(argc == 2, "one regression scenario is required");
scenario = argv[1];
native_output_scheduler_prepare();
switch_native_output_prepare();
if (std::strcmp(scenario, "approval") == 0) test_approval();
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, "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, "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");
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_HAPTICS_EXPERIMENT_RAM=0",
str(root / "src" / "firmware" / "input" / "haptics_experiment.cpp"),
str(
root
/ "src"
/ "firmware"
/ "input"
/ "native_output_scheduler.cpp"
),
str(
root
/ "src"

View file

@ -41,6 +41,7 @@ def test_haptics_experiment_native(
f"-I{root / 'src' / 'firmware'}",
str(root / "tests" / "haptics_experiment_test.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" / "usb" / "switch" / "switch_haptics.cpp"),
"-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{firmware}",
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_haptics.cpp"),
str(firmware / "configuration" / "adapter_configuration.cpp"),
@ -54,5 +55,8 @@ def test_switch_native_output_native(tmp_path: Path) -> None:
"credit-driven",
"held-state",
"pending-hold",
"silent-priority",
"stop-priority",
"reused-credit",
):
subprocess.run([str(executable), scenario], check=True, cwd=root)