From 853909996ba81fe2d30b4ea8a34dfffaff40da95 Mon Sep 17 00:00:00 2001 From: Ethan Lee Date: Wed, 8 Sep 2021 23:04:46 -0400 Subject: [PATCH] Add support for SDL controller accelerometer/gyro events --- CMakeLists.txt | 2 +- .../configuration/configure_motion_touch.cpp | 78 ++++++++++++++++++- .../configuration/configure_motion_touch.h | 13 ++++ .../configuration/configure_motion_touch.ui | 18 +++++ src/input_common/sdl/sdl_impl.cpp | 78 +++++++++++++++++++ src/input_common/sdl/sdl_impl.h | 2 + 6 files changed, 187 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ff250465..48d4d7584 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,7 +144,7 @@ if (ENABLE_SDL2) if (CITRA_USE_BUNDLED_SDL2) # Detect toolchain and platform if ((MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS 1930) AND ARCHITECTURE_x86_64) - set(SDL2_VER "SDL2-2.0.12") + set(SDL2_VER "SDL2-2.0.16") else() message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRA_USE_BUNDLED_SDL2 and provide your own.") endif() diff --git a/src/citra_qt/configuration/configure_motion_touch.cpp b/src/citra_qt/configuration/configure_motion_touch.cpp index b0398a77d..b0e8bef5b 100644 --- a/src/citra_qt/configuration/configure_motion_touch.cpp +++ b/src/citra_qt/configuration/configure_motion_touch.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "citra_qt/configuration/configure_motion_touch.h" #include "citra_qt/configuration/configure_touch_from_button.h" @@ -70,16 +71,18 @@ void CalibrationConfigurationDialog::UpdateButtonText(QString text) { cancel_button->setText(text); } -const std::array, 2> MotionProviders = { +const std::array, 3> MotionProviders = { {{"motion_emu", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "Mouse (Right Click)")}, - {"cemuhookudp", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "CemuhookUDP")}}}; + {"cemuhookudp", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "CemuhookUDP")}, + {"sdl", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "SDL")}}}; const std::array, 2> TouchProviders = { {{"emu_window", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "Emulator Window")}, {"cemuhookudp", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "CemuhookUDP")}}}; ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent) - : QDialog(parent), ui(std::make_unique()) { + : QDialog(parent), ui(std::make_unique()), + timeout_timer(std::make_unique()), poll_timer(std::make_unique()) { ui->setupUi(this); for (auto [provider, name] : MotionProviders) { ui->motion_provider->addItem(tr(name), QString::fromUtf8(provider)); @@ -95,6 +98,22 @@ ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent) "using-a-controller-or-android-phone-for-motion-or-touch-input'>Learn More")); + timeout_timer->setSingleShot(true); + connect(timeout_timer.get(), &QTimer::timeout, [this]() { SetPollingResult({}, true); }); + + connect(poll_timer.get(), &QTimer::timeout, [this]() { + Common::ParamPackage params; + for (auto& poller : device_pollers) { + params = poller->GetNextInput(); + // We want all the input systems to be in a "polling" state, but we only care about the + // input from SDL. + if (params.Has("engine") && params.Get("engine", "") == "sdl") { + SetPollingResult(params, false); + return; + } + } + }); + SetConfiguration(); UpdateUiDisplay(); ConnectEvents(); @@ -122,6 +141,9 @@ void ConfigureMotionTouch::SetConfiguration() { Settings::values.current_input_profile.touch_from_button_map_index); ui->motion_sensitivity->setValue(motion_param.Get("sensitivity", 0.01f)); + guid = motion_param.Get("guid", "0"); + port = motion_param.Get("port", 0); + min_x = touch_param.Get("min_x", 100); min_y = touch_param.Get("min_y", 50); max_x = touch_param.Get("max_x", 1800); @@ -145,6 +167,14 @@ void ConfigureMotionTouch::UpdateUiDisplay() { ui->motion_sensitivity->setVisible(false); } + if (motion_engine == "sdl") { + ui->motion_controller_label->setVisible(true); + ui->motion_controller_button->setVisible(true); + } else { + ui->motion_controller_label->setVisible(false); + ui->motion_controller_button->setVisible(false); + } + if (touch_engine == "cemuhookudp") { ui->touch_calibration->setVisible(true); ui->touch_calibration_config->setVisible(true); @@ -172,6 +202,30 @@ void ConfigureMotionTouch::ConnectEvents() { connect(ui->touch_provider, static_cast(&QComboBox::currentIndexChanged), this, [this]([[maybe_unused]] int index) { UpdateUiDisplay(); }); + connect(ui->motion_controller_button, &QPushButton::clicked, [=]() { + if (QMessageBox::information(this, tr("Information"), + tr("After pressing OK, press a button on the controller whose " + "motion you want to track."), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + ui->motion_controller_button->setText(tr("[press button]")); + ui->motion_controller_button->setFocus(); + + input_setter = [=](const Common::ParamPackage& params) { + guid = params.Get("guid", "0"); + port = params.Get("port", 0); + }; + + device_pollers = + InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button); + + for (auto& poller : device_pollers) { + poller->Start(); + } + + timeout_timer->start(5000); // Cancel after 5 seconds + poll_timer->start(200); // Check for new inputs every 200ms + } + }); connect(ui->udp_test, &QPushButton::clicked, this, &ConfigureMotionTouch::OnCemuhookUDPTest); connect(ui->touch_calibration_config, &QPushButton::clicked, this, &ConfigureMotionTouch::OnConfigureTouchCalibration); @@ -183,6 +237,21 @@ void ConfigureMotionTouch::ConnectEvents() { }); } +void ConfigureMotionTouch::SetPollingResult(const Common::ParamPackage& params, bool abort) { + timeout_timer->stop(); + poll_timer->stop(); + for (auto& poller : device_pollers) { + poller->Stop(); + } + + if (!abort && input_setter) { + (*input_setter)(params); + } + + ui->motion_controller_button->setText(tr("Configure")); + input_setter.reset(); +} + void ConfigureMotionTouch::OnCemuhookUDPTest() { ui->udp_test->setEnabled(false); ui->udp_test->setText(tr("Testing")); @@ -285,6 +354,9 @@ void ConfigureMotionTouch::ApplyConfiguration() { if (motion_engine == "motion_emu") { motion_param.Set("sensitivity", static_cast(ui->motion_sensitivity->value())); + } else if (motion_engine == "sdl") { + motion_param.Set("guid", guid); + motion_param.Set("port", port); } if (touch_engine == "cemuhookudp") { diff --git a/src/citra_qt/configuration/configure_motion_touch.h b/src/citra_qt/configuration/configure_motion_touch.h index 11485f2ec..247f02df5 100644 --- a/src/citra_qt/configuration/configure_motion_touch.h +++ b/src/citra_qt/configuration/configure_motion_touch.h @@ -8,11 +8,13 @@ #include #include "common/param_package.h" #include "core/settings.h" +#include "input_common/main.h" #include "input_common/udp/udp.h" class QVBoxLayout; class QLabel; class QPushButton; +class QTimer; namespace Ui { class ConfigureMotionTouch; @@ -63,10 +65,21 @@ private: void SetConfiguration(); void UpdateUiDisplay(); void ConnectEvents(); + void SetPollingResult(const Common::ParamPackage& params, bool abort); bool CanCloseDialog(); std::unique_ptr ui; + // Used for SDL input polling + std::string guid; + int port; + std::unique_ptr timeout_timer; + std::unique_ptr poll_timer; + std::vector> device_pollers; + + /// This will be the the setting function when an input is awaiting configuration. + std::optional> input_setter; + // Coordinate system of the CemuhookUDP touch provider int min_x, min_y, max_x, max_y; diff --git a/src/citra_qt/configuration/configure_motion_touch.ui b/src/citra_qt/configuration/configure_motion_touch.ui index 602cf8cd8..292b5c5bd 100644 --- a/src/citra_qt/configuration/configure_motion_touch.ui +++ b/src/citra_qt/configuration/configure_motion_touch.ui @@ -67,6 +67,24 @@ + + + + + + Controller: + + + + + + + Configure + + + + + diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index 8f0101b49..998cafdf1 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -173,6 +173,24 @@ public: std::lock_guard lock{mutex}; return (state.hats.at(hat) & direction) != 0; } + + void SetAccel(const float x, const float y, const float z) { + std::lock_guard lock{mutex}; + state.accel.x = x; + state.accel.y = y; + state.accel.z = z; + } + void SetGyro(const float pitch, const float yaw, const float roll) { + std::lock_guard lock{mutex}; + state.gyro.x = pitch; + state.gyro.y = yaw; + state.gyro.z = roll; + } + std::tuple, Common::Vec3> GetMotion() const { + std::lock_guard lock{mutex}; + return std::make_tuple(state.accel, state.gyro); + } + /** * The guid of the joystick */ @@ -204,6 +222,8 @@ private: std::unordered_map buttons; std::unordered_map axes; std::unordered_map hats; + Common::Vec3 accel; + Common::Vec3 gyro; } state; std::string guid; int port; @@ -473,6 +493,14 @@ void SDLState::InitGameController(int controller_index) { LOG_WARNING(Input, "failed to open joystick {} as controller", controller_index); return; } +#if SDL_VERSION_ATLEAST(2, 0, 14) + if (SDL_GameControllerHasSensor(sdl_controller, SDL_SENSOR_ACCEL)) { + SDL_GameControllerSetSensorEnabled(sdl_controller, SDL_SENSOR_ACCEL, SDL_TRUE); + } + if (SDL_GameControllerHasSensor(sdl_controller, SDL_SENSOR_GYRO)) { + SDL_GameControllerSetSensorEnabled(sdl_controller, SDL_SENSOR_GYRO, SDL_TRUE); + } +#endif const std::string guid = GetGUID(SDL_GameControllerGetJoystick(sdl_controller)); LOG_INFO(Input, "opened joystick {} as controller", controller_index); @@ -557,6 +585,25 @@ void SDLState::HandleGameControllerEvent(const SDL_Event& event) { } break; } +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: { + if (auto joystick = GetSDLJoystickBySDLID(event.csensor.which)) { + switch (event.csensor.sensor) { + case SDL_SENSOR_ACCEL: + joystick->SetAccel(event.csensor.data[0] / SDL_STANDARD_GRAVITY, + -event.csensor.data[1] / SDL_STANDARD_GRAVITY, + event.csensor.data[2] / SDL_STANDARD_GRAVITY); + break; + case SDL_SENSOR_GYRO: + joystick->SetGyro(-event.csensor.data[0] * (180.0f / Common::PI), + event.csensor.data[1] * (180.0f / Common::PI), + -event.csensor.data[2] * (180.0f / Common::PI)); + break; + } + } + break; + } +#endif case SDL_JOYDEVICEREMOVED: LOG_DEBUG(Input, "Joystick removed with Instance_ID {}", event.jdevice.which); CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which)); @@ -658,6 +705,18 @@ private: const float deadzone; }; +class SDLMotion final : public Input::MotionDevice { +public: + explicit SDLMotion(std::shared_ptr joystick_) : joystick(std::move(joystick_)) {} + + std::tuple, Common::Vec3> GetStatus() const override { + return joystick->GetMotion(); + } + +private: + std::shared_ptr joystick; +}; + /// A button device factory that creates button devices from SDL joystick class SDLButtonFactory final : public Input::Factory { public: @@ -764,10 +823,28 @@ private: SDLState& state; }; +class SDLMotionFactory final : public Input::Factory { +public: + explicit SDLMotionFactory(SDLState& state_) : state(state_) {} + + std::unique_ptr Create(const Common::ParamPackage& params) override { + const std::string guid = params.Get("guid", "0"); + const int port = params.Get("port", 0); + + auto joystick = state.GetSDLJoystickByGUID(guid, port); + + return std::make_unique(joystick); + } + +private: + SDLState& state; +}; + SDLState::SDLState() { using namespace Input; RegisterFactory("sdl", std::make_shared(*this)); RegisterFactory("sdl", std::make_shared(*this)); + RegisterFactory("sdl", std::make_shared(*this)); // If the frontend is going to manage the event loop, then we dont start one here start_thread = !SDL_WasInit(SDL_INIT_GAMECONTROLLER); @@ -812,6 +889,7 @@ SDLState::~SDLState() { using namespace Input; UnregisterFactory("sdl"); UnregisterFactory("sdl"); + UnregisterFactory("sdl"); CloseJoysticks(); CloseGameControllers(); diff --git a/src/input_common/sdl/sdl_impl.h b/src/input_common/sdl/sdl_impl.h index 05f627d0f..0be659cd0 100644 --- a/src/input_common/sdl/sdl_impl.h +++ b/src/input_common/sdl/sdl_impl.h @@ -22,6 +22,7 @@ class SDLJoystick; class SDLGameController; class SDLButtonFactory; class SDLAnalogFactory; +class SDLMotionFactory; class SDLState : public State { public: @@ -73,6 +74,7 @@ private: std::shared_ptr button_factory; std::shared_ptr analog_factory; + std::shared_ptr motion_factory; bool start_thread = false; std::atomic initialized = false;