switch-pico/tests/switch_native_output_test.cpp

787 lines
35 KiB
C++

#include "input/switch_native_output.h"
#include "input/native_output_scheduler.h"
#include <btstack.h>
#include <parser/uni_hid_parser_switch.h>
#include <uni.h>
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <initializer_list>
#include <iostream>
#include <map>
#include <vector>
namespace {
// These are rumble payloads at the parser's native-send boundary, not simulated
// HCI packets or evidence of physical playback. Only accepted sends advance the
// independent decoder representing the controller's received command history.
constexpr uint8_t kNeutral[8] = {0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40};
constexpr uint8_t kSeed[8] = {0x00, 0x21, 0x40, 0x48, 0x00, 0x01, 0x40, 0x40};
constexpr uint8_t kThree[8] = {0x78, 0x77, 0x1c, 0xe9, 0x00, 0x01, 0x40, 0x40};
constexpr uint8_t kDifferent[8] = {0xa8, 0x89, 0xe3, 0x4d, 0x80, 0x00, 0x40, 0x52};
constexpr SwitchHapticsSample kSteps[3] = {
{65, 65, 2139, 2282}, {65, 66, 2139, 2093}, {65, 66, 2093, 2093},
};
constexpr uint32_t kGeneration = 7;
const char* scenario = "startup";
uint64_t now_us = 1000000;
bool poll_requested = false;
bool writable = true;
std::deque<bool> send_results;
std::vector<btstack_data_source_t*> sources;
std::vector<btstack_timer_source_t*> timers;
std::map<uint16_t, uni_hid_device_t*> radio_devices;
std::deque<uint16_t> permission_requests;
uint16_t next_cid = 0x40;
bool credit_event_only = false, in_credit_event = false;
struct WireFrame {
uni_hid_device_t* device;
uint64_t submitted_us;
std::array<uint8_t, 8> bytes;
ControllerRumbleOutput decoded;
};
struct CompatibilityCall {
uni_hid_device_t* device;
uint16_t delay_ms;
uint16_t duration_ms;
uint8_t weak;
uint8_t strong;
size_t wire_position;
uint64_t submitted_us;
};
std::vector<WireFrame> wire;
std::vector<CompatibilityCall> compatibility;
std::map<uni_hid_device_t*, SwitchHapticsDecoder> physical;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << scenario << ": " << message << '\n';
std::exit(1);
}
}
// Run the actual registered data-source and timer callbacks. The IRQ wake is
// deliberately deferred: submitting on the producer never calls the owner.
void run_until(uint64_t target_us) {
require(target_us >= now_us, "fake clock moved backwards");
for (unsigned dispatches = 0; dispatches < 10000; ++dispatches) {
if (writable && !credit_event_only && !permission_requests.empty()) {
const auto cid = permission_requests.front();
permission_requests.pop_front();
require(!native_output_scheduler_on_can_send_now(radio_devices[cid], cid),
"Nintendo arbitration swallowed the generic LED FIFO event");
continue;
}
if (poll_requested) {
poll_requested = false;
const auto ready = sources;
for (auto* source : ready) {
if (source->callbacks & DATA_SOURCE_CALLBACK_POLL) {
require(source->handler != nullptr, "data source lacks handler");
source->handler(source, DATA_SOURCE_CALLBACK_POLL);
}
}
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) {
now_us = target_us;
return;
}
auto* timer = *next;
timers.erase(next);
require(timer->due_us >= now_us && timer->handler != nullptr,
"invalid timer deadline or callback");
now_us = timer->due_us;
timer->handler(timer);
}
require(false, "runloop did not quiesce within bounded dispatches");
}
void flush() { run_until(now_us); }
void advance_ms(uint32_t milliseconds) { run_until(now_us + uint64_t{milliseconds} * 1000); }
void conventional(uni_hid_device_t* device, uint16_t delay_ms,
uint16_t duration_ms, uint8_t weak, uint8_t strong) {
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});
}
uni_hid_device_t device() {
uni_hid_device_t result{};
result.report_parser.play_dual_rumble = conventional;
return result;
}
ControllerIdentity identity(uint8_t address = 1, uint16_t product = 0x2009) {
ControllerIdentity result{};
result.stable = true;
result.transport = ControllerTransport::kClassic;
result.address[0] = 0x24;
result.address[1] = 0x68;
result.address[5] = address;
result.vendor_id = 0x057e;
result.product_id = product;
return result;
}
AdapterConfiguration persisted(std::initializer_list<ControllerIdentity> identities) {
auto configuration = adapter_configuration_default();
for (const auto& approved : identities) {
configuration.native_switch_controllers[configuration.native_switch_controller_count++] = approved;
}
std::array<uint8_t, ADAPTER_CONFIGURATION_ENCODED_SIZE> bytes{};
require(adapter_configuration_encode(configuration, bytes.data(), bytes.size()),
"could not persist approval fixture through real configuration codec");
AdapterConfiguration restored{};
require(adapter_configuration_decode(ADAPTER_CONFIGURATION_SCHEMA_VERSION,
bytes.data(), bytes.size(), &restored),
"could not restore persisted approval fixture");
return restored;
}
void attach_approved(uni_hid_device_t& target) {
switch_native_output_configure(persisted({identity()}), 1);
switch_native_output_attach(0, kGeneration, &target, identity());
require(switch_native_output_owns(&target), "persisted physical approval did not acquire owner");
}
SwitchNativeOutputDiagnostics diagnostics(uint8_t slot = 0) {
SwitchNativeOutputDiagnostics result{};
switch_native_output_snapshot(slot, &result);
return result;
}
void submit(const ControllerRumbleOutput& rumble, bool stateful = false,
uint8_t slot = 0, uint32_t generation = kGeneration) {
require(switch_native_output_submit(slot, generation, now_us, rumble, stateful),
"current approved host command was rejected");
}
ControllerRumbleOutput three_steps() {
SwitchHapticsDecoder host;
host.decode(kSeed);
return host.decode(kThree);
}
const WireFrame& last_frame(uni_hid_device_t& target) {
const auto found = std::find_if(wire.rbegin(), wire.rend(),
[&](const auto& frame) { return frame.device == &target; });
require(found != wire.rend(), "controller has no accepted output");
return *found;
}
size_t frame_count(uni_hid_device_t& target) {
return std::count_if(wire.begin(), wire.end(),
[&](const auto& frame) { return frame.device == &target; });
}
bool is_neutral(const WireFrame& frame) {
return std::memcmp(frame.bytes.data(), kNeutral, sizeof(kNeutral)) == 0;
}
void expect_bytes(const WireFrame& frame, const uint8_t expected[8], const char* message) {
require(std::memcmp(frame.bytes.data(), expected, 8) == 0, message);
}
void expect_sample(const SwitchHapticsSample& actual, const SwitchHapticsSample& expected) {
require(actual.low_frequency_index == expected.low_frequency_index &&
actual.high_frequency_index == expected.high_frequency_index &&
actual.low_amplitude_q15 == expected.low_amplitude_q15 &&
actual.high_amplitude_q15 == expected.high_amplitude_q15,
"accepted output has wrong actuator band amplitude or frequency");
}
void expect_state(uni_hid_device_t& target, SwitchHapticsSample left,
SwitchHapticsSample right = {}) {
const auto& frame = last_frame(target).decoded.hd;
require(frame.actuators[0].sample_count > 0 && frame.actuators[1].sample_count > 0,
"accepted payload has no decoded endpoint");
expect_sample(frame.actuators[0].samples[frame.actuators[0].sample_count - 1], left);
expect_sample(frame.actuators[1].samples[frame.actuators[1].sample_count - 1], right);
}
void expect_three_steps(uni_hid_device_t& target) {
const auto& left = last_frame(target).decoded.hd.actuators[0];
require(left.sample_count == 3, "bounded schedule collapsed ordered host substeps");
for (uint8_t i = 0; i < 3; ++i) expect_sample(left.samples[i], kSteps[i]);
expect_state(target, kSteps[2]);
}
void expect_silence_since(size_t first) {
for (size_t i = first; i < wire.size(); ++i) {
require(is_neutral(wire[i]), "stopped or disconnected host vibration reappeared");
}
}
void test_approval() {
auto target = device();
switch_native_output_configure(persisted({}), 1);
switch_native_output_attach(0, kGeneration, &target, identity());
require(!switch_native_output_owns(&target) && target.acquisitions == 0,
"Nintendo VID/PID automatically enabled native output");
require(!switch_native_output_submit(0, kGeneration, now_us, {255, 255}, true) &&
!switch_native_output_feedback(&target, 255, 255, 10),
"unapproved native host or feedback output was accepted");
flush();
require(wire.empty() && compatibility.empty(), "unapproved attach disturbed conventional output");
switch_native_output_configure(persisted({identity(2)}), 2);
require(!switch_native_output_owns(&target) && wire.empty(),
"approval leaked to another physical address of the same model");
switch_native_output_configure(persisted({identity()}), 3);
require(switch_native_output_owns(&target) && target.acquisitions == 1,
"persisted matching identity did not acquire native output");
require(is_neutral(last_frame(target)), "approval handoff did not establish physical neutral");
SwitchHapticsDecoder host;
const size_t before = wire.size();
submit(host.decode(kSeed));
require(wire.size() == before, "producer submission bypassed deferred runloop wake");
flush();
expect_bytes(last_frame(target), kSeed, "approved unity output changed a safe native payload");
}
void test_model_gate() {
auto mismatch = device();
mismatch.controller_type = 1;
switch_native_output_configure(persisted({identity()}), 1);
switch_native_output_attach(0, kGeneration, &mismatch, identity());
require(!switch_native_output_owns(&mismatch) && mismatch.acquisitions == 0 && wire.empty(),
"approved identity overrode mismatched parser controller type");
auto unavailable = device();
unavailable.info_ready = false;
switch_native_output_attach(0, kGeneration, &unavailable, identity());
require(!switch_native_output_owns(&unavailable) && wire.empty(),
"missing parser device-info enabled native output");
auto unstable = device();
auto transient = identity();
transient.stable = false;
switch_native_output_attach(0, kGeneration, &unstable, transient);
require(!switch_native_output_owns(&unstable) && wire.empty(),
"unstable identity inherited a persisted approval");
switch_native_output_detach(&unstable);
auto refused = device();
refused.acquire_allowed = false;
switch_native_output_attach(0, kGeneration, &refused, identity());
require(!switch_native_output_owns(&refused) &&
!switch_native_output_submit(0, kGeneration, now_us, {255, 0}, true) && wire.empty(),
"failed parser acquisition swallowed compatibility host commands");
}
void test_revocation(bool expired) {
auto target = device();
attach_approved(target);
SwitchHapticsDecoder host;
submit(host.decode(kSeed));
flush();
advance_ms(5);
const auto latest = host.decode(kDifferent);
const uint64_t receipt = now_us;
submit(latest); // Revoke before the queued replacement reaches the owner.
const size_t before = wire.size();
writable = false;
switch_native_output_configure(persisted({}), 2);
require(switch_native_output_owns(&target) && target.releases == 0 && compatibility.empty(),
"revocation released owner before congestion allowed neutral");
require(!switch_native_output_submit(0, kGeneration, now_us, {255, 255}, true),
"revoked identity continued accepting native host updates");
advance_ms(expired ? 55 : 10);
require(wire.size() == before && compatibility.empty(),
"blocked neutral leaked output or resumed compatibility early");
writable = true;
advance_ms(1);
require(!switch_native_output_owns(&target) && target.releases == 1 && compatibility.size() == 1,
"neutral completion did not release and resume conventional output");
const auto& resumed = compatibility.back();
require(resumed.device == &target && resumed.delay_ms == 0 &&
resumed.wire_position > before && is_neutral(wire[resumed.wire_position - 1]),
"compatibility did not follow the accepted neutral barrier");
if (expired) {
require(resumed.duration_ms == 0 && resumed.weak == 0 && resumed.strong == 0,
"revocation resurrected an expired host effect");
} else {
const uint16_t remaining = static_cast<uint16_t>((receipt + 50000 - resumed.submitted_us + 999) / 1000);
require(resumed.duration_ms == remaining &&
resumed.weak == latest.high_frequency_magnitude &&
resumed.strong == latest.low_frequency_magnitude,
"compatibility resumed stale magnitudes or restarted the 50ms host lifetime");
}
const size_t released = wire.size();
advance_ms(100);
require(wire.size() == released && compatibility.size() == 1,
"retired native timer wrote after compatibility resumed");
}
void test_generation() {
auto old = device();
auto other = device();
auto replacement = device();
switch_native_output_configure(persisted({identity(), identity(2)}), 1);
switch_native_output_attach(0, kGeneration, &old, identity());
switch_native_output_attach(1, 21, &other, identity(2));
submit({255, 0}, true);
flush();
submit(three_steps());
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());
const size_t replacement_count = frame_count(replacement);
require(is_neutral(last_frame(replacement)), "reconnect inherited a previous physical baseline");
require(!switch_native_output_submit(0, kGeneration, now_us, {255, 255}, true),
"old generation was accepted after same-address reconnect");
submit({0, 255}, true, 1, 21);
advance_ms(60); // Includes the outstanding producer wake and old refresh/expiry deadlines.
require(frame_count(old) == old_count && frame_count(replacement) == replacement_count,
"old queued command or timer touched disconnected/replacement device");
expect_state(other, {}, {64, 64, 0, 17867});
submit({255, 0}, true, 0, kGeneration + 1);
flush();
expect_state(replacement, {64, 64, 17867, 0});
expect_state(other, {}, {64, 64, 0, 17867});
require(diagnostics(0).completed_commands == 1 && diagnostics(1).completed_commands == 1,
"slot or generation completion accounting crossed controllers");
}
void test_overflow() {
auto target = device();
attach_approved(target);
const auto before_diagnostics = diagnostics();
const size_t before = wire.size();
SwitchHapticsDecoder host;
for (unsigned i = 0; i < 39; ++i)
submit(host.decode((i & 1u) ? kDifferent : kSeed));
submit(host.decode(kThree));
require(wire.size() == before && diagnostics().queue_depth <= 16,
"producer bypassed bounded deferred command queue");
flush();
require(wire.size() > before && wire.size() <= before + 3 && is_neutral(wire[before]),
"queue loss replayed a backlog instead of neutral plus bounded newest schedule");
expect_three_steps(target);
const auto after = diagnostics();
require(after.received_commands == 40 && after.dropped_commands == 39 &&
after.completed_commands == 1 && after.queue_depth == 0 &&
after.resynchronizations > before_diagnostics.resynchronizations,
"queue pressure did not distinguish discarded commands from the completed newest state");
const size_t drained = wire.size();
advance_ms(12);
require(wire.size() == drained, "owner caught up obsolete queued vibrations after draining");
}
void test_retry() {
auto target = device();
attach_approved(target);
const size_t before = wire.size();
const auto before_diagnostics = diagnostics();
send_results = {false};
submit(three_steps());
flush();
require(wire.size() == before && diagnostics().completed_commands == 0,
"failed first schedule packet counted as physical output or completion");
send_results = {true, false};
advance_ms(1);
require(wire.size() == before + 1 && diagnostics().completed_commands == 0,
"baseline-only partial submission counted as complete host command");
advance_ms(1);
expect_three_steps(target);
const auto after = diagnostics();
require(wire.size() == before + 2 && after.completed_commands == 1 &&
after.dropped_commands == 0 && after.congested_attempts == 2 &&
after.resynchronizations == before_diagnostics.resynchronizations,
"short congestion retry duplicated baseline, dropped history, or miscounted completion");
}
void test_partial_replacement() {
auto target = device();
attach_approved(target);
send_results = {true, false};
submit(three_steps());
flush();
require(diagnostics().completed_commands == 0, "partial schedule was already complete");
const size_t before = wire.size();
SwitchHapticsDecoder latest_host;
submit(latest_host.decode(kDifferent));
flush();
require(wire.size() == before + 2 && is_neutral(wire[before]),
"replacing prepared/partially sent schedule omitted the physical reset barrier");
expect_bytes(last_frame(target), kDifferent, "old prepared tail replaced newest host output");
const auto after = diagnostics();
require(after.completed_commands == 1 && after.dropped_commands == 1,
"discarded partial command was reported as completed or vanished from loss accounting");
const size_t replaced = wire.size();
advance_ms(12);
require(wire.size() == replaced, "discarded prepared tail was retried after replacement");
}
void test_stalled_schedule(bool partial) {
auto target = device();
attach_approved(target);
writable = partial;
if (partial) send_results = {true, false};
const size_t before = wire.size();
submit(three_steps());
flush();
require(wire.size() == before + (partial ? 1 : 0), "incorrect initial blocked schedule setup");
writable = false;
advance_ms(20);
require(diagnostics().completed_commands == 0, "unsubmitted stalled schedule was counted complete");
const size_t stalled = wire.size();
writable = true;
advance_ms(1);
require(wire.size() > stalled, "current endpoint was not submitted after congestion");
if (partial) require(is_neutral(wire[stalled]),
"partially submitted expired timeline lacked physical resynchronization");
for (size_t i = stalled; i < wire.size(); ++i) {
if (is_neutral(wire[i])) continue;
const auto& left = wire[i].decoded.hd.actuators[0];
require(left.sample_count == 1, "congestion replayed host substeps whose time had passed");
expect_sample(left.samples[0], kSteps[2]);
}
expect_state(target, kSteps[2]);
require(diagnostics().completed_commands == 1,
"resynchronized latest endpoint did not finish its current command");
}
void test_feedback_resume() {
auto target = device();
attach_approved(target);
SwitchHapticsDecoder host;
submit(host.decode(kSeed));
flush();
const uint64_t receipt = now_us;
require(switch_native_output_feedback(&target, 0, 255, 6), "approved local feedback was rejected");
submit(host.decode(kThree));
flush();
expect_state(target, {}, {64, 64, 0, 17867});
advance_ms(5);
expect_state(target, {}, {64, 64, 0, 17867});
const size_t before_resume = wire.size();
advance_ms(1);
require(wire.size() > before_resume && is_neutral(wire[before_resume]),
"local-feedback handoff omitted host resynchronization");
require(last_frame(target).decoded.hd.actuators[0].sample_count == 1,
"feedback resumed host timeline from its beginning instead of current substep");
expect_state(target, kSteps[2]);
run_until(receipt + 49000);
expect_state(target, kSteps[2]);
run_until(receipt + 50000);
require(is_neutral(last_frame(target)), "Switch host effect survived its original 50ms deadline");
const size_t expired = wire.size();
advance_ms(100);
expect_silence_since(expired);
}
void test_feedback_outlives_host() {
auto target = device();
attach_approved(target);
require(switch_native_output_feedback(&target, 0, 255, 60), "local feedback was rejected");
const uint64_t receipt = now_us;
submit(three_steps());
flush();
run_until(receipt + 49000);
expect_state(target, {}, {64, 64, 0, 17867});
const size_t before_expiry = wire.size();
run_until(receipt + 50000);
require(wire.size() == before_expiry,
"expiry of suppressed host command interrupted active local feedback");
run_until(receipt + 59000);
expect_state(target, {}, {64, 64, 0, 17867});
run_until(receipt + 60000);
require(is_neutral(last_frame(target)), "feedback completion resurrected already expired host output");
const size_t finished = wire.size();
advance_ms(100);
expect_silence_since(finished);
}
void test_feedback_congestion() {
auto target = device();
attach_approved(target);
SwitchHapticsDecoder host;
submit(host.decode(kSeed));
flush();
send_results = {true, false}; // Neutral reaches the sink; prepared feedback does not.
require(switch_native_output_feedback(&target, 0, 255, 30), "local feedback was rejected");
require(is_neutral(last_frame(target)), "feedback congestion fixture did not accept its neutral barrier");
submit(host.decode(kThree));
flush();
advance_ms(1);
expect_state(target, {}, {64, 64, 0, 17867});
advance_ms(28);
expect_state(target, {}, {64, 64, 0, 17867});
advance_ms(1);
expect_state(target, kSteps[2]);
require(last_frame(target).decoded.hd.actuators[0].sample_count == 1,
"congested feedback restarted suppressed host substeps on resume");
}
void test_stateful() {
auto target = device();
attach_approved(target);
submit({255, 0}, true);
flush();
expect_state(target, {64, 64, 17867, 0});
advance_ms(120);
expect_state(target, {64, 64, 17867, 0});
require(diagnostics().completed_commands == 1,
"stateful refreshes were counted as additional host commands");
require(switch_native_output_feedback(&target, 0, 255, 5), "stateful overlay feedback was rejected");
expect_state(target, {}, {64, 64, 0, 17867});
advance_ms(5);
expect_state(target, {64, 64, 17867, 0});
advance_ms(200);
expect_state(target, {64, 64, 17867, 0});
const size_t before_zero = wire.size();
submit({}, true);
flush();
require(wire.size() > before_zero && is_neutral(wire[before_zero]),
"explicit XInput zero did not immediately stop physical output");
expect_silence_since(before_zero);
const size_t stopped = wire.size();
advance_ms(200);
require(wire.size() == stopped && diagnostics().completed_commands == 2,
"stateful zero kept refreshing or changed host completion accounting");
}
void test_credit_driven_delivery() {
auto target = device();
attach_approved(target);
credit_event_only = true;
SwitchHapticsDecoder host;
const auto before = wire.size();
submit(host.decode(kSeed));
flush();
advance_ms(5);
require(wire.size() == before, "output attempted without an available credit window");
require(!permission_requests.empty(), "native owner did not request credit notification");
const auto cid = permission_requests.front();
permission_requests.pop_front();
in_credit_event = true;
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");
}
void test_held_state_coalescing() {
auto target = device();
attach_approved(target);
SwitchHapticsDecoder host;
submit(host.decode(kSeed));
flush();
const auto started = wire.size();
for (unsigned i = 0; i < 10; ++i) {
advance_ms(8);
submit(host.decode(kSeed));
flush();
}
require(diagnostics().received_commands == 11 &&
diagnostics().completed_commands == 1 &&
diagnostics().coalesced_commands == 10 &&
diagnostics().dropped_commands == 0,
"held state was lost or incorrectly counted as new radio submissions");
require(wire.size() <= started + 2, "identical reports caused redundant radio traffic");
for (size_t i = started; i < wire.size(); ++i)
expect_bytes(wire[i], kSeed, "refresh changed a held native effect");
advance_ms(49);
require(!is_neutral(last_frame(target)), "coalescing failed to extend the host watchdog");
advance_ms(2);
require(is_neutral(last_frame(target)), "held-state watchdog did not expire");
}
void test_pending_hold_preserves_initial_latency() {
auto target = device();
attach_approved(target);
writable = false;
SwitchHapticsDecoder host;
const uint64_t first = now_us;
for (unsigned i = 0; i < 3; ++i) {
submit(host.decode(kSeed));
flush();
advance_ms(8);
}
writable = true;
advance_ms(1);
expect_bytes(last_frame(target), kSeed, "pending coalescence changed the current effect");
require(diagnostics().completed_commands == 1 &&
diagnostics().coalesced_commands == 2 &&
diagnostics().dropped_commands == 0 &&
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
uint64_t time_us_64() { return now_us; }
uint32_t time_us_32() { return static_cast<uint32_t>(now_us); }
void btstack_run_loop_set_data_source_handler(
btstack_data_source_t* source,
void (*handler)(btstack_data_source_t*, btstack_data_source_callback_type_t)) {
source->handler = handler;
}
void btstack_run_loop_enable_data_source_callbacks(btstack_data_source_t* source,
uint16_t callbacks) {
source->callbacks |= callbacks;
}
void btstack_run_loop_add_data_source(btstack_data_source_t* source) {
require(std::find(sources.begin(), sources.end(), source) == sources.end(),
"data source was registered twice");
sources.push_back(source);
}
void btstack_run_loop_poll_data_sources_from_irq() { poll_requested = true; }
void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer,
void (*handler)(btstack_timer_source_t*)) {
timer->handler = handler;
}
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t timeout_ms) {
// Pico runloop adds a tick; deadlines use its millisecond clock, not a busy
// callback loop at the current instant when the owner requests timeout zero.
timer->due_us = (now_us / 1000 + uint64_t{timeout_ms} + 1) * 1000;
}
void btstack_run_loop_add_timer(btstack_timer_source_t* timer) {
require(std::find(timers.begin(), timers.end(), timer) == timers.end(),
"timer added while already scheduled");
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;
}
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)
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);
return 0;
}
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;
if (firmware_lo) *firmware_lo = 1;
return true;
}
bool uni_hid_parser_switch_native_acquire(uni_hid_device_t* target) {
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->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;
if (!send_results.empty()) {
accepted = send_results.front();
send_results.pop_front();
}
if (!accepted) return false;
WireFrame frame{target, now_us, {}, physical[target].decode(rumble)};
std::memcpy(frame.bytes.data(), rumble, frame.bytes.size());
wire.push_back(frame);
return true;
}
void uni_hid_parser_switch_native_release(uni_hid_device_t* target) {
require(target && target->conn.connected && target->native_owned,
"parser released while disconnected or already unowned");
target->native_owned = false;
++target->releases;
}
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();
else if (std::strcmp(scenario, "revocation") == 0) test_revocation(false);
else if (std::strcmp(scenario, "revocation-expired") == 0) test_revocation(true);
else if (std::strcmp(scenario, "generation") == 0) test_generation();
else if (std::strcmp(scenario, "overflow") == 0) test_overflow();
else if (std::strcmp(scenario, "retry") == 0) test_retry();
else if (std::strcmp(scenario, "partial-replacement") == 0) test_partial_replacement();
else if (std::strcmp(scenario, "stalled") == 0) test_stalled_schedule(false);
else if (std::strcmp(scenario, "stalled-partial") == 0) test_stalled_schedule(true);
else if (std::strcmp(scenario, "feedback-resume") == 0) test_feedback_resume();
else if (std::strcmp(scenario, "feedback-outlives-host") == 0) test_feedback_outlives_host();
else if (std::strcmp(scenario, "feedback-congestion") == 0) test_feedback_congestion();
else if (std::strcmp(scenario, "stateful") == 0) test_stateful();
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;
}