feat(native-usb): route gameplay rumble and attach only after startup readiness
This commit is contained in:
parent
9f8678af96
commit
ae1ca51b9a
18 changed files with 1158 additions and 43 deletions
|
|
@ -1,8 +1,29 @@
|
|||
// Reuse the backend's transport/storage fixture; these scenarios exercise only
|
||||
// the native gamepad contract, not a second implementation of its scheduler.
|
||||
#include <uni.h>
|
||||
extern "C" bool uni_hid_parser_wii_rumble_ready(uni_hid_device_t*);
|
||||
#define profile_service_active_profile_snapshot fixture_active_profile_snapshot
|
||||
#define main backend_fixture_main
|
||||
#include "bluepad32_backend_lifecycle_test.cpp"
|
||||
#undef main
|
||||
#undef profile_service_active_profile_snapshot
|
||||
|
||||
namespace {
|
||||
void (*during_profile_resolution)() = nullptr;
|
||||
bool native_wii_ready = true;
|
||||
}
|
||||
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity, ProfileServiceActiveProfileSnapshot* output) {
|
||||
require(state_lock_depth == 0, "profile service callbacks must not hold the backend lock");
|
||||
if (during_profile_resolution) during_profile_resolution();
|
||||
fixture_active_profile_snapshot(identity, output);
|
||||
}
|
||||
|
||||
extern "C" bool uni_hid_parser_wii_rumble_ready(uni_hid_device_t*) {
|
||||
require(state_lock_depth == 0, "Wii readiness must not hold the backend lock");
|
||||
return native_wii_ready;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct SensorFixture {
|
||||
|
|
@ -600,6 +621,316 @@ void mono_rumble() {
|
|||
"retired mono work must never enter a replacement connection");
|
||||
}
|
||||
|
||||
void gameplay_timeline() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "gameplay source must connect");
|
||||
uint8_t right[] = {40, 80, 120};
|
||||
const uint8_t left[] = {60, 180};
|
||||
const uint8_t stop = 0;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, right, 3) &&
|
||||
bluepad32_input_backend_native_rumble_submit(1, left, 2),
|
||||
"mixed sample counts must be accepted independently");
|
||||
right[0] = 255;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 40 && pad.last_low == 60 && pad.last_rumble_duration_ms == 4,
|
||||
"copied right/weak and left/strong samples share the earliest finite boundary");
|
||||
now_ms = 5;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 80 && pad.last_low == 60 && pad.last_rumble_duration_ms == 1,
|
||||
"5 ms polling skips elapsed time rather than replaying the first sample");
|
||||
now_ms = 10;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 120 && pad.last_low == 180 && pad.last_rumble_duration_ms == 40,
|
||||
"last samples hold only to their original receipt watchdog");
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &stop, 1),
|
||||
"explicit zero magnitude must be a valid side stop");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 0 && pad.last_low == 180 && pad.last_rumble_duration_ms == 40,
|
||||
"right stop must preserve the left contribution and deadline");
|
||||
now_ms = 49;
|
||||
const int calls = pad.rumble_calls;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == calls, "unchanged finite holds must not produce duplicate writes");
|
||||
now_ms = 50;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "watchdog expiry must stop without further host packets");
|
||||
now_ms = 500;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
const int stopped = pad.rumble_calls;
|
||||
const uint8_t delayed[] = {21, 42, 84};
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, delayed, 3), "delayed block must queue");
|
||||
now_ms = 520;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == stopped + 1 && pad.last_high == 84 &&
|
||||
pad.last_rumble_duration_ms == 30,
|
||||
"a stalled timer dispatches only the current sample with its remaining lifetime");
|
||||
bluepad32_input_backend_native_rumble_cancel(0);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "explicit cancellation must stop its live hold");
|
||||
now_ms = UINT32_MAX - 9u;
|
||||
const uint8_t pulse = 99;
|
||||
require(bluepad32_input_backend_native_rumble_submit(1, &pulse, 1), "pre-wrap request must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_low == 99 && pad.last_rumble_duration_ms == 50,
|
||||
"finite host lifetime must remain valid before clock wrap");
|
||||
now_ms = 39;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_low == 99, "wrap must not cause early watchdog expiry");
|
||||
now_ms = 40;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "watchdog must expire exactly across clock wrap");
|
||||
}
|
||||
|
||||
void gameplay_availability() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "busy gameplay source must connect");
|
||||
const uint8_t old[] = {10, 20, 30};
|
||||
const uint8_t newest[] = {70, 140};
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, old, 3), "old block must queue");
|
||||
dualsense_transport_available = false;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 5;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, newest, 2), "busy source must retain a new block");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == 0, "driver rejection cannot count as a successful output");
|
||||
now_ms = 15;
|
||||
dualsense_transport_available = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == 1 && pad.last_high == 140 && pad.last_rumble_duration_ms == 40,
|
||||
"driver recovery must dispatch only the newest current phase, not old samples");
|
||||
const uint8_t next = 210;
|
||||
during_dualsense_dispatch = [] {
|
||||
const uint8_t replacement = 33;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &replacement, 1),
|
||||
"gameplay must replace work while an earlier driver call is in flight");
|
||||
};
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &next, 1), "dispatch race must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
during_dualsense_dispatch = nullptr;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 33 && pad.last_rumble_duration_ms == 50,
|
||||
"older dispatch completion cannot consume a newly accepted revision");
|
||||
bluepad32_input_backend_native_rumble_cancel(0);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
const int stopped = pad.rumble_calls;
|
||||
dualsense_transport_available = false;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, old, 3), "stale block must queue");
|
||||
now_ms += 50;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
dualsense_transport_available = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == stopped, "expired rejected work must never replay after driver recovery");
|
||||
}
|
||||
|
||||
void gameplay_priority() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "priority source must connect");
|
||||
report_dualsense(pad);
|
||||
const uint8_t game = 45;
|
||||
uint64_t old_cue, new_cue;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &old_cue) &&
|
||||
bluepad32_input_backend_native_rumble_submit(0, &game, 1),
|
||||
"gameplay must replace a pending cue on the same side");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 45 &&
|
||||
bluepad32_input_backend_native_sample_result(0, old_cue) == -1,
|
||||
"replaced cue cannot dispatch or complete after gameplay");
|
||||
require(bluepad32_input_backend_native_sample_request(0, 6, &new_cue),
|
||||
"a new cue must replace live gameplay");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 96 && pad.last_rumble_duration_ms == 60 &&
|
||||
bluepad32_input_backend_native_sample_result(0, new_cue) == 1,
|
||||
"built-in cue completion must retain its driver-dispatch semantics");
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &game, 1) &&
|
||||
bluepad32_input_backend_native_rumble_submit(1, &game, 1),
|
||||
"fresh gameplay must replace the playing cue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
const auto source = bridge_snapshot();
|
||||
bluepad32_input_backend_queue_profile_feedback(source.slot,
|
||||
source.controller.connection_generation, 1, ControllerProfileConfirmationPolicy::kRumble);
|
||||
require(!bluepad32_input_backend_native_rumble_submit(0, &game, 1),
|
||||
"queued higher-priority feedback must not admit gameplay for later replay");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == UINT8_MAX && pad.last_low == UINT8_MAX &&
|
||||
pad.last_rumble_duration_ms == 75, "profile feedback must own the only motor writer");
|
||||
now_ms = 150;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
const int after_feedback = pad.rumble_calls;
|
||||
now_ms = 155;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == after_feedback, "profile completion must never resurrect interrupted gameplay");
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &game, 1), "fresh post-feedback gameplay must resume");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bluepad32_input_backend_toggle_motion(source.slot, source.controller.connection_generation),
|
||||
"motion feedback must queue through the existing local-feedback path");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
const int local_calls = pad.rumble_calls;
|
||||
now_ms += 3000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == local_calls, "local feedback must cancel rather than retain interrupted gameplay");
|
||||
}
|
||||
|
||||
void gameplay_source_epochs() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "epoch source must connect");
|
||||
const uint8_t game = 170;
|
||||
require(!bluepad32_input_backend_native_rumble_submit(0, nullptr, 1) &&
|
||||
!bluepad32_input_backend_native_rumble_submit(0, &game, 0) &&
|
||||
!bluepad32_input_backend_native_rumble_submit(0, &game, 4) &&
|
||||
!bluepad32_input_backend_native_rumble_submit(PROBE_CONTROLLER_COUNT, &game, 1),
|
||||
"invalid gameplay frames must fail before dispatch");
|
||||
pad.report_parser.play_dual_rumble = nullptr;
|
||||
require(!bluepad32_input_backend_native_rumble_submit(0, &game, 1),
|
||||
"source without a rumble driver must reject gameplay");
|
||||
pad.report_parser.play_dual_rumble = observe_dualsense_rumble;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &game, 1), "epoch block must queue");
|
||||
during_dualsense_dispatch = [] { bluepad32_input_backend_select_native_source(0, nullptr); };
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
during_dualsense_dispatch = nullptr;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "same-source reselection must stop an in-flight retired epoch");
|
||||
require(bluepad32_input_backend_native_rumble_submit(1, &game, 1), "disconnect block must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
platform_on_device_disconnected(&pad);
|
||||
require(pad.last_rumble_duration_ms == 0 &&
|
||||
!bluepad32_input_backend_native_rumble_submit(0, &game, 1),
|
||||
"disconnect must retire the driver's finite timer and refuse new work");
|
||||
pad = dualsense(0);
|
||||
platform_on_device_connected(&pad);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "same-address slot replacement must connect");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == 0, "slot memory reuse cannot inherit old samples or stop obligations");
|
||||
controller_profile_runtime_reset();
|
||||
during_profile_resolution = [] { bluepad32_input_backend_select_native_source(0, nullptr); };
|
||||
require(!bluepad32_input_backend_native_rumble_submit(0, &game, 1),
|
||||
"source generation must be rechecked after unlocked profile callbacks");
|
||||
during_profile_resolution = nullptr;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == 0, "a profile-resolution race cannot reach the replacement epoch");
|
||||
}
|
||||
|
||||
void gameplay_profile_gain() {
|
||||
start_pairing_backend();
|
||||
initialize_runtime_profile_storage();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "profile source must connect");
|
||||
const auto identity = identity_for_device(&pad);
|
||||
auto profile = controller_profile_default(identity, 0);
|
||||
profile.weak_rumble_scale = 128;
|
||||
profile.strong_rumble_scale = 64;
|
||||
require(runtime_profile_storage.set(identity, 0, profile) == ProfileStorageResult::kOk,
|
||||
"profile must configure independent host motor gains");
|
||||
const uint8_t maximum = 255;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &maximum, 1) &&
|
||||
bluepad32_input_backend_native_rumble_submit(1, &maximum, 1), "scaled gameplay must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 128 && pad.last_low == 64,
|
||||
"right/weak and left/strong magnitudes must use their matching persisted profile gains");
|
||||
profile.weak_rumble_scale = 0;
|
||||
profile.strong_rumble_scale = 0;
|
||||
require(runtime_profile_storage.set(identity, 0, profile) == ProfileStorageResult::kOk,
|
||||
"profile mute must update its generation");
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &maximum, 1) &&
|
||||
bluepad32_input_backend_native_rumble_submit(1, &maximum, 1), "muted gameplay must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 0 && pad.last_low == 0 && pad.last_rumble_duration_ms == 0,
|
||||
"muting a profile must stop live host output rather than retaining unscaled samples");
|
||||
uint64_t cue;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 7, &cue), "muted profile still permits local cues");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 220 && bluepad32_input_backend_native_sample_result(0, cue) == 1,
|
||||
"host gain must not scale built-in local confirmation cues");
|
||||
}
|
||||
|
||||
void gameplay_two_pairs() {
|
||||
start_pairing_backend();
|
||||
auto first = dualsense(0);
|
||||
auto second = dualsense(1);
|
||||
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
|
||||
platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "both gameplay pairs must connect");
|
||||
const uint8_t values[] = {31, 62, 93, 124};
|
||||
for (uint8_t instance = 0; instance < 4; ++instance)
|
||||
require(bluepad32_input_backend_native_rumble_submit(instance, values + instance, 1),
|
||||
"each gameplay child must bind its own pair");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(first.last_high == 31 && first.last_low == 62 &&
|
||||
second.last_high == 93 && second.last_low == 124,
|
||||
"four logical contributions must route to two independent physical sources");
|
||||
bluepad32_input_backend_native_rumble_cancel(0);
|
||||
now_ms = 10;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(first.last_high == 0 && first.last_low == 62 && first.last_rumble_duration_ms == 40 &&
|
||||
second.rumble_calls == 1, "side cancellation cannot dispatch into another pair");
|
||||
bluepad32_input_backend_select_native_source(0, nullptr);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(first.last_rumble_duration_ms == 0 && second.rumble_calls == 1,
|
||||
"source reselection must retire only its own pair's output");
|
||||
platform_on_device_disconnected(&first);
|
||||
now_ms = 50;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(second.last_rumble_duration_ms == 0 && second.rumble_calls == 2,
|
||||
"the surviving pair must expire on its original independent watchdog");
|
||||
}
|
||||
|
||||
void gameplay_paired_revision() {
|
||||
start_pairing_backend();
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
ready_switch2(left);
|
||||
ready_switch2(right);
|
||||
right.report_parser.play_dual_rumble = [](
|
||||
uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) {
|
||||
play_rumble(pad, delay, duration, weak, strong);
|
||||
const uint8_t fresh = 150;
|
||||
require(bluepad32_input_backend_native_rumble_submit(1, &fresh, 1),
|
||||
"right dispatch may accept a newer left revision");
|
||||
};
|
||||
const uint8_t game = 75;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &game, 1), "paired gameplay must queue");
|
||||
const int previous_left = left.rumble_calls;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(right.last_high == 75 && right.last_low == 75 && left.rumble_calls == previous_left,
|
||||
"new left work must invalidate even a prepared zero-output command before left dispatch");
|
||||
right.report_parser.play_dual_rumble = play_rumble;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(left.last_high == 150 && left.last_low == 150,
|
||||
"the newer paired-side revision must remain pending until its real driver dispatch");
|
||||
const uint8_t stop = 0;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &stop, 1), "paired right stop must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(right.last_rumble_duration_ms == 0 && left.last_high == 150 &&
|
||||
left.last_rumble_duration_ms == 50, "paired stop must preserve only the live sibling contribution");
|
||||
}
|
||||
|
||||
void gameplay_wii() {
|
||||
start_pairing_backend();
|
||||
auto remote = wii_device(0);
|
||||
remote.report_parser.play_dual_rumble = observe_mono_rumble;
|
||||
require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii GAMEPAD source must connect");
|
||||
const uint8_t right = 70, left = 140;
|
||||
require(bluepad32_input_backend_native_rumble_submit(0, &right, 1) &&
|
||||
bluepad32_input_backend_native_rumble_submit(1, &left, 1), "Wii contributions must queue");
|
||||
native_wii_ready = false;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(remote.rumble_calls == 0, "Wii topology setup must not consume pending output");
|
||||
native_wii_ready = true;
|
||||
now_ms = 10;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(mono_magnitude == 140 && remote.last_rumble_duration_ms == 40,
|
||||
"Wii compatibility combines both contributions on its finite mono motor");
|
||||
bluepad32_input_backend_native_rumble_cancel(1);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(mono_magnitude == 70 && remote.last_rumble_duration_ms == 40,
|
||||
"Wii side cancellation must not stop its sibling's mono contribution");
|
||||
now_ms = 50;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(mono_magnitude == 0, "Wii gameplay must stop at its original watchdog");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t*, const uint8_t*, uint16_t) {}
|
||||
|
|
@ -993,6 +1324,14 @@ int main(int argc, char** argv) {
|
|||
if (scenario == "source-isolation") source_isolation();
|
||||
else if (scenario == "cue-lifetime") cue_lifetime();
|
||||
else if (scenario == "cue-races") cue_races();
|
||||
else if (scenario == "gameplay-timeline") gameplay_timeline();
|
||||
else if (scenario == "gameplay-availability") gameplay_availability();
|
||||
else if (scenario == "gameplay-priority") gameplay_priority();
|
||||
else if (scenario == "gameplay-source-epochs") gameplay_source_epochs();
|
||||
else if (scenario == "gameplay-profile-gain") gameplay_profile_gain();
|
||||
else if (scenario == "gameplay-two-pairs") gameplay_two_pairs();
|
||||
else if (scenario == "gameplay-paired-revision") gameplay_paired_revision();
|
||||
else if (scenario == "gameplay-wii") gameplay_wii();
|
||||
else if (scenario == "stable-logical-slot") stable_logical_slot();
|
||||
else if (scenario == "sensorless-admission") sensorless_admission();
|
||||
else if (scenario == "independent-motion") independent_motion();
|
||||
|
|
|
|||
270
tests/native_hub_startup_test.c
Normal file
270
tests/native_hub_startup_test.c
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hardware_stub.h"
|
||||
|
||||
static void startup_set_bits(volatile uint32_t* address, uint32_t bits);
|
||||
static void startup_clear_bits(volatile uint32_t* address, uint32_t bits);
|
||||
static void startup_reset(uint32_t mask);
|
||||
static void startup_launch(void (*entry)(void));
|
||||
static void startup_irq_handler(unsigned irq, void (*handler)(void));
|
||||
static void startup_irq_enable(unsigned irq, bool enabled);
|
||||
static void startup_wait(void);
|
||||
|
||||
#define hw_set_bits(address, bits) startup_set_bits(address, bits)
|
||||
#define hw_clear_bits(address, bits) startup_clear_bits(address, bits)
|
||||
#define reset_block(mask) startup_reset(mask)
|
||||
#define multicore_launch_core1(entry) startup_launch(entry)
|
||||
#define irq_set_exclusive_handler(irq, handler) startup_irq_handler(irq, handler)
|
||||
#define irq_set_enabled(irq, enabled) startup_irq_enable(irq, enabled)
|
||||
#define tight_loop_contents() startup_wait()
|
||||
#define NATIVE_TEST_EXTERNAL_ROUTER 1
|
||||
#define NATIVE_TEST_EXTERNAL_IRQ 1
|
||||
#include "native_hub_transport_fixture.c"
|
||||
|
||||
// This is a register/IRQ model, not a physical USB bus or Core1 timing model.
|
||||
// The physical pull-up follows DIRECT when its override is enabled; otherwise
|
||||
// it follows SIE_CTRL. In particular, a disabled SIE pull-up cannot mask a
|
||||
// forced physical pull-up. Register bit writes and startup hardware/router
|
||||
// boundaries sample this signal. Each scenario starts in fresh process BSS.
|
||||
static bool attached, observer_initialized, observer_launched, observer_ready;
|
||||
static bool routing_enabled, addresses_published, irq_enabled, enumeration_done;
|
||||
static uint8_t routed_addresses[PROBE_ROUTER_SLOTS], routed_default;
|
||||
static void (*installed_irq)(void);
|
||||
static uint32_t ready_delay_us, observer_start_us, wait_us, attach_time_us;
|
||||
static unsigned attach_edges, detach_edges, controller_resets, setup_irqs;
|
||||
static bool observer_never_ready;
|
||||
|
||||
static bool physical_pullup(void) {
|
||||
if (usb_hw->phy_direct_override & USB_USBPHY_DIRECT_OVERRIDE_DP_PULLUP_EN_OVERRIDE_EN_BITS)
|
||||
return (usb_hw->phy_direct & USB_USBPHY_DIRECT_DP_PULLUP_EN_BITS) != 0;
|
||||
return (usb_hw->sie_ctrl & USB_SIE_CTRL_PULLUP_EN_BITS) != 0;
|
||||
}
|
||||
|
||||
static uint8_t route_address(uint8_t address) {
|
||||
assert(routing_enabled && addresses_published);
|
||||
if (!address) return routed_default;
|
||||
for (uint8_t slot = 0; slot < PROBE_ROUTER_SLOTS; ++slot)
|
||||
if (routed_addresses[slot] == address) return slot;
|
||||
return NONE;
|
||||
}
|
||||
|
||||
void native_test_service_interrupt(void) {
|
||||
if (native_test_interrupt_mask || servicing_interrupt || !irq_enabled ||
|
||||
!installed_irq || !(usb_hw->ints & usb_hw->inte)) return;
|
||||
servicing_interrupt = true;
|
||||
if (usb_hw->ints & USB_INTS_SETUP_REQ_BITS) ++setup_irqs;
|
||||
installed_irq();
|
||||
usb_hw->ints = 0;
|
||||
servicing_interrupt = false;
|
||||
}
|
||||
|
||||
static void host_setup(uint8_t address, const tusb_control_request_t* request) {
|
||||
assert(attached && physical_pullup());
|
||||
uint8_t slot = route_address(address);
|
||||
assert(slot == 0 && "root request must route through the published address table");
|
||||
assert(native_hub_select_device(address,slot,UINT32_MAX / 2u));
|
||||
memcpy(usb_dpram->setup_packet,request,sizeof(*request));
|
||||
usb_hw->sie_status = USB_SIE_STATUS_SETUP_REC_BITS;
|
||||
usb_hw->ints = USB_INTS_SETUP_REQ_BITS;
|
||||
unsigned previous = setup_irqs;
|
||||
native_test_service_interrupt();
|
||||
assert(setup_irqs == previous+1u && "first host SETUP must reach the installed IRQ");
|
||||
native_hub_task();
|
||||
assert(!failed);
|
||||
}
|
||||
|
||||
static void receive_descriptor(uint8_t type, uint16_t expected_length) {
|
||||
uint8_t response[PACKET];
|
||||
uint16_t length = 0;
|
||||
assert(native_test_in(0,response,&length,true));
|
||||
assert(length == expected_length && response[1] == type);
|
||||
if (type == TUSB_DESC_DEVICE) {
|
||||
assert(response[0] == 18 && response[4] == 9 && response[7] == PACKET);
|
||||
} else {
|
||||
assert(response[0] == 9 && response[2] == expected_length && response[3] == 0);
|
||||
assert(response[4] == 1 && response[5] == 1);
|
||||
}
|
||||
assert(native_test_out(0,NULL,0,true));
|
||||
assert(devices[0].control.stage == IDLE && !failed);
|
||||
}
|
||||
|
||||
static void enumerate_at_attach(void) {
|
||||
// Do not reset/reinitialize the transport here: that could repair the very
|
||||
// startup state being tested. Deliver the first SETUP inside the physical
|
||||
// attach write, before native_hub_init has even returned to its caller.
|
||||
const tusb_control_request_t descriptor = {
|
||||
.bmRequestType = 0x80, .bRequest = TUSB_REQ_GET_DESCRIPTOR,
|
||||
.wValue = TUSB_DESC_DEVICE << 8, .wLength = 18,
|
||||
};
|
||||
host_setup(0,&descriptor);
|
||||
receive_descriptor(TUSB_DESC_DEVICE,18);
|
||||
|
||||
const tusb_control_request_t set_address = {
|
||||
.bmRequestType = 0, .bRequest = TUSB_REQ_SET_ADDRESS, .wValue = 9,
|
||||
};
|
||||
host_setup(0,&set_address);
|
||||
assert(route_address(0) == 0 && route_address(9) == NONE);
|
||||
uint8_t response[PACKET];
|
||||
uint16_t length = 1;
|
||||
assert(native_test_in(0,response,&length,true) && length == 0);
|
||||
assert(route_address(9) == 0 && route_address(0) == NONE);
|
||||
assert(usb_hw->dev_addr_ctrl == 9);
|
||||
|
||||
const tusb_control_request_t configuration = {
|
||||
.bmRequestType = 0x80, .bRequest = TUSB_REQ_GET_DESCRIPTOR,
|
||||
.wValue = TUSB_DESC_CONFIGURATION << 8, .wLength = 25,
|
||||
};
|
||||
host_setup(9,&configuration);
|
||||
receive_descriptor(TUSB_DESC_CONFIGURATION,25);
|
||||
enumeration_done = true;
|
||||
}
|
||||
|
||||
static void observe_pullup(void) {
|
||||
bool visible = physical_pullup();
|
||||
if (visible == attached) return;
|
||||
attached = visible;
|
||||
if (!visible) {
|
||||
++detach_edges;
|
||||
return;
|
||||
}
|
||||
++attach_edges;
|
||||
attach_time_us = native_test_time_us;
|
||||
assert((usb_hw->main_ctrl & USB_MAIN_CTRL_CONTROLLER_EN_BITS) &&
|
||||
"physical attach preceded controller readiness");
|
||||
assert((usb_hw->sie_ctrl & USB_SIE_CTRL_EP0_INT_1BUF_BITS) &&
|
||||
"physical attach preceded EP0 readiness");
|
||||
assert(observer_ready && routing_enabled && addresses_published &&
|
||||
"physical attach preceded observer/address routing readiness");
|
||||
assert(routed_addresses[0] == 0 && routed_default == 0);
|
||||
for (unsigned slot = 1; slot < PROBE_ROUTER_SLOTS; ++slot)
|
||||
assert(routed_addresses[slot] == NONE);
|
||||
assert(irq_enabled && installed_irq && !native_test_interrupt_mask &&
|
||||
"physical attach preceded IRQ readiness");
|
||||
assert((usb_hw->inte & (USB_INTS_SETUP_REQ_BITS | USB_INTS_BUFF_STATUS_BITS |
|
||||
USB_INTS_BUS_RESET_BITS)) ==
|
||||
(USB_INTS_SETUP_REQ_BITS | USB_INTS_BUFF_STATUS_BITS | USB_INTS_BUS_RESET_BITS));
|
||||
assert(started && "physical attach preceded foreground/pending-IRQ readiness");
|
||||
enumerate_at_attach();
|
||||
}
|
||||
|
||||
static void startup_set_bits(volatile uint32_t* address, uint32_t bits) {
|
||||
(hw_set_bits)(address,bits);
|
||||
observe_pullup();
|
||||
}
|
||||
|
||||
static void startup_clear_bits(volatile uint32_t* address, uint32_t bits) {
|
||||
(hw_clear_bits)(address,bits);
|
||||
observe_pullup();
|
||||
}
|
||||
|
||||
static void startup_reset(uint32_t mask) {
|
||||
assert(mask == RESETS_RESET_USBCTRL_BITS);
|
||||
++controller_resets;
|
||||
memset(usb_hw,0,sizeof(*usb_hw));
|
||||
observe_pullup();
|
||||
}
|
||||
|
||||
static void startup_launch(void (*entry)(void)) {
|
||||
observe_pullup();
|
||||
assert(observer_initialized && entry == probe_router_core1);
|
||||
observer_launched = true;
|
||||
}
|
||||
|
||||
static void startup_irq_handler(unsigned irq, void (*handler)(void)) {
|
||||
observe_pullup();
|
||||
assert(irq == USBCTRL_IRQ);
|
||||
installed_irq = handler;
|
||||
}
|
||||
|
||||
static void startup_irq_enable(unsigned irq, bool enabled) {
|
||||
observe_pullup();
|
||||
assert(irq == USBCTRL_IRQ);
|
||||
irq_enabled = enabled;
|
||||
native_test_service_interrupt();
|
||||
}
|
||||
|
||||
static void startup_wait(void) {
|
||||
observe_pullup();
|
||||
assert(!attached && "host must remain detached throughout observer startup");
|
||||
native_test_time_us += 1000u;
|
||||
wait_us += 1000u;
|
||||
assert(wait_us <= 100000u && "observer startup must have a bounded timeout");
|
||||
}
|
||||
|
||||
void probe_router_init(uint32_t hz) {
|
||||
observe_pullup();
|
||||
assert(hz == 240000000u);
|
||||
observer_initialized = true;
|
||||
observer_start_us = native_test_time_us;
|
||||
memset(routed_addresses,NONE,sizeof(routed_addresses));
|
||||
routed_default = NONE;
|
||||
}
|
||||
|
||||
void probe_router_core1(void) {}
|
||||
|
||||
void probe_router_publish(const uint8_t values[PROBE_ROUTER_SLOTS], uint8_t slot) {
|
||||
observe_pullup();
|
||||
memcpy(routed_addresses,values,sizeof(routed_addresses));
|
||||
routed_default = slot;
|
||||
addresses_published = true;
|
||||
}
|
||||
|
||||
void probe_router_enable(bool enabled) {
|
||||
observe_pullup();
|
||||
routing_enabled = enabled;
|
||||
}
|
||||
|
||||
bool probe_router_set_phase(uint32_t phase) {
|
||||
(void)phase;
|
||||
observe_pullup();
|
||||
return true;
|
||||
}
|
||||
|
||||
void probe_router_snapshot(probe_router_stats* snapshot) {
|
||||
observe_pullup();
|
||||
assert(observer_initialized && observer_launched);
|
||||
if (!observer_never_ready && native_test_time_us-observer_start_us >= ready_delay_us)
|
||||
observer_ready = true;
|
||||
memset(snapshot,0,sizeof(*snapshot));
|
||||
snapshot->ready = observer_ready;
|
||||
}
|
||||
|
||||
bool tud_vendor_control_xfer_cb(uint8_t slot, uint8_t stage, const tusb_control_request_t* request) {
|
||||
(void)slot; (void)stage; (void)request;
|
||||
assert(false && "root standard enumeration must not invoke vendor handling");
|
||||
return false;
|
||||
}
|
||||
|
||||
void reset_usb_boot(uint32_t gpio_mask, uint32_t disable_mask) {
|
||||
(void)gpio_mask; (void)disable_mask;
|
||||
assert(false && "cold startup must not enter BOOTSEL");
|
||||
abort();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
if (strcmp(argv[1],"delayed") == 0) ready_delay_us = 75000u;
|
||||
else if (strcmp(argv[1],"timeout") == 0) observer_never_ready = true;
|
||||
else assert(strcmp(argv[1],"ready") == 0);
|
||||
|
||||
// No native_test_initialize/startup helper: call the actual initializer
|
||||
// from cold BSS, rather than inheriting the transport fixture's ready state.
|
||||
assert(!physical_pullup());
|
||||
bool initialized = native_hub_init();
|
||||
observe_pullup();
|
||||
assert(controller_resets == 1 && detach_edges == 0);
|
||||
if (observer_never_ready) {
|
||||
assert(!initialized && !started && !physical_pullup());
|
||||
assert(!attach_edges && !enumeration_done && !setup_irqs);
|
||||
assert(wait_us == 100000u);
|
||||
} else {
|
||||
assert(initialized && attached && attach_edges == 1);
|
||||
assert(enumeration_done && setup_irqs == 3);
|
||||
assert(wait_us == ready_delay_us && attach_time_us-observer_start_us == ready_delay_us);
|
||||
assert(startup_time == attach_time_us && !failed);
|
||||
}
|
||||
printf("native cold startup %s passed for %u children\n",argv[1],CHILDREN);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ uint16_t native_test_received_length[CHILDREN][2];
|
|||
uint8_t native_test_received_data[CHILDREN][2][PACKET];
|
||||
static bool servicing_interrupt;
|
||||
|
||||
#ifndef NATIVE_TEST_EXTERNAL_IRQ
|
||||
void native_test_service_interrupt(void) {
|
||||
if (native_test_interrupt_mask || servicing_interrupt || !usb_hw->ints) return;
|
||||
servicing_interrupt = true;
|
||||
|
|
@ -22,13 +23,16 @@ void native_test_service_interrupt(void) {
|
|||
usb_hw->ints = 0;
|
||||
servicing_interrupt = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef NATIVE_TEST_EXTERNAL_ROUTER
|
||||
void probe_router_init(uint32_t hz) { (void)hz; }
|
||||
void probe_router_core1(void) {}
|
||||
void probe_router_publish(const uint8_t values[PROBE_ROUTER_SLOTS], uint8_t slot) { (void)values; (void)slot; }
|
||||
void probe_router_enable(bool enabled) { (void)enabled; }
|
||||
bool probe_router_set_phase(uint32_t phase) { (void)phase; return true; }
|
||||
void probe_router_snapshot(probe_router_stats* snapshot) { memset(snapshot,0,sizeof(*snapshot)); snapshot->ready = 1; }
|
||||
#endif
|
||||
#ifndef NATIVE_TEST_EXTERNAL_LOG
|
||||
int probe_debug_printf(const char* format, ...) { (void)format; return 0; }
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -76,6 +76,13 @@ void bluepad32_input_backend_native_sample_cancel(uint8_t instance) {
|
|||
assert(instance < PROBE_CONTROLLER_COUNT);
|
||||
cue_tokens[instance] = 0;
|
||||
}
|
||||
bool bluepad32_input_backend_native_rumble_submit(uint8_t, const uint8_t*, uint8_t) {
|
||||
assert(false && "gameplay motor dispatch belongs to the native backend fixture");
|
||||
return false;
|
||||
}
|
||||
void bluepad32_input_backend_native_rumble_cancel(uint8_t) {
|
||||
assert(false && "gameplay motor cancellation belongs to the native backend fixture");
|
||||
}
|
||||
void bluepad32_input_backend_queue_profile_feedback(uint8_t, uint32_t, uint8_t, ControllerProfileConfirmationPolicy) {}
|
||||
void controller_profile_runtime_reset() {
|
||||
for (ControllerProfile& value : profiles)
|
||||
|
|
|
|||
|
|
@ -680,7 +680,122 @@ static void test_indexed_memory(void) {
|
|||
}
|
||||
}
|
||||
|
||||
static void expect_invalid_rumble(uint8_t report_id, const uint8_t* data, size_t length) {
|
||||
probe_rumble_frame output = {.count = 3, .magnitude = {17, 93, 241}};
|
||||
assert(!probe_protocol_decode_rumble(report_id, data, length, &output));
|
||||
assert(output.count == 3);
|
||||
assert(output.magnitude[0] == 17 && output.magnitude[1] == 93 && output.magnitude[2] == 241);
|
||||
}
|
||||
|
||||
static void test_native_rumble(void) {
|
||||
// Independent block bytes from public rumble-procon-gccon.pcapng.gz,
|
||||
// packets 9970 and 257200. These are Pro Controller report 02 LRA blocks;
|
||||
// the report 01 wrapper below is synthetic, not a captured Joy-Con packet.
|
||||
static const uint8_t captured_blocks[][16] = {
|
||||
{0x50, 0x81, 0x01, 0x10, 0x1e, 0x00},
|
||||
{0x52, 0x9f, 0x19, 0xe0, 0x9d, 0x00},
|
||||
};
|
||||
probe_rumble_frame output;
|
||||
uint8_t wire[65];
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
memset(wire, 0xa5, sizeof(wire));
|
||||
wire[0] = 0x01;
|
||||
memcpy(wire + 1, captured_blocks[i], sizeof(captured_blocks[i]));
|
||||
assert(probe_protocol_decode_rumble(0, wire, 64, &output));
|
||||
assert(output.count == 1 && output.magnitude[0] == i);
|
||||
assert(probe_protocol_decode_rumble(1, wire + 1, 63, &output));
|
||||
assert(output.count == 1 && output.magnitude[0] == i);
|
||||
}
|
||||
|
||||
// Manually specified byte boundaries, not an encoder/decoder roundtrip.
|
||||
// Frequencies are both 1023: they must not leak into either amplitude,
|
||||
// nor be rejected merely because this compatibility decoder ignores them.
|
||||
static const struct {
|
||||
uint8_t sample[5];
|
||||
uint8_t expected;
|
||||
} boundaries[] = {
|
||||
{{0xff, 0x03, 0xf0, 0x3f, 0x00}, 0}, // amplitudes 0, 0
|
||||
{{0xff, 0x0b, 0xf0, 0x3f, 0x00}, 0}, // 2, 0 rounds down
|
||||
{{0xff, 0x03, 0xf0, 0xff, 0x00}, 1}, // 0, 3 rounds up
|
||||
{{0xff, 0xff, 0xf0, 0x3f, 0x00}, 16}, // 63, 0
|
||||
{{0xff, 0x03, 0xf1, 0x3f, 0x00}, 16}, // 64, 0
|
||||
{{0xff, 0xff, 0xf7, 0x3f, 0x80}, 128}, // 511, 512
|
||||
{{0xff, 0x03, 0xf8, 0xff, 0x7f}, 128}, // 512, 511
|
||||
{{0xff, 0xff, 0xff, 0x3f, 0x00}, 255}, // 1023, 0
|
||||
{{0xff, 0x03, 0xf0, 0xff, 0xff}, 255}, // 0, 1023
|
||||
};
|
||||
wire[1] = 0x5f;
|
||||
for (unsigned i = 0; i < sizeof(boundaries) / sizeof(boundaries[0]); ++i) {
|
||||
memcpy(wire + 2, boundaries[i].sample, 5);
|
||||
assert(probe_protocol_decode_rumble(0, wire, 17, &output));
|
||||
assert(output.count == 1 && output.magnitude[0] == boundaries[i].expected);
|
||||
}
|
||||
|
||||
// Three distinguishable samples retain wire order; a shorter count ignores
|
||||
// stale later samples. Both callback envelopes accept minimal/compact/USB sizes.
|
||||
static const uint8_t ordered[16] = {
|
||||
0x70,
|
||||
0x00, 0xfc, 0x0f, 0x00, 0x00, // amplitudes 1023, 0 -> 255
|
||||
0x00, 0x00, 0x00, 0xc0, 0x3f, // amplitudes 0, 255 -> 64
|
||||
0xff, 0xff, 0xf7, 0x3f, 0x80, // amplitudes 511, 512 -> 128
|
||||
};
|
||||
static const size_t lengths[] = {17, 42, 64};
|
||||
memcpy(wire + 1, ordered, sizeof(ordered));
|
||||
for (unsigned count = 1; count <= 3; ++count) {
|
||||
for (unsigned sequence = 0; sequence < 16; ++sequence) {
|
||||
wire[1] = (uint8_t)(0x40u | (count << 4) | sequence);
|
||||
for (unsigned i = 0; i < sizeof(lengths) / sizeof(lengths[0]); ++i) {
|
||||
for (unsigned form = 0; form < 2; ++form) {
|
||||
assert(probe_protocol_decode_rumble((uint8_t)form, wire + form,
|
||||
lengths[i] - form, &output));
|
||||
assert(output.count == count && output.magnitude[0] == 255);
|
||||
if (count >= 2) assert(output.magnitude[1] == 64);
|
||||
if (count == 3) assert(output.magnitude[2] == 128);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wire[1] = 0x4f; // HOLD: nonzero stale samples must not become a stop/update.
|
||||
assert(probe_protocol_decode_rumble(0, wire, 17, &output));
|
||||
assert(output.count == 0);
|
||||
assert(probe_protocol_decode_rumble(1, wire + 1, 16, &output));
|
||||
assert(output.count == 0);
|
||||
|
||||
// Complete 16-byte block required even for HOLD or a one-sample update.
|
||||
for (unsigned count = 0; count <= 3; ++count) {
|
||||
wire[1] = (uint8_t)(0x40u | (count << 4));
|
||||
for (size_t length = 0; length < 17; ++length)
|
||||
expect_invalid_rumble(0, wire, length);
|
||||
for (size_t length = 0; length < 16; ++length)
|
||||
expect_invalid_rumble(1, wire + 1, length);
|
||||
}
|
||||
expect_invalid_rumble(0, wire, 65);
|
||||
expect_invalid_rumble(1, wire + 1, 64);
|
||||
expect_invalid_rumble(0, wire, SIZE_MAX);
|
||||
expect_invalid_rumble(1, wire + 1, SIZE_MAX);
|
||||
expect_invalid_rumble(0, NULL, 64);
|
||||
expect_invalid_rumble(1, NULL, 63);
|
||||
assert(!probe_protocol_decode_rumble(0, wire, 64, NULL));
|
||||
assert(!probe_protocol_decode_rumble(1, wire + 1, 63, NULL));
|
||||
for (unsigned id = 0; id <= UINT8_MAX; ++id) {
|
||||
if (id != 1) {
|
||||
wire[0] = (uint8_t)id;
|
||||
expect_invalid_rumble(0, wire, 64);
|
||||
}
|
||||
if (id > 1) expect_invalid_rumble((uint8_t)id, wire + 1, 63);
|
||||
}
|
||||
wire[0] = 1;
|
||||
for (unsigned header = 0; header <= UINT8_MAX; ++header) {
|
||||
if ((header & 0xc0u) == 0x40u) continue;
|
||||
wire[1] = (uint8_t)header;
|
||||
expect_invalid_rumble(0, wire, 64);
|
||||
expect_invalid_rumble(1, wire + 1, 63);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_native_rumble();
|
||||
test_indexed_memory();
|
||||
test_descriptors();
|
||||
for (unsigned side = 0; side < 2; ++side) {
|
||||
|
|
|
|||
|
|
@ -53,13 +53,37 @@ def test_native_gamepad_backend_native(
|
|||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
scenarios = ["stable-logical-slot", "cue-lifetime", "cue-races"]
|
||||
scenarios = [
|
||||
"stable-logical-slot",
|
||||
"cue-lifetime",
|
||||
"cue-races",
|
||||
"gameplay-timeline",
|
||||
"gameplay-availability",
|
||||
"gameplay-priority",
|
||||
"gameplay-source-epochs",
|
||||
"gameplay-profile-gain",
|
||||
]
|
||||
if controller_count == 2:
|
||||
scenarios.append("source-isolation")
|
||||
else:
|
||||
scenarios.extend(("two-pair-sources", "two-pair-cues", "explicit-precedence"))
|
||||
scenarios.extend(
|
||||
(
|
||||
"two-pair-sources",
|
||||
"two-pair-cues",
|
||||
"explicit-precedence",
|
||||
"gameplay-two-pairs",
|
||||
)
|
||||
)
|
||||
if source == "GAMEPAD":
|
||||
scenarios.extend(("paired-source", "pair-cue-races", "mono-rumble"))
|
||||
scenarios.extend(
|
||||
(
|
||||
"paired-source",
|
||||
"pair-cue-races",
|
||||
"mono-rumble",
|
||||
"gameplay-paired-revision",
|
||||
"gameplay-wii",
|
||||
)
|
||||
)
|
||||
if controller_count == 2:
|
||||
scenarios.extend(("sensorless-admission", "independent-motion"))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -93,3 +93,43 @@ def test_native_hub_management_native(
|
|||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(router_executable)], check=True, cwd=root)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("controller_count", [2, 4], ids=["one-pair", "two-pair"])
|
||||
@pytest.mark.parametrize("trace_enabled", [False, True], ids=["plain", "trace"])
|
||||
def test_native_hub_cold_startup(
|
||||
tmp_path: Path, controller_count: int, trace_enabled: bool
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
cc = shutil.which("cc") or shutil.which("gcc")
|
||||
assert cc is not None, "a host C compiler is required"
|
||||
executable = tmp_path / "native_hub_startup_test"
|
||||
flags = [f"-DPROBE_CONTROLLER_COUNT={controller_count}"]
|
||||
if trace_enabled:
|
||||
flags.append("-DSWITCH2_PROBE_TRACE_NATIVE_INPUT=1")
|
||||
subprocess.run(
|
||||
[
|
||||
cc,
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-DSWITCH2_PROBE_HUB=1",
|
||||
*flags,
|
||||
f"-I{root / 'tests' / 'native_hub_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
f"-I{root / 'tools' / 'pico_usb_address_probe'}",
|
||||
f"-I{root / 'tools' / 'switch2_usb_probe'}",
|
||||
str(root / "tests" / "native_hub_startup_test.c"),
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
for scenario in ("ready", "delayed", "timeout"):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue