feat(native-usb): add 32-frame HD gameplay rumble and USB flight diagnostics
Preserve native frequency/amplitude timelines for DualSense PCM output, remove the unsupported 64-frame path, and retain compatibility rumble for other controllers. Use the validated 300 MHz sampling phase and restrict non-bondable Classic discovery autoconnect to previously paired peers. Buffer native-hub UART stdout and release USB IRQs around port-reset callbacks. Add observer liveness and pre-SETUP root-response observations without changing recovery behavior. Cover transport and haptics boundaries. Record the user-accepted 0.108 trial: controls remained responsive and rumble felt fine. Instrumentation changes timing; the disconnect root cause and long-term reliability remain unqualified. Validation: 624 tests, 11 affected firmware/probe builds, and on-device concurrent USB and HD-auto-start checks. Private captures, generated images, and unrelated working-tree files are intentionally excluded.
This commit is contained in:
parent
ae1ca51b9a
commit
80b788099b
46 changed files with 2873 additions and 412 deletions
|
|
@ -34,6 +34,21 @@ SwitchHapticsFrame one_side(unsigned side, SwitchHapticsSample sample = state())
|
|||
return frame;
|
||||
}
|
||||
|
||||
NativeHapticsSample native_state(uint16_t low_code = 385, uint16_t low = 1023,
|
||||
uint16_t high_code = 481, uint16_t high = 0) {
|
||||
return {low_code, high_code, low, high};
|
||||
}
|
||||
|
||||
NativeHapticsFrame native_side(unsigned side, NativeHapticsSample sample = native_state()) {
|
||||
NativeHapticsFrame frame{};
|
||||
frame.actuators[side] = {1, {sample}};
|
||||
return frame;
|
||||
}
|
||||
|
||||
uint16_t native_q15(uint16_t amplitude) {
|
||||
return static_cast<uint16_t>((uint32_t{amplitude} * 32768 + 511) / 1023);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> render(SwitchHdRumbleSynth& synth, uint64_t first,
|
||||
uint32_t frames) {
|
||||
std::vector<uint8_t> pcm(static_cast<size_t>(frames) * 2, 0xcc);
|
||||
|
|
@ -556,6 +571,194 @@ void test_duplicate_order_and_invalid_frames() {
|
|||
}, "duplicate timestamp last-wins without phase reset or malformed-state mutation");
|
||||
}
|
||||
|
||||
void test_native_precision_and_bands() {
|
||||
for (uint16_t code : {1, 193, 385, 481, 482, 483, 670}) {
|
||||
for (unsigned band = 0; band < 2; ++band) {
|
||||
SwitchHdRumbleSynth synth;
|
||||
synth.reset(0);
|
||||
const unsigned side = band;
|
||||
const auto frame = native_side(side, native_state(code, band ? 0 : 1023,
|
||||
code, band ? 1023 : 0));
|
||||
std::vector<uint8_t> pcm(12000);
|
||||
for (unsigned first = 0; first < 6000; first += 60) {
|
||||
expect(synth.push_native(frame, first * 1000 / 3),
|
||||
"native periodic refresh accepted");
|
||||
synth.render(first, 60, pcm.data() + first * 2);
|
||||
}
|
||||
const double hz = 10 * std::exp2((code - 1) / 96.0);
|
||||
expect_wave(pcm, side, [hz](size_t n) { return wave(hz * n / 3000); },
|
||||
"native 96-step frequency retains wire precision");
|
||||
expect_wave(pcm, 1 - side, [](size_t) { return 0; },
|
||||
"native bands stay on their physical actuator");
|
||||
expect(spectral_amplitude(pcm, side, hz) > 125,
|
||||
"native PCM has its expected physical spectral peak");
|
||||
if (code >= 481 && code <= 483) {
|
||||
const double adjacent = 10 * std::exp2(code / 96.0);
|
||||
expect(spectral_amplitude(pcm, side, adjacent) < 15,
|
||||
"adjacent native codes are spectrally distinct, not rounded to Switch indices");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwitchHdRumbleSynth synth;
|
||||
synth.reset(0);
|
||||
synth.push_native(native_side(0, native_state(385, 682, 481, 341)), 0);
|
||||
expect_wave(render(synth, 0, 150), 0, [](size_t n) {
|
||||
return 127.0 * (2 * std::sin(kTau * 160 * n / 3000) +
|
||||
std::sin(kTau * 320 * n / 3000)) / 3;
|
||||
}, "native joint gain preserves independent band mixture");
|
||||
|
||||
std::vector<uint8_t> previous;
|
||||
for (uint16_t amplitude : {128, 129}) {
|
||||
synth.reset(0);
|
||||
synth.push_native(native_side(0, native_state(385, amplitude)), 0);
|
||||
auto pcm = render(synth, 0, 150);
|
||||
expect_wave(pcm, 0, [amplitude](size_t n) {
|
||||
return wave(160.0 * n / 3000, native_q15(amplitude));
|
||||
}, "native amplitude normalizes all ten bits before existing gain");
|
||||
if (!previous.empty()) expect(previous != pcm, "adjacent ten-bit amplitudes remain distinguishable");
|
||||
previous = pcm;
|
||||
}
|
||||
}
|
||||
|
||||
void test_native_windows_watchdogs_and_legacy() {
|
||||
SwitchHdRumbleSynth synth;
|
||||
synth.reset(0);
|
||||
NativeHapticsFrame frame{};
|
||||
frame.actuators[0] = {3, {native_state(), native_state(385, 0), native_state(385, 512)}};
|
||||
frame.actuators[1] = {2, {native_state(385, 0), native_state(385, 0, 481, 1023)}};
|
||||
synth.push_native(frame, 0);
|
||||
synth.push_native(native_side(0, native_state(385, 512)), 20000);
|
||||
synth.push_native(NativeHapticsFrame{}, 40000);
|
||||
const auto pcm = render(synth, 0, 230);
|
||||
expect_wave(pcm, 0, [](size_t n) {
|
||||
return n < 210 ? wave(160.0 * n / 3000,
|
||||
n < 16 ? 32768 : n < 32 ? 0 : native_q15(512)) : 0;
|
||||
}, "native samples use fixed 16-frame spacing with an independent refreshed watchdog");
|
||||
expect_wave(pcm, 1, [](size_t n) {
|
||||
return n >= 16 && n < 150 ? wave(320.0 * n / 3000) : 0;
|
||||
}, "native two-sample update uses 16-frame spacing and untouched side expires at 50 ms");
|
||||
|
||||
synth.reset(0);
|
||||
synth.push_native(frame, 0);
|
||||
auto legacy = one_side(0);
|
||||
legacy.actuators[0] = {3, {state(), state(64, 0), state(64, 16384)}};
|
||||
synth.push(legacy, 0);
|
||||
const auto mixed = render(synth, 0, 40);
|
||||
expect_wave(mixed, 0, [](size_t n) {
|
||||
return wave(160.0 * n / 3000, n < 8 ? 32768 : n < 16 ? 0 : 16384);
|
||||
}, "legacy replacement uses its own 8 ms window in a native stream");
|
||||
expect_wave(mixed, 1, [](size_t n) { return n < 16 ? 0 : wave(320.0 * n / 3000); },
|
||||
"legacy partial update does not shorten the other native window");
|
||||
expect(!synth.push_native(frame, UINT64_MAX),
|
||||
"native and legacy updates share host timestamp ordering");
|
||||
|
||||
synth.reset(0);
|
||||
synth.push_rumble(255, 255, 0);
|
||||
synth.push_native(native_side(0, native_state(385, 0)), 1000);
|
||||
auto held = render(synth, 0, 300);
|
||||
expect_wave(held, 0, [](size_t n) { return n < 3 ? wave(160.0 * n / 3000) : 0; },
|
||||
"native zero stops the targeted persistent motor");
|
||||
expect_wave(held, 1, [](size_t n) { return wave(320.0 * n / 3000); },
|
||||
"native zero-count side preserves stateful XInput output");
|
||||
}
|
||||
|
||||
void test_native_late_overflow_and_cancellation() {
|
||||
NativeHapticsFrame steps{};
|
||||
steps.actuators[0] = {3, {native_state(), native_state(385, 512), native_state(385, 256)}};
|
||||
SwitchHdRumbleSynth synth;
|
||||
synth.reset(10000);
|
||||
expect(synth.push_native(steps, 4000), "recent pre-epoch native update accepted");
|
||||
expect_wave(render(synth, 0, 150), 0, [](size_t n) {
|
||||
return n < 132 ? wave(160.0 * n / 3000, native_q15(n < 14 ? 512 : 256)) : 0;
|
||||
}, "pre-epoch native update keeps original 16-frame sample positions and expiry");
|
||||
synth.reset(0);
|
||||
render(synth, 0, 40);
|
||||
synth.push_native(steps, 0);
|
||||
expect_wave(render(synth, 40, 130), 0, [](size_t n) {
|
||||
return n + 40 < 150 ? wave(160.0 * (n + 40) / 3000, native_q15(256)) : 0;
|
||||
}, "late native update skips elapsed substeps without refreshing expiry");
|
||||
synth.reset(100000);
|
||||
expect(!synth.push_native(steps, 50000), "expired native pre-epoch command rejected");
|
||||
|
||||
SwitchHdRumbleSynth reference;
|
||||
synth.reset(0);
|
||||
reference.reset(0);
|
||||
for (unsigned n = 0; n < 40; ++n) {
|
||||
const auto frame = native_side(n % 2, native_state(static_cast<uint16_t>(385 + n % 5)));
|
||||
synth.push_native(frame, n * 1000);
|
||||
reference.push_native(frame, n * 1000);
|
||||
render(reference, n * 3, 3);
|
||||
}
|
||||
expect(synth.dropped_updates() > 0, "native bounded timeline accounts for overflow");
|
||||
expect_silent(render(synth, 0, 30), "native overflow never replays discarded history");
|
||||
expect(render(synth, 120, 120) == render(reference, 120, 120),
|
||||
"native overflow preserves partial sides and full-precision accumulated phases");
|
||||
|
||||
synth.reset(0);
|
||||
auto both = native_side(0);
|
||||
both.actuators[1] = both.actuators[0];
|
||||
synth.push_native(both, 0);
|
||||
render(synth, 0, 15);
|
||||
synth.push_native(both, 10000); // Queued update must also be canceled.
|
||||
synth.feedback(5000, 5000, 0, 255);
|
||||
synth.cancel_native(1);
|
||||
const auto canceled = render(synth, 15, 90);
|
||||
expect_wave(canceled, 0, [](size_t n) {
|
||||
return n < 15 ? feedback_wave(320.0 * (n + 15) / 3000) : 0;
|
||||
}, "native cancel preserves overlay but removes live and queued left host work");
|
||||
expect_wave(canceled, 1, [](size_t n) {
|
||||
return n < 15 ? feedback_wave(320.0 * (n + 15) / 3000) :
|
||||
wave(160.0 * (n + 15) / 3000);
|
||||
}, "native cancellation leaves the other side and its queued updates intact");
|
||||
synth.push_native(native_side(0), 35000);
|
||||
expect_wave(render(synth, 105, 30), 0, [](size_t n) {
|
||||
return wave(160.0 * (n + 105) / 3000);
|
||||
}, "fresh native command after cancellation resumes without oscillator reset");
|
||||
synth.cancel_native(3);
|
||||
expect_silent(render(synth, 135, 120), "both-side cancellation is a lasting stop");
|
||||
synth.push_native(both, 100000);
|
||||
synth.reset(100000);
|
||||
expect_silent(render(synth, 0, 150), "stream reset discards pending native work");
|
||||
}
|
||||
|
||||
void test_native_validation_is_atomic() {
|
||||
SwitchHdRumbleSynth synth;
|
||||
synth.reset(0);
|
||||
synth.push_native(native_side(0), 0);
|
||||
auto invalid = native_side(0, native_state(385, 0));
|
||||
invalid.actuators[1] = {1, {native_state(671)}};
|
||||
expect(!synth.push_native(invalid, 2000), "unmeasured active native frequency rejects whole frame");
|
||||
invalid.actuators[1] = {1, {native_state(0)}};
|
||||
expect(!synth.push_native(invalid, 2000), "active code zero cannot generate DC");
|
||||
invalid.actuators[1] = {1, {native_state(385, 1024)}};
|
||||
expect(!synth.push_native(invalid, 2000), "native amplitude overflow rejected");
|
||||
invalid.actuators[1].sample_count = 4;
|
||||
expect(!synth.push_native(invalid, 2000), "native count overflow rejected");
|
||||
expect(synth.push_native(native_side(1, native_state(0, 0, 1023, 0)), 1000),
|
||||
"invalid frames do not advance timestamp ordering; silent bands accept wire range");
|
||||
const auto pcm = render(synth, 0, 120);
|
||||
expect_wave(pcm, 0, [](size_t n) { return wave(160.0 * n / 3000); },
|
||||
"malformed right side cannot partially stop left host state");
|
||||
expect_wave(pcm, 1, [](size_t) { return 0; }, "silent arbitrary codes never produce DC");
|
||||
}
|
||||
|
||||
void test_native_side_feedback_preserves_host() {
|
||||
SwitchHdRumbleSynth synth;
|
||||
synth.reset(0);
|
||||
auto both = native_side(0);
|
||||
both.actuators[1] = both.actuators[0];
|
||||
synth.push_native(both, 0);
|
||||
synth.feedback_native(5000, 10000, 0, 255);
|
||||
const auto pcm = render(synth, 0, 90);
|
||||
expect_wave(pcm, 0, [](size_t n) { return wave(160.0 * n / 3000); },
|
||||
"right native cue does not mute or attenuate the untouched left host");
|
||||
expect_wave(pcm, 1, [](size_t n) {
|
||||
return n >= 15 && n < 45 ? feedback_wave(320.0 * n / 3000) :
|
||||
wave(160.0 * n / 3000);
|
||||
}, "native cue overlays only the requested side and resumes live host on expiry");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
|
|
@ -573,6 +776,11 @@ int main() {
|
|||
test_stateful_rumble_hd_order_and_watchdogs();
|
||||
test_stateful_rumble_feedback_resume();
|
||||
test_stateful_rumble_overflow_and_reset();
|
||||
test_native_precision_and_bands();
|
||||
test_native_windows_watchdogs_and_legacy();
|
||||
test_native_late_overflow_and_cancellation();
|
||||
test_native_validation_is_atomic();
|
||||
test_native_side_feedback_preserves_host();
|
||||
if (failures) {
|
||||
std::cerr << failures << " synthesis scenarios failed\n";
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue