Apply persistent controller profiles
This commit is contained in:
parent
3f90d04a50
commit
cfec816dac
25 changed files with 2179 additions and 96 deletions
63
tests/controller_profile_legacy_fixtures.h
Normal file
63
tests/controller_profile_legacy_fixtures.h
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_profile.h"
|
||||
|
||||
constexpr uint8_t kLegacyDefaultProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
constexpr uint8_t
|
||||
kLegacyNarrowRawRangeProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x30, 0x75, 0x40, 0x9c, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x30, 0x75,
|
||||
0x40, 0x9c, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
constexpr uint8_t kLegacyCustomThresholdProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0x00, 0x01, 0xcd, 0xab, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
273
tests/controller_profile_runtime_test.cpp
Normal file
273
tests/controller_profile_runtime_test.cpp
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
#include "controller_profile_runtime.h"
|
||||
|
||||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "profile_service.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct FakeProfileRow {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t active_profile = 0;
|
||||
ControllerProfile profiles[CONTROLLER_PROFILE_COUNT]{};
|
||||
};
|
||||
|
||||
std::array<FakeProfileRow, CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT> rows{};
|
||||
uint32_t database_generation = 7;
|
||||
unsigned active_snapshot_count = 0;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ControllerIdentity make_identity(uint8_t value) {
|
||||
ControllerIdentity identity{};
|
||||
identity.stable = true;
|
||||
identity.transport = ControllerTransport::kClassic;
|
||||
identity.address[0] = value;
|
||||
identity.address[5] = static_cast<uint8_t>(value + 0x40u);
|
||||
identity.vendor_id = static_cast<uint16_t>(0x1000u + value);
|
||||
identity.product_id = static_cast<uint16_t>(0x2000u + value);
|
||||
return identity;
|
||||
}
|
||||
|
||||
void prepare_profiles() {
|
||||
database_generation = 7;
|
||||
active_snapshot_count = 0;
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
FakeProfileRow& row = rows[slot];
|
||||
row = {};
|
||||
row.identity = make_identity(static_cast<uint8_t>(slot + 1u));
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
row.profiles[profile_index] =
|
||||
controller_profile_default(row.identity, profile_index);
|
||||
}
|
||||
row.profiles[0].triggers[0].digital_threshold =
|
||||
static_cast<uint16_t>(1000u + slot);
|
||||
row.profiles[0].triggers[1].digital_threshold =
|
||||
static_cast<uint16_t>(5000u + slot);
|
||||
row.profiles[0].confirmation_policy =
|
||||
slot == 0 ? ControllerProfileConfirmationPolicy::kLed
|
||||
: ControllerProfileConfirmationPolicy::kRumble;
|
||||
}
|
||||
rows[0].profiles[0].strong_rumble_scale = 0;
|
||||
rows[0].profiles[0].weak_rumble_scale = UINT8_MAX;
|
||||
rows[1].profiles[0].strong_rumble_scale = UINT8_MAX;
|
||||
rows[1].profiles[0].weak_rumble_scale = 0;
|
||||
controller_profile_runtime_reset();
|
||||
}
|
||||
|
||||
Bluepad32SlotSnapshot make_snapshot(uint8_t slot,
|
||||
uint32_t connection_generation = 1) {
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
snapshot.active = true;
|
||||
snapshot.connection_generation = connection_generation;
|
||||
snapshot.identity = rows[slot].identity;
|
||||
snapshot.state = controller_neutral_state();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
bool motion_equal(const ControllerState& first,
|
||||
const ControllerState& second) {
|
||||
return first.motion_sample_count == second.motion_sample_count &&
|
||||
std::memcmp(first.motion_samples, second.motion_samples,
|
||||
sizeof(first.motion_samples)) == 0;
|
||||
}
|
||||
|
||||
void test_four_slot_cache_and_unchanged_generation() {
|
||||
prepare_profiles();
|
||||
std::array<Bluepad32SlotSnapshot,
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||
snapshots{};
|
||||
std::array<ControllerProfileTransformResult,
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||
transformed{};
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
snapshots[slot] = make_snapshot(slot);
|
||||
transformed[slot] =
|
||||
controller_profile_runtime_transform(slot, snapshots[slot]);
|
||||
require(transformed[slot].left_trigger_digital_threshold ==
|
||||
static_cast<uint16_t>(1000u + slot) &&
|
||||
transformed[slot].right_trigger_digital_threshold ==
|
||||
static_cast<uint16_t>(5000u + slot),
|
||||
"a slot did not receive its own cached profile");
|
||||
}
|
||||
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
|
||||
"initial slot loads did not fetch exactly one profile each");
|
||||
|
||||
rows[0].profiles[0].triggers[0].digital_threshold = 65000;
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
transformed[slot] =
|
||||
controller_profile_runtime_transform(slot, snapshots[slot]);
|
||||
}
|
||||
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
|
||||
"unchanged generations copied profiles on the report path");
|
||||
require(transformed[0].left_trigger_digital_threshold == 1000,
|
||||
"an unchanged generation bypassed the slot cache");
|
||||
|
||||
snapshots[2].connection_generation = 2;
|
||||
transformed[2] =
|
||||
controller_profile_runtime_transform(2, snapshots[2]);
|
||||
require(active_snapshot_count ==
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 1 &&
|
||||
transformed[2].left_trigger_digital_threshold == 1002,
|
||||
"one slot connection generation did not refresh in isolation");
|
||||
|
||||
snapshots[3].identity = rows[1].identity;
|
||||
transformed[3] =
|
||||
controller_profile_runtime_transform(3, snapshots[3]);
|
||||
require(active_snapshot_count ==
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 2 &&
|
||||
transformed[3].left_trigger_digital_threshold == 1001,
|
||||
"an exact identity change did not refresh only its slot");
|
||||
}
|
||||
|
||||
void test_activation_disconnect_and_default_preservation() {
|
||||
prepare_profiles();
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
snapshot.state.button_south = true;
|
||||
ControllerProfileTransformResult transformed =
|
||||
controller_profile_runtime_transform(0, snapshot);
|
||||
require(transformed.state.button_south &&
|
||||
transformed.left_trigger_digital_threshold == 1000,
|
||||
"initial active profile was not applied");
|
||||
|
||||
rows[0].active_profile = 1;
|
||||
rows[0].profiles[1].button_map[
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth)] =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
|
||||
rows[0].profiles[1].triggers[0].digital_threshold = 12345;
|
||||
++database_generation;
|
||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
||||
require(!transformed.state.button_south && transformed.state.button_north &&
|
||||
transformed.left_trigger_digital_threshold == 12345,
|
||||
"profile activation generation did not refresh the slot cache");
|
||||
|
||||
const unsigned reads_before_disconnect = active_snapshot_count;
|
||||
snapshot.active = false;
|
||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
||||
require(!transformed.state.button_south && !transformed.state.button_north &&
|
||||
transformed.left_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
active_snapshot_count == reads_before_disconnect,
|
||||
"disconnect did not neutralize output without fetching a profile");
|
||||
|
||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
||||
require(active_snapshot_count == reads_before_disconnect,
|
||||
"an unchanged inactive slot repeatedly cleared or fetched state");
|
||||
snapshot.active = true;
|
||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
||||
require(active_snapshot_count == reads_before_disconnect + 1 &&
|
||||
transformed.state.button_north,
|
||||
"reconnection did not reload a cleared slot cache");
|
||||
|
||||
Bluepad32SlotSnapshot default_snapshot = make_snapshot(2);
|
||||
rows[2].profiles[0] =
|
||||
controller_profile_default(rows[2].identity, 0);
|
||||
++database_generation;
|
||||
default_snapshot.state.button_east = true;
|
||||
default_snapshot.state.dpad_left = true;
|
||||
default_snapshot.state.left_trigger = 32123;
|
||||
default_snapshot.state.right_trigger = 54321;
|
||||
default_snapshot.state.left_stick_x = -12345;
|
||||
default_snapshot.state.right_stick_y = 23456;
|
||||
default_snapshot.state.motion_sample_count = 2;
|
||||
default_snapshot.state.motion_samples[0] = {1, 2, 3, 4, 5, 6};
|
||||
default_snapshot.state.motion_samples[1] = {-1, -2, -3, -4, -5, -6};
|
||||
transformed = controller_profile_runtime_transform(2, default_snapshot);
|
||||
require(transformed.state.button_east && transformed.state.dpad_left &&
|
||||
transformed.state.left_trigger == 32123 &&
|
||||
transformed.state.right_trigger == 54321 &&
|
||||
transformed.state.left_stick_x == -12345 &&
|
||||
transformed.state.right_stick_y == 23456 &&
|
||||
motion_equal(transformed.state, default_snapshot.state) &&
|
||||
transformed.left_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
transformed.right_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"default runtime profile changed serializer input or motion");
|
||||
}
|
||||
|
||||
void test_analog_thresholds_rumble_and_local_confirmation() {
|
||||
prepare_profiles();
|
||||
Bluepad32SlotSnapshot first = make_snapshot(0);
|
||||
Bluepad32SlotSnapshot second = make_snapshot(1);
|
||||
first.state.left_trigger = second.state.left_trigger = 23456;
|
||||
first.state.right_trigger = second.state.right_trigger = 45678;
|
||||
const ControllerProfileTransformResult first_output =
|
||||
controller_profile_runtime_transform(0, first);
|
||||
const ControllerProfileTransformResult second_output =
|
||||
controller_profile_runtime_transform(1, second);
|
||||
require(first_output.state.left_trigger ==
|
||||
second_output.state.left_trigger &&
|
||||
first_output.state.right_trigger ==
|
||||
second_output.state.right_trigger &&
|
||||
first_output.left_trigger_digital_threshold !=
|
||||
second_output.left_trigger_digital_threshold,
|
||||
"digital thresholds changed XInput-visible analog trigger state");
|
||||
|
||||
const ControllerRumbleOutput host{91, 73};
|
||||
const ControllerRumbleOutput first_host =
|
||||
controller_profile_runtime_scale_host_rumble(0, first, host);
|
||||
const ControllerRumbleOutput second_host =
|
||||
controller_profile_runtime_scale_host_rumble(1, second, host);
|
||||
require(first_host.low_frequency_magnitude == 0 &&
|
||||
first_host.high_frequency_magnitude == 73 &&
|
||||
second_host.low_frequency_magnitude == 91 &&
|
||||
second_host.high_frequency_magnitude == 0,
|
||||
"host rumble was not scaled through each slot profile");
|
||||
|
||||
const ControllerProfileRuntimeLocalConfirmation confirmation =
|
||||
controller_profile_runtime_local_confirmation(0, first, host);
|
||||
require(confirmation.rumble.low_frequency_magnitude == 91 &&
|
||||
confirmation.rumble.high_frequency_magnitude == 73 &&
|
||||
confirmation.policy ==
|
||||
ControllerProfileConfirmationPolicy::kLed,
|
||||
"local confirmation was scaled or lost its profile policy");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t profile_service_database_generation() {
|
||||
return database_generation;
|
||||
}
|
||||
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity,
|
||||
ProfileServiceActiveProfileSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
++active_snapshot_count;
|
||||
*output = {};
|
||||
output->metadata.state = ProfileServiceState::kReady;
|
||||
output->metadata.generation = database_generation;
|
||||
for (const FakeProfileRow& row : rows) {
|
||||
if (!controller_identity_equal(row.identity, identity)) {
|
||||
continue;
|
||||
}
|
||||
output->valid = true;
|
||||
output->profile_index = row.active_profile;
|
||||
output->profile = row.profiles[row.active_profile];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_four_slot_cache_and_unchanged_generation();
|
||||
test_activation_disconnect_and_default_preservation();
|
||||
test_analog_thresholds_rumble_and_local_confirmation();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "tests/controller_profile_legacy_fixtures.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
|
@ -44,9 +45,9 @@ void test_profile_wire_schema() {
|
|||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"default profile did not encode");
|
||||
require(encoded[0] == 1 && encoded[1] == 0 &&
|
||||
require(encoded[0] == 2 && encoded[1] == 0 &&
|
||||
encoded[2] == 0 && encoded[3] == 1,
|
||||
"profile header is not little-endian v1/256");
|
||||
"profile header is not little-endian v2/256");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
require(encoded[4 + index] == index,
|
||||
|
|
@ -56,7 +57,12 @@ void test_profile_wire_schema() {
|
|||
encoded[30] == 0,
|
||||
"default stick encoding changed");
|
||||
require(encoded[54] == 0xff && encoded[55] == 0xff &&
|
||||
encoded[58] == 0x00 && encoded[59] == 0x80,
|
||||
encoded[58] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
|
||||
encoded[59] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8),
|
||||
"default trigger encoding changed");
|
||||
require(encoded[72] == 0xff && encoded[73] == 0xff &&
|
||||
encoded[74] == 3 && encoded[78] == 0xff &&
|
||||
|
|
@ -84,11 +90,35 @@ void test_profile_wire_schema() {
|
|||
invalid.sticks[0].outer_saturation;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"empty stick range was accepted");
|
||||
ControllerProfile boundary = profile;
|
||||
boundary.triggers[0].lower_deadzone = 30000;
|
||||
boundary.triggers[0].upper_saturation = 40000;
|
||||
boundary.triggers[0].digital_threshold = 0;
|
||||
uint8_t boundary_encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(boundary, boundary_encoded,
|
||||
sizeof(boundary_encoded)) &&
|
||||
controller_profile_decode(boundary_encoded,
|
||||
sizeof(boundary_encoded),
|
||||
&decoded) &&
|
||||
decoded.triggers[0].digital_threshold == 0,
|
||||
"current profile rejected zero transformed trigger threshold");
|
||||
boundary.triggers[0].digital_threshold = UINT16_MAX;
|
||||
require(controller_profile_encode(boundary, boundary_encoded,
|
||||
sizeof(boundary_encoded)) &&
|
||||
controller_profile_decode(boundary_encoded,
|
||||
sizeof(boundary_encoded),
|
||||
&decoded) &&
|
||||
decoded.triggers[0].digital_threshold == UINT16_MAX,
|
||||
"current profile rejected maximum transformed trigger threshold");
|
||||
invalid = profile;
|
||||
invalid.triggers[0].digital_threshold = 0;
|
||||
invalid.triggers[0].lower_deadzone = 1;
|
||||
invalid.triggers[0].lower_deadzone =
|
||||
invalid.triggers[0].upper_saturation;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"trigger threshold outside its range was accepted");
|
||||
"empty raw trigger range was accepted");
|
||||
invalid.triggers[0].lower_deadzone = UINT16_MAX;
|
||||
invalid.triggers[0].upper_saturation = UINT16_MAX - 1;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"reversed raw trigger range was accepted");
|
||||
invalid = profile;
|
||||
invalid.turbo_modes[0] =
|
||||
static_cast<ControllerProfileTurboMode>(3);
|
||||
|
|
@ -110,6 +140,97 @@ void test_profile_wire_schema() {
|
|||
"macro without a final end was accepted");
|
||||
}
|
||||
|
||||
void test_legacy_profile_migration() {
|
||||
ControllerProfile migrated{};
|
||||
require(controller_profile_decode(
|
||||
kLegacyDefaultProfile, sizeof(kLegacyDefaultProfile),
|
||||
&migrated),
|
||||
"legacy default profile did not decode");
|
||||
require(migrated.triggers[0].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
migrated.triggers[1].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"legacy inherited thresholds were not migrated");
|
||||
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"migrated default profile did not encode");
|
||||
for (size_t index = 0; index < sizeof(encoded); ++index) {
|
||||
const bool schema_byte = index == 0;
|
||||
const bool threshold_byte =
|
||||
(index >= 58 && index < 60) ||
|
||||
(index >= 68 && index < 70);
|
||||
if (!schema_byte && !threshold_byte) {
|
||||
require(encoded[index] == kLegacyDefaultProfile[index],
|
||||
"legacy default profile changed an unrelated byte");
|
||||
}
|
||||
}
|
||||
require(encoded[0] == CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
encoded[58] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
|
||||
encoded[59] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8) &&
|
||||
encoded[68] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
|
||||
encoded[69] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8),
|
||||
"migrated default profile did not encode as v2");
|
||||
|
||||
require(controller_profile_decode(
|
||||
kLegacyNarrowRawRangeProfile,
|
||||
sizeof(kLegacyNarrowRawRangeProfile), &migrated),
|
||||
"legacy narrow-raw-range profile did not decode");
|
||||
require(migrated.triggers[0].lower_deadzone == 30000 &&
|
||||
migrated.triggers[0].upper_saturation == 40000 &&
|
||||
migrated.triggers[0].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
migrated.triggers[1].lower_deadzone == 30000 &&
|
||||
migrated.triggers[1].upper_saturation == 40000 &&
|
||||
migrated.triggers[1].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"legacy narrow raw range or inherited threshold was not migrated");
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"migrated narrow-raw-range profile did not encode");
|
||||
for (size_t index = 0; index < sizeof(encoded); ++index) {
|
||||
const bool schema_byte = index == 0;
|
||||
const bool threshold_byte =
|
||||
(index >= 58 && index < 60) ||
|
||||
(index >= 68 && index < 70);
|
||||
if (!schema_byte && !threshold_byte) {
|
||||
require(
|
||||
encoded[index] == kLegacyNarrowRawRangeProfile[index],
|
||||
"narrow-raw-range migration changed unrelated profile data");
|
||||
}
|
||||
}
|
||||
|
||||
require(controller_profile_decode(
|
||||
kLegacyCustomThresholdProfile,
|
||||
sizeof(kLegacyCustomThresholdProfile), &migrated),
|
||||
"legacy custom-threshold profile did not decode");
|
||||
require(migrated.triggers[0].digital_threshold == 0x1234 &&
|
||||
migrated.triggers[1].digital_threshold == 0xabcd,
|
||||
"legacy custom thresholds were not preserved");
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"legacy custom-threshold profile did not re-encode");
|
||||
for (size_t index = 1; index < sizeof(encoded); ++index) {
|
||||
require(encoded[index] == kLegacyCustomThresholdProfile[index],
|
||||
"legacy custom-threshold profile changed data");
|
||||
}
|
||||
|
||||
ControllerProfile current =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
current.triggers[0].digital_threshold = 0x8000;
|
||||
require(controller_profile_encode(current, encoded, sizeof(encoded)) &&
|
||||
controller_profile_decode(encoded, sizeof(encoded),
|
||||
&migrated) &&
|
||||
migrated.triggers[0].digital_threshold == 0x8000,
|
||||
"v2 custom threshold matching the legacy default was migrated");
|
||||
}
|
||||
|
||||
void test_database_round_trip_and_capacity() {
|
||||
controller_profile_database_default(&database);
|
||||
for (uint8_t index = 0;
|
||||
|
|
@ -141,6 +262,10 @@ void test_database_round_trip_and_capacity() {
|
|||
database, offset, &encoded_database[offset], size),
|
||||
"database range did not encode");
|
||||
}
|
||||
require(encoded_database[4] ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION &&
|
||||
encoded_database[5] == 0,
|
||||
"database encoder did not emit v2");
|
||||
require(controller_profile_database_decode(
|
||||
read_encoded_database, nullptr, &decoded_database),
|
||||
"database did not decode");
|
||||
|
|
@ -157,6 +282,7 @@ void test_database_round_trip_and_capacity() {
|
|||
} // namespace
|
||||
int main() {
|
||||
test_profile_wire_schema();
|
||||
test_legacy_profile_migration();
|
||||
test_database_round_trip_and_capacity();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
385
tests/controller_profile_transform_test.cpp
Normal file
385
tests/controller_profile_transform_test.cpp
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "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;
|
||||
}
|
||||
|
|
@ -82,6 +82,13 @@ ProfileServiceTransactionSnapshot transaction_snapshot() {
|
|||
return snapshot;
|
||||
}
|
||||
|
||||
ProfileServiceActiveProfileSnapshot active_profile_snapshot(
|
||||
const ControllerIdentity& identity) {
|
||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
ControllerProfileDatabase reload_database(
|
||||
const ProfileServiceTransactionSnapshot& transaction,
|
||||
uint32_t expected_generation) {
|
||||
|
|
@ -102,6 +109,14 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
|
|||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
profile_service_prepare();
|
||||
profile_service_initialize_on_storage_core();
|
||||
ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(controller_identity_global());
|
||||
require(active.valid &&
|
||||
active.metadata.state == ProfileServiceState::kReady &&
|
||||
active.metadata.generation == 0 &&
|
||||
profile_service_database_generation() == 0 &&
|
||||
active.profile_index == 0,
|
||||
"initial active profile snapshot was not coherent");
|
||||
|
||||
const ControllerIdentity identity = controller_identity_global();
|
||||
constexpr uint8_t kProfileIndex = 2;
|
||||
|
|
@ -128,6 +143,11 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
|
|||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"profile write baseline did not commit");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 1 &&
|
||||
profile_service_database_generation() == 1 &&
|
||||
active.profile_index == 0,
|
||||
"profile write did not publish one coherent generation");
|
||||
|
||||
constexpr uint32_t kResetTransactionId = 0xa5a55a5a;
|
||||
require(profile_service_reset(kResetTransactionId, identity,
|
||||
|
|
@ -150,6 +170,11 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
|
|||
require(recovered.fallback_profiles[kProfileIndex].strong_rumble_scale ==
|
||||
UINT8_MAX,
|
||||
"terminal reset status was published before reset persisted");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 2 &&
|
||||
profile_service_database_generation() == 2 &&
|
||||
active.profile_index == 0,
|
||||
"profile reset did not refresh the active snapshot generation");
|
||||
|
||||
constexpr uint32_t kActivateTransactionId = 0x50607080;
|
||||
constexpr uint8_t kActivatedProfile = 3;
|
||||
|
|
@ -162,6 +187,10 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
|
|||
activate.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"pending activation lost its transaction identity");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 2 &&
|
||||
active.profile_index == 0,
|
||||
"pending activation leaked an uncommitted active profile");
|
||||
|
||||
profile_service_task_on_storage_core(2000);
|
||||
activate = transaction_snapshot();
|
||||
|
|
@ -172,6 +201,14 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
|
|||
recovered = reload_database(activate, 3);
|
||||
require(recovered.fallback_active_profile == kActivatedProfile,
|
||||
"terminal activation status was published before activation persisted");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 3 &&
|
||||
profile_service_database_generation() == 3 &&
|
||||
active.profile_index == kActivatedProfile &&
|
||||
active.profile.strong_rumble_scale ==
|
||||
recovered.fallback_profiles[kActivatedProfile]
|
||||
.strong_rumble_scale,
|
||||
"activation did not publish profile, index, and generation together");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "profile_storage.h"
|
||||
#include "tests/controller_profile_legacy_fixtures.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
|
@ -15,6 +16,7 @@ struct FakeFlash {
|
|||
bool corrupt_next_program = false;
|
||||
bool fail_reads_after_header_program = false;
|
||||
bool header_programmed = false;
|
||||
int erase_count = 0;
|
||||
};
|
||||
|
||||
FakeFlash flash{};
|
||||
|
|
@ -35,6 +37,7 @@ void erase_all() {
|
|||
flash.corrupt_next_program = false;
|
||||
flash.fail_reads_after_header_program = false;
|
||||
flash.header_programmed = false;
|
||||
flash.erase_count = 0;
|
||||
}
|
||||
|
||||
bool fake_read(void* context, uint8_t bank, size_t offset,
|
||||
|
|
@ -62,6 +65,7 @@ bool fake_erase_sector(void* context, uint8_t bank, size_t offset) {
|
|||
PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
++storage->erase_count;
|
||||
memset(&storage->bytes[bank][offset], 0xff,
|
||||
PROFILE_STORAGE_SECTOR_SIZE);
|
||||
return true;
|
||||
|
|
@ -107,6 +111,91 @@ ProfileStorageIo fake_io() {
|
|||
};
|
||||
}
|
||||
|
||||
uint16_t fixture_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
static_cast<uint16_t>(input[1] << 8);
|
||||
}
|
||||
|
||||
void fixture_write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
void fixture_write_u32(uint8_t* output, uint32_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
output[2] = static_cast<uint8_t>(value >> 16);
|
||||
output[3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
void install_legacy_database_bank_fixture() {
|
||||
erase_all();
|
||||
constexpr uint8_t kBank = 1;
|
||||
constexpr uint32_t kGeneration = 41;
|
||||
constexpr size_t kFallbackOffset =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntryOffset =
|
||||
kFallbackOffset +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
constexpr uint8_t kEntryHeader[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE] = {
|
||||
1, 1, 7, 0, 1, 2, 3, 4, 5, 6, 0x7e, 0x05, 0x09, 0x20, 3, 1,
|
||||
};
|
||||
|
||||
uint8_t* const record = flash.bytes[kBank];
|
||||
uint8_t* const payload = record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
memset(record, 0, PROFILE_STORAGE_RECORD_HEADER_SIZE);
|
||||
memset(payload, 0, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
|
||||
memcpy(payload, "SPDB", 4);
|
||||
fixture_write_u16(&payload[4],
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
|
||||
fixture_write_u16(
|
||||
&payload[6],
|
||||
static_cast<uint16_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
|
||||
payload[8] = CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
payload[9] = CONTROLLER_PROFILE_COUNT;
|
||||
payload[10] = 2;
|
||||
payload[11] = 1;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint8_t* fixture =
|
||||
profile_index == 0
|
||||
? kLegacyNarrowRawRangeProfile
|
||||
: profile_index == 1 ? kLegacyCustomThresholdProfile
|
||||
: kLegacyDefaultProfile;
|
||||
memcpy(&payload[kFallbackOffset +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
|
||||
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
}
|
||||
|
||||
memcpy(&payload[kEntryOffset], kEntryHeader, sizeof(kEntryHeader));
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint8_t* fixture =
|
||||
profile_index == 0
|
||||
? kLegacyNarrowRawRangeProfile
|
||||
: profile_index == 3 ? kLegacyCustomThresholdProfile
|
||||
: kLegacyDefaultProfile;
|
||||
memcpy(&payload[kEntryOffset +
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
|
||||
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
}
|
||||
|
||||
const uint32_t payload_crc = profile_storage_crc32(
|
||||
payload, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
memcpy(record, "SPPF", 4);
|
||||
fixture_write_u16(&record[4], 1);
|
||||
fixture_write_u16(&record[6],
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
|
||||
fixture_write_u32(&record[8], kGeneration);
|
||||
fixture_write_u32(
|
||||
&record[12],
|
||||
static_cast<uint32_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
|
||||
fixture_write_u32(&record[16], payload_crc);
|
||||
fixture_write_u32(&record[20], profile_storage_crc32(record, 20));
|
||||
}
|
||||
|
||||
void test_two_bank_recovery() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
|
|
@ -225,11 +314,180 @@ void test_payload_corruption_prevents_header_publication() {
|
|||
"headerless corrupt payload was recovered");
|
||||
}
|
||||
|
||||
void test_legacy_database_bank_migration() {
|
||||
install_legacy_database_bank_fixture();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &recovered_database) &&
|
||||
storage.snapshot().valid &&
|
||||
storage.snapshot().active_bank == 1 &&
|
||||
storage.snapshot().generation == 41,
|
||||
"legacy v1 database bank was not selected");
|
||||
require(flash.erase_count == 0,
|
||||
"legacy bank admission erased flash");
|
||||
require(recovered_database.fallback_active_profile == 2,
|
||||
"legacy fallback active profile was not preserved");
|
||||
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint16_t expected_left =
|
||||
profile_index == 1
|
||||
? 0x1234
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
const uint16_t expected_right =
|
||||
profile_index == 1
|
||||
? 0xabcd
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
require(recovered_database.fallback_profiles[profile_index]
|
||||
.triggers[0]
|
||||
.digital_threshold == expected_left &&
|
||||
recovered_database.fallback_profiles[profile_index]
|
||||
.triggers[1]
|
||||
.digital_threshold == expected_right,
|
||||
"legacy fallback thresholds were not selectively migrated");
|
||||
}
|
||||
require(recovered_database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[1]
|
||||
.lower_deadzone == 30000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[1]
|
||||
.upper_saturation == 40000,
|
||||
"legacy fallback raw trigger ranges were not preserved");
|
||||
|
||||
const ControllerProfileDatabaseEntry& entry =
|
||||
recovered_database.entries[0];
|
||||
require(entry.used && entry.active_profile == 3 &&
|
||||
entry.identity.stable &&
|
||||
entry.identity.transport == ControllerTransport::kClassic &&
|
||||
entry.identity.address_type == 7 &&
|
||||
entry.identity.address[0] == 1 &&
|
||||
entry.identity.address[5] == 6 &&
|
||||
entry.identity.vendor_id == 0x057e &&
|
||||
entry.identity.product_id == 0x2009,
|
||||
"legacy entry identity or active profile was not preserved");
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint16_t expected_left =
|
||||
profile_index == 3
|
||||
? 0x1234
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
const uint16_t expected_right =
|
||||
profile_index == 3
|
||||
? 0xabcd
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
require(entry.profiles[profile_index]
|
||||
.triggers[0]
|
||||
.digital_threshold == expected_left &&
|
||||
entry.profiles[profile_index]
|
||||
.triggers[1]
|
||||
.digital_threshold == expected_right,
|
||||
"legacy entry thresholds were not selectively migrated");
|
||||
}
|
||||
require(entry.profiles[0].triggers[0].lower_deadzone == 30000 &&
|
||||
entry.profiles[0].triggers[0].upper_saturation == 40000 &&
|
||||
entry.profiles[0].triggers[1].lower_deadzone == 30000 &&
|
||||
entry.profiles[0].triggers[1].upper_saturation == 40000,
|
||||
"legacy entry raw trigger ranges were not preserved");
|
||||
|
||||
recovered_database.fallback_profiles[2].weak_rumble_scale = 17;
|
||||
require(storage.commit(recovered_database) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
storage.snapshot().active_bank == 0 &&
|
||||
storage.snapshot().generation == 42,
|
||||
"mutation after legacy admission did not commit");
|
||||
const uint8_t* const current_record = flash.bytes[0];
|
||||
const uint8_t* const current_payload =
|
||||
current_record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
require(fixture_read_u16(¤t_record[6]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION &&
|
||||
fixture_read_u16(¤t_payload[4]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION,
|
||||
"post-migration commit did not emit v2 storage schemas");
|
||||
constexpr size_t kFallbackOffset =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntryOffset =
|
||||
kFallbackOffset +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
require(fixture_read_u16(
|
||||
¤t_payload[kFallbackOffset +
|
||||
profile_index *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
fixture_read_u16(
|
||||
¤t_payload[
|
||||
kEntryOffset +
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION,
|
||||
"post-migration commit retained a v1 profile");
|
||||
}
|
||||
require(fixture_read_u16(&flash.bytes[1][6]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
|
||||
fixture_read_u16(
|
||||
&flash.bytes[1][PROFILE_STORAGE_RECORD_HEADER_SIZE + 4]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION,
|
||||
"post-migration commit erased or rewrote the admitted legacy bank");
|
||||
|
||||
ProfileStorage reloaded;
|
||||
require(reloaded.initialize(fake_io(), &database) &&
|
||||
reloaded.snapshot().generation == 42 &&
|
||||
database.fallback_profiles[2].weak_rumble_scale == 17 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
database.fallback_profiles[1]
|
||||
.triggers[0]
|
||||
.digital_threshold == 0x1234 &&
|
||||
database.fallback_profiles[1]
|
||||
.triggers[1]
|
||||
.digital_threshold == 0xabcd &&
|
||||
database.entries[0].used &&
|
||||
database.entries[0].active_profile == 3 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
database.entries[0]
|
||||
.profiles[3]
|
||||
.triggers[0]
|
||||
.digital_threshold == 0x1234 &&
|
||||
database.entries[0]
|
||||
.profiles[3]
|
||||
.triggers[1]
|
||||
.digital_threshold == 0xabcd,
|
||||
"v2 migration commit did not reload without data loss");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
int main() {
|
||||
test_two_bank_recovery();
|
||||
test_interrupted_commit_retains_previous_bank();
|
||||
test_successful_header_program_is_commit_point();
|
||||
test_payload_corruption_prevents_header_publication();
|
||||
test_legacy_database_bank_migration();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,7 +289,9 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
states[3].button_west = true;
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
switch_pro_set_input(instance, states[instance]);
|
||||
switch_pro_set_input(instance, states[instance],
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
}
|
||||
|
||||
now_ms = 15;
|
||||
|
|
@ -328,7 +330,9 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
ControllerState changed_zero = states[0];
|
||||
changed_zero.button_east = false;
|
||||
changed_zero.button_system = true;
|
||||
switch_pro_set_input(0, changed_zero);
|
||||
switch_pro_set_input(0, changed_zero,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 30;
|
||||
expect(switch_pro_task(0),
|
||||
"instance 0 did not apply its changed input state");
|
||||
|
|
@ -341,7 +345,9 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
ControllerState changed_three = states[3];
|
||||
changed_three.button_west = false;
|
||||
changed_three.button_capture = true;
|
||||
switch_pro_set_input(3, changed_three);
|
||||
switch_pro_set_input(3, changed_three,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 45;
|
||||
expect(switch_pro_task(3),
|
||||
"instance 3 did not apply its changed input state");
|
||||
|
|
@ -370,8 +376,10 @@ void test_callback_send_and_imu_modes_are_isolated() {
|
|||
ControllerState one = zero;
|
||||
one.button_north = true;
|
||||
one.motion_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
|
||||
switch_pro_set_input(0, zero);
|
||||
switch_pro_set_input(1, one);
|
||||
switch_pro_set_input(0, zero, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(1, one, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 21;
|
||||
expect(switch_pro_task(0), "raw-IMU instance did not send input");
|
||||
expect(switch_pro_task(1), "off-IMU instance timer did not send input");
|
||||
|
|
@ -400,8 +408,11 @@ void test_callback_send_and_imu_modes_are_isolated() {
|
|||
stationary.right_stick_x = stationary.right_stick_y = 0;
|
||||
stationary.motion_sample_count = 1;
|
||||
stationary.motion_samples[0] = {1000, 2000, 3000, 0, 0, 0};
|
||||
switch_pro_set_input(0, moving);
|
||||
switch_pro_set_input(1, stationary);
|
||||
switch_pro_set_input(0, moving, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(1, stationary,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 21;
|
||||
expect(switch_pro_task(0), "moving quaternion instance did not report");
|
||||
expect(switch_pro_task(1), "stationary quaternion timer crossed instances");
|
||||
|
|
@ -578,7 +589,9 @@ void test_lifecycle_and_invalid_instances() {
|
|||
ControllerState ignored{};
|
||||
ignored.button_system = true;
|
||||
switch_pro_init(kInvalidInstance);
|
||||
switch_pro_set_input(kInvalidInstance, ignored);
|
||||
switch_pro_set_input(kInvalidInstance, ignored,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
|
||||
expect(!switch_pro_task(kInvalidInstance),
|
||||
"invalid instance ran a driver task");
|
||||
|
|
@ -598,7 +611,8 @@ void test_protocol_neutral_trigger_threshold() {
|
|||
state.left_trigger =
|
||||
static_cast<uint16_t>(SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD - 1u);
|
||||
state.right_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
switch_pro_set_input(0, state);
|
||||
switch_pro_set_input(0, state, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 15;
|
||||
expect(switch_pro_task(0), "trigger threshold report was not sent");
|
||||
SwitchProReport report = copy_switch_report(latest_regular_report(0));
|
||||
|
|
@ -607,7 +621,8 @@ void test_protocol_neutral_trigger_threshold() {
|
|||
|
||||
state.left_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
state.right_trigger = CONTROLLER_TRIGGER_MIN;
|
||||
switch_pro_set_input(0, state);
|
||||
switch_pro_set_input(0, state, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 30;
|
||||
expect(switch_pro_task(0), "second trigger threshold report was not sent");
|
||||
report = copy_switch_report(latest_regular_report(0));
|
||||
|
|
@ -615,13 +630,36 @@ void test_protocol_neutral_trigger_threshold() {
|
|||
"Switch trigger threshold changed at the upper boundary");
|
||||
}
|
||||
|
||||
void test_custom_trigger_thresholds_are_isolated() {
|
||||
initialize_contexts();
|
||||
ControllerState state{};
|
||||
state.left_trigger = 300;
|
||||
state.right_trigger = 300;
|
||||
switch_pro_set_input(0, state, 300, 301);
|
||||
switch_pro_set_input(1, state, 301, 300);
|
||||
|
||||
now_ms = 15;
|
||||
expect(switch_pro_task(0) && switch_pro_task(1),
|
||||
"custom trigger threshold reports were not sent");
|
||||
const SwitchProReport first =
|
||||
copy_switch_report(latest_regular_report(0));
|
||||
const SwitchProReport second =
|
||||
copy_switch_report(latest_regular_report(1));
|
||||
expect(first.inputs.buttonZL && !first.inputs.buttonZR,
|
||||
"instance 0 did not use its exact left/right trigger thresholds");
|
||||
expect(!second.inputs.buttonZL && second.inputs.buttonZR,
|
||||
"instance 1 trigger thresholds crossed HID contexts");
|
||||
}
|
||||
|
||||
void test_uart_parser_is_pure() {
|
||||
initialize_contexts();
|
||||
ControllerState driver_state{};
|
||||
driver_state.left_stick_x = driver_state.left_stick_y =
|
||||
driver_state.right_stick_x = driver_state.right_stick_y = 0;
|
||||
driver_state.button_north = true;
|
||||
switch_pro_set_input(0, driver_state);
|
||||
switch_pro_set_input(0, driver_state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 15;
|
||||
switch_pro_task(0);
|
||||
|
||||
|
|
@ -724,6 +762,7 @@ int main() {
|
|||
test_grip_colors_are_isolated();
|
||||
test_lifecycle_and_invalid_instances();
|
||||
test_protocol_neutral_trigger_threshold();
|
||||
test_custom_trigger_thresholds_are_isolated();
|
||||
test_uart_parser_is_pure();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " driver context test(s) failed\n";
|
||||
|
|
|
|||
|
|
@ -500,6 +500,13 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerIdentity.from_bytes(malformed_identity)
|
||||
|
||||
default_profile = config_manager.ControllerProfile.default()
|
||||
assert default_profile.left_trigger.digital_threshold == 22934
|
||||
assert default_profile.right_trigger.digital_threshold == 22934
|
||||
default_wire = default_profile.to_bytes()
|
||||
assert struct.unpack_from("<H", default_wire, 58)[0] == 22934
|
||||
assert struct.unpack_from("<H", default_wire, 68)[0] == 22934
|
||||
|
||||
profile = custom_profile()
|
||||
encoded = profile.to_bytes()
|
||||
assert len(encoded) == config_manager.PROFILE_SIZE
|
||||
|
|
@ -513,11 +520,143 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
|
||||
|
||||
serialized = profile.to_json()
|
||||
assert serialized.startswith('{\n "schema_version": 1,\n "size": 256,')
|
||||
assert serialized.startswith('{\n "schema_version": 2,\n "size": 256,')
|
||||
decoded = config_manager.ControllerProfile.from_json(serialized)
|
||||
assert decoded == profile
|
||||
assert decoded.to_json() == serialized
|
||||
|
||||
legacy_default_wire = bytearray(default_wire)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
legacy_default_wire,
|
||||
0,
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION,
|
||||
)
|
||||
struct.pack_into(
|
||||
"<HHHH",
|
||||
legacy_default_wire,
|
||||
52,
|
||||
30000,
|
||||
40000,
|
||||
256,
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
struct.pack_into(
|
||||
"<HHHH",
|
||||
legacy_default_wire,
|
||||
62,
|
||||
30000,
|
||||
40000,
|
||||
256,
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
migrated_default = config_manager.ControllerProfile.from_bytes(
|
||||
legacy_default_wire
|
||||
)
|
||||
assert (
|
||||
migrated_default.left_trigger.digital_threshold
|
||||
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
assert (
|
||||
migrated_default.right_trigger.digital_threshold
|
||||
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
assert migrated_default.left_trigger.lower_deadzone == 30000
|
||||
assert migrated_default.left_trigger.upper_saturation == 40000
|
||||
assert migrated_default.right_trigger.lower_deadzone == 30000
|
||||
assert migrated_default.right_trigger.upper_saturation == 40000
|
||||
assert migrated_default.to_bytes()[0] == config_manager.PROFILE_SCHEMA_VERSION
|
||||
|
||||
current_old_value_wire = bytearray(default_wire)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
current_old_value_wire,
|
||||
58,
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
assert (
|
||||
config_manager.ControllerProfile.from_bytes(
|
||||
current_old_value_wire
|
||||
).left_trigger.digital_threshold
|
||||
== config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
|
||||
legacy_custom_wire = bytearray(encoded)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
legacy_custom_wire,
|
||||
0,
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION,
|
||||
)
|
||||
assert (
|
||||
config_manager.ControllerProfile.from_bytes(legacy_custom_wire)
|
||||
== profile
|
||||
)
|
||||
|
||||
legacy_json_object = default_profile.to_json_object()
|
||||
legacy_json_object["schema_version"] = (
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION
|
||||
)
|
||||
legacy_json_object["triggers"]["left"]["digital_threshold"] = (
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
legacy_json_object["triggers"]["right"]["digital_threshold"] = 33000
|
||||
legacy_json_object["triggers"]["left"]["lower_deadzone"] = 30000
|
||||
legacy_json_object["triggers"]["left"]["upper_saturation"] = 40000
|
||||
migrated_json = config_manager.ControllerProfile.from_json_object(
|
||||
legacy_json_object
|
||||
)
|
||||
assert (
|
||||
migrated_json.left_trigger.digital_threshold
|
||||
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
assert migrated_json.right_trigger.digital_threshold == 33000
|
||||
assert migrated_json.left_trigger.lower_deadzone == 30000
|
||||
assert migrated_json.left_trigger.upper_saturation == 40000
|
||||
|
||||
|
||||
def test_trigger_threshold_uses_transformed_output_domain() -> None:
|
||||
for threshold in (0, 0xFFFF):
|
||||
trigger = config_manager.TriggerConfig(30000, 40000, 256, threshold)
|
||||
assert config_manager.TriggerConfig.from_bytes(trigger.to_bytes()) == trigger
|
||||
assert (
|
||||
config_manager.TriggerConfig.from_json_object(
|
||||
trigger.to_json_object(), "trigger"
|
||||
)
|
||||
== trigger
|
||||
)
|
||||
|
||||
current_wire = bytearray(
|
||||
config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
struct.pack_into(
|
||||
"<HHHH", current_wire, 52, 30000, 40000, 256, threshold
|
||||
)
|
||||
current_profile = config_manager.ControllerProfile.from_bytes(
|
||||
current_wire
|
||||
)
|
||||
assert current_profile.left_trigger.digital_threshold == threshold
|
||||
assert current_profile.to_bytes() == current_wire
|
||||
|
||||
for threshold in (-1, 0x10000):
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="trigger digital_threshold",
|
||||
):
|
||||
config_manager.TriggerConfig(30000, 40000, 256, threshold)
|
||||
|
||||
for lower_deadzone, upper_saturation in (
|
||||
(40000, 40000),
|
||||
(40001, 40000),
|
||||
):
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="lower_deadzone must be below upper_saturation",
|
||||
):
|
||||
config_manager.TriggerConfig(
|
||||
lower_deadzone, upper_saturation, 256, 0
|
||||
)
|
||||
|
||||
|
||||
def test_profile_list_select_read_and_chunked_commit() -> None:
|
||||
device = FakeDevice()
|
||||
|
|
|
|||
32
tests/test_controller_profile_runtime_native.py
Normal file
32
tests/test_controller_profile_runtime_native.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_controller_profile_runtime_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "controller_profile_runtime_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "controller_profile_runtime_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "controller_profile_transform.cpp"),
|
||||
str(root / "controller_profile_runtime.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
31
tests/test_controller_profile_transform_native.py
Normal file
31
tests/test_controller_profile_transform_native.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_controller_profile_transform_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "controller_profile_transform_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "controller_profile_transform_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "controller_profile_transform.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -279,7 +279,12 @@ void test_profile_vendor_requests() {
|
|||
control_payload.size() ==
|
||||
kResponseHeaderSize +
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
control_payload[kResponseHeaderSize] == 1 &&
|
||||
control_payload[10] ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
control_payload[kResponseHeaderSize] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION) &&
|
||||
control_payload[kResponseHeaderSize + 1] == 0 &&
|
||||
control_payload[kResponseHeaderSize + 2] == 0 &&
|
||||
control_payload[kResponseHeaderSize + 3] == 1,
|
||||
"selected profile response was not encoded");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue