InputManager: Support inverted full axis

i.e. pedals
This commit is contained in:
Connor McLaughlin
2023-01-15 14:00:51 +10:00
parent 01270bac35
commit 395e9a934b
39 changed files with 1022 additions and 366 deletions

View File

@ -56,7 +56,7 @@ std::string DInputSource::GetDeviceIdentifier(u32 index)
}
static constexpr std::array<const char*, DInputSource::NUM_HAT_DIRECTIONS> s_hat_directions = {
{"Up", "Right", "Down", "Left"}};
{"Up", "Down", "Left", "Right"}};
bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
{
@ -88,14 +88,15 @@ bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex
// need to release the lock while we're enumerating, because we call winId().
settings_lock.unlock();
const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo());
if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32)
{
Log_ErrorPrintf("Missing top level window, cannot add DInput devices.");
return false;
}
const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo());
settings_lock.lock();
if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32)
{
Log_ErrorPrintf("Missing top level window, cannot add DInput devices.");
return false;
}
m_toplevel_window = static_cast<HWND>(toplevel_wi->window_handle);
ReloadDevices();
return true;
@ -106,15 +107,6 @@ void DInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::m
// noop
}
void DInputSource::Shutdown()
{
while (!m_controllers.empty())
{
Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1)));
m_controllers.pop_back();
}
}
static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef)
{
static_cast<std::vector<DIDEVICEINSTANCEW>*>(pvRef)->push_back(*lpddi);
@ -126,6 +118,7 @@ bool DInputSource::ReloadDevices()
// detect any removals
PollEvents();
// look for new devices
std::vector<DIDEVICEINSTANCEW> devices;
m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY);
@ -156,7 +149,7 @@ bool DInputSource::ReloadDevices()
{
const u32 index = static_cast<u32>(m_controllers.size());
m_controllers.push_back(std::move(cd));
Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name);
InputManager::OnInputDeviceConnected(GetDeviceIdentifier(index), name);
changed = true;
}
}
@ -164,6 +157,15 @@ bool DInputSource::ReloadDevices()
return changed;
}
void DInputSource::Shutdown()
{
while (!m_controllers.empty())
{
InputManager::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1)));
m_controllers.pop_back();
}
}
bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
{
HRESULT hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE);
@ -204,11 +206,11 @@ bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
cd.num_buttons = caps.dwButtons;
static constexpr auto axis_offsets =
make_array(offsetof(DIJOYSTATE, lX), offsetof(DIJOYSTATE, lY), offsetof(DIJOYSTATE, lZ), offsetof(DIJOYSTATE, lRz),
offsetof(DIJOYSTATE, lRx), offsetof(DIJOYSTATE, lRy), offsetof(DIJOYSTATE, rglSlider[0]),
offsetof(DIJOYSTATE, rglSlider[1]));
for (const auto offset : axis_offsets)
static constexpr const u32 axis_offsets[] = {offsetof(DIJOYSTATE, lX), offsetof(DIJOYSTATE, lY),
offsetof(DIJOYSTATE, lZ), offsetof(DIJOYSTATE, lRz),
offsetof(DIJOYSTATE, lRx), offsetof(DIJOYSTATE, lRy),
offsetof(DIJOYSTATE, rglSlider[0]), offsetof(DIJOYSTATE, rglSlider[1])};
for (const u32 offset : axis_offsets)
{
// ask for 16 bits of axis range
DIPROPRANGE range = {};
@ -222,9 +224,7 @@ bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
// did it apply?
if (SUCCEEDED(cd.device->GetProperty(DIPROP_RANGE, &range.diph)))
{
cd.axis_offsets.push_back(static_cast<u32>(offset));
}
cd.axis_offsets.push_back(offset);
}
cd.num_hats = caps.dwPOVs;
@ -263,7 +263,7 @@ void DInputSource::PollEvents()
if (hr != DI_OK)
{
Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(i)));
InputManager::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(i)));
m_controllers.erase(m_controllers.begin() + i);
continue;
}
@ -336,13 +336,28 @@ std::optional<InputBindingKey> DInputSource::ParseKeyString(const std::string_vi
if (StringUtil::StartsWith(binding, "+Axis") || StringUtil::StartsWith(binding, "-Axis"))
{
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5));
std::string_view end;
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5), 10, &end);
if (!axis_index.has_value())
return std::nullopt;
key.source_subtype = InputSubclass::ControllerAxis;
key.data = axis_index.value();
key.negative = (binding[0] == '-');
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
key.invert = (end == "~");
return key;
}
else if (StringUtil::StartsWith(binding, "FullAxis"))
{
std::string_view end;
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(8), 10, &end);
if (!axis_index.has_value())
return std::nullopt;
key.source_subtype = InputSubclass::ControllerAxis;
key.data = axis_index.value();
key.modifier = InputModifier::FullAxis;
key.invert = (end == "~");
return key;
}
else if (StringUtil::StartsWith(binding, "Hat"))
@ -388,7 +403,9 @@ std::string DInputSource::ConvertKeyToString(InputBindingKey key)
{
if (key.source_subtype == InputSubclass::ControllerAxis)
{
ret = fmt::format("DInput-{}/{}Axis{}", u32(key.source_index), key.negative ? '-' : '+', u32(key.data));
const char* modifier =
(key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
ret = fmt::format("DInput-{}/{}Axis{}{}", u32(key.source_index), modifier, u32(key.data), key.invert ? "~" : "");
}
else if (key.source_subtype == InputSubclass::ControllerButton && key.data >= MAX_NUM_BUTTONS)
{

View File

@ -4,8 +4,8 @@
#pragma once
#define DIRECTINPUT_VERSION 0x0800
#include "common/windows_headers.h"
#include "input_source.h"
#include "core/types.h"
#include "input_source.h"
#include <array>
#include <dinput.h>
#include <functional>
@ -80,7 +80,7 @@ private:
ControllerDataArray m_controllers;
HMODULE m_dinput_module{};
LPCDIDATAFORMAT m_joystick_data_format{};
ComPtr<IDirectInput8W> m_dinput;
LPCDIDATAFORMAT m_joystick_data_format{};
HWND m_toplevel_window = NULL;
};

View File

@ -44,6 +44,8 @@
#include <bitset>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
Log_SetChannel(FullscreenUI);
#ifdef WITH_CHEEVOS
@ -362,11 +364,10 @@ static void PopulateGraphicsAdapterList();
static void PopulateGameListDirectoryCache(SettingsInterface* si);
static void PopulatePostProcessingChain();
static void SavePostProcessingChain();
static void BeginInputBinding(SettingsInterface* bsi, Controller::ControllerBindingType type,
const std::string_view& section, const std::string_view& key,
const std::string_view& display_name);
static void BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const std::string_view& section,
const std::string_view& key, const std::string_view& display_name);
static void DrawInputBindingWindow();
static void DrawInputBindingButton(SettingsInterface* bsi, Controller::ControllerBindingType type, const char* section,
static void DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section,
const char* name, const char* display_name, bool show_type = true);
static void ClearInputBindingVariables();
static void StartAutomaticBinding(u32 port);
@ -381,11 +382,12 @@ static FrontendCommon::PostProcessingChain s_postprocessing_chain;
static std::vector<const HotkeyInfo*> s_hotkey_list_cache;
static std::atomic_bool s_settings_changed{false};
static std::atomic_bool s_game_settings_changed{false};
static Controller::ControllerBindingType s_input_binding_type = Controller::ControllerBindingType::Unknown;
static InputBindingInfo::Type s_input_binding_type = InputBindingInfo::Type::Unknown;
static std::string s_input_binding_section;
static std::string s_input_binding_key;
static std::string s_input_binding_display_name;
static std::vector<InputBindingKey> s_input_binding_new_bindings;
static std::vector<std::pair<InputBindingKey, std::pair<float, float>>> s_input_binding_value_ranges;
static Common::Timer s_input_binding_timer;
//////////////////////////////////////////////////////////////////////////
@ -788,7 +790,7 @@ void FullscreenUI::Render()
if (s_about_window_open)
DrawAboutWindow();
if (s_input_binding_type != Controller::ControllerBindingType::Unknown)
if (s_input_binding_type != InputBindingInfo::Type::Unknown)
DrawInputBindingWindow();
ImGuiFullscreen::EndLayout();
@ -1277,9 +1279,8 @@ std::string FullscreenUI::GetEffectiveStringSetting(SettingsInterface* bsi, cons
return ret;
}
void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::ControllerBindingType type,
const char* section, const char* name, const char* display_name,
bool show_type)
void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section,
const char* name, const char* display_name, bool show_type)
{
TinyString title;
title.Fmt("{}/{}", section, name);
@ -1299,17 +1300,17 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::Co
{
switch (type)
{
case Controller::ControllerBindingType::Button:
case InputBindingInfo::Type::Button:
title = fmt::format(ICON_FA_DOT_CIRCLE " {}", display_name);
break;
case Controller::ControllerBindingType::Axis:
case Controller::ControllerBindingType::HalfAxis:
case InputBindingInfo::Type::Axis:
case InputBindingInfo::Type::HalfAxis:
title = fmt::format(ICON_FA_BULLSEYE " {}", display_name);
break;
case Controller::ControllerBindingType::Motor:
case InputBindingInfo::Type::Motor:
title = fmt::format(ICON_FA_BELL " {}", display_name);
break;
case Controller::ControllerBindingType::Macro:
case InputBindingInfo::Type::Macro:
title = fmt::format(ICON_FA_PIZZA_SLICE " {}", display_name);
break;
default:
@ -1343,18 +1344,19 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::Co
void FullscreenUI::ClearInputBindingVariables()
{
s_input_binding_type = Controller::ControllerBindingType::Unknown;
s_input_binding_type = InputBindingInfo::Type::Unknown;
s_input_binding_section = {};
s_input_binding_key = {};
s_input_binding_display_name = {};
s_input_binding_new_bindings = {};
s_input_binding_value_ranges = {};
}
void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::ControllerBindingType type,
void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type,
const std::string_view& section, const std::string_view& key,
const std::string_view& display_name)
{
if (s_input_binding_type != Controller::ControllerBindingType::Unknown)
if (s_input_binding_type != InputBindingInfo::Type::Unknown)
{
InputManager::RemoveHook();
ClearInputBindingVariables();
@ -1365,25 +1367,50 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control
s_input_binding_key = key;
s_input_binding_display_name = display_name;
s_input_binding_new_bindings = {};
s_input_binding_value_ranges = {};
s_input_binding_timer.Reset();
InputManager::SetHook([game_settings = IsEditingGameSettings(bsi)](
InputBindingKey key, float value) -> InputInterceptHook::CallbackResult {
const bool game_settings = IsEditingGameSettings(bsi);
InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult {
if (s_input_binding_type == InputBindingInfo::Type::Unknown)
return InputInterceptHook::CallbackResult::StopProcessingEvent;
// holding the settings lock here will protect the input binding list
auto lock = Host::GetSettingsLock();
const float abs_value = std::abs(value);
for (InputBindingKey other_key : s_input_binding_new_bindings)
float initial_value = value;
float min_value = value;
auto it = std::find_if(s_input_binding_value_ranges.begin(), s_input_binding_value_ranges.end(),
[key](const auto& it) { return it.first.bits == key.bits; });
if (it != s_input_binding_value_ranges.end())
{
initial_value = it->second.first;
min_value = it->second.second = std::min(it->second.second, value);
}
else
{
s_input_binding_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
}
const float abs_value = std::abs(value);
const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
for (InputBindingKey& other_key : s_input_binding_new_bindings)
{
// if this key is in our new binding list, it's a "release", and we're done
if (other_key.MaskDirection() == key.MaskDirection())
{
if (abs_value < 0.5f)
// for pedals, we wait for it to go back to near its starting point to commit the binding
if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
{
// if this key is in our new binding list, it's a "release", and we're done
// did we go the full range?
if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
other_key.modifier = InputModifier::FullAxis;
SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
const std::string new_binding(InputManager::ConvertInputBindingKeysToString(
s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size()));
s_input_binding_type, s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size()));
bsi->SetStringValue(s_input_binding_section.c_str(), s_input_binding_key.c_str(), new_binding.c_str());
SetSettingsChanged(bsi);
ClearInputBindingVariables();
@ -1396,10 +1423,11 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control
}
// new binding, add it to the list, but wait for a decent distance first, and then wait for release
if (abs_value >= 0.5f)
if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
{
InputBindingKey key_to_add = key;
key_to_add.negative = (value < 0.0f);
key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
key_to_add.invert = reverse_threshold;
s_input_binding_new_bindings.push_back(key_to_add);
}
@ -1409,7 +1437,7 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control
void FullscreenUI::DrawInputBindingWindow()
{
DebugAssert(s_input_binding_type != Controller::ControllerBindingType::Unknown);
DebugAssert(s_input_binding_type != InputBindingInfo::Type::Unknown);
const double time_remaining = INPUT_BINDING_TIMEOUT_SECONDS - s_input_binding_timer.GetTimeSeconds();
if (time_remaining <= 0.0)
@ -3180,7 +3208,7 @@ void FullscreenUI::DrawControllerSettingsPage()
for (u32 macro_index = 0; macro_index < InputManager::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_index++)
{
DrawInputBindingButton(bsi, Controller::ControllerBindingType::Macro, section.c_str(),
DrawInputBindingButton(bsi, InputBindingInfo::Type::Macro, section.c_str(),
fmt::format("Macro{}", macro_index + 1).c_str(),
fmt::format("Macro {} Trigger", macro_index + 1).c_str());
@ -3194,9 +3222,8 @@ void FullscreenUI::DrawControllerSettingsPage()
for (u32 i = 0; i < ci->num_bindings; i++)
{
const Controller::ControllerBindingInfo& bi = ci->bindings[i];
if (bi.type != Controller::ControllerBindingType::Button &&
bi.type != Controller::ControllerBindingType::Axis &&
bi.type != Controller::ControllerBindingType::HalfAxis)
if (bi.type != InputBindingInfo::Type::Button && bi.type != InputBindingInfo::Type::Axis &&
bi.type != InputBindingInfo::Type::HalfAxis)
{
continue;
}
@ -3350,8 +3377,7 @@ void FullscreenUI::DrawHotkeySettingsPage()
last_category = hotkey;
}
DrawInputBindingButton(bsi, Controller::ControllerBindingType::Button, "Hotkeys", hotkey->name,
hotkey->display_name, false);
DrawInputBindingButton(bsi, InputBindingInfo::Type::Button, "Hotkeys", hotkey->name, hotkey->display_name, false);
}
EndMenuButtons();
@ -5831,9 +5857,12 @@ void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
{
ImGuiFullscreen::ChoiceDialogOptions options = {
{ICON_FA_WRENCH " Game Properties", false}, {ICON_FA_PLAY " Resume Game", false},
{ICON_FA_UNDO " Load State", false}, {ICON_FA_COMPACT_DISC " Default Boot", false},
{ICON_FA_LIGHTBULB " Fast Boot", false}, {ICON_FA_MAGIC " Slow Boot", false},
{ICON_FA_WRENCH " Game Properties", false},
{ICON_FA_PLAY " Resume Game", false},
{ICON_FA_UNDO " Load State", false},
{ICON_FA_COMPACT_DISC " Default Boot", false},
{ICON_FA_LIGHTBULB " Fast Boot", false},
{ICON_FA_MAGIC " Slow Boot", false},
{ICON_FA_FOLDER_MINUS " Reset Play Time", false},
{ICON_FA_WINDOW_CLOSE " Close Menu", false},
};

View File

@ -523,8 +523,8 @@ void ImGuiManager::DrawInputsOverlay()
const Controller::ControllerBindingInfo& bi = cinfo->bindings[bind];
switch (bi.type)
{
case Controller::ControllerBindingType::Axis:
case Controller::ControllerBindingType::HalfAxis:
case InputBindingInfo::Type::Axis:
case InputBindingInfo::Type::HalfAxis:
{
// axes are always shown
const float value = controller->GetBindState(bi.bind_index);
@ -535,7 +535,7 @@ void ImGuiManager::DrawInputsOverlay()
}
break;
case Controller::ControllerBindingType::Button:
case InputBindingInfo::Type::Button:
{
// buttons only shown when active
const float value = controller->GetBindState(bi.bind_index);
@ -544,9 +544,10 @@ void ImGuiManager::DrawInputsOverlay()
}
break;
case Controller::ControllerBindingType::Motor:
case Controller::ControllerBindingType::Macro:
case Controller::ControllerBindingType::Unknown:
case InputBindingInfo::Type::Motor:
case InputBindingInfo::Type::Macro:
case InputBindingInfo::Type::Unknown:
case InputBindingInfo::Type::Pointer:
default:
break;
}

View File

@ -281,48 +281,72 @@ bool InputManager::ParseBindingAndGetSource(const std::string_view& binding, Inp
return false;
}
std::string InputManager::ConvertInputBindingKeyToString(InputBindingKey key)
std::string InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key)
{
if (key.source_type == InputSourceType::Keyboard)
if (binding_type == InputBindingInfo::Type::Pointer)
{
const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data));
if (str.has_value() && !str->empty())
return fmt::format("Keyboard/{}", str->c_str());
}
else if (key.source_type == InputSourceType::Pointer)
{
if (key.source_subtype == InputSubclass::PointerButton)
// pointer and device bindings don't have a data part
if (key.source_type == InputSourceType::Pointer)
{
if (key.data < s_pointer_button_names.size())
return fmt::format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_names[key.data]);
else
return fmt::format("Pointer-{}/Button{}", u32{key.source_index}, key.data);
return GetPointerDeviceName(key.data);
}
else if (key.source_subtype == InputSubclass::PointerAxis)
else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
{
return fmt::format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data],
key.negative ? '-' : '+');
// This assumes that it always follows the Type/Binding form.
std::string keystr(s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key));
std::string::size_type pos = keystr.find('/');
if (pos != std::string::npos)
keystr.erase(pos);
return keystr;
}
}
else if (key.source_type == InputSourceType::Sensor)
else
{
if (key.source_subtype == InputSubclass::SensorAccelerometer && key.data < s_sensor_accelerometer_names.size())
return fmt::format("Sensor/{}", s_sensor_accelerometer_names[key.data]);
}
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);
if (key.source_type == InputSourceType::Keyboard)
{
const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data));
if (str.has_value() && !str->empty())
return fmt::format("Keyboard/{}", str->c_str());
}
else if (key.source_type == InputSourceType::Pointer)
{
if (key.source_subtype == InputSubclass::PointerButton)
{
if (key.data < s_pointer_button_names.size())
return fmt::format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_names[key.data]);
else
return fmt::format("Pointer-{}/Button{}", u32{key.source_index}, key.data);
}
else if (key.source_subtype == InputSubclass::PointerAxis)
{
return fmt::format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data],
key.modifier == InputModifier::Negate ? '-' : '+');
}
}
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::string InputManager::ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type,
const InputBindingKey* keys, size_t num_keys)
{
// can't have a chord of devices/pointers
if (binding_type == InputBindingInfo::Type::Pointer)
{
// so only take the first
if (num_keys > 0)
return ConvertInputBindingKeyToString(binding_type, keys[0]);
}
std::stringstream ss;
for (size_t i = 0; i < num_keys; i++)
{
const std::string keystr(ConvertInputBindingKeyToString(keys[i]));
const std::string keystr(ConvertInputBindingKeyToString(binding_type, keys[i]));
if (keystr.empty())
return std::string();
@ -574,9 +598,9 @@ std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_v
const std::string_view dir_part(sub_binding.substr(std::strlen(s_pointer_axis_names[i])));
if (dir_part == "+")
key.negative = false;
key.modifier = InputModifier::None;
else if (dir_part == "-")
key.negative = true;
key.modifier = InputModifier::Negate;
else
return std::nullopt;
@ -597,6 +621,23 @@ std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_v
return std::nullopt;
}
std::optional<u32> InputManager::GetIndexFromPointerBinding(const std::string_view& source)
{
if (!StringUtil::StartsWith(source, "Pointer-"))
return std::nullopt;
const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(source.substr(8));
if (!pointer_index.has_value() || pointer_index.value() < 0)
return std::nullopt;
return static_cast<u32>(pointer_index.value());
}
std::string InputManager::GetPointerDeviceName(u32 pointer_index)
{
return fmt::format("Pointer-{}", pointer_index);
}
std::optional<InputBindingKey> InputManager::ParseSensorKey(const std::string_view& source,
const std::string_view& sub_binding)
{
@ -616,9 +657,9 @@ std::optional<InputBindingKey> InputManager::ParseSensorKey(const std::string_vi
const std::string_view dir_part(sub_binding.substr(std::strlen(s_sensor_accelerometer_names[i])));
if (dir_part == "+")
key.negative = false;
key.modifier = InputModifier::None;
else if (dir_part == "-")
key.negative = true;
key.modifier = InputModifier::Negate;
else
return std::nullopt;
@ -777,7 +818,6 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBi
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++)
{
@ -785,11 +825,26 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBi
continue;
const u8 bit = static_cast<u8>(1) << i;
const bool negative = binding->keys[i].negative;
const bool negative = binding->keys[i].modifier == InputModifier::Negate;
const bool new_state = (negative ? (value < 0.0f) : (value > 0.0f));
float value_to_pass = 0.0f;
switch (binding->keys[i].modifier)
{
case InputModifier::None:
if (value > 0.0f)
value_to_pass = value;
break;
case InputModifier::Negate:
if (value < 0.0f)
value_to_pass = -value;
break;
case InputModifier::FullAxis:
value_to_pass = value * 0.5f + 0.5f;
break;
}
// 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);
// handle inverting, needed for some wheels.
value_to_pass = binding->keys[i].invert ? (1.0f - value_to_pass) : value_to_pass;
// axes are fired regardless of a state change, unless they're zero
// (but going from not-zero to zero will still fire, because of the full state)
@ -1130,6 +1185,16 @@ std::vector<std::string> InputManager::GetInputProfileNames()
return ret;
}
void InputManager::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name)
{
Host::OnInputDeviceConnected(identifier, device_name);
}
void InputManager::OnInputDeviceDisconnected(const std::string_view& identifier)
{
Host::OnInputDeviceDisconnected(identifier);
}
// ------------------------------------------------------------------------
// Vibration
// ------------------------------------------------------------------------

View File

@ -14,6 +14,8 @@
#include "common/types.h"
#include "common/window_info.h"
#include "core/input_types.h"
/// Class, or source of an input event.
enum class InputSourceType : u32
{
@ -47,12 +49,20 @@ enum class InputSubclass : u32
ControllerButton = 0,
ControllerAxis = 1,
ControllerMotor = 2,
ControllerHaptic = 3,
ControllerHat = 2,
ControllerMotor = 3,
ControllerHaptic = 4,
SensorAccelerometer = 0,
};
enum class InputModifier : u32
{
None = 0,
Negate, ///< Input * -1, gets the negative side of the axis
FullAxis, ///< (Input * 0.5) + 0.5, uses both the negative and positive side of the axis together
};
/// A composite type representing a full input key which is part of an event.
union InputBindingKey
{
@ -60,9 +70,10 @@ union InputBindingKey
{
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;
InputSubclass source_subtype : 3; ///< if 1, binding is for an axis and not a button (used for controllers)
InputModifier modifier : 2;
u32 invert : 1; ///< if 1, value is inverted prior to being sent to the sink
u32 unused : 14;
u32 data;
};
@ -77,7 +88,8 @@ union InputBindingKey
{
InputBindingKey r;
r.bits = bits;
r.negative = false;
r.modifier = InputModifier::None;
r.invert = 0;
return r;
}
};
@ -181,6 +193,12 @@ bool GetInputSourceDefaultEnabled(InputSourceType type);
/// Parses an input class string.
std::optional<InputSourceType> ParseInputSourceString(const std::string_view& str);
/// Parses a pointer device string, i.e. tells you which pointer is specified.
std::optional<u32> GetIndexFromPointerBinding(const std::string_view& str);
/// Returns the device name for a pointer index (e.g. Pointer-0).
std::string GetPointerDeviceName(u32 pointer_index);
/// Converts a key code from a human-readable string to an identifier.
std::optional<u32> ConvertHostKeyboardStringToCode(const std::string_view& str);
@ -204,10 +222,11 @@ InputBindingKey MakeSensorAxisKey(InputSubclass sensor, u32 axis);
std::optional<InputBindingKey> ParseInputBindingKey(const std::string_view& binding);
/// Converts a input key to a string.
std::string ConvertInputBindingKeyToString(InputBindingKey key);
std::string ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key);
/// Converts a chord of binding keys to a string.
std::string ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys);
std::string ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type, const InputBindingKey* keys,
size_t num_keys);
/// Returns a list of all hotkeys.
std::vector<const HotkeyInfo*> GetHotkeyList();
@ -263,7 +282,7 @@ void AddVibrationBinding(u32 pad_index, const InputBindingKey* motor_0_binding,
/// 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, GenericInputBinding generic_key);
bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key = GenericInputBinding::Unknown);
/// 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.
@ -315,6 +334,12 @@ bool MapController(SettingsInterface& si, u32 controller,
/// Returns a list of input profiles available.
std::vector<std::string> GetInputProfileNames();
/// Called when a new input device is connected.
void OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name);
/// Called when an input device is disconnected.
void OnInputDeviceDisconnected(const std::string_view& identifier);
} // namespace InputManager
namespace Host {

View File

@ -38,6 +38,17 @@ InputBindingKey InputSource::MakeGenericControllerButtonKey(InputSourceType claz
return key;
}
InputBindingKey InputSource::MakeGenericControllerHatKey(InputSourceType clazz, u32 controller_index, s32 hat_index,
u8 hat_direction, u32 num_directions)
{
InputBindingKey key = {};
key.source_type = clazz;
key.source_index = controller_index;
key.source_subtype = InputSubclass::ControllerHat;
key.data = static_cast<u32>(hat_index) * num_directions + hat_direction;
return key;
}
InputBindingKey InputSource::MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index)
{
InputBindingKey key = {};
@ -81,12 +92,21 @@ std::optional<InputBindingKey> InputSource::ParseGenericControllerKey(InputSourc
key.data = static_cast<u32>(axis_number.value());
if (sub_binding[0] == '+')
key.negative = false;
key.modifier = InputModifier::None;
else if (sub_binding[0] == '-')
key.negative = true;
key.modifier = InputModifier::Negate;
else
return std::nullopt;
}
else if (StringUtil::StartsWith(sub_binding, "FullAxis"))
{
const std::optional<s32> axis_number = StringUtil::FromChars<s32>(sub_binding.substr(8));
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());
key.modifier = InputModifier::FullAxis;
}
else if (StringUtil::StartsWith(sub_binding, "Button"))
{
const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
@ -108,8 +128,21 @@ 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);
const char* modifier = "";
switch (key.modifier)
{
case InputModifier::None:
modifier = "+";
break;
case InputModifier::Negate:
modifier = "-";
break;
case InputModifier::FullAxis:
modifier = "Full";
break;
}
return StringUtil::StdStringFromFormat("%s-%u/%sAxis%u", InputManager::InputSourceToString(key.source_type),
key.source_index, modifier, key.data);
}
else if (key.source_subtype == InputSubclass::ControllerButton)
{
@ -120,4 +153,4 @@ std::string InputSource::ConvertGenericControllerKeyToString(InputBindingKey key
{
return {};
}
}
}

View File

@ -55,6 +55,10 @@ public:
/// 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 hat event.
static InputBindingKey MakeGenericControllerHatKey(InputSourceType clazz, u32 controller_index, s32 hat_index,
u8 hat_direction, u32 num_directions);
/// Creates a key for a generic controller motor event.
static InputBindingKey MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index);

View File

@ -14,7 +14,7 @@
#endif
Log_SetChannel(SDLInputSource);
static const char* s_sdl_axis_names[] = {
static constexpr const char* s_sdl_axis_names[] = {
"LeftX", // SDL_CONTROLLER_AXIS_LEFTX
"LeftY", // SDL_CONTROLLER_AXIS_LEFTY
"RightX", // SDL_CONTROLLER_AXIS_RIGHTX
@ -22,7 +22,7 @@ static const char* s_sdl_axis_names[] = {
"LeftTrigger", // SDL_CONTROLLER_AXIS_TRIGGERLEFT
"RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
};
static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
static constexpr const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
{GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // SDL_CONTROLLER_AXIS_LEFTX
{GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // SDL_CONTROLLER_AXIS_LEFTY
{GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_CONTROLLER_AXIS_RIGHTX
@ -31,7 +31,7 @@ static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
{GenericInputBinding::Unknown, GenericInputBinding::R2}, // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
};
static const char* s_sdl_button_names[] = {
static constexpr const char* s_sdl_button_names[] = {
"A", // SDL_CONTROLLER_BUTTON_A
"B", // SDL_CONTROLLER_BUTTON_B
"X", // SDL_CONTROLLER_BUTTON_X
@ -54,7 +54,7 @@ static const char* s_sdl_button_names[] = {
"Paddle4", // SDL_CONTROLLER_BUTTON_PADDLE4
"Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD
};
static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
static constexpr const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
GenericInputBinding::Cross, // SDL_CONTROLLER_BUTTON_A
GenericInputBinding::Circle, // SDL_CONTROLLER_BUTTON_B
GenericInputBinding::Square, // SDL_CONTROLLER_BUTTON_X
@ -78,11 +78,20 @@ static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_TOUCHPAD
};
static constexpr const char* s_sdl_hat_direction_names[] = {
// clang-format off
"North",
"East",
"South",
"West",
// clang-format on
};
SDLInputSource::SDLInputSource() = default;
SDLInputSource::~SDLInputSource()
{
DebugAssert(m_controllers.empty());
Assert(m_controllers.empty());
}
bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
@ -123,6 +132,13 @@ void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std:
}
}
bool SDLInputSource::ReloadDevices()
{
// We'll get a GC added/removed event here.
PollEvents();
return false;
}
void SDLInputSource::Shutdown()
{
ShutdownSubsystem();
@ -131,36 +147,32 @@ void SDLInputSource::Shutdown()
void SDLInputSource::LoadSettings(SettingsInterface& si)
{
m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
}
bool SDLInputSource::ReloadDevices()
{
// We'll get a GC added/removed event here.
PollEvents();
return false;
m_sdl_hints = si.GetKeyValueList("SDLHints");
}
void SDLInputSource::SetHints()
{
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
// Enable Wii U Pro Controller support
// New as of SDL 2.26, so use string
SDL_SetHint("SDL_JOYSTICK_HIDAPI_WII", "1");
#ifndef _WIN32
// Gets us pressure sensitive button support on Linux
// Apparently doesn't work on Windows, so leave it off there
// New as of SDL 2.26, so use string
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS3", "1");
#endif
for (const std::pair<std::string, std::string>& hint : m_sdl_hints)
SDL_SetHint(hint.first.c_str(), hint.second.c_str());
}
bool SDLInputSource::InitializeSubsystem()
{
int result;
#ifdef __APPLE__
// On macOS, SDL_InitSubSystem runs a main-thread-only call to some GameController framework method
// So send this to be run on the main thread
dispatch_sync_f(dispatch_get_main_queue(), &result, [](void* ctx) {
*static_cast<int*>(ctx) = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
});
#else
result = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
#endif
if (result < 0)
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
{
Log_ErrorPrintf("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
Log_ErrorPrint("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
return false;
}
@ -172,16 +184,11 @@ bool SDLInputSource::InitializeSubsystem()
void SDLInputSource::ShutdownSubsystem()
{
while (!m_controllers.empty())
CloseGameController(m_controllers.begin()->joystick_id);
CloseDevice(m_controllers.begin()->joystick_id);
if (m_sdl_subsystem_initialized)
{
#ifdef __APPLE__
dispatch_sync_f(dispatch_get_main_queue(), nullptr,
[](void*) { SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); });
#else
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
#endif
m_sdl_subsystem_initialized = false;
}
}
@ -206,7 +213,7 @@ std::vector<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevice
{
std::string id(StringUtil::StdStringFromFormat("SDL-%d", cd.player_id));
const char* name = SDL_GameControllerName(cd.game_controller);
const char* name = cd.game_controller ? SDL_GameControllerName(cd.game_controller) : SDL_JoystickName(cd.joystick);
if (name)
ret.emplace_back(std::move(id), name);
else
@ -258,6 +265,19 @@ std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_
{
// likely an axis
const std::string_view axis_name(binding.substr(1));
if (StringUtil::StartsWith(axis_name, "Axis"))
{
std::string_view end;
if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4), 10, &end))
{
key.source_subtype = InputSubclass::ControllerAxis;
key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
key.invert = (end == "~");
return key;
}
}
for (u32 i = 0; i < std::size(s_sdl_axis_names); i++)
{
if (axis_name == s_sdl_axis_names[i])
@ -265,14 +285,51 @@ std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_
// found an axis!
key.source_subtype = InputSubclass::ControllerAxis;
key.data = i;
key.negative = (binding[0] == '-');
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
return key;
}
}
}
else if (StringUtil::StartsWith(binding, "FullAxis"))
{
std::string_view end;
if (auto value = StringUtil::FromChars<u32>(binding.substr(8), 10, &end))
{
key.source_subtype = InputSubclass::ControllerAxis;
key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
key.modifier = InputModifier::FullAxis;
key.invert = (end == "~");
return key;
}
}
else if (StringUtil::StartsWith(binding, "Hat"))
{
std::string_view hat_dir;
if (auto value = StringUtil::FromChars<u32>(binding.substr(3), 10, &hat_dir); value.has_value() && !hat_dir.empty())
{
for (u8 dir = 0; dir < static_cast<u8>(std::size(s_sdl_hat_direction_names)); dir++)
{
if (hat_dir == s_sdl_hat_direction_names[dir])
{
key.source_subtype = InputSubclass::ControllerHat;
key.data = value.value() * static_cast<u32>(std::size(s_sdl_hat_direction_names)) + dir;
return key;
}
}
}
}
else
{
// must be a button
if (StringUtil::StartsWith(binding, "Button"))
{
if (auto value = StringUtil::FromChars<u32>(binding.substr(6)))
{
key.source_subtype = InputSubclass::ControllerButton;
key.data = *value + static_cast<u32>(std::size(s_sdl_button_names));
return key;
}
}
for (u32 i = 0; i < std::size(s_sdl_button_names); i++)
{
if (binding == s_sdl_button_names[i])
@ -294,14 +351,39 @@ std::string SDLInputSource::ConvertKeyToString(InputBindingKey key)
if (key.source_type == InputSourceType::SDL)
{
if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_sdl_axis_names))
if (key.source_subtype == InputSubclass::ControllerAxis)
{
ret = StringUtil::StdStringFromFormat("SDL-%u/%c%s", key.source_index, key.negative ? '-' : '+',
s_sdl_axis_names[key.data]);
const char* modifier =
(key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
if (key.data < std::size(s_sdl_axis_names))
{
ret = StringUtil::StdStringFromFormat("SDL-%u/%s%s", key.source_index, modifier, s_sdl_axis_names[key.data]);
}
else
{
ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u%s", key.source_index, modifier,
key.data - static_cast<u32>(std::size(s_sdl_axis_names)),
key.invert ? "~" : "");
}
}
else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_sdl_button_names))
else if (key.source_subtype == InputSubclass::ControllerButton)
{
ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
if (key.data < std::size(s_sdl_button_names))
{
ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
}
else
{
ret = StringUtil::StdStringFromFormat("SDL-%u/Button%u", key.source_index,
key.data - static_cast<u32>(std::size(s_sdl_button_names)));
}
}
else if (key.source_subtype == InputSubclass::ControllerHat)
{
const u32 hat_index = key.data / static_cast<u32>(std::size(s_sdl_hat_direction_names));
const u32 hat_direction = key.data % static_cast<u32>(std::size(s_sdl_hat_direction_names));
ret = StringUtil::StdStringFromFormat("SDL-%u/Hat%u%s", key.source_index, hat_index,
s_sdl_hat_direction_names[hat_direction]);
}
else if (key.source_subtype == InputSubclass::ControllerMotor)
{
@ -323,14 +405,37 @@ bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
case SDL_CONTROLLERDEVICEADDED:
{
Log_InfoPrintf("(SDLInputSource) Controller %d inserted", event->cdevice.which);
OpenGameController(event->cdevice.which);
OpenDevice(event->cdevice.which, true);
return true;
}
case SDL_CONTROLLERDEVICEREMOVED:
{
Log_InfoPrintf("(SDLInputSource) Controller %d removed", event->cdevice.which);
CloseGameController(event->cdevice.which);
CloseDevice(event->cdevice.which);
return true;
}
case SDL_JOYDEVICEADDED:
{
// Let game controller handle.. well.. game controllers.
if (SDL_IsGameController(event->jdevice.which))
return false;
Log_InfoPrintf("(SDLInputSource) Joystick %d inserted", event->jdevice.which);
OpenDevice(event->cdevice.which, false);
return true;
}
break;
case SDL_JOYDEVICEREMOVED:
{
if (auto it = GetControllerDataForJoystickId(event->cdevice.which);
it != m_controllers.end() && it->game_controller)
return false;
Log_InfoPrintf("(SDLInputSource) Joystick %d removed", event->jdevice.which);
CloseDevice(event->cdevice.which);
return true;
}
@ -341,11 +446,37 @@ bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
case SDL_CONTROLLERBUTTONUP:
return HandleControllerButtonEvent(&event->cbutton);
case SDL_JOYAXISMOTION:
return HandleJoystickAxisEvent(&event->jaxis);
case SDL_JOYBUTTONDOWN:
case SDL_JOYBUTTONUP:
return HandleJoystickButtonEvent(&event->jbutton);
case SDL_JOYHATMOTION:
return HandleJoystickHatEvent(&event->jhat);
default:
return false;
}
}
SDL_Joystick* SDLInputSource::GetJoystickForDevice(const std::string_view& device)
{
if (!StringUtil::StartsWith(device, "SDL-"))
return nullptr;
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
if (!player_id.has_value() || player_id.value() < 0)
return nullptr;
auto it = GetControllerDataForPlayerId(player_id.value());
if (it == m_controllers.end())
return nullptr;
return it->joystick;
}
SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id)
{
return std::find_if(m_controllers.begin(), m_controllers.end(),
@ -375,11 +506,23 @@ int SDLInputSource::GetFreePlayerId() const
return 0;
}
bool SDLInputSource::OpenGameController(int index)
bool SDLInputSource::OpenDevice(int index, bool is_gamecontroller)
{
SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
if (!gcontroller || !joystick)
SDL_GameController* gcontroller;
SDL_Joystick* joystick;
if (is_gamecontroller)
{
gcontroller = SDL_GameControllerOpen(index);
joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
}
else
{
gcontroller = nullptr;
joystick = SDL_JoystickOpen(index);
}
if (!gcontroller && !joystick)
{
Log_ErrorPrintf("(SDLInputSource) Failed to open controller %d", index);
if (gcontroller)
@ -389,7 +532,7 @@ bool SDLInputSource::OpenGameController(int index)
}
const int joystick_id = SDL_JoystickInstanceID(joystick);
int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
int player_id = gcontroller ? SDL_GameControllerGetPlayerIndex(gcontroller) : SDL_JoystickGetPlayerIndex(joystick);
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
{
const int free_player_id = GetFreePlayerId();
@ -399,20 +542,49 @@ bool SDLInputSource::OpenGameController(int index)
player_id = free_player_id;
}
Log_InfoPrintf("(SDLInputSource) Opened controller %d (instance id %d, player id %d): %s", index, joystick_id,
player_id, SDL_GameControllerName(gcontroller));
const char* name = gcontroller ? SDL_GameControllerName(gcontroller) : SDL_JoystickName(joystick);
if (!name)
name = "Unknown Device";
Log_VerbosePrintf("(SDLInputSource) Opened %s %d (instance id %d, player id %d): %s",
is_gamecontroller ? "game controller" : "joystick", index, joystick_id, player_id, name);
ControllerData cd = {};
cd.player_id = player_id;
cd.joystick_id = joystick_id;
cd.haptic_left_right_effect = -1;
cd.game_controller = gcontroller;
cd.joystick = joystick;
cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
if (gcontroller)
{
const int num_axes = SDL_JoystickNumAxes(joystick);
const int num_buttons = SDL_JoystickNumButtons(joystick);
cd.joy_axis_used_in_gc.resize(num_axes, false);
cd.joy_button_used_in_gc.resize(num_buttons, false);
auto mark_bind = [&](SDL_GameControllerButtonBind bind) {
if (bind.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && bind.value.axis < num_axes)
cd.joy_axis_used_in_gc[bind.value.axis] = true;
if (bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON && bind.value.button < num_buttons)
cd.joy_button_used_in_gc[bind.value.button] = true;
};
for (size_t i = 0; i < std::size(s_sdl_axis_names); i++)
mark_bind(SDL_GameControllerGetBindForAxis(gcontroller, static_cast<SDL_GameControllerAxis>(i)));
for (size_t i = 0; i < std::size(s_sdl_button_names); i++)
mark_bind(SDL_GameControllerGetBindForButton(gcontroller, static_cast<SDL_GameControllerButton>(i)));
}
else
{
// GC doesn't have the concept of hats, so we only need to do this for joysticks.
const int num_hats = SDL_JoystickNumHats(joystick);
if (num_hats > 0)
cd.last_hat_state.resize(static_cast<size_t>(num_hats), u8(0));
}
cd.use_game_controller_rumble = (gcontroller && SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
if (cd.use_game_controller_rumble)
{
Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller",
SDL_GameControllerName(gcontroller));
Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller", name);
}
else
{
@ -431,7 +603,7 @@ bool SDLInputSource::OpenGameController(int index)
}
else
{
Log_WarningPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
Log_ErrorPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
{
cd.haptic = haptic;
@ -445,37 +617,43 @@ bool SDLInputSource::OpenGameController(int index)
}
if (cd.haptic)
Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", name);
}
if (!cd.haptic && !cd.use_game_controller_rumble)
Log_WarningPrintf("(SDLInputSource) Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));
Log_VerbosePrintf("(SDLInputSource) Rumble is not supported on '%s'", name);
m_controllers.push_back(std::move(cd));
const char* name = SDL_GameControllerName(cd.game_controller);
Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name ? name : "Unknown Device");
InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name);
return true;
}
bool SDLInputSource::CloseGameController(int joystick_index)
bool SDLInputSource::CloseDevice(int joystick_index)
{
auto it = GetControllerDataForJoystickId(joystick_index);
if (it == m_controllers.end())
return false;
InputManager::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", it->player_id));
if (it->haptic)
SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
SDL_HapticClose(it->haptic);
SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller));
if (it->game_controller)
SDL_GameControllerClose(it->game_controller);
else
SDL_JoystickClose(it->joystick);
const int player_id = it->player_id;
m_controllers.erase(it);
Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", player_id));
return true;
}
static float NormalizeS16(s16 value)
{
return static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
}
bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
{
auto it = GetControllerDataForJoystickId(ev->which);
@ -483,8 +661,8 @@ bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev
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, GenericInputBinding::Unknown);
InputManager::InvokeEvents(key, NormalizeS16(ev->value));
return true;
}
bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
@ -497,7 +675,62 @@ bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent
const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ?
s_sdl_generic_binding_button_mapping[ev->button] :
GenericInputBinding::Unknown;
return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key);
InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key);
return true;
}
bool SDLInputSource::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev)
{
auto it = GetControllerDataForJoystickId(ev->which);
if (it == m_controllers.end())
return false;
if (ev->axis < it->joy_axis_used_in_gc.size() && it->joy_axis_used_in_gc[ev->axis])
return false; // Will get handled by GC event
const u32 axis = ev->axis + static_cast<u32>(std::size(s_sdl_axis_names)); // Ensure we don't conflict with GC axes
const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, axis));
InputManager::InvokeEvents(key, NormalizeS16(ev->value));
return true;
}
bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev)
{
auto it = GetControllerDataForJoystickId(ev->which);
if (it == m_controllers.end())
return false;
if (ev->button < it->joy_button_used_in_gc.size() && it->joy_button_used_in_gc[ev->button])
return false; // Will get handled by GC event
const u32 button =
ev->button + static_cast<u32>(std::size(s_sdl_button_names)); // Ensure we don't conflict with GC buttons
const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button));
InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f);
return true;
}
bool SDLInputSource::HandleJoystickHatEvent(const SDL_JoyHatEvent* ev)
{
auto it = GetControllerDataForJoystickId(ev->which);
if (it == m_controllers.end() || ev->hat >= it->last_hat_state.size())
return false;
const unsigned long last_direction = it->last_hat_state[ev->hat];
it->last_hat_state[ev->hat] = ev->value;
unsigned long changed_direction = last_direction ^ ev->value;
while (changed_direction != 0)
{
unsigned long pos;
_BitScanForward(&pos, changed_direction);
const unsigned long mask = (1u << pos);
changed_direction &= ~mask;
const InputBindingKey key(MakeGenericControllerHatKey(InputSourceType::SDL, it->player_id, ev->hat,
static_cast<u8>(pos),
static_cast<u32>(std::size(s_sdl_hat_direction_names))));
InputManager::InvokeEvents(key, (last_direction & mask) ? 0.0f : 1.0f);
}
return true;
}
std::vector<InputBindingKey> SDLInputSource::EnumerateMotors()
@ -581,7 +814,7 @@ bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, Ge
}
else
{
// joysticks, which we haven't implemented yet anyway.
// joysticks have arbitrary axis numbers, so automapping isn't going to work here.
return false;
}
}

View File

@ -36,22 +36,26 @@ public:
bool ProcessSDLEvent(const SDL_Event* event);
private:
enum : int
{
MAX_NUM_AXES = 7,
MAX_NUM_BUTTONS = 16,
};
SDL_Joystick* GetJoystickForDevice(const std::string_view& device);
private:
struct ControllerData
{
SDL_Haptic* haptic;
SDL_GameController* game_controller;
SDL_Joystick* joystick;
u16 rumble_intensity[2];
int haptic_left_right_effect;
int joystick_id;
int player_id;
bool use_game_controller_rumble;
// Used to disable Joystick controls that are used in GameController inputs so we don't get double events
std::vector<bool> joy_button_used_in_gc;
std::vector<bool> joy_axis_used_in_gc;
// Track last hat state so we can send "unpressed" events.
std::vector<u8> last_hat_state;
};
using ControllerDataVector = std::vector<ControllerData>;
@ -65,14 +69,18 @@ private:
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);
bool OpenDevice(int index, bool is_gamecontroller);
bool CloseDevice(int joystick_index);
bool HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev);
bool HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev);
bool HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev);
bool HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev);
bool HandleJoystickHatEvent(const SDL_JoyHatEvent* ev);
void SendRumbleUpdate(ControllerData* cd);
ControllerDataVector m_controllers;
bool m_sdl_subsystem_initialized = false;
bool m_controller_enhanced_mode = false;
std::vector<std::pair<std::string, std::string>> m_sdl_hints;
};

View File

@ -260,7 +260,7 @@ std::optional<InputBindingKey> XInputSource::ParseKeyString(const std::string_vi
// found an axis!
key.source_subtype = InputSubclass::ControllerAxis;
key.data = i;
key.negative = (binding[0] == '-');
key.modifier = binding[0] == '-' ? InputModifier::Negate : InputModifier::None;
return key;
}
}
@ -291,8 +291,8 @@ std::string XInputSource::ConvertKeyToString(InputBindingKey key)
{
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]);
const char modifier = key.modifier == InputModifier::Negate ? '-' : '+';
ret = StringUtil::StdStringFromFormat("XInput-%u/%c%s", key.source_index, modifier, s_axis_names[key.data]);
}
else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names))
{
@ -382,16 +382,15 @@ void XInputSource::HandleControllerConnection(u32 index)
cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0;
cd.last_state = {};
Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("XInput-%u", index),
StringUtil::StdStringFromFormat("XInput Controller %u", index));
InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("XInput-%u", index),
StringUtil::StdStringFromFormat("XInput Controller %u", index));
}
void XInputSource::HandleControllerDisconnection(u32 index)
{
Log_InfoPrintf("XInput controller %u disconnected.", index);
InputManager::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("XInput-%u", index));
m_controllers[index] = {};
Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("XInput-%u", index));
}
void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)