From 59cf79949117336fc80e284534e3854026054324 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 16 Feb 2020 00:14:53 +0900 Subject: [PATCH] Qt: Implement controller interface/binding --- src/core/host_interface.h | 3 + src/duckstation-qt/CMakeLists.txt | 2 +- src/duckstation-qt/inputbindingwidgets.cpp | 46 ++ src/duckstation-qt/inputbindingwidgets.h | 4 + src/duckstation-qt/qthostinterface.cpp | 110 ++++- src/duckstation-qt/qthostinterface.h | 20 + src/frontend-common/CMakeLists.txt | 2 + src/frontend-common/frontend-common.vcxproj | 2 + .../frontend-common.vcxproj.filters | 2 + .../sdl_controller_interface.cpp | 425 ++++++++++++++++++ .../sdl_controller_interface.h | 107 +++++ 11 files changed, 720 insertions(+), 3 deletions(-) create mode 100644 src/frontend-common/sdl_controller_interface.cpp create mode 100644 src/frontend-common/sdl_controller_interface.h diff --git a/src/core/host_interface.h b/src/core/host_interface.h index 4ca44b5ec..ebd8f0f35 100644 --- a/src/core/host_interface.h +++ b/src/core/host_interface.h @@ -39,6 +39,9 @@ public: /// Returns the game list. ALWAYS_INLINE const GameList* GetGameList() const { return m_game_list.get(); } + /// Access to emulated system. + ALWAYS_INLINE System* GetSystem() const { return m_system.get(); } + bool BootSystemFromFile(const char* filename); bool BootSystemFromBIOS(); void PauseSystem(bool paused); diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 3afa97f12..f17787d7f 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -43,7 +43,7 @@ add_executable(duckstation-qt settingsdialog.ui ) -target_link_libraries(duckstation-qt PRIVATE core common imgui glad minizip Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Network) +target_link_libraries(duckstation-qt PRIVATE frontend-common core common imgui glad minizip Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Network) if(WIN32) target_sources(duckstation-qt PRIVATE diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp index 2dbca4e89..485dd13eb 100644 --- a/src/duckstation-qt/inputbindingwidgets.cpp +++ b/src/duckstation-qt/inputbindingwidgets.cpp @@ -1,5 +1,6 @@ #include "inputbindingwidgets.h" #include "core/settings.h" +#include "frontend-common/sdl_controller_interface.h" #include "qthostinterface.h" #include "qtutils.h" #include @@ -81,6 +82,49 @@ void InputButtonBindingWidget::onInputListenTimerTimeout() setText(tr("Push Button... [%1]").arg(m_input_listen_remaining_seconds)); } +void InputButtonBindingWidget::hookControllerInput() +{ + m_host_interface->enableBackgroundControllerPolling(); + g_sdl_controller_interface.SetHook([this](const SDLControllerInterface::Hook& ei) { + if (ei.type == SDLControllerInterface::Hook::Type::Axis) + { + // TODO: this probably should consider the "last value" + QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, ei.value > 0)); + return SDLControllerInterface::Hook::CallbackResult::StopMonitoring; + } + else if (ei.type == SDLControllerInterface::Hook::Type::Button && ei.value > 0.0f) + { + QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number)); + return SDLControllerInterface::Hook::CallbackResult::StopMonitoring; + } + + return SDLControllerInterface::Hook::CallbackResult::ContinueMonitoring; + }); +} + +void InputButtonBindingWidget::unhookControllerInput() +{ + g_sdl_controller_interface.ClearHook(); + m_host_interface->disableBackgroundControllerPolling(); +} + +void InputButtonBindingWidget::bindToControllerAxis(int controller_index, int axis_index, bool positive) +{ + m_new_binding_value = + QStringLiteral("Controller%1/%2Axis%3").arg(controller_index).arg(positive ? '+' : '-').arg(axis_index); + setNewBinding(); + stopListeningForInput(); +} + +void InputButtonBindingWidget::bindToControllerButton(int controller_index, int button_index) +{ + m_new_binding_value = QStringLiteral("Controller%1/Button%2").arg(controller_index).arg(button_index); + setNewBinding(); + stopListeningForInput(); +} + void InputButtonBindingWidget::startListeningForInput() { m_input_listen_timer = new QTimer(this); @@ -95,6 +139,7 @@ void InputButtonBindingWidget::startListeningForInput() installEventFilter(this); grabKeyboard(); grabMouse(); + hookControllerInput(); } void InputButtonBindingWidget::stopListeningForInput() @@ -103,6 +148,7 @@ void InputButtonBindingWidget::stopListeningForInput() delete m_input_listen_timer; m_input_listen_timer = nullptr; + unhookControllerInput(); releaseMouse(); releaseKeyboard(); removeEventFilter(this); diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h index 9604aed6d..e8d1c392d 100644 --- a/src/duckstation-qt/inputbindingwidgets.h +++ b/src/duckstation-qt/inputbindingwidgets.h @@ -20,12 +20,16 @@ protected: private Q_SLOTS: void onPressed(); void onInputListenTimerTimeout(); + void bindToControllerAxis(int controller_index, int axis_index, bool positive); + void bindToControllerButton(int controller_index, int button_index); private: bool isListeningForInput() const { return m_input_listen_timer != nullptr; } void startListeningForInput(); void stopListeningForInput(); void setNewBinding(); + void hookControllerInput(); + void unhookControllerInput(); QtHostInterface* m_host_interface; QString m_setting_name; diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index c7f0702b1..a887b7a64 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -8,12 +8,14 @@ #include "core/game_list.h" #include "core/gpu.h" #include "core/system.h" +#include "frontend-common/sdl_controller_interface.h" #include "qtsettingsinterface.h" #include "qtutils.h" #include #include #include #include +#include #include #include #include @@ -28,7 +30,6 @@ QtHostInterface::QtHostInterface(QObject* parent) { checkSettings(); refreshGameList(); - doUpdateInputMap(); createThread(); } @@ -283,6 +284,7 @@ void QtHostInterface::OnSystemCreated() HostInterface::OnSystemCreated(); wakeThread(); + destroyBackgroundControllerPollTimer(); emit emulationStarted(); } @@ -301,6 +303,9 @@ void QtHostInterface::OnSystemDestroyed() { HostInterface::OnSystemDestroyed(); + if (m_background_controller_polling_enable_count > 0) + createBackgroundControllerPollTimer(); + emit emulationStopped(); } @@ -350,7 +355,9 @@ void QtHostInterface::updateInputMap() void QtHostInterface::doUpdateInputMap() { m_keyboard_input_handlers.clear(); + g_sdl_controller_interface.ClearControllerBindings(); + std::lock_guard lock(m_qsettings_mutex); updateControllerInputMap(); updateHotkeyInputMap(); } @@ -493,9 +500,41 @@ void QtHostInterface::addButtonToInputMap(const QString& binding, InputButtonHan m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler)); } + else if (device.startsWith(QStringLiteral("Controller"))) + { + bool controller_index_okay; + const int controller_index = device.mid(10).toInt(&controller_index_okay); + + if (!controller_index_okay || controller_index < 0) + { + qWarning() << "Malformed controller binding: " << binding; + return; + } + if (button.startsWith(QStringLiteral("Button"))) + { + bool button_index_okay; + const int button_index = button.mid(6).toInt(&button_index_okay); + if (!button_index_okay || + !g_sdl_controller_interface.BindControllerButton(controller_index, button_index, std::move(handler))) + { + qWarning() << "Failed to bind " << binding; + } + } + else if (button.startsWith(QStringLiteral("+Axis")) || button.startsWith(QStringLiteral("-Axis"))) + { + bool axis_index_okay; + const int axis_index = button.mid(5).toInt(&axis_index_okay); + const bool positive = (button[0] == '+'); + if (!axis_index_okay || !g_sdl_controller_interface.BindControllerAxisToButton(controller_index, axis_index, + positive, std::move(handler))) + { + qWarning() << "Failed to bind " << binding; + } + } + } else { - qWarning() << "Unknown input device: " << device; + qWarning() << "Unknown input device: " << binding; return; } } @@ -659,6 +698,63 @@ void QtHostInterface::saveState(bool global, qint32 slot, bool block_until_done SaveState(global, slot); } +void QtHostInterface::enableBackgroundControllerPolling() +{ + if (!isOnWorkerThread()) + { + QMetaObject::invokeMethod(this, "enableBackgroundControllerPolling", Qt::BlockingQueuedConnection); + return; + } + + if (m_background_controller_polling_enable_count++ > 0) + return; + + if (!m_system) + { + createBackgroundControllerPollTimer(); + + // drain the event queue so we don't get events late + g_sdl_controller_interface.PumpSDLEvents(); + } +} + +void QtHostInterface::disableBackgroundControllerPolling() +{ + if (!isOnWorkerThread()) + { + QMetaObject::invokeMethod(this, "disableBackgroundControllerPolling"); + return; + } + + Assert(m_background_controller_polling_enable_count > 0); + if (--m_background_controller_polling_enable_count > 0) + return; + + if (!m_system) + destroyBackgroundControllerPollTimer(); +} + +void QtHostInterface::doBackgroundControllerPoll() +{ + g_sdl_controller_interface.PumpSDLEvents(); +} + +void QtHostInterface::createBackgroundControllerPollTimer() +{ + DebugAssert(!m_background_controller_polling_timer); + m_background_controller_polling_timer = new QTimer(this); + m_background_controller_polling_timer->setSingleShot(false); + m_background_controller_polling_timer->setTimerType(Qt::VeryCoarseTimer); + connect(m_background_controller_polling_timer, &QTimer::timeout, this, &QtHostInterface::doBackgroundControllerPoll); + m_background_controller_polling_timer->start(BACKGROUND_CONTROLLER_POLLING_INTERVAL); +} + +void QtHostInterface::destroyBackgroundControllerPollTimer() +{ + delete m_background_controller_polling_timer; + m_background_controller_polling_timer = nullptr; +} + void QtHostInterface::createThread() { m_original_thread = QThread::currentThread(); @@ -685,6 +781,12 @@ void QtHostInterface::threadEntryPoint() { m_worker_thread_event_loop = new QEventLoop(); + // set up controller interface and immediate poll to pick up the controller attached events + g_sdl_controller_interface.Initialize(this, true); + g_sdl_controller_interface.PumpSDLEvents(); + + doUpdateInputMap(); + // TODO: Event which flags the thread as ready while (!m_shutdown_flag.load()) { @@ -710,10 +812,14 @@ void QtHostInterface::threadEntryPoint() m_system->Throttle(); m_worker_thread_event_loop->processEvents(QEventLoop::AllEvents); + g_sdl_controller_interface.PumpSDLEvents(); } m_system.reset(); m_audio_stream.reset(); + + g_sdl_controller_interface.Shutdown(); + delete m_worker_thread_event_loop; m_worker_thread_event_loop = nullptr; diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h index e767a6080..97d7509a2 100644 --- a/src/duckstation-qt/qthostinterface.h +++ b/src/duckstation-qt/qthostinterface.h @@ -18,6 +18,7 @@ class ByteStream; class QEventLoop; class QMenu; class QWidget; +class QTimer; class GameList; @@ -88,11 +89,19 @@ public Q_SLOTS: void loadState(bool global, qint32 slot); void saveState(bool global, qint32 slot, bool block_until_done = false); + /// Enables controller polling even without a system active. Must be matched by a call to + /// disableBackgroundControllerPolling. + void enableBackgroundControllerPolling(); + + /// Disables background controller polling. + void disableBackgroundControllerPolling(); + private Q_SLOTS: void doStopThread(); void doUpdateInputMap(); void doHandleKeyEvent(int key, bool pressed); void onDisplayWindowResized(int width, int height); + void doBackgroundControllerPoll(); protected: bool AcquireHostDisplay() override; @@ -107,6 +116,12 @@ protected: void OnControllerTypeChanged(u32 slot) override; private: + enum : u32 + { + BACKGROUND_CONTROLLER_POLLING_INTERVAL = + 100 /// Interval at which the controllers are polled when the system is not active. + }; + using InputButtonHandler = std::function; class Thread : public QThread @@ -124,6 +139,8 @@ private: void checkSettings(); void updateQSettingsFromCoreSettings(); + void createBackgroundControllerPollTimer(); + void destroyBackgroundControllerPollTimer(); void updateControllerInputMap(); void updateHotkeyInputMap(); @@ -145,4 +162,7 @@ private: // input key maps, todo hotkeys std::map m_keyboard_input_handlers; + + QTimer* m_background_controller_polling_timer = nullptr; + u32 m_background_controller_polling_enable_count = 0; }; diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 134c750d9..a16b3df24 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -5,6 +5,8 @@ add_library(frontend-common imgui_styles.h sdl_audio_stream.cpp sdl_audio_stream.h + sdl_controller_interface.cpp + sdl_controller_interface.h ) target_include_directories(frontend-common PRIVATE ${SDL2_INCLUDE_DIRS}) diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index e86f81745..158d815bf 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -49,11 +49,13 @@ + + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index 9b1e945a7..3bb58c520 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -4,11 +4,13 @@ + + diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp new file mode 100644 index 000000000..a85e66638 --- /dev/null +++ b/src/frontend-common/sdl_controller_interface.cpp @@ -0,0 +1,425 @@ +#include "sdl_controller_interface.h" +#include "common/assert.h" +#include "common/log.h" +#include "core/controller.h" +#include "core/host_interface.h" +#include "core/system.h" +#include +Log_SetChannel(SDLControllerInterface); + +SDLControllerInterface g_sdl_controller_interface; + +SDLControllerInterface::SDLControllerInterface() = default; + +SDLControllerInterface::~SDLControllerInterface() +{ + Assert(m_controllers.empty()); +} + +bool SDLControllerInterface::Initialize(HostInterface* host_interface, bool init_sdl) +{ + if (init_sdl) + { + if (SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0) + { + Log_ErrorPrintf("SDL_Init() failed"); + return false; + } + } + + // we should open the controllers as the connected events come in, so no need to do any more here + m_host_interface = host_interface; + return true; +} + +void SDLControllerInterface::Shutdown() +{ + while (!m_controllers.empty()) + CloseGameController(m_controllers.begin()->first); + + if (m_sdl_initialized_by_us) + { + SDL_Quit(); + m_sdl_initialized_by_us = false; + } + + m_host_interface = nullptr; +} + +void SDLControllerInterface::PumpSDLEvents() +{ + for (;;) + { + SDL_Event ev; + if (SDL_PollEvent(&ev)) + ProcessSDLEvent(&ev); + else + break; + } +} + +bool SDLControllerInterface::ProcessSDLEvent(const SDL_Event* event) +{ + switch (event->type) + { + case SDL_CONTROLLERDEVICEADDED: + { + Log_InfoPrintf("Controller %d inserted", event->cdevice.which); + OpenGameController(event->cdevice.which); + return true; + } + + case SDL_CONTROLLERDEVICEREMOVED: + { + Log_InfoPrintf("Controller %d removed", event->cdevice.which); + CloseGameController(event->cdevice.which); + return true; + } + + case SDL_CONTROLLERAXISMOTION: + return HandleControllerAxisEvent(event); + + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + return HandleControllerButtonEvent(event); + + default: + return false; + } +} + +System* SDLControllerInterface::GetSystem() const +{ + return m_host_interface->GetSystem(); +} + +Controller* SDLControllerInterface::GetController(u32 slot) const +{ + System* system = GetSystem(); + return system ? system->GetController(slot) : nullptr; +} + +void SDLControllerInterface::SetHook(Hook::Callback callback) +{ + std::unique_lock lock(m_event_intercept_mutex); + Assert(!m_event_intercept_callback); + m_event_intercept_callback = std::move(callback); +} + +void SDLControllerInterface::ClearHook() +{ + std::unique_lock lock(m_event_intercept_mutex); + if (m_event_intercept_callback) + m_event_intercept_callback = {}; +} + +bool SDLControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value) +{ + std::unique_lock lock(m_event_intercept_mutex); + if (!m_event_intercept_callback) + return false; + + const Hook ei{type, controller_index, button_or_axis_number, value}; + const Hook::CallbackResult action = m_event_intercept_callback(ei); + if (action == Hook::CallbackResult::StopMonitoring) + m_event_intercept_callback = {}; + + return true; +} + +bool SDLControllerInterface::OpenGameController(int index) +{ + if (m_controllers.find(index) != m_controllers.end()) + CloseGameController(index); + + SDL_GameController* gcontroller = SDL_GameControllerOpen(index); + if (!gcontroller) + { + Log_WarningPrintf("Failed to open controller %d", index); + return false; + } + + Log_InfoPrintf("Opened controller %d: %s", index, SDL_GameControllerName(gcontroller)); + + ControllerData cd = {}; + cd.controller = gcontroller; + + SDL_Joystick* joystick = SDL_GameControllerGetJoystick(gcontroller); + if (joystick) + { + SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick); + if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) == 0) + cd.haptic = haptic; + else + SDL_HapticClose(haptic); + } + + if (cd.haptic) + Log_InfoPrintf("Rumble is supported on '%s'", SDL_GameControllerName(gcontroller)); + else + Log_WarningPrintf("Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller)); + + m_controllers.emplace(index, cd); + return true; +} + +void SDLControllerInterface::CloseGameControllers() +{ + while (!m_controllers.empty()) + CloseGameController(m_controllers.begin()->first); +} + +bool SDLControllerInterface::CloseGameController(int index) +{ + auto it = m_controllers.find(index); + if (it == m_controllers.end()) + return false; + + if (it->second.haptic) + SDL_HapticClose(static_cast(it->second.haptic)); + + SDL_GameControllerClose(static_cast(it->second.controller)); + m_controllers.erase(it); + return true; +} + +void SDLControllerInterface::ClearControllerBindings() +{ + for (auto& it : m_controllers) + { + for (AxisCallback& ac : it.second.axis_mapping) + ac = {}; + for (ButtonCallback& bc : it.second.button_mapping) + bc = {}; + } +} + +bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) +{ + auto it = m_controllers.find(controller_index); + if (it == m_controllers.end()) + return false; + + if (axis_number < 0 || axis_number >= MAX_NUM_AXISES) + return false; + + it->second.axis_mapping[axis_number] = std::move(callback); + return true; +} + +bool SDLControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback) +{ + auto it = m_controllers.find(controller_index); + if (it == m_controllers.end()) + return false; + + if (button_number < 0 || button_number >= MAX_NUM_BUTTONS) + return false; + + it->second.button_mapping[button_number] = std::move(callback); + return true; +} + +bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) +{ + auto it = m_controllers.find(controller_index); + if (it == m_controllers.end()) + return false; + + if (axis_number < 0 || axis_number >= MAX_NUM_AXISES) + return false; + + it->second.axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback); + return true; +} + +void SDLControllerInterface::SetDefaultBindings() +{ + ClearControllerBindings(); + + const ControllerType type = m_host_interface->GetSettings().controller_types[0]; + if (type == ControllerType::None || m_controllers.empty()) + return; + + const int first_controller_index = m_controllers.begin()->first; + +#define SET_AXIS_MAP(axis, name) \ + do \ + { \ + std::optional code = Controller::GetAxisCodeByName(type, name); \ + if (code) \ + { \ + const s32 code_value = code.value(); \ + BindControllerAxis(first_controller_index, axis, [this, code_value](float value) { \ + Controller* controller = GetController(0); \ + if (controller) \ + controller->SetAxisState(code_value, value); \ + }); \ + } \ + } while (0) + +#define SET_BUTTON_MAP(button, name) \ + do \ + { \ + std::optional code = Controller::GetButtonCodeByName(type, name); \ + if (code) \ + { \ + const s32 code_value = code.value(); \ + BindControllerButton(first_controller_index, button, [this, code_value](bool pressed) { \ + Controller* controller = GetController(0); \ + if (controller) \ + controller->SetButtonState(code_value, pressed); \ + }); \ + } \ + } while (0) + +#define SET_AXIS_BUTTON_MAP(axis, direction, name) \ + do \ + { \ + std::optional code = Controller::GetButtonCodeByName(type, name); \ + if (code) \ + { \ + const s32 code_value = code.value(); \ + BindControllerAxisToButton(first_controller_index, axis, direction, [this, code_value](bool pressed) { \ + Controller* controller = GetController(0); \ + if (controller) \ + controller->SetButtonState(code_value, pressed); \ + }); \ + } \ + } while (0) + + SET_AXIS_MAP(SDL_CONTROLLER_AXIS_LEFTX, "LeftX"); + SET_AXIS_MAP(SDL_CONTROLLER_AXIS_LEFTY, "LeftY"); + SET_AXIS_MAP(SDL_CONTROLLER_AXIS_RIGHTX, "RightX"); + SET_AXIS_MAP(SDL_CONTROLLER_AXIS_RIGHTY, "RightY"); + SET_AXIS_MAP(SDL_CONTROLLER_AXIS_TRIGGERLEFT, "LeftTrigger"); + SET_AXIS_MAP(SDL_CONTROLLER_AXIS_TRIGGERRIGHT, "RightTrigger"); + + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_DPAD_UP, "Up"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_DPAD_DOWN, "Down"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_DPAD_LEFT, "Left"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_DPAD_RIGHT, "Right"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_Y, "Triangle"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_A, "Cross"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_X, "Square"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_B, "Circle"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_LEFTSHOULDER, "L1"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, "R1"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_LEFTSTICK, "L3"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_RIGHTSTICK, "R3"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_START, "Start"); + SET_BUTTON_MAP(SDL_CONTROLLER_BUTTON_BACK, "Select"); + + // fallback axis -> button mappings + SET_AXIS_BUTTON_MAP(SDL_CONTROLLER_AXIS_LEFTX, false, "Left"); + SET_AXIS_BUTTON_MAP(SDL_CONTROLLER_AXIS_LEFTX, true, "Right"); + SET_AXIS_BUTTON_MAP(SDL_CONTROLLER_AXIS_LEFTY, false, "Up"); + SET_AXIS_BUTTON_MAP(SDL_CONTROLLER_AXIS_LEFTY, true, "Down"); + SET_AXIS_BUTTON_MAP(SDL_CONTROLLER_AXIS_TRIGGERLEFT, true, "L2"); + SET_AXIS_BUTTON_MAP(SDL_CONTROLLER_AXIS_TRIGGERRIGHT, true, "R2"); + +#undef SET_AXIS_MAP +#undef SET_BUTTON_MAP +#undef SET_AXIS_BUTTON_MAP + + // TODO: L2/R2 -> buttons +} + +bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev) +{ + Log_DebugPrintf("controller %d axis %d %d", ev->caxis.which, ev->caxis.axis, ev->caxis.value); + + // TODO: Make deadzone customizable. + static constexpr float deadzone = 8192.0f / 32768.0f; + + const float value = static_cast(ev->caxis.value) / (ev->caxis.value < 0 ? 32768.0f : 32767.0f); + const bool outside_deadzone = (std::abs(value) >= deadzone); + + // only send monitor events if it's outside of the deadzone, otherwise it's really hard to bind + if (outside_deadzone && DoEventHook(Hook::Type::Axis, ev->caxis.which, ev->caxis.axis, value)) + return true; + + auto it = m_controllers.find(ev->caxis.which); + if (it == m_controllers.end()) + return false; + + const ControllerData& cd = it->second; + const AxisCallback& cb = cd.axis_mapping[ev->caxis.axis]; + if (cb) + { + cb(value); + return true; + } + + // set the other direction to false so large movements don't leave the opposite on + const bool positive = (value >= 0.0f); + const ButtonCallback& other_button_cb = cd.axis_button_mapping[ev->caxis.axis][BoolToUInt8(!positive)]; + const ButtonCallback& button_cb = cd.axis_button_mapping[ev->caxis.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 SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev) +{ + Log_DebugPrintf("controller %d button %d %s", ev->cbutton.which, ev->cbutton.button, + ev->cbutton.state == SDL_PRESSED ? "pressed" : "released"); + + const bool pressed = (ev->cbutton.state == SDL_PRESSED); + if (DoEventHook(Hook::Type::Button, ev->cbutton.which, ev->cbutton.button, pressed ? 1.0f : 0.0f)) + return true; + + auto it = m_controllers.find(ev->caxis.which); + if (it == m_controllers.end()) + return false; + + const ButtonCallback& cb = it->second.button_mapping[ev->cbutton.button]; + if (!cb) + return false; + + cb(pressed); + return true; +} + +void SDLControllerInterface::UpdateControllerRumble() +{ + for (auto& it : m_controllers) + { + ControllerData& cd = it.second; + if (!cd.haptic) + continue; + + float new_strength = 0.0f; + Controller* controller = GetController(cd.controller_index); + if (controller) + { + const u32 motor_count = controller->GetVibrationMotorCount(); + for (u32 i = 0; i < motor_count; i++) + new_strength = std::max(new_strength, controller->GetVibrationMotorStrength(i)); + } + + if (cd.last_rumble_strength == new_strength) + continue; + + if (new_strength > 0.01f) + SDL_HapticRumblePlay(static_cast(cd.haptic), new_strength, 100000); + else + SDL_HapticRumbleStop(static_cast(cd.haptic)); + + cd.last_rumble_strength = new_strength; + } +} diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h new file mode 100644 index 000000000..003352b9f --- /dev/null +++ b/src/frontend-common/sdl_controller_interface.h @@ -0,0 +1,107 @@ +#pragma once +#include "core/types.h" +#include +#include +#include +#include + +class HostInterface; +class System; +class Controller; + +union SDL_Event; + +class SDLControllerInterface +{ +public: + enum : int + { + MAX_NUM_AXISES = 7, + MAX_NUM_BUTTONS = 15 + }; + + using AxisCallback = std::function; + using ButtonCallback = std::function; + + SDLControllerInterface(); + ~SDLControllerInterface(); + + bool Initialize(HostInterface* host_interface, bool init_sdl); + void Shutdown(); + + // Removes all bindings. Call before setting new bindings. + void ClearControllerBindings(); + + // Binding to events. If a binding for this axis/button already exists, returns false. + bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback); + bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback); + bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback); + + // Default bindings, used by SDL frontend. + void SetDefaultBindings(); + + void PumpSDLEvents(); + + bool ProcessSDLEvent(const SDL_Event* event); + + void UpdateControllerRumble(); + + // Input monitoring for external access. + struct Hook + { + enum class Type + { + Axis, + Button + }; + + enum class CallbackResult + { + StopMonitoring, + ContinueMonitoring + }; + + using Callback = std::function; + + Type type; + int controller_index; + int button_or_axis_number; + float value; // 0/1 for buttons, -1..1 for axises + }; + void SetHook(Hook::Callback callback); + void ClearHook(); + +private: + System* GetSystem() const; + Controller* GetController(u32 slot) const; + bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value); + + bool OpenGameController(int index); + bool CloseGameController(int index); + void CloseGameControllers(); + bool HandleControllerAxisEvent(const SDL_Event* event); + bool HandleControllerButtonEvent(const SDL_Event* event); + + struct ControllerData + { + void* controller; + void* haptic; + u32 controller_index; + float last_rumble_strength; + + std::array axis_mapping; + std::array button_mapping; + std::array, MAX_NUM_AXISES> axis_button_mapping; + }; + + HostInterface* m_host_interface = nullptr; + + std::map m_controllers; + + std::mutex m_event_intercept_mutex; + Hook::Callback m_event_intercept_callback; + + bool m_sdl_initialized_by_us = false; +}; + +extern SDLControllerInterface g_sdl_controller_interface;