switch-pico/tests/xinput_feasibility_test.cpp

211 lines
8.7 KiB
C++

#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iostream>
#include "adapter_host_probe_state.h"
#include "xinput_feasibility_descriptors.h"
#include "xinput_feasibility_protocol.h"
namespace {
int failures = 0;
void expect(bool condition, const char *message) {
if (!condition) {
std::cerr << "FAIL: " << message << '\n';
++failures;
}
}
uint16_t read_le16(const uint8_t *data) {
return static_cast<uint16_t>(data[0] | (data[1] << 8));
}
uint32_t read_le32(const uint8_t *data) {
return static_cast<uint32_t>(data[0]) |
(static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) |
(static_cast<uint32_t>(data[3]) << 24);
}
void test_device_and_configuration_descriptors() {
using namespace XInputFeasibility;
expect(read_le16(&kSwitchProbeDeviceDescriptor[8]) ==
kSwitchProbeVendorId,
"Switch probe VID mismatch");
expect(read_le16(&kSwitchProbeDeviceDescriptor[10]) ==
kSwitchProbeProductId,
"Switch probe PID mismatch");
expect(read_le16(&kSwitchProbeDeviceDescriptor[12]) ==
kSwitchProbeDeviceRevision,
"Switch probe revision mismatch");
expect(kSwitchProbeDeviceRevision != 0x0210,
"Switch probe reuses the genuine controller cache identity");
expect(read_le16(&kDeviceDescriptor[8]) == kPrototypeVendorId,
"prototype VID mismatch");
expect(read_le16(&kDeviceDescriptor[10]) == kPrototypeProductId,
"prototype PID mismatch");
expect(read_le16(&kDeviceDescriptor[12]) ==
kPrototypeDeviceRevision,
"prototype revision mismatch");
expect(kDeviceDescriptor[4] == 0 && kDeviceDescriptor[5] == 0 &&
kDeviceDescriptor[6] == 0,
"multi-interface prototype is not a composite USB device");
expect(kPrototypeVendorId != 0x045e,
"prototype must not impersonate Microsoft's VID");
expect(read_le16(&kConfigurationDescriptor[2]) ==
sizeof(kConfigurationDescriptor),
"configuration total length mismatch");
expect(kConfigurationDescriptor[4] == SWITCH_PICO_HID_INSTANCE_COUNT,
"configuration interface count mismatch");
std::array<bool, 16> endpoints{};
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
++instance) {
const size_t offset = 9 + instance * kInterfaceDescriptorSize;
const uint8_t *interface = &kConfigurationDescriptor[offset];
expect(interface[0] == 9 && interface[1] == 4,
"missing interface descriptor");
expect(interface[2] == instance, "interface number mismatch");
expect(interface[5] == 0xff && interface[6] == 0x5d &&
interface[7] == 0x01,
"XInput interface class tuple mismatch");
expect(interface[9] == 0x10 && interface[10] == 0x21,
"XInput capability descriptor missing");
const uint8_t in_endpoint = interface[27];
const uint8_t out_endpoint = interface[34];
expect(in_endpoint == static_cast<uint8_t>(0x81 + instance),
"input endpoint mismatch");
expect(out_endpoint == static_cast<uint8_t>(0x01 + instance),
"output endpoint mismatch");
expect(interface[15] == in_endpoint && interface[21] == out_endpoint,
"capability descriptor endpoint mismatch");
expect(!endpoints[in_endpoint & 0x0f] &&
!endpoints[out_endpoint & 0x0f],
"endpoint number reused");
endpoints[in_endpoint & 0x0f] = true;
}
}
void test_microsoft_compatible_id_descriptor() {
using namespace XInputFeasibility;
expect(read_le32(kMsCompatIdDescriptor) == sizeof(kMsCompatIdDescriptor),
"Microsoft descriptor total length mismatch");
expect(read_le16(&kMsCompatIdDescriptor[4]) == 0x0100,
"Microsoft descriptor version mismatch");
expect(read_le16(&kMsCompatIdDescriptor[6]) == kMsCompatIdIndex,
"Microsoft descriptor index mismatch");
expect(kMsCompatIdDescriptor[8] == SWITCH_PICO_HID_INSTANCE_COUNT,
"Microsoft function count mismatch");
for (uint8_t instance = 0;
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
expect(function[0] == instance,
"Microsoft descriptor interface mismatch");
expect(std::memcmp(&function[2], "XUSB10", 6) == 0,
"XUSB10 compatible ID missing");
}
expect(read_le32(kProbeMsCompatIdDescriptor) == 16 &&
kProbeMsCompatIdDescriptor[8] == 0,
"probe descriptor must expose no compatible functions");
}
void test_input_report_mapping() {
ControllerState state{};
auto report = XInputFeasibility::build_input_report(state);
expect(report.report_id == 0 && report.report_size == 20,
"neutral report header mismatch");
expect(report.buttons == 0 && report.left_trigger == 0 &&
report.right_trigger == 0,
"neutral report controls mismatch");
expect(report.left_x == 0 && report.left_y == 0 && report.right_x == 0 &&
report.right_y == 0,
"neutral axes mismatch");
state.dpad_up = true;
state.button_south = true;
state.button_east = true;
state.button_west = true;
state.button_north = true;
state.button_start = true;
state.button_select = true;
state.button_system = true;
state.left_trigger = UINT16_MAX;
state.right_trigger = UINT16_MAX;
state.left_stick_x = INT16_MIN;
state.left_stick_y = INT16_MIN;
state.right_stick_x = INT16_MAX;
state.right_stick_y = INT16_MAX;
report = XInputFeasibility::build_input_report(state);
expect((report.buttons & XInputFeasibility::kDpadUp) != 0,
"D-pad mapping missing");
expect((report.buttons & XInputFeasibility::kButtonA) != 0 &&
(report.buttons & XInputFeasibility::kButtonB) != 0 &&
(report.buttons & XInputFeasibility::kButtonX) != 0 &&
(report.buttons & XInputFeasibility::kButtonY) != 0,
"positional face-button mapping mismatch");
expect(report.left_trigger == 0xff && report.right_trigger == 0xff,
"full analog trigger mapping mismatch");
expect(report.left_x == INT16_MIN && report.left_y == INT16_MAX &&
report.right_x == INT16_MAX &&
report.right_y == -INT16_MAX,
"axis endpoint mapping mismatch");
state.left_trigger = 0x8000;
state.right_trigger = 0x7fff;
report = XInputFeasibility::build_input_report(state);
expect(report.left_trigger == 0x80 && report.right_trigger == 0x7f,
"analog trigger precision was discarded");
}
void test_rumble_report() {
const uint8_t packet[8] = {0x00, 0x08, 0x00, 0xa5, 0x5a, 0x00, 0x00, 0x00};
ControllerRumbleOutput output{};
expect(
XInputFeasibility::parse_rumble_report(packet, sizeof(packet), &output),
"valid rumble report rejected");
expect(output.low_frequency_magnitude == 0xa5 &&
output.high_frequency_magnitude == 0x5a,
"rumble magnitudes mapped incorrectly");
expect(!XInputFeasibility::parse_rumble_report(packet, 4, &output),
"truncated rumble report accepted");
uint8_t wrong_type[8]{};
expect(!XInputFeasibility::parse_rumble_report(wrong_type,
sizeof(wrong_type), &output),
"wrong rumble report type accepted");
}
void test_host_probe_sequence() {
AdapterHostProbeState state;
state.note_ms_compat_id_request(10);
expect(!state.windows_confirmed(),
"compatible-ID request without signature confirmed Windows");
state.note_ms_os_string();
expect(state.saw_ms_os_string(),
"Microsoft OS string observation was not retained");
state.note_ms_compat_id_request(20);
expect(state.windows_confirmed(),
"two-stage Windows signature not confirmed");
expect(!state.should_reboot(119), "probe rebooted before delay");
expect(state.should_reboot(120), "probe did not reboot at deadline");
AdapterHostProbeState wrapped;
wrapped.note_ms_os_string();
wrapped.note_ms_compat_id_request(UINT32_MAX - 50);
expect(!wrapped.should_reboot(48),
"wrapped timer rebooted before deadline");
expect(wrapped.should_reboot(49), "wrapped timer missed deadline");
}
} // namespace
int main() {
test_device_and_configuration_descriptors();
test_microsoft_compatible_id_descriptor();
test_input_report_mapping();
test_rumble_report();
test_host_probe_sequence();
return failures == 0 ? 0 : 1;
}