Add deterministic macros and turbo
This commit is contained in:
parent
cfec816dac
commit
114266836d
12 changed files with 1019 additions and 23 deletions
|
|
@ -102,6 +102,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||||
controller_identity.cpp
|
controller_identity.cpp
|
||||||
controller_profile.cpp
|
controller_profile.cpp
|
||||||
controller_profile_transform.cpp
|
controller_profile_transform.cpp
|
||||||
|
controller_synthetic_input.cpp
|
||||||
controller_profile_runtime.cpp
|
controller_profile_runtime.cpp
|
||||||
profile_storage.cpp
|
profile_storage.cpp
|
||||||
profile_service.cpp
|
profile_service.cpp
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ bool g_prepared = false;
|
||||||
ConfigurationStorage g_storage;
|
ConfigurationStorage g_storage;
|
||||||
ConfigurationTransaction g_transaction;
|
ConfigurationTransaction g_transaction;
|
||||||
ConfigurationServiceSnapshot g_snapshot;
|
ConfigurationServiceSnapshot g_snapshot;
|
||||||
|
uint32_t g_published_reset_generation = 0;
|
||||||
bool g_has_committed = false;
|
bool g_has_committed = false;
|
||||||
uint32_t g_last_commit_ms = 0;
|
uint32_t g_last_commit_ms = 0;
|
||||||
|
|
||||||
|
|
@ -45,6 +46,7 @@ void configuration_service_prepare() {
|
||||||
}
|
}
|
||||||
critical_section_init(&g_lock);
|
critical_section_init(&g_lock);
|
||||||
g_snapshot = {};
|
g_snapshot = {};
|
||||||
|
__atomic_store_n(&g_published_reset_generation, 0, __ATOMIC_RELAXED);
|
||||||
g_snapshot.configuration = adapter_configuration_default();
|
g_snapshot.configuration = adapter_configuration_default();
|
||||||
g_transaction.clear();
|
g_transaction.clear();
|
||||||
g_prepared = true;
|
g_prepared = true;
|
||||||
|
|
@ -176,6 +178,10 @@ ConfigurationTransactionStatus configuration_service_reset(
|
||||||
++g_snapshot.reset_generation;
|
++g_snapshot.reset_generation;
|
||||||
}
|
}
|
||||||
g_snapshot.transaction = g_transaction.snapshot();
|
g_snapshot.transaction = g_transaction.snapshot();
|
||||||
|
if (status == ConfigurationTransactionStatus::kPending) {
|
||||||
|
__atomic_store_n(&g_published_reset_generation,
|
||||||
|
g_snapshot.reset_generation, __ATOMIC_RELEASE);
|
||||||
|
}
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
@ -188,3 +194,7 @@ void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
|
||||||
*output = g_snapshot;
|
*output = g_snapshot;
|
||||||
critical_section_exit(&g_lock);
|
critical_section_exit(&g_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t configuration_service_reset_generation() {
|
||||||
|
return __atomic_load_n(&g_published_reset_generation, __ATOMIC_ACQUIRE);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,7 @@ ConfigurationTransactionStatus configuration_service_commit(
|
||||||
ConfigurationTransactionStatus configuration_service_reset(
|
ConfigurationTransactionStatus configuration_service_reset(
|
||||||
uint32_t transaction_id);
|
uint32_t transaction_id);
|
||||||
void configuration_service_snapshot(ConfigurationServiceSnapshot* output);
|
void configuration_service_snapshot(ConfigurationServiceSnapshot* output);
|
||||||
|
|
||||||
|
// Lock-free publication for the report path. The value changes as soon as a
|
||||||
|
// configuration reset is accepted.
|
||||||
|
uint32_t configuration_service_reset_generation();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#include "controller_profile_runtime.h"
|
#include "controller_profile_runtime.h"
|
||||||
|
|
||||||
|
#include "configuration_service.h"
|
||||||
#include "controller_identity.h"
|
#include "controller_identity.h"
|
||||||
|
#include "controller_synthetic_input.h"
|
||||||
#include "profile_service.h"
|
#include "profile_service.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
@ -12,6 +14,10 @@ struct ControllerProfileRuntimeContext {
|
||||||
uint32_t database_generation = 0;
|
uint32_t database_generation = 0;
|
||||||
uint8_t active_profile_index = 0;
|
uint8_t active_profile_index = 0;
|
||||||
ControllerProfile profile{};
|
ControllerProfile profile{};
|
||||||
|
ControllerSyntheticInputContext synthetic{};
|
||||||
|
bool runtime_generations_initialized = false;
|
||||||
|
AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe;
|
||||||
|
uint32_t configuration_reset_generation = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
ControllerProfileRuntimeContext
|
ControllerProfileRuntimeContext
|
||||||
|
|
@ -41,7 +47,8 @@ void clear_context(ControllerProfileRuntimeContext* context) {
|
||||||
void refresh_profile(ControllerProfileRuntimeContext* context,
|
void refresh_profile(ControllerProfileRuntimeContext* context,
|
||||||
const ControllerIdentity& identity,
|
const ControllerIdentity& identity,
|
||||||
uint32_t connection_generation,
|
uint32_t connection_generation,
|
||||||
uint32_t observed_database_generation) {
|
uint32_t observed_database_generation,
|
||||||
|
uint16_t current_input_button_mask) {
|
||||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||||
|
|
||||||
|
|
@ -53,6 +60,8 @@ void refresh_profile(ControllerProfileRuntimeContext* context,
|
||||||
: observed_database_generation;
|
: observed_database_generation;
|
||||||
context->active_profile_index = snapshot.valid ? snapshot.profile_index : 0;
|
context->active_profile_index = snapshot.valid ? snapshot.profile_index : 0;
|
||||||
context->profile = snapshot.valid ? snapshot.profile : g_default_profile;
|
context->profile = snapshot.valid ? snapshot.profile : g_default_profile;
|
||||||
|
controller_synthetic_input_cancel(&context->synthetic,
|
||||||
|
current_input_button_mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
ControllerProfileRuntimeContext* update_context(
|
ControllerProfileRuntimeContext* update_context(
|
||||||
|
|
@ -75,9 +84,10 @@ ControllerProfileRuntimeContext* update_context(
|
||||||
context.connection_generation != snapshot.connection_generation ||
|
context.connection_generation != snapshot.connection_generation ||
|
||||||
!controller_identity_equal(context.identity, snapshot.identity) ||
|
!controller_identity_equal(context.identity, snapshot.identity) ||
|
||||||
context.database_generation != database_generation) {
|
context.database_generation != database_generation) {
|
||||||
refresh_profile(&context, snapshot.identity,
|
refresh_profile(
|
||||||
snapshot.connection_generation,
|
&context, snapshot.identity, snapshot.connection_generation,
|
||||||
database_generation);
|
database_generation,
|
||||||
|
controller_profile_extract_button_mask(snapshot.state));
|
||||||
}
|
}
|
||||||
return &context;
|
return &context;
|
||||||
}
|
}
|
||||||
|
|
@ -94,14 +104,32 @@ void controller_profile_runtime_reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot) {
|
uint8_t slot, const Bluepad32SlotSnapshot& snapshot, uint32_t now_ms,
|
||||||
|
AdapterUsbMode output_mode) {
|
||||||
initialize_defaults();
|
initialize_defaults();
|
||||||
ControllerProfileRuntimeContext* context =
|
ControllerProfileRuntimeContext* context =
|
||||||
update_context(slot, snapshot);
|
update_context(slot, snapshot);
|
||||||
if (context == nullptr) {
|
if (context == nullptr) {
|
||||||
return g_neutral_output;
|
return g_neutral_output;
|
||||||
}
|
}
|
||||||
return controller_profile_transform(snapshot.state, context->profile);
|
|
||||||
|
const uint32_t reset_generation =
|
||||||
|
configuration_service_reset_generation();
|
||||||
|
if (!context->runtime_generations_initialized) {
|
||||||
|
context->runtime_generations_initialized = true;
|
||||||
|
context->output_mode = output_mode;
|
||||||
|
context->configuration_reset_generation = reset_generation;
|
||||||
|
} else if (context->output_mode != output_mode ||
|
||||||
|
context->configuration_reset_generation !=
|
||||||
|
reset_generation) {
|
||||||
|
controller_synthetic_input_cancel(
|
||||||
|
&context->synthetic,
|
||||||
|
controller_profile_extract_button_mask(snapshot.state));
|
||||||
|
context->output_mode = output_mode;
|
||||||
|
context->configuration_reset_generation = reset_generation;
|
||||||
|
}
|
||||||
|
return controller_synthetic_input_apply(
|
||||||
|
&context->synthetic, snapshot.state, context->profile, now_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "adapter_usb_mode.h"
|
||||||
|
|
||||||
#include "bluepad32_input_backend.h"
|
#include "bluepad32_input_backend.h"
|
||||||
#include "controller_profile_transform.h"
|
#include "controller_profile_transform.h"
|
||||||
|
|
||||||
|
|
@ -17,10 +19,12 @@ struct ControllerProfileRuntimeLocalConfirmation {
|
||||||
void controller_profile_runtime_reset();
|
void controller_profile_runtime_reset();
|
||||||
|
|
||||||
// Refresh a slot only when its connection key or database generation changes,
|
// Refresh a slot only when its connection key or database generation changes,
|
||||||
// then transform the raw snapshot. Inactive snapshots return neutral output and
|
// cancel synthetic state on every runtime invalidation, then apply the shared
|
||||||
// invalidate the slot immediately.
|
// transform and synthetic pipeline. Inactive snapshots return neutral output
|
||||||
|
// and invalidate the slot immediately.
|
||||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot);
|
uint8_t slot, const Bluepad32SlotSnapshot& snapshot, uint32_t now_ms,
|
||||||
|
AdapterUsbMode output_mode);
|
||||||
|
|
||||||
// Refresh from the current slot snapshot and scale host-originated rumble.
|
// Refresh from the current slot snapshot and scale host-originated rumble.
|
||||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||||
|
|
|
||||||
237
controller_synthetic_input.cpp
Normal file
237
controller_synthetic_input.cpp
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
#include "controller_synthetic_input.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint32_t kTurboTransitionsPerSecond = 30;
|
||||||
|
constexpr uint32_t kTurboPhaseUnitsPerTransition = 1000;
|
||||||
|
|
||||||
|
constexpr uint16_t button_bit(uint8_t button) {
|
||||||
|
return static_cast<uint16_t>(1u << button);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_bound_button(uint8_t button) {
|
||||||
|
return button < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_binding(ControllerSyntheticBindingState* binding) {
|
||||||
|
*binding = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_binding(ControllerSyntheticBindingState* binding,
|
||||||
|
uint32_t now_ms) {
|
||||||
|
binding->active = true;
|
||||||
|
binding->phase_on = true;
|
||||||
|
binding->phase_units = 0;
|
||||||
|
binding->last_update_ms = now_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
void advance_binding(ControllerSyntheticBindingState* binding,
|
||||||
|
uint32_t now_ms) {
|
||||||
|
const uint32_t elapsed_ms = now_ms - binding->last_update_ms;
|
||||||
|
const uint64_t total_units =
|
||||||
|
static_cast<uint64_t>(binding->phase_units) +
|
||||||
|
static_cast<uint64_t>(elapsed_ms) * kTurboTransitionsPerSecond;
|
||||||
|
const uint64_t transition_count =
|
||||||
|
total_units / kTurboPhaseUnitsPerTransition;
|
||||||
|
if ((transition_count & 1u) != 0) {
|
||||||
|
binding->phase_on = !binding->phase_on;
|
||||||
|
}
|
||||||
|
binding->phase_units = static_cast<uint16_t>(
|
||||||
|
total_units -
|
||||||
|
transition_count * kTurboPhaseUnitsPerTransition);
|
||||||
|
binding->last_update_ms = now_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deadline_reached(uint32_t now_ms, uint32_t deadline_ms) {
|
||||||
|
return now_ms - deadline_ms < (UINT32_MAX / 2u + 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop_macro(ControllerSyntheticInputContext* context) {
|
||||||
|
context->macro_active = false;
|
||||||
|
context->macro_step_index = 0;
|
||||||
|
context->macro_deadline_ms = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool start_macro(ControllerSyntheticInputContext* context,
|
||||||
|
const ControllerProfile& profile, uint32_t now_ms) {
|
||||||
|
if (profile.macro_step_count == 0 ||
|
||||||
|
profile.macro_steps[0].type !=
|
||||||
|
ControllerProfileMacroStepType::kState) {
|
||||||
|
stop_macro(context);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
context->macro_active = true;
|
||||||
|
context->macro_step_index = 0;
|
||||||
|
context->macro_deadline_ms =
|
||||||
|
now_ms + profile.macro_steps[0].duration_ms;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void advance_macro(ControllerSyntheticInputContext* context,
|
||||||
|
const ControllerProfile& profile, uint32_t now_ms) {
|
||||||
|
for (uint8_t transition = 0;
|
||||||
|
transition < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY;
|
||||||
|
++transition) {
|
||||||
|
if (!context->macro_active ||
|
||||||
|
context->macro_step_index >= profile.macro_step_count) {
|
||||||
|
stop_macro(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ControllerProfileMacroStep& current =
|
||||||
|
profile.macro_steps[context->macro_step_index];
|
||||||
|
if (current.type != ControllerProfileMacroStepType::kState) {
|
||||||
|
stop_macro(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!deadline_reached(now_ms, context->macro_deadline_ms)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t next_index =
|
||||||
|
static_cast<uint8_t>(context->macro_step_index + 1u);
|
||||||
|
if (next_index >= profile.macro_step_count ||
|
||||||
|
profile.macro_steps[next_index].type ==
|
||||||
|
ControllerProfileMacroStepType::kEnd) {
|
||||||
|
stop_macro(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context->macro_step_index = next_index;
|
||||||
|
context->macro_deadline_ms +=
|
||||||
|
profile.macro_steps[next_index].duration_ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_macro_override(const ControllerProfileMacroStep& step,
|
||||||
|
ControllerState* output) {
|
||||||
|
if ((step.override_flags & kControllerProfileOverrideButtons) != 0) {
|
||||||
|
controller_profile_apply_button_mask(step.output_button_mask,
|
||||||
|
output);
|
||||||
|
}
|
||||||
|
if ((step.override_flags & kControllerProfileOverrideLeftStick) != 0) {
|
||||||
|
output->left_stick_x = step.left_stick_x;
|
||||||
|
output->left_stick_y = step.left_stick_y;
|
||||||
|
}
|
||||||
|
if ((step.override_flags & kControllerProfileOverrideRightStick) != 0) {
|
||||||
|
output->right_stick_x = step.right_stick_x;
|
||||||
|
output->right_stick_y = step.right_stick_y;
|
||||||
|
}
|
||||||
|
if ((step.override_flags & kControllerProfileOverrideLeftTrigger) != 0) {
|
||||||
|
output->left_trigger = step.left_trigger;
|
||||||
|
}
|
||||||
|
if ((step.override_flags & kControllerProfileOverrideRightTrigger) != 0) {
|
||||||
|
output->right_trigger = step.right_trigger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void controller_synthetic_input_cancel(
|
||||||
|
ControllerSyntheticInputContext* context,
|
||||||
|
uint16_t current_input_button_mask) {
|
||||||
|
if (context == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*context = {};
|
||||||
|
context->previous_input_button_mask = current_input_button_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerProfileTransformResult controller_synthetic_input_apply(
|
||||||
|
ControllerSyntheticInputContext* context, const ControllerState& input,
|
||||||
|
const ControllerProfile& profile, uint32_t now_ms) {
|
||||||
|
if (context == nullptr) {
|
||||||
|
return controller_profile_transform(input, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t input_button_mask =
|
||||||
|
controller_profile_extract_button_mask(input);
|
||||||
|
uint16_t rising_button_mask = static_cast<uint16_t>(
|
||||||
|
input_button_mask & ~context->previous_input_button_mask);
|
||||||
|
const bool cancel_pressed =
|
||||||
|
is_bound_button(profile.macro_cancel) &&
|
||||||
|
(input_button_mask & button_bit(profile.macro_cancel)) != 0;
|
||||||
|
if (cancel_pressed) {
|
||||||
|
controller_synthetic_input_cancel(context, input_button_mask);
|
||||||
|
rising_button_mask = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool macro_started = false;
|
||||||
|
if (!cancel_pressed && is_bound_button(profile.macro_trigger) &&
|
||||||
|
(rising_button_mask & button_bit(profile.macro_trigger)) != 0) {
|
||||||
|
macro_started = start_macro(context, profile, now_ms);
|
||||||
|
}
|
||||||
|
if (!macro_started) {
|
||||||
|
advance_macro(context, profile, now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t gated_input_button_mask = 0;
|
||||||
|
for (uint8_t input_button = 0;
|
||||||
|
input_button < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT;
|
||||||
|
++input_button) {
|
||||||
|
ControllerSyntheticBindingState& binding =
|
||||||
|
context->bindings[input_button];
|
||||||
|
if (input_button == profile.macro_trigger ||
|
||||||
|
input_button == profile.macro_cancel) {
|
||||||
|
clear_binding(&binding);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t bit = button_bit(input_button);
|
||||||
|
const bool pressed = (input_button_mask & bit) != 0;
|
||||||
|
const bool rising = (rising_button_mask & bit) != 0;
|
||||||
|
switch (profile.turbo_modes[input_button]) {
|
||||||
|
case ControllerProfileTurboMode::kOff:
|
||||||
|
clear_binding(&binding);
|
||||||
|
if (pressed) {
|
||||||
|
gated_input_button_mask |= bit;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ControllerProfileTurboMode::kTurbo:
|
||||||
|
if (!pressed) {
|
||||||
|
clear_binding(&binding);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!binding.active) {
|
||||||
|
start_binding(&binding, now_ms);
|
||||||
|
} else {
|
||||||
|
advance_binding(&binding, now_ms);
|
||||||
|
}
|
||||||
|
if (binding.phase_on) {
|
||||||
|
gated_input_button_mask |= bit;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ControllerProfileTurboMode::kAutoBurst:
|
||||||
|
if (rising) {
|
||||||
|
if (binding.active) {
|
||||||
|
clear_binding(&binding);
|
||||||
|
} else {
|
||||||
|
start_binding(&binding, now_ms);
|
||||||
|
}
|
||||||
|
} else if (binding.active) {
|
||||||
|
advance_binding(&binding, now_ms);
|
||||||
|
}
|
||||||
|
if (binding.active && binding.phase_on) {
|
||||||
|
gated_input_button_mask |= bit;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerState gated_input = input;
|
||||||
|
controller_profile_apply_button_mask(gated_input_button_mask,
|
||||||
|
&gated_input);
|
||||||
|
ControllerProfileTransformResult result =
|
||||||
|
controller_profile_transform(gated_input, profile);
|
||||||
|
if (context->macro_active &&
|
||||||
|
context->macro_step_index < profile.macro_step_count) {
|
||||||
|
const ControllerProfileMacroStep& step =
|
||||||
|
profile.macro_steps[context->macro_step_index];
|
||||||
|
if (step.type == ControllerProfileMacroStepType::kState) {
|
||||||
|
apply_macro_override(step, &result.state);
|
||||||
|
} else {
|
||||||
|
stop_macro(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context->previous_input_button_mask = input_button_mask;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
34
controller_synthetic_input.h
Normal file
34
controller_synthetic_input.h
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "controller_profile_transform.h"
|
||||||
|
|
||||||
|
struct ControllerSyntheticBindingState {
|
||||||
|
bool active = false;
|
||||||
|
bool phase_on = false;
|
||||||
|
uint16_t phase_units = 0;
|
||||||
|
uint32_t last_update_ms = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ControllerSyntheticInputContext {
|
||||||
|
bool macro_active = false;
|
||||||
|
uint8_t macro_step_index = 0;
|
||||||
|
uint32_t macro_deadline_ms = 0;
|
||||||
|
uint16_t previous_input_button_mask = 0;
|
||||||
|
ControllerSyntheticBindingState
|
||||||
|
bindings[CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT]{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear every synthetic source. Inputs already held at cancellation remain
|
||||||
|
// consumed or physical, but edge-triggered Macro and Auto Burst bindings do not
|
||||||
|
// restart until they are released and pressed again.
|
||||||
|
void controller_synthetic_input_cancel(
|
||||||
|
ControllerSyntheticInputContext* context,
|
||||||
|
uint16_t current_input_button_mask = 0);
|
||||||
|
|
||||||
|
// Apply raw-input consumption, mapped physical contributions, Turbo/Auto Burst
|
||||||
|
// gating, and finally the active macro step's field overrides.
|
||||||
|
ControllerProfileTransformResult controller_synthetic_input_apply(
|
||||||
|
ControllerSyntheticInputContext* context, const ControllerState& input,
|
||||||
|
const ControllerProfile& profile, uint32_t now_ms);
|
||||||
|
|
@ -306,16 +306,24 @@ int main() {
|
||||||
case BootselPairingButtonEvent::kNone:
|
case BootselPairingButtonEvent::kNone:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
const uint32_t now_ms =
|
||||||
|
static_cast<uint32_t>(to_ms_since_boot(get_absolute_time()));
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
const AdapterUsbMode output_mode = adapter_host_probe_mode();
|
||||||
|
#else
|
||||||
|
constexpr AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe;
|
||||||
|
#endif
|
||||||
for (uint8_t instance = 0;
|
for (uint8_t instance = 0;
|
||||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||||
Bluepad32SlotSnapshot snapshot{};
|
Bluepad32SlotSnapshot snapshot{};
|
||||||
bluepad32_input_backend_snapshot(instance, &snapshot);
|
bluepad32_input_backend_snapshot(instance, &snapshot);
|
||||||
const ControllerProfileTransformResult transformed =
|
const ControllerProfileTransformResult transformed =
|
||||||
controller_profile_runtime_transform(instance, snapshot);
|
controller_profile_runtime_transform(
|
||||||
|
instance, snapshot, now_ms, output_mode);
|
||||||
g_user_states[instance] = transformed.state;
|
g_user_states[instance] = transformed.state;
|
||||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
bool sent = false;
|
bool sent = false;
|
||||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
if (output_mode == AdapterUsbMode::kXInput) {
|
||||||
xinput_feasibility_set_input(instance,
|
xinput_feasibility_set_input(instance,
|
||||||
g_user_states[instance]);
|
g_user_states[instance]);
|
||||||
sent = xinput_feasibility_task(instance);
|
sent = xinput_feasibility_task(instance);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ struct FakeProfileRow {
|
||||||
|
|
||||||
std::array<FakeProfileRow, CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT> rows{};
|
std::array<FakeProfileRow, CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT> rows{};
|
||||||
uint32_t database_generation = 7;
|
uint32_t database_generation = 7;
|
||||||
|
uint32_t configuration_reset_generation = 3;
|
||||||
unsigned active_snapshot_count = 0;
|
unsigned active_snapshot_count = 0;
|
||||||
|
|
||||||
void require(bool condition, const char* message) {
|
void require(bool condition, const char* message) {
|
||||||
|
|
@ -41,6 +42,7 @@ ControllerIdentity make_identity(uint8_t value) {
|
||||||
|
|
||||||
void prepare_profiles() {
|
void prepare_profiles() {
|
||||||
database_generation = 7;
|
database_generation = 7;
|
||||||
|
configuration_reset_generation = 3;
|
||||||
active_snapshot_count = 0;
|
active_snapshot_count = 0;
|
||||||
for (uint8_t slot = 0;
|
for (uint8_t slot = 0;
|
||||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||||
|
|
@ -77,6 +79,14 @@ Bluepad32SlotSnapshot make_snapshot(uint8_t slot,
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ControllerProfileTransformResult runtime_transform(
|
||||||
|
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||||
|
uint32_t now_ms = 0,
|
||||||
|
AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe) {
|
||||||
|
return controller_profile_runtime_transform(slot, snapshot, now_ms,
|
||||||
|
output_mode);
|
||||||
|
}
|
||||||
|
|
||||||
bool motion_equal(const ControllerState& first,
|
bool motion_equal(const ControllerState& first,
|
||||||
const ControllerState& second) {
|
const ControllerState& second) {
|
||||||
return first.motion_sample_count == second.motion_sample_count &&
|
return first.motion_sample_count == second.motion_sample_count &&
|
||||||
|
|
@ -96,7 +106,7 @@ void test_four_slot_cache_and_unchanged_generation() {
|
||||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||||
snapshots[slot] = make_snapshot(slot);
|
snapshots[slot] = make_snapshot(slot);
|
||||||
transformed[slot] =
|
transformed[slot] =
|
||||||
controller_profile_runtime_transform(slot, snapshots[slot]);
|
runtime_transform(slot, snapshots[slot]);
|
||||||
require(transformed[slot].left_trigger_digital_threshold ==
|
require(transformed[slot].left_trigger_digital_threshold ==
|
||||||
static_cast<uint16_t>(1000u + slot) &&
|
static_cast<uint16_t>(1000u + slot) &&
|
||||||
transformed[slot].right_trigger_digital_threshold ==
|
transformed[slot].right_trigger_digital_threshold ==
|
||||||
|
|
@ -110,7 +120,7 @@ void test_four_slot_cache_and_unchanged_generation() {
|
||||||
for (uint8_t slot = 0;
|
for (uint8_t slot = 0;
|
||||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||||
transformed[slot] =
|
transformed[slot] =
|
||||||
controller_profile_runtime_transform(slot, snapshots[slot]);
|
runtime_transform(slot, snapshots[slot]);
|
||||||
}
|
}
|
||||||
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
|
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
|
||||||
"unchanged generations copied profiles on the report path");
|
"unchanged generations copied profiles on the report path");
|
||||||
|
|
@ -119,7 +129,7 @@ void test_four_slot_cache_and_unchanged_generation() {
|
||||||
|
|
||||||
snapshots[2].connection_generation = 2;
|
snapshots[2].connection_generation = 2;
|
||||||
transformed[2] =
|
transformed[2] =
|
||||||
controller_profile_runtime_transform(2, snapshots[2]);
|
runtime_transform(2, snapshots[2]);
|
||||||
require(active_snapshot_count ==
|
require(active_snapshot_count ==
|
||||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 1 &&
|
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 1 &&
|
||||||
transformed[2].left_trigger_digital_threshold == 1002,
|
transformed[2].left_trigger_digital_threshold == 1002,
|
||||||
|
|
@ -127,7 +137,7 @@ void test_four_slot_cache_and_unchanged_generation() {
|
||||||
|
|
||||||
snapshots[3].identity = rows[1].identity;
|
snapshots[3].identity = rows[1].identity;
|
||||||
transformed[3] =
|
transformed[3] =
|
||||||
controller_profile_runtime_transform(3, snapshots[3]);
|
runtime_transform(3, snapshots[3]);
|
||||||
require(active_snapshot_count ==
|
require(active_snapshot_count ==
|
||||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 2 &&
|
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 2 &&
|
||||||
transformed[3].left_trigger_digital_threshold == 1001,
|
transformed[3].left_trigger_digital_threshold == 1001,
|
||||||
|
|
@ -139,7 +149,7 @@ void test_activation_disconnect_and_default_preservation() {
|
||||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||||
snapshot.state.button_south = true;
|
snapshot.state.button_south = true;
|
||||||
ControllerProfileTransformResult transformed =
|
ControllerProfileTransformResult transformed =
|
||||||
controller_profile_runtime_transform(0, snapshot);
|
runtime_transform(0, snapshot);
|
||||||
require(transformed.state.button_south &&
|
require(transformed.state.button_south &&
|
||||||
transformed.left_trigger_digital_threshold == 1000,
|
transformed.left_trigger_digital_threshold == 1000,
|
||||||
"initial active profile was not applied");
|
"initial active profile was not applied");
|
||||||
|
|
@ -150,25 +160,25 @@ void test_activation_disconnect_and_default_preservation() {
|
||||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
|
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
|
||||||
rows[0].profiles[1].triggers[0].digital_threshold = 12345;
|
rows[0].profiles[1].triggers[0].digital_threshold = 12345;
|
||||||
++database_generation;
|
++database_generation;
|
||||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
transformed = runtime_transform(0, snapshot);
|
||||||
require(!transformed.state.button_south && transformed.state.button_north &&
|
require(!transformed.state.button_south && transformed.state.button_north &&
|
||||||
transformed.left_trigger_digital_threshold == 12345,
|
transformed.left_trigger_digital_threshold == 12345,
|
||||||
"profile activation generation did not refresh the slot cache");
|
"profile activation generation did not refresh the slot cache");
|
||||||
|
|
||||||
const unsigned reads_before_disconnect = active_snapshot_count;
|
const unsigned reads_before_disconnect = active_snapshot_count;
|
||||||
snapshot.active = false;
|
snapshot.active = false;
|
||||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
transformed = runtime_transform(0, snapshot);
|
||||||
require(!transformed.state.button_south && !transformed.state.button_north &&
|
require(!transformed.state.button_south && !transformed.state.button_north &&
|
||||||
transformed.left_trigger_digital_threshold ==
|
transformed.left_trigger_digital_threshold ==
|
||||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||||
active_snapshot_count == reads_before_disconnect,
|
active_snapshot_count == reads_before_disconnect,
|
||||||
"disconnect did not neutralize output without fetching a profile");
|
"disconnect did not neutralize output without fetching a profile");
|
||||||
|
|
||||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
transformed = runtime_transform(0, snapshot);
|
||||||
require(active_snapshot_count == reads_before_disconnect,
|
require(active_snapshot_count == reads_before_disconnect,
|
||||||
"an unchanged inactive slot repeatedly cleared or fetched state");
|
"an unchanged inactive slot repeatedly cleared or fetched state");
|
||||||
snapshot.active = true;
|
snapshot.active = true;
|
||||||
transformed = controller_profile_runtime_transform(0, snapshot);
|
transformed = runtime_transform(0, snapshot);
|
||||||
require(active_snapshot_count == reads_before_disconnect + 1 &&
|
require(active_snapshot_count == reads_before_disconnect + 1 &&
|
||||||
transformed.state.button_north,
|
transformed.state.button_north,
|
||||||
"reconnection did not reload a cleared slot cache");
|
"reconnection did not reload a cleared slot cache");
|
||||||
|
|
@ -186,7 +196,7 @@ void test_activation_disconnect_and_default_preservation() {
|
||||||
default_snapshot.state.motion_sample_count = 2;
|
default_snapshot.state.motion_sample_count = 2;
|
||||||
default_snapshot.state.motion_samples[0] = {1, 2, 3, 4, 5, 6};
|
default_snapshot.state.motion_samples[0] = {1, 2, 3, 4, 5, 6};
|
||||||
default_snapshot.state.motion_samples[1] = {-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);
|
transformed = runtime_transform(2, default_snapshot);
|
||||||
require(transformed.state.button_east && transformed.state.dpad_left &&
|
require(transformed.state.button_east && transformed.state.dpad_left &&
|
||||||
transformed.state.left_trigger == 32123 &&
|
transformed.state.left_trigger == 32123 &&
|
||||||
transformed.state.right_trigger == 54321 &&
|
transformed.state.right_trigger == 54321 &&
|
||||||
|
|
@ -207,9 +217,9 @@ void test_analog_thresholds_rumble_and_local_confirmation() {
|
||||||
first.state.left_trigger = second.state.left_trigger = 23456;
|
first.state.left_trigger = second.state.left_trigger = 23456;
|
||||||
first.state.right_trigger = second.state.right_trigger = 45678;
|
first.state.right_trigger = second.state.right_trigger = 45678;
|
||||||
const ControllerProfileTransformResult first_output =
|
const ControllerProfileTransformResult first_output =
|
||||||
controller_profile_runtime_transform(0, first);
|
runtime_transform(0, first);
|
||||||
const ControllerProfileTransformResult second_output =
|
const ControllerProfileTransformResult second_output =
|
||||||
controller_profile_runtime_transform(1, second);
|
runtime_transform(1, second);
|
||||||
require(first_output.state.left_trigger ==
|
require(first_output.state.left_trigger ==
|
||||||
second_output.state.left_trigger &&
|
second_output.state.left_trigger &&
|
||||||
first_output.state.right_trigger ==
|
first_output.state.right_trigger ==
|
||||||
|
|
@ -238,12 +248,172 @@ void test_analog_thresholds_rumble_and_local_confirmation() {
|
||||||
"local confirmation was scaled or lost its profile policy");
|
"local confirmation was scaled or lost its profile policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void configure_synthetic_profile(uint8_t slot) {
|
||||||
|
ControllerProfile& profile = rows[slot].profiles[0];
|
||||||
|
profile = controller_profile_default(rows[slot].identity, 0);
|
||||||
|
profile.macro_trigger =
|
||||||
|
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth);
|
||||||
|
profile.macro_cancel =
|
||||||
|
static_cast<uint8_t>(ControllerProfileLogicalButton::kCapture);
|
||||||
|
profile.macro_step_count = 2;
|
||||||
|
profile.macro_steps[0].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile.macro_steps[0].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile.macro_steps[0].duration_ms = 1000;
|
||||||
|
profile.macro_steps[0].output_button_mask = static_cast<uint16_t>(
|
||||||
|
1u << static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kNorth));
|
||||||
|
profile.macro_steps[1] = {};
|
||||||
|
profile.macro_steps[1].type =
|
||||||
|
ControllerProfileMacroStepType::kEnd;
|
||||||
|
profile.turbo_modes[static_cast<uint8_t>(
|
||||||
|
ControllerProfileLogicalButton::kEast)] =
|
||||||
|
ControllerProfileTurboMode::kAutoBurst;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_macro(Bluepad32SlotSnapshot* snapshot, uint32_t now_ms,
|
||||||
|
AdapterUsbMode mode = AdapterUsbMode::kSwitchProbe) {
|
||||||
|
snapshot->state = controller_neutral_state();
|
||||||
|
(void)runtime_transform(0, *snapshot, now_ms, mode);
|
||||||
|
snapshot->state.button_south = true;
|
||||||
|
const ControllerProfileTransformResult started =
|
||||||
|
runtime_transform(0, *snapshot, now_ms + 1u, mode);
|
||||||
|
require(started.state.button_north && !started.state.button_south,
|
||||||
|
"runtime fixture did not start its macro");
|
||||||
|
snapshot->state = controller_neutral_state();
|
||||||
|
const ControllerProfileTransformResult held =
|
||||||
|
runtime_transform(0, *snapshot, now_ms + 2u, mode);
|
||||||
|
require(held.state.button_north,
|
||||||
|
"runtime fixture macro did not remain active");
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_auto_burst(Bluepad32SlotSnapshot* snapshot, uint32_t now_ms,
|
||||||
|
AdapterUsbMode mode =
|
||||||
|
AdapterUsbMode::kSwitchProbe) {
|
||||||
|
snapshot->state = controller_neutral_state();
|
||||||
|
(void)runtime_transform(0, *snapshot, now_ms, mode);
|
||||||
|
snapshot->state.button_east = true;
|
||||||
|
const ControllerProfileTransformResult started =
|
||||||
|
runtime_transform(0, *snapshot, now_ms + 1u, mode);
|
||||||
|
require(started.state.button_east,
|
||||||
|
"runtime fixture did not start Auto Burst");
|
||||||
|
snapshot->state = controller_neutral_state();
|
||||||
|
const ControllerProfileTransformResult latched =
|
||||||
|
runtime_transform(0, *snapshot, now_ms + 2u, mode);
|
||||||
|
require(latched.state.button_east,
|
||||||
|
"runtime fixture Auto Burst did not latch");
|
||||||
|
}
|
||||||
|
|
||||||
|
void require_no_synthetic_output(
|
||||||
|
const ControllerProfileTransformResult& output,
|
||||||
|
const char* message) {
|
||||||
|
require(!output.state.button_north && !output.state.button_east &&
|
||||||
|
!output.state.button_south &&
|
||||||
|
!output.state.button_capture,
|
||||||
|
message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_all_runtime_cancellation_causes() {
|
||||||
|
prepare_profiles();
|
||||||
|
configure_synthetic_profile(0);
|
||||||
|
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||||
|
start_macro(&snapshot, 10);
|
||||||
|
snapshot.active = false;
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
runtime_transform(0, snapshot, 13);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "disconnect did not cancel synthetic output");
|
||||||
|
snapshot.active = true;
|
||||||
|
output = runtime_transform(0, snapshot, 14);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "reconnection restored stale synthetic output");
|
||||||
|
|
||||||
|
prepare_profiles();
|
||||||
|
configure_synthetic_profile(0);
|
||||||
|
snapshot = make_snapshot(0);
|
||||||
|
start_macro(&snapshot, 20);
|
||||||
|
++snapshot.connection_generation;
|
||||||
|
output = runtime_transform(0, snapshot, 23);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "connection replacement did not cancel before output");
|
||||||
|
|
||||||
|
prepare_profiles();
|
||||||
|
configure_synthetic_profile(0);
|
||||||
|
snapshot = make_snapshot(0);
|
||||||
|
start_macro(&snapshot, 30);
|
||||||
|
rows[0].active_profile = 1;
|
||||||
|
++database_generation;
|
||||||
|
output = runtime_transform(0, snapshot, 33);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "profile/database generation change did not cancel");
|
||||||
|
|
||||||
|
prepare_profiles();
|
||||||
|
configure_synthetic_profile(0);
|
||||||
|
snapshot = make_snapshot(0);
|
||||||
|
start_auto_burst(&snapshot, 40);
|
||||||
|
output = runtime_transform(0, snapshot, 43,
|
||||||
|
AdapterUsbMode::kXInput);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "output-mode change did not cancel Auto Burst");
|
||||||
|
output = runtime_transform(0, snapshot, 44,
|
||||||
|
AdapterUsbMode::kXInput);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "output-mode cancellation left a stuck output");
|
||||||
|
|
||||||
|
prepare_profiles();
|
||||||
|
configure_synthetic_profile(0);
|
||||||
|
snapshot = make_snapshot(0);
|
||||||
|
start_auto_burst(&snapshot, 50);
|
||||||
|
++configuration_reset_generation;
|
||||||
|
output = runtime_transform(0, snapshot, 53);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "configuration reset did not cancel Auto Burst");
|
||||||
|
output = runtime_transform(0, snapshot, 54);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
output, "configuration reset cancellation left a stuck output");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_runtime_slot_synthetic_isolation() {
|
||||||
|
prepare_profiles();
|
||||||
|
configure_synthetic_profile(0);
|
||||||
|
configure_synthetic_profile(1);
|
||||||
|
Bluepad32SlotSnapshot first = make_snapshot(0);
|
||||||
|
Bluepad32SlotSnapshot second = make_snapshot(1);
|
||||||
|
(void)runtime_transform(0, first, 0);
|
||||||
|
(void)runtime_transform(1, second, 0);
|
||||||
|
first.state.button_east = true;
|
||||||
|
second.state.button_east = true;
|
||||||
|
require(runtime_transform(0, first, 1).state.button_east &&
|
||||||
|
runtime_transform(1, second, 1).state.button_east,
|
||||||
|
"runtime slots did not activate independently");
|
||||||
|
first.state = controller_neutral_state();
|
||||||
|
second.state = controller_neutral_state();
|
||||||
|
require(runtime_transform(0, first, 2).state.button_east &&
|
||||||
|
runtime_transform(1, second, 2).state.button_east,
|
||||||
|
"runtime Auto Burst state did not remain isolated");
|
||||||
|
|
||||||
|
first.state.button_capture = true;
|
||||||
|
const ControllerProfileTransformResult cancelled =
|
||||||
|
runtime_transform(0, first, 3);
|
||||||
|
const ControllerProfileTransformResult untouched =
|
||||||
|
runtime_transform(1, second, 3);
|
||||||
|
require_no_synthetic_output(
|
||||||
|
cancelled, "slot-local configured cancel did not clear its output");
|
||||||
|
require(untouched.state.button_east,
|
||||||
|
"slot-local configured cancel affected another slot");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
uint32_t profile_service_database_generation() {
|
uint32_t profile_service_database_generation() {
|
||||||
return database_generation;
|
return database_generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t configuration_service_reset_generation() {
|
||||||
|
return configuration_reset_generation;
|
||||||
|
}
|
||||||
|
|
||||||
void profile_service_active_profile_snapshot(
|
void profile_service_active_profile_snapshot(
|
||||||
const ControllerIdentity& identity,
|
const ControllerIdentity& identity,
|
||||||
ProfileServiceActiveProfileSnapshot* output) {
|
ProfileServiceActiveProfileSnapshot* output) {
|
||||||
|
|
@ -269,5 +439,7 @@ int main() {
|
||||||
test_four_slot_cache_and_unchanged_generation();
|
test_four_slot_cache_and_unchanged_generation();
|
||||||
test_activation_disconnect_and_default_preservation();
|
test_activation_disconnect_and_default_preservation();
|
||||||
test_analog_thresholds_rumble_and_local_confirmation();
|
test_analog_thresholds_rumble_and_local_confirmation();
|
||||||
|
test_all_runtime_cancellation_causes();
|
||||||
|
test_runtime_slot_synthetic_isolation();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
465
tests/controller_synthetic_input_test.cpp
Normal file
465
tests/controller_synthetic_input_test.cpp
Normal file
|
|
@ -0,0 +1,465 @@
|
||||||
|
#include "controller_synthetic_input.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint8_t button_index(ControllerProfileLogicalButton button) {
|
||||||
|
return static_cast<uint8_t>(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t button_bit(ControllerProfileLogicalButton button) {
|
||||||
|
return static_cast<uint16_t>(1u << button_index(button));
|
||||||
|
}
|
||||||
|
|
||||||
|
void require(bool condition, const char* message) {
|
||||||
|
if (!condition) {
|
||||||
|
std::cerr << message << '\n';
|
||||||
|
std::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerState state_with_buttons(uint16_t mask) {
|
||||||
|
ControllerState state = controller_neutral_state();
|
||||||
|
controller_profile_apply_button_mask(mask, &state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_button(const ControllerProfileTransformResult& result,
|
||||||
|
ControllerProfileLogicalButton button) {
|
||||||
|
return (controller_profile_extract_button_mask(result.state) &
|
||||||
|
button_bit(button)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerProfile profile_with_macro(
|
||||||
|
ControllerProfileLogicalButton trigger,
|
||||||
|
ControllerProfileLogicalButton cancel =
|
||||||
|
ControllerProfileLogicalButton::kCapture) {
|
||||||
|
ControllerProfile profile =
|
||||||
|
controller_profile_default(controller_identity_global(), 0);
|
||||||
|
profile.macro_trigger = button_index(trigger);
|
||||||
|
profile.macro_cancel = button_index(cancel);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_end(ControllerProfile* profile, uint8_t index) {
|
||||||
|
profile->macro_steps[index] = {};
|
||||||
|
profile->macro_steps[index].type =
|
||||||
|
ControllerProfileMacroStepType::kEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_immediate_press_release_dpad_and_explicit_end() {
|
||||||
|
ControllerProfile profile =
|
||||||
|
profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||||
|
profile.macro_step_count = 3;
|
||||||
|
profile.macro_steps[0].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile.macro_steps[0].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile.macro_steps[0].duration_ms = 10;
|
||||||
|
profile.macro_steps[0].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kNorth) |
|
||||||
|
button_bit(ControllerProfileLogicalButton::kDpadUp);
|
||||||
|
profile.macro_steps[1].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile.macro_steps[1].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile.macro_steps[1].duration_ms = 20;
|
||||||
|
profile.macro_steps[1].output_button_mask = 0;
|
||||||
|
set_end(&profile, 2);
|
||||||
|
|
||||||
|
ControllerSyntheticInputContext context{};
|
||||||
|
ControllerState input =
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth));
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
controller_synthetic_input_apply(&context, input, profile, 100);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kNorth) &&
|
||||||
|
has_button(output, ControllerProfileLogicalButton::kDpadUp) &&
|
||||||
|
!has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"macro step zero was not immediate or its trigger leaked");
|
||||||
|
|
||||||
|
input = controller_neutral_state();
|
||||||
|
output = controller_synthetic_input_apply(&context, input, profile, 109);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kNorth) &&
|
||||||
|
has_button(output, ControllerProfileLogicalButton::kDpadUp),
|
||||||
|
"macro press state ended before its scheduled deadline");
|
||||||
|
output = controller_synthetic_input_apply(&context, input, profile, 110);
|
||||||
|
require(controller_profile_extract_button_mask(output.state) == 0,
|
||||||
|
"macro release state did not replace the full button mask");
|
||||||
|
|
||||||
|
input =
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kWest));
|
||||||
|
output = controller_synthetic_input_apply(&context, input, profile, 129);
|
||||||
|
require(!has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"active macro release state did not override physical buttons");
|
||||||
|
output = controller_synthetic_input_apply(&context, input, profile, 130);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kWest) &&
|
||||||
|
!context.macro_active,
|
||||||
|
"explicit end did not clear overrides and restore physical input");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_optional_field_overrides_and_motion_preservation() {
|
||||||
|
ControllerProfile profile =
|
||||||
|
profile_with_macro(ControllerProfileLogicalButton::kSelect);
|
||||||
|
profile.macro_step_count = 2;
|
||||||
|
ControllerProfileMacroStep& step = profile.macro_steps[0];
|
||||||
|
step.type = ControllerProfileMacroStepType::kState;
|
||||||
|
step.override_flags = kControllerProfileOverrideLeftStick |
|
||||||
|
kControllerProfileOverrideRightStick |
|
||||||
|
kControllerProfileOverrideLeftTrigger |
|
||||||
|
kControllerProfileOverrideRightTrigger;
|
||||||
|
step.duration_ms = 100;
|
||||||
|
step.left_stick_x = INT16_MIN;
|
||||||
|
step.left_stick_y = 1234;
|
||||||
|
step.right_stick_x = -2345;
|
||||||
|
step.right_stick_y = INT16_MAX;
|
||||||
|
step.left_trigger = 0;
|
||||||
|
step.right_trigger = UINT16_MAX;
|
||||||
|
set_end(&profile, 1);
|
||||||
|
|
||||||
|
ControllerState input = state_with_buttons(
|
||||||
|
button_bit(ControllerProfileLogicalButton::kSelect) |
|
||||||
|
button_bit(ControllerProfileLogicalButton::kSouth));
|
||||||
|
input.left_stick_x = 10;
|
||||||
|
input.left_stick_y = 20;
|
||||||
|
input.right_stick_x = 30;
|
||||||
|
input.right_stick_y = 40;
|
||||||
|
input.left_trigger = 111;
|
||||||
|
input.right_trigger = 222;
|
||||||
|
input.motion_sample_count = 2;
|
||||||
|
input.motion_samples[0] = {1, 2, 3, 4, 5, 6};
|
||||||
|
input.motion_samples[1] = {-1, -2, -3, -4, -5, -6};
|
||||||
|
|
||||||
|
ControllerSyntheticInputContext context{};
|
||||||
|
const ControllerProfileTransformResult output =
|
||||||
|
controller_synthetic_input_apply(&context, input, profile, 5);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kSouth) &&
|
||||||
|
!has_button(output, ControllerProfileLogicalButton::kSelect),
|
||||||
|
"a trigger-only macro override changed buttons or leaked trigger");
|
||||||
|
require(output.state.left_stick_x == INT16_MIN &&
|
||||||
|
output.state.left_stick_y == 1234 &&
|
||||||
|
output.state.right_stick_x == -2345 &&
|
||||||
|
output.state.right_stick_y == INT16_MAX &&
|
||||||
|
output.state.left_trigger == 0 &&
|
||||||
|
output.state.right_trigger == UINT16_MAX,
|
||||||
|
"one or more optional macro fields were not overridden");
|
||||||
|
require(output.state.motion_sample_count == input.motion_sample_count &&
|
||||||
|
std::memcmp(output.state.motion_samples,
|
||||||
|
input.motion_samples,
|
||||||
|
sizeof(input.motion_samples)) == 0,
|
||||||
|
"synthetic processing changed motion samples");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_zero_max_wait_and_scheduled_catch_up() {
|
||||||
|
ControllerProfile profile =
|
||||||
|
profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||||
|
profile.macro_step_count = 4;
|
||||||
|
profile.macro_steps[0].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile.macro_steps[0].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile.macro_steps[0].duration_ms = 0;
|
||||||
|
profile.macro_steps[0].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||||
|
profile.macro_steps[1].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile.macro_steps[1].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile.macro_steps[1].duration_ms = CONTROLLER_PROFILE_MAX_WAIT_MS;
|
||||||
|
profile.macro_steps[1].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kEast);
|
||||||
|
profile.macro_steps[2].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile.macro_steps[2].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile.macro_steps[2].duration_ms = 0;
|
||||||
|
profile.macro_steps[2].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kWest);
|
||||||
|
set_end(&profile, 3);
|
||||||
|
|
||||||
|
ControllerSyntheticInputContext context{};
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
controller_synthetic_input_apply(
|
||||||
|
&context,
|
||||||
|
state_with_buttons(
|
||||||
|
button_bit(ControllerProfileLogicalButton::kSouth)),
|
||||||
|
profile, 0);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||||
|
"zero-wait step zero was skipped on its trigger report");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 0);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kEast),
|
||||||
|
"zero wait did not advance on the next scheduler observation");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile,
|
||||||
|
CONTROLLER_PROFILE_MAX_WAIT_MS - 1u);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kEast),
|
||||||
|
"maximum legal wait expired early");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile,
|
||||||
|
CONTROLLER_PROFILE_MAX_WAIT_MS);
|
||||||
|
require(controller_profile_extract_button_mask(output.state) == 0 &&
|
||||||
|
!context.macro_active,
|
||||||
|
"zero-duration catch-up did not reach the explicit end");
|
||||||
|
|
||||||
|
profile.macro_step_count = 4;
|
||||||
|
profile.macro_steps[0].duration_ms = 10;
|
||||||
|
profile.macro_steps[0].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||||
|
profile.macro_steps[1].duration_ms = 20;
|
||||||
|
profile.macro_steps[1].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kEast);
|
||||||
|
profile.macro_steps[2].duration_ms = 30;
|
||||||
|
profile.macro_steps[2].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kWest);
|
||||||
|
context = {};
|
||||||
|
(void)controller_synthetic_input_apply(
|
||||||
|
&context,
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth)),
|
||||||
|
profile, 100);
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 145);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"catch-up used observation time instead of prior deadlines");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 1000);
|
||||||
|
require(!context.macro_active &&
|
||||||
|
controller_profile_extract_button_mask(output.state) == 0,
|
||||||
|
"large time jump did not finish the bounded macro");
|
||||||
|
|
||||||
|
profile.macro_step_count = 2;
|
||||||
|
profile.macro_steps[0].duration_ms = 10;
|
||||||
|
profile.macro_steps[0].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||||
|
set_end(&profile, 1);
|
||||||
|
context = {};
|
||||||
|
constexpr uint32_t macro_near_wrap = UINT32_MAX - 5u;
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context,
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth)),
|
||||||
|
profile, macro_near_wrap);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||||
|
"macro did not start immediately near uint32 wrap");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 3);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||||
|
"macro deadline expired early across uint32 wrap");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 4);
|
||||||
|
require(!context.macro_active &&
|
||||||
|
controller_profile_extract_button_mask(output.state) == 0,
|
||||||
|
"macro deadline was not uint32-wrap safe");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_consumption_cancel_precedence_and_duplicate_contributors() {
|
||||||
|
ControllerProfile profile =
|
||||||
|
profile_with_macro(ControllerProfileLogicalButton::kSelect,
|
||||||
|
ControllerProfileLogicalButton::kCapture);
|
||||||
|
profile.macro_step_count = 2;
|
||||||
|
profile.macro_steps[0].type =
|
||||||
|
ControllerProfileMacroStepType::kState;
|
||||||
|
profile.macro_steps[0].override_flags =
|
||||||
|
kControllerProfileOverrideButtons;
|
||||||
|
profile.macro_steps[0].duration_ms = 100;
|
||||||
|
profile.macro_steps[0].output_button_mask =
|
||||||
|
button_bit(ControllerProfileLogicalButton::kDpadLeft);
|
||||||
|
set_end(&profile, 1);
|
||||||
|
profile.button_map[button_index(ControllerProfileLogicalButton::kSouth)] =
|
||||||
|
button_index(ControllerProfileLogicalButton::kNorth);
|
||||||
|
profile.button_map[button_index(ControllerProfileLogicalButton::kEast)] =
|
||||||
|
button_index(ControllerProfileLogicalButton::kNorth);
|
||||||
|
profile.turbo_modes[button_index(ControllerProfileLogicalButton::kSouth)] =
|
||||||
|
ControllerProfileTurboMode::kTurbo;
|
||||||
|
|
||||||
|
ControllerSyntheticInputContext context{};
|
||||||
|
ControllerState input = state_with_buttons(
|
||||||
|
button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||||
|
button_bit(ControllerProfileLogicalButton::kEast));
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
controller_synthetic_input_apply(&context, input, profile, 0);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||||
|
"duplicate mapped contributors were not ORed");
|
||||||
|
output = controller_synthetic_input_apply(&context, input, profile, 34);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||||
|
"Turbo gating erased a duplicate physical contributor");
|
||||||
|
|
||||||
|
input = state_with_buttons(
|
||||||
|
button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||||
|
button_bit(ControllerProfileLogicalButton::kSelect));
|
||||||
|
output = controller_synthetic_input_apply(&context, input, profile, 35);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kDpadLeft) &&
|
||||||
|
!has_button(output, ControllerProfileLogicalButton::kNorth) &&
|
||||||
|
!has_button(output, ControllerProfileLogicalButton::kSelect),
|
||||||
|
"macro button override did not outrank Turbo or consume trigger");
|
||||||
|
|
||||||
|
input = state_with_buttons(
|
||||||
|
button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||||
|
button_bit(ControllerProfileLogicalButton::kCapture));
|
||||||
|
output = controller_synthetic_input_apply(&context, input, profile, 36);
|
||||||
|
require(!context.macro_active &&
|
||||||
|
!has_button(output, ControllerProfileLogicalButton::kCapture) &&
|
||||||
|
has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||||
|
"configured cancel leaked or failed to clear macro state");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_turbo_rate_release_and_uint32_wrap() {
|
||||||
|
ControllerProfile profile =
|
||||||
|
controller_profile_default(controller_identity_global(), 0);
|
||||||
|
const uint8_t south =
|
||||||
|
button_index(ControllerProfileLogicalButton::kSouth);
|
||||||
|
profile.turbo_modes[south] = ControllerProfileTurboMode::kTurbo;
|
||||||
|
const ControllerState pressed =
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth));
|
||||||
|
ControllerSyntheticInputContext context{};
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
controller_synthetic_input_apply(&context, pressed, profile, 0);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"Turbo did not begin in its ON phase");
|
||||||
|
|
||||||
|
constexpr std::array<uint32_t, 4> deltas = {17, 29, 11, 23};
|
||||||
|
uint32_t now_ms = 0;
|
||||||
|
size_t delta_index = 0;
|
||||||
|
bool previous_on = true;
|
||||||
|
unsigned completed_activations = 0;
|
||||||
|
while (now_ms < 1000) {
|
||||||
|
uint32_t delta = deltas[delta_index++ % deltas.size()];
|
||||||
|
if (delta > 1000 - now_ms) {
|
||||||
|
delta = 1000 - now_ms;
|
||||||
|
}
|
||||||
|
now_ms += delta;
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, pressed, profile, now_ms);
|
||||||
|
const bool on =
|
||||||
|
has_button(output, ControllerProfileLogicalButton::kSouth);
|
||||||
|
if (previous_on && !on) {
|
||||||
|
++completed_activations;
|
||||||
|
}
|
||||||
|
previous_on = on;
|
||||||
|
}
|
||||||
|
require(completed_activations == 15 && previous_on,
|
||||||
|
"irregular ticks did not produce exactly 15 activations per second");
|
||||||
|
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 1001);
|
||||||
|
require(!has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"Turbo release left a stuck output");
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 1002);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"Turbo repress did not restart in the ON phase");
|
||||||
|
|
||||||
|
context = {};
|
||||||
|
constexpr uint32_t near_wrap = UINT32_MAX - 10u;
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, pressed, profile, near_wrap);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"Turbo wrap test did not start ON");
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 23);
|
||||||
|
require(!has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"Turbo phase accumulation was not uint32-wrap safe");
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 56);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"Turbo remainder was lost across uint32 wrap");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_auto_burst_toggle_cancel_and_external_cancel() {
|
||||||
|
ControllerProfile profile =
|
||||||
|
profile_with_macro(ControllerProfileLogicalButton::kSelect,
|
||||||
|
ControllerProfileLogicalButton::kEast);
|
||||||
|
const uint8_t west = button_index(ControllerProfileLogicalButton::kWest);
|
||||||
|
profile.turbo_modes[west] = ControllerProfileTurboMode::kAutoBurst;
|
||||||
|
const ControllerState pressed =
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kWest));
|
||||||
|
ControllerSyntheticInputContext context{};
|
||||||
|
|
||||||
|
ControllerProfileTransformResult output =
|
||||||
|
controller_synthetic_input_apply(&context, pressed, profile, 0);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"Auto Burst did not toggle on in the ON phase");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 1);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"Auto Burst stopped when its physical input was released");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 34);
|
||||||
|
require(!has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"Auto Burst did not enter its OFF phase");
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 35);
|
||||||
|
require(!has_button(output, ControllerProfileLogicalButton::kWest) &&
|
||||||
|
!context.bindings[west].active,
|
||||||
|
"second Auto Burst rising press did not toggle it off");
|
||||||
|
|
||||||
|
(void)controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 36);
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 40);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"Auto Burst did not toggle on a second time");
|
||||||
|
output = controller_synthetic_input_apply(
|
||||||
|
&context,
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kEast)),
|
||||||
|
profile, 41);
|
||||||
|
require(!has_button(output, ControllerProfileLogicalButton::kWest) &&
|
||||||
|
!has_button(output, ControllerProfileLogicalButton::kEast) &&
|
||||||
|
!context.bindings[west].active,
|
||||||
|
"macro cancel did not clear all Auto Burst state or was not consumed");
|
||||||
|
|
||||||
|
(void)controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 42);
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 50);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"Auto Burst could not restart after configured cancellation");
|
||||||
|
controller_synthetic_input_cancel(
|
||||||
|
&context, button_bit(ControllerProfileLogicalButton::kWest));
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 51);
|
||||||
|
require(!has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"external cancel retriggered an already-held Auto Burst input");
|
||||||
|
(void)controller_synthetic_input_apply(
|
||||||
|
&context, controller_neutral_state(), profile, 52);
|
||||||
|
output = controller_synthetic_input_apply(&context, pressed, profile, 53);
|
||||||
|
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||||
|
"Auto Burst did not restart after release following cancellation");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_four_contexts_are_isolated() {
|
||||||
|
ControllerProfile profile =
|
||||||
|
controller_profile_default(controller_identity_global(), 0);
|
||||||
|
profile.turbo_modes[button_index(ControllerProfileLogicalButton::kSouth)] =
|
||||||
|
ControllerProfileTurboMode::kAutoBurst;
|
||||||
|
std::array<ControllerSyntheticInputContext, 4> contexts{};
|
||||||
|
const ControllerState pressed =
|
||||||
|
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth));
|
||||||
|
|
||||||
|
ControllerProfileTransformResult slot0 =
|
||||||
|
controller_synthetic_input_apply(&contexts[0], pressed, profile, 0);
|
||||||
|
ControllerProfileTransformResult slot1 = controller_synthetic_input_apply(
|
||||||
|
&contexts[1], controller_neutral_state(), profile, 0);
|
||||||
|
require(has_button(slot0, ControllerProfileLogicalButton::kSouth) &&
|
||||||
|
!has_button(slot1, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"synthetic activation leaked into another slot");
|
||||||
|
(void)controller_synthetic_input_apply(
|
||||||
|
&contexts[0], controller_neutral_state(), profile, 1);
|
||||||
|
slot1 = controller_synthetic_input_apply(&contexts[1], pressed, profile, 10);
|
||||||
|
controller_synthetic_input_cancel(&contexts[0]);
|
||||||
|
slot0 = controller_synthetic_input_apply(
|
||||||
|
&contexts[0], controller_neutral_state(), profile, 11);
|
||||||
|
slot1 = controller_synthetic_input_apply(
|
||||||
|
&contexts[1], controller_neutral_state(), profile, 11);
|
||||||
|
require(!has_button(slot0, ControllerProfileLogicalButton::kSouth) &&
|
||||||
|
has_button(slot1, ControllerProfileLogicalButton::kSouth),
|
||||||
|
"cancelling one slot changed another slot's Auto Burst state");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_immediate_press_release_dpad_and_explicit_end();
|
||||||
|
test_optional_field_overrides_and_motion_preservation();
|
||||||
|
test_zero_max_wait_and_scheduled_catch_up();
|
||||||
|
test_consumption_cancel_precedence_and_duplicate_contributors();
|
||||||
|
test_turbo_rate_release_and_uint32_wrap();
|
||||||
|
test_auto_burst_toggle_cancel_and_external_cancel();
|
||||||
|
test_four_contexts_are_isolated();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ def test_controller_profile_runtime_native(tmp_path: Path) -> None:
|
||||||
str(root / "controller_identity.cpp"),
|
str(root / "controller_identity.cpp"),
|
||||||
str(root / "controller_profile.cpp"),
|
str(root / "controller_profile.cpp"),
|
||||||
str(root / "controller_profile_transform.cpp"),
|
str(root / "controller_profile_transform.cpp"),
|
||||||
|
str(root / "controller_synthetic_input.cpp"),
|
||||||
str(root / "controller_profile_runtime.cpp"),
|
str(root / "controller_profile_runtime.cpp"),
|
||||||
"-o",
|
"-o",
|
||||||
str(executable),
|
str(executable),
|
||||||
|
|
|
||||||
32
tests/test_controller_synthetic_input_native.py
Normal file
32
tests/test_controller_synthetic_input_native.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_controller_synthetic_input_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_synthetic_input_test"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
compiler,
|
||||||
|
"-std=c++17",
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
"-Werror",
|
||||||
|
"-pedantic",
|
||||||
|
f"-I{root}",
|
||||||
|
str(root / "tests" / "controller_synthetic_input_test.cpp"),
|
||||||
|
str(root / "controller_identity.cpp"),
|
||||||
|
str(root / "controller_profile.cpp"),
|
||||||
|
str(root / "controller_profile_transform.cpp"),
|
||||||
|
str(root / "controller_synthetic_input.cpp"),
|
||||||
|
"-o",
|
||||||
|
str(executable),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
cwd=root,
|
||||||
|
)
|
||||||
|
subprocess.run([str(executable)], check=True, cwd=root)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue