diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt
index 926e913156..ef7a206e14 100644
--- a/pcsx2/CMakeLists.txt
+++ b/pcsx2/CMakeLists.txt
@@ -63,6 +63,12 @@ endif()
if(TARGET PkgConfig::SDL2)
target_compile_definitions(PCSX2_FLAGS INTERFACE SDL_BUILD)
target_link_libraries(PCSX2_FLAGS INTERFACE PkgConfig::SDL2)
+ if(PCSX2_CORE)
+ target_sources(PCSX2 PRIVATE
+ Frontend/SDLInputSource.cpp
+ Frontend/SDLInputSource.h
+ )
+ endif()
endif()
if(WIN32)
diff --git a/pcsx2/Counters.cpp b/pcsx2/Counters.cpp
index 52ae8b57ce..1c2a0d6251 100644
--- a/pcsx2/Counters.cpp
+++ b/pcsx2/Counters.cpp
@@ -35,6 +35,7 @@
#ifndef PCSX2_CORE
#include "gui/App.h"
#else
+#include "PAD/Host/PAD.h"
#include "VMManager.h"
#endif
@@ -552,6 +553,11 @@ static __fi void VSyncStart(u32 sCycle)
}
#endif
+#ifdef PCSX2_CORE
+ // Update vibration at the end of a frame.
+ PAD::Update();
+#endif
+
frameLimit(); // limit FPS
gsPostVsyncStart(); // MUST be after framelimit; doing so before causes funk with frame times!
diff --git a/pcsx2/Frontend/InputManager.cpp b/pcsx2/Frontend/InputManager.cpp
new file mode 100644
index 0000000000..de5f53ffac
--- /dev/null
+++ b/pcsx2/Frontend/InputManager.cpp
@@ -0,0 +1,929 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "PrecompiledHeader.h"
+#include "Frontend/InputManager.h"
+#include "Frontend/InputSource.h"
+#include "PAD/Host/PAD.h"
+#include "common/StringUtil.h"
+#include "common/Timer.h"
+#include "VMManager.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+// ------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------
+
+enum : u32
+{
+ MAX_KEYS_PER_BINDING = 4,
+ MAX_MOTORS_PER_PAD = 2,
+ FIRST_EXTERNAL_INPUT_SOURCE = static_cast(InputSourceType::Mouse) + 1u,
+ LAST_EXTERNAL_INPUT_SOURCE = static_cast(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.
+
+class InputEventHandler
+{
+public:
+ InputEventHandler()
+ {
+ new (&u.button) InputButtonEventHandler;
+ is_axis = false;
+ }
+
+ InputEventHandler(InputButtonEventHandler button)
+ {
+ new (&u.button) InputButtonEventHandler(std::move(button));
+ is_axis = false;
+ }
+
+ InputEventHandler(InputAxisEventHandler axis)
+ {
+ new (&u.axis) InputAxisEventHandler(std::move(axis));
+ is_axis = true;
+ }
+
+ InputEventHandler(const InputEventHandler& copy)
+ {
+ if (copy.is_axis)
+ new (&u.axis) InputAxisEventHandler(copy.u.axis);
+ else
+ new (&u.button) InputButtonEventHandler(copy.u.button);
+ is_axis = copy.is_axis;
+ }
+
+ InputEventHandler(InputEventHandler&& move)
+ {
+ if (move.is_axis)
+ new (&u.axis) InputAxisEventHandler(std::move(move.u.axis));
+ else
+ new (&u.button) InputButtonEventHandler(std::move(move.u.button));
+ is_axis = move.is_axis;
+ }
+
+ ~InputEventHandler()
+ {
+ // call the right destructor... :D
+ if (is_axis)
+ u.axis.InputAxisEventHandler::~InputAxisEventHandler();
+ else
+ u.button.InputButtonEventHandler::~InputButtonEventHandler();
+ }
+
+ InputEventHandler& operator=(const InputEventHandler& copy)
+ {
+ InputEventHandler::~InputEventHandler();
+
+ if (copy.is_axis)
+ new (&u.axis) InputAxisEventHandler(copy.u.axis);
+ else
+ new (&u.button) InputButtonEventHandler(copy.u.button);
+ is_axis = copy.is_axis;
+ return *this;
+ }
+
+ InputEventHandler& operator=(InputEventHandler&& move)
+ {
+ InputEventHandler::~InputEventHandler();
+
+ if (move.is_axis)
+ new (&u.axis) InputAxisEventHandler(std::move(move.u.axis));
+ else
+ new (&u.button) InputButtonEventHandler(std::move(move.u.button));
+ is_axis = move.is_axis;
+ return *this;
+ }
+
+ __fi bool IsAxis() const { return is_axis; }
+
+ __fi void Invoke(float value) const
+ {
+ if (is_axis)
+ u.axis(value);
+ else
+ u.button(value > 0.0f);
+ }
+
+private:
+ union HandlerUnion
+ {
+ // constructor/destructor needs to be declared
+ HandlerUnion() {}
+ ~HandlerUnion() {}
+
+ InputButtonEventHandler button;
+ InputAxisEventHandler axis;
+ } u;
+
+ bool is_axis;
+};
+
+// ------------------------------------------------------------------------
+// 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 ParseHostKeyboardKey(
+ const std::string_view& source, const std::string_view& sub_binding);
+ static std::optional ParseHostMouseKey(
+ const std::string_view& source, const std::string_view& sub_binding);
+
+ static std::vector SplitChord(const std::string_view& binding);
+ static bool SplitBinding(const std::string_view& binding, std::string_view* source, std::string_view* sub_binding);
+ static void AddBindings(const std::vector& bindings, const InputEventHandler& handler);
+ static bool ParseBindingAndGetSource(const std::string_view& binding, InputBindingKey* key, InputSource** source);
+
+ static void AddHotkeyBindings(SettingsInterface& si);
+ static void AddPadBindings(SettingsInterface& si, u32 pad, const char* default_type);
+ static void UpdateContinuedVibration();
+
+ static bool DoEventHook(InputBindingKey key, float value);
+} // namespace InputManager
+
+// ------------------------------------------------------------------------
+// Local Variables
+// ------------------------------------------------------------------------
+
+// This is a multimap containing any binds related to the specified key.
+using BindingMap = std::unordered_multimap, InputBindingKeyHash>;
+using VibrationBindingArray = std::vector;
+static BindingMap s_binding_map;
+static VibrationBindingArray s_pad_vibration_array;
+static std::mutex s_binding_map_write_lock;
+
+// 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, static_cast(InputSourceType::Count)> s_input_sources;
+
+// ------------------------------------------------------------------------
+// Hotkeys
+// ------------------------------------------------------------------------
+static const HotkeyInfo* const s_hotkey_list[] = {g_vm_manager_hotkeys, g_host_hotkeys};
+
+// ------------------------------------------------------------------------
+// Binding Parsing
+// ------------------------------------------------------------------------
+
+std::vector InputManager::SplitChord(const std::string_view& binding)
+{
+ std::vector 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(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 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 (StringUtil::StartsWith(source, "Keyboard"))
+ {
+ return ParseHostKeyboardKey(source, sub_binding);
+ }
+ else if (StringUtil::StartsWith(source, "Mouse"))
+ {
+ return ParseHostMouseKey(source, sub_binding);
+ }
+ else
+ {
+ for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
+ {
+ if (s_input_sources[i])
+ {
+ std::optional 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])
+ {
+ std::optional 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;
+}
+
+std::string InputManager::ConvertInputBindingKeyToString(InputBindingKey key)
+{
+ if (key.source_type == InputSourceType::Keyboard)
+ {
+ const std::optional str(ConvertHostKeyboardCodeToString(key.data));
+ if (str.has_value() && !str->empty())
+ return StringUtil::StdStringFromFormat("Keyboard/%s", str->c_str());
+ }
+ else if (key.source_type == InputSourceType::Mouse)
+ {
+ if (key.source_subtype == InputSubclass::MouseButton)
+ return StringUtil::StdStringFromFormat("Mouse%u/Button%u", key.source_index, key.data);
+ else if (key.source_subtype == InputSubclass::MousePointer)
+ return StringUtil::StdStringFromFormat("Mouse%u/Pointer%u", key.source_index, key.data);
+ else if (key.source_subtype == InputSubclass::MouseWheel)
+ return StringUtil::StdStringFromFormat(
+ "Mouse%u/Wheel%u%c", key.source_index, key.data, key.negative ? '-' : '+');
+ }
+ else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast(key.source_type)])
+ {
+ return s_input_sources[static_cast(key.source_type)]->ConvertKeyToString(key);
+ }
+
+ return {};
+}
+
+std::string InputManager::ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys)
+{
+ std::stringstream ss;
+ for (size_t i = 0; i < num_keys; i++)
+ {
+ const std::string keystr(ConvertInputBindingKeyToString(keys[i]));
+ if (keystr.empty())
+ return std::string();
+
+ if (i > 0)
+ ss << " & ";
+
+ ss << keystr;
+ }
+
+ return ss.str();
+}
+
+void InputManager::AddBindings(const std::vector& bindings, const InputEventHandler& handler)
+{
+ for (const std::string& binding : bindings)
+ {
+ std::shared_ptr ibinding;
+ const std::vector chord_bindings(SplitChord(binding));
+
+ for (const std::string_view& chord_binding : chord_bindings)
+ {
+ std::optional key = ParseInputBindingKey(chord_binding);
+ if (!key.has_value())
+ {
+ Console.WriteLn("Invalid binding: '%s'", binding.c_str());
+ ibinding.reset();
+ break;
+ }
+
+ if (!ibinding)
+ {
+ ibinding = std::make_shared();
+ ibinding->handler = handler;
+ }
+
+ if (ibinding->num_keys == MAX_KEYS_PER_BINDING)
+ {
+ Console.WriteLn("Too many chord parts, max is %u (%s)", MAX_KEYS_PER_BINDING, binding.c_str());
+ ibinding.reset();
+ break;
+ }
+
+ ibinding->keys[ibinding->num_keys] = key.value();
+ ibinding->full_mask |= (static_cast(1) << ibinding->num_keys);
+ ibinding->num_keys++;
+ }
+
+ if (!ibinding)
+ continue;
+
+ // 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);
+ }
+}
+
+// ------------------------------------------------------------------------
+// Key Decoders
+// ------------------------------------------------------------------------
+
+InputBindingKey InputManager::MakeHostKeyboardKey(s32 key_code)
+{
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::Keyboard;
+ key.data = static_cast(key_code);
+ return key;
+}
+
+InputBindingKey InputManager::MakeHostMouseButtonKey(s32 button_index)
+{
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::Mouse;
+ key.source_subtype = InputSubclass::MouseButton;
+ key.data = static_cast(button_index);
+ return key;
+}
+
+InputBindingKey InputManager::MakeHostMouseWheelKey(s32 axis_index)
+{
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::Mouse;
+ key.source_subtype = InputSubclass::MouseWheel;
+ key.data = static_cast(axis_index);
+ return key;
+}
+
+// ------------------------------------------------------------------------
+// Bind Encoders
+// ------------------------------------------------------------------------
+
+static std::array(InputSourceType::Count)> s_input_class_names = {{
+ "Keyboard",
+ "Mouse",
+#ifdef _WIN32
+ "XInput",
+#endif
+#ifdef SDL_BUILD
+ "SDL",
+#endif
+}};
+
+InputSource* InputManager::GetInputSourceInterface(InputSourceType type)
+{
+ return s_input_sources[static_cast(type)].get();
+}
+
+const char* InputManager::InputSourceToString(InputSourceType clazz)
+{
+ return s_input_class_names[static_cast(clazz)];
+}
+
+std::optional InputManager::ParseInputSourceString(const std::string_view& str)
+{
+ for (u32 i = 0; i < static_cast(InputSourceType::Count); i++)
+ {
+ if (str == s_input_class_names[i])
+ return static_cast(i);
+ }
+
+ return std::nullopt;
+}
+
+std::optional InputManager::ParseHostKeyboardKey(
+ const std::string_view& source, const std::string_view& sub_binding)
+{
+ if (source != "Keyboard")
+ return std::nullopt;
+
+ const std::optional code = ConvertHostKeyboardStringToCode(sub_binding);
+ if (!code.has_value())
+ return std::nullopt;
+
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::Keyboard;
+ key.data = static_cast(code.value());
+ return key;
+}
+
+std::optional InputManager::ParseHostMouseKey(
+ const std::string_view& source, const std::string_view& sub_binding)
+{
+ if (source != "Mouse")
+ return std::nullopt;
+
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::Mouse;
+
+ if (StringUtil::StartsWith(sub_binding, "Button"))
+ {
+ const std::optional button_number = StringUtil::FromChars(sub_binding.substr(6));
+ if (!button_number.has_value() || button_number.value() < 0)
+ return std::nullopt;
+
+ key.source_subtype = InputSubclass::MouseButton;
+ key.data = static_cast(button_number.value());
+ }
+ else
+ {
+ return std::nullopt;
+ }
+
+ return key;
+}
+
+// ------------------------------------------------------------------------
+// Binding Enumeration
+// ------------------------------------------------------------------------
+
+std::vector InputManager::GetHotkeyList()
+{
+ std::vector ret;
+ for (const HotkeyInfo* hotkey_list : s_hotkey_list)
+ {
+ for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
+ ret.push_back(hotkey);
+ }
+ std::sort(ret.begin(), ret.end(), [](const HotkeyInfo* left, const HotkeyInfo* right)
+ {
+ // category -> display name
+ if (const int res = StringUtil::Strcasecmp(left->category, right->category); res != 0)
+ return (res < 0);
+ return (StringUtil::Strcasecmp(left->display_name, right->display_name) < 0);
+ });
+ return ret;
+}
+
+void InputManager::AddHotkeyBindings(SettingsInterface& si)
+{
+ for (const HotkeyInfo* hotkey_list : s_hotkey_list)
+ {
+ for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
+ {
+ const std::vector bindings(si.GetStringList("Hotkeys", hotkey->name));
+ if (bindings.empty())
+ continue;
+
+ AddBindings(bindings, InputButtonEventHandler{hotkey->handler});
+ }
+ }
+}
+
+void InputManager::AddPadBindings(SettingsInterface& si, u32 pad_index, const char* default_type)
+{
+ const std::string section(StringUtil::StdStringFromFormat("Pad%u", pad_index + 1));
+ const std::string type(si.GetStringValue(section.c_str(), "Type", default_type));
+ if (type.empty() || type == "None")
+ return;
+
+ const std::vector bind_names = PAD::GetControllerBinds(type);
+ if (!bind_names.empty())
+ {
+ for (u32 bind_index = 0; bind_index < static_cast(bind_names.size()); bind_index++)
+ {
+ const std::string& bind_name = bind_names[bind_index];
+ const std::vector bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
+ if (!bindings.empty())
+ {
+ // we use axes for all pad bindings to simplify things, and because they are pressure sensitive
+ AddBindings(bindings, InputAxisEventHandler{ [pad_index, bind_index, bind_names](float value) {
+ PAD::SetControllerState(pad_index, bind_index, value);
+ } });
+ }
+ }
+ }
+
+ const PAD::VibrationCapabilities vibcaps = PAD::GetControllerVibrationCapabilities(type);
+ if (vibcaps != PAD::VibrationCapabilities::NoVibration)
+ {
+ PadVibrationBinding vib;
+ vib.pad_index = pad_index;
+
+ bool has_any_bindings = false;
+ switch (vibcaps)
+ {
+ case PAD::VibrationCapabilities::LargeSmallMotors:
+ {
+ const std::string large_binding(si.GetStringValue(section.c_str(), "LargeMotor"));
+ const std::string small_binding(si.GetStringValue(section.c_str(), "SmallMotor"));
+ has_any_bindings |= ParseBindingAndGetSource(large_binding, &vib.motors[0].binding, &vib.motors[0].source);
+ has_any_bindings |= ParseBindingAndGetSource(small_binding, &vib.motors[1].binding, &vib.motors[1].source);
+ }
+ break;
+
+ case PAD::VibrationCapabilities::SingleMotor:
+ {
+ const std::string binding(si.GetStringValue(section.c_str(), "Motor"));
+ 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));
+ }
+}
+
+// ------------------------------------------------------------------------
+// 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::InvokeEvents(InputBindingKey key, float value)
+{
+ if (DoEventHook(key, value))
+ return true;
+
+ // 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;
+
+ 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(1) << i;
+ const bool negative = binding->keys[i].negative;
+ const bool new_state = (negative ? (value < 0.0f) : (value > 0.0f));
+
+ // 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;
+
+ // invert if we're negative, since the handler expects 0..1
+ const float value_to_pass = (negative ? ((value < 0.0f) ? -value : 0.0f) : (value > 0.0f) ? value : 0.0f);
+
+ // axes are fired regardless of a state change, unless they're zero
+ // 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 (prev_full_state != new_full_state || (binding->handler.IsAxis() && value_to_pass > 0.0f))
+ {
+ binding->handler.Invoke(value_to_pass);
+ }
+
+ // bail out, since we shouldn't have the same key twice in the chord
+ break;
+ }
+ }
+
+ return true;
+}
+
+// ------------------------------------------------------------------------
+// Vibration
+// ------------------------------------------------------------------------
+
+void InputManager::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity)
+{
+ 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()
+{
+ 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 lock(m_event_intercept_mutex);
+ pxAssert(!m_event_intercept_callback);
+ m_event_intercept_callback = std::move(callback);
+}
+
+void InputManager::RemoveHook()
+{
+ std::unique_lock lock(m_event_intercept_mutex);
+ if (m_event_intercept_callback)
+ m_event_intercept_callback = {};
+}
+
+bool InputManager::HasHook()
+{
+ std::unique_lock lock(m_event_intercept_mutex);
+ return (bool)m_event_intercept_callback;
+}
+
+bool InputManager::DoEventHook(InputBindingKey key, float value)
+{
+ std::unique_lock 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::StopMonitoring)
+ m_event_intercept_callback = {};
+
+ return true;
+}
+
+// ------------------------------------------------------------------------
+// Binding Updater
+// ------------------------------------------------------------------------
+
+// TODO(Stenzek): Find a better place for this. Maybe in PAD?
+static constexpr std::array s_default_pad_types = {{
+ "DualShock2", // Pad 1
+ "None" // Pad 2
+}};
+
+void InputManager::ReloadBindings(SettingsInterface& si)
+{
+ PauseVibration();
+
+ std::unique_lock lock(s_binding_map_write_lock);
+
+ s_binding_map.clear();
+ s_pad_vibration_array.clear();
+
+ AddHotkeyBindings(si);
+
+ for (u32 pad = 0; pad < MAX_PAD_NUMBER; pad++)
+ AddPadBindings(si, pad, s_default_pad_types[pad]);
+}
+
+// ------------------------------------------------------------------------
+// Source Management
+// ------------------------------------------------------------------------
+
+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]->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])
+ s_input_sources[i]->PollEvents();
+ }
+
+ if (VMManager::GetState() == VMState::Running && !s_pad_vibration_array.empty())
+ UpdateContinuedVibration();
+}
+
+template
+static void UpdateInputSourceState(SettingsInterface& si, InputSourceType type, bool default_state)
+{
+ const bool old_state = (s_input_sources[static_cast(type)] != nullptr);
+ const bool new_state = si.GetBoolValue("InputSources", InputManager::InputSourceToString(type), default_state);
+ if (old_state == new_state)
+ return;
+
+ if (new_state)
+ {
+ std::unique_ptr source = std::make_unique();
+ if (!source->Initialize(si))
+ {
+ Console.Error("(InputManager) Source '%s' failed to initialize.", InputManager::InputSourceToString(type));
+ return;
+ }
+
+ s_input_sources[static_cast(type)] = std::move(source);
+ }
+ else
+ {
+ s_input_sources[static_cast(type)]->Shutdown();
+ s_input_sources[static_cast(type)].reset();
+ }
+}
+
+#ifdef _WIN32
+#include "Frontend/XInputSource.h"
+#endif
+
+#ifdef SDL_BUILD
+#include "Frontend/SDLInputSource.h"
+#endif
+
+void InputManager::ReloadSources(SettingsInterface& si)
+{
+#ifdef _WIN32
+ UpdateInputSourceState(si, InputSourceType::XInput, false);
+#endif
+#ifdef SDL_BUILD
+ UpdateInputSourceState(si, InputSourceType::SDL, true);
+#endif
+}
diff --git a/pcsx2/Frontend/InputManager.h b/pcsx2/Frontend/InputManager.h
new file mode 100644
index 0000000000..f115393edd
--- /dev/null
+++ b/pcsx2/Frontend/InputManager.h
@@ -0,0 +1,210 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "common/Pcsx2Types.h"
+#include "common/SettingsInterface.h"
+
+/// Class, or source of an input event.
+enum class InputSourceType : u32
+{
+ Keyboard,
+ Mouse,
+#ifdef _WIN32
+ //DInput,
+ XInput,
+#endif
+#ifdef SDL_BUILD
+ SDL,
+#endif
+ Count,
+};
+
+/// Subtype of a key for an input source.
+enum class InputSubclass : u32
+{
+ None = 0,
+
+ MouseButton = 0,
+ MousePointer = 1,
+ MouseWheel = 2,
+
+ ControllerButton = 0,
+ ControllerAxis = 1,
+ ControllerMotor = 2,
+ ControllerHaptic = 3,
+};
+
+/// A composite type representing a full input key which is part of an event.
+union InputBindingKey
+{
+ struct
+ {
+ InputSourceType source_type : 4;
+ u32 source_index : 8; ///< controller number
+ InputSubclass source_subtype : 2; ///< if 1, binding is for an axis and not a button (used for controllers)
+ u32 negative : 1; ///< if 1, binding is for the negative side of the axis
+ u32 unused : 17;
+ u32 data;
+ };
+
+ u64 bits;
+
+ bool operator==(const InputBindingKey& k) const { return bits == k.bits; }
+ bool operator!=(const InputBindingKey& k) const { return bits != k.bits; }
+
+ /// Removes the direction bit from the key, which is used to look up the bindings for it.
+ /// This is because negative bindings should still fire when they reach zero again.
+ InputBindingKey MaskDirection() const
+ {
+ InputBindingKey r;
+ r.bits = bits;
+ r.negative = false;
+ return r;
+ }
+};
+static_assert(sizeof(InputBindingKey) == sizeof(u64), "Input binding key is 64 bits");
+
+/// Hashability for InputBindingKey
+struct InputBindingKeyHash
+{
+ std::size_t operator()(const InputBindingKey& k) const { return std::hash{}(k.bits); }
+};
+
+/// Callback type for a binary event. Usually used for hotkeys.
+using InputButtonEventHandler = std::function;
+
+/// Callback types for a normalized event. Usually used for pads.
+using InputAxisEventHandler = std::function;
+
+/// Input monitoring for external access.
+struct InputInterceptHook
+{
+ enum class CallbackResult
+ {
+ StopMonitoring,
+ ContinueMonitoring
+ };
+
+ using Callback = std::function;
+};
+
+/// Hotkeys are actions (e.g. toggle frame limit) which can be bound to keys or chords.
+struct HotkeyInfo
+{
+ const char* name;
+ const char* category;
+ const char* display_name;
+ void(*handler)(bool pressed);
+};
+#define DECLARE_HOTKEY_LIST(name) extern const HotkeyInfo name[]
+#define BEGIN_HOTKEY_LIST(name) const HotkeyInfo name[] = {
+#define DEFINE_HOTKEY(name, category, display_name, handler) {(name), (category), (display_name), (handler)},
+#define END_HOTKEY_LIST() {nullptr, nullptr, nullptr, nullptr} };
+
+DECLARE_HOTKEY_LIST(g_vm_manager_hotkeys);
+DECLARE_HOTKEY_LIST(g_host_hotkeys);
+
+/// External input source class.
+class InputSource;
+
+namespace InputManager
+{
+ /// Number of emulated pads. TODO: Multitap support.
+ static constexpr u32 MAX_PAD_NUMBER = 2;
+
+ /// Minimum interval between vibration updates when the effect is continuous.
+ static constexpr double VIBRATION_UPDATE_INTERVAL_SECONDS = 0.5; // 500ms
+
+ /// Returns a pointer to the external input source class, if present.
+ InputSource* GetInputSourceInterface(InputSourceType type);
+
+ /// Converts an input class to a string.
+ const char* InputSourceToString(InputSourceType clazz);
+
+ /// Parses an input class string.
+ std::optional ParseInputSourceString(const std::string_view& str);
+
+ /// Converts a key code from a human-readable string to an identifier.
+ std::optional ConvertHostKeyboardStringToCode(const std::string_view& str);
+
+ /// Converts a key code from an identifier to a human-readable string.
+ std::optional ConvertHostKeyboardCodeToString(u32 code);
+
+ /// Creates a key for a host-specific key code.
+ InputBindingKey MakeHostKeyboardKey(s32 key_code);
+
+ /// Creates a key for a host-specific button.
+ InputBindingKey MakeHostMouseButtonKey(s32 button_index);
+
+ /// Creates a key for a host-specific mouse wheel axis (0 = vertical, 1 = horizontal).
+ InputBindingKey MakeHostMouseWheelKey(s32 axis_index);
+
+ /// Parses an input binding key string.
+ std::optional ParseInputBindingKey(const std::string_view& binding);
+
+ /// Converts a input key to a string.
+ std::string ConvertInputBindingKeyToString(InputBindingKey key);
+
+ /// Converts a chord of binding keys to a string.
+ std::string ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys);
+
+ /// Returns a list of all hotkeys.
+ std::vector GetHotkeyList();
+
+ /// Re-parses the config and registers all hotkey and pad bindings.
+ void ReloadBindings(SettingsInterface& si);
+
+ /// Re-parses the sources part of the config and initializes any backends.
+ void ReloadSources(SettingsInterface& si);
+
+ /// Shuts down any enabled input sources.
+ void CloseSources();
+
+ /// Polls input sources for events (e.g. external controllers).
+ void PollSources();
+
+ /// Returns true if any bindings exist for the specified key.
+ /// This is the only function which can be safely called on another thread.
+ bool HasAnyBindingsForKey(InputBindingKey key);
+
+ /// Updates internal state for any binds for this key, and fires callbacks as needed.
+ /// Returns true if anything was bound to this key, otherwise false.
+ bool InvokeEvents(InputBindingKey key, float value);
+
+ /// Sets a hook which can be used to intercept events before they're processed by the normal bindings.
+ /// This is typically used when binding new controls to detect what gets pressed.
+ void SetHook(InputInterceptHook::Callback callback);
+
+ /// Removes any currently-active interception hook.
+ void RemoveHook();
+
+ /// Returns true if there is an interception hook present.
+ bool HasHook();
+
+ /// Internal method used by pads to dispatch vibration updates to input sources.
+ /// Intensity is normalized from 0 to 1.
+ void SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity);
+
+ /// Zeros all vibration intensities. Call when pausing.
+ /// The pad vibration state will internally remain, so that when emulation is unpaused, the effect resumes.
+ void PauseVibration();
+} // namespace InputManager
diff --git a/pcsx2/Frontend/InputSource.cpp b/pcsx2/Frontend/InputSource.cpp
new file mode 100644
index 0000000000..2f6c09387d
--- /dev/null
+++ b/pcsx2/Frontend/InputSource.cpp
@@ -0,0 +1,134 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "PrecompiledHeader.h"
+#include "Frontend/InputSource.h"
+#include "common/StringUtil.h"
+
+InputSource::InputSource() = default;
+
+InputSource::~InputSource() = default;
+
+void InputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity)
+{
+ if (large_key.bits != 0)
+ UpdateMotorState(large_key, large_intensity);
+ if (small_key.bits != 0)
+ UpdateMotorState(small_key, small_intensity);
+}
+
+InputBindingKey InputSource::MakeGenericControllerAxisKey(InputSourceType clazz, u32 controller_index, s32 axis_index)
+{
+ InputBindingKey key = {};
+ key.source_type = clazz;
+ key.source_index = controller_index;
+ key.source_subtype = InputSubclass::ControllerAxis;
+ key.data = static_cast(axis_index);
+ return key;
+}
+
+InputBindingKey InputSource::MakeGenericControllerButtonKey(
+ InputSourceType clazz, u32 controller_index, s32 button_index)
+{
+ InputBindingKey key = {};
+ key.source_type = clazz;
+ key.source_index = controller_index;
+ key.source_subtype = InputSubclass::ControllerButton;
+ key.data = static_cast(button_index);
+ return key;
+}
+
+InputBindingKey InputSource::MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index)
+{
+ InputBindingKey key = {};
+ key.source_type = clazz;
+ key.source_index = controller_index;
+ key.source_subtype = InputSubclass::ControllerMotor;
+ key.data = static_cast(motor_index);
+ return key;
+}
+
+std::optional InputSource::ParseGenericControllerKey(
+ InputSourceType clazz, const std::string_view& source, const std::string_view& sub_binding)
+{
+ // try to find the number, this function doesn't care about whether it's xinput or sdl or whatever
+ std::string_view::size_type pos = 0;
+ while (pos < source.size())
+ {
+ if (source[pos] >= '0' && source[pos] <= '9')
+ break;
+ pos++;
+ }
+ if (pos == source.size())
+ return std::nullopt;
+
+ const std::optional source_index = StringUtil::FromChars(source.substr(pos));
+ if (source_index.has_value() || source_index.value() < 0)
+ return std::nullopt;
+
+ InputBindingKey key = {};
+ key.source_type = clazz;
+ key.source_index = source_index.value();
+
+ if (StringUtil::StartsWith(sub_binding, "+Axis") || StringUtil::StartsWith(sub_binding, "-Axis"))
+ {
+ const std::optional axis_number = StringUtil::FromChars(sub_binding.substr(5));
+ if (!axis_number.has_value() || axis_number.value() < 0)
+ return std::nullopt;
+
+ key.source_subtype = InputSubclass::ControllerAxis;
+ key.data = static_cast(axis_number.value());
+
+ if (sub_binding[0] == '+')
+ key.negative = false;
+ else if (sub_binding[0] == '-')
+ key.negative = true;
+ else
+ return std::nullopt;
+ }
+ else if (StringUtil::StartsWith(sub_binding, "Button"))
+ {
+ const std::optional button_number = StringUtil::FromChars(sub_binding.substr(6));
+ if (!button_number.has_value() || button_number.value() < 0)
+ return std::nullopt;
+
+ key.source_subtype = InputSubclass::ControllerButton;
+ key.data = static_cast(button_number.value());
+ }
+ else
+ {
+ return std::nullopt;
+ }
+
+ return key;
+}
+
+std::string InputSource::ConvertGenericControllerKeyToString(InputBindingKey key)
+{
+ if (key.source_subtype == InputSubclass::ControllerAxis)
+ {
+ return StringUtil::StdStringFromFormat("%s-%u/%cAxis%u", InputManager::InputSourceToString(key.source_type),
+ key.source_index, key.negative ? '+' : '-', key.data);
+ }
+ else if (key.source_subtype == InputSubclass::ControllerButton)
+ {
+ return StringUtil::StdStringFromFormat(
+ "%s%u/Button%u", InputManager::InputSourceToString(key.source_type), key.source_index, key.data);
+ }
+ else
+ {
+ return {};
+ }
+}
diff --git a/pcsx2/Frontend/InputSource.h b/pcsx2/Frontend/InputSource.h
new file mode 100644
index 0000000000..be4cf1f375
--- /dev/null
+++ b/pcsx2/Frontend/InputSource.h
@@ -0,0 +1,66 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "common/Pcsx2Defs.h"
+#include "Frontend/InputManager.h"
+
+class SettingsInterface;
+
+class InputSource
+{
+public:
+ InputSource();
+ virtual ~InputSource();
+
+ virtual bool Initialize(SettingsInterface& si) = 0;
+ virtual void Shutdown() = 0;
+
+ virtual void PollEvents() = 0;
+
+ virtual std::optional ParseKeyString(
+ const std::string_view& device, const std::string_view& binding) = 0;
+ virtual std::string ConvertKeyToString(InputBindingKey key) = 0;
+
+ /// Enumerates available vibration motors at the time of call.
+ virtual std::vector EnumerateMotors() = 0;
+
+ /// Informs the source of a new vibration motor state. Changes may not take effect until the next PollEvents() call.
+ virtual void UpdateMotorState(InputBindingKey key, float intensity) = 0;
+
+ /// Concurrently update both motors where possible, to avoid redundant packets.
+ virtual void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity);
+
+ /// Creates a key for a generic controller axis event.
+ static InputBindingKey MakeGenericControllerAxisKey(InputSourceType clazz, u32 controller_index, s32 axis_index);
+
+ /// Creates a key for a generic controller button event.
+ static InputBindingKey MakeGenericControllerButtonKey(InputSourceType clazz, u32 controller_index, s32 button_index);
+
+ /// Creates a key for a generic controller motor event.
+ static InputBindingKey MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index);
+
+ /// Parses a generic controller key string.
+ static std::optional ParseGenericControllerKey(
+ InputSourceType clazz, const std::string_view& source, const std::string_view& sub_binding);
+
+ /// Converts a generic controller key to a string.
+ static std::string ConvertGenericControllerKeyToString(InputBindingKey key);
+};
diff --git a/pcsx2/Frontend/SDLInputSource.cpp b/pcsx2/Frontend/SDLInputSource.cpp
new file mode 100644
index 0000000000..6b1352ddcd
--- /dev/null
+++ b/pcsx2/Frontend/SDLInputSource.cpp
@@ -0,0 +1,513 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "PrecompiledHeader.h"
+#include "Frontend/SDLInputSource.h"
+#include "Frontend/InputManager.h"
+#include "Host.h"
+#include "HostSettings.h"
+#include "common/Assertions.h"
+#include "common/StringUtil.h"
+#include "common/Console.h"
+#include
+
+static const char* s_sdl_axis_names[] = {
+ "LeftX", // SDL_CONTROLLER_AXIS_LEFTX
+ "LeftY", // SDL_CONTROLLER_AXIS_LEFTY
+ "RightX", // SDL_CONTROLLER_AXIS_RIGHTX
+ "RightY", // SDL_CONTROLLER_AXIS_RIGHTY
+ "LeftTrigger", // SDL_CONTROLLER_AXIS_TRIGGERLEFT
+ "RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
+};
+
+static const char* s_sdl_button_names[] = {
+ "A", // SDL_CONTROLLER_BUTTON_A
+ "B", // SDL_CONTROLLER_BUTTON_B
+ "X", // SDL_CONTROLLER_BUTTON_X
+ "Y", // SDL_CONTROLLER_BUTTON_Y
+ "Back", // SDL_CONTROLLER_BUTTON_BACK
+ "Guide", // SDL_CONTROLLER_BUTTON_GUIDE
+ "Start", // SDL_CONTROLLER_BUTTON_START
+ "LeftStick", // SDL_CONTROLLER_BUTTON_LEFTSTICK
+ "RightStick", // SDL_CONTROLLER_BUTTON_RIGHTSTICK
+ "LeftShoulder", // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
+ "RightShoulder", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
+ "DPadUp", // SDL_CONTROLLER_BUTTON_DPAD_UP
+ "DPadDown", // SDL_CONTROLLER_BUTTON_DPAD_DOWN
+ "DPadLeft", // SDL_CONTROLLER_BUTTON_DPAD_LEFT
+ "DPadRight", // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
+ "Misc1", // SDL_CONTROLLER_BUTTON_MISC1
+ "Paddle1", // SDL_CONTROLLER_BUTTON_PADDLE1
+ "Paddle2", // SDL_CONTROLLER_BUTTON_PADDLE2
+ "Paddle3", // SDL_CONTROLLER_BUTTON_PADDLE3
+ "Paddle4", // SDL_CONTROLLER_BUTTON_PADDLE4
+ "Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD
+};
+
+SDLInputSource::SDLInputSource() = default;
+
+SDLInputSource::~SDLInputSource() { pxAssert(m_controllers.empty()); }
+
+bool SDLInputSource::Initialize(SettingsInterface& si)
+{
+ std::optional> controller_db_data = Host::ReadResourceFile("game_controller_db.txt");
+ if (controller_db_data.has_value())
+ {
+ SDL_RWops* ops = SDL_RWFromConstMem(controller_db_data->data(), static_cast(controller_db_data->size()));
+ if (SDL_GameControllerAddMappingsFromRW(ops, true) < 0)
+ Console.Error("SDL_GameControllerAddMappingsFromRW() failed: %s", SDL_GetError());
+ }
+ else
+ {
+ Console.Error("Controller database resource is missing.");
+ }
+
+ const bool ds4_rumble_enabled = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
+ if (ds4_rumble_enabled)
+ {
+ Console.WriteLn("Enabling PS4/PS5 enhanced mode.");
+#if SDL_VERSION_ATLEAST(2, 0, 9)
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "true");
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "true");
+#endif
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5, "true");
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "true");
+#endif
+ }
+
+ if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
+ {
+ Console.Error("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
+ return false;
+ }
+
+ // we should open the controllers as the connected events come in, so no need to do any more here
+ m_sdl_subsystem_initialized = true;
+ return true;
+}
+
+void SDLInputSource::Shutdown()
+{
+ while (!m_controllers.empty())
+ CloseGameController(m_controllers.begin()->joystick_id);
+
+ if (m_sdl_subsystem_initialized)
+ {
+ SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
+ m_sdl_subsystem_initialized = false;
+ }
+}
+
+void SDLInputSource::PollEvents()
+{
+ for (;;)
+ {
+ SDL_Event ev;
+ if (SDL_PollEvent(&ev))
+ ProcessSDLEvent(&ev);
+ else
+ break;
+ }
+}
+
+std::optional SDLInputSource::ParseKeyString(
+ const std::string_view& device, const std::string_view& binding)
+{
+ if (!StringUtil::StartsWith(device, "SDL-") || binding.empty())
+ return std::nullopt;
+
+ const std::optional player_id = StringUtil::FromChars(device.substr(4));
+ if (!player_id.has_value() || player_id.value() < 0)
+ return std::nullopt;
+
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::SDL;
+ key.source_index = static_cast(player_id.value());
+
+ if (StringUtil::EndsWith(binding, "Motor"))
+ {
+ key.source_subtype = InputSubclass::ControllerMotor;
+ if (binding == "LargeMotor")
+ {
+ key.data = 0;
+ return key;
+ }
+ else if (binding == "SmallMotor")
+ {
+ key.data = 1;
+ return key;
+ }
+ else
+ {
+ return std::nullopt;
+ }
+ }
+ else if (StringUtil::EndsWith(binding, "Haptic"))
+ {
+ key.source_subtype = InputSubclass::ControllerHaptic;
+ key.data = 0;
+ return key;
+ }
+ else if (binding[0] == '+' || binding[0] == '-')
+ {
+ // likely an axis
+ const std::string_view axis_name(binding.substr(1));
+ for (u32 i = 0; i < std::size(s_sdl_axis_names); i++)
+ {
+ if (axis_name == s_sdl_axis_names[i])
+ {
+ // found an axis!
+ key.source_subtype = InputSubclass::ControllerAxis;
+ key.data = i;
+ key.negative = (binding[0] == '-');
+ return key;
+ }
+ }
+ }
+ else
+ {
+ // must be a button
+ for (u32 i = 0; i < std::size(s_sdl_button_names); i++)
+ {
+ if (binding == s_sdl_button_names[i])
+ {
+ key.source_subtype = InputSubclass::ControllerButton;
+ key.data = i;
+ return key;
+ }
+ }
+ }
+
+ // unknown axis/button
+ return std::nullopt;
+}
+
+std::string SDLInputSource::ConvertKeyToString(InputBindingKey key)
+{
+ std::string ret;
+
+ if (key.source_type == InputSourceType::SDL)
+ {
+ if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_sdl_axis_names))
+ {
+ ret = StringUtil::StdStringFromFormat(
+ "SDL-%u/%c%s", key.source_index, key.negative ? '-' : '+', s_sdl_axis_names[key.data]);
+ }
+ else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_sdl_button_names))
+ {
+ ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
+ }
+ else if (key.source_subtype == InputSubclass::ControllerMotor)
+ {
+ ret = StringUtil::StdStringFromFormat("SDL-%u/%sMotor", key.source_index, key.data ? "Large" : "Small");
+ }
+ else if (key.source_subtype == InputSubclass::ControllerHaptic)
+ {
+ ret = StringUtil::StdStringFromFormat("SDL-%u/Haptic", key.source_index);
+ }
+ }
+
+ return ret;
+}
+
+bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
+{
+ switch (event->type)
+ {
+ case SDL_CONTROLLERDEVICEADDED:
+ {
+ Console.WriteLn("(SDLInputSource) Controller %d inserted", event->cdevice.which);
+ OpenGameController(event->cdevice.which);
+ return true;
+ }
+
+ case SDL_CONTROLLERDEVICEREMOVED:
+ {
+ Console.WriteLn("(SDLInputSource) Controller %d removed", event->cdevice.which);
+ CloseGameController(event->cdevice.which);
+ return true;
+ }
+
+ case SDL_CONTROLLERAXISMOTION:
+ return HandleControllerAxisEvent(&event->caxis);
+
+ case SDL_CONTROLLERBUTTONDOWN:
+ case SDL_CONTROLLERBUTTONUP:
+ return HandleControllerButtonEvent(&event->cbutton);
+
+ default:
+ return false;
+ }
+}
+
+SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id)
+{
+ return std::find_if(
+ m_controllers.begin(), m_controllers.end(), [id](const ControllerData& cd) { return cd.joystick_id == id; });
+}
+
+SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForPlayerId(int id)
+{
+ return std::find_if(
+ m_controllers.begin(), m_controllers.end(), [id](const ControllerData& cd) { return cd.player_id == id; });
+}
+
+int SDLInputSource::GetFreePlayerId() const
+{
+ for (int player_id = 0;; player_id++)
+ {
+ size_t i;
+ for (i = 0; i < m_controllers.size(); i++)
+ {
+ if (m_controllers[i].player_id == player_id)
+ break;
+ }
+ if (i == m_controllers.size())
+ return player_id;
+ }
+
+ return 0;
+}
+
+bool SDLInputSource::OpenGameController(int index)
+{
+ SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
+ SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
+ if (!gcontroller || !joystick)
+ {
+ Console.Error("(SDLInputSource) Failed to open controller %d", index);
+ if (gcontroller)
+ SDL_GameControllerClose(gcontroller);
+
+ return false;
+ }
+
+ int joystick_id = SDL_JoystickInstanceID(joystick);
+#if SDL_VERSION_ATLEAST(2, 0, 9)
+ int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
+#else
+ int player_id = -1;
+#endif
+ if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
+ {
+ const int free_player_id = GetFreePlayerId();
+ Console.Warning("(SDLInputSource) Controller %d (joystick %d) returned player ID %d, which is invalid or in "
+ "use. Using ID %d instead.",
+ index, joystick_id, player_id, free_player_id);
+ player_id = free_player_id;
+ }
+
+ Console.WriteLn("(SDLInputSource) Opened controller %d (instance id %d, player id %d): %s", index, joystick_id,
+ player_id, SDL_GameControllerName(gcontroller));
+
+ ControllerData cd = {};
+ cd.player_id = player_id;
+ cd.joystick_id = joystick_id;
+ cd.haptic_left_right_effect = -1;
+ cd.game_controller = gcontroller;
+
+#if SDL_VERSION_ATLEAST(2, 0, 9)
+ cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
+#else
+ cd.use_game_controller_rumble = false;
+#endif
+
+ if (cd.use_game_controller_rumble)
+ {
+ Console.WriteLn(
+ "(SDLInputSource) Rumble is supported on '%s' via gamecontroller", SDL_GameControllerName(gcontroller));
+ }
+ else
+ {
+ SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
+ if (haptic)
+ {
+ SDL_HapticEffect ef = {};
+ ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
+ ef.leftright.length = 1000;
+
+ int ef_id = SDL_HapticNewEffect(haptic, &ef);
+ if (ef_id >= 0)
+ {
+ cd.haptic = haptic;
+ cd.haptic_left_right_effect = ef_id;
+ }
+ else
+ {
+ Console.Error("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
+ if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
+ {
+ cd.haptic = haptic;
+ }
+ else
+ {
+ Console.Error("(SDLInputSource) No haptic rumble supported: %s", SDL_GetError());
+ SDL_HapticClose(haptic);
+ }
+ }
+ }
+
+ if (cd.haptic)
+ Console.WriteLn(
+ "(SDLInputSource) Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
+ }
+
+ if (!cd.haptic && !cd.use_game_controller_rumble)
+ Console.Warning("(SDLInputSource) Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));
+
+ m_controllers.push_back(std::move(cd));
+ return true;
+}
+
+bool SDLInputSource::CloseGameController(int joystick_index)
+{
+ auto it = GetControllerDataForJoystickId(joystick_index);
+ if (it == m_controllers.end())
+ return false;
+
+ if (it->haptic)
+ SDL_HapticClose(static_cast(it->haptic));
+
+ SDL_GameControllerClose(static_cast(it->game_controller));
+ m_controllers.erase(it);
+ return true;
+}
+
+bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
+{
+ auto it = GetControllerDataForJoystickId(ev->which);
+ if (it == m_controllers.end())
+ return false;
+
+ const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis));
+ const float value = static_cast(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f);
+ return InputManager::InvokeEvents(key, value);
+}
+
+bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
+{
+ auto it = GetControllerDataForJoystickId(ev->which);
+ if (it == m_controllers.end())
+ return false;
+
+ const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button));
+ return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f);
+}
+
+std::vector SDLInputSource::EnumerateMotors()
+{
+ std::vector ret;
+
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::SDL;
+
+ for (ControllerData& cd : m_controllers)
+ {
+ key.source_index = cd.player_id;
+
+ if (cd.use_game_controller_rumble || cd.haptic_left_right_effect)
+ {
+ // two motors
+ key.source_subtype = InputSubclass::ControllerMotor;
+ key.data = 0;
+ ret.push_back(key);
+ key.data = 1;
+ ret.push_back(key);
+ }
+ else if (cd.haptic)
+ {
+ // haptic effect
+ key.source_subtype = InputSubclass::ControllerHaptic;
+ key.data = 0;
+ ret.push_back(key);
+ }
+ }
+
+ return ret;
+}
+
+void SDLInputSource::UpdateMotorState(InputBindingKey key, float intensity)
+{
+ if (key.source_subtype != InputSubclass::ControllerMotor && key.source_subtype != InputSubclass::ControllerHaptic)
+ return;
+
+ auto it = GetControllerDataForPlayerId(key.source_index);
+ if (it == m_controllers.end())
+ return;
+
+ it->rumble_intensity[key.data] = static_cast(intensity * 65535.0f);
+ SendRumbleUpdate(&(*it));
+}
+
+void SDLInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity)
+{
+ if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
+ small_key.source_subtype != InputSubclass::ControllerMotor)
+ {
+ // bonkers config where they're mapped to different controllers... who would do such a thing?
+ UpdateMotorState(large_key, large_intensity);
+ UpdateMotorState(small_key, small_intensity);
+ return;
+ }
+
+ auto it = GetControllerDataForPlayerId(large_key.source_index);
+ if (it == m_controllers.end())
+ return;
+
+ it->rumble_intensity[large_key.data] = static_cast(large_intensity * 65535.0f);
+ it->rumble_intensity[small_key.data] = static_cast(small_intensity * 65535.0f);
+ SendRumbleUpdate(&(*it));
+}
+
+void SDLInputSource::SendRumbleUpdate(ControllerData* cd)
+{
+ // we'll update before this duration is elapsed
+ static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS
+
+#if SDL_VERSION_ATLEAST(2, 0, 9)
+ if (cd->use_game_controller_rumble)
+ {
+ SDL_GameControllerRumble(cd->game_controller, cd->rumble_intensity[0], cd->rumble_intensity[1], DURATION);
+ return;
+ }
+#endif
+
+ if (cd->haptic_left_right_effect >= 0)
+ {
+ if ((static_cast(cd->rumble_intensity[0]) + static_cast(cd->rumble_intensity[1])) > 0)
+ {
+ SDL_HapticEffect ef;
+ ef.type = SDL_HAPTIC_LEFTRIGHT;
+ ef.leftright.large_magnitude = cd->rumble_intensity[0];
+ ef.leftright.small_magnitude = cd->rumble_intensity[1];
+ ef.leftright.length = DURATION;
+ SDL_HapticUpdateEffect(cd->haptic, cd->haptic_left_right_effect, &ef);
+ SDL_HapticRunEffect(cd->haptic, cd->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
+ }
+ else
+ {
+ SDL_HapticStopEffect(cd->haptic, cd->haptic_left_right_effect);
+ }
+ }
+ else
+ {
+ const float strength = static_cast(std::max(cd->rumble_intensity[0], cd->rumble_intensity[1])) * (1.0f / 65535.0f);
+ if (strength > 0.0f)
+ SDL_HapticRumblePlay(cd->haptic, strength, DURATION);
+ else
+ SDL_HapticRumbleStop(cd->haptic);
+ }
+}
diff --git a/pcsx2/Frontend/SDLInputSource.h b/pcsx2/Frontend/SDLInputSource.h
new file mode 100644
index 0000000000..a70a3cfefc
--- /dev/null
+++ b/pcsx2/Frontend/SDLInputSource.h
@@ -0,0 +1,78 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+#include "Frontend/InputSource.h"
+#include "SDL.h"
+#include
+#include
+#include
+#include
+
+class SettingsInterface;
+
+class SDLInputSource final : public InputSource
+{
+public:
+ SDLInputSource();
+ ~SDLInputSource();
+
+ bool Initialize(SettingsInterface& si) override;
+ void Shutdown() override;
+
+ void PollEvents() override;
+ std::vector EnumerateMotors() override;
+ void UpdateMotorState(InputBindingKey key, float intensity) override;
+ void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity) override;
+
+ std::optional ParseKeyString(const std::string_view& device, const std::string_view& binding) override;
+ std::string ConvertKeyToString(InputBindingKey key) override;
+
+ bool ProcessSDLEvent(const SDL_Event* event);
+
+private:
+ enum : int
+ {
+ MAX_NUM_AXES = 7,
+ MAX_NUM_BUTTONS = 16,
+ };
+
+ struct ControllerData
+ {
+ SDL_Haptic* haptic;
+ SDL_GameController* game_controller;
+ u16 rumble_intensity[2];
+ int haptic_left_right_effect;
+ int joystick_id;
+ int player_id;
+ bool use_game_controller_rumble;
+ };
+
+ using ControllerDataVector = std::vector;
+
+ ControllerDataVector::iterator GetControllerDataForJoystickId(int id);
+ ControllerDataVector::iterator GetControllerDataForPlayerId(int id);
+ int GetFreePlayerId() const;
+
+ bool OpenGameController(int index);
+ bool CloseGameController(int joystick_index);
+ bool HandleControllerAxisEvent(const SDL_ControllerAxisEvent* event);
+ bool HandleControllerButtonEvent(const SDL_ControllerButtonEvent* event);
+ void SendRumbleUpdate(ControllerData* cd);
+
+ ControllerDataVector m_controllers;
+
+ bool m_sdl_subsystem_initialized = false;
+};
diff --git a/pcsx2/Frontend/XInputSource.cpp b/pcsx2/Frontend/XInputSource.cpp
new file mode 100644
index 0000000000..33aa153959
--- /dev/null
+++ b/pcsx2/Frontend/XInputSource.cpp
@@ -0,0 +1,368 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "PrecompiledHeader.h"
+#include "Frontend/XInputSource.h"
+#include "Frontend/InputManager.h"
+#include "HostSettings.h"
+#include "common/Assertions.h"
+#include "common/StringUtil.h"
+#include "common/Console.h"
+#include
+
+const char* XInputSource::s_axis_names[XInputSource::NUM_AXES] = {
+ "LeftX", // AXIS_LEFTX
+ "LeftY", // AXIS_LEFTY
+ "RightX", // AXIS_RIGHTX
+ "RightY", // AXIS_RIGHTY
+ "LeftTrigger", // AXIS_TRIGGERLEFT
+ "RightTrigger", // AXIS_TRIGGERRIGHT
+};
+
+const char* XInputSource::s_button_names[XInputSource::NUM_BUTTONS] = {
+ "DPadUp", // XINPUT_GAMEPAD_DPAD_UP
+ "DPadDown", // XINPUT_GAMEPAD_DPAD_DOWN
+ "DPadLeft", // XINPUT_GAMEPAD_DPAD_LEFT
+ "DPadRight", // XINPUT_GAMEPAD_DPAD_RIGHT
+ "Start", // XINPUT_GAMEPAD_START
+ "Back", // XINPUT_GAMEPAD_BACK
+ "LeftStick", // XINPUT_GAMEPAD_LEFT_THUMB
+ "RightStick", // XINPUT_GAMEPAD_RIGHT_THUMB
+ "LeftShoulder", // XINPUT_GAMEPAD_LEFT_SHOULDER
+ "RightShoulder", // XINPUT_GAMEPAD_RIGHT_SHOULDER
+ "A", // XINPUT_GAMEPAD_A
+ "B", // XINPUT_GAMEPAD_B
+ "X", // XINPUT_GAMEPAD_X
+ "Y", // XINPUT_GAMEPAD_Y
+ "Guide", // XINPUT_GAMEPAD_GUIDE
+};
+const u16 XInputSource::s_button_masks[XInputSource::NUM_BUTTONS] = {
+ XINPUT_GAMEPAD_DPAD_UP,
+ XINPUT_GAMEPAD_DPAD_DOWN,
+ XINPUT_GAMEPAD_DPAD_LEFT,
+ XINPUT_GAMEPAD_DPAD_RIGHT,
+ XINPUT_GAMEPAD_START,
+ XINPUT_GAMEPAD_BACK,
+ XINPUT_GAMEPAD_LEFT_THUMB,
+ XINPUT_GAMEPAD_RIGHT_THUMB,
+ XINPUT_GAMEPAD_LEFT_SHOULDER,
+ XINPUT_GAMEPAD_RIGHT_SHOULDER,
+ XINPUT_GAMEPAD_A,
+ XINPUT_GAMEPAD_B,
+ XINPUT_GAMEPAD_X,
+ XINPUT_GAMEPAD_Y,
+ 0x400, // XINPUT_GAMEPAD_GUIDE
+};
+
+XInputSource::XInputSource() = default;
+
+XInputSource::~XInputSource() = default;
+
+bool XInputSource::Initialize(SettingsInterface& si)
+{
+#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
+ m_xinput_module = LoadLibraryW(L"xinput1_4");
+ if (!m_xinput_module)
+ {
+ m_xinput_module = LoadLibraryW(L"xinput1_3");
+ }
+ if (!m_xinput_module)
+ {
+ m_xinput_module = LoadLibraryW(L"xinput9_1_0");
+ }
+ if (!m_xinput_module)
+ {
+ Console.Error("Failed to load XInput module.");
+ return false;
+ }
+
+ // Try the hidden version of XInputGetState(), which lets us query the guide button.
+ m_xinput_get_state =
+ reinterpret_cast(GetProcAddress(m_xinput_module, reinterpret_cast(100)));
+ if (!m_xinput_get_state)
+ reinterpret_cast(GetProcAddress(m_xinput_module, "XInputGetState"));
+ m_xinput_set_state =
+ reinterpret_cast(GetProcAddress(m_xinput_module, "XInputSetState"));
+ m_xinput_get_capabilities =
+ reinterpret_cast(GetProcAddress(m_xinput_module, "XInputGetCapabilities"));
+#else
+ m_xinput_get_state = XInputGetState;
+ m_xinput_set_state = XInputSetState;
+ m_xinput_get_capabilities = XInputGetCapabilities;
+#endif
+ if (!m_xinput_get_state || !m_xinput_set_state || !m_xinput_get_capabilities)
+ {
+ Console.Error("Failed to get XInput function pointers.");
+ return false;
+ }
+
+ return true;
+}
+
+void XInputSource::Shutdown()
+{
+#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
+ if (m_xinput_module)
+ {
+ FreeLibrary(m_xinput_module);
+ m_xinput_module = nullptr;
+ }
+#endif
+
+ m_xinput_get_state = nullptr;
+ m_xinput_set_state = nullptr;
+ m_xinput_get_capabilities = nullptr;
+}
+
+void XInputSource::PollEvents()
+{
+ for (u32 i = 0; i < NUM_CONTROLLERS; i++)
+ {
+ XINPUT_STATE new_state;
+ const DWORD result = m_xinput_get_state(i, &new_state);
+ const bool was_connected = m_controllers[i].connected;
+ if (result == ERROR_SUCCESS)
+ {
+ if (!was_connected)
+ HandleControllerConnection(i);
+
+ CheckForStateChanges(i, new_state);
+ }
+ else
+ {
+ if (result != ERROR_DEVICE_NOT_CONNECTED)
+ Console.Warning("XInputGetState(%u) failed: 0x%08X / 0x%08X", i, result, GetLastError());
+
+ if (was_connected)
+ HandleControllerDisconnection(i);
+ }
+ }
+}
+
+std::optional XInputSource::ParseKeyString(
+ const std::string_view& device, const std::string_view& binding)
+{
+ if (!StringUtil::StartsWith(device, "XInput-") || binding.empty())
+ return std::nullopt;
+
+ const std::optional player_id = StringUtil::FromChars(device.substr(7));
+ if (!player_id.has_value() || player_id.value() < 0)
+ return std::nullopt;
+
+ InputBindingKey key = {};
+ key.source_type = InputSourceType::XInput;
+ key.source_index = static_cast(player_id.value());
+
+ if (StringUtil::EndsWith(binding, "Motor"))
+ {
+ key.source_subtype = InputSubclass::ControllerMotor;
+ if (binding == "LargeMotor")
+ {
+ key.data = 0;
+ return key;
+ }
+ else if (binding == "SmallMotor")
+ {
+ key.data = 1;
+ return key;
+ }
+ else
+ {
+ return std::nullopt;
+ }
+ }
+ else if (binding[0] == '+' || binding[0] == '-')
+ {
+ // likely an axis
+ const std::string_view axis_name(binding.substr(1));
+ for (u32 i = 0; i < std::size(s_axis_names); i++)
+ {
+ if (axis_name == s_axis_names[i])
+ {
+ // found an axis!
+ key.source_subtype = InputSubclass::ControllerAxis;
+ key.data = i;
+ key.negative = (binding[0] == '-');
+ return key;
+ }
+ }
+ }
+ else
+ {
+ // must be a button
+ for (u32 i = 0; i < std::size(s_button_names); i++)
+ {
+ if (binding == s_button_names[i])
+ {
+ key.source_subtype = InputSubclass::ControllerButton;
+ key.data = i;
+ return key;
+ }
+ }
+ }
+
+ // unknown axis/button
+ return std::nullopt;
+}
+
+std::string XInputSource::ConvertKeyToString(InputBindingKey key)
+{
+ std::string ret;
+
+ if (key.source_type == InputSourceType::XInput)
+ {
+ if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_axis_names))
+ {
+ ret = StringUtil::StdStringFromFormat(
+ "XInput-%u/%c%s", key.source_index, key.negative ? '-' : '+', s_axis_names[key.data]);
+ }
+ else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names))
+ {
+ ret = StringUtil::StdStringFromFormat("XInput-%u/%s", key.source_index, s_button_names[key.data]);
+ }
+ else if (key.source_subtype == InputSubclass::ControllerMotor)
+ {
+ ret = StringUtil::StdStringFromFormat("XInput-%u/%sMotor", key.source_index, key.data ? "Large" : "Small");
+ }
+ }
+
+ return ret;
+}
+
+std::vector XInputSource::EnumerateMotors()
+{
+ std::vector ret;
+
+ for (u32 i = 0; i < NUM_CONTROLLERS; i++)
+ {
+ const ControllerData& cd = m_controllers[i];
+ if (!cd.connected)
+ continue;
+
+ if (cd.has_large_motor)
+ ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 0));
+
+ if (cd.has_small_motor)
+ ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 1));
+ }
+
+ return ret;
+}
+
+void XInputSource::HandleControllerConnection(u32 index)
+{
+ Console.WriteLn("XInput controller %u connected.", index);
+
+ XINPUT_CAPABILITIES caps = {};
+ if (m_xinput_get_capabilities(index, 0, &caps) != ERROR_SUCCESS)
+ Console.Warning("Failed to get XInput capabilities for controller %u", index);
+
+ ControllerData& cd = m_controllers[index];
+ cd.connected = true;
+ cd.has_large_motor = caps.Vibration.wLeftMotorSpeed != 0;
+ cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0;
+}
+
+void XInputSource::HandleControllerDisconnection(u32 index)
+{
+ Console.WriteLn("XInput controller %u disconnected.", index);
+ m_controllers[index] = {};
+}
+
+void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)
+{
+ ControllerData& cd = m_controllers[index];
+ if (new_state.dwPacketNumber == cd.last_state.dwPacketNumber)
+ return;
+
+ cd.last_state.dwPacketNumber = new_state.dwPacketNumber;
+
+ XINPUT_GAMEPAD& ogp = cd.last_state.Gamepad;
+ const XINPUT_GAMEPAD& ngp = new_state.Gamepad;
+
+#define CHECK_AXIS(field, axis, min_value, max_value) \
+ if (ogp.field != ngp.field) \
+ { \
+ InputManager::InvokeEvents( \
+ MakeGenericControllerAxisKey(InputSourceType::XInput, index, axis), \
+ static_cast(ngp.field) / ((ngp.field < 0) ? min_value : max_value)); \
+ ogp.field = ngp.field; \
+ }
+
+ CHECK_AXIS(sThumbLX, AXIS_LEFTX, -32768, 32767);
+ CHECK_AXIS(sThumbLY, AXIS_LEFTY, -32768, 32767);
+ CHECK_AXIS(sThumbRX, AXIS_RIGHTX, -32768, 32767);
+ CHECK_AXIS(sThumbRY, AXIS_RIGHTY, -32768, 32767);
+ CHECK_AXIS(bLeftTrigger, AXIS_LEFTTRIGGER, -128, 127);
+ CHECK_AXIS(bRightTrigger, AXIS_RIGHTTRIGGER, -128, 127);
+
+#undef CHECK_AXIS
+
+ const u16 old_button_bits = ogp.wButtons;
+ const u16 new_button_bits = ngp.wButtons;
+ if (old_button_bits != new_button_bits)
+ {
+ for (u32 button = 0; button < NUM_BUTTONS; button++)
+ {
+ const u16 button_mask = s_button_masks[button];
+ if ((old_button_bits & button_mask) != (new_button_bits & button_mask))
+ {
+ InputManager::InvokeEvents(
+ MakeGenericControllerButtonKey(InputSourceType::XInput, index, button),
+ (new_button_bits & button_mask) != 0);
+ }
+
+ ogp.wButtons = ngp.wButtons;
+ }
+ }
+}
+
+void XInputSource::UpdateMotorState(InputBindingKey key, float intensity)
+{
+ if (key.source_subtype != InputSubclass::ControllerMotor || key.source_index >= NUM_CONTROLLERS)
+ return;
+
+ ControllerData& cd = m_controllers[key.source_index];
+ if (!cd.connected)
+ return;
+
+ const u16 i_intensity = static_cast(intensity * 65535.0f);
+ if (key.data != 0)
+ cd.last_vibration.wRightMotorSpeed = i_intensity;
+ else
+ cd.last_vibration.wLeftMotorSpeed = i_intensity;
+
+ m_xinput_set_state(key.source_index, &cd.last_vibration);
+}
+
+void XInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity)
+{
+ if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
+ small_key.source_subtype != InputSubclass::ControllerMotor)
+ {
+ // bonkers config where they're mapped to different controllers... who would do such a thing?
+ UpdateMotorState(large_key, large_intensity);
+ UpdateMotorState(small_key, small_intensity);
+ return;
+ }
+
+ ControllerData& cd = m_controllers[large_key.source_index];
+ if (!cd.connected)
+ return;
+
+ cd.last_vibration.wLeftMotorSpeed = static_cast(large_intensity * 65535.0f);
+ cd.last_vibration.wRightMotorSpeed = static_cast(small_intensity * 65535.0f);
+ m_xinput_set_state(large_key.source_index, &cd.last_vibration);
+}
diff --git a/pcsx2/Frontend/XInputSource.h b/pcsx2/Frontend/XInputSource.h
new file mode 100644
index 0000000000..e7e59c6b06
--- /dev/null
+++ b/pcsx2/Frontend/XInputSource.h
@@ -0,0 +1,86 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+#include "Frontend/InputSource.h"
+#include
+#include
+#include
+#include
+#include
+
+class SettingsInterface;
+
+class XInputSource final : public InputSource
+{
+public:
+ XInputSource();
+ ~XInputSource();
+
+ bool Initialize(SettingsInterface& si) override;
+ void Shutdown() override;
+
+ void PollEvents() override;
+ std::vector EnumerateMotors() override;
+ void UpdateMotorState(InputBindingKey key, float intensity) override;
+ void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity) override;
+
+ std::optional ParseKeyString(const std::string_view& device, const std::string_view& binding) override;
+ std::string ConvertKeyToString(InputBindingKey key) override;
+
+private:
+ enum : u32
+ {
+ NUM_CONTROLLERS = XUSER_MAX_COUNT, // 4
+ NUM_BUTTONS = 15,
+ };
+
+ enum : u32
+ {
+ AXIS_LEFTX,
+ AXIS_LEFTY,
+ AXIS_RIGHTX,
+ AXIS_RIGHTY,
+ AXIS_LEFTTRIGGER,
+ AXIS_RIGHTTRIGGER,
+ NUM_AXES,
+ };
+
+ struct ControllerData
+ {
+ XINPUT_STATE last_state = {};
+ XINPUT_VIBRATION last_vibration = {};
+ bool connected = false;
+ bool has_large_motor = false;
+ bool has_small_motor = false;
+ };
+
+ using ControllerDataArray = std::array;
+
+ void CheckForStateChanges(u32 index, const XINPUT_STATE& new_state);
+ void HandleControllerConnection(u32 index);
+ void HandleControllerDisconnection(u32 index);
+
+ ControllerDataArray m_controllers;
+
+ HMODULE m_xinput_module{};
+ DWORD(WINAPI* m_xinput_get_state)(DWORD, XINPUT_STATE*);
+ DWORD(WINAPI* m_xinput_set_state)(DWORD, XINPUT_VIBRATION*);
+ DWORD(WINAPI* m_xinput_get_capabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*);
+
+ static const char* s_axis_names[NUM_AXES];
+ static const char* s_button_names[NUM_BUTTONS];
+ static const u16 s_button_masks[NUM_BUTTONS];
+};
diff --git a/pcsx2/PAD/Host/KeyStatus.h b/pcsx2/PAD/Host/KeyStatus.h
index ab3e59b2a0..d2b543d543 100644
--- a/pcsx2/PAD/Host/KeyStatus.h
+++ b/pcsx2/PAD/Host/KeyStatus.h
@@ -32,6 +32,7 @@ private:
u8 m_button_pressure[GAMEPAD_NUMBER][MAX_KEYS];
PADAnalog m_analog[GAMEPAD_NUMBER];
float m_axis_scale[GAMEPAD_NUMBER];
+ float m_vibration_scale[GAMEPAD_NUMBER][2];
public:
KeyStatus();
@@ -39,7 +40,9 @@ public:
void Set(u32 pad, u32 index, float value);
- void SetAxisScale(u32 pad, float scale) { m_axis_scale[pad] = scale; }
+ __fi void SetAxisScale(u32 pad, float scale) { m_axis_scale[pad] = scale; }
+ __fi float GetVibrationScale(u32 pad, u32 motor) const { return m_vibration_scale[pad][motor]; }
+ __fi void SetVibrationScale(u32 pad, u32 motor, float scale) { m_vibration_scale[pad][motor] = scale; }
u16 GetButtons(u32 pad);
u8 GetPressure(u32 pad, u32 index);
diff --git a/pcsx2/PAD/Host/PAD.cpp b/pcsx2/PAD/Host/PAD.cpp
index b911bec8b8..10e7a15ea6 100644
--- a/pcsx2/PAD/Host/PAD.cpp
+++ b/pcsx2/PAD/Host/PAD.cpp
@@ -167,11 +167,20 @@ void PAD::LoadConfig(const SettingsInterface& si)
{
const std::string section(StringUtil::StdStringFromFormat("Pad%u", i + 1u));
const float axis_scale = si.GetFloatValue(section.c_str(), "AxisScale", 1.0f);
+ const float large_motor_scale = si.GetFloatValue(section.c_str(), "LargeMotorScale", 1.0f);
+ const float small_motor_scale = si.GetFloatValue(section.c_str(), "SmallMotorScale", 1.0f);
g_key_status.SetAxisScale(i, axis_scale);
+ g_key_status.SetVibrationScale(i, 0, large_motor_scale);
+ g_key_status.SetVibrationScale(i, 1, small_motor_scale);
}
}
+void PAD::Update()
+{
+ Pad::rumble_all();
+}
+
std::vector PAD::GetControllerTypeNames()
{
return {"DualShock2"};
@@ -211,6 +220,18 @@ std::vector PAD::GetControllerBinds(const std::string_view& type)
return {};
}
+PAD::VibrationCapabilities PAD::GetControllerVibrationCapabilities(const std::string_view& type)
+{
+ if (type == "DualShock2")
+ {
+ return VibrationCapabilities::LargeSmallMotors;
+ }
+ else
+ {
+ return VibrationCapabilities::NoVibration;
+ }
+}
+
void PAD::SetControllerState(u32 controller, u32 bind, float value)
{
if (controller >= GAMEPAD_NUMBER || bind >= MAX_KEYS)
diff --git a/pcsx2/PAD/Host/PAD.h b/pcsx2/PAD/Host/PAD.h
index 0d62f31542..04243da511 100644
--- a/pcsx2/PAD/Host/PAD.h
+++ b/pcsx2/PAD/Host/PAD.h
@@ -35,15 +35,29 @@ u8 PADpoll(u8 value);
namespace PAD
{
+ enum class VibrationCapabilities
+ {
+ NoVibration,
+ LargeSmallMotors,
+ SingleMotor,
+ Count
+ };
+
/// Reloads configuration.
void LoadConfig(const SettingsInterface& si);
+ /// Updates vibration and other internal state. Called at the *end* of a frame.
+ void Update();
+
/// Returns a list of controller type names.
std::vector GetControllerTypeNames();
/// Returns the list of binds for the specified controller type.
std::vector GetControllerBinds(const std::string_view& type);
+ /// Returns the vibration configuration for the specified controller type.
+ VibrationCapabilities GetControllerVibrationCapabilities(const std::string_view& type);
+
/// Sets the specified bind on a controller to the specified pressure (normalized to 0..1).
void SetControllerState(u32 controller, u32 bind, float value);
} // namespace PAD
diff --git a/pcsx2/PAD/Host/StateManagement.cpp b/pcsx2/PAD/Host/StateManagement.cpp
index 62e27d9e34..b2530202c9 100644
--- a/pcsx2/PAD/Host/StateManagement.cpp
+++ b/pcsx2/PAD/Host/StateManagement.cpp
@@ -17,6 +17,7 @@
#include "PAD/Host/StateManagement.h"
#include "PAD/Host/KeyStatus.h"
+#include "Frontend/InputManager.h"
template
static bool __fi test_bit(T& value, int bit)
@@ -113,17 +114,15 @@ void Pad::reset()
void Pad::rumble(unsigned port)
{
- for (unsigned motor = 0; motor < 2; motor++)
- {
- // TODO: Probably be better to send all of these at once.
- if (nextVibrate[motor] | currentVibrate[motor])
- {
- currentVibrate[motor] = nextVibrate[motor];
+ if (nextVibrate[0] == currentVibrate[0] && nextVibrate[1] == currentVibrate[1])
+ return;
- // TODO: Implement in InputManager
- // Device::DoRumble(motor, port);
- }
- }
+ currentVibrate[0] = nextVibrate[0];
+ currentVibrate[1] = nextVibrate[1];
+ InputManager::SetPadVibrationIntensity(port,
+ std::min(static_cast(currentVibrate[0]) * g_key_status.GetVibrationScale(port, 0) * (1.0f / 255.0f), 1.0f),
+ std::min(static_cast(currentVibrate[1]) * g_key_status.GetVibrationScale(port, 1) * (1.0f / 255.0f), 1.0f)
+ );
}
void Pad::stop_vibrate_all()