Apply persistent controller profiles

This commit is contained in:
Joey Yakimowich-Payne 2026-09-02 17:35:13 -06:00
commit cfec816dac
25 changed files with 2179 additions and 96 deletions

View 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;
}