switch-pico/tests/controller_profile_transform_test.cpp

384 lines
17 KiB
C++

#include "core/controller_identity.h"
#include "profile/controller_profile.h"
#include "profile/controller_profile_transform.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <limits.h>
namespace {
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
ControllerProfile default_profile() {
return controller_profile_default(controller_identity_global(), 0);
}
bool states_equal(const ControllerState& left, const ControllerState& right) {
return left.dpad_up == right.dpad_up &&
left.dpad_down == right.dpad_down &&
left.dpad_left == right.dpad_left &&
left.dpad_right == right.dpad_right &&
left.button_south == right.button_south &&
left.button_east == right.button_east &&
left.button_west == right.button_west &&
left.button_north == right.button_north &&
left.button_left_shoulder == right.button_left_shoulder &&
left.button_right_shoulder == right.button_right_shoulder &&
left.button_select == right.button_select &&
left.button_start == right.button_start &&
left.button_system == right.button_system &&
left.button_capture == right.button_capture &&
left.button_left_stick == right.button_left_stick &&
left.button_right_stick == right.button_right_stick &&
left.left_trigger == right.left_trigger &&
left.right_trigger == right.right_trigger &&
left.left_stick_x == right.left_stick_x &&
left.left_stick_y == right.left_stick_y &&
left.right_stick_x == right.right_stick_x &&
left.right_stick_y == right.right_stick_y &&
left.motion_sample_count == right.motion_sample_count &&
std::memcmp(left.motion_samples, right.motion_samples,
sizeof(left.motion_samples)) == 0;
}
ControllerProfileTransformResult transform_left_stick(
const ControllerProfileStickConfiguration& configuration, int16_t x,
int16_t y) {
ControllerProfile profile = default_profile();
profile.sticks[0] = configuration;
ControllerState state{};
state.left_stick_x = x;
state.left_stick_y = y;
return controller_profile_transform(state, profile);
}
uint16_t transform_left_trigger(
const ControllerProfileTriggerConfiguration& configuration,
uint16_t value) {
ControllerProfile profile = default_profile();
profile.triggers[0] = configuration;
ControllerState state{};
state.left_trigger = value;
return controller_profile_transform(state, profile).state.left_trigger;
}
void test_button_masks_and_direct_mapping() {
ControllerState state{};
controller_profile_apply_button_mask(UINT16_MAX, &state);
require(controller_profile_extract_button_mask(state) == UINT16_MAX,
"button mask application omitted a logical button");
controller_profile_apply_button_mask(0, &state);
require(controller_profile_extract_button_mask(state) == 0,
"zero button mask did not clear every logical button");
controller_profile_apply_button_mask(UINT16_MAX, nullptr);
for (uint8_t input = 0;
input < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++input) {
ControllerProfile profile = default_profile();
const uint8_t output = static_cast<uint8_t>(
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT - 1u - input);
profile.button_map[input] = output;
state = {};
controller_profile_apply_button_mask(
static_cast<uint16_t>(1u << input), &state);
const ControllerProfileTransformResult transformed =
controller_profile_transform(state, profile);
require(controller_profile_extract_button_mask(transformed.state) ==
static_cast<uint16_t>(1u << output),
"a logical button did not map directly to its output");
}
ControllerProfile profile = default_profile();
profile.button_map[0] = 1;
profile.button_map[1] = 2;
require(controller_profile_map_button_mask(1u, profile) == 2u,
"button mapping recursively remapped an output");
profile.button_map[1] = 1;
require(controller_profile_map_button_mask(3u, profile) == 2u,
"duplicate mapped outputs were not combined");
profile.button_map[0] = CONTROLLER_PROFILE_NO_BUTTON;
require(controller_profile_map_button_mask(1u, profile) == 0,
"disabled button mapping still produced output");
ControllerProfile invalid = default_profile();
invalid.button_map[0] = CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT;
require(!controller_profile_validate(invalid),
"logical output 16 was accepted");
invalid.button_map[0] = 0xfe;
require(!controller_profile_validate(invalid),
"logical output 0xfe was accepted");
invalid.button_map[0] = CONTROLLER_PROFILE_NO_BUTTON;
require(controller_profile_validate(invalid),
"disabled logical output 0xff was rejected");
}
void test_stick_center_boundaries_and_inversion() {
ControllerProfileStickConfiguration configuration{};
configuration.center_x = 1234;
configuration.center_y = -2345;
configuration.inner_deadzone = 1000;
configuration.outer_saturation = 20000;
configuration.curve_q8_8 = 256;
ControllerProfileTransformResult transformed = transform_left_stick(
configuration, configuration.center_x, configuration.center_y);
require(transformed.state.left_stick_x == 0 &&
transformed.state.left_stick_y == 0,
"center calibration did not precede stick shaping");
transformed = transform_left_stick(
configuration,
static_cast<int16_t>(configuration.center_x +
configuration.inner_deadzone),
configuration.center_y);
require(transformed.state.left_stick_x == 0 &&
transformed.state.left_stick_y == 0,
"inner deadzone boundary was not neutral");
transformed = transform_left_stick(
configuration,
static_cast<int16_t>(configuration.center_x +
configuration.inner_deadzone + 1),
configuration.center_y);
require(transformed.state.left_stick_x > 0,
"first value outside inner deadzone stayed neutral");
configuration.center_x = 0;
configuration.center_y = 0;
configuration.inner_deadzone = 0;
configuration.outer_saturation = 20000;
transformed = transform_left_stick(configuration, 19999, 0);
require(transformed.state.left_stick_x > 0 &&
transformed.state.left_stick_x < INT16_MAX,
"value below outer saturation reached an endpoint");
transformed = transform_left_stick(configuration, 20000, 0);
require(transformed.state.left_stick_x == INT16_MAX,
"positive outer saturation boundary missed endpoint");
transformed = transform_left_stick(configuration, -20000, 0);
require(transformed.state.left_stick_x == INT16_MIN,
"negative outer saturation boundary missed endpoint");
configuration.outer_saturation = 32767;
configuration.invert_x = true;
configuration.invert_y = true;
transformed = transform_left_stick(configuration, INT16_MIN, 0);
require(transformed.state.left_stick_x == INT16_MAX,
"negative stick endpoint did not invert to positive endpoint");
transformed = transform_left_stick(configuration, 0, INT16_MAX);
require(transformed.state.left_stick_y == INT16_MIN,
"positive stick endpoint did not invert to negative endpoint");
configuration.invert_x = false;
configuration.invert_y = false;
configuration.outer_saturation = 20000;
transformed = transform_left_stick(configuration, 15000, 15000);
require(transformed.state.left_stick_x < INT16_MAX &&
transformed.state.left_stick_y < INT16_MAX,
"stick magnitude was not Chebyshev magnitude");
}
void test_stick_curves_and_monotonicity() {
ControllerProfileStickConfiguration linear{};
linear.inner_deadzone = 0;
linear.outer_saturation = 32767;
linear.curve_q8_8 = 256;
ControllerProfileStickConfiguration slow = linear;
slow.curve_q8_8 = 512;
ControllerProfileStickConfiguration fast = linear;
fast.curve_q8_8 = 128;
const int16_t linear_mid =
transform_left_stick(linear, 16384, 0).state.left_stick_x;
const int16_t slow_mid =
transform_left_stick(slow, 16384, 0).state.left_stick_x;
const int16_t fast_mid =
transform_left_stick(fast, 16384, 0).state.left_stick_x;
require(slow_mid < linear_mid && linear_mid < fast_mid,
"stick curve directions are reversed or ineffective");
require(transform_left_stick(slow, 0, 0).state.left_stick_x == 0 &&
transform_left_stick(fast, 32767, 0)
.state.left_stick_x == INT16_MAX,
"stick response curve did not preserve endpoints");
const uint16_t curves[] = {1, 128, 256, 512, UINT16_MAX};
for (uint16_t curve : curves) {
ControllerProfileStickConfiguration configuration = linear;
configuration.curve_q8_8 = curve;
int16_t previous = 0;
for (int32_t input = 0; input <= INT16_MAX; ++input) {
const int16_t output = transform_left_stick(
configuration,
static_cast<int16_t>(input), 0)
.state.left_stick_x;
require(output >= previous,
"stick response was not monotonic");
previous = output;
}
require(previous == INT16_MAX,
"monotonic stick response missed positive endpoint");
}
}
void test_trigger_boundaries_curves_and_thresholds() {
ControllerProfileTriggerConfiguration configuration{};
configuration.lower_deadzone = 1000;
configuration.upper_saturation = 60000;
configuration.curve_q8_8 = 256;
configuration.digital_threshold = 32000;
require(transform_left_trigger(configuration, 999) == 0 &&
transform_left_trigger(configuration, 1000) == 0,
"trigger lower deadzone boundary was not zero");
require(transform_left_trigger(configuration, 1001) > 0,
"first trigger value above lower deadzone stayed zero");
require(transform_left_trigger(configuration, 59999) < UINT16_MAX &&
transform_left_trigger(configuration, 60000) == UINT16_MAX &&
transform_left_trigger(configuration, UINT16_MAX) ==
UINT16_MAX,
"trigger upper saturation boundary missed full scale");
ControllerProfileTriggerConfiguration slow = configuration;
slow.curve_q8_8 = 512;
ControllerProfileTriggerConfiguration fast = configuration;
fast.curve_q8_8 = 128;
const uint16_t midpoint = static_cast<uint16_t>(
(static_cast<uint32_t>(configuration.lower_deadzone) +
configuration.upper_saturation) /
2u);
const uint16_t linear_mid =
transform_left_trigger(configuration, midpoint);
const uint16_t slow_mid = transform_left_trigger(slow, midpoint);
const uint16_t fast_mid = transform_left_trigger(fast, midpoint);
require(slow_mid < linear_mid && linear_mid < fast_mid,
"trigger curve directions are reversed or ineffective");
require(transform_left_trigger(slow, configuration.lower_deadzone) == 0 &&
transform_left_trigger(fast,
configuration.upper_saturation) ==
UINT16_MAX,
"trigger response curve did not preserve endpoints");
uint16_t previous = 0;
for (uint32_t input = 0; input <= UINT16_MAX; ++input) {
const uint16_t output = transform_left_trigger(
slow, static_cast<uint16_t>(input));
require(output >= previous,
"trigger response was not monotonic");
previous = output;
}
ControllerProfile profile = default_profile();
profile.triggers[0] = slow;
profile.triggers[0].digital_threshold = 12345;
profile.triggers[1].digital_threshold = 54321;
ControllerState state{};
state.left_trigger = midpoint;
const ControllerProfileTransformResult transformed =
controller_profile_transform(state, profile);
require(transformed.left_trigger_digital_threshold == 12345 &&
transformed.right_trigger_digital_threshold == 54321,
"profile-owned digital thresholds were not returned");
}
void test_default_whole_state_equivalence() {
ControllerState input{};
input.dpad_up = true;
input.dpad_right = true;
input.button_south = true;
input.button_west = true;
input.button_left_shoulder = true;
input.button_select = true;
input.button_system = true;
input.button_capture = true;
input.button_right_stick = true;
input.left_trigger = 0;
input.right_trigger = UINT16_MAX;
input.left_stick_x = INT16_MIN;
input.left_stick_y = INT16_MAX;
input.right_stick_x = INT16_MAX;
input.right_stick_y = INT16_MIN;
input.motion_sample_count = CONTROLLER_MOTION_SAMPLE_CAPACITY;
input.motion_samples[0] =
{INT16_MIN, -30000, -1, 0, 1, INT16_MAX};
input.motion_samples[1] = {1, 2, 3, 4, 5, 6};
input.motion_samples[2] =
{INT16_MAX, 30000, 1, 0, -1, INT16_MIN};
const ControllerProfile profile = default_profile();
const ControllerProfileTransformResult transformed =
controller_profile_transform(input, profile);
require(states_equal(input, transformed.state),
"default profile changed whole controller state or motion");
require(transformed.left_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
transformed.right_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD == 22934,
"default digital threshold changed");
for (uint32_t trigger = 0; trigger <= UINT16_MAX; ++trigger) {
ControllerState trigger_state{};
trigger_state.left_trigger = static_cast<uint16_t>(trigger);
trigger_state.right_trigger = static_cast<uint16_t>(trigger);
const ControllerState output =
controller_profile_transform(trigger_state, profile).state;
require(output.left_trigger == trigger_state.left_trigger &&
output.right_trigger == trigger_state.right_trigger,
"default profile lost full trigger analog precision");
}
}
void test_rumble_scaling_and_confirmation_policy() {
ControllerProfile profile = default_profile();
for (uint16_t magnitude = 0; magnitude <= UINT8_MAX; ++magnitude) {
const ControllerRumbleOutput input{
static_cast<uint8_t>(magnitude),
static_cast<uint8_t>(UINT8_MAX - magnitude),
};
const ControllerRumbleOutput output =
controller_profile_scale_host_rumble(input, profile);
require(output.low_frequency_magnitude ==
input.low_frequency_magnitude &&
output.high_frequency_magnitude ==
input.high_frequency_magnitude,
"default rumble scaling was not bit-exact identity");
}
profile.strong_rumble_scale = 0;
profile.weak_rumble_scale = 0;
ControllerRumbleOutput output = controller_profile_scale_host_rumble(
{UINT8_MAX, UINT8_MAX}, profile);
require(output.low_frequency_magnitude == 0 &&
output.high_frequency_magnitude == 0,
"zero rumble scales did not mute both bands");
profile.strong_rumble_scale = 128;
profile.weak_rumble_scale = 64;
output = controller_profile_scale_host_rumble({200, 201}, profile);
require(output.low_frequency_magnitude == 100 &&
output.high_frequency_magnitude == 50,
"strong/weak mid-scale rumble mapping was incorrect");
require(controller_profile_scale_rumble_magnitude(UINT8_MAX,
UINT8_MAX) ==
UINT8_MAX,
"full rumble scaling did not saturate at uint8 maximum");
profile.confirmation_policy = ControllerProfileConfirmationPolicy::kLed;
require(controller_profile_confirmation_policy(profile) ==
ControllerProfileConfirmationPolicy::kLed,
"confirmation policy was not exposed unchanged");
}
} // namespace
int main() {
test_button_masks_and_direct_mapping();
test_stick_center_boundaries_and_inversion();
test_stick_curves_and_monotonicity();
test_trigger_boundaries_curves_and_thresholds();
test_default_whole_state_equivalence();
test_rumble_scaling_and_confirmation_policy();
return 0;
}