feat: coordinate native rumble with deadline-aware scheduling
This commit is contained in:
parent
be0f3579c6
commit
dcc97e05ba
28 changed files with 1079 additions and 106 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
187
tests/native_output_scheduler_test.cpp
Normal file
187
tests/native_output_scheduler_test.cpp
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#include "input/native_output_scheduler.h"
|
||||
#include <btstack.h>
|
||||
#include <uni.h>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
uint64_t now = 1000;
|
||||
int credits = 0;
|
||||
bool available = true, synchronous = false, hold = false;
|
||||
uint8_t request_error = 0;
|
||||
unsigned resubmit = 0;
|
||||
std::vector<uint16_t> delivered;
|
||||
std::vector<btstack_timer_source_t*> timers;
|
||||
uni_hid_device_t devices[4];
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) { std::cerr << message << '\n'; std::exit(1); }
|
||||
}
|
||||
bool send(uni_hid_device_t* device, uint16_t cid, uint32_t generation) {
|
||||
require(native_output_scheduler_granted(device), "sender ran without its grant");
|
||||
delivered.push_back(cid);
|
||||
--credits;
|
||||
if (!hold) native_output_scheduler_complete(device, generation);
|
||||
if (resubmit && device == &devices[0]) {
|
||||
--resubmit;
|
||||
native_output_scheduler_request(device, 1, 1000, false, send);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool yield_generic(uni_hid_device_t* device, uint16_t cid, uint32_t generation) {
|
||||
if (!uni_circular_buffer_is_empty(&device->outgoing_buffer)) {
|
||||
native_output_scheduler_complete(device, generation);
|
||||
return false;
|
||||
}
|
||||
return send(device, cid, generation);
|
||||
}
|
||||
void notify(unsigned index) {
|
||||
const bool consumed = native_output_scheduler_on_can_send_now(&devices[index], devices[index].conn.interrupt_cid);
|
||||
if (!consumed && devices[index].outgoing_buffer.queued) {
|
||||
--devices[index].outgoing_buffer.queued;
|
||||
--credits;
|
||||
}
|
||||
}
|
||||
void request(unsigned index, uint64_t deadline, bool urgent = false) {
|
||||
require(native_output_scheduler_request(&devices[index], 1, deadline, urgent, send) == 0,
|
||||
"valid scheduler request rejected");
|
||||
}
|
||||
void edf() {
|
||||
request(0, 3000); request(1, 1000); request(2, 2000);
|
||||
require(delivered.empty(), "scheduler sent without transport credits");
|
||||
credits = 3; notify(0);
|
||||
require(delivered == std::vector<uint16_t>({65, 66, 64}), "deadline ordering followed callback/slot order");
|
||||
}
|
||||
void urgent() {
|
||||
request(0, 1000); request(1, 90000, true);
|
||||
credits = 2; notify(0);
|
||||
require(delivered == std::vector<uint16_t>({65, 64}), "stop did not precede ordinary overdue work");
|
||||
}
|
||||
void reservation() {
|
||||
native_output_scheduler_reserve(&devices[1], 1, 10000);
|
||||
credits = 1;
|
||||
request(0, 20000);
|
||||
require(delivered.empty(), "future earlier stream lost the last available credit");
|
||||
request(1, 10000);
|
||||
require(delivered == std::vector<uint16_t>({65}), "reserved stream could not use its credit");
|
||||
native_output_scheduler_reserve(&devices[1], 1, UINT64_MAX);
|
||||
credits = 1; notify(0);
|
||||
require(delivered == std::vector<uint16_t>({65, 64}), "retired reservation blocked remaining work");
|
||||
}
|
||||
void reservation_roll_forward() {
|
||||
native_output_scheduler_reserve(&devices[1], 1, 10000);
|
||||
request(0, 20000);
|
||||
credits = 1;
|
||||
// A periodic writer announces its next interval immediately before
|
||||
// registering its already-due current packet.
|
||||
native_output_scheduler_reserve(&devices[1], 1, 30000);
|
||||
request(1, 10000);
|
||||
require(delivered == std::vector<uint16_t>({65}),
|
||||
"reservation update dispatched later work before the due request was registered");
|
||||
}
|
||||
|
||||
void reservation_tie() {
|
||||
native_output_scheduler_reserve(&devices[0], 1, 10000);
|
||||
native_output_scheduler_reserve(&devices[1], 1, 10000);
|
||||
request(0, 10000); request(1, 10000);
|
||||
credits = 1; notify(0);
|
||||
require(delivered.size() == 1, "equal periodic reservations deadlocked");
|
||||
credits = 1; notify(1);
|
||||
require(delivered.size() == 2 && delivered[0] != delivered[1], "reservation ties starved a peer");
|
||||
}
|
||||
void grant_lifetime() {
|
||||
credits = 2; hold = true;
|
||||
request(0, 1000); request(1, 500);
|
||||
require(delivered.size() == 1 && native_output_scheduler_granted(&devices[0]), "another client stole a live grant");
|
||||
require(native_output_scheduler_on_can_send_now(&devices[0], 90), "reentrant generic event escaped live output ownership");
|
||||
hold = false;
|
||||
native_output_scheduler_complete(&devices[0], 1);
|
||||
require(delivered == std::vector<uint16_t>({64, 65}), "grant completion did not unblock urgent peer");
|
||||
}
|
||||
void reuse() {
|
||||
request(0, 1000);
|
||||
native_output_scheduler_cancel(&devices[0]);
|
||||
devices[0].conn.interrupt_cid = 80;
|
||||
require(native_output_scheduler_request(&devices[0], 2, 2000, false, send) == 0, "new generation rejected");
|
||||
credits = 1;
|
||||
native_output_scheduler_on_can_send_now(&devices[0], 64);
|
||||
require(delivered == std::vector<uint16_t>({80}), "old generation/CID reached sender");
|
||||
}
|
||||
void stale_completion() {
|
||||
credits = 2;
|
||||
hold = true;
|
||||
request(0, 1000);
|
||||
native_output_scheduler_cancel(&devices[0]);
|
||||
require(native_output_scheduler_request(&devices[0], 2, 2000, false, send) == 0,
|
||||
"replacement generation was rejected");
|
||||
native_output_scheduler_complete(&devices[0], 1);
|
||||
require(native_output_scheduler_granted(&devices[0]),
|
||||
"old completion released the replacement generation's grant");
|
||||
native_output_scheduler_complete(&devices[0], 2);
|
||||
require(!native_output_scheduler_granted(&devices[0]), "current completion did not release its grant");
|
||||
}
|
||||
|
||||
void bounded_reentry() {
|
||||
credits = 100; resubmit = 50;
|
||||
request(0, 1000);
|
||||
require(delivered.size() <= 8 && !timers.empty(), "synchronous producer monopolized event loop");
|
||||
const size_t before = delivered.size();
|
||||
request(1, 1000);
|
||||
require(delivered.size() > before && delivered[before] == 65, "equal deadline tie favored self-resubmitting client");
|
||||
}
|
||||
void generic() {
|
||||
credits = 1; synchronous = true;
|
||||
devices[0].outgoing_buffer.queued = 1;
|
||||
require(native_output_scheduler_request(&devices[0], 1, 1000, false, yield_generic) == 0, "generic-yield request rejected");
|
||||
require(delivered.empty() && devices[0].outgoing_buffer.queued == 0,
|
||||
"generic FIFO yield stalled or emitted native output first");
|
||||
credits = 1;
|
||||
require(native_output_scheduler_request(&devices[0], 1, 2000, false, yield_generic) == 0, "native retry rejected");
|
||||
require(delivered == std::vector<uint16_t>({64}), "native output did not resume after generic FIFO drain");
|
||||
}
|
||||
void error() {
|
||||
available = false; request_error = 0x44;
|
||||
require(native_output_scheduler_request(&devices[0], 1, 1000, false, send) == 0x44,
|
||||
"immediate transport request error was hidden");
|
||||
require(delivered.empty() && !native_output_scheduler_granted(&devices[0]), "failed request granted output");
|
||||
}
|
||||
}
|
||||
uint64_t time_us_64() { return now; }
|
||||
bool l2cap_can_send_packet_now(uint16_t) { return available && credits > 0; }
|
||||
int hci_number_free_acl_slots_for_handle(uint16_t) { return credits; }
|
||||
uint8_t l2cap_request_can_send_now_event(uint16_t cid) {
|
||||
if (request_error) return request_error;
|
||||
if (synchronous && available && credits > 0)
|
||||
for (unsigned i = 0; i < 4; ++i) if (devices[i].conn.interrupt_cid == cid) notify(i);
|
||||
return 0;
|
||||
}
|
||||
void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer, void (*callback)(btstack_timer_source_t*)) { timer->handler = callback; }
|
||||
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t delay) { timer->due_us = (now / 1000 + delay + 1) * 1000; }
|
||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer) { timers.push_back(timer); }
|
||||
bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
|
||||
const auto found = std::find(timers.begin(), timers.end(), timer);
|
||||
if (found == timers.end()) return false;
|
||||
timers.erase(found); return true;
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
require(argc == 2, "scenario required");
|
||||
for (uint16_t i = 0; i < 4; ++i) {
|
||||
devices[i].conn.handle = 10 + i;
|
||||
devices[i].conn.interrupt_cid = 64 + i;
|
||||
}
|
||||
native_output_scheduler_prepare();
|
||||
if (!std::strcmp(argv[1], "edf")) edf();
|
||||
else if (!std::strcmp(argv[1], "urgent")) urgent();
|
||||
else if (!std::strcmp(argv[1], "reservation")) reservation();
|
||||
else if (!std::strcmp(argv[1], "reservation-tie")) reservation_tie();
|
||||
else if (!std::strcmp(argv[1], "reservation-roll-forward")) reservation_roll_forward();
|
||||
else if (!std::strcmp(argv[1], "grant")) grant_lifetime();
|
||||
else if (!std::strcmp(argv[1], "reuse")) reuse();
|
||||
else if (!std::strcmp(argv[1], "stale-completion")) stale_completion();
|
||||
else if (!std::strcmp(argv[1], "bounded")) bounded_reentry();
|
||||
else if (!std::strcmp(argv[1], "generic")) generic();
|
||||
else if (!std::strcmp(argv[1], "error")) error();
|
||||
else require(false, "unknown scenario");
|
||||
}
|
||||
|
|
@ -28,4 +28,6 @@ void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer,
|
|||
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t timeout_ms);
|
||||
void btstack_run_loop_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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
45
tests/test_native_output_scheduler_native.py
Normal file
45
tests/test_native_output_scheduler_native.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_native_output_scheduler_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
executable = tmp_path / "native_output_scheduler_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root / 'tests' / 'switch_native_output_native_stubs'}",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
str(root / "src" / "firmware" / "input" / "native_output_scheduler.cpp"),
|
||||
str(root / "tests" / "native_output_scheduler_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
for scenario in (
|
||||
"edf",
|
||||
"urgent",
|
||||
"reservation",
|
||||
"reservation-tie",
|
||||
"reservation-roll-forward",
|
||||
"grant",
|
||||
"reuse",
|
||||
"stale-completion",
|
||||
"bounded",
|
||||
"generic",
|
||||
"error",
|
||||
):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
|
|
@ -23,6 +23,7 @@ def test_switch_native_output_native(tmp_path: Path) -> None:
|
|||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue