Files
ARMSX2/pcsx2/Input/InputManager.cpp
Brian Degenhardt 3e56da7f86 Merge upstream PCSX2 (2026-07-15 .. 2026-08-10)
71 commits from 474ad59818 to 2cf8dabe6b, triaged rather than taken wholesale.

Declined, resolved to ours:

- AGENTS.md: upstream's AI-agent instructions; we carry our own and do not
  want a second, conflicting policy file.
- CI deps bump (setup-node, labeler): both target workflows are absent here,
  and the labeler job is gated on the repository being PCSX2/pcsx2.
- KDDockWidgets 2.4.1: two of the six files do not exist here; we already
  build 2.4.0 against a 2.3.0 floor, so there is nothing to gain.
- The FullscreenUI Achievements-layout realignment: our section already
  carries the same settings, and ours is the branded copy.
- The GS draw/vertex-buffer cluster (7887919e74, b2fa00844e, 99cfbb49c1,
  5c611f85e1, 9945046a49, af48193ebb, d88510e3a6, 8c1bb5742e). Our vertex
  kick is an ARM64 rewrite of the same hot path -- register-resident cursor,
  fused min/max with a rewind watermark, and a scalar cull mirror that
  dual-issues against the NEON parse -- so upstream's generic pointer-logic
  optimisation is a variant of work already banked here, and their growth
  restructure replaces per-buffer capacity with a single global value, which
  the pooled draw-node model cannot express. Two of the four August commits
  in that cluster repair regressions the July rewrite introduced, and the
  third's genuine fix (staging arrays sized from an unrelated buffer) we had
  already made independently.

Taken with adjustment:

- EATAN coefficients (aae9438f98). Upstream relabelled mVU_Globals so the
  names match the powers; we had fixed the same defect by ordering the arm64
  call sites by power instead. Both fixes are correct alone and CANCEL when
  combined, so the arm64 call sites move to plain ascending order in the same
  commit. The values never moved, so this emits an identical instruction
  sequence. Their fix also repairs the x86 mVU we still carry.
- Shader cache version: upstream numbered their tfx.glsl change 109, which is
  below our 110. Taking their value would hand every user a stale blob, so
  this lands as 111.
- FullscreenUI: took the two readback-spin toggles, placed outside our
  non-Apple guard rather than inside upstream's unguarded run.
- Restored tools/generate_fullscreen_ui_translation_strings.py, dropped by
  431ca0c063, and regenerated both string areas. That also registers the Big
  Picture setup-wizard strings, which had never been extractable.

GameDB: the three serials upstream gave gsHWFixes (SLES-53869, PAPX-90020,
SCPS-15064) are absent from the mobile overlay, so no fix is silently erased
on handhelds.
2026-08-10 18:24:24 -07:00

1925 lines
64 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "ImGui/ImGuiManager.h"
#include "Input/InputManager.h"
#include "Input/InputSource.h"
#include "SIO/Pad/Pad.h"
#include "SIO/Sio.h"
#include "USB/USB.h"
#include "VMManager.h"
#include "LayeredSettingsInterface.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include "IconsPromptFont.h"
#include "fmt/format.h"
#include <array>
#include <atomic>
#include <memory>
#include <mutex>
#include <sstream>
#include <unordered_map>
#include <variant>
#include <vector>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
// ------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------
enum : u32
{
MAX_KEYS_PER_BINDING = 4,
MAX_MOTORS_PER_PAD = 2,
FIRST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Pointer) + 1u,
LAST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Count),
};
// ------------------------------------------------------------------------
// Event Handler Type
// ------------------------------------------------------------------------
// This class acts as an adapter to convert from normalized values to
// binary values when the callback is a binary/button handler. That way
// you don't need to convert float->bool in your callbacks.
using InputEventHandler = std::variant<InputAxisEventHandler, InputButtonEventHandler>;
// ------------------------------------------------------------------------
// Binding Type
// ------------------------------------------------------------------------
// This class tracks both the keys which make it up (for chords), as well
// as the state of all buttons. For button callbacks, it's fired when
// all keys go active, and for axis callbacks, when all are active and
// the value changes.
struct InputBinding
{
InputBindingKey keys[MAX_KEYS_PER_BINDING] = {};
InputEventHandler handler;
u8 num_keys = 0;
u8 full_mask = 0;
u8 current_mask = 0;
};
struct PadVibrationBinding
{
struct Motor
{
InputBindingKey binding;
u64 last_update_time;
InputSource* source;
float last_intensity;
};
u32 pad_index = 0;
Motor motors[MAX_MOTORS_PER_PAD] = {};
/// Returns true if the two motors are bound to the same host motor.
__fi bool AreMotorsCombined() const { return motors[0].binding == motors[1].binding; }
/// Returns the intensity when both motors are combined.
__fi float GetCombinedIntensity() const { return std::max(motors[0].last_intensity, motors[1].last_intensity); }
};
// ------------------------------------------------------------------------
// Forward Declarations (for static qualifier)
// ------------------------------------------------------------------------
namespace InputManager
{
static std::optional<InputBindingKey> ParseHostKeyboardKey(const std::string_view source, const std::string_view sub_binding);
static std::optional<InputBindingKey> ParsePointerKey(const std::string_view source, const std::string_view sub_binding);
static TinyString ConvertKeyboardKeyToString(InputBindingKey key, bool display = false);
static TinyString ConvertPointerKeyToString(InputBindingKey key, bool display = false);
static bool SplitBinding(const std::string_view binding, std::string_view* source, std::string_view* sub_binding);
static void PrettifyInputBindingPart(const std::string_view binding, SmallString& ret, bool& changed, bool use_icons);
static std::shared_ptr<InputBinding> AddBinding(const std::string_view binding, const InputEventHandler& handler);
// Will also apply SDL2-SDL3 migrations and update the provided section & key
static void AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler,
InputBindingInfo::Type binding_type, SettingsInterface& si, const char* section, const char* key, bool is_profile);
static bool ParseBindingAndGetSource(const std::string_view binding, InputBindingKey* key, InputSource** source);
static bool IsAxisHandler(const InputEventHandler& handler);
static float ApplySingleBindingScale(float sensitivity, float deadzone, float value);
static void AddHotkeyBindings(SettingsInterface& si, bool is_profile);
static void AddPadBindings(SettingsInterface& si, u32 pad, bool is_profile, SettingsInterface* nav_si);
static void AddUSBBindings(SettingsInterface& si, u32 port, bool is_profile);
static void UpdateContinuedVibration();
static void GenerateRelativeMouseEvents();
static bool DoEventHook(InputBindingKey key, float value);
static bool PreprocessEvent(InputBindingKey key, float value, GenericInputBinding generic_key,
GenericInputBinding axis_neg_key, GenericInputBinding axis_pos_key);
static bool ProcessEvent(InputBindingKey key, float value, bool skip_button_handlers);
template <typename T>
static void UpdateInputSourceState(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock, InputSourceType type);
} // namespace InputManager
// ------------------------------------------------------------------------
// Local Variables
// ------------------------------------------------------------------------
// This is a multimap containing any binds related to the specified key.
using BindingMap = std::unordered_multimap<InputBindingKey, std::shared_ptr<InputBinding>, InputBindingKeyHash>;
using VibrationBindingArray = std::vector<PadVibrationBinding>;
static BindingMap s_binding_map;
static VibrationBindingArray s_pad_vibration_array;
static std::mutex s_binding_map_write_lock;
// Reverse lookups for controller navigation; user bindings take priority over static defaults.
using ControllerButtonGenericMap = std::unordered_map<InputBindingKey, GenericInputBinding, InputBindingKeyHash>;
static ControllerButtonGenericMap s_controller_button_generic_map;
using ControllerAxisGenericMap = std::unordered_map<InputBindingKey, std::array<GenericInputBinding, 2>, InputBindingKeyHash>;
static ControllerAxisGenericMap s_controller_axis_generic_map;
// Hooks/intercepting (for setting bindings)
static std::mutex m_event_intercept_mutex;
static InputInterceptHook::Callback m_event_intercept_callback;
// Input sources. Keyboard/mouse don't exist here.
static std::array<std::unique_ptr<InputSource>, static_cast<u32>(InputSourceType::Count)> s_input_sources;
// Layout preference for gamepad controller glyphs.
static std::atomic<InputLayout> s_gamepad_icon_preference = InputLayout::Unknown;
// ------------------------------------------------------------------------
// Hotkeys
// ------------------------------------------------------------------------
static const HotkeyInfo* const s_hotkey_list[] = {g_common_hotkeys, g_gs_hotkeys, g_host_hotkeys};
// ------------------------------------------------------------------------
// Tracking host mouse movement and turning into relative events
// 4 axes: pointer left/right, wheel vertical/horizontal. Last/Next/Normalized.
// ------------------------------------------------------------------------
static constexpr const std::array<const char*, static_cast<u8>(InputPointerAxis::Count)> s_pointer_axis_setting_names = {
{"X", "Y", "WheelX", "WheelY"}};
static constexpr const std::array<const char*, static_cast<u8>(InputPointerAxis::Count)> s_pointer_axis_names = {
{"X", "Y", "Wheel X", "Wheel Y"}};
static constexpr const std::array<const char*, 3> s_pointer_button_setting_names = {{"LeftButton", "RightButton", "MiddleButton"}};
static constexpr const std::array<const char*, 3> s_pointer_button_names = {{"Left Button", "Right Button", "Middle Button"}};
struct PointerAxisState
{
std::atomic<s32> delta;
float last_value;
};
static std::array<std::array<float, static_cast<u8>(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES> s_host_pointer_positions;
static std::array<std::array<PointerAxisState, static_cast<u8>(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES>
s_pointer_state;
static std::array<float, 2> s_pointer_axis_speed;
static std::array<float, 2> s_pointer_axis_dead_zone;
static std::array<float, 2> s_pointer_axis_range;
static std::array<float, 2> s_pointer_pos = {0.0f, 0.0f};
static float s_pointer_inertia = 0.0f;
using PointerMoveCallback = std::function<void(InputBindingKey key, float value)>;
using KeyboardEventCallback = std::function<void(InputBindingKey key, float value)>;
static std::vector<KeyboardEventCallback> s_keyboard_event_callbacks;
static std::vector<std::pair<u32, PointerMoveCallback>> s_pointer_move_callbacks;
// ------------------------------------------------------------------------
// Binding Parsing
// ------------------------------------------------------------------------
std::vector<std::string_view> InputManager::SplitChord(const std::string_view binding)
{
std::vector<std::string_view> parts;
// under an if for RVO
if (!binding.empty())
{
std::string_view::size_type last = 0;
std::string_view::size_type next;
while ((next = binding.find('&', last)) != std::string_view::npos)
{
if (last != next)
{
std::string_view part(StringUtil::StripWhitespace(binding.substr(last, next - last)));
if (!part.empty())
parts.push_back(std::move(part));
}
last = next + 1;
}
if (last < (binding.size() - 1))
{
std::string_view part(StringUtil::StripWhitespace(binding.substr(last)));
if (!part.empty())
parts.push_back(std::move(part));
}
}
return parts;
}
bool InputManager::SplitBinding(const std::string_view binding, std::string_view* source, std::string_view* sub_binding)
{
const std::string_view::size_type slash_pos = binding.find('/');
if (slash_pos == std::string_view::npos)
{
Console.Warning("Malformed binding: '%.*s'", static_cast<int>(binding.size()), binding.data());
return false;
}
*source = std::string_view(binding).substr(0, slash_pos);
*sub_binding = std::string_view(binding).substr(slash_pos + 1);
return true;
}
std::optional<InputBindingKey> InputManager::ParseInputBindingKey(const std::string_view binding)
{
std::string_view source, sub_binding;
if (!SplitBinding(binding, &source, &sub_binding))
return std::nullopt;
// lameee, string matching
if (source.starts_with("Keyboard"))
{
return ParseHostKeyboardKey(source, sub_binding);
}
else if (source.starts_with("Pointer"))
{
return ParsePointerKey(source, sub_binding);
}
else
{
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i]->IsInitialized())
{
std::optional<InputBindingKey> key = s_input_sources[i]->ParseKeyString(source, sub_binding);
if (key.has_value())
return key;
}
}
}
return std::nullopt;
}
bool InputManager::ParseBindingAndGetSource(const std::string_view binding, InputBindingKey* key, InputSource** source)
{
std::string_view source_string, sub_binding;
if (!SplitBinding(binding, &source_string, &sub_binding))
return false;
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i]->IsInitialized())
{
std::optional<InputBindingKey> parsed_key = s_input_sources[i]->ParseKeyString(source_string, sub_binding);
if (parsed_key.has_value())
{
*key = parsed_key.value();
*source = s_input_sources[i].get();
return true;
}
}
}
return false;
}
TinyString InputManager::ConvertKeyboardKeyToString(InputBindingKey key, bool display)
{
TinyString ret;
if (key.source_type == InputSourceType::Keyboard)
{
const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data));
if (str.has_value() && !str->empty())
{
if (display)
// Keyboard keys arn't spaced out for display yet
ret.format("Keyboard {}", str->c_str());
else
ret.format("Keyboard/{}", str->c_str());
}
}
return ret;
}
TinyString InputManager::ConvertPointerKeyToString(InputBindingKey key, bool display)
{
TinyString ret;
if (key.source_type == InputSourceType::Pointer)
{
if (key.source_subtype == InputSubclass::PointerButton)
{
if (display)
{
if (key.data < s_pointer_button_setting_names.size())
ret.format("Pointer-{} {}", u32{key.source_index}, s_pointer_button_names[key.data]);
else
ret.format("Pointer-{} Button{}", u32{key.source_index}, key.data + 1);
}
else
{
if (key.data < s_pointer_button_setting_names.size())
ret.format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_setting_names[key.data]);
else
ret.format("Pointer-{}/Button{}", u32{key.source_index}, key.data);
}
}
else if (key.source_subtype == InputSubclass::PointerAxis)
{
if (display)
ret.format("Pointer-{} {}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data],
key.modifier == InputModifier::Negate ? '-' : '+');
else
ret.format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_setting_names[key.data],
key.modifier == InputModifier::Negate ? '-' : '+');
}
}
return ret;
}
std::string InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key, bool migration)
{
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::Device)
{
// pointer and device bindings don't have a data part
if (key.source_type == InputSourceType::Keyboard)
{
return "Keyboard";
}
else if (key.source_type == InputSourceType::Pointer)
{
return GetPointerDeviceName(key.data);
}
else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
{
// This assumes that it always follows the Type/Binding form.
std::string keystr(s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key));
std::string::size_type pos = keystr.find('/');
if (pos != std::string::npos)
keystr.erase(pos);
return keystr;
}
}
else
{
if (key.source_type == InputSourceType::Keyboard)
{
return std::string(ConvertKeyboardKeyToString(key));
}
else if (key.source_type == InputSourceType::Pointer)
{
return std::string(ConvertPointerKeyToString(key));
}
else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
{
return std::string(s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key, false, migration));
}
}
return {};
}
std::string InputManager::ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type, const InputBindingKey* keys, size_t num_keys, bool migration)
{
// can't have a chord of devices/pointers
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::Device)
{
// so only take the first
if (num_keys > 0)
return ConvertInputBindingKeyToString(binding_type, keys[0], migration);
}
std::stringstream ss;
for (size_t i = 0; i < num_keys; i++)
{
const std::string keystr(ConvertInputBindingKeyToString(binding_type, keys[i], migration));
if (keystr.empty())
return std::string();
if (i > 0)
ss << " & ";
ss << keystr;
}
return ss.str();
}
bool InputManager::PrettifyInputBinding(SmallStringBase& binding, bool use_icons)
{
if (binding.empty())
return false;
const std::string_view binding_view = binding.view();
SmallString ret;
bool changed = false;
std::string_view::size_type last = 0;
std::string_view::size_type next;
while ((next = binding_view.find('&', last)) != std::string_view::npos)
{
if (last != next)
{
const std::string_view part = StringUtil::StripWhitespace(binding_view.substr(last, next - last));
if (!part.empty())
{
if (!ret.empty())
ret.append(" + ");
PrettifyInputBindingPart(part, ret, changed, use_icons);
}
}
last = next + 1;
}
if (last < (binding_view.size() - 1))
{
const std::string_view part = StringUtil::StripWhitespace(binding_view.substr(last));
if (!part.empty())
{
if (!ret.empty())
ret.append(" + ");
PrettifyInputBindingPart(part, ret, changed, use_icons);
}
}
if (changed)
binding = ret;
return changed;
}
void InputManager::SetGamepadIconPreference(InputLayout layout)
{
s_gamepad_icon_preference.store(layout, std::memory_order_relaxed);
}
InputLayout InputManager::GetGamepadIconPreference()
{
return s_gamepad_icon_preference.load(std::memory_order_relaxed);
}
void InputManager::PrettifyInputBindingPart(const std::string_view binding, SmallString& ret, bool& changed, bool use_icons)
{
std::string_view source, sub_binding;
if (!SplitBinding(binding, &source, &sub_binding))
return;
// lameee, string matching
if (source.starts_with("Keyboard"))
{
std::optional<InputBindingKey> key = ParseHostKeyboardKey(source, sub_binding);
if (key.has_value())
{
if (use_icons)
{
const char* icon = ConvertHostKeyboardCodeToIcon(key->data);
if (icon)
{
ret.append(icon);
changed = true;
return;
}
else
{
ret.append(ConvertKeyboardKeyToString(key.value(), true));
changed = true;
return;
}
}
else
{
ret.append(ConvertKeyboardKeyToString(key.value(), true));
changed = true;
return;
}
}
}
else if (source.starts_with("Pointer"))
{
const std::optional<InputBindingKey> key = ParsePointerKey(source, sub_binding);
if (key.has_value())
{
if (use_icons && key->source_subtype == InputSubclass::PointerButton)
{
static constexpr const char* button_icons[] = {
ICON_PF_MOUSE_BUTTON_1,
ICON_PF_MOUSE_BUTTON_2,
ICON_PF_MOUSE_BUTTON_3,
ICON_PF_MOUSE_BUTTON_4,
ICON_PF_MOUSE_BUTTON_5,
};
if (key->data < std::size(button_icons))
ret.append(button_icons[key->data]);
else
ret.append(ConvertPointerKeyToString(key.value(), true));
}
else
ret.append(ConvertPointerKeyToString(key.value(), true));
changed = true;
return;
}
}
else
{
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
// We call ConvertKeyToIcon/String() even on disabled sources
// This ensures consistant appearance between enabled and disabled sources
if (s_input_sources[i])
{
std::optional<InputBindingKey> key = s_input_sources[i]->ParseKeyString(source, sub_binding);
if (key.has_value())
{
if (use_icons)
{
const TinyString icon = s_input_sources[i]->ConvertKeyToIcon(key.value());
if (!icon.empty())
ret.append(icon);
else
ret.append(s_input_sources[i]->ConvertKeyToString(key.value(), true));
}
else
ret.append(s_input_sources[i]->ConvertKeyToString(key.value(), true));
changed = true;
return;
}
}
}
}
ret.append(binding);
}
std::shared_ptr<InputBinding> InputManager::AddBinding(const std::string_view binding, const InputEventHandler& handler)
{
std::shared_ptr<InputBinding> ibinding;
const std::vector<std::string_view> chord_bindings(SplitChord(binding));
for (const std::string_view& chord_binding : chord_bindings)
{
std::optional<InputBindingKey> key = ParseInputBindingKey(chord_binding);
if (!key.has_value())
{
Console.WriteLn(fmt::format("Invalid binding: '{}'", binding));
ibinding.reset();
break;
}
if (!ibinding)
{
ibinding = std::make_shared<InputBinding>();
ibinding->handler = handler;
}
if (ibinding->num_keys == MAX_KEYS_PER_BINDING)
{
Console.WriteLn(fmt::format("Too many chord parts, max is {} ({})", static_cast<u32>(MAX_KEYS_PER_BINDING), binding));
ibinding.reset();
break;
}
ibinding->keys[ibinding->num_keys] = key.value();
ibinding->full_mask |= (static_cast<u8>(1) << ibinding->num_keys);
ibinding->num_keys++;
}
if (!ibinding)
return nullptr;
// plop it in the input map for all the keys
for (u32 i = 0; i < ibinding->num_keys; i++)
s_binding_map.emplace(ibinding->keys[i].MaskDirection(), ibinding);
return ibinding;
}
void InputManager::AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler,
InputBindingInfo::Type binding_type, SettingsInterface& si, const char* section, const char* key, bool is_profile)
{
std::vector<std::shared_ptr<InputBinding>> ibindings;
bool migrate = false;
for (const std::string& binding : bindings)
{
std::shared_ptr<InputBinding> ibinding = AddBinding(binding, handler);
ibindings.push_back(ibinding);
if (ibinding)
{
// Check for SDL2-3 migrations
for (u32 i = 0; i < ibinding->num_keys; i++)
{
if (ibinding->keys[i].needs_migration)
migrate = true;
}
}
}
// Save migrations
if (migrate)
{
std::vector<std::string> new_bindings;
new_bindings.reserve(bindings.size());
for (size_t i = 0; i < bindings.size(); i++)
{
if (ibindings[i])
new_bindings.push_back(ConvertInputBindingKeysToString(binding_type, ibindings[i]->keys, ibindings[i]->num_keys, true));
else
// Retain invalid bindings as is
new_bindings.push_back(bindings[i]);
}
if (is_profile)
{
// INISettingsInterface, can just update directly
si.SetStringList(section, key, new_bindings);
si.Save();
}
else
{
// LayeredSettingsInterface, Need to find which layer our binding came from
LayeredSettingsInterface& lsi = static_cast<LayeredSettingsInterface&>(si);
for (u32 i = 0; i < LayeredSettingsInterface::NUM_LAYERS; i++)
{
SettingsInterface* layer = lsi.GetLayer(static_cast<LayeredSettingsInterface::Layer>(i));
if (layer && layer->GetStringList(section, key) == bindings)
{
// Layer found, update settings
layer->SetStringList(section, key, new_bindings);
layer->Save();
}
}
}
}
}
// ------------------------------------------------------------------------
// Key Decoders
// ------------------------------------------------------------------------
InputBindingKey InputManager::MakeHostKeyboardKey(u32 key_code)
{
InputBindingKey key = {};
key.source_type = InputSourceType::Keyboard;
key.data = key_code;
return key;
}
InputBindingKey InputManager::MakePointerButtonKey(u32 index, u32 button_index)
{
InputBindingKey key = {};
key.source_index = index;
key.source_type = InputSourceType::Pointer;
key.source_subtype = InputSubclass::PointerButton;
key.data = button_index;
return key;
}
InputBindingKey InputManager::MakePointerAxisKey(u32 index, InputPointerAxis axis)
{
InputBindingKey key = {};
key.data = static_cast<u32>(axis);
key.source_index = index;
key.source_type = InputSourceType::Pointer;
key.source_subtype = InputSubclass::PointerAxis;
return key;
}
// ------------------------------------------------------------------------
// Bind Encoders
// ------------------------------------------------------------------------
static std::array<const char*, static_cast<u32>(InputSourceType::Count)> s_input_class_names = {{
"Keyboard",
"Mouse",
"SDL",
#ifdef _WIN32
"DInput",
"XInput",
#endif
}};
InputSource* InputManager::GetInputSourceInterface(InputSourceType type)
{
return s_input_sources[static_cast<u32>(type)].get();
}
const char* InputManager::InputSourceToString(InputSourceType clazz)
{
return s_input_class_names[static_cast<u32>(clazz)];
}
bool InputManager::GetInputSourceDefaultEnabled(InputSourceType type)
{
switch (type)
{
case InputSourceType::Keyboard:
case InputSourceType::Pointer:
case InputSourceType::SDL:
return true;
#ifdef _WIN32
case InputSourceType::DInput:
return false;
case InputSourceType::XInput:
return false;
#endif
default:
return false;
}
}
std::optional<InputSourceType> InputManager::ParseInputSourceString(const std::string_view str)
{
for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)
{
if (str == s_input_class_names[i])
return static_cast<InputSourceType>(i);
}
return std::nullopt;
}
std::optional<InputBindingKey> InputManager::ParseHostKeyboardKey(const std::string_view source, const std::string_view sub_binding)
{
if (source != "Keyboard")
return std::nullopt;
const std::optional<s32> code = ConvertHostKeyboardStringToCode(sub_binding);
if (!code.has_value())
return std::nullopt;
InputBindingKey key = {};
key.source_type = InputSourceType::Keyboard;
key.data = static_cast<u32>(code.value());
return key;
}
std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_view source, const std::string_view sub_binding)
{
const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(source.substr(8));
if (!pointer_index.has_value() || pointer_index.value() < 0)
return std::nullopt;
InputBindingKey key = {};
key.source_type = InputSourceType::Pointer;
key.source_index = static_cast<u32>(pointer_index.value());
if (sub_binding.starts_with("Button"))
{
const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
if (!button_number.has_value() || button_number.value() < 0)
return std::nullopt;
key.source_subtype = InputSubclass::PointerButton;
key.data = static_cast<u32>(button_number.value());
return key;
}
for (u32 i = 0; i < s_pointer_axis_setting_names.size(); i++)
{
if (sub_binding.starts_with(s_pointer_axis_setting_names[i]))
{
key.source_subtype = InputSubclass::PointerAxis;
key.data = i;
const std::string_view dir_part(sub_binding.substr(std::strlen(s_pointer_axis_setting_names[i])));
if (dir_part == "+")
key.modifier = InputModifier::None;
else if (dir_part == "-")
key.modifier = InputModifier::Negate;
else
return std::nullopt;
return key;
}
}
for (u32 i = 0; i < s_pointer_button_setting_names.size(); i++)
{
if (sub_binding == s_pointer_button_setting_names[i])
{
key.source_subtype = InputSubclass::PointerButton;
key.data = i;
return key;
}
}
return std::nullopt;
}
std::optional<u32> InputManager::GetIndexFromPointerBinding(const std::string_view source)
{
if (!source.starts_with("Pointer-"))
return std::nullopt;
const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(source.substr(8));
if (!pointer_index.has_value() || pointer_index.value() < 0)
return std::nullopt;
return static_cast<u32>(pointer_index.value());
}
std::string InputManager::GetPointerDeviceName(u32 pointer_index)
{
return fmt::format("Pointer-{}", pointer_index);
}
// ------------------------------------------------------------------------
// Binding Enumeration
// ------------------------------------------------------------------------
float InputManager::ApplySingleBindingScale(float scale, float deadzone, float value)
{
const float svalue = std::clamp(value * scale, 0.0f, 1.0f);
return (deadzone > 0.0f && svalue < deadzone) ? 0.0f : svalue;
}
std::vector<const HotkeyInfo*> InputManager::GetHotkeyList()
{
std::vector<const HotkeyInfo*> ret;
for (const HotkeyInfo* hotkey_list : s_hotkey_list)
{
for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
ret.push_back(hotkey);
}
return ret;
}
void InputManager::AddHotkeyBindings(SettingsInterface& si, bool is_profile)
{
for (const HotkeyInfo* hotkey_list : s_hotkey_list)
{
for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
{
const std::vector<std::string> bindings(si.GetStringList("Hotkeys", hotkey->name));
if (bindings.empty())
continue;
AddBindings(bindings, InputButtonEventHandler{hotkey->handler}, InputBindingInfo::Type::Button, si, "Hotkeys", hotkey->name, is_profile);
}
}
}
void InputManager::AddPadBindings(SettingsInterface& si, u32 pad_index, bool is_profile, SettingsInterface* nav_si)
{
const Pad::ControllerType type = EmuConfig.Pad.Ports[pad_index].Type;
// Don't bother checking macros/vibration if it's not a connected type.
if (type == Pad::ControllerType::NotConnected)
return;
// Or if it's a multitap port, and this multitap isn't enabled.
if (sioPadIsMultitapSlot(pad_index))
{
const auto& [mt_port, mt_slot] = sioConvertPadToPortAndSlot(pad_index);
if (!EmuConfig.Pad.IsMultitapPortEnabled(mt_port))
return;
}
const std::string section = Pad::GetConfigSection(pad_index);
const Pad::ControllerInfo* cinfo = Pad::GetControllerInfo(type);
pxAssert(cinfo);
for (const InputBindingInfo& bi : cinfo->bindings)
{
switch (bi.bind_type)
{
case InputBindingInfo::Type::Button:
case InputBindingInfo::Type::Axis:
case InputBindingInfo::Type::HalfAxis:
{
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bi.name));
if (!bindings.empty())
{
// we use axes for all pad bindings to simplify things, and because they are pressure sensitive
const float sensitivity = si.GetFloatValue(section.c_str(), fmt::format("{}Scale", bi.name).c_str(), 1.0f);
const float deadzone = si.GetFloatValue(section.c_str(), fmt::format("{}Deadzone", bi.name).c_str(), 0.0f);
AddBindings(
bindings, InputAxisEventHandler{[pad_index, bind_index = bi.bind_index, sensitivity, deadzone](InputBindingKey key, float value) {
Pad::SetControllerState(pad_index, bind_index, ApplySingleBindingScale(sensitivity, deadzone, value));
}},
bi.bind_type, si, section.c_str(), bi.name, is_profile);
}
// So per-game bindings never affect FullscreenUI navigation.
if (nav_si && bi.generic_mapping != GenericInputBinding::Unknown)
{
for (const std::string& binding_str : nav_si->GetStringList(section.c_str(), bi.name))
{
InputBindingKey bkey;
InputSource* bsrc;
if (!ParseBindingAndGetSource(binding_str, &bkey, &bsrc))
continue;
if (bkey.source_subtype == InputSubclass::ControllerButton)
{
s_controller_button_generic_map.emplace(bkey.MaskDirection(), bi.generic_mapping);
}
else if (bkey.source_subtype == InputSubclass::ControllerAxis)
{
auto& entry = s_controller_axis_generic_map[bkey.MaskDirection()];
// Negate modifier = negative half of axis (e.g. "-Axis0" → LeftStickLeft)
if (bkey.modifier == InputModifier::Negate)
entry[0] = bi.generic_mapping;
else
entry[1] = bi.generic_mapping;
}
}
}
}
break;
// TODO: Move vibration motors in here.
default:
break;
}
}
for (u32 macro_button_index = 0; macro_button_index < Pad::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_button_index++)
{
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), fmt::format("Macro{}", macro_button_index + 1).c_str()));
if (!bindings.empty())
{
const float deadzone = si.GetFloatValue(section.c_str(), fmt::format("Macro{}Deadzone", macro_button_index + 1).c_str(), 0.0f);
AddBindings(
bindings, InputAxisEventHandler{[pad_index, macro_button_index, deadzone](InputBindingKey key, float value) {
const bool state = (value > deadzone);
Pad::SetMacroButtonState(key, pad_index, macro_button_index, state);
}},
InputBindingInfo::Type::Macro, si, section.c_str(), fmt::format("Macro{}", macro_button_index + 1).c_str(), is_profile);
}
}
if (cinfo->vibration_caps != Pad::VibrationCapabilities::NoVibration)
{
PadVibrationBinding vib;
vib.pad_index = pad_index;
bool has_any_bindings = false;
switch (cinfo->vibration_caps)
{
case Pad::VibrationCapabilities::LargeSmallMotors:
{
if (const std::string large_binding(si.GetStringValue(section.c_str(), "LargeMotor")); !large_binding.empty())
has_any_bindings |= ParseBindingAndGetSource(large_binding, &vib.motors[0].binding, &vib.motors[0].source);
if (const std::string small_binding(si.GetStringValue(section.c_str(), "SmallMotor")); !small_binding.empty())
has_any_bindings |= ParseBindingAndGetSource(small_binding, &vib.motors[1].binding, &vib.motors[1].source);
}
break;
case Pad::VibrationCapabilities::SingleMotor:
{
if (const std::string binding(si.GetStringValue(section.c_str(), "Motor")); !binding.empty())
has_any_bindings |= ParseBindingAndGetSource(binding, &vib.motors[0].binding, &vib.motors[0].source);
}
break;
default:
break;
}
if (has_any_bindings)
s_pad_vibration_array.push_back(std::move(vib));
}
}
void InputManager::AddUSBBindings(SettingsInterface& si, u32 port, bool is_profile)
{
const std::string device(USB::GetConfigDevice(si, port));
if (device.empty() || device == "None")
return;
const std::string section(USB::GetConfigSection(port));
const u32 subtype = USB::GetConfigSubType(si, port, device);
for (const InputBindingInfo& bi : USB::GetDeviceBindings(device, subtype))
{
const std::string bind_name(USB::GetConfigSubKey(device, bi.name));
switch (bi.bind_type)
{
case InputBindingInfo::Type::Button:
case InputBindingInfo::Type::Axis:
case InputBindingInfo::Type::HalfAxis:
{
// normal bindings
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
if (!bindings.empty())
{
const float sensitivity = si.GetFloatValue(section.c_str(), fmt::format("{}Scale", bi.name).c_str(), 1.0f);
const float deadzone = si.GetFloatValue(section.c_str(), fmt::format("{}Deadzone", bi.name).c_str(), 0.0f);
AddBindings(
bindings, InputAxisEventHandler{[port, bind_index = bi.bind_index, sensitivity, deadzone](InputBindingKey key, float value) {
USB::SetDeviceBindValue(port, bind_index, ApplySingleBindingScale(sensitivity, deadzone, value));
}},
bi.bind_type, si, section.c_str(), bind_name.c_str(), is_profile);
}
}
break;
case InputBindingInfo::Type::Keyboard:
{
// set up to receive keyboard events
s_keyboard_event_callbacks.push_back([port, base = static_cast<u32>(bi.bind_index)](InputBindingKey key, float value) {
USB::SetDeviceBindValue(port, base + key.data, value);
});
}
break;
case InputBindingInfo::Type::Pointer:
{
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
for (const std::string& binding : bindings)
{
const std::optional<u32> key(GetIndexFromPointerBinding(binding));
if (!key.has_value())
continue;
s_pointer_move_callbacks.emplace_back(key.value(), [port, base = bi.bind_index](InputBindingKey key, float value) {
USB::SetDeviceBindValue(port, base + key.data, value);
});
}
}
break;
case InputBindingInfo::Type::Motor:
{
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
for (const std::string& binding : bindings)
{
PadVibrationBinding vib;
vib.pad_index = Pad::NUM_CONTROLLER_PORTS + port;
ParseBindingAndGetSource(binding, &vib.motors[0].binding, &vib.motors[0].source);
s_pad_vibration_array.push_back(std::move(vib));
}
}
break;
default:
break;
}
}
}
// ------------------------------------------------------------------------
// Event Handling
// ------------------------------------------------------------------------
bool InputManager::HasAnyBindingsForKey(InputBindingKey key)
{
std::unique_lock lock(s_binding_map_write_lock);
return (s_binding_map.find(key.MaskDirection()) != s_binding_map.end());
}
bool InputManager::HasAnyBindingsForSource(InputBindingKey key)
{
std::unique_lock lock(s_binding_map_write_lock);
for (const auto& it : s_binding_map)
{
const InputBindingKey& okey = it.first;
if (okey.source_type == key.source_type && okey.source_index == key.source_index && okey.source_subtype == key.source_subtype)
{
return true;
}
}
return false;
}
bool InputManager::IsAxisHandler(const InputEventHandler& handler)
{
return std::holds_alternative<InputAxisEventHandler>(handler);
}
bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key,
GenericInputBinding axis_neg_key, GenericInputBinding axis_pos_key)
{
if (DoEventHook(key, value))
return true;
// If imgui ate the event, don't fire our handlers.
const bool skip_button_handlers = PreprocessEvent(key, value, generic_key, axis_neg_key, axis_pos_key);
return ProcessEvent(key, value, skip_button_handlers);
}
bool InputManager::ProcessEvent(InputBindingKey key, float value, bool skip_button_handlers)
{
// find all the bindings associated with this key
const InputBindingKey masked_key = key.MaskDirection();
const auto range = s_binding_map.equal_range(masked_key);
if (range.first == s_binding_map.end())
return false;
// Now we can actually fire/activate bindings.
u32 min_num_keys = 0;
for (auto it = range.first; it != range.second; ++it)
{
InputBinding* binding = it->second.get();
// find the key which matches us
for (u32 i = 0; i < binding->num_keys; i++)
{
if (binding->keys[i].MaskDirection() != masked_key)
continue;
const u8 bit = static_cast<u8>(1) << i;
const bool negative = binding->keys[i].modifier == InputModifier::Negate;
const bool new_state = (negative ? (value < 0.0f) : (value > 0.0f));
float value_to_pass = 0.0f;
switch (binding->keys[i].modifier)
{
case InputModifier::None:
if (value > 0.0f)
value_to_pass = value;
break;
case InputModifier::Negate:
if (value < 0.0f)
value_to_pass = -value;
break;
case InputModifier::FullAxis:
value_to_pass = value * 0.5f + 0.5f;
break;
}
// handle inverting, needed for some wheels.
value_to_pass = binding->keys[i].invert ? (1.0f - value_to_pass) : value_to_pass;
// axes are fired regardless of a state change, unless they're zero
// (but going from not-zero to zero will still fire, because of the full state)
// for buttons, we can use the state of the last chord key, because it'll be 1 on press,
// and 0 on release (when the full state changes).
if (IsAxisHandler(binding->handler))
{
if (value_to_pass >= 0.0f && (!skip_button_handlers || value_to_pass == 0.0f))
std::get<InputAxisEventHandler>(binding->handler)(key, value_to_pass);
}
else if (binding->num_keys >= min_num_keys)
{
// update state based on whether the whole chord was activated
const u8 new_mask = (new_state ? (binding->current_mask | bit) : (binding->current_mask & ~bit));
const bool prev_full_state = (binding->current_mask == binding->full_mask);
const bool new_full_state = (new_mask == binding->full_mask);
binding->current_mask = new_mask;
// Workaround for multi-key bindings that share the same keys.
if (binding->num_keys > 1 && new_full_state && prev_full_state != new_full_state && range.first != range.second)
{
// Because the binding map isn't ordered, we could iterate in the order of Shift+F1 and then
// F1, which would mean that F1 wouldn't get cancelled and still activate. So, to handle this
// case, we skip activating any future bindings with a fewer number of keys.
min_num_keys = std::max<u32>(min_num_keys, binding->num_keys);
// Basically, if we bind say, F1 and Shift+F1, and press shift and then F1, we'll fire bindings
// for both F1 and Shift+F1, when we really only want to fire the binding for Shift+F1. So,
// when we activate a multi-key chord (key press), we go through the binding map for all the
// other keys in the chord, and cancel them if they have a shorter chord. If they're longer,
// they could still activate and take precedence over us, so we leave them alone.
for (u32 i = 0; i < binding->num_keys; i++)
{
const auto range = s_binding_map.equal_range(binding->keys[i].MaskDirection());
for (auto it = range.first; it != range.second; ++it)
{
InputBinding* other_binding = it->second.get();
if (other_binding == binding || IsAxisHandler(other_binding->handler) ||
other_binding->num_keys >= binding->num_keys)
{
continue;
}
// We only need to cancel the binding if it was fully active before. Which in the above
// case of Shift+F1 / F1, it will be.
if (other_binding->current_mask == other_binding->full_mask)
std::get<InputButtonEventHandler>(other_binding->handler)(-1);
// Zero out the current bits so that we don't release this binding, if the other part
// of the chord releases first.
other_binding->current_mask = 0;
}
}
}
if (prev_full_state != new_full_state && binding->num_keys >= min_num_keys)
{
const s32 pressed = skip_button_handlers ? -1 : static_cast<s32>(value_to_pass > 0.0f);
std::get<InputButtonEventHandler>(binding->handler)(pressed);
}
}
// bail out, since we shouldn't have the same key twice in the chord
break;
}
}
return true;
}
void InputManager::ClearBindStateFromSource(InputBindingKey key)
{
// Why are we doing it this way? Because any of the bindings could cause a reload and invalidate our iterators :(.
// Axis handlers should be fine, so we'll do those as a first pass.
for (const auto& [match_key, binding] : s_binding_map)
{
if (key.source_type != match_key.source_type || key.source_subtype != match_key.source_subtype ||
key.source_index != match_key.source_index || !IsAxisHandler(binding->handler))
{
continue;
}
for (u32 i = 0; i < binding->num_keys; i++)
{
if (binding->keys[i].MaskDirection() != match_key)
continue;
std::get<InputAxisEventHandler>(binding->handler)(key, 0.0f);
break;
}
}
// Now go through the button handlers, and pick them off.
bool matched;
do
{
matched = false;
for (const auto& [match_key, binding] : s_binding_map)
{
if (key.source_type != match_key.source_type || key.source_subtype != match_key.source_subtype ||
key.source_index != match_key.source_index || IsAxisHandler(binding->handler))
{
continue;
}
for (u32 i = 0; i < binding->num_keys; i++)
{
if (binding->keys[i].MaskDirection() != match_key)
continue;
// Skip if we weren't pressed.
const u8 bit = static_cast<u8>(1) << i;
if ((binding->current_mask & bit) == 0)
continue;
// Only fire handler if we're changing from active state.
const u8 current_mask = binding->current_mask;
binding->current_mask &= ~bit;
if (current_mask == binding->full_mask)
{
std::get<InputButtonEventHandler>(binding->handler)(0.0f);
matched = true;
break;
}
}
// Need to start again, might've reloaded.
if (matched)
break;
}
} while (matched);
}
bool InputManager::PreprocessEvent(InputBindingKey key, float value, GenericInputBinding generic_key,
GenericInputBinding axis_neg_key, GenericInputBinding axis_pos_key)
{
// does imgui want the event?
if (key.source_type == InputSourceType::Keyboard)
{
if (ImGuiManager::ProcessHostKeyEvent(key, value))
return true;
for (const KeyboardEventCallback& kbc : s_keyboard_event_callbacks)
kbc(key, value);
}
else if (key.source_type == InputSourceType::Pointer && key.source_subtype == InputSubclass::PointerButton)
{
if (ImGuiManager::ProcessPointerButtonEvent(key, value))
return true;
}
else if (key.source_subtype == InputSubclass::ControllerButton)
{
// Static table takes priority; falls back to the Shared/global map only if the source has none (e.g. DInput).
GenericInputBinding resolved = generic_key;
if (resolved == GenericInputBinding::Unknown)
{
const auto it = s_controller_button_generic_map.find(key.MaskDirection());
if (it != s_controller_button_generic_map.end())
resolved = it->second;
}
if (resolved != GenericInputBinding::Unknown)
{
const u32 controller_id = (static_cast<u32>(key.source_type) << 8) | key.source_index;
const InputLayout layout = s_input_sources[static_cast<u32>(key.source_type)]->GetControllerLayout(key.source_index);
if (ImGuiManager::ProcessGenericInputEvent(resolved, layout, value, controller_id) && value != 0.0f)
return true;
}
}
else if (key.source_subtype == InputSubclass::ControllerAxis)
{
// Same priority as above; map only fills in directions the source couldn't resolve.
GenericInputBinding neg = axis_neg_key;
GenericInputBinding pos = axis_pos_key;
if (neg == GenericInputBinding::Unknown || pos == GenericInputBinding::Unknown)
{
const auto it = s_controller_axis_generic_map.find(key.MaskDirection());
if (it != s_controller_axis_generic_map.end())
{
if (neg == GenericInputBinding::Unknown)
neg = it->second[0];
if (pos == GenericInputBinding::Unknown)
pos = it->second[1];
}
}
if (neg != GenericInputBinding::Unknown || pos != GenericInputBinding::Unknown)
{
const u32 controller_id = (static_cast<u32>(key.source_type) << 8) | key.source_index;
const InputLayout layout = s_input_sources[static_cast<u32>(key.source_type)]->GetControllerLayout(key.source_index);
ImGuiManager::ProcessGenericAxisEvent(neg, pos, layout, value, controller_id);
}
}
return false;
}
void InputManager::GenerateRelativeMouseEvents()
{
for (u32 device = 0; device < MAX_POINTER_DEVICES; device++)
{
for (u32 axis = 0; axis < static_cast<u32>(static_cast<u8>(InputPointerAxis::Count)); axis++)
{
const InputBindingKey key(MakePointerAxisKey(device, static_cast<InputPointerAxis>(axis)));
PointerAxisState& state = s_pointer_state[device][axis];
const float delta = static_cast<float>(state.delta.exchange(0, std::memory_order_acquire)) / 65536.0f;
float value = 0.0f;
if (axis <= static_cast<u32>(InputPointerAxis::Y))
{
s_pointer_pos[axis] += delta * s_pointer_axis_speed[axis];
value = std::clamp(s_pointer_pos[axis], -1.0f, 1.0f);
s_pointer_pos[axis] -= value;
s_pointer_pos[axis] *= s_pointer_inertia;
value *= s_pointer_axis_range[axis];
if (value > 0.0f)
value += s_pointer_axis_dead_zone[axis];
else if (value < 0.0f)
value -= s_pointer_axis_dead_zone[axis];
}
else
{
// ImGui can consume mouse wheel events when the mouse is over a UI element.
if (delta != 0.0f && ImGuiManager::ProcessPointerAxisEvent(key, delta))
continue;
value = std::clamp(delta, -1.0f, 1.0f);
}
if (value != state.last_value)
{
state.last_value = value;
ProcessEvent(key, value, false);
}
if (delta != 0.0f)
{
for (const std::pair<u32, PointerMoveCallback>& pmc : s_pointer_move_callbacks)
{
if (pmc.first == device)
pmc.second(key, delta);
}
}
}
}
}
std::pair<float, float> InputManager::GetPointerAbsolutePosition(u32 index)
{
return std::make_pair(s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::X)],
s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::Y)]);
}
void InputManager::UpdatePointerAbsolutePosition(u32 index, float x, float y)
{
const float dx = x - std::exchange(s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::X)], x);
const float dy = y - std::exchange(s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::Y)], y);
if (dx != 0.0f)
s_pointer_state[index][static_cast<u8>(InputPointerAxis::X)].delta.fetch_add(
static_cast<s32>(dx * 65536.0f), std::memory_order_release);
if (dy != 0.0f)
s_pointer_state[index][static_cast<u8>(InputPointerAxis::Y)].delta.fetch_add(
static_cast<s32>(dy * 65536.0f), std::memory_order_release);
if (index == 0)
ImGuiManager::UpdateMousePosition(x, y);
}
void InputManager::UpdatePointerRelativeDelta(u32 index, InputPointerAxis axis, float d, bool raw_input)
{
s_host_pointer_positions[index][static_cast<u8>(axis)] += d;
s_pointer_state[index][static_cast<u8>(axis)].delta.fetch_add(static_cast<s32>(d * 65536.0f), std::memory_order_release);
if (index == 0 && axis <= InputPointerAxis::Y)
ImGuiManager::UpdateMousePosition(s_host_pointer_positions[0][0], s_host_pointer_positions[0][1]);
}
void InputManager::OnInputDeviceConnected(const std::string_view identifier, const std::string_view device_name)
{
if (VMManager::HasValidVM())
USB::InputDeviceConnected(identifier);
Host::OnInputDeviceConnected(identifier, device_name);
}
void InputManager::OnInputDeviceDisconnected(const InputBindingKey key, const std::string_view identifier)
{
if (VMManager::HasValidVM())
USB::InputDeviceDisconnected(identifier);
Host::OnInputDeviceDisconnected(key, identifier);
}
// ------------------------------------------------------------------------
// Vibration
// ------------------------------------------------------------------------
void InputManager::SetUSBVibrationIntensity(u32 port, float large_or_single_motor_intensity, float small_motor_intensity)
{
SetPadVibrationIntensity(Pad::NUM_CONTROLLER_PORTS + port, large_or_single_motor_intensity, small_motor_intensity);
}
#ifdef __ANDROID__
// The Android pad is fed by the custom JNI input path, not an input source with motor
// bindings, so s_pad_vibration_array is empty and the loop below never drives a vibrator
// (this is why rumble did nothing after the mono migration). Forward intensity changes
// straight to the gamepad's Android vibrator via onPadRumble (NativeApp routes them to
// that player's controller, or the handheld's own haptic as a fallback). Deduped per pad
// to match the Java one-shot model (RUMBLE_MS re-issued only on change, cancelled on 0).
namespace Native { void onPadRumble(int pad, int largeMotor, int smallMotor); }
#endif
#if defined(__APPLE__) && TARGET_OS_IPHONE
// Same story as Android above: iOS drives pads through its own SDL bridge and binds no
// motors, so s_pad_vibration_array is empty and the loop below never reaches a motor.
// Hand every intensity change to the iOS rumble queue, which the VM thread drains once a
// frame and turns into SDL rumble, a CoreHaptics pulse, or the phone's own taptic engine.
extern "C" void ARMSX2_iOSUpdatePadVibration(u32 pad_index, float large_intensity, float small_intensity);
#endif
void InputManager::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity)
{
#if defined(__APPLE__) && TARGET_OS_IPHONE
ARMSX2_iOSUpdatePadVibration(pad_index, large_or_single_motor_intensity, small_motor_intensity);
#endif
#ifdef __ANDROID__
if (pad_index < Pad::NUM_CONTROLLER_PORTS)
{
static float s_android_last[Pad::NUM_CONTROLLER_PORTS][2] = {};
if (s_android_last[pad_index][0] != large_or_single_motor_intensity ||
s_android_last[pad_index][1] != small_motor_intensity)
{
s_android_last[pad_index][0] = large_or_single_motor_intensity;
s_android_last[pad_index][1] = small_motor_intensity;
Native::onPadRumble(static_cast<int>(pad_index),
static_cast<int>(large_or_single_motor_intensity * 255.0f + 0.5f),
static_cast<int>(small_motor_intensity * 255.0f + 0.5f));
}
}
#endif
for (PadVibrationBinding& pad : s_pad_vibration_array)
{
if (pad.pad_index != pad_index)
continue;
PadVibrationBinding::Motor& large_motor = pad.motors[0];
PadVibrationBinding::Motor& small_motor = pad.motors[1];
if (large_motor.last_intensity == large_or_single_motor_intensity && small_motor.last_intensity == small_motor_intensity)
continue;
if (pad.AreMotorsCombined())
{
// if the motors are combined, we need to adjust to the maximum of both
const float report_intensity = std::max(large_or_single_motor_intensity, small_motor_intensity);
if (large_motor.source)
{
large_motor.last_update_time = Common::Timer::GetCurrentValue();
large_motor.source->UpdateMotorState(large_motor.binding, report_intensity);
}
}
else if (large_motor.source == small_motor.source)
{
// both motors are bound to the same source, do an optimal update
large_motor.last_update_time = Common::Timer::GetCurrentValue();
large_motor.source->UpdateMotorState(
large_motor.binding, small_motor.binding, large_or_single_motor_intensity, small_motor_intensity);
}
else
{
// update motors independently
if (large_motor.source && large_motor.last_intensity != large_or_single_motor_intensity)
{
large_motor.last_update_time = Common::Timer::GetCurrentValue();
large_motor.source->UpdateMotorState(large_motor.binding, large_or_single_motor_intensity);
}
if (small_motor.source && small_motor.last_intensity != small_motor_intensity)
{
small_motor.last_update_time = Common::Timer::GetCurrentValue();
small_motor.source->UpdateMotorState(small_motor.binding, small_motor_intensity);
}
}
large_motor.last_intensity = large_or_single_motor_intensity;
small_motor.last_intensity = small_motor_intensity;
}
}
void InputManager::PauseVibration()
{
#if defined(__APPLE__) && TARGET_OS_IPHONE
// Nothing else stops the motor on the iOS path, so without this a pad that was
// rumbling when you opened the menu keeps rumbling behind it.
for (u32 pad_index = 0; pad_index < Pad::NUM_CONTROLLER_PORTS; pad_index++)
ARMSX2_iOSUpdatePadVibration(pad_index, 0.0f, 0.0f);
#endif
for (PadVibrationBinding& binding : s_pad_vibration_array)
{
for (u32 motor_index = 0; motor_index < MAX_MOTORS_PER_PAD; motor_index++)
{
PadVibrationBinding::Motor& motor = binding.motors[motor_index];
if (!motor.source || motor.last_intensity == 0.0f)
continue;
// we deliberately don't zero the intensity here, so it can resume later
motor.last_update_time = 0;
motor.source->UpdateMotorState(motor.binding, 0.0f);
}
}
}
void InputManager::UpdateContinuedVibration()
{
// update vibration intensities, so if the game does a long effect, it continues
const u64 current_time = Common::Timer::GetCurrentValue();
for (PadVibrationBinding& pad : s_pad_vibration_array)
{
if (pad.AreMotorsCombined())
{
// motors are combined
PadVibrationBinding::Motor& large_motor = pad.motors[0];
if (!large_motor.source)
continue;
// so only check the first one
const double dt = Common::Timer::ConvertValueToSeconds(current_time - large_motor.last_update_time);
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
continue;
// but take max of both motors for the intensity
const float intensity = pad.GetCombinedIntensity();
if (intensity == 0.0f)
continue;
large_motor.last_update_time = current_time;
large_motor.source->UpdateMotorState(large_motor.binding, intensity);
}
else
{
// independent motor control
for (u32 i = 0; i < MAX_MOTORS_PER_PAD; i++)
{
PadVibrationBinding::Motor& motor = pad.motors[i];
if (!motor.source || motor.last_intensity == 0.0f)
continue;
const double dt = Common::Timer::ConvertValueToSeconds(current_time - motor.last_update_time);
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
continue;
// re-notify the source of the continued effect
motor.last_update_time = current_time;
motor.source->UpdateMotorState(motor.binding, motor.last_intensity);
}
}
}
}
// ------------------------------------------------------------------------
// Hooks/Event Intercepting
// ------------------------------------------------------------------------
void InputManager::SetHook(InputInterceptHook::Callback callback)
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
pxAssert(!m_event_intercept_callback);
m_event_intercept_callback = std::move(callback);
}
void InputManager::RemoveHook()
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
if (m_event_intercept_callback)
m_event_intercept_callback = {};
}
bool InputManager::HasHook()
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
return (bool)m_event_intercept_callback;
}
bool InputManager::DoEventHook(InputBindingKey key, float value)
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
if (!m_event_intercept_callback)
return false;
const InputInterceptHook::CallbackResult action = m_event_intercept_callback(key, value);
if (action >= InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent)
m_event_intercept_callback = {};
return (action == InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent ||
action == InputInterceptHook::CallbackResult::StopProcessingEvent);
}
// ------------------------------------------------------------------------
// Binding Updater
// ------------------------------------------------------------------------
void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& binding_si, SettingsInterface& hotkey_binding_si, bool is_binding_profile, bool is_hotkey_profile)
{
PauseVibration();
std::unique_lock lock(s_binding_map_write_lock);
s_binding_map.clear();
s_pad_vibration_array.clear();
s_keyboard_event_callbacks.clear();
s_pointer_move_callbacks.clear();
s_controller_button_generic_map.clear();
s_controller_axis_generic_map.clear();
// Hotkeys use the base configuration, except if the custom hotkeys option is enabled.
AddHotkeyBindings(hotkey_binding_si, is_hotkey_profile);
// nav_si is always the base config, so per-game profiles can't affect UI navigation.
const LayeredSettingsInterface& lsi = static_cast<LayeredSettingsInterface&>(si);
SettingsInterface* base_si = lsi.GetLayer(LayeredSettingsInterface::LAYER_BASE);
for (u32 pad = 0; pad < Pad::NUM_CONTROLLER_PORTS; pad++)
AddPadBindings(binding_si, pad, is_binding_profile, base_si);
constexpr float ui_ctrl_range = 100.0f;
constexpr float pointer_sensitivity = 0.05f;
for (u32 axis = 0; axis <= static_cast<u32>(InputPointerAxis::Y); axis++)
{
s_pointer_axis_speed[axis] = si.GetFloatValue("Pad", fmt::format("Pointer{}Speed", s_pointer_axis_setting_names[axis]).c_str(), 40.0f) /
ui_ctrl_range * pointer_sensitivity;
s_pointer_axis_dead_zone[axis] = std::min(
si.GetFloatValue("Pad", fmt::format("Pointer{}DeadZone", s_pointer_axis_setting_names[axis]).c_str(), 20.0f) / ui_ctrl_range, 1.0f);
s_pointer_axis_range[axis] = 1.0f - s_pointer_axis_dead_zone[axis];
}
s_pointer_inertia = si.GetFloatValue("Pad", "PointerInertia", 10.0f) / ui_ctrl_range;
s_pointer_pos = {};
for (u32 port = 0; port < USB::NUM_PORTS; port++)
AddUSBBindings(binding_si, port, is_binding_profile);
UpdateHostMouseMode();
}
void InputManager::UpdateHostMouseMode()
{
// Check for relative mode bindings, and enable if there's anything using it.
bool has_relative_mode_bindings = !s_pointer_move_callbacks.empty();
if (!has_relative_mode_bindings)
{
for (const auto& it : s_binding_map)
{
const InputBindingKey& key = it.first;
if (key.source_type == InputSourceType::Pointer && key.source_subtype == InputSubclass::PointerAxis &&
key.data >= static_cast<u32>(InputPointerAxis::X) && key.data <= static_cast<u32>(InputPointerAxis::Y))
{
has_relative_mode_bindings = true;
break;
}
}
}
const bool has_software_cursor = ImGuiManager::HasSoftwareCursor(0);
Host::SetMouseMode(has_relative_mode_bindings, has_relative_mode_bindings || has_software_cursor);
}
// ------------------------------------------------------------------------
// Source Management
// ------------------------------------------------------------------------
bool InputManager::ReloadDevices()
{
bool changed = false;
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i]->IsInitialized())
changed |= s_input_sources[i]->ReloadDevices();
}
return changed;
}
void InputManager::CloseSources()
{
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i] && s_input_sources[i]->IsInitialized())
{
s_input_sources[i]->Shutdown();
}
s_input_sources[i].reset();
}
}
void InputManager::PollSources()
{
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i]->IsInitialized())
s_input_sources[i]->PollEvents();
}
GenerateRelativeMouseEvents();
if (VMManager::GetState() == VMState::Running && !s_pad_vibration_array.empty())
UpdateContinuedVibration();
}
std::vector<std::pair<std::string, std::string>> InputManager::EnumerateDevices()
{
std::vector<std::pair<std::string, std::string>> ret;
ret.emplace_back("Keyboard", "Keyboard");
ret.emplace_back("Mouse", "Mouse");
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i]->IsInitialized())
{
std::vector<std::pair<std::string, std::string>> devs(s_input_sources[i]->EnumerateDevices());
if (ret.empty())
ret = std::move(devs);
else
std::move(devs.begin(), devs.end(), std::back_inserter(ret));
}
}
return ret;
}
std::vector<InputBindingKey> InputManager::EnumerateMotors()
{
std::vector<InputBindingKey> ret;
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i]->IsInitialized())
{
std::vector<InputBindingKey> devs(s_input_sources[i]->EnumerateMotors());
if (ret.empty())
ret = std::move(devs);
else
std::move(devs.begin(), devs.end(), std::back_inserter(ret));
}
}
return ret;
}
static void GetKeyboardGenericBindingMapping(std::vector<std::pair<GenericInputBinding, std::string>>* mapping)
{
mapping->emplace_back(GenericInputBinding::DPadUp, "Keyboard/Up");
mapping->emplace_back(GenericInputBinding::DPadRight, "Keyboard/Right");
mapping->emplace_back(GenericInputBinding::DPadDown, "Keyboard/Down");
mapping->emplace_back(GenericInputBinding::DPadLeft, "Keyboard/Left");
mapping->emplace_back(GenericInputBinding::LeftStickUp, "Keyboard/W");
mapping->emplace_back(GenericInputBinding::LeftStickRight, "Keyboard/D");
mapping->emplace_back(GenericInputBinding::LeftStickDown, "Keyboard/S");
mapping->emplace_back(GenericInputBinding::LeftStickLeft, "Keyboard/A");
mapping->emplace_back(GenericInputBinding::RightStickUp, "Keyboard/T");
mapping->emplace_back(GenericInputBinding::RightStickRight, "Keyboard/H");
mapping->emplace_back(GenericInputBinding::RightStickDown, "Keyboard/G");
mapping->emplace_back(GenericInputBinding::RightStickLeft, "Keyboard/F");
mapping->emplace_back(GenericInputBinding::Start, "Keyboard/Return");
mapping->emplace_back(GenericInputBinding::Select, "Keyboard/Backspace");
mapping->emplace_back(GenericInputBinding::Triangle, "Keyboard/I");
mapping->emplace_back(GenericInputBinding::Circle, "Keyboard/L");
mapping->emplace_back(GenericInputBinding::Cross, "Keyboard/K");
mapping->emplace_back(GenericInputBinding::Square, "Keyboard/J");
mapping->emplace_back(GenericInputBinding::L1, "Keyboard/Q");
mapping->emplace_back(GenericInputBinding::L2, "Keyboard/1");
mapping->emplace_back(GenericInputBinding::L3, "Keyboard/2");
mapping->emplace_back(GenericInputBinding::R1, "Keyboard/E");
mapping->emplace_back(GenericInputBinding::R2, "Keyboard/3");
mapping->emplace_back(GenericInputBinding::R3, "Keyboard/4");
}
static bool GetInternalGenericBindingMapping(const std::string_view device, InputManager::GenericInputBindingMapping* mapping)
{
if (device == "Keyboard")
{
GetKeyboardGenericBindingMapping(mapping);
return true;
}
return false;
}
InputManager::GenericInputBindingMapping InputManager::GetGenericBindingMapping(const std::string_view device)
{
GenericInputBindingMapping mapping;
if (!GetInternalGenericBindingMapping(device, &mapping))
{
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i]->IsInitialized() && s_input_sources[i]->GetGenericBindingMapping(device, &mapping))
break;
}
}
return mapping;
}
bool InputManager::IsInputSourceEnabled(SettingsInterface& si, InputSourceType type)
{
return si.GetBoolValue("InputSources", InputManager::InputSourceToString(type), InputManager::GetInputSourceDefaultEnabled(type));
}
template <typename T>
void InputManager::UpdateInputSourceState(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock, InputSourceType type)
{
if (!s_input_sources[static_cast<u32>(type)])
{
std::unique_ptr<InputSource> source = std::make_unique<T>();
if (!source->Initialize(si, settings_lock))
Console.Error("(InputManager) Source '%s' failed to initialize.", InputSourceToString(type));
s_input_sources[static_cast<u32>(type)] = std::move(source);
}
const bool enabled = IsInputSourceEnabled(si, type);
if (enabled)
{
if (s_input_sources[static_cast<u32>(type)]->IsInitialized())
{
s_input_sources[static_cast<u32>(type)]->UpdateSettings(si, settings_lock);
}
else if (!s_input_sources[static_cast<u32>(type)]->Initialize(si, settings_lock))
{
Console.Error("(InputManager) Source '%s' failed to initialize.", InputSourceToString(type));
}
}
else
{
if (s_input_sources[static_cast<u32>(type)]->IsInitialized())
{
settings_lock.unlock();
s_input_sources[static_cast<u32>(type)]->Shutdown();
settings_lock.lock();
}
}
}
#include "Input/SDLInputSource.h"
#ifdef _WIN32
#include "Input/DInputSource.h"
#include "Input/XInputSource.h"
#endif
void InputManager::ReloadSources(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
{
UpdateInputSourceState<SDLInputSource>(si, settings_lock, InputSourceType::SDL);
#ifdef _WIN32
UpdateInputSourceState<DInputSource>(si, settings_lock, InputSourceType::DInput);
UpdateInputSourceState<XInputSource>(si, settings_lock, InputSourceType::XInput);
#endif
}