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()