InputManager: Add InputManager implementation

This commit is contained in:
Connor McLaughlin 2021-12-31 18:02:17 +10:00 committed by refractionpcsx2
parent b511a54445
commit 14c9cfb310
14 changed files with 2444 additions and 11 deletions

View File

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

View File

@ -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!

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <array>
#include <memory>
#include <mutex>
#include <sstream>
#include <unordered_map>
#include <vector>
// ------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------
enum : u32
{
MAX_KEYS_PER_BINDING = 4,
MAX_MOTORS_PER_PAD = 2,
FIRST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Mouse) + 1u,
LAST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Count),
};
// ------------------------------------------------------------------------
// Event Handler Type
// ------------------------------------------------------------------------
// This class acts as an adapter to convert from normalized values to
// binary values when the callback is a binary/button handler. That way
// you don't need to convert float->bool in your callbacks.
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<InputBindingKey> ParseHostKeyboardKey(
const std::string_view& source, const std::string_view& sub_binding);
static std::optional<InputBindingKey> ParseHostMouseKey(
const std::string_view& source, const std::string_view& sub_binding);
static std::vector<std::string_view> 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<std::string>& 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<InputBindingKey, std::shared_ptr<InputBinding>, InputBindingKeyHash>;
using VibrationBindingArray = std::vector<PadVibrationBinding>;
static BindingMap s_binding_map;
static VibrationBindingArray s_pad_vibration_array;
static std::mutex s_binding_map_write_lock;
// Hooks/intercepting (for setting bindings)
static std::mutex m_event_intercept_mutex;
static InputInterceptHook::Callback m_event_intercept_callback;
// Input sources. Keyboard/mouse don't exist here.
static std::array<std::unique_ptr<InputSource>, static_cast<u32>(InputSourceType::Count)> s_input_sources;
// ------------------------------------------------------------------------
// Hotkeys
// ------------------------------------------------------------------------
static const HotkeyInfo* const s_hotkey_list[] = {g_vm_manager_hotkeys, g_host_hotkeys};
// ------------------------------------------------------------------------
// Binding Parsing
// ------------------------------------------------------------------------
std::vector<std::string_view> InputManager::SplitChord(const std::string_view& binding)
{
std::vector<std::string_view> parts;
// under an if for RVO
if (!binding.empty())
{
std::string_view::size_type last = 0;
std::string_view::size_type next;
while ((next = binding.find('&', last)) != std::string_view::npos)
{
if (last != next)
{
std::string_view part(StringUtil::StripWhitespace(binding.substr(last, next - last)));
if (!part.empty())
parts.push_back(std::move(part));
}
last = next + 1;
}
if (last < (binding.size() - 1))
{
std::string_view part(StringUtil::StripWhitespace(binding.substr(last)));
if (!part.empty())
parts.push_back(std::move(part));
}
}
return parts;
}
bool InputManager::SplitBinding(
const std::string_view& binding, std::string_view* source, std::string_view* sub_binding)
{
const std::string_view::size_type slash_pos = binding.find('/');
if (slash_pos == std::string_view::npos)
{
Console.Warning("Malformed binding: '%*s'", static_cast<int>(binding.size()), binding.data());
return false;
}
*source = std::string_view(binding).substr(0, slash_pos);
*sub_binding = std::string_view(binding).substr(slash_pos + 1);
return true;
}
std::optional<InputBindingKey> InputManager::ParseInputBindingKey(const std::string_view& binding)
{
std::string_view source, sub_binding;
if (!SplitBinding(binding, &source, &sub_binding))
return std::nullopt;
// lameee, string matching
if (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<InputBindingKey> key = s_input_sources[i]->ParseKeyString(source, sub_binding);
if (key.has_value())
return key;
}
}
}
return std::nullopt;
}
bool InputManager::ParseBindingAndGetSource(const std::string_view& binding, InputBindingKey* key, InputSource** source)
{
std::string_view source_string, sub_binding;
if (!SplitBinding(binding, &source_string, &sub_binding))
return false;
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i])
{
std::optional<InputBindingKey> parsed_key = s_input_sources[i]->ParseKeyString(source_string, sub_binding);
if (parsed_key.has_value())
{
*key = parsed_key.value();
*source = s_input_sources[i].get();
return true;
}
}
}
return false;
}
std::string InputManager::ConvertInputBindingKeyToString(InputBindingKey key)
{
if (key.source_type == InputSourceType::Keyboard)
{
const std::optional<std::string> 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<u32>(key.source_type)])
{
return s_input_sources[static_cast<u32>(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<std::string>& bindings, const InputEventHandler& handler)
{
for (const std::string& binding : bindings)
{
std::shared_ptr<InputBinding> ibinding;
const std::vector<std::string_view> chord_bindings(SplitChord(binding));
for (const std::string_view& chord_binding : chord_bindings)
{
std::optional<InputBindingKey> key = ParseInputBindingKey(chord_binding);
if (!key.has_value())
{
Console.WriteLn("Invalid binding: '%s'", binding.c_str());
ibinding.reset();
break;
}
if (!ibinding)
{
ibinding = std::make_shared<InputBinding>();
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<u8>(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<u32>(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<u32>(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<u32>(axis_index);
return key;
}
// ------------------------------------------------------------------------
// Bind Encoders
// ------------------------------------------------------------------------
static std::array<const char*, static_cast<u32>(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<u32>(type)].get();
}
const char* InputManager::InputSourceToString(InputSourceType clazz)
{
return s_input_class_names[static_cast<u32>(clazz)];
}
std::optional<InputSourceType> InputManager::ParseInputSourceString(const std::string_view& str)
{
for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)
{
if (str == s_input_class_names[i])
return static_cast<InputSourceType>(i);
}
return std::nullopt;
}
std::optional<InputBindingKey> InputManager::ParseHostKeyboardKey(
const std::string_view& source, const std::string_view& sub_binding)
{
if (source != "Keyboard")
return std::nullopt;
const std::optional<s32> code = ConvertHostKeyboardStringToCode(sub_binding);
if (!code.has_value())
return std::nullopt;
InputBindingKey key = {};
key.source_type = InputSourceType::Keyboard;
key.data = static_cast<u32>(code.value());
return key;
}
std::optional<InputBindingKey> InputManager::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<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
if (!button_number.has_value() || button_number.value() < 0)
return std::nullopt;
key.source_subtype = InputSubclass::MouseButton;
key.data = static_cast<u32>(button_number.value());
}
else
{
return std::nullopt;
}
return key;
}
// ------------------------------------------------------------------------
// Binding Enumeration
// ------------------------------------------------------------------------
std::vector<const HotkeyInfo*> InputManager::GetHotkeyList()
{
std::vector<const HotkeyInfo*> ret;
for (const HotkeyInfo* hotkey_list : s_hotkey_list)
{
for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
ret.push_back(hotkey);
}
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<std::string> 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<std::string> bind_names = PAD::GetControllerBinds(type);
if (!bind_names.empty())
{
for (u32 bind_index = 0; bind_index < static_cast<u32>(bind_names.size()); bind_index++)
{
const std::string& bind_name = bind_names[bind_index];
const std::vector<std::string> 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<u8>(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<std::mutex> lock(m_event_intercept_mutex);
pxAssert(!m_event_intercept_callback);
m_event_intercept_callback = std::move(callback);
}
void InputManager::RemoveHook()
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
if (m_event_intercept_callback)
m_event_intercept_callback = {};
}
bool InputManager::HasHook()
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
return (bool)m_event_intercept_callback;
}
bool InputManager::DoEventHook(InputBindingKey key, float value)
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
if (!m_event_intercept_callback)
return false;
const InputInterceptHook::CallbackResult action = m_event_intercept_callback(key, value);
if (action == InputInterceptHook::CallbackResult::StopMonitoring)
m_event_intercept_callback = {};
return true;
}
// ------------------------------------------------------------------------
// Binding Updater
// ------------------------------------------------------------------------
// TODO(Stenzek): Find a better place for this. Maybe in PAD?
static constexpr std::array<const char*, InputManager::MAX_PAD_NUMBER> 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 <typename T>
static void UpdateInputSourceState(SettingsInterface& si, InputSourceType type, bool default_state)
{
const bool old_state = (s_input_sources[static_cast<u32>(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<InputSource> source = std::make_unique<T>();
if (!source->Initialize(si))
{
Console.Error("(InputManager) Source '%s' failed to initialize.", InputManager::InputSourceToString(type));
return;
}
s_input_sources[static_cast<u32>(type)] = std::move(source);
}
else
{
s_input_sources[static_cast<u32>(type)]->Shutdown();
s_input_sources[static_cast<u32>(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<XInputSource>(si, InputSourceType::XInput, false);
#endif
#ifdef SDL_BUILD
UpdateInputSourceState<SDLInputSource>(si, InputSourceType::SDL, true);
#endif
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <optional>
#include <variant>
#include <utility>
#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<u64>{}(k.bits); }
};
/// Callback type for a binary event. Usually used for hotkeys.
using InputButtonEventHandler = std::function<void(bool value)>;
/// Callback types for a normalized event. Usually used for pads.
using InputAxisEventHandler = std::function<void(float value)>;
/// Input monitoring for external access.
struct InputInterceptHook
{
enum class CallbackResult
{
StopMonitoring,
ContinueMonitoring
};
using Callback = std::function<CallbackResult(InputBindingKey key, float value)>;
};
/// 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<InputSourceType> ParseInputSourceString(const std::string_view& str);
/// Converts a key code from a human-readable string to an identifier.
std::optional<u32> ConvertHostKeyboardStringToCode(const std::string_view& str);
/// Converts a key code from an identifier to a human-readable string.
std::optional<std::string> 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<InputBindingKey> 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<const HotkeyInfo*> 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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<u32>(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<u32>(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<u32>(motor_index);
return key;
}
std::optional<InputBindingKey> 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<s32> source_index = StringUtil::FromChars<s32>(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<s32> axis_number = StringUtil::FromChars<s32>(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<u32>(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<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
if (!button_number.has_value() || button_number.value() < 0)
return std::nullopt;
key.source_subtype = InputSubclass::ControllerButton;
key.data = static_cast<u32>(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 {};
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string_view>
#include <vector>
#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<InputBindingKey> 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<InputBindingKey> 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<InputBindingKey> 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);
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <cmath>
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<std::vector<u8>> 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<int>(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<InputBindingKey> 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<s32> player_id = StringUtil::FromChars<s32>(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<u32>(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<SDL_Haptic*>(it->haptic));
SDL_GameControllerClose(static_cast<SDL_GameController*>(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<float>(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<InputBindingKey> SDLInputSource::EnumerateMotors()
{
std::vector<InputBindingKey> 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<u16>(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<u16>(large_intensity * 65535.0f);
it->rumble_intensity[small_key.data] = static_cast<u16>(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<u32>(cd->rumble_intensity[0]) + static_cast<u32>(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<float>(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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "Frontend/InputSource.h"
#include "SDL.h"
#include <array>
#include <functional>
#include <mutex>
#include <vector>
class SettingsInterface;
class SDLInputSource final : public InputSource
{
public:
SDLInputSource();
~SDLInputSource();
bool Initialize(SettingsInterface& si) override;
void Shutdown() override;
void PollEvents() override;
std::vector<InputBindingKey> 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<InputBindingKey> 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<ControllerData>;
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;
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <cmath>
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<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, reinterpret_cast<LPCSTR>(100)));
if (!m_xinput_get_state)
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, "XInputGetState"));
m_xinput_set_state =
reinterpret_cast<decltype(m_xinput_set_state)>(GetProcAddress(m_xinput_module, "XInputSetState"));
m_xinput_get_capabilities =
reinterpret_cast<decltype(m_xinput_get_capabilities)>(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<InputBindingKey> 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<s32> player_id = StringUtil::FromChars<s32>(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<u32>(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<InputBindingKey> XInputSource::EnumerateMotors()
{
std::vector<InputBindingKey> 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<float>(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<u16>(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<u16>(large_intensity * 65535.0f);
cd.last_vibration.wRightMotorSpeed = static_cast<u16>(small_intensity * 65535.0f);
m_xinput_set_state(large_key.source_index, &cd.last_vibration);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "Frontend/InputSource.h"
#include <Xinput.h>
#include <array>
#include <functional>
#include <mutex>
#include <vector>
class SettingsInterface;
class XInputSource final : public InputSource
{
public:
XInputSource();
~XInputSource();
bool Initialize(SettingsInterface& si) override;
void Shutdown() override;
void PollEvents() override;
std::vector<InputBindingKey> 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<InputBindingKey> 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<ControllerData, NUM_CONTROLLERS>;
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];
};

View File

@ -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);

View File

@ -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<std::string> PAD::GetControllerTypeNames()
{
return {"DualShock2"};
@ -211,6 +220,18 @@ std::vector<std::string> 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)

View File

@ -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<std::string> GetControllerTypeNames();
/// Returns the list of binds for the specified controller type.
std::vector<std::string> 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

View File

@ -17,6 +17,7 @@
#include "PAD/Host/StateManagement.h"
#include "PAD/Host/KeyStatus.h"
#include "Frontend/InputManager.h"
template <class T>
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<float>(currentVibrate[0]) * g_key_status.GetVibrationScale(port, 0) * (1.0f / 255.0f), 1.0f),
std::min(static_cast<float>(currentVibrate[1]) * g_key_status.GetVibrationScale(port, 1) * (1.0f / 255.0f), 1.0f)
);
}
void Pad::stop_vibrate_all()