From 3c46f7b44cf72374f5db4ec519ceaa8bcd60b232 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 22 Aug 2020 16:44:06 +1000 Subject: [PATCH] ControllerInterface: Add XInput controller backend --- README.md | 1 + src/duckstation-qt/duckstation-qt.vcxproj | 16 +- src/duckstation-qt/generalsettingswidget.cpp | 15 + src/duckstation-qt/generalsettingswidget.ui | 27 +- src/duckstation-sdl/duckstation-sdl.vcxproj | 16 +- src/duckstation-sdl/sdl_host_interface.cpp | 5 - src/duckstation-sdl/sdl_host_interface.h | 1 - src/frontend-common/CMakeLists.txt | 4 +- src/frontend-common/common_host_interface.cpp | 63 ++-- src/frontend-common/common_host_interface.h | 2 +- src/frontend-common/controller_interface.cpp | 59 ++++ src/frontend-common/controller_interface.h | 19 ++ src/frontend-common/frontend-common.vcxproj | 2 + .../frontend-common.vcxproj.filters | 2 + .../sdl_controller_interface.cpp | 5 + .../sdl_controller_interface.h | 1 + .../xinput_controller_interface.cpp | 318 ++++++++++++++++++ .../xinput_controller_interface.h | 89 +++++ 18 files changed, 600 insertions(+), 45 deletions(-) create mode 100644 src/frontend-common/xinput_controller_interface.cpp create mode 100644 src/frontend-common/xinput_controller_interface.h diff --git a/README.md b/README.md index 5b78ebf41..7d738a5eb 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c ## Latest News +- 2020/08/22: XInput controller backend added. - 2020/08/20: Per-game setting overrides added. Mostly for compatibility, but some options are customizable. - 2020/08/19: CPU PGXP mode added. It is very slow and incompatible with the recompiler, only use for games which need it. - 2020/08/15: Playlist support/single memcard for multi-disc games in Qt frontend added. diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 7f75cbbad..29441b4f3 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -364,7 +364,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) @@ -386,7 +386,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) @@ -408,7 +408,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) @@ -432,7 +432,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) @@ -457,7 +457,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) true false @@ -481,7 +481,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) true true @@ -506,7 +506,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) true false @@ -530,7 +530,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) true true diff --git a/src/duckstation-qt/generalsettingswidget.cpp b/src/duckstation-qt/generalsettingswidget.cpp index 6ed0929a7..efe38d487 100644 --- a/src/duckstation-qt/generalsettingswidget.cpp +++ b/src/duckstation-qt/generalsettingswidget.cpp @@ -1,5 +1,6 @@ #include "generalsettingswidget.h" #include "autoupdaterdialog.h" +#include "frontend-common/controller_interface.h" #include "settingsdialog.h" #include "settingwidgetbinder.h" @@ -8,6 +9,12 @@ GeneralSettingsWidget::GeneralSettingsWidget(QtHostInterface* host_interface, QW { m_ui.setupUi(this); + for (u32 i = 0; i < static_cast(ControllerInterface::Backend::Count); i++) + { + m_ui.controllerBackend->addItem(qApp->translate( + "ControllerInterface", ControllerInterface::GetBackendName(static_cast(i)))); + } + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.pauseOnStart, "Main", "StartPaused", false); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.startFullscreen, "Main", "StartFullscreen", false); @@ -32,6 +39,9 @@ GeneralSettingsWidget::GeneralSettingsWidget(QtHostInterface* host_interface, QW "IncreaseTimerResolution", true); SettingWidgetBinder::BindWidgetToNormalizedSetting(m_host_interface, m_ui.emulationSpeed, "Main", "EmulationSpeed", 100.0f, 1.0f); + SettingWidgetBinder::BindWidgetToEnumSetting( + m_host_interface, m_ui.controllerBackend, "Main", "ControllerBackend", &ControllerInterface::ParseBackendName, + &ControllerInterface::GetBackendName, ControllerInterface::GetDefaultBackend()); connect(m_ui.enableSpeedLimiter, &QCheckBox::stateChanged, this, &GeneralSettingsWidget::onEnableSpeedLimiterStateChanged); @@ -87,6 +97,11 @@ GeneralSettingsWidget::GeneralSettingsWidget(QtHostInterface* host_interface, QW dialog->registerWidgetHelp( m_ui.showSpeed, tr("Show Speed"), tr("Unchecked"), tr("Shows the current emulation speed of the system in the top-right corner of the display as a percentage.")); + dialog->registerWidgetHelp(m_ui.controllerBackend, tr("Controller Backend"), + qApp->translate("ControllerInterface", ControllerInterface::GetBackendName( + ControllerInterface::GetDefaultBackend())), + tr("Determines the backend which is used for controller input. Windows users may prefer " + "to use XInput over SDL2 for compatibility.")); // Since this one is compile-time selected, we don't put it in the .ui file. int current_col = 1; diff --git a/src/duckstation-qt/generalsettingswidget.ui b/src/duckstation-qt/generalsettingswidget.ui index 40489f03d..9d3b09b68 100644 --- a/src/duckstation-qt/generalsettingswidget.ui +++ b/src/duckstation-qt/generalsettingswidget.ui @@ -6,8 +6,8 @@ 0 0 - 502 - 358 + 652 + 483 @@ -190,6 +190,29 @@ + + + + Miscellaneous + + + + + + + + Controller Backend: + + + + + + + + + + + diff --git a/src/duckstation-sdl/duckstation-sdl.vcxproj b/src/duckstation-sdl/duckstation-sdl.vcxproj index 076a94b33..3537042ac 100644 --- a/src/duckstation-sdl/duckstation-sdl.vcxproj +++ b/src/duckstation-sdl/duckstation-sdl.vcxproj @@ -222,7 +222,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -244,7 +244,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -266,7 +266,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -291,7 +291,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -317,7 +317,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false @@ -341,7 +341,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true @@ -366,7 +366,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false @@ -390,7 +390,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\nativefiledialog\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp index b4d6bd393..84b20e750 100644 --- a/src/duckstation-sdl/sdl_host_interface.cpp +++ b/src/duckstation-sdl/sdl_host_interface.cpp @@ -277,11 +277,6 @@ std::unique_ptr SDLHostInterface::CreateAudioStream(AudioBackend ba } } -std::unique_ptr SDLHostInterface::CreateControllerInterface() -{ - return std::make_unique(); -} - std::optional SDLHostInterface::GetHostKeyCode(const std::string_view key_code) const { const std::optional code = SDLKeyNames::ParseKeyString(key_code); diff --git a/src/duckstation-sdl/sdl_host_interface.h b/src/duckstation-sdl/sdl_host_interface.h index ac0eae485..538728111 100644 --- a/src/duckstation-sdl/sdl_host_interface.h +++ b/src/duckstation-sdl/sdl_host_interface.h @@ -46,7 +46,6 @@ protected: bool AcquireHostDisplay() override; void ReleaseHostDisplay() override; std::unique_ptr CreateAudioStream(AudioBackend backend) override; - std::unique_ptr CreateControllerInterface() override; void OnSystemCreated() override; void OnSystemPaused(bool paused) override; diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 81f74a4b6..ddae45acc 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -23,6 +23,8 @@ if(WIN32) target_sources(frontend-common PRIVATE d3d11_host_display.cpp d3d11_host_display.h + xinput_controller_interface.cpp + xinput_controller_interface.h ) target_link_libraries(frontend-common PRIVATE d3d11.lib dxgi.lib) endif() @@ -36,7 +38,7 @@ if(SDL2_FOUND AND NOT BUILD_LIBRETRO_CORE) sdl_initializer.cpp sdl_initializer.h ) - target_compile_definitions(frontend-common PRIVATE "WITH_SDL2=1") + target_compile_definitions(frontend-common PUBLIC "WITH_SDL2=1") target_include_directories(frontend-common PRIVATE ${SDL2_INCLUDE_DIRS}) target_link_libraries(frontend-common PRIVATE ${SDL2_LIBRARIES}) diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index b9181a44c..b29b205fb 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -30,7 +30,6 @@ #ifdef WITH_SDL2 #include "sdl_audio_stream.h" -#include "sdl_controller_interface.h" #endif #ifdef WITH_DISCORD_PRESENCE @@ -81,17 +80,7 @@ bool CommonHostInterface::Initialize() RegisterSaveStateHotkeys(); RegisterAudioHotkeys(); - m_controller_interface = CreateControllerInterface(); - if (m_controller_interface && !m_controller_interface->Initialize(this)) - { - Log_WarningPrintf("Failed to initialize controller bindings are not possible."); - m_controller_interface.reset(); - } - else if (!m_controller_interface) - { - Log_WarningPrintf("No controller interface created, controller bindings are not possible."); - } - + UpdateControllerInterface(); return true; } @@ -459,14 +448,45 @@ std::unique_ptr CommonHostInterface::CreateAudioStream(AudioBackend } } -std::unique_ptr CommonHostInterface::CreateControllerInterface() +void CommonHostInterface::UpdateControllerInterface() { - // In the future we might want to use different controller interfaces. -#ifdef WITH_SDL2 - return std::make_unique(); -#else - return nullptr; -#endif + const std::string backend_str = GetStringSettingValue( + "Main", "ControllerBackend", ControllerInterface::GetBackendName(ControllerInterface::GetDefaultBackend())); + const std::optional new_backend = + ControllerInterface::ParseBackendName(backend_str.c_str()); + const ControllerInterface::Backend current_backend = + (m_controller_interface ? m_controller_interface->GetBackend() : ControllerInterface::Backend::None); + if (new_backend == current_backend) + return; + + if (m_controller_interface) + { + m_controller_interface->Shutdown(); + m_controller_interface.reset(); + } + + if (!new_backend.has_value()) + { + Log_ErrorPrintf("Invalid controller interface type: '%s'", backend_str.c_str()); + return; + } + + if (new_backend == ControllerInterface::Backend::None) + { + Log_WarningPrintf("No controller interface created, controller bindings are not possible."); + return; + } + + m_controller_interface = ControllerInterface::Create(new_backend.value()); + if (!m_controller_interface || !m_controller_interface->Initialize(this)) + { + Log_WarningPrintf("Failed to initialize controller interface, bindings are not possible."); + if (m_controller_interface) + { + m_controller_interface->Shutdown(); + m_controller_interface.reset(); + } + } } bool CommonHostInterface::LoadState(bool global, s32 slot) @@ -1831,6 +1851,9 @@ void CommonHostInterface::SetDefaultSettings(SettingsInterface& si) si.SetStringValue("Hotkeys", "DecreaseResolutionScale", "Keyboard/PageDown"); si.SetStringValue("Hotkeys", "ToggleSoftwareRendering", "Keyboard/End"); + si.SetStringValue("Main", "ControllerBackend", + ControllerInterface::GetBackendName(ControllerInterface::GetDefaultBackend())); + #ifdef WITH_DISCORD_PRESENCE si.SetBoolValue("Main", "EnableDiscordPresence", false); #endif @@ -1854,6 +1877,8 @@ void CommonHostInterface::CheckForSettingsChanges(const Settings& old_settings) { HostInterface::CheckForSettingsChanges(old_settings); + UpdateControllerInterface(); + if (System::IsValid()) { if (g_settings.audio_backend != old_settings.audio_backend || diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index b365f5dd3..0c110dac5 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -182,7 +182,7 @@ protected: virtual bool SetFullscreen(bool enabled); virtual std::unique_ptr CreateAudioStream(AudioBackend backend) override; - virtual std::unique_ptr CreateControllerInterface(); + virtual void UpdateControllerInterface(); virtual void OnSystemCreated() override; virtual void OnSystemPaused(bool paused); diff --git a/src/frontend-common/controller_interface.cpp b/src/frontend-common/controller_interface.cpp index 8816454f1..0cfc58e8f 100644 --- a/src/frontend-common/controller_interface.cpp +++ b/src/frontend-common/controller_interface.cpp @@ -1,6 +1,7 @@ #include "controller_interface.h" #include "common/assert.h" #include "common/log.h" +#include "common/string_util.h" #include "core/controller.h" #include "core/system.h" #include @@ -79,3 +80,61 @@ bool ControllerInterface::BindControllerAxisToButton(int controller_index, int a return false; } +static constexpr std::array(ControllerInterface::Backend::Count)> s_backend_names = {{ + TRANSLATABLE("ControllerInterface", "None"), +#ifdef WITH_SDL2 + TRANSLATABLE("ControllerInterface", "SDL"), +#endif +#ifdef WIN32 + TRANSLATABLE("ControllerInterface", "XInput"), +#endif +}}; + +std::optional ControllerInterface::ParseBackendName(const char* name) +{ + for (u32 i = 0; i < static_cast(s_backend_names.size()); i++) + { + if (StringUtil::Strcasecmp(name, s_backend_names[i]) == 0) + return static_cast(i); + } + + return std::nullopt; +} + +const char* ControllerInterface::GetBackendName(Backend type) +{ + return s_backend_names[static_cast(type)]; +} + +ControllerInterface::Backend ControllerInterface::GetDefaultBackend() +{ +#ifdef WITH_SDL2 + return Backend::SDL; +#endif +#ifdef WIN32 + return Backend::XInput; +#else + return Backend::None; +#endif +} + +#ifdef WITH_SDL2 +#include "sdl_controller_interface.h" +#endif +#ifdef WIN32 +#include "xinput_controller_interface.h" +#endif + +std::unique_ptr ControllerInterface::Create(Backend type) +{ +#ifdef WITH_SDL2 + if (type == Backend::SDL) + return std::make_unique(); +#endif +#ifdef WIN32 + if (type == Backend::XInput) + return std::make_unique(); +#endif + + return {}; +} diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h index 6f9af9247..ab877a72a 100644 --- a/src/frontend-common/controller_interface.h +++ b/src/frontend-common/controller_interface.h @@ -3,6 +3,7 @@ #include "core/types.h" #include #include +#include #include #include @@ -12,6 +13,18 @@ class Controller; class ControllerInterface { public: + enum class Backend + { + None, +#ifdef WITH_SDL2 + SDL, +#endif +#ifdef WIN32 + XInput, +#endif + Count + }; + enum : int { MAX_NUM_AXISES = 7, @@ -24,6 +37,12 @@ public: ControllerInterface(); virtual ~ControllerInterface(); + static std::optional ParseBackendName(const char* name); + static const char* GetBackendName(Backend type); + static Backend GetDefaultBackend(); + static std::unique_ptr Create(Backend type); + + virtual Backend GetBackend() const = 0; virtual bool Initialize(CommonHostInterface* host_interface); virtual void Shutdown(); diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index 45b535207..ebcae7ee0 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -76,6 +76,7 @@ + @@ -90,6 +91,7 @@ + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index bf40abb3d..d84627ceb 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -13,6 +13,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp index 96a07b04d..919228d49 100644 --- a/src/frontend-common/sdl_controller_interface.cpp +++ b/src/frontend-common/sdl_controller_interface.cpp @@ -17,6 +17,11 @@ SDLControllerInterface::~SDLControllerInterface() Assert(m_controllers.empty()); } +ControllerInterface::Backend SDLControllerInterface::GetBackend() const +{ + return ControllerInterface::Backend::SDL; +} + bool SDLControllerInterface::Initialize(CommonHostInterface* host_interface) { if (!ControllerInterface::Initialize(host_interface)) diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h index 7636d9372..4fa43e556 100644 --- a/src/frontend-common/sdl_controller_interface.h +++ b/src/frontend-common/sdl_controller_interface.h @@ -14,6 +14,7 @@ public: SDLControllerInterface(); ~SDLControllerInterface(); + Backend GetBackend() const override; bool Initialize(CommonHostInterface* host_interface) override; void Shutdown() override; diff --git a/src/frontend-common/xinput_controller_interface.cpp b/src/frontend-common/xinput_controller_interface.cpp new file mode 100644 index 000000000..92866fd72 --- /dev/null +++ b/src/frontend-common/xinput_controller_interface.cpp @@ -0,0 +1,318 @@ +#include "xinput_controller_interface.h" +#include "common/assert.h" +#include "common/file_system.h" +#include "common/log.h" +#include "core/controller.h" +#include "core/host_interface.h" +#include "core/system.h" +#include +Log_SetChannel(XInputControllerInterface); + +XInputControllerInterface::XInputControllerInterface() = default; + +XInputControllerInterface::~XInputControllerInterface() +{ + if (m_xinput_module) + FreeLibrary(m_xinput_module); +} + +ControllerInterface::Backend XInputControllerInterface::GetBackend() const +{ + return ControllerInterface::Backend::XInput; +} + +bool XInputControllerInterface::Initialize(CommonHostInterface* host_interface) +{ + m_xinput_module = LoadLibraryA("xinput1_4.dll"); + if (!m_xinput_module) + { + m_xinput_module = LoadLibraryA("xinput9_1_0.dll"); + if (!m_xinput_module) + { + Log_ErrorPrintf("Failed to load XInput module."); + return false; + } + } + + // Try the hidden version of XInputGetState(), which lets us query the guide button. + m_xinput_get_state = + reinterpret_cast(GetProcAddress(m_xinput_module, reinterpret_cast(100))); + if (!m_xinput_get_state) + reinterpret_cast(GetProcAddress(m_xinput_module, "XInputGetState")); + m_xinput_get_capabilities = + reinterpret_cast(GetProcAddress(m_xinput_module, "XInputGetCapabilities")); + m_xinput_set_state = + reinterpret_cast(GetProcAddress(m_xinput_module, "XInputSetState")); + if (!m_xinput_get_state || !m_xinput_get_capabilities || !m_xinput_set_state) + { + Log_ErrorPrintf("Failed to get XInput function pointers."); + return false; + } + + if (!ControllerInterface::Initialize(host_interface)) + return false; + + return true; +} + +void XInputControllerInterface::Shutdown() +{ + ControllerInterface::Shutdown(); +} + +void XInputControllerInterface::PollEvents() +{ + for (u32 i = 0; i < XUSER_MAX_COUNT; i++) + { + XINPUT_STATE new_state; + const DWORD result = m_xinput_get_state(i, &new_state); + ControllerData& cd = m_controllers[i]; + if (result == ERROR_SUCCESS) + { + if (!cd.connected) + { + cd.connected = true; + UpdateCapabilities(i); + OnControllerConnected(static_cast(i)); + } + + CheckForStateChanges(i, new_state); + } + else + { + if (result != ERROR_DEVICE_NOT_CONNECTED) + Log_WarningPrintf("XInputGetState(%u) failed: 0x%08X / 0x%08X", i, result, GetLastError()); + + if (cd.connected) + { + cd.connected = false; + cd.last_state = {}; + OnControllerDisconnected(static_cast(i)); + } + } + } +} + +void XInputControllerInterface::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; + if (ogp.sThumbLX != ngp.sThumbLX) + { + HandleAxisEvent(index, Axis::LeftX, ngp.sThumbLX); + ogp.sThumbLX = ngp.sThumbLX; + } + if (ogp.sThumbLY != ngp.sThumbLY) + { + HandleAxisEvent(index, Axis::LeftY, -ngp.sThumbLY); + ogp.sThumbLY = ngp.sThumbLY; + } + if (ogp.sThumbRX != ngp.sThumbRX) + { + HandleAxisEvent(index, Axis::RightX, ngp.sThumbRX); + ogp.sThumbRX = ngp.sThumbRX; + } + if (ogp.sThumbRY != ngp.sThumbRY) + { + HandleAxisEvent(index, Axis::RightY, -ngp.sThumbRY); + ogp.sThumbRY = ngp.sThumbRY; + } + if (ogp.bLeftTrigger != ngp.bLeftTrigger) + { + HandleAxisEvent(index, Axis::LeftTrigger, static_cast(ZeroExtend32(ngp.bLeftTrigger) << 8)); + ogp.bLeftTrigger = ngp.bLeftTrigger; + } + if (ogp.bRightTrigger != ngp.bRightTrigger) + { + HandleAxisEvent(index, Axis::RightTrigger, static_cast(ZeroExtend32(ngp.bRightTrigger) << 8)); + ogp.bRightTrigger = ngp.bRightTrigger; + } + + static constexpr std::array button_masks = { + {XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_B, XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_Y, XINPUT_GAMEPAD_BACK, + 0x400 /* XINPUT_GAMEPAD_GUIDE */, XINPUT_GAMEPAD_START, XINPUT_GAMEPAD_LEFT_THUMB, XINPUT_GAMEPAD_RIGHT_THUMB, + XINPUT_GAMEPAD_LEFT_SHOULDER, XINPUT_GAMEPAD_RIGHT_SHOULDER, XINPUT_GAMEPAD_DPAD_UP, XINPUT_GAMEPAD_DPAD_DOWN, + XINPUT_GAMEPAD_DPAD_LEFT, XINPUT_GAMEPAD_DPAD_RIGHT}}; + + 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 < static_cast(button_masks.size()); button++) + { + const u16 button_mask = button_masks[button]; + if ((old_button_bits & button_mask) != (new_button_bits & button_mask)) + HandleButtonEvent(index, button, (new_button_bits & button_mask) != 0); + } + + ogp.wButtons = ngp.wButtons; + } +} + +void XInputControllerInterface::UpdateCapabilities(u32 index) +{ + ControllerData& cd = m_controllers[index]; + + XINPUT_CAPABILITIES caps = {}; + m_xinput_get_capabilities(index, 0, &caps); + cd.supports_rumble = (caps.Flags & 0x0001 /* XINPUT_CAPS_FFB_SUPPORTED */); + + Log_InfoPrintf("Controller %u: Rumble is %s", index, cd.supports_rumble ? "supported" : "not supported"); +} + +void XInputControllerInterface::ClearBindings() +{ + for (auto& it : m_controllers) + { + for (AxisCallback& ac : it.axis_mapping) + ac = {}; + for (ButtonCallback& bc : it.button_mapping) + bc = {}; + } +} + +bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) +{ + if (static_cast(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected) + return false; + + if (axis_number < 0 || axis_number >= NUM_AXISES) + return false; + + m_controllers[controller_index].axis_mapping[axis_number] = std::move(callback); + return true; +} + +bool XInputControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback) +{ + if (static_cast(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected) + return false; + + if (button_number < 0 || button_number >= NUM_BUTTONS) + return false; + + m_controllers[controller_index].button_mapping[button_number] = std::move(callback); + return true; +} + +bool XInputControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) +{ + if (static_cast(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected) + return false; + + if (axis_number < 0 || axis_number >= MAX_NUM_AXISES) + return false; + + m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback); + return true; +} + +bool XInputControllerInterface::HandleAxisEvent(u32 index, Axis axis, s32 value) +{ + const float f_value = static_cast(value) / (value < 0 ? 32768.0f : 32767.0f); + Log_DevPrintf("controller %u axis %u %d %f", index, static_cast(axis), value, f_value); + DebugAssert(index < XUSER_MAX_COUNT); + + if (DoEventHook(Hook::Type::Axis, index, static_cast(axis), f_value)) + return true; + + const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast(axis)]; + if (cb) + { + // Apply axis scaling only when controller axis is mapped to an axis + cb(std::clamp(m_controllers[index].axis_scale * f_value, -1.0f, 1.0f)); + return true; + } + + // set the other direction to false so large movements don't leave the opposite on + const bool outside_deadzone = (std::abs(f_value) >= m_controllers[index].deadzone); + const bool positive = (f_value >= 0.0f); + const ButtonCallback& other_button_cb = + m_controllers[index].axis_button_mapping[static_cast(axis)][BoolToUInt8(!positive)]; + const ButtonCallback& button_cb = + m_controllers[index].axis_button_mapping[static_cast(axis)][BoolToUInt8(positive)]; + if (button_cb) + { + button_cb(outside_deadzone); + if (other_button_cb) + other_button_cb(false); + return true; + } + else if (other_button_cb) + { + other_button_cb(false); + return true; + } + else + { + return false; + } +} + +bool XInputControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed) +{ + Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released"); + DebugAssert(index < XUSER_MAX_COUNT); + + if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f)) + return true; + + const ButtonCallback& cb = m_controllers[index].button_mapping[button]; + if (!cb) + return false; + + cb(pressed); + return true; +} + +u32 XInputControllerInterface::GetControllerRumbleMotorCount(int controller_index) +{ + if (static_cast(controller_index) >= XUSER_MAX_COUNT || !m_controllers[controller_index].connected) + return 0; + + return m_controllers[controller_index].supports_rumble ? NUM_RUMBLE_MOTORS : 0; +} + +void XInputControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, + u32 num_motors) +{ + DebugAssert(static_cast(controller_index) < XUSER_MAX_COUNT); + + // we'll update before this duration is elapsed + static constexpr float MIN_STRENGTH = 0.01f; + static constexpr u32 DURATION = 100000; + + XINPUT_VIBRATION vib; + vib.wLeftMotorSpeed = (strengths[0] >= MIN_STRENGTH) ? static_cast(strengths[0] * 65535.0f) : 0; + vib.wRightMotorSpeed = (strengths[1] >= MIN_STRENGTH) ? static_cast(strengths[1] * 65535.0f) : 0; + m_xinput_set_state(static_cast(controller_index), &vib); +} + +bool XInputControllerInterface::SetControllerAxisScale(int controller_index, float scale /* = 1.00f */) +{ + if (static_cast(controller_index) >= XUSER_MAX_COUNT || !m_controllers[controller_index].connected) + return false; + + m_controllers[static_cast(controller_index)].axis_scale = std::clamp(std::abs(scale), 0.01f, 1.50f); + Log_InfoPrintf("Controller %d axis scale set to %f", controller_index, + m_controllers[static_cast(controller_index)].axis_scale); + return true; +} + +bool XInputControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */) +{ + if (static_cast(controller_index) >= XUSER_MAX_COUNT || !m_controllers[controller_index].connected) + return false; + + m_controllers[static_cast(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f); + Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, + m_controllers[static_cast(controller_index)].deadzone); + return true; +} diff --git a/src/frontend-common/xinput_controller_interface.h b/src/frontend-common/xinput_controller_interface.h new file mode 100644 index 000000000..5949922e6 --- /dev/null +++ b/src/frontend-common/xinput_controller_interface.h @@ -0,0 +1,89 @@ +#pragma once +#include "common/windows_headers.h" +#include "controller_interface.h" +#include "core/types.h" +#include +#include +#include +#include +#include + +class XInputControllerInterface final : public ControllerInterface +{ +public: + XInputControllerInterface(); + ~XInputControllerInterface() override; + + Backend GetBackend() const override; + bool Initialize(CommonHostInterface* host_interface) override; + void Shutdown() override; + + // Removes all bindings. Call before setting new bindings. + void ClearBindings() override; + + // Binding to events. If a binding for this axis/button already exists, returns false. + bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override; + bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; + bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) override; + + // Changing rumble strength. + u32 GetControllerRumbleMotorCount(int controller_index) override; + void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override; + + // Set scaling that will be applied on axis-to-axis mappings + bool SetControllerAxisScale(int controller_index, float scale = 1.00f) override; + + // Set deadzone that will be applied on axis-to-button mappings + bool SetControllerDeadzone(int controller_index, float size = 0.25f) override; + + void PollEvents() override; + +private: + enum : u32 + { + NUM_AXISES = 6, + NUM_BUTTONS = 15, + NUM_RUMBLE_MOTORS = 2 + }; + enum class Axis : u32 + { + LeftX, + LeftY, + RightX, + RightY, + LeftTrigger, + RightTrigger + }; + + struct ControllerData + { + XINPUT_STATE last_state = {}; + bool connected = false; + bool supports_rumble = false; + + // Scaling value of 1.30f to 1.40f recommended when using recent controllers + float axis_scale = 1.00f; + float deadzone = 0.25f; + + std::array axis_mapping; + std::array button_mapping; + std::array, MAX_NUM_AXISES> axis_button_mapping; + }; + + using ControllerDataArray = std::array; + + void CheckForStateChanges(u32 index, const XINPUT_STATE& new_state); + void UpdateCapabilities(u32 index); + bool HandleAxisEvent(u32 index, Axis axis, s32 value); + bool HandleButtonEvent(u32 index, u32 button, bool pressed); + + ControllerDataArray m_controllers; + + HMODULE m_xinput_module{}; + DWORD(WINAPI* m_xinput_get_state)(DWORD, XINPUT_STATE*); + DWORD(WINAPI* m_xinput_get_capabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*); + DWORD(WINAPI* m_xinput_set_state)(DWORD, XINPUT_VIBRATION*); + std::mutex m_event_intercept_mutex; + Hook::Callback m_event_intercept_callback; +};