Preserve analog triggers in generic HID modes

This commit is contained in:
Joey Yakimowich-Payne 2026-09-03 11:51:30 -06:00
commit ecd8ca2b40
9 changed files with 252 additions and 130 deletions

View file

@ -2063,9 +2063,26 @@ void test_motion_hotkey() {
void test_protocol_neutral_analog_state() {
start_pairing_backend();
uni_hid_device_t controller = device(0);
uni_hid_device_t peer = device(1);
platform_on_device_connected(&controller);
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
"analog-state controller did not become ready");
platform_on_device_connected(&peer);
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS &&
platform_on_device_ready(&peer) == UNI_ERROR_SUCCESS,
"analog-state controllers did not become ready");
uni_controller_t peer_input{};
peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
peer_input.gamepad.brake = 256;
peer_input.gamepad.throttle = 768;
peer_input.gamepad.buttons =
BUTTON_TRIGGER_L | BUTTON_TRIGGER_R;
platform_on_controller_data(&peer, &peer_input);
ControllerState peer_state{};
require(read_controller_state(1, &peer_state) &&
peer_state.left_trigger == 16399 &&
peer_state.right_trigger == 49199,
"peer analog trigger state was not published exactly");
uni_controller_t input{};
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
@ -2073,33 +2090,69 @@ void test_protocol_neutral_analog_state() {
input.gamepad.axis_y = 0;
input.gamepad.axis_rx = 511;
input.gamepad.axis_ry = -256;
input.gamepad.brake = 1;
input.gamepad.throttle = 512;
platform_on_controller_data(&controller, &input);
input.gamepad.buttons =
BUTTON_TRIGGER_L | BUTTON_TRIGGER_R;
struct TriggerCase {
int32_t brake;
int32_t throttle;
uint16_t expected_left;
uint16_t expected_right;
};
constexpr TriggerCase trigger_cases[] = {
{4, 512, 256, 32799},
{512, 1020, 32799, UINT16_MAX},
{1020, 1016, UINT16_MAX, 65086},
{1016, 1023, 65086, UINT16_MAX},
{1023, 4, UINT16_MAX, 256},
};
ControllerState state{};
require(read_controller_state(0, &state),
"analog state was not published");
for (const TriggerCase& trigger_case : trigger_cases) {
input.gamepad.brake = trigger_case.brake;
input.gamepad.throttle = trigger_case.throttle;
platform_on_controller_data(&controller, &input);
require(read_controller_state(0, &state) &&
state.left_trigger == trigger_case.expected_left &&
state.right_trigger == trigger_case.expected_right,
"digital trigger bits flattened analog trigger precision");
require(read_controller_state(1, &peer_state) &&
peer_state.left_trigger == 16399 &&
peer_state.right_trigger == 49199,
"slot 0 trigger update changed slot 1");
}
require(state.left_stick_x == INT16_MIN &&
state.left_stick_y == 0 &&
state.right_stick_x == INT16_MAX &&
state.right_stick_y == -16384,
"stick axes were not normalized to signed full range");
require(state.left_trigger == scale_trigger(1) &&
state.left_trigger > 0 &&
state.right_trigger == scale_trigger(512) &&
state.right_trigger < UINT16_MAX,
"analog trigger precision was discarded");
input.gamepad.brake = 0;
input.gamepad.throttle = 0;
input.gamepad.buttons =
BUTTON_TRIGGER_L | BUTTON_TRIGGER_R;
input.gamepad.buttons = BUTTON_TRIGGER_L;
platform_on_controller_data(&controller, &input);
require(read_controller_state(0, &state) &&
state.left_trigger == UINT16_MAX &&
state.right_trigger == 0,
"left digital-only trigger did not map independently");
input.gamepad.buttons = BUTTON_TRIGGER_R;
platform_on_controller_data(&controller, &input);
require(read_controller_state(0, &state) &&
state.left_trigger == 0 &&
state.right_trigger == UINT16_MAX,
"digital trigger buttons did not map to full analog range");
"right digital-only trigger did not map independently");
input.gamepad.buttons = 0;
platform_on_controller_data(&controller, &input);
require(read_controller_state(0, &state) &&
state.left_trigger == 0 &&
state.right_trigger == 0,
"released triggers did not return to zero");
require(read_controller_state(1, &peer_state) &&
peer_state.left_trigger == 16399 &&
peer_state.right_trigger == 49199,
"slot 0 digital trigger fallback changed slot 1");
}
void test_host_rumble_mode_duration() {

View file

@ -12,11 +12,13 @@
#endif
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == EXPECTED_HID_INSTANCE_COUNT);
static_assert(sizeof(GenericHid::kConfigurationDescriptor) ==
static_assert(sizeof(GenericHid::kDInputConfigurationDescriptor) ==
9u + 25u * EXPECTED_HID_INSTANCE_COUNT);
static_assert(sizeof(GenericHid::kMacConfigurationDescriptor) ==
9u + 25u * EXPECTED_HID_INSTANCE_COUNT);
static_assert(sizeof(GenericHid::InputReport) == 15);
static_assert(sizeof(GenericHid::kDInputReportDescriptor) ==
sizeof(GenericHid::kMacReportDescriptor));
static_assert(sizeof(GenericHid::kMacReportDescriptor) ==
sizeof(GenericHid::kDInputReportDescriptor) + 4u);
namespace {
@ -64,6 +66,55 @@ constexpr std::array<ItemGolden, 43> kDInputReportItemGolden{{
{0, 12, 0, 0x00},
}};
bool expected_report_item(size_t decoded, bool mac_variant,
ItemGolden* expected) {
if (!mac_variant) {
if (decoded >= kDInputReportItemGolden.size()) {
return false;
}
*expected = kDInputReportItemGolden[decoded];
return true;
}
if (decoded < 16) {
if (decoded >= kDInputReportItemGolden.size()) {
return false;
}
*expected = kDInputReportItemGolden[decoded];
if (decoded == 10) {
expected->value = 0x32;
} else if (decoded == 11) {
expected->value = 0x33;
}
return true;
}
if (decoded == 16) {
*expected = {1, 0, 1, 0x02};
return true;
}
if (decoded == 17) {
*expected = {2, 0, 1, 0xc5};
return true;
}
if (decoded == 18) {
*expected = {2, 0, 1, 0xc4};
return true;
}
if (decoded == 19) {
*expected = kDInputReportItemGolden[18];
return true;
}
if (decoded == 20) {
*expected = {1, 0, 1, 0x01};
return true;
}
if (decoded - 2u >= kDInputReportItemGolden.size()) {
return false;
}
*expected = kDInputReportItemGolden[decoded - 2u];
return true;
}
int32_t sign_extend(uint32_t value, uint8_t size) {
if (size == 4 || size == 0) {
return static_cast<int32_t>(value);
@ -111,7 +162,7 @@ void inspect_report_descriptor(const uint8_t* descriptor,
mac_variant ? std::array<uint32_t, 4>{{0x30, 0x31, 0x32, 0x33}}
: std::array<uint32_t, 4>{{0x30, 0x31, 0x33, 0x34}};
const std::array<uint32_t, 2> expected_trigger_usages =
mac_variant ? std::array<uint32_t, 2>{{0x34, 0x35}}
mac_variant ? std::array<uint32_t, 2>{{0xc5, 0xc4}}
: std::array<uint32_t, 2>{{0x32, 0x35}};
size_t offset = 0;
size_t decoded = 0;
@ -149,24 +200,13 @@ void inspect_report_descriptor(const uint8_t* descriptor,
const uint8_t type = static_cast<uint8_t>((prefix >> 2u) & 0x03u);
const uint8_t tag = static_cast<uint8_t>(prefix >> 4u);
expect(decoded < kDInputReportItemGolden.size(),
"report descriptor contains an extra HID item");
if (decoded < kDInputReportItemGolden.size()) {
const ItemGolden& golden = kDInputReportItemGolden[decoded];
uint32_t expected_value = golden.value;
if (mac_variant) {
if (decoded == 10) {
expected_value = 0x32;
} else if (decoded == 11) {
expected_value = 0x33;
} else if (decoded == 16) {
expected_value = 0x34;
} else if (decoded == 17) {
expected_value = 0x35;
}
}
ItemGolden golden{};
const bool has_golden =
expected_report_item(decoded, mac_variant, &golden);
expect(has_golden, "report descriptor contains an extra HID item");
if (has_golden) {
expect(type == golden.type && tag == golden.tag &&
size == golden.size && value == expected_value,
size == golden.size && value == golden.value,
"decoded HID item differs from its mode golden");
}
++decoded;
@ -265,8 +305,9 @@ void inspect_report_descriptor(const uint8_t* descriptor,
locals.clear();
}
expect(offset == descriptor_size, "report descriptor was not fully decoded");
expect(decoded == kDInputReportItemGolden.size(),
const size_t expected_item_count =
kDInputReportItemGolden.size() + (mac_variant ? 2u : 0u);
expect(decoded == expected_item_count,
"report descriptor is missing a golden HID item");
expect(gamepad_application && collection_depth == 0,
"report is not one balanced Game Pad application collection");
@ -296,35 +337,22 @@ void inspect_report_descriptor(const uint8_t* descriptor,
const InputField& triggers = fields[1];
expect(triggers.bit_offset == 64 && triggers.size == 16 &&
triggers.count == 2 && triggers.flags == 0x02 &&
triggers.globals.usage_page == 1 &&
triggers.globals.usage_page == (mac_variant ? 2u : 1u) &&
triggers.globals.logical_minimum == 0 &&
triggers.globals.logical_maximum == 65535 &&
triggers.locals.usage_count ==
expected_trigger_usages.size() &&
triggers.locals.usages[0] == expected_trigger_usages[0] &&
triggers.locals.usages[1] == expected_trigger_usages[1],
mac_variant ? "unsigned Mac Ry/Rz trigger field layout is wrong"
: "unsigned DInput Z/Rz trigger field layout is wrong");
mac_variant
? "unsigned Mac Brake/Accelerator trigger field layout is wrong"
: "unsigned DInput Z/Rz trigger field layout is wrong");
if (mac_variant) {
const std::array<uint32_t, 6> usage_indices{{
sticks.locals.usages[0] - 0x30u,
sticks.locals.usages[1] - 0x30u,
sticks.locals.usages[2] - 0x30u,
sticks.locals.usages[3] - 0x30u,
triggers.locals.usages[0] - 0x30u,
triggers.locals.usages[1] - 0x30u,
}};
constexpr std::array<uint32_t, 6> kSemanticAxisGolden{{
0, 1, 2, 3, 4, 5,
}};
expect(usage_indices == kSemanticAxisGolden,
"Mac physical fields do not map to Chromium axes 0..5");
expect((sticks.bit_offset + 3u * sticks.size) / 8u == 6u &&
usage_indices[3] == 3u &&
sticks.locals.usages[3] == 0x33u &&
triggers.bit_offset / 8u == 8u &&
usage_indices[4] == 4u,
"Mac field offset 6 must be Rx/axis3 and offset 8 must be "
"Ry/axis4; reversing them reproduces the hardware failure");
(triggers.bit_offset + triggers.size) / 8u == 10u,
"Mac X/Y/Z/Rx sticks and following trigger offsets changed");
}
const InputField& hat = fields[2];
@ -356,34 +384,6 @@ void inspect_report_descriptor(const uint8_t* descriptor,
buttons.locals.usage_maximum == 16,
"sequential Button 1..16 field layout is wrong");
}
void inspect_report_descriptor_parity() {
constexpr std::array<uint8_t, 3> kDInputDifferentBytes{{
0x33, 0x34, 0x32,
}};
constexpr std::array<uint8_t, 3> kMacDifferentBytes{{
0x32, 0x33, 0x34,
}};
size_t difference_count = 0;
for (size_t offset = 0;
offset < sizeof(GenericHid::kDInputReportDescriptor); ++offset) {
const uint8_t dinput =
GenericHid::kDInputReportDescriptor[offset];
const uint8_t mac = GenericHid::kMacReportDescriptor[offset];
if (dinput == mac) {
continue;
}
expect(difference_count < kDInputDifferentBytes.size(),
"report descriptors differ outside the three Mac axis usages");
if (difference_count < kDInputDifferentBytes.size()) {
expect(dinput == kDInputDifferentBytes[difference_count] &&
mac == kMacDifferentBytes[difference_count],
"report descriptor axis usage difference is wrong");
}
++difference_count;
}
expect(difference_count == kDInputDifferentBytes.size(),
"report descriptors do not differ at exactly three axis usages");
}
void inspect_device_descriptors_and_strings() {
@ -423,13 +423,13 @@ void inspect_device_descriptors_and_strings() {
"DInput and Mac identities do not have distinct strings");
}
void inspect_configuration_descriptor() {
void inspect_configuration_descriptor(
const uint8_t* descriptor, size_t descriptor_size,
size_t expected_report_descriptor_size) {
constexpr uint8_t kConfiguration = 0x02;
constexpr uint8_t kInterface = 0x04;
constexpr uint8_t kEndpoint = 0x05;
constexpr uint8_t kHid = 0x21;
const uint8_t* descriptor = GenericHid::kConfigurationDescriptor;
const size_t descriptor_size = sizeof(GenericHid::kConfigurationDescriptor);
expect(descriptor[0] == 9 && descriptor[1] == kConfiguration &&
read_u16(descriptor + 2) == descriptor_size &&
@ -474,9 +474,7 @@ void inspect_configuration_descriptor() {
const uint16_t report_descriptor_length =
read_u16(descriptor + offset + 7);
expect(report_descriptor_length ==
sizeof(GenericHid::kDInputReportDescriptor) &&
report_descriptor_length ==
sizeof(GenericHid::kMacReportDescriptor),
expected_report_descriptor_size,
"HID descriptor advertises the wrong report length");
if (current_interface >= 0) {
++hid_counts[static_cast<size_t>(current_interface)];
@ -523,6 +521,28 @@ void inspect_configuration_descriptor() {
}
}
void inspect_configuration_descriptor_parity() {
size_t difference_count = 0;
for (size_t offset = 0;
offset < sizeof(GenericHid::kDInputConfigurationDescriptor);
++offset) {
const uint8_t dinput =
GenericHid::kDInputConfigurationDescriptor[offset];
const uint8_t mac = GenericHid::kMacConfigurationDescriptor[offset];
if (dinput == mac) {
continue;
}
const size_t expected_offset = 25u + 25u * difference_count;
expect(offset == expected_offset &&
dinput == sizeof(GenericHid::kDInputReportDescriptor) &&
mac == sizeof(GenericHid::kMacReportDescriptor),
"configuration descriptors differ outside report lengths");
++difference_count;
}
expect(difference_count == EXPECTED_HID_INSTANCE_COUNT,
"configuration descriptors do not differ once per interface");
}
void inspect_report_encoding() {
ControllerState state{};
state.left_stick_x = INT16_MIN;
@ -622,9 +642,16 @@ int main() {
false);
inspect_report_descriptor(GenericHid::kMacReportDescriptor,
sizeof(GenericHid::kMacReportDescriptor), true);
inspect_report_descriptor_parity();
inspect_device_descriptors_and_strings();
inspect_configuration_descriptor();
inspect_configuration_descriptor(
GenericHid::kDInputConfigurationDescriptor,
sizeof(GenericHid::kDInputConfigurationDescriptor),
sizeof(GenericHid::kDInputReportDescriptor));
inspect_configuration_descriptor(
GenericHid::kMacConfigurationDescriptor,
sizeof(GenericHid::kMacConfigurationDescriptor),
sizeof(GenericHid::kMacReportDescriptor));
inspect_configuration_descriptor_parity();
inspect_report_encoding();
return failures == 0 ? 0 : 1;
}

View file

@ -560,9 +560,11 @@ void test_xinput_boundary_dispatch() {
void test_generic_boundary_dispatch(
AdapterUsbMode mode, const uint8_t* expected_device_descriptor,
const uint8_t* expected_configuration_descriptor,
const uint8_t* expected_report_descriptor,
const char* expected_driver_name, const char* expected_mode_name,
const char* expected_product, const char* expected_serial) {
size_t expected_report_descriptor_size, const char* expected_driver_name,
const char* expected_mode_name, const char* expected_product,
const char* expected_serial) {
reset_usb_harness();
usb_output_driver_init(mode);
@ -588,14 +590,14 @@ void test_generic_boundary_dispatch(
tud_descriptor_configuration_cb(0);
expect(configuration_descriptor != nullptr &&
std::memcmp(configuration_descriptor,
GenericHid::kConfigurationDescriptor,
expected_configuration_descriptor,
GenericHid::kConfigurationDescriptorSize) == 0,
"generic configuration descriptor was not selected");
const uint8_t* report_descriptor =
tud_hid_descriptor_report_cb(0);
expect(report_descriptor != nullptr &&
std::memcmp(report_descriptor, expected_report_descriptor,
sizeof(GenericHid::kDInputReportDescriptor)) == 0 &&
expected_report_descriptor_size) == 0 &&
tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr,
"generic report descriptor routing was incorrect");
@ -679,11 +681,15 @@ void test_generic_boundary_dispatch(
void test_generic_modes_boundary_dispatch() {
test_generic_boundary_dispatch(
AdapterUsbMode::kDInput, GenericHid::kDInputDeviceDescriptor,
GenericHid::kDInputReportDescriptor, "DINPUT", "DInput",
GenericHid::kDInputConfigurationDescriptor,
GenericHid::kDInputReportDescriptor,
sizeof(GenericHid::kDInputReportDescriptor), "DINPUT", "DInput",
GenericHid::kDInputProductString, GenericHid::kDInputSerialString);
test_generic_boundary_dispatch(
AdapterUsbMode::kMac, GenericHid::kMacDeviceDescriptor,
GenericHid::kMacReportDescriptor, "MAC", "Mac",
GenericHid::kMacConfigurationDescriptor,
GenericHid::kMacReportDescriptor,
sizeof(GenericHid::kMacReportDescriptor), "MAC", "Mac",
GenericHid::kMacProductString, GenericHid::kMacSerialString);
}