Apply persistent controller profiles
This commit is contained in:
parent
3f90d04a50
commit
cfec816dac
25 changed files with 2179 additions and 96 deletions
|
|
@ -101,6 +101,8 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
bluepad32_input_backend.cpp
|
||||
controller_identity.cpp
|
||||
controller_profile.cpp
|
||||
controller_profile_transform.cpp
|
||||
controller_profile_runtime.cpp
|
||||
profile_storage.cpp
|
||||
profile_service.cpp
|
||||
pico_profile_storage.cpp
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ constexpr uint8_t kMacroOverrideMask =
|
|||
kControllerProfileOverrideRightStick |
|
||||
kControllerProfileOverrideLeftTrigger |
|
||||
kControllerProfileOverrideRightTrigger;
|
||||
constexpr uint16_t kLegacyDefaultDigitalThreshold = 0x8000;
|
||||
|
||||
uint16_t profile_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
|
|
@ -144,7 +145,8 @@ ControllerProfile controller_profile_default(const ControllerIdentity& identity,
|
|||
trigger.lower_deadzone = 0;
|
||||
trigger.upper_saturation = UINT16_MAX;
|
||||
trigger.curve_q8_8 = 256;
|
||||
trigger.digital_threshold = 0x8000;
|
||||
trigger.digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
}
|
||||
profile.weak_rumble_scale = UINT8_MAX;
|
||||
profile.strong_rumble_scale = UINT8_MAX;
|
||||
|
|
@ -176,9 +178,7 @@ bool controller_profile_validate(const ControllerProfile& profile) {
|
|||
for (const ControllerProfileTriggerConfiguration& trigger :
|
||||
profile.triggers) {
|
||||
if (trigger.lower_deadzone >= trigger.upper_saturation ||
|
||||
trigger.curve_q8_8 == 0 ||
|
||||
trigger.digital_threshold < trigger.lower_deadzone ||
|
||||
trigger.digital_threshold > trigger.upper_saturation) {
|
||||
trigger.curve_q8_8 == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -272,13 +272,19 @@ bool controller_profile_encode(const ControllerProfile& profile,
|
|||
bool controller_profile_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerProfile* output) {
|
||||
if (input == nullptr || output == nullptr ||
|
||||
input_size != CONTROLLER_PROFILE_ENCODED_SIZE ||
|
||||
profile_read_u16(&input[0]) != CONTROLLER_PROFILE_SCHEMA_VERSION ||
|
||||
input_size != CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t schema_version = profile_read_u16(&input[0]);
|
||||
if ((schema_version != CONTROLLER_PROFILE_LEGACY_SCHEMA_VERSION &&
|
||||
schema_version != CONTROLLER_PROFILE_SCHEMA_VERSION) ||
|
||||
profile_read_u16(&input[2]) != CONTROLLER_PROFILE_ENCODED_SIZE ||
|
||||
!profile_bytes_are_zero(&input[31], 5) || !profile_bytes_are_zero(&input[47], 5) ||
|
||||
!profile_bytes_are_zero(&input[60], 2) || !profile_bytes_are_zero(&input[70], 2) ||
|
||||
input[75] != 0 || input[81] != 0 ||
|
||||
!profile_bytes_are_zero(&input[98], 2) || !profile_bytes_are_zero(&input[252], 4)) {
|
||||
!profile_bytes_are_zero(&input[31], 5) ||
|
||||
!profile_bytes_are_zero(&input[47], 5) ||
|
||||
!profile_bytes_are_zero(&input[60], 2) ||
|
||||
!profile_bytes_are_zero(&input[70], 2) || input[75] != 0 ||
|
||||
input[81] != 0 || !profile_bytes_are_zero(&input[98], 2) ||
|
||||
!profile_bytes_are_zero(&input[252], 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -307,6 +313,11 @@ bool controller_profile_decode(const uint8_t* input, size_t input_size,
|
|||
trigger.upper_saturation = profile_read_u16(&encoded[2]);
|
||||
trigger.curve_q8_8 = profile_read_u16(&encoded[4]);
|
||||
trigger.digital_threshold = profile_read_u16(&encoded[6]);
|
||||
if (schema_version == CONTROLLER_PROFILE_LEGACY_SCHEMA_VERSION &&
|
||||
trigger.digital_threshold == kLegacyDefaultDigitalThreshold) {
|
||||
trigger.digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
}
|
||||
}
|
||||
profile.weak_rumble_scale = input[72];
|
||||
profile.strong_rumble_scale = input[73];
|
||||
|
|
@ -489,11 +500,16 @@ bool controller_profile_database_decode(
|
|||
return false;
|
||||
}
|
||||
uint8_t header[CONTROLLER_PROFILE_DATABASE_HEADER_SIZE]{};
|
||||
if (!read(context, 0, header, sizeof(header)) ||
|
||||
memcmp(header, kDatabaseMagic, sizeof(kDatabaseMagic)) != 0 ||
|
||||
profile_read_u16(&header[4]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION ||
|
||||
profile_read_u16(&header[6]) != CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
if (!read(context, 0, header, sizeof(header))) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t schema_version = profile_read_u16(&header[4]);
|
||||
if (memcmp(header, kDatabaseMagic, sizeof(kDatabaseMagic)) != 0 ||
|
||||
(schema_version !=
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
|
||||
schema_version != CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION) ||
|
||||
profile_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header[8] != CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY ||
|
||||
header[9] != CONTROLLER_PROFILE_COUNT ||
|
||||
header[10] >= CONTROLLER_PROFILE_COUNT ||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
#include "controller_identity.h"
|
||||
|
||||
constexpr uint16_t CONTROLLER_PROFILE_SCHEMA_VERSION = 1;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_LEGACY_SCHEMA_VERSION = 1;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_SCHEMA_VERSION = 2;
|
||||
constexpr size_t CONTROLLER_PROFILE_ENCODED_SIZE = 256;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_COUNT = 4;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT = 16;
|
||||
|
|
@ -13,9 +14,12 @@ constexpr uint8_t CONTROLLER_PROFILE_MACRO_STEP_CAPACITY = 8;
|
|||
constexpr uint16_t CONTROLLER_PROFILE_MAX_WAIT_MS = 10000;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_NO_BUTTON = 0xff;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_ALL = 0xff;
|
||||
// Exact 16-bit counterpart of the existing 358-of-1023 Switch boundary.
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD = 22934;
|
||||
|
||||
constexpr uint8_t CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY = 16;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION = 1;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION = 1;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION = 2;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_HEADER_SIZE = 32;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE = 16;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE =
|
||||
|
|
@ -92,7 +96,9 @@ struct ControllerProfileTriggerConfiguration {
|
|||
uint16_t lower_deadzone = 0;
|
||||
uint16_t upper_saturation = UINT16_MAX;
|
||||
uint16_t curve_q8_8 = 256;
|
||||
uint16_t digital_threshold = 0x8000;
|
||||
// Compared against the transformed uint16 trigger output.
|
||||
uint16_t digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
};
|
||||
|
||||
struct ControllerProfileMacroStep {
|
||||
|
|
|
|||
131
controller_profile_runtime.cpp
Normal file
131
controller_profile_runtime.cpp
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#include "controller_profile_runtime.h"
|
||||
|
||||
#include "controller_identity.h"
|
||||
#include "profile_service.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct ControllerProfileRuntimeContext {
|
||||
bool active = false;
|
||||
uint32_t connection_generation = 0;
|
||||
ControllerIdentity identity{};
|
||||
uint32_t database_generation = 0;
|
||||
uint8_t active_profile_index = 0;
|
||||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
ControllerProfileRuntimeContext
|
||||
g_contexts[CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT]{};
|
||||
ControllerProfile g_default_profile{};
|
||||
ControllerProfileTransformResult g_neutral_output{};
|
||||
bool g_initialized = false;
|
||||
|
||||
void initialize_defaults() {
|
||||
if (g_initialized) {
|
||||
return;
|
||||
}
|
||||
g_default_profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
g_neutral_output = controller_profile_transform(
|
||||
controller_neutral_state(), g_default_profile);
|
||||
g_initialized = true;
|
||||
}
|
||||
|
||||
void clear_context(ControllerProfileRuntimeContext* context) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
*context = {};
|
||||
}
|
||||
|
||||
void refresh_profile(ControllerProfileRuntimeContext* context,
|
||||
const ControllerIdentity& identity,
|
||||
uint32_t connection_generation,
|
||||
uint32_t observed_database_generation) {
|
||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||
|
||||
context->active = true;
|
||||
context->connection_generation = connection_generation;
|
||||
context->identity = identity;
|
||||
context->database_generation = snapshot.valid
|
||||
? snapshot.metadata.generation
|
||||
: observed_database_generation;
|
||||
context->active_profile_index = snapshot.valid ? snapshot.profile_index : 0;
|
||||
context->profile = snapshot.valid ? snapshot.profile : g_default_profile;
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeContext* update_context(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot) {
|
||||
if (slot >= CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeContext& context = g_contexts[slot];
|
||||
if (!snapshot.active) {
|
||||
if (context.active) {
|
||||
clear_context(&context);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint32_t database_generation =
|
||||
profile_service_database_generation();
|
||||
if (!context.active ||
|
||||
context.connection_generation != snapshot.connection_generation ||
|
||||
!controller_identity_equal(context.identity, snapshot.identity) ||
|
||||
context.database_generation != database_generation) {
|
||||
refresh_profile(&context, snapshot.identity,
|
||||
snapshot.connection_generation,
|
||||
database_generation);
|
||||
}
|
||||
return &context;
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
void controller_profile_runtime_reset() {
|
||||
g_initialized = false;
|
||||
initialize_defaults();
|
||||
for (ControllerProfileRuntimeContext& context : g_contexts) {
|
||||
clear_context(&context);
|
||||
}
|
||||
}
|
||||
|
||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot) {
|
||||
initialize_defaults();
|
||||
ControllerProfileRuntimeContext* context =
|
||||
update_context(slot, snapshot);
|
||||
if (context == nullptr) {
|
||||
return g_neutral_output;
|
||||
}
|
||||
return controller_profile_transform(snapshot.state, context->profile);
|
||||
}
|
||||
|
||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble) {
|
||||
initialize_defaults();
|
||||
ControllerProfileRuntimeContext* context =
|
||||
update_context(slot, snapshot);
|
||||
const ControllerProfile& profile =
|
||||
context == nullptr ? g_default_profile : context->profile;
|
||||
return controller_profile_scale_host_rumble(rumble, profile);
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeLocalConfirmation
|
||||
controller_profile_runtime_local_confirmation(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble) {
|
||||
initialize_defaults();
|
||||
ControllerProfileRuntimeContext* context =
|
||||
update_context(slot, snapshot);
|
||||
const ControllerProfile& profile =
|
||||
context == nullptr ? g_default_profile : context->profile;
|
||||
return {
|
||||
rumble,
|
||||
controller_profile_confirmation_policy(profile),
|
||||
};
|
||||
}
|
||||
34
controller_profile_runtime.h
Normal file
34
controller_profile_runtime.h
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "controller_profile_transform.h"
|
||||
|
||||
constexpr uint8_t CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT = 4;
|
||||
|
||||
struct ControllerProfileRuntimeLocalConfirmation {
|
||||
ControllerRumbleOutput rumble{};
|
||||
ControllerProfileConfirmationPolicy policy =
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
};
|
||||
|
||||
// Reset all four fixed slot caches to the default profile.
|
||||
void controller_profile_runtime_reset();
|
||||
|
||||
// Refresh a slot only when its connection key or database generation changes,
|
||||
// then transform the raw snapshot. Inactive snapshots return neutral output and
|
||||
// invalidate the slot immediately.
|
||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot);
|
||||
|
||||
// Refresh from the current slot snapshot and scale host-originated rumble.
|
||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble);
|
||||
|
||||
// Preserve local confirmation rumble exactly while exposing profile policy.
|
||||
ControllerProfileRuntimeLocalConfirmation
|
||||
controller_profile_runtime_local_confirmation(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble);
|
||||
344
controller_profile_transform.cpp
Normal file
344
controller_profile_transform.cpp
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
#include "controller_profile_transform.h"
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kQ16One = 1u << 16;
|
||||
|
||||
constexpr uint16_t button_bit(ControllerProfileLogicalButton button) {
|
||||
return static_cast<uint16_t>(
|
||||
1u << static_cast<uint8_t>(button));
|
||||
}
|
||||
|
||||
int32_t clamp_centered_axis(int16_t value, int16_t center) {
|
||||
const int32_t adjusted =
|
||||
static_cast<int32_t>(value) - static_cast<int32_t>(center);
|
||||
if (adjusted < INT16_MIN) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
if (adjusted > INT16_MAX) {
|
||||
return INT16_MAX;
|
||||
}
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
uint32_t absolute_axis(int32_t value) {
|
||||
return value < 0 ? static_cast<uint32_t>(-value)
|
||||
: static_cast<uint32_t>(value);
|
||||
}
|
||||
|
||||
// This endpoint-preserving rational curve is x / (c + (1 - c) * x),
|
||||
// evaluated with x in Q16 and c in Q8.8. Values above 1.0 reduce the
|
||||
// response below the linear curve; values below 1.0 increase it.
|
||||
uint32_t apply_curve_q16(uint32_t input_q16, uint16_t curve_q8_8) {
|
||||
if (input_q16 == 0 || input_q16 == kQ16One || curve_q8_8 == 256) {
|
||||
return input_q16;
|
||||
}
|
||||
|
||||
const uint64_t denominator =
|
||||
static_cast<uint64_t>(curve_q8_8) *
|
||||
(kQ16One - input_q16) +
|
||||
static_cast<uint64_t>(256) * input_q16;
|
||||
const uint64_t numerator =
|
||||
static_cast<uint64_t>(input_q16) * 256u * kQ16One;
|
||||
const uint64_t curved = (numerator + denominator / 2u) / denominator;
|
||||
return curved > kQ16One ? kQ16One
|
||||
: static_cast<uint32_t>(curved);
|
||||
}
|
||||
|
||||
bool is_default_stick_configuration(
|
||||
const ControllerProfileStickConfiguration& configuration) {
|
||||
return configuration.center_x == 0 && configuration.center_y == 0 &&
|
||||
configuration.inner_deadzone == 0 &&
|
||||
configuration.outer_saturation == 32767 &&
|
||||
configuration.curve_q8_8 == 256 && !configuration.invert_x &&
|
||||
!configuration.invert_y;
|
||||
}
|
||||
|
||||
bool is_default_trigger_configuration(
|
||||
const ControllerProfileTriggerConfiguration& configuration) {
|
||||
return configuration.lower_deadzone == 0 &&
|
||||
configuration.upper_saturation == UINT16_MAX &&
|
||||
configuration.curve_q8_8 == 256;
|
||||
}
|
||||
|
||||
int16_t scale_stick_axis(int32_t adjusted_axis, uint32_t magnitude,
|
||||
uint32_t response_q16, bool invert) {
|
||||
if (adjusted_axis == 0 || magnitude == 0 || response_q16 == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool negative = adjusted_axis < 0;
|
||||
if (invert) {
|
||||
negative = !negative;
|
||||
}
|
||||
const uint32_t axis_magnitude = absolute_axis(adjusted_axis);
|
||||
const uint32_t output_limit =
|
||||
negative ? static_cast<uint32_t>(-static_cast<int32_t>(INT16_MIN))
|
||||
: static_cast<uint32_t>(INT16_MAX);
|
||||
const uint64_t numerator =
|
||||
static_cast<uint64_t>(axis_magnitude) * output_limit *
|
||||
response_q16;
|
||||
const uint64_t denominator =
|
||||
static_cast<uint64_t>(magnitude) * kQ16One;
|
||||
uint32_t output_magnitude = static_cast<uint32_t>(
|
||||
(numerator + denominator / 2u) / denominator);
|
||||
if (output_magnitude > output_limit) {
|
||||
output_magnitude = output_limit;
|
||||
}
|
||||
|
||||
if (!negative) {
|
||||
return static_cast<int16_t>(output_magnitude);
|
||||
}
|
||||
if (output_magnitude ==
|
||||
static_cast<uint32_t>(-static_cast<int32_t>(INT16_MIN))) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
return static_cast<int16_t>(-static_cast<int32_t>(output_magnitude));
|
||||
}
|
||||
|
||||
void transform_stick(const ControllerProfileStickConfiguration& configuration,
|
||||
int16_t input_x, int16_t input_y, int16_t* output_x,
|
||||
int16_t* output_y) {
|
||||
if (is_default_stick_configuration(configuration)) {
|
||||
*output_x = input_x;
|
||||
*output_y = input_y;
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t adjusted_x =
|
||||
clamp_centered_axis(input_x, configuration.center_x);
|
||||
const int32_t adjusted_y =
|
||||
clamp_centered_axis(input_y, configuration.center_y);
|
||||
const uint32_t magnitude_x = absolute_axis(adjusted_x);
|
||||
const uint32_t magnitude_y = absolute_axis(adjusted_y);
|
||||
const uint32_t magnitude =
|
||||
magnitude_x > magnitude_y ? magnitude_x : magnitude_y;
|
||||
|
||||
uint32_t response_q16 = 0;
|
||||
if (magnitude <= configuration.inner_deadzone) {
|
||||
response_q16 = 0;
|
||||
} else if (magnitude >= configuration.outer_saturation) {
|
||||
response_q16 = kQ16One;
|
||||
} else {
|
||||
const uint32_t input_range =
|
||||
static_cast<uint32_t>(configuration.outer_saturation) -
|
||||
configuration.inner_deadzone;
|
||||
const uint32_t input_offset =
|
||||
magnitude - configuration.inner_deadzone;
|
||||
const uint32_t normalized_q16 = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(input_offset) * kQ16One +
|
||||
input_range / 2u) /
|
||||
input_range);
|
||||
response_q16 =
|
||||
apply_curve_q16(normalized_q16, configuration.curve_q8_8);
|
||||
}
|
||||
|
||||
*output_x = scale_stick_axis(adjusted_x, magnitude, response_q16,
|
||||
configuration.invert_x);
|
||||
*output_y = scale_stick_axis(adjusted_y, magnitude, response_q16,
|
||||
configuration.invert_y);
|
||||
}
|
||||
|
||||
uint16_t transform_trigger(
|
||||
uint16_t input,
|
||||
const ControllerProfileTriggerConfiguration& configuration) {
|
||||
if (is_default_trigger_configuration(configuration)) {
|
||||
return input;
|
||||
}
|
||||
if (input <= configuration.lower_deadzone) {
|
||||
return 0;
|
||||
}
|
||||
if (input >= configuration.upper_saturation) {
|
||||
return UINT16_MAX;
|
||||
}
|
||||
|
||||
const uint32_t input_range =
|
||||
static_cast<uint32_t>(configuration.upper_saturation) -
|
||||
configuration.lower_deadzone;
|
||||
const uint32_t input_offset =
|
||||
static_cast<uint32_t>(input) - configuration.lower_deadzone;
|
||||
if (configuration.curve_q8_8 == 256) {
|
||||
return static_cast<uint16_t>(
|
||||
(static_cast<uint64_t>(input_offset) * UINT16_MAX +
|
||||
input_range / 2u) /
|
||||
input_range);
|
||||
}
|
||||
|
||||
const uint32_t normalized_q16 = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(input_offset) * kQ16One +
|
||||
input_range / 2u) /
|
||||
input_range);
|
||||
const uint32_t curved_q16 =
|
||||
apply_curve_q16(normalized_q16, configuration.curve_q8_8);
|
||||
return static_cast<uint16_t>(
|
||||
(static_cast<uint64_t>(curved_q16) * UINT16_MAX +
|
||||
kQ16One / 2u) /
|
||||
kQ16One);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint16_t controller_profile_extract_button_mask(const ControllerState& state) {
|
||||
uint16_t mask = 0;
|
||||
mask |= state.button_south
|
||||
? button_bit(ControllerProfileLogicalButton::kSouth)
|
||||
: 0;
|
||||
mask |= state.button_east
|
||||
? button_bit(ControllerProfileLogicalButton::kEast)
|
||||
: 0;
|
||||
mask |= state.button_west
|
||||
? button_bit(ControllerProfileLogicalButton::kWest)
|
||||
: 0;
|
||||
mask |= state.button_north
|
||||
? button_bit(ControllerProfileLogicalButton::kNorth)
|
||||
: 0;
|
||||
mask |= state.button_left_shoulder
|
||||
? button_bit(ControllerProfileLogicalButton::kLeftShoulder)
|
||||
: 0;
|
||||
mask |= state.button_right_shoulder
|
||||
? button_bit(ControllerProfileLogicalButton::kRightShoulder)
|
||||
: 0;
|
||||
mask |= state.button_select
|
||||
? button_bit(ControllerProfileLogicalButton::kSelect)
|
||||
: 0;
|
||||
mask |= state.button_start
|
||||
? button_bit(ControllerProfileLogicalButton::kStart)
|
||||
: 0;
|
||||
mask |= state.button_system
|
||||
? button_bit(ControllerProfileLogicalButton::kSystem)
|
||||
: 0;
|
||||
mask |= state.button_capture
|
||||
? button_bit(ControllerProfileLogicalButton::kCapture)
|
||||
: 0;
|
||||
mask |= state.button_left_stick
|
||||
? button_bit(ControllerProfileLogicalButton::kLeftStick)
|
||||
: 0;
|
||||
mask |= state.button_right_stick
|
||||
? button_bit(ControllerProfileLogicalButton::kRightStick)
|
||||
: 0;
|
||||
mask |= state.dpad_up
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadUp)
|
||||
: 0;
|
||||
mask |= state.dpad_down
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadDown)
|
||||
: 0;
|
||||
mask |= state.dpad_left
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadLeft)
|
||||
: 0;
|
||||
mask |= state.dpad_right
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadRight)
|
||||
: 0;
|
||||
return mask;
|
||||
}
|
||||
|
||||
void controller_profile_apply_button_mask(uint16_t button_mask,
|
||||
ControllerState* state) {
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
state->button_south =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kSouth)) != 0;
|
||||
state->button_east =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kEast)) != 0;
|
||||
state->button_west =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kWest)) != 0;
|
||||
state->button_north =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kNorth)) != 0;
|
||||
state->button_left_shoulder =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kLeftShoulder)) != 0;
|
||||
state->button_right_shoulder =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kRightShoulder)) != 0;
|
||||
state->button_select =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kSelect)) != 0;
|
||||
state->button_start =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kStart)) != 0;
|
||||
state->button_system =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kSystem)) != 0;
|
||||
state->button_capture =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kCapture)) != 0;
|
||||
state->button_left_stick =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kLeftStick)) != 0;
|
||||
state->button_right_stick =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kRightStick)) != 0;
|
||||
state->dpad_up =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadUp)) != 0;
|
||||
state->dpad_down =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadDown)) != 0;
|
||||
state->dpad_left =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadLeft)) != 0;
|
||||
state->dpad_right =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadRight)) != 0;
|
||||
}
|
||||
|
||||
uint16_t controller_profile_map_button_mask(
|
||||
uint16_t input_button_mask, const ControllerProfile& profile) {
|
||||
uint16_t output_button_mask = 0;
|
||||
for (uint8_t input = 0;
|
||||
input < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++input) {
|
||||
if ((input_button_mask & static_cast<uint16_t>(1u << input)) == 0) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t output = profile.button_map[input];
|
||||
if (output < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT) {
|
||||
output_button_mask |= static_cast<uint16_t>(1u << output);
|
||||
}
|
||||
}
|
||||
return output_button_mask;
|
||||
}
|
||||
|
||||
ControllerProfileTransformResult controller_profile_transform(
|
||||
const ControllerState& input, const ControllerProfile& profile) {
|
||||
ControllerProfileTransformResult result{};
|
||||
result.state = input;
|
||||
const uint16_t input_button_mask =
|
||||
controller_profile_extract_button_mask(input);
|
||||
controller_profile_apply_button_mask(
|
||||
controller_profile_map_button_mask(input_button_mask, profile),
|
||||
&result.state);
|
||||
|
||||
transform_stick(profile.sticks[0], input.left_stick_x,
|
||||
input.left_stick_y, &result.state.left_stick_x,
|
||||
&result.state.left_stick_y);
|
||||
transform_stick(profile.sticks[1], input.right_stick_x,
|
||||
input.right_stick_y, &result.state.right_stick_x,
|
||||
&result.state.right_stick_y);
|
||||
result.state.left_trigger = transform_trigger(input.left_trigger,
|
||||
profile.triggers[0]);
|
||||
result.state.right_trigger = transform_trigger(input.right_trigger,
|
||||
profile.triggers[1]);
|
||||
result.left_trigger_digital_threshold =
|
||||
profile.triggers[0].digital_threshold;
|
||||
result.right_trigger_digital_threshold =
|
||||
profile.triggers[1].digital_threshold;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t controller_profile_scale_rumble_magnitude(uint8_t magnitude,
|
||||
uint8_t scale) {
|
||||
const uint32_t scaled =
|
||||
(static_cast<uint32_t>(magnitude) * scale + UINT8_MAX / 2u) /
|
||||
UINT8_MAX;
|
||||
return scaled > UINT8_MAX ? UINT8_MAX
|
||||
: static_cast<uint8_t>(scaled);
|
||||
}
|
||||
|
||||
ControllerRumbleOutput controller_profile_scale_host_rumble(
|
||||
const ControllerRumbleOutput& input, const ControllerProfile& profile) {
|
||||
return {
|
||||
controller_profile_scale_rumble_magnitude(
|
||||
input.low_frequency_magnitude, profile.strong_rumble_scale),
|
||||
controller_profile_scale_rumble_magnitude(
|
||||
input.high_frequency_magnitude, profile.weak_rumble_scale),
|
||||
};
|
||||
}
|
||||
|
||||
ControllerProfileConfirmationPolicy controller_profile_confirmation_policy(
|
||||
const ControllerProfile& profile) {
|
||||
return profile.confirmation_policy;
|
||||
}
|
||||
31
controller_profile_transform.h
Normal file
31
controller_profile_transform.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_profile.h"
|
||||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
struct ControllerProfileTransformResult {
|
||||
ControllerState state{};
|
||||
uint16_t left_trigger_digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
uint16_t right_trigger_digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
};
|
||||
|
||||
uint16_t controller_profile_extract_button_mask(const ControllerState& state);
|
||||
void controller_profile_apply_button_mask(uint16_t button_mask,
|
||||
ControllerState* state);
|
||||
uint16_t controller_profile_map_button_mask(
|
||||
uint16_t input_button_mask, const ControllerProfile& profile);
|
||||
|
||||
ControllerProfileTransformResult controller_profile_transform(
|
||||
const ControllerState& input, const ControllerProfile& profile);
|
||||
|
||||
uint8_t controller_profile_scale_rumble_magnitude(uint8_t magnitude,
|
||||
uint8_t scale);
|
||||
ControllerRumbleOutput controller_profile_scale_host_rumble(
|
||||
const ControllerRumbleOutput& input, const ControllerProfile& profile);
|
||||
ControllerProfileConfirmationPolicy controller_profile_confirmation_policy(
|
||||
const ControllerProfile& profile);
|
||||
|
|
@ -29,13 +29,23 @@ struct ProfileTransaction {
|
|||
uint8_t payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
};
|
||||
|
||||
struct PublishedActiveProfile {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
critical_section_t g_lock;
|
||||
bool g_prepared = false;
|
||||
ProfileStorage g_storage;
|
||||
ControllerProfileDatabase g_database;
|
||||
ProfileServiceMetadata g_metadata;
|
||||
uint32_t g_published_generation = 0;
|
||||
ProfileServiceListSnapshot g_list;
|
||||
ProfileServiceSelectedSnapshot g_selected;
|
||||
PublishedActiveProfile
|
||||
g_active_profiles[PROFILE_SERVICE_LIST_CAPACITY]{};
|
||||
uint8_t g_active_profile_count = 0;
|
||||
ProfileTransaction g_transaction;
|
||||
PendingCommand g_command;
|
||||
bool g_identity_dirty = false;
|
||||
|
|
@ -65,6 +75,26 @@ void refresh_list_locked() {
|
|||
}
|
||||
}
|
||||
|
||||
void refresh_active_profiles_locked() {
|
||||
g_active_profile_count = 1;
|
||||
g_active_profiles[0].identity = controller_identity_global();
|
||||
g_active_profiles[0].profile_index =
|
||||
g_database.fallback_active_profile;
|
||||
g_active_profiles[0].profile =
|
||||
g_database.fallback_profiles[g_database.fallback_active_profile];
|
||||
for (const ControllerProfileDatabaseEntry& entry : g_database.entries) {
|
||||
if (!entry.used ||
|
||||
g_active_profile_count >= PROFILE_SERVICE_LIST_CAPACITY) {
|
||||
continue;
|
||||
}
|
||||
PublishedActiveProfile& active =
|
||||
g_active_profiles[g_active_profile_count++];
|
||||
active.identity = entry.identity;
|
||||
active.profile_index = entry.active_profile;
|
||||
active.profile = entry.profiles[entry.active_profile];
|
||||
}
|
||||
}
|
||||
|
||||
void refresh_selected_locked() {
|
||||
g_selected.metadata = g_metadata;
|
||||
const ControllerProfile* profile = controller_profile_database_get(
|
||||
|
|
@ -84,8 +114,11 @@ void refresh_metadata_locked(ProfileServiceState state) {
|
|||
g_metadata.state = state;
|
||||
g_metadata.generation = stored.valid ? stored.generation : 0;
|
||||
g_metadata.payload_crc = stored.valid ? stored.payload_crc : 0;
|
||||
refresh_active_profiles_locked();
|
||||
refresh_list_locked();
|
||||
refresh_selected_locked();
|
||||
__atomic_store_n(&g_published_generation, g_metadata.generation,
|
||||
__ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus database_result_status(
|
||||
|
|
@ -134,8 +167,11 @@ void profile_service_prepare() {
|
|||
}
|
||||
critical_section_init(&g_lock);
|
||||
g_metadata = {};
|
||||
__atomic_store_n(&g_published_generation, 0, __ATOMIC_RELAXED);
|
||||
g_list = {};
|
||||
g_selected = {};
|
||||
g_active_profiles[0] = {};
|
||||
g_active_profile_count = 0;
|
||||
g_selected.identity = controller_identity_global();
|
||||
g_selected.profile_index = 0;
|
||||
g_transaction = {};
|
||||
|
|
@ -522,27 +558,36 @@ void profile_service_transaction_snapshot(
|
|||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
bool profile_service_active_profile(const ControllerIdentity& identity,
|
||||
ControllerProfile* output,
|
||||
uint8_t* profile_index) {
|
||||
if (output == nullptr || profile_index == nullptr ||
|
||||
!valid_identity(identity)) {
|
||||
return false;
|
||||
uint32_t profile_service_database_generation() {
|
||||
return __atomic_load_n(&g_published_generation, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity,
|
||||
ProfileServiceActiveProfileSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
*output = {};
|
||||
if (!valid_identity(identity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_metadata.state != ProfileServiceState::kReady) {
|
||||
critical_section_exit(&g_lock);
|
||||
return false;
|
||||
}
|
||||
const ControllerProfileDatabaseEntry* entry =
|
||||
controller_profile_database_find(g_database, identity);
|
||||
if (entry != nullptr) {
|
||||
*profile_index = entry->active_profile;
|
||||
*output = entry->profiles[entry->active_profile];
|
||||
} else {
|
||||
*profile_index = g_database.fallback_active_profile;
|
||||
*output = g_database.fallback_profiles[*profile_index];
|
||||
output->metadata = g_metadata;
|
||||
if (g_metadata.state == ProfileServiceState::kReady &&
|
||||
g_active_profile_count != 0) {
|
||||
const PublishedActiveProfile* active = &g_active_profiles[0];
|
||||
for (uint8_t index = 1; index < g_active_profile_count; ++index) {
|
||||
if (controller_identity_equal(
|
||||
g_active_profiles[index].identity, identity)) {
|
||||
active = &g_active_profiles[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
output->profile_index = active->profile_index;
|
||||
output->profile = active->profile;
|
||||
output->valid = true;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ struct ProfileServiceSelectedSnapshot {
|
|||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
struct ProfileServiceActiveProfileSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
bool valid = false;
|
||||
uint8_t profile_index = 0;
|
||||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
struct ProfileServiceTransactionSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
ControllerIdentity identity{};
|
||||
|
|
@ -78,6 +85,7 @@ void profile_service_selected_snapshot(
|
|||
ProfileServiceSelectedSnapshot* output);
|
||||
void profile_service_transaction_snapshot(
|
||||
ProfileServiceTransactionSnapshot* output);
|
||||
bool profile_service_active_profile(const ControllerIdentity& identity,
|
||||
ControllerProfile* output,
|
||||
uint8_t* profile_index);
|
||||
uint32_t profile_service_database_generation();
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity,
|
||||
ProfileServiceActiveProfileSnapshot* output);
|
||||
|
|
|
|||
|
|
@ -236,8 +236,10 @@ bool ProfileStorage::read_header(uint8_t bank, BankHeader* output) const {
|
|||
!io_.read(io_.context, bank, 0, header, sizeof(header)) ||
|
||||
memcmp(header, kRecordMagic, sizeof(kRecordMagic)) != 0 ||
|
||||
storage_read_u16(&header[4]) != kRecordFormatVersion ||
|
||||
storage_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION ||
|
||||
(storage_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
|
||||
storage_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION) ||
|
||||
storage_read_u32(&header[12]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
profile_storage_crc32(header, kHeaderCrcOffset) !=
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ PAIRING_RECORD_CAPACITY = 16
|
|||
TRANSPORT_UNKNOWN = 0
|
||||
TRANSPORT_CLASSIC = 1
|
||||
TRANSPORT_BLE = 2
|
||||
PROFILE_SCHEMA_VERSION = 1
|
||||
PROFILE_LEGACY_SCHEMA_VERSION = 1
|
||||
PROFILE_SCHEMA_VERSION = 2
|
||||
PROFILE_SIZE = 256
|
||||
PROFILE_CAPACITY = 4
|
||||
PROFILE_IDENTITY_CAPACITY = 16
|
||||
|
|
@ -80,6 +81,8 @@ PROFILE_NONE_BUTTON = 0xFF
|
|||
PROFILE_MACRO_STEP_CAPACITY = 8
|
||||
PROFILE_MACRO_STEP_SIZE = 19
|
||||
PROFILE_MAXIMUM_WAIT_MS = 10000
|
||||
PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD = 0x8000
|
||||
PROFILE_DEFAULT_DIGITAL_THRESHOLD = 22934
|
||||
|
||||
LOGICAL_BUTTONS = (
|
||||
"south",
|
||||
|
|
@ -513,10 +516,7 @@ class TriggerConfig:
|
|||
)
|
||||
_require_int(self.curve_q8_8, "trigger curve_q8_8", 1, 0xFFFF)
|
||||
_require_int(
|
||||
self.digital_threshold,
|
||||
"trigger digital_threshold",
|
||||
self.lower_deadzone,
|
||||
self.upper_saturation,
|
||||
self.digital_threshold, "trigger digital_threshold", 0, 0xFFFF
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -575,6 +575,19 @@ class TriggerConfig:
|
|||
),
|
||||
)
|
||||
|
||||
def _migrate_legacy_trigger_threshold(trigger: TriggerConfig) -> TriggerConfig:
|
||||
if (
|
||||
trigger.digital_threshold
|
||||
!= PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
|
||||
):
|
||||
return trigger
|
||||
return TriggerConfig(
|
||||
trigger.lower_deadzone,
|
||||
trigger.upper_saturation,
|
||||
trigger.curve_q8_8,
|
||||
PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MacroStep:
|
||||
|
|
@ -866,7 +879,9 @@ class ControllerProfile:
|
|||
@classmethod
|
||||
def default(cls) -> ControllerProfile:
|
||||
stick = StickConfig(0, 0, 0, 0x7FFF, 256, False, False)
|
||||
trigger = TriggerConfig(0, 0xFFFF, 256, 0x8000)
|
||||
trigger = TriggerConfig(
|
||||
0, 0xFFFF, 256, PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
return cls(
|
||||
button_map=tuple(range(len(LOGICAL_BUTTONS))),
|
||||
left_stick=stick,
|
||||
|
|
@ -889,7 +904,11 @@ class ControllerProfile:
|
|||
if len(payload) != PROFILE_SIZE:
|
||||
raise ConfigManagerError("invalid profile size")
|
||||
version, size = struct.unpack_from("<HH", payload)
|
||||
if version != PROFILE_SCHEMA_VERSION or size != PROFILE_SIZE:
|
||||
if (
|
||||
version
|
||||
not in (PROFILE_LEGACY_SCHEMA_VERSION, PROFILE_SCHEMA_VERSION)
|
||||
or size != PROFILE_SIZE
|
||||
):
|
||||
raise ConfigManagerError("unsupported profile schema")
|
||||
if payload[75] != 0 or payload[81] != 0:
|
||||
raise ConfigManagerError("profile reserved fields must be zero")
|
||||
|
|
@ -912,12 +931,17 @@ class ControllerProfile:
|
|||
step != MacroStep.end() for step in all_steps[macro_count:]
|
||||
):
|
||||
raise ConfigManagerError("unused macro steps must be canonical end")
|
||||
left_trigger = TriggerConfig.from_bytes(payload[52:62])
|
||||
right_trigger = TriggerConfig.from_bytes(payload[62:72])
|
||||
if version == PROFILE_LEGACY_SCHEMA_VERSION:
|
||||
left_trigger = _migrate_legacy_trigger_threshold(left_trigger)
|
||||
right_trigger = _migrate_legacy_trigger_threshold(right_trigger)
|
||||
return cls(
|
||||
button_map=tuple(payload[4:20]),
|
||||
left_stick=StickConfig.from_bytes(payload[20:36]),
|
||||
right_stick=StickConfig.from_bytes(payload[36:52]),
|
||||
left_trigger=TriggerConfig.from_bytes(payload[52:62]),
|
||||
right_trigger=TriggerConfig.from_bytes(payload[62:72]),
|
||||
left_trigger=left_trigger,
|
||||
right_trigger=right_trigger,
|
||||
weak_rumble_scale=payload[72],
|
||||
strong_rumble_scale=payload[73],
|
||||
confirmation_policy=payload[74],
|
||||
|
|
@ -1020,11 +1044,12 @@ class ControllerProfile:
|
|||
"turbo",
|
||||
)
|
||||
obj = _require_object(value, fields, "profile")
|
||||
schema_version = _require_int(
|
||||
obj["schema_version"], "profile.schema_version", 0, 0xFFFF
|
||||
)
|
||||
if (
|
||||
_require_int(
|
||||
obj["schema_version"], "profile.schema_version", 0, 0xFFFF
|
||||
)
|
||||
!= PROFILE_SCHEMA_VERSION
|
||||
schema_version
|
||||
not in (PROFILE_LEGACY_SCHEMA_VERSION, PROFILE_SCHEMA_VERSION)
|
||||
or _require_int(obj["size"], "profile.size", 0, 0xFFFF)
|
||||
!= PROFILE_SIZE
|
||||
):
|
||||
|
|
@ -1056,6 +1081,15 @@ class ControllerProfile:
|
|||
raise ConfigManagerError(
|
||||
"profile.macro.steps must contain one to eight steps"
|
||||
)
|
||||
left_trigger = TriggerConfig.from_json_object(
|
||||
triggers["left"], "profile.triggers.left"
|
||||
)
|
||||
right_trigger = TriggerConfig.from_json_object(
|
||||
triggers["right"], "profile.triggers.right"
|
||||
)
|
||||
if schema_version == PROFILE_LEGACY_SCHEMA_VERSION:
|
||||
left_trigger = _migrate_legacy_trigger_threshold(left_trigger)
|
||||
right_trigger = _migrate_legacy_trigger_threshold(right_trigger)
|
||||
return cls(
|
||||
button_map=tuple(
|
||||
_button_index(
|
||||
|
|
@ -1069,12 +1103,8 @@ class ControllerProfile:
|
|||
right_stick=StickConfig.from_json_object(
|
||||
sticks["right"], "profile.sticks.right"
|
||||
),
|
||||
left_trigger=TriggerConfig.from_json_object(
|
||||
triggers["left"], "profile.triggers.left"
|
||||
),
|
||||
right_trigger=TriggerConfig.from_json_object(
|
||||
triggers["right"], "profile.triggers.right"
|
||||
),
|
||||
left_trigger=left_trigger,
|
||||
right_trigger=right_trigger,
|
||||
weak_rumble_scale=_require_int(
|
||||
rumble["weak_scale"],
|
||||
"profile.rumble.weak_scale",
|
||||
|
|
@ -1345,7 +1375,10 @@ def reset_configuration(device: UsbDevice, timeout: float) -> TransactionStatus:
|
|||
|
||||
def parse_profile_list(envelope: Envelope) -> tuple[ProfileListEntry, ...]:
|
||||
_raise_status(envelope)
|
||||
if envelope.schema_version != PROFILE_SCHEMA_VERSION:
|
||||
if envelope.schema_version not in (
|
||||
PROFILE_LEGACY_SCHEMA_VERSION,
|
||||
PROFILE_SCHEMA_VERSION,
|
||||
):
|
||||
raise ConfigManagerError("unsupported profile-list schema")
|
||||
if not envelope.payload:
|
||||
raise ConfigManagerError("short profile-list payload")
|
||||
|
|
@ -1407,7 +1440,10 @@ def select_profile(
|
|||
def read_selected_profile(device: UsbDevice) -> ControllerProfile:
|
||||
envelope = _control_in(device, OP_PROFILE_READ)
|
||||
_raise_status(envelope)
|
||||
if envelope.schema_version != PROFILE_SCHEMA_VERSION:
|
||||
if envelope.schema_version not in (
|
||||
PROFILE_LEGACY_SCHEMA_VERSION,
|
||||
PROFILE_SCHEMA_VERSION,
|
||||
):
|
||||
raise ConfigManagerError("unsupported profile schema")
|
||||
return ControllerProfile.from_bytes(envelope.payload)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "hardware/uart.h"
|
||||
#else
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "controller_profile_runtime.h"
|
||||
#include "bootsel_pairing_button.h"
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#include "adapter_host_probe.h"
|
||||
|
|
@ -33,6 +34,10 @@
|
|||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT ==
|
||||
BLUEPAD32_INPUT_BACKEND_SLOT_COUNT);
|
||||
static_assert(CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT ==
|
||||
BLUEPAD32_INPUT_BACKEND_SLOT_COUNT);
|
||||
static_assert(SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
static bool g_last_ready[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
|
||||
static ControllerState
|
||||
g_user_states[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
|
||||
|
|
@ -80,7 +85,11 @@ static void on_rumble_from_switch(uint8_t instance,
|
|||
if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) {
|
||||
return;
|
||||
}
|
||||
bluepad32_input_backend_queue_rumble(instance, rumble);
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(instance, &snapshot);
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
instance, controller_profile_runtime_scale_host_rumble(
|
||||
instance, snapshot, rumble));
|
||||
#else
|
||||
if (instance != SWITCH_HID_INSTANCE) {
|
||||
return;
|
||||
|
|
@ -217,6 +226,7 @@ int main() {
|
|||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bluepad32_input_backend_init();
|
||||
controller_profile_runtime_reset();
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
adapter_host_probe_init();
|
||||
#endif
|
||||
|
|
@ -241,13 +251,19 @@ int main() {
|
|||
switch_pro_set_rumble_callback(instance,
|
||||
on_rumble_from_switch);
|
||||
g_user_states[instance] = neutral_input();
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
}
|
||||
#else
|
||||
switch_pro_init(instance);
|
||||
switch_pro_set_rumble_callback(instance, on_rumble_from_switch);
|
||||
g_user_states[instance] = neutral_input();
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
|
|
@ -255,7 +271,9 @@ int main() {
|
|||
switch_pro_set_rumble_callback(SWITCH_HID_INSTANCE,
|
||||
on_rumble_from_switch);
|
||||
g_user_state = neutral_input();
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state);
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
|
|
@ -292,8 +310,9 @@ int main() {
|
|||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(instance, &snapshot);
|
||||
g_user_states[instance] =
|
||||
snapshot.active ? snapshot.state : controller_neutral_state();
|
||||
const ControllerProfileTransformResult transformed =
|
||||
controller_profile_runtime_transform(instance, snapshot);
|
||||
g_user_states[instance] = transformed.state;
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
bool sent = false;
|
||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||
|
|
@ -301,14 +320,20 @@ int main() {
|
|||
g_user_states[instance]);
|
||||
sent = xinput_feasibility_task(instance);
|
||||
} else {
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
transformed.left_trigger_digital_threshold,
|
||||
transformed.right_trigger_digital_threshold);
|
||||
sent = switch_pro_task(instance);
|
||||
}
|
||||
if (sent) {
|
||||
bluepad32_input_backend_report_sent(instance);
|
||||
}
|
||||
#else
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
transformed.left_trigger_digital_threshold,
|
||||
transformed.right_trigger_digital_threshold);
|
||||
if (switch_pro_task(instance)) {
|
||||
bluepad32_input_backend_report_sent(instance);
|
||||
}
|
||||
|
|
@ -318,7 +343,9 @@ int main() {
|
|||
bool new_data = poll_uart_frames(); // Pull controller state from UART1
|
||||
(void)new_data;
|
||||
ControllerState state = g_user_state;
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, state);
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
(void)switch_pro_task(SWITCH_HID_INSTANCE);
|
||||
#endif
|
||||
log_usb_state();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ struct MotionQuaternion {
|
|||
|
||||
struct SwitchProContext {
|
||||
ControllerState input_state{};
|
||||
uint16_t left_trigger_threshold =
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
uint16_t right_trigger_threshold =
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE]{};
|
||||
SwitchProReport switch_report{};
|
||||
uint8_t last_report_counter = 0;
|
||||
|
|
@ -420,6 +424,8 @@ static ControllerState make_neutral_state() {
|
|||
static void reset_context_runtime(SwitchProContext& context, uint32_t now,
|
||||
bool ready_before_mount) {
|
||||
context.input_state = make_neutral_state();
|
||||
context.left_trigger_threshold = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
context.right_trigger_threshold = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
memset(context.report_buffer, 0x00, sizeof(context.report_buffer));
|
||||
context.switch_report = {};
|
||||
context.switch_report.reportID = 0x30;
|
||||
|
|
@ -747,7 +753,7 @@ static void update_switch_report_from_state(SwitchProContext& context) {
|
|||
inputs.buttonRightSL = 0;
|
||||
inputs.buttonR = state.button_right_shoulder;
|
||||
inputs.buttonZR =
|
||||
state.right_trigger >= SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
state.right_trigger >= context.right_trigger_threshold;
|
||||
inputs.buttonMinus = state.button_select;
|
||||
inputs.buttonPlus = state.button_start;
|
||||
inputs.buttonThumbR = state.button_right_stick;
|
||||
|
|
@ -758,7 +764,7 @@ static void update_switch_report_from_state(SwitchProContext& context) {
|
|||
inputs.buttonLeftSL = 0;
|
||||
inputs.buttonL = state.button_left_shoulder;
|
||||
inputs.buttonZL =
|
||||
state.left_trigger >= SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
state.left_trigger >= context.left_trigger_threshold;
|
||||
|
||||
uint16_t left_x =
|
||||
scale16To12(controller_axis_to_unsigned(state.left_stick_x));
|
||||
|
|
@ -813,10 +819,14 @@ void switch_pro_init(uint8_t instance) {
|
|||
to_ms_since_boot(get_absolute_time()), true);
|
||||
}
|
||||
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state) {
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state,
|
||||
uint16_t left_trigger_threshold,
|
||||
uint16_t right_trigger_threshold) {
|
||||
SwitchProContext* context = context_for(instance);
|
||||
if (context != nullptr) {
|
||||
context->input_state = state;
|
||||
context->left_trigger_threshold = left_trigger_threshold;
|
||||
context->right_trigger_threshold = right_trigger_threshold;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,11 @@ constexpr uint16_t SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD =
|
|||
// Initialize one HID instance before entering the main loop.
|
||||
void switch_pro_init(uint8_t instance);
|
||||
|
||||
// Update the desired controller state for one HID instance.
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state);
|
||||
// Update the desired controller state and digital trigger thresholds for one
|
||||
// HID instance.
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state,
|
||||
uint16_t left_trigger_threshold,
|
||||
uint16_t right_trigger_threshold);
|
||||
|
||||
// Drive one Switch Pro USB state machine; returns true only when a regular
|
||||
// 0x30 input report was successfully queued.
|
||||
|
|
|
|||
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