From b2e04bf8f30455aabd5ff87d192744c73092b545 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Sat, 14 Jan 2023 17:07:51 +0100 Subject: [PATCH] Stash "DONE" # Conflicts: # src/citra_qt/bootmanager.cpp # src/citra_qt/configuration/configure_motion_touch.ui # src/citra_qt/main.cpp # src/common/settings.cpp # src/core/CMakeLists.txt # src/core/core.h --- src/citra_qt/bootmanager.cpp | 124 +++-- src/citra_qt/bootmanager.h | 13 +- src/citra_qt/configuration/config.cpp | 15 +- .../configuration/configure_dialog.cpp | 5 +- src/citra_qt/configuration/configure_dialog.h | 5 + .../configuration/configure_motion_touch.cpp | 110 ++-- .../configuration/configure_motion_touch.h | 21 +- .../configuration/configure_motion_touch.ui | 29 +- .../configure_touch_from_button.cpp | 43 +- .../configure_touch_from_button.h | 21 +- src/citra_qt/main.cpp | 35 +- src/citra_qt/main.h | 7 + src/common/quaternion.h | 30 + src/common/settings.cpp | 3 + src/common/settings.h | 1 + src/core/CMakeLists.txt | 18 +- src/core/core.cpp | 8 + src/core/core.h | 9 + src/core/frontend/emu_window.cpp | 117 +--- src/core/frontend/emu_window.h | 29 +- src/core/hid/emulated_console.cpp | 302 ++++++++++ src/core/hid/emulated_console.h | 197 +++++++ src/core/hid/emulated_controller.cpp | 518 ++++++++++++++++++ src/core/hid/emulated_controller.h | 226 ++++++++ src/core/hid/hid_core.cpp | 51 ++ src/core/hid/hid_core.h | 45 ++ src/core/hid/hid_types.h | 40 ++ src/core/hid/input_converter.cpp | 340 ++++++++++++ src/core/hid/input_converter.h | 85 +++ src/core/hid/motion_input.cpp | 284 ++++++++++ src/core/hid/motion_input.h | 87 +++ src/core/hle/service/ir/extra_hid.h | 13 +- src/core/hle/service/ir/ir_rst.h | 9 +- src/core/hle/service/ir/ir_user.cpp | 4 - src/core/hle/service/ir/ir_user.h | 2 - src/input_common/drivers/udp_client.h | 3 - src/input_common/input_mapping.cpp | 3 +- 37 files changed, 2508 insertions(+), 344 deletions(-) create mode 100644 src/core/hid/emulated_console.cpp create mode 100644 src/core/hid/emulated_console.h create mode 100644 src/core/hid/emulated_controller.cpp create mode 100644 src/core/hid/emulated_controller.h create mode 100644 src/core/hid/hid_core.cpp create mode 100644 src/core/hid/hid_core.h create mode 100644 src/core/hid/hid_types.h create mode 100644 src/core/hid/input_converter.cpp create mode 100644 src/core/hid/input_converter.h create mode 100644 src/core/hid/motion_input.cpp create mode 100644 src/core/hid/motion_input.h diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index baabf8641..434fb88ed 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -20,9 +20,11 @@ #include "core/core.h" #include "core/frontend/framebuffer_layout.h" #include "core/perf_stats.h" -#include "input_common/keyboard.h" +#include "input_common/drivers/keyboard.h" +#include "input_common/drivers/mouse.h" +#include "input_common/drivers/touch_screen.h" #include "input_common/main.h" -#include "input_common/motion_emu.h" +#include "network/network.h" #include "video_core/custom_textures/custom_tex_manager.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" @@ -402,8 +404,11 @@ static Frontend::EmuWindow::WindowSystemInfo GetWindowSystemInfo(QWindow* window std::unique_ptr GRenderWindow::main_context; -GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread, bool is_secondary_) - : QWidget(parent_), EmuWindow(is_secondary_), emu_thread(emu_thread) { +GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread, + std::shared_ptr input_subsystem_, + bool is_secondary_) + : QWidget(parent_), EmuWindow(is_secondary_), + emu_thread(emu_thread), input_subsystem{std::move(input_subsystem_)} { setWindowTitle(QStringLiteral("Citra %1 | %2-%3") .arg(QString::fromUtf8(Common::g_build_name), @@ -493,78 +498,107 @@ void GRenderWindow::closeEvent(QCloseEvent* event) { } void GRenderWindow::keyPressEvent(QKeyEvent* event) { - InputCommon::GetKeyboard()->PressKey(event->key()); + if (!event->isAutoRepeat()) { + input_subsystem->GetKeyboard()->PressKey(event->key()); + } } void GRenderWindow::keyReleaseEvent(QKeyEvent* event) { - InputCommon::GetKeyboard()->ReleaseKey(event->key()); + if (!event->isAutoRepeat()) { + input_subsystem->GetKeyboard()->ReleaseKey(event->key()); + } +} + +InputCommon::MouseButton GRenderWindow::QtButtonToMouseButton(Qt::MouseButton button) { + switch (button) { + case Qt::LeftButton: + return InputCommon::MouseButton::Left; + case Qt::RightButton: + return InputCommon::MouseButton::Right; + case Qt::MiddleButton: + return InputCommon::MouseButton::Wheel; + case Qt::BackButton: + return InputCommon::MouseButton::Backward; + case Qt::ForwardButton: + return InputCommon::MouseButton::Forward; + case Qt::TaskButton: + return InputCommon::MouseButton::Task; + default: + return InputCommon::MouseButton::Extra; + } } void GRenderWindow::mousePressEvent(QMouseEvent* event) { + // Touch input is handled in TouchBeginEvent if (event->source() == Qt::MouseEventSynthesizedBySystem) { - return; // touch input is handled in TouchBeginEvent + return; } + // Qt sometimes returns the parent coordinates. To avoid this we read the global mouse + // coordinates and map them to the current render area + const auto pos = mapFromGlobal(QCursor::pos()); + const auto [x, y] = ScaleTouch(pos); + const auto [touch_x, touch_y] = MapToTouchScreen(x, y); + const auto button = QtButtonToMouseButton(event->button()); + input_subsystem->GetMouse()->PressButton(x, y, touch_x, touch_y, button); - auto pos = event->pos(); - if (event->button() == Qt::LeftButton) { - const auto [x, y] = ScaleTouch(pos); - this->TouchPressed(x, y); - } else if (event->button() == Qt::RightButton) { - InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y()); - } emit MouseActivity(); } void GRenderWindow::mouseMoveEvent(QMouseEvent* event) { + // Touch input is handled in TouchUpdateEvent if (event->source() == Qt::MouseEventSynthesizedBySystem) { - return; // touch input is handled in TouchUpdateEvent + return; } - - auto pos = event->pos(); + // Qt sometimes returns the parent coordinates. To avoid this we read the global mouse + // coordinates and map them to the current render area + const auto pos = mapFromGlobal(QCursor::pos()); const auto [x, y] = ScaleTouch(pos); - this->TouchMoved(x, y); - InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y()); + const auto [touch_x, touch_y] = MapToTouchScreen(x, y); + const int center_x = width() / 2; + const int center_y = height() / 2; + input_subsystem->GetMouse()->MouseMove(x, y, touch_x, touch_y, center_x, center_y); + emit MouseActivity(); } void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) { + // Touch input is handled in TouchEndEvent if (event->source() == Qt::MouseEventSynthesizedBySystem) { - return; // touch input is handled in TouchEndEvent + return; } - if (event->button() == Qt::LeftButton) - this->TouchReleased(); - else if (event->button() == Qt::RightButton) - InputCommon::GetMotionEmu()->EndTilt(); - emit MouseActivity(); + const auto button = QtButtonToMouseButton(event->button()); + input_subsystem->GetMouse()->ReleaseButton(button); +} + +void GRenderWindow::wheelEvent(QWheelEvent* event) { + const int x = event->angleDelta().x(); + const int y = event->angleDelta().y(); + input_subsystem->GetMouse()->MouseWheelChange(x, y); } void GRenderWindow::TouchBeginEvent(const QTouchEvent* event) { - // TouchBegin always has exactly one touch point, so take the .first() - const auto [x, y] = ScaleTouch(event->points().first().position()); - this->TouchPressed(x, y); + QList touch_points = event->touchPoints(); + for (const auto& touch_point : touch_points) { + const auto [x, y] = ScaleTouch(touch_point.pos()); + const auto [touch_x, touch_y] = MapToTouchScreen(x, y); + input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, touch_point.id()); + } } void GRenderWindow::TouchUpdateEvent(const QTouchEvent* event) { - QPointF pos; - int active_points = 0; - - // average all active touch points - for (const auto& tp : event->points()) { - if (tp.state() & (Qt::TouchPointPressed | Qt::TouchPointMoved | Qt::TouchPointStationary)) { - active_points++; - pos += tp.position(); - } + QList touch_points = event->touchPoints(); + input_subsystem->GetTouchScreen()->ClearActiveFlag(); + for (const auto& touch_point : touch_points) { + const auto [x, y] = ScaleTouch(touch_point.pos()); + const auto [touch_x, touch_y] = MapToTouchScreen(x, y); + input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, touch_point.id()); } - - pos /= active_points; - - const auto [x, y] = ScaleTouch(pos); - this->TouchMoved(x, y); + input_subsystem->GetTouchScreen()->ReleaseInactiveTouch(); } void GRenderWindow::TouchEndEvent() { - this->TouchReleased(); + input_subsystem->GetTouchScreen()->ReleaseAllTouch(); } bool GRenderWindow::event(QEvent* event) { @@ -588,7 +622,9 @@ bool GRenderWindow::event(QEvent* event) { void GRenderWindow::focusOutEvent(QFocusEvent* event) { QWidget::focusOutEvent(event); - InputCommon::GetKeyboard()->ReleaseAllKeys(); + input_subsystem->GetKeyboard()->ReleaseAllKeys(); + input_subsystem->GetMouse()->ReleaseAllButtons(); + input_subsystem->GetTouchScreen()->ReleaseAllTouch(); has_focus = false; } diff --git a/src/citra_qt/bootmanager.h b/src/citra_qt/bootmanager.h index f0019886d..8f6b3afd4 100644 --- a/src/citra_qt/bootmanager.h +++ b/src/citra_qt/bootmanager.h @@ -18,6 +18,11 @@ class QTouchEvent; class GRenderWindow; +namespace InputCommon { +class InputSubsystem; +enum class MouseButton; +} // namespace InputCommon + namespace VideoCore { enum class LoadCallbackStage; } @@ -112,7 +117,8 @@ class GRenderWindow : public QWidget, public Frontend::EmuWindow { Q_OBJECT public: - GRenderWindow(QWidget* parent, EmuThread* emu_thread, bool is_secondary); + GRenderWindow(QWidget* parent, EmuThread* emu_thread, + std::shared_ptr input_subsystem_, bool is_secondary); ~GRenderWindow() override; // EmuWindow implementation. @@ -135,9 +141,13 @@ public: void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; + /// Converts a Qt mouse button into MouseInput mouse button + static InputCommon::MouseButton QtButtonToMouseButton(Qt::MouseButton button); + void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; bool event(QEvent* event) override; @@ -188,6 +198,7 @@ private: QWidget* child_widget = nullptr; EmuThread* emu_thread; + std::shared_ptr input_subsystem; /// Main context that will be shared with all other contexts that are requested. /// If this is used in a shared context setting, then this should not be used directly, but diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index fcb268d32..52364fb84 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -10,8 +10,8 @@ #include "common/file_util.h" #include "common/settings.h" #include "core/hle/service/service.h" +#include "input_common/drivers/udp_client.h" #include "input_common/main.h" -#include "input_common/udp/client.h" #include "network/network.h" #include "network/network_settings.h" @@ -396,13 +396,11 @@ void Config::ReadControlValues() { profile.touch_from_button_map_index = std::clamp(profile.touch_from_button_map_index, 0, num_touch_from_button_maps - 1); profile.udp_input_address = - ReadSetting(QStringLiteral("udp_input_address"), - QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR)) + ReadSetting(QStringLiteral("udp_input_address"), QString::fromUtf8("127.0.0.1")) .toString() .toStdString(); - profile.udp_input_port = static_cast( - ReadSetting(QStringLiteral("udp_input_port"), InputCommon::CemuhookUDP::DEFAULT_PORT) - .toInt()); + profile.udp_input_port = + static_cast(ReadSetting(QStringLiteral("udp_input_port"), 26760).toInt()); profile.udp_pad_index = static_cast(ReadSetting(QStringLiteral("udp_pad_index"), 0).toUInt()); Settings::values.input_profiles.emplace_back(std::move(profile)); @@ -918,9 +916,8 @@ void Config::SaveControlValues() { 0); WriteSetting(QStringLiteral("udp_input_address"), QString::fromStdString(profile.udp_input_address), - QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR)); - WriteSetting(QStringLiteral("udp_input_port"), profile.udp_input_port, - InputCommon::CemuhookUDP::DEFAULT_PORT); + QString::fromUtf8("127.0.0.1")); + WriteSetting(QStringLiteral("udp_input_port"), profile.udp_input_port, 26760); WriteSetting(QStringLiteral("udp_pad_index"), profile.udp_pad_index, 0); } qt_config->endArray(); diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp index 1b00ddbe3..47145d1f9 100644 --- a/src/citra_qt/configuration/configure_dialog.cpp +++ b/src/citra_qt/configuration/configure_dialog.cpp @@ -10,11 +10,14 @@ #include "common/settings.h" #include "ui_configure.h" -ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry, bool enable_web_config) +ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry, + InputCommon::InputSubsystem* input_subsystem, + bool enable_web_config) : QDialog(parent), ui(std::make_unique()), registry(registry) { Settings::SetConfiguringGlobal(true); ui->setupUi(this); + ui->inputTab->Initialize(input_subsystem); ui->hotkeysTab->Populate(registry); ui->webTab->SetWebServiceConfigEnabled(enable_web_config); diff --git a/src/citra_qt/configuration/configure_dialog.h b/src/citra_qt/configuration/configure_dialog.h index 3631ab20a..7ad01b1b9 100644 --- a/src/citra_qt/configuration/configure_dialog.h +++ b/src/citra_qt/configuration/configure_dialog.h @@ -13,11 +13,16 @@ namespace Ui { class ConfigureDialog; } +namespace InputCommon { +class InputSubsystem; +} + class ConfigureDialog : public QDialog { Q_OBJECT public: explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry, + InputCommon::InputSubsystem* input_subsystem, bool enable_web_config = true); ~ConfigureDialog() override; diff --git a/src/citra_qt/configuration/configure_motion_touch.cpp b/src/citra_qt/configuration/configure_motion_touch.cpp index 3bfbba71d..d02c825e3 100644 --- a/src/citra_qt/configuration/configure_motion_touch.cpp +++ b/src/citra_qt/configuration/configure_motion_touch.cpp @@ -7,11 +7,14 @@ #include #include #include +#include #include #include #include "citra_qt/configuration/configure_motion_touch.h" #include "citra_qt/configuration/configure_touch_from_button.h" #include "common/logging/log.h" +#include "input_common/drivers/udp_client.h" +#include "input_common/helpers/udp_protocol.h" #include "input_common/main.h" #include "ui_configure_motion_touch.h" @@ -34,26 +37,27 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent, using namespace InputCommon::CemuhookUDP; job = std::make_unique( - host, port, pad_index, client_id, + host, port, [this](CalibrationConfigurationJob::Status status) { - QString text; - switch (status) { - case CalibrationConfigurationJob::Status::Ready: - text = tr("Touch the top left corner
of your touchpad."); - break; - case CalibrationConfigurationJob::Status::Stage1Completed: - text = tr("Now touch the bottom right corner
of your touchpad."); - break; - case CalibrationConfigurationJob::Status::Completed: - text = tr("Configuration completed!"); - break; - default: - LOG_ERROR(Frontend, "Unknown calibration status {}", status); - break; - } - QMetaObject::invokeMethod(this, "UpdateLabelText", Q_ARG(QString, text)); + QMetaObject::invokeMethod(this, [status, this] { + QString text; + switch (status) { + case CalibrationConfigurationJob::Status::Ready: + text = tr("Touch the top left corner
of your touchpad."); + break; + case CalibrationConfigurationJob::Status::Stage1Completed: + text = tr("Now touch the bottom right corner
of your touchpad."); + break; + case CalibrationConfigurationJob::Status::Completed: + text = tr("Configuration completed!"); + break; + default: + break; + } + UpdateLabelText(text); + }); if (status == CalibrationConfigurationJob::Status::Completed) { - QMetaObject::invokeMethod(this, "UpdateButtonText", Q_ARG(QString, tr("OK"))); + QMetaObject::invokeMethod(this, [this] { UpdateButtonText(tr("OK")); }); } }, [this](u16 min_x_, u16 min_y_, u16 max_x_, u16 max_y_) { @@ -86,10 +90,13 @@ constexpr std::array, 2> TouchProviders = {{ {"cemuhookudp", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "CemuhookUDP")}, }}; -ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent) - : QDialog(parent), ui(std::make_unique()), - timeout_timer(std::make_unique()), poll_timer(std::make_unique()) { +ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent, + InputCommon::InputSubsystem* input_subsystem_) + : QDialog(parent), input_subsystem{input_subsystem_}, + ui(std::make_unique()), timeout_timer(std::make_unique()), + poll_timer(std::make_unique()) { ui->setupUi(this); + for (const auto& [provider, name] : MotionProviders) { ui->motion_provider->addItem(tr(name), QString::fromUtf8(provider)); } @@ -104,22 +111,7 @@ 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, [this]() { SetPollingResult({}, true); }); - - connect(poll_timer.get(), &QTimer::timeout, this, [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; - } - } - }); - + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); SetConfiguration(); UpdateUiDisplay(); ConnectEvents(); @@ -205,30 +197,6 @@ void ConfigureMotionTouch::ConnectEvents() { [this]([[maybe_unused]] int index) { UpdateUiDisplay(); }); connect(ui->touch_provider, qOverload(&QComboBox::currentIndexChanged), this, [this]([[maybe_unused]] int index) { UpdateUiDisplay(); }); - connect(ui->motion_controller_button, &QPushButton::clicked, this, [this]() { - 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 = [this](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); @@ -243,28 +211,12 @@ 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")); udp_test_in_progress = true; InputCommon::CemuhookUDP::TestCommunication( ui->udp_server->text().toStdString(), static_cast(ui->udp_port->text().toInt()), - static_cast(ui->udp_pad_index->currentIndex()), 24872, [this] { LOG_INFO(Frontend, "UDP input test success"); QMetaObject::invokeMethod(this, "ShowUDPTestResult", Q_ARG(bool, true)); @@ -322,7 +274,7 @@ void ConfigureMotionTouch::ShowUDPTestResult(bool result) { } void ConfigureMotionTouch::OnConfigureTouchFromButton() { - ConfigureTouchFromButton dialog{this, touch_from_button_maps, + ConfigureTouchFromButton dialog{this, touch_from_button_maps, input_subsystem, ui->touch_from_button_map->currentIndex()}; if (dialog.exec() != QDialog::Accepted) { return; @@ -387,7 +339,7 @@ void ConfigureMotionTouch::ApplyConfiguration() { Settings::values.current_input_profile.udp_pad_index = static_cast(ui->udp_pad_index->currentIndex()); Settings::SaveProfile(Settings::values.current_input_profile_index); - InputCommon::ReloadInputDevices(); + input_subsystem->ReloadInputDevices(); accept(); } diff --git a/src/citra_qt/configuration/configure_motion_touch.h b/src/citra_qt/configuration/configure_motion_touch.h index 3b10f752a..30f02782f 100644 --- a/src/citra_qt/configuration/configure_motion_touch.h +++ b/src/citra_qt/configuration/configure_motion_touch.h @@ -6,15 +6,21 @@ #include #include -#include "common/param_package.h" #include "common/settings.h" -#include "input_common/main.h" -#include "input_common/udp/udp.h" class QVBoxLayout; class QLabel; class QPushButton; class QTimer; +class QStringListModel; + +namespace InputCommon { +class InputSubsystem; +} + +namespace InputCommon::CemuhookUDP { +class CalibrationConfigurationJob; +} namespace Ui { class ConfigureMotionTouch; @@ -51,7 +57,7 @@ class ConfigureMotionTouch : public QDialog { Q_OBJECT public: - explicit ConfigureMotionTouch(QWidget* parent = nullptr); + explicit ConfigureMotionTouch(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_); ~ConfigureMotionTouch() override; public slots: @@ -68,9 +74,10 @@ private: void SetConfiguration(); void UpdateUiDisplay(); void ConnectEvents(); - void SetPollingResult(const Common::ParamPackage& params, bool abort); bool CanCloseDialog(); + InputCommon::InputSubsystem* input_subsystem; + std::unique_ptr ui; // Used for SDL input polling @@ -78,10 +85,6 @@ private: 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{}; diff --git a/src/citra_qt/configuration/configure_motion_touch.ui b/src/citra_qt/configuration/configure_motion_touch.ui index 7f4a5c36a..f1aa1b543 100644 --- a/src/citra_qt/configuration/configure_motion_touch.ui +++ b/src/citra_qt/configuration/configure_motion_touch.ui @@ -2,17 +2,17 @@ ConfigureMotionTouch - - Configure Motion / Touch - 0 0 500 - 450 + 580 + + Configure Motion / Touch + @@ -324,4 +324,25 @@ + + + buttonBox + accepted() + ConfigureMotionTouch + ApplyConfiguration() + + + 220 + 380 + + + 220 + 200 + + + + + + ApplyConfiguration() + diff --git a/src/citra_qt/configuration/configure_touch_from_button.cpp b/src/citra_qt/configuration/configure_touch_from_button.cpp index 76f2a77bc..b9f88efe2 100644 --- a/src/citra_qt/configuration/configure_touch_from_button.cpp +++ b/src/citra_qt/configuration/configure_touch_from_button.cpp @@ -12,7 +12,9 @@ #include "citra_qt/configuration/configure_touch_from_button.h" #include "citra_qt/configuration/configure_touch_widget.h" #include "common/param_package.h" +#include "common/settings.h" #include "core/3ds.h" +#include "core/frontend/framebuffer_layout.h" #include "input_common/main.h" #include "ui_configure_touch_from_button.h" @@ -68,11 +70,11 @@ static QString ButtonToText(const Common::ParamPackage& param) { } ConfigureTouchFromButton::ConfigureTouchFromButton( - QWidget* parent, const std::vector& touch_maps, - const int default_index) - : QDialog(parent), ui(std::make_unique()), touch_maps(touch_maps), - selected_index(default_index), timeout_timer(std::make_unique()), - poll_timer(std::make_unique()) { + QWidget* parent, const std::vector& touch_maps_, + InputCommon::InputSubsystem* input_subsystem_, const int default_index) + : QDialog(parent), ui(std::make_unique()), + touch_maps{touch_maps_}, input_subsystem{input_subsystem_}, selected_index{default_index}, + timeout_timer(std::make_unique()), poll_timer(std::make_unique()) { ui->setupUi(this); binding_list_model = new QStandardItemModel(0, 3, this); binding_list_model->setHorizontalHeaderLabels( @@ -162,13 +164,10 @@ void ConfigureTouchFromButton::ConnectEvents() { 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(); - if (params.Has("engine")) { - SetPollingResult(params, false); - return; - } + const auto& params = input_subsystem->GetNextInput(); + if (params.Has("engine")) { + SetPollingResult(params, false); + return; } }); } @@ -229,6 +228,9 @@ void ConfigureTouchFromButton::RenameMapping() { } void ConfigureTouchFromButton::GetButtonInput(const int row_index, const bool is_new) { + if (timeout_timer->isActive()) { + return; + } binding_list_model->item(row_index, 0)->setText(tr("[press key]")); input_setter = [this, row_index, is_new](const Common::ParamPackage& params, @@ -247,11 +249,7 @@ void ConfigureTouchFromButton::GetButtonInput(const int row_index, const bool is } }; - device_pollers = InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button); - - for (auto& poller : device_pollers) { - poller->Start(); - } + input_subsystem->BeginMapping(InputCommon::Polling::InputType::Button); grabKeyboard(); grabMouse(); @@ -363,14 +361,14 @@ void ConfigureTouchFromButton::SetCoordinates(const int dot_id, const QPoint& po void ConfigureTouchFromButton::SetPollingResult(const Common::ParamPackage& params, const bool cancel) { + timeout_timer->stop(); + poll_timer->stop(); + input_subsystem->StopMapping(); + releaseKeyboard(); releaseMouse(); qApp->restoreOverrideCursor(); - timeout_timer->stop(); - poll_timer->stop(); - for (auto& poller : device_pollers) { - poller->Stop(); - } + if (input_setter) { (*input_setter)(params, cancel); input_setter.reset(); @@ -597,7 +595,6 @@ std::optional TouchScreenPreview::MapToDeviceCoords(const int screen_x, (Core::kScreenBottomHeight - 1) / (contentsRect().height() - 1); if (t_x >= 0.5f && t_x < Core::kScreenBottomWidth && t_y >= 0.5f && t_y < Core::kScreenBottomHeight) { - return QPoint{static_cast(t_x), static_cast(t_y)}; } return std::nullopt; diff --git a/src/citra_qt/configuration/configure_touch_from_button.h b/src/citra_qt/configuration/configure_touch_from_button.h index bd95a4945..5a1416d00 100644 --- a/src/citra_qt/configuration/configure_touch_from_button.h +++ b/src/citra_qt/configuration/configure_touch_from_button.h @@ -1,6 +1,5 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2020 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -9,7 +8,6 @@ #include #include #include -#include "common/settings.h" class QItemSelection; class QModelIndex; @@ -22,10 +20,12 @@ class ParamPackage; } namespace InputCommon { -namespace Polling { -class DevicePoller; +class InputSubsystem; +} + +namespace Settings { +struct TouchFromButtonMap; } -} // namespace InputCommon namespace Ui { class ConfigureTouchFromButton; @@ -36,7 +36,8 @@ class ConfigureTouchFromButton : public QDialog { public: explicit ConfigureTouchFromButton(QWidget* parent, - const std::vector& touch_maps, + const std::vector& touch_maps_, + InputCommon::InputSubsystem* input_subsystem_, int default_index = 0); ~ConfigureTouchFromButton() override; @@ -72,13 +73,13 @@ private: void SaveCurrentMapping(); std::unique_ptr ui; - QStandardItemModel* binding_list_model; std::vector touch_maps; + QStandardItemModel* binding_list_model; + InputCommon::InputSubsystem* input_subsystem; int selected_index; std::unique_ptr timeout_timer; std::unique_ptr poll_timer; - std::vector> device_pollers; std::optional> input_setter; static constexpr int DataRoleDot = Qt::ItemDataRole::UserRole + 2; diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 6a99129c6..5956fb0e9 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -123,6 +123,7 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; #endif constexpr int default_mouse_timeout = 2500; +constexpr int default_input_update_timeout = 1; /** * "Callouts" are one-time instructional messages shown to the user. In the config settings, there @@ -189,8 +190,9 @@ static QString PrettyProductName() { } GMainWindow::GMainWindow() - : ui{std::make_unique()}, config{std::make_unique()}, emu_thread{ - nullptr} { + : ui{std::make_unique()}, + input_subsystem{std::make_shared()}, + config{std::make_unique()}, emu_thread{nullptr} { InitializeLogging(); Debugger::ToggleConsole(); Settings::LogSettings(); @@ -236,6 +238,8 @@ GMainWindow::GMainWindow() ConnectMenuEvents(); ConnectWidgetEvents(); + Core::System::GetInstance().HIDCore().ReloadInputDevices(); + LOG_INFO(Frontend, "Citra Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); #if CITRA_ARCH(x86_64) @@ -273,6 +277,10 @@ GMainWindow::GMainWindow() connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); + update_input_timer.setInterval(default_input_update_timeout); + connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers); + update_input_timer.start(); + if (UISettings::values.check_for_update_on_start) { CheckForUpdates(); } @@ -296,8 +304,9 @@ void GMainWindow::InitializeWidgets() { #ifdef CITRA_ENABLE_COMPATIBILITY_REPORTING ui->action_Report_Compatibility->setVisible(true); #endif - render_window = new GRenderWindow(this, emu_thread.get(), false); - secondary_window = new GRenderWindow(this, emu_thread.get(), true); + input_subsystem->Initialize(); + render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, false); + secondary_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, true); render_window->hide(); secondary_window->hide(); secondary_window->setParent(nullptr); @@ -320,7 +329,6 @@ void GMainWindow::InitializeWidgets() { } }); - InputCommon::Init(); multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room, ui->action_Show_Room); multiplayer_state->setVisible(false); @@ -1142,6 +1150,7 @@ void GMainWindow::BootGame(const QString& filename) { const std::string config_file_name = title_id == 0 ? name : fmt::format("{:016X}", title_id); Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + Core::System::GetInstance().HIDCore().ReloadInputDevices(); Settings::Apply(); LOG_INFO(Frontend, "Using per game config file for title id {}", config_file_name); @@ -1772,6 +1781,7 @@ void GMainWindow::OnPauseContinueGame() { void GMainWindow::OnStopGame() { ShutdownGame(); Settings::RestoreGlobalState(false); + Core::System::GetInstance().HIDCore().ReloadInputDevices(); } void GMainWindow::OnLoadComplete() { @@ -1958,7 +1968,7 @@ void GMainWindow::OnLoadState() { void GMainWindow::OnConfigure() { game_list->SetDirectoryWatcherEnabled(false); Settings::SetConfiguringGlobal(true); - ConfigureDialog configureDialog(this, hotkey_registry, + ConfigureDialog configureDialog(this, hotkey_registry, input_subsystem.get(), !multiplayer_state->IsHostingPublicRoom()); connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, &GMainWindow::OnLanguageChanged); @@ -2294,6 +2304,13 @@ void GMainWindow::UpdateBootHomeMenuState() { } } +void GMainWindow::UpdateInputDrivers() { + if (!input_subsystem) { + return; + } + input_subsystem->PumpEvents(); +} + void GMainWindow::HideMouseCursor() { if (emu_thread == nullptr || !UISettings::values.hide_mouse.GetValue()) { mouse_hide_timer.stop(); @@ -2432,7 +2449,10 @@ void GMainWindow::closeEvent(QCloseEvent* event) { render_window->close(); secondary_window->close(); multiplayer_state->Close(); - InputCommon::Shutdown(); + Core::System::GetInstance().HIDCore().UnloadInputDevices(); + update_input_timer.stop(); + input_subsystem->Shutdown(); + QWidget::closeEvent(event); } @@ -2605,6 +2625,7 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_nam // Do not cause the global config to write local settings into the config file const bool is_powered_on = system.IsPoweredOn(); Settings::RestoreGlobalState(system.IsPoweredOn()); + Core::System::GetInstance().HIDCore().ReloadInputDevices(); if (!is_powered_on) { config->Save(); diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index a5ee269f6..0632fd40f 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -57,6 +57,10 @@ namespace DiscordRPC { class DiscordInterface; } +namespace InputCommon { +class InputSubsystem; +} + namespace Ui { class MainWindow; } @@ -255,11 +259,13 @@ private: void UpdateUISettings(); void RetranslateStatusBar(); void InstallCIA(QStringList filepaths); + void UpdateInputDrivers(); void HideMouseCursor(); void ShowMouseCursor(); void OpenPerGameConfiguration(u64 title_id, const QString& file_name); std::unique_ptr ui; + std::shared_ptr input_subsystem; GRenderWindow* render_window; GRenderWindow* secondary_window; @@ -289,6 +295,7 @@ private: bool auto_paused = false; QTimer mouse_hide_timer; + QTimer update_input_timer; // Movie bool movie_record_on_start = false; diff --git a/src/common/quaternion.h b/src/common/quaternion.h index da44f35cd..4d0871eb4 100644 --- a/src/common/quaternion.h +++ b/src/common/quaternion.h @@ -36,6 +36,36 @@ public: T length = std::sqrt(xyz.Length2() + w * w); return {xyz / length, w / length}; } + + [[nodiscard]] std::array ToMatrix() const { + const T x2 = xyz[0] * xyz[0]; + const T y2 = xyz[1] * xyz[1]; + const T z2 = xyz[2] * xyz[2]; + + const T xy = xyz[0] * xyz[1]; + const T wz = w * xyz[2]; + const T xz = xyz[0] * xyz[2]; + const T wy = w * xyz[1]; + const T yz = xyz[1] * xyz[2]; + const T wx = w * xyz[0]; + + return {1.0f - 2.0f * (y2 + z2), + 2.0f * (xy + wz), + 2.0f * (xz - wy), + 0.0f, + 2.0f * (xy - wz), + 1.0f - 2.0f * (x2 + z2), + 2.0f * (yz + wx), + 0.0f, + 2.0f * (xz + wy), + 2.0f * (yz - wx), + 1.0f - 2.0f * (x2 + y2), + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f}; + } }; template diff --git a/src/common/settings.cpp b/src/common/settings.cpp index eea0a6f71..3902456ee 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -101,6 +101,7 @@ void Apply() { Core::DSP().SetSink(values.output_type.GetValue(), values.output_device.GetValue()); Core::DSP().EnableStretching(values.enable_audio_stretching.GetValue()); +<<<<<<< HEAD auto hid = Service::HID::GetModule(system); if (hid) { hid->ReloadInputDevices(); @@ -119,6 +120,8 @@ void Apply() { if (ir_rst) ir_rst->ReloadInputDevices(); +======= +>>>>>>> 811fc4f3a (Stash "DONE") auto cam = Service::CAM::GetModule(system); if (cam) { cam->ReloadCameraDevices(); diff --git a/src/common/settings.h b/src/common/settings.h index d38afaacc..cec580345 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -397,6 +397,7 @@ struct InputProfile { std::string name; std::array buttons; std::array analogs; + std::array motions; std::string motion_device; std::string touch_device; bool use_touch_from_button; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 7863e8ba7..98db8052a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -109,11 +109,27 @@ add_library(citra_core STATIC frontend/framebuffer_layout.h frontend/image_interface.cpp frontend/image_interface.h - frontend/input.h gdbstub/gdbstub.cpp gdbstub/gdbstub.h gdbstub/hio.cpp gdbstub/hio.h + frontend/mic.cpp + frontend/mic.h + frontend/scope_acquire_context.cpp + frontend/scope_acquire_context.h + gdbstub/gdbstub.cpp + gdbstub/gdbstub.h + hid/emulated_console.cpp + hid/emulated_console.h + hid/emulated_controller.cpp + hid/emulated_controller.h + hid/hid_core.cpp + hid/hid_core.h + hid/hid_types.h + hid/input_converter.cpp + hid/input_converter.h + hid/motion_input.cpp + hid/motion_input.h hle/applets/applet.cpp hle/applets/applet.h hle/applets/erreula.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index 31de0537e..0815184c7 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -464,6 +464,14 @@ const Service::FS::ArchiveManager& System::ArchiveManager() const { return *archive_manager; } +HID::HIDCore& System::HIDCore() { + return hid_core; +} + +const HID::HIDCore& System::HIDCore() const { + return hid_core; +} + Kernel::KernelSystem& System::Kernel() { return *kernel; } diff --git a/src/core/core.h b/src/core/core.h index d4cab87e3..b9f60895f 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -12,6 +12,7 @@ #include "common/common_types.h" #include "core/frontend/applets/mii_selector.h" #include "core/frontend/applets/swkbd.h" +#include "core/hid/hid_core.h" #include "core/loader/loader.h" #include "core/memory.h" #include "core/perf_stats.h" @@ -225,6 +226,12 @@ public: /// Gets a const reference to the archive manager [[nodiscard]] const Service::FS::ArchiveManager& ArchiveManager() const; + /// Gets a mutable reference to the HID interface. + [[nodiscard]] HID::HIDCore& HIDCore(); + + /// Gets an immutable reference to the HID interface. + [[nodiscard]] const HID::HIDCore& HIDCore() const; + /// Gets a reference to the kernel [[nodiscard]] Kernel::KernelSystem& Kernel(); @@ -389,6 +396,8 @@ private: std::unique_ptr exclusive_monitor; + HID::HIDCore hid_core{}; + private: static System s_instance; diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp index f70a5fc60..115de32ff 100644 --- a/src/core/frontend/emu_window.cpp +++ b/src/core/frontend/emu_window.cpp @@ -4,57 +4,21 @@ #include #include +#include "common/input.h" #include "common/settings.h" #include "core/3ds.h" #include "core/frontend/emu_window.h" -#include "core/frontend/input.h" namespace Frontend { -/// We need a global touch state that is shared across the different window instances -static std::weak_ptr global_touch_state; GraphicsContext::~GraphicsContext() = default; -class EmuWindow::TouchState : public Input::Factory, - public std::enable_shared_from_this { -public: - std::unique_ptr Create(const Common::ParamPackage&) override { - return std::make_unique(shared_from_this()); - } +EmuWindow::EmuWindow(){}; - std::mutex mutex; - - bool touch_pressed = false; ///< True if touchpad area is currently pressed, otherwise false - - float touch_x = 0.0f; ///< Touchpad X-position - float touch_y = 0.0f; ///< Touchpad Y-position - -private: - class Device : public Input::TouchDevice { - public: - explicit Device(std::weak_ptr&& touch_state) : touch_state(touch_state) {} - std::tuple GetStatus() const override { - if (auto state = touch_state.lock()) { - std::lock_guard guard{state->mutex}; - return std::make_tuple(state->touch_x, state->touch_y, state->touch_pressed); - } - return std::make_tuple(0.0f, 0.0f, false); - } - - private: - std::weak_ptr touch_state; - }; -}; - -EmuWindow::EmuWindow() { - CreateTouchState(); -}; - -EmuWindow::EmuWindow(bool is_secondary_) : is_secondary{is_secondary_} { - CreateTouchState(); -} +EmuWindow::EmuWindow(bool is_secondary_) : is_secondary{is_secondary_} {} EmuWindow::~EmuWindow() = default; + /** * Check if the given x/y coordinates are within the touchpad specified by the framebuffer layout * @param layout FramebufferLayout object describing the framebuffer size and screen positions @@ -87,6 +51,18 @@ static bool IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigne } } +std::pair EmuWindow::MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const { + std::tie(framebuffer_x, framebuffer_y) = ClipToTouchScreen(framebuffer_x, framebuffer_y); + const float x = static_cast(framebuffer_x - framebuffer_layout.bottom_screen.left) / + static_cast(framebuffer_layout.bottom_screen.right - + framebuffer_layout.bottom_screen.left); + const float y = static_cast(framebuffer_y - framebuffer_layout.bottom_screen.top) / + static_cast(framebuffer_layout.bottom_screen.bottom - + framebuffer_layout.bottom_screen.top); + + return std::make_pair(x, y); +} + std::tuple EmuWindow::ClipToTouchScreen(unsigned new_x, unsigned new_y) const { if (new_x >= framebuffer_layout.width / 2) { if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::SideBySide) @@ -109,67 +85,6 @@ std::tuple EmuWindow::ClipToTouchScreen(unsigned new_x, unsi return std::make_tuple(new_x, new_y); } -void EmuWindow::CreateTouchState() { - if (touch_state = global_touch_state.lock()) { - return; - } - touch_state = std::make_shared(); - Input::RegisterFactory("emu_window", touch_state); - global_touch_state = touch_state; -} - -bool EmuWindow::TouchPressed(unsigned framebuffer_x, unsigned framebuffer_y) { - if (!IsWithinTouchscreen(framebuffer_layout, framebuffer_x, framebuffer_y)) - return false; - - if (framebuffer_x >= framebuffer_layout.width / 2) { - if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::SideBySide) - framebuffer_x -= framebuffer_layout.width / 2; - else if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR) - framebuffer_x -= - (framebuffer_layout.width / 2) - (framebuffer_layout.cardboard.user_x_shift * 2); - } - std::lock_guard guard(touch_state->mutex); - if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::SideBySide) { - touch_state->touch_x = - static_cast(framebuffer_x - framebuffer_layout.bottom_screen.left / 2) / - (framebuffer_layout.bottom_screen.right / 2 - - framebuffer_layout.bottom_screen.left / 2); - } else { - touch_state->touch_x = - static_cast(framebuffer_x - framebuffer_layout.bottom_screen.left) / - (framebuffer_layout.bottom_screen.right - framebuffer_layout.bottom_screen.left); - } - touch_state->touch_y = - static_cast(framebuffer_y - framebuffer_layout.bottom_screen.top) / - (framebuffer_layout.bottom_screen.bottom - framebuffer_layout.bottom_screen.top); - - if (!framebuffer_layout.is_rotated) { - std::swap(touch_state->touch_x, touch_state->touch_y); - touch_state->touch_x = 1.f - touch_state->touch_x; - } - - touch_state->touch_pressed = true; - return true; -} - -void EmuWindow::TouchReleased() { - std::lock_guard guard{touch_state->mutex}; - touch_state->touch_pressed = false; - touch_state->touch_x = 0; - touch_state->touch_y = 0; -} - -void EmuWindow::TouchMoved(unsigned framebuffer_x, unsigned framebuffer_y) { - if (!touch_state->touch_pressed) - return; - - if (!IsWithinTouchscreen(framebuffer_layout, framebuffer_x, framebuffer_y)) - std::tie(framebuffer_x, framebuffer_y) = ClipToTouchScreen(framebuffer_x, framebuffer_y); - - TouchPressed(framebuffer_x, framebuffer_y); -} - void EmuWindow::UpdateCurrentFramebufferLayout(unsigned width, unsigned height, bool is_portrait_mode) { Layout::FramebufferLayout layout; diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h index 26c14ef50..6a0d62030 100644 --- a/src/core/frontend/emu_window.h +++ b/src/core/frontend/emu_window.h @@ -122,8 +122,6 @@ public: */ class EmuWindow : public GraphicsContext { public: - class TouchState; - /// Data structure to store emuwindow configuration struct WindowConfig { bool fullscreen = false; @@ -176,24 +174,6 @@ public: */ virtual void RestoreContext(){}; - /** - * Signal that a touch pressed event has occurred (e.g. mouse click pressed) - * @param framebuffer_x Framebuffer x-coordinate that was pressed - * @param framebuffer_y Framebuffer y-coordinate that was pressed - * @returns True if the coordinates are within the touchpad, otherwise false - */ - bool TouchPressed(unsigned framebuffer_x, unsigned framebuffer_y); - - /// Signal that a touch released event has occurred (e.g. mouse click released) - void TouchReleased(); - - /** - * Signal that a touch movement event has occurred (e.g. mouse was moved over the emu window) - * @param framebuffer_x Framebuffer x-coordinate - * @param framebuffer_y Framebuffer y-coordinate - */ - void TouchMoved(unsigned framebuffer_x, unsigned framebuffer_y); - /** * Returns currently active configuration. * @note Accesses to the returned object need not be consistent because it may be modified in @@ -271,6 +251,11 @@ protected: framebuffer_layout = layout; } + /** + * Converts a screen postion into the equivalent touchscreen position. + */ + std::pair MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const; + bool is_secondary{}; bool strict_context_required{}; WindowSystemInfo window_info; @@ -286,16 +271,12 @@ private: // By default, ignore this request and do nothing. } - void CreateTouchState(); - Layout::FramebufferLayout framebuffer_layout; ///< Current framebuffer layout WindowConfig config{}; ///< Internal configuration (changes pending for being applied in /// ProcessConfigurationChanges) WindowConfig active_config{}; ///< Internal active configuration - std::shared_ptr touch_state; - /** * Clip the provided coordinates to be inside the touchscreen area. */ diff --git a/src/core/hid/emulated_console.cpp b/src/core/hid/emulated_console.cpp new file mode 100644 index 000000000..e6c561a5f --- /dev/null +++ b/src/core/hid/emulated_console.cpp @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/settings.h" +#include "core/hid/emulated_console.h" +#include "core/hid/input_converter.h" + +namespace Core::HID { +EmulatedConsole::EmulatedConsole() = default; + +EmulatedConsole::~EmulatedConsole() = default; + +void EmulatedConsole::ReloadFromSettings() { + // Using first motion device from player 1. No need to assign any unique config at the moment + const auto& player = Settings::values.current_input_profile; + motion_params = Common::ParamPackage(player.motions[0]); + + ReloadInput(); +} + +void EmulatedConsole::SetTouchParams() { + std::size_t index = 0; + + touch_params[index++] = Common::ParamPackage{"engine:mouse,axis_x:10,axis_y:11,button:0"}; + touch_params[index++] = + Common::ParamPackage{"engine:cemuhookudp,axis_x:17,axis_y:18,button:65536"}; + touch_params[index++] = + Common::ParamPackage{"engine:cemuhookudp,axis_x:19,axis_y:20,button:131072"}; + + for (int i = 0; i < static_cast(MaxActiveTouchInputs); i++) { + Common::ParamPackage touchscreen_param{}; + touchscreen_param.Set("engine", "touch"); + touchscreen_param.Set("axis_x", i * 2); + touchscreen_param.Set("axis_y", (i * 2) + 1); + touchscreen_param.Set("button", i); + touch_params[index++] = std::move(touchscreen_param); + } + + if (Settings::values.touch_from_button_maps.empty()) { + LOG_WARNING(Input, "touch_from_button_maps is unset by frontend config"); + return; + } + + const auto button_index = + static_cast(Settings::values.current_input_profile.touch_from_button_map_index); + const auto& touch_buttons = Settings::values.touch_from_button_maps[button_index].buttons; + + // Map the rest of the fingers from touch from button configuration + for (const auto& config_entry : touch_buttons) { + if (index >= MaxTouchDevices) { + continue; + } + Common::ParamPackage params{config_entry}; + Common::ParamPackage touch_button_params; + const int x = params.Get("x", 0); + const int y = params.Get("y", 0); + params.Erase("x"); + params.Erase("y"); + touch_button_params.Set("engine", "touch_from_button"); + touch_button_params.Set("button", params.Serialize()); + touch_button_params.Set("x", x); + touch_button_params.Set("y", y); + touch_params[index] = std::move(touch_button_params); + index++; + } +} + +void EmulatedConsole::ReloadInput() { + // If you load any device here add the equivalent to the UnloadInput() function + SetTouchParams(); + + motion_devices = Common::Input::CreateInputDevice(motion_params); + if (motion_devices) { + motion_devices->SetCallback({ + .on_change = + [this](const Common::Input::CallbackStatus& callback) { SetMotion(callback); }, + }); + } + + // Unique index for identifying touch device source + std::size_t index = 0; + for (auto& touch_device : touch_devices) { + touch_device = Common::Input::CreateInputDevice(touch_params[index]); + if (!touch_device) { + continue; + } + touch_device->SetCallback({ + .on_change = + [this, index](const Common::Input::CallbackStatus& callback) { + SetTouch(callback, index); + }, + }); + index++; + } +} + +void EmulatedConsole::UnloadInput() { + motion_devices.reset(); + for (auto& touch : touch_devices) { + touch.reset(); + } +} + +void EmulatedConsole::EnableConfiguration() { + is_configuring = true; + SaveCurrentConfig(); +} + +void EmulatedConsole::DisableConfiguration() { + is_configuring = false; +} + +bool EmulatedConsole::IsConfiguring() const { + return is_configuring; +} + +void EmulatedConsole::SaveCurrentConfig() { + if (!is_configuring) { + return; + } +} + +void EmulatedConsole::RestoreConfig() { + if (!is_configuring) { + return; + } + ReloadFromSettings(); +} + +Common::ParamPackage EmulatedConsole::GetMotionParam() const { + return motion_params; +} + +void EmulatedConsole::SetMotionParam(Common::ParamPackage param) { + motion_params = std::move(param); + ReloadInput(); +} + +void EmulatedConsole::SetMotion(const Common::Input::CallbackStatus& callback) { + std::unique_lock lock{mutex}; + auto& raw_status = console.motion_values.raw_status; + auto& emulated = console.motion_values.emulated; + + raw_status = TransformToMotion(callback); + emulated.SetAcceleration(Common::Vec3f{ + raw_status.accel.x.value, + raw_status.accel.y.value, + raw_status.accel.z.value, + }); + emulated.SetGyroscope(Common::Vec3f{ + raw_status.gyro.x.value, + raw_status.gyro.y.value, + raw_status.gyro.z.value, + }); + emulated.UpdateRotation(raw_status.delta_timestamp); + emulated.UpdateOrientation(raw_status.delta_timestamp); + + if (is_configuring) { + lock.unlock(); + TriggerOnChange(ConsoleTriggerType::Motion); + return; + } + + auto& motion = console.motion_state; + motion.accel = emulated.GetAcceleration(); + motion.gyro = emulated.GetGyroscope(); + motion.rotation = emulated.GetRotations(); + motion.orientation = emulated.GetOrientation(); + motion.quaternion = emulated.GetQuaternion(); + motion.gyro_bias = emulated.GetGyroBias(); + motion.is_at_rest = !emulated.IsMoving(motion_sensitivity); + // Find what is this value + motion.verticalization_error = 0.0f; + + lock.unlock(); + TriggerOnChange(ConsoleTriggerType::Motion); +} + +void EmulatedConsole::SetTouch(const Common::Input::CallbackStatus& callback, std::size_t index) { + if (index >= MaxTouchDevices) { + return; + } + std::unique_lock lock{mutex}; + + const auto touch_input = TransformToTouch(callback); + auto touch_index = GetIndexFromFingerId(index); + bool is_new_input = false; + + if (!touch_index.has_value() && touch_input.pressed.value) { + touch_index = GetNextFreeIndex(); + is_new_input = true; + } + + // No free entries or invalid state. Ignore input + if (!touch_index.has_value()) { + return; + } + + auto& touch_value = console.touch_values[touch_index.value()]; + + if (is_new_input) { + touch_value.pressed.value = true; + touch_value.id = static_cast(index); + } + + touch_value.x = touch_input.x; + touch_value.y = touch_input.y; + + if (!touch_input.pressed.value) { + touch_value.pressed.value = false; + } + + if (is_configuring) { + lock.unlock(); + TriggerOnChange(ConsoleTriggerType::Touch); + return; + } + + // Touch outside allowed range. Ignore input + if (touch_index.value() >= MaxActiveTouchInputs) { + return; + } + + console.touch_state[touch_index.value()] = { + .position_x = touch_value.x.value, + .position_y = touch_value.y.value, + .id = static_cast(touch_index.value()), + .pressed = touch_input.pressed.value, + }; + + lock.unlock(); + TriggerOnChange(ConsoleTriggerType::Touch); +} + +ConsoleMotionValues EmulatedConsole::GetMotionValues() const { + std::scoped_lock lock{mutex}; + return console.motion_values; +} + +TouchValues EmulatedConsole::GetTouchValues() const { + std::scoped_lock lock{mutex}; + return console.touch_values; +} + +ConsoleMotion EmulatedConsole::GetMotion() const { + std::scoped_lock lock{mutex}; + return console.motion_state; +} + +TouchFingerState EmulatedConsole::GetTouch() const { + std::scoped_lock lock{mutex}; + return console.touch_state; +} + +std::optional EmulatedConsole::GetIndexFromFingerId(std::size_t finger_id) const { + for (std::size_t index = 0; index < MaxTouchDevices; ++index) { + const auto& finger = console.touch_values[index]; + if (!finger.pressed.value) { + continue; + } + if (finger.id == static_cast(finger_id)) { + return index; + } + } + return std::nullopt; +} + +std::optional EmulatedConsole::GetNextFreeIndex() const { + for (std::size_t index = 0; index < MaxTouchDevices; ++index) { + if (!console.touch_values[index].pressed.value) { + return index; + } + } + return std::nullopt; +} + +void EmulatedConsole::TriggerOnChange(ConsoleTriggerType type) { + std::scoped_lock lock{callback_mutex}; + for (const auto& poller_pair : callback_list) { + const ConsoleUpdateCallback& poller = poller_pair.second; + if (poller.on_change) { + poller.on_change(type); + } + } +} + +int EmulatedConsole::SetCallback(ConsoleUpdateCallback update_callback) { + std::scoped_lock lock{callback_mutex}; + callback_list.insert_or_assign(last_callback_key, std::move(update_callback)); + return last_callback_key++; +} + +void EmulatedConsole::DeleteCallback(int key) { + std::scoped_lock lock{callback_mutex}; + const auto& iterator = callback_list.find(key); + if (iterator == callback_list.end()) { + LOG_ERROR(Input, "Tried to delete non-existent callback {}", key); + return; + } + callback_list.erase(iterator); +} +} // namespace Core::HID diff --git a/src/core/hid/emulated_console.h b/src/core/hid/emulated_console.h new file mode 100644 index 000000000..a916e2114 --- /dev/null +++ b/src/core/hid/emulated_console.h @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/input.h" +#include "common/param_package.h" +#include "common/quaternion.h" +#include "common/vector_math.h" +#include "core/hid/hid_types.h" +#include "core/hid/motion_input.h" + +namespace Core::HID { + +static constexpr std::size_t MaxTouchDevices = 32; +static constexpr std::size_t MaxActiveTouchInputs = 16; + +struct ConsoleMotionInfo { + Common::Input::MotionStatus raw_status{}; + MotionInput emulated{}; +}; + +using ConsoleMotionDevices = std::unique_ptr; +using TouchDevices = std::array, MaxTouchDevices>; + +using ConsoleMotionParams = Common::ParamPackage; +using TouchParams = std::array; + +using ConsoleMotionValues = ConsoleMotionInfo; +using TouchValues = std::array; + +struct TouchFinger { + u64 last_touch{}; + float position_x{}; + float position_y{}; + u32 id{}; + bool pressed{}; +}; + +// Contains all motion related data that is used on the services +struct ConsoleMotion { + Common::Vec3f accel{}; + Common::Vec3f gyro{}; + Common::Vec3f rotation{}; + std::array orientation{}; + Common::Quaternion quaternion{}; + Common::Vec3f gyro_bias{}; + f32 verticalization_error{}; + bool is_at_rest{}; +}; + +using TouchFingerState = std::array; + +struct ConsoleStatus { + // Data from input_common + ConsoleMotionValues motion_values{}; + TouchValues touch_values{}; + + // Data for HID services + ConsoleMotion motion_state{}; + TouchFingerState touch_state{}; +}; + +enum class ConsoleTriggerType { + Motion, + Touch, + All, +}; + +struct ConsoleUpdateCallback { + std::function on_change; +}; + +class EmulatedConsole { +public: + /** + * Contains all input data within the emulated switch console tablet such as touch and motion + */ + explicit EmulatedConsole(); + ~EmulatedConsole(); + + /// Removes all callbacks created from input devices + void UnloadInput(); + + /** + * Sets the emulated console into configuring mode + * This prevents the modification of the HID state of the emulated console by input commands + */ + void EnableConfiguration(); + + /// Returns the emulated console into normal mode, allowing the modification of the HID state + void DisableConfiguration(); + + /// Returns true if the emulated console is in configuring mode + bool IsConfiguring() const; + + /// Reload all input devices + void ReloadInput(); + + /// Overrides current mapped devices with the stored configuration and reloads all input devices + void ReloadFromSettings(); + + /// Saves the current mapped configuration + void SaveCurrentConfig(); + + /// Reverts any mapped changes made that weren't saved + void RestoreConfig(); + + // Returns the current mapped motion device + Common::ParamPackage GetMotionParam() const; + + /** + * Updates the current mapped motion device + * @param param ParamPackage with controller data to be mapped + */ + void SetMotionParam(Common::ParamPackage param); + + /// Returns the latest status of motion input from the console with parameters + ConsoleMotionValues GetMotionValues() const; + + /// Returns the latest status of touch input from the console with parameters + TouchValues GetTouchValues() const; + + /// Returns the latest status of motion input from the console + ConsoleMotion GetMotion() const; + + /// Returns the latest status of touch input from the console + TouchFingerState GetTouch() const; + + /** + * Adds a callback to the list of events + * @param update_callback A ConsoleUpdateCallback that will be triggered + * @return an unique key corresponding to the callback index in the list + */ + int SetCallback(ConsoleUpdateCallback update_callback); + + /** + * Removes a callback from the list stopping any future events to this object + * @param key Key corresponding to the callback index in the list + */ + void DeleteCallback(int key); + +private: + /// Creates and stores the touch params + void SetTouchParams(); + + /** + * Updates the motion status of the console + * @param callback A CallbackStatus containing gyro and accelerometer data + */ + void SetMotion(const Common::Input::CallbackStatus& callback); + + /** + * Updates the touch status of the console + * @param callback A CallbackStatus containing the touch position + * @param index Finger ID to be updated + */ + void SetTouch(const Common::Input::CallbackStatus& callback, std::size_t index); + + std::optional GetIndexFromFingerId(std::size_t finger_id) const; + + std::optional GetNextFreeIndex() const; + + /** + * Triggers a callback that something has changed on the console status + * @param type Input type of the event to trigger + */ + void TriggerOnChange(ConsoleTriggerType type); + + bool is_configuring{false}; + f32 motion_sensitivity{0.01f}; + + ConsoleMotionParams motion_params; + TouchParams touch_params; + + ConsoleMotionDevices motion_devices; + TouchDevices touch_devices; + + mutable std::mutex mutex; + mutable std::mutex callback_mutex; + std::unordered_map callback_list; + int last_callback_key = 0; + + // Stores the current status of all console input + ConsoleStatus console; +}; + +} // namespace Core::HID diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp new file mode 100644 index 000000000..a638f28ff --- /dev/null +++ b/src/core/hid/emulated_controller.cpp @@ -0,0 +1,518 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "core/hid/emulated_controller.h" +#include "core/hid/input_converter.h" + +namespace Core::HID { + +// Use a common UUID for Virtual Gamepad +constexpr Common::UUID VIRTUAL_UUID = + Common::UUID{{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xFF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; + +// xperia64: 0x9A seems to be the calibrated limit of the circle pad +// Verified by using Input Redirector with very large-value digital inputs +// on the circle pad and calibrating using the system settings application +constexpr int MAX_CIRCLEPAD_POS = 0x9A; // Max value for a circle pad position + +EmulatedController::EmulatedController() {} + +EmulatedController::~EmulatedController() = default; + +void EmulatedController::LoadDevices() { + LoadVirtualGamepadParams(); + + std::ranges::transform(button_params, button_devices.begin(), Common::Input::CreateInputDevice); + std::ranges::transform(stick_params, stick_devices.begin(), Common::Input::CreateInputDevice); + std::ranges::transform(motion_params, motion_devices.begin(), Common::Input::CreateInputDevice); + + // Initialize virtual gamepad devices + std::ranges::transform(virtual_button_params, virtual_button_devices.begin(), + Common::Input::CreateInputDevice); + std::ranges::transform(virtual_stick_params, virtual_stick_devices.begin(), + Common::Input::CreateInputDevice); +} + +void EmulatedController::LoadVirtualGamepadParams() { + Common::ParamPackage common_params{}; + common_params.Set("engine", "virtual_gamepad"); + common_params.Set("port", 0); + for (auto& param : virtual_button_params) { + param = common_params; + } + for (auto& param : virtual_stick_params) { + param = common_params; + } + + // TODO(german77): Replace this with an input profile or something better + virtual_button_params[Settings::NativeButton::A].Set("button", 0); + virtual_button_params[Settings::NativeButton::B].Set("button", 1); + virtual_button_params[Settings::NativeButton::X].Set("button", 2); + virtual_button_params[Settings::NativeButton::Y].Set("button", 3); + virtual_button_params[Settings::NativeButton::DUp].Set("button", 4); + virtual_button_params[Settings::NativeButton::DDown].Set("button", 5); + virtual_button_params[Settings::NativeButton::DLeft].Set("button", 6); + virtual_button_params[Settings::NativeButton::DRight].Set("button", 7); + virtual_button_params[Settings::NativeButton::L].Set("button", 8); + virtual_button_params[Settings::NativeButton::R].Set("button", 9); + virtual_button_params[Settings::NativeButton::Start].Set("button", 10); + virtual_button_params[Settings::NativeButton::Select].Set("button", 11); + virtual_button_params[Settings::NativeButton::Debug].Set("button", 12); + virtual_button_params[Settings::NativeButton::Gpio14].Set("button", 13); + virtual_button_params[Settings::NativeButton::ZL].Set("button", 14); + virtual_button_params[Settings::NativeButton::ZR].Set("button", 15); + virtual_button_params[Settings::NativeButton::Home].Set("button", 16); + + virtual_stick_params[Settings::NativeAnalog::CirclePad].Set("axis_x", 0); + virtual_stick_params[Settings::NativeAnalog::CirclePad].Set("axis_y", 1); + virtual_stick_params[Settings::NativeAnalog::CStick].Set("axis_x", 2); + virtual_stick_params[Settings::NativeAnalog::CStick].Set("axis_y", 3); +} + +void EmulatedController::UnloadInput() { + for (auto& button : button_devices) { + button.reset(); + } + for (auto& stick : stick_devices) { + stick.reset(); + } + for (auto& motion : motion_devices) { + motion.reset(); + } + for (auto& button : virtual_button_devices) { + button.reset(); + } + for (auto& stick : virtual_stick_devices) { + stick.reset(); + } +} + +void EmulatedController::EnableConfiguration() { + is_configuring = true; +} + +void EmulatedController::DisableConfiguration() { + is_configuring = false; +} + +void EmulatedController::ReloadInput() { + // If you load any device here add the equivalent to the UnloadInput() function + LoadDevices(); + for (std::size_t index = 0; index < button_devices.size(); ++index) { + if (!button_devices[index]) { + continue; + } + const auto uuid = Common::UUID{button_params[index].Get("guid", "")}; + button_devices[index]->SetCallback({ + .on_change = + [this, index, uuid](const Common::Input::CallbackStatus& callback) { + SetButton(callback, index, uuid); + }, + }); + button_devices[index]->ForceUpdate(); + } + + for (std::size_t index = 0; index < stick_devices.size(); ++index) { + if (!stick_devices[index]) { + continue; + } + const auto uuid = Common::UUID{stick_params[index].Get("guid", "")}; + stick_devices[index]->SetCallback({ + .on_change = + [this, index, uuid](const Common::Input::CallbackStatus& callback) { + SetStick(callback, index, uuid); + }, + }); + stick_devices[index]->ForceUpdate(); + } + + for (std::size_t index = 0; index < motion_devices.size(); ++index) { + if (!motion_devices[index]) { + continue; + } + motion_devices[index]->SetCallback({ + .on_change = + [this, index](const Common::Input::CallbackStatus& callback) { + // SetMotion(callback, index); + }, + }); + motion_devices[index]->ForceUpdate(); + } + + // Register virtual devices. No need to force update + for (std::size_t index = 0; index < virtual_button_devices.size(); ++index) { + if (!virtual_button_devices[index]) { + continue; + } + virtual_button_devices[index]->SetCallback({ + .on_change = + [this, index](const Common::Input::CallbackStatus& callback) { + SetButton(callback, index, VIRTUAL_UUID); + }, + }); + } + + for (std::size_t index = 0; index < virtual_stick_devices.size(); ++index) { + if (!virtual_stick_devices[index]) { + continue; + } + virtual_stick_devices[index]->SetCallback({ + .on_change = + [this, index](const Common::Input::CallbackStatus& callback) { + SetStick(callback, index, VIRTUAL_UUID); + }, + }); + } +} + +void EmulatedController::ReloadFromSettings() { + const auto& player = Settings::values.current_input_profile; + for (std::size_t i = 0; i < player.buttons.size(); i++) { + button_params[i] = Common::ParamPackage(player.buttons[i]); + } + for (std::size_t i = 0; i < player.analogs.size(); i++) { + stick_params[i] = Common::ParamPackage(player.analogs[i]); + } + for (std::size_t index = 0; index < player.motions.size(); ++index) { + motion_params[index] = Common::ParamPackage(player.motions[index]); + } + + ReloadInput(); +} + +void EmulatedController::SaveCurrentConfig() { + auto& player = Settings::values.current_input_profile; + for (std::size_t index = 0; index < player.buttons.size(); ++index) { + player.buttons[index] = button_params[index].Serialize(); + } + for (std::size_t index = 0; index < player.analogs.size(); ++index) { + player.analogs[index] = stick_params[index].Serialize(); + } + for (std::size_t index = 0; index < player.motions.size(); ++index) { + player.motions[index] = motion_params[index].Serialize(); + } +} + +std::vector EmulatedController::GetMappedDevices() const { + std::vector devices; + for (const auto& param : button_params) { + if (!param.Has("engine")) { + continue; + } + const auto devices_it = std::find_if( + devices.begin(), devices.end(), [¶m](const Common::ParamPackage& param_) { + return param.Get("engine", "") == param_.Get("engine", "") && + param.Get("guid", "") == param_.Get("guid", "") && + param.Get("port", 0) == param_.Get("port", 0) && + param.Get("pad", 0) == param_.Get("pad", 0); + }); + if (devices_it != devices.end()) { + continue; + } + + auto& device = devices.emplace_back(); + device.Set("engine", param.Get("engine", "")); + device.Set("guid", param.Get("guid", "")); + device.Set("port", param.Get("port", 0)); + device.Set("pad", param.Get("pad", 0)); + } + + for (const auto& param : stick_params) { + if (!param.Has("engine")) { + continue; + } + if (param.Get("engine", "") == "analog_from_button") { + continue; + } + const auto devices_it = std::find_if( + devices.begin(), devices.end(), [¶m](const Common::ParamPackage& param_) { + return param.Get("engine", "") == param_.Get("engine", "") && + param.Get("guid", "") == param_.Get("guid", "") && + param.Get("port", 0) == param_.Get("port", 0) && + param.Get("pad", 0) == param_.Get("pad", 0); + }); + if (devices_it != devices.end()) { + continue; + } + + auto& device = devices.emplace_back(); + device.Set("engine", param.Get("engine", "")); + device.Set("guid", param.Get("guid", "")); + device.Set("port", param.Get("port", 0)); + device.Set("pad", param.Get("pad", 0)); + } + return devices; +} + +Common::ParamPackage EmulatedController::GetButtonParam(std::size_t index) const { + if (index >= button_params.size()) { + return {}; + } + return button_params[index]; +} + +Common::ParamPackage EmulatedController::GetStickParam(std::size_t index) const { + if (index >= stick_params.size()) { + return {}; + } + return stick_params[index]; +} + +Common::ParamPackage EmulatedController::GetMotionParam(std::size_t index) const { + if (index >= motion_params.size()) { + return {}; + } + return motion_params[index]; +} + +void EmulatedController::SetButtonParam(std::size_t index, Common::ParamPackage param) { + if (index >= button_params.size()) { + return; + } + button_params[index] = std::move(param); + ReloadInput(); +} + +void EmulatedController::SetStickParam(std::size_t index, Common::ParamPackage param) { + if (index >= stick_params.size()) { + return; + } + stick_params[index] = std::move(param); + ReloadInput(); +} + +void EmulatedController::SetMotionParam(std::size_t index, Common::ParamPackage param) { + if (index >= motion_params.size()) { + return; + } + motion_params[index] = std::move(param); + ReloadInput(); +} + +ButtonValues EmulatedController::GetButtonsValues() const { + std::scoped_lock lock{mutex}; + return controller.button_values; +} + +SticksValues EmulatedController::GetSticksValues() const { + std::scoped_lock lock{mutex}; + return controller.stick_values; +} + +PadState EmulatedController::GetPadState() const { + std::scoped_lock lock{mutex}; + if (is_configuring) { + return {}; + } + return controller.pad_state; +} + +ExtraState EmulatedController::GetExtraState() const { + std::scoped_lock lock{mutex}; + if (is_configuring) { + return {}; + } + return controller.extra_state; +} + +AnalogSticks EmulatedController::GetSticks() const { + std::unique_lock lock{mutex}; + if (is_configuring) { + return {}; + } + return controller.analog_stick_state; +} + +void EmulatedController::SetButton(const Common::Input::CallbackStatus& callback, std::size_t index, + Common::UUID uuid) { + if (index >= controller.button_values.size()) { + return; + } + std::unique_lock lock{mutex}; + bool value_changed = false; + const auto new_status = TransformToButton(callback); + auto& current_status = controller.button_values[index]; + + // Only read button values that have the same uuid or are pressed once + if (current_status.uuid != uuid) { + if (!new_status.value) { + return; + } + } + + current_status.toggle = new_status.toggle; + current_status.uuid = uuid; + + // Update button status with current + if (!current_status.toggle) { + current_status.locked = false; + if (current_status.value != new_status.value) { + current_status.value = new_status.value; + value_changed = true; + } + } else { + // Toggle button and lock status + if (new_status.value && !current_status.locked) { + current_status.locked = true; + current_status.value = !current_status.value; + value_changed = true; + } + + // Unlock button ready for next press + if (!new_status.value && current_status.locked) { + current_status.locked = false; + } + } + + if (!value_changed) { + return; + } + + if (is_configuring) { + controller.pad_state.hex = 0; + lock.unlock(); + TriggerOnChange(ControllerTriggerType::Button, false); + return; + } + + switch (index) { + case Settings::NativeButton::A: + controller.pad_state.a.Assign(current_status.value); + break; + case Settings::NativeButton::B: + controller.pad_state.b.Assign(current_status.value); + break; + case Settings::NativeButton::X: + controller.pad_state.x.Assign(current_status.value); + break; + case Settings::NativeButton::Y: + controller.pad_state.y.Assign(current_status.value); + break; + case Settings::NativeButton::L: + controller.pad_state.l.Assign(current_status.value); + break; + case Settings::NativeButton::R: + controller.pad_state.r.Assign(current_status.value); + break; + case Settings::NativeButton::ZL: + controller.extra_state.zl = current_status.value; + break; + case Settings::NativeButton::ZR: + controller.extra_state.zr = current_status.value; + break; + case Settings::NativeButton::Start: + controller.pad_state.start.Assign(current_status.value); + break; + case Settings::NativeButton::Select: + controller.pad_state.select.Assign(current_status.value); + break; + case Settings::NativeButton::DLeft: + controller.pad_state.left.Assign(current_status.value); + break; + case Settings::NativeButton::DUp: + controller.pad_state.up.Assign(current_status.value); + break; + case Settings::NativeButton::DRight: + controller.pad_state.right.Assign(current_status.value); + break; + case Settings::NativeButton::DDown: + controller.pad_state.down.Assign(current_status.value); + break; + case Settings::NativeButton::Debug: + controller.pad_state.debug.Assign(current_status.value); + break; + case Settings::NativeButton::Gpio14: + controller.pad_state.gpio14.Assign(current_status.value); + break; + } + + lock.unlock(); + TriggerOnChange(ControllerTriggerType::Button, true); +} + +void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback, std::size_t index, + Common::UUID uuid) { + if (index >= controller.stick_values.size()) { + return; + } + std::unique_lock lock{mutex}; + const auto stick_value = TransformToStick(callback); + + // Only read stick values that have the same uuid or are over the threshold to avoid flapping + if (controller.stick_values[index].uuid != uuid) { + if (!stick_value.down && !stick_value.up && !stick_value.left && !stick_value.right) { + return; + } + } + + controller.stick_values[index] = stick_value; + controller.stick_values[index].uuid = uuid; + + if (is_configuring) { + controller.analog_stick_state.circle_pad = {}; + controller.analog_stick_state.c_stick = {}; + lock.unlock(); + TriggerOnChange(ControllerTriggerType::Stick, false); + return; + } + + const AnalogStickState stick{ + .x = static_cast( + std::roundf(controller.stick_values[index].x.value * MAX_CIRCLEPAD_POS)), + .y = static_cast( + std::roundf(controller.stick_values[index].y.value * MAX_CIRCLEPAD_POS)), + }; + + switch (index) { + case Settings::NativeAnalog::CirclePad: + controller.analog_stick_state.circle_pad = stick; + controller.pad_state.circle_left.Assign(controller.stick_values[index].left); + controller.pad_state.circle_up.Assign(controller.stick_values[index].up); + controller.pad_state.circle_right.Assign(controller.stick_values[index].right); + controller.pad_state.circle_down.Assign(controller.stick_values[index].down); + break; + case Settings::NativeAnalog::CStick: + controller.analog_stick_state.c_stick = stick; + controller.extra_state.c_stick_left = controller.stick_values[index].left; + controller.extra_state.c_stick_up = controller.stick_values[index].up; + controller.extra_state.c_stick_right = controller.stick_values[index].right; + controller.extra_state.c_stick_down = controller.stick_values[index].down; + break; + } + + lock.unlock(); + TriggerOnChange(ControllerTriggerType::Stick, true); +} + +void EmulatedController::TriggerOnChange(ControllerTriggerType type, bool is_hid_service_update) { + std::scoped_lock lock{callback_mutex}; + for (const auto& poller_pair : callback_list) { + const ControllerUpdateCallback& poller = poller_pair.second; + if (!is_hid_service_update && poller.is_hid_service) { + continue; + } + if (poller.on_change) { + poller.on_change(type); + } + } +} + +int EmulatedController::SetCallback(ControllerUpdateCallback update_callback) { + std::scoped_lock lock{callback_mutex}; + callback_list.insert_or_assign(last_callback_key, std::move(update_callback)); + return last_callback_key++; +} + +void EmulatedController::DeleteCallback(int key) { + std::scoped_lock lock{callback_mutex}; + const auto& iterator = callback_list.find(key); + if (iterator == callback_list.end()) { + LOG_ERROR(Input, "Tried to delete non-existent callback {}", key); + return; + } + callback_list.erase(iterator); +} + +} // namespace Core::HID diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h new file mode 100644 index 000000000..9b9cc59d3 --- /dev/null +++ b/src/core/hid/emulated_controller.h @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/bit_field.h" +#include "common/common_types.h" +#include "common/input.h" +#include "common/param_package.h" +#include "common/settings.h" +#include "common/vector_math.h" +#include "core/hid/hid_types.h" + +namespace Core::HID { + +using ButtonDevices = + std::array, Settings::NativeButton::NumButtons>; +using StickDevices = + std::array, Settings::NativeAnalog::NumAnalogs>; +using ControllerMotionDevices = + std::array, Settings::NativeMotion::NumMotions>; + +using ButtonParams = std::array; +using StickParams = std::array; +using ControllerMotionParams = std::array; + +using ButtonValues = std::array; +using SticksValues = std::array; + +struct AnalogStickState { + s16 x{}; + s16 y{}; +}; + +struct AnalogSticks { + AnalogStickState circle_pad{}; + AnalogStickState c_stick{}; +}; + +struct ExtraState { + bool zr{}; + bool zl{}; + + bool c_stick_right{}; + bool c_stick_left{}; + bool c_stick_up{}; + bool c_stick_down{}; +}; + +struct ControllerStatus { + // Data from input_common + ButtonValues button_values{}; + SticksValues stick_values{}; + + // Data for HID services + PadState pad_state{}; + AnalogSticks analog_stick_state{}; + ExtraState extra_state{}; +}; + +enum class ControllerTriggerType { + Button, + Stick, + Motion, + Connected, + Disconnected, + Type, + All, +}; + +struct ControllerUpdateCallback { + std::function on_change; + bool is_hid_service; +}; + +class EmulatedController { +public: + /** + * Contains all input data (buttons, joysticks, and motion) within this controller. + */ + explicit EmulatedController(); + ~EmulatedController(); + + /// Removes all callbacks created from input devices + void UnloadInput(); + + /** + * Sets the emulated controller into configuring mode + * This prevents the modification of the HID state of the emulated controller by input commands + */ + void EnableConfiguration(); + + /// Returns the emulated controller into normal mode, allowing the modification of the HID state + void DisableConfiguration(); + + /// Reload all input devices + void ReloadInput(); + + /// Overrides current mapped devices with the stored configuration and reloads all input devices + void ReloadFromSettings(); + + /// Saves the current mapped configuration + void SaveCurrentConfig(); + + /// Returns a vector of mapped devices from the mapped button and stick parameters + std::vector GetMappedDevices() const; + + // Returns the current mapped button device + Common::ParamPackage GetButtonParam(std::size_t index) const; + + // Returns the current mapped stick device + Common::ParamPackage GetStickParam(std::size_t index) const; + + // Returns the current mapped motion device + Common::ParamPackage GetMotionParam(std::size_t index) const; + + /** + * Updates the current mapped button device + * @param param ParamPackage with controller data to be mapped + */ + void SetButtonParam(std::size_t index, Common::ParamPackage param); + + /** + * Updates the current mapped stick device + * @param param ParamPackage with controller data to be mapped + */ + void SetStickParam(std::size_t index, Common::ParamPackage param); + + /** + * Updates the current mapped motion device + * @param param ParamPackage with controller data to be mapped + */ + void SetMotionParam(std::size_t index, Common::ParamPackage param); + + /// Returns the latest button status from the controller with parameters + ButtonValues GetButtonsValues() const; + + /// Returns the latest analog stick status from the controller with parameters + SticksValues GetSticksValues() const; + + /// Returns the latest status of button input for the hid service + PadState GetPadState() const; + + /// Returns the latest status of extra button input for the hid service + ExtraState GetExtraState() const; + + /// Returns the latest status of stick input from the mouse + AnalogSticks GetSticks() const; + + /** + * Adds a callback to the list of events + * @param update_callback A ConsoleUpdateCallback that will be triggered + * @return an unique key corresponding to the callback index in the list + */ + int SetCallback(ControllerUpdateCallback update_callback); + + /** + * Removes a callback from the list stopping any future events to this object + * @param key Key corresponding to the callback index in the list + */ + void DeleteCallback(int key); + +private: + /// creates input devices from params + void LoadDevices(); + + /// Set the params for virtual pad devices + void LoadVirtualGamepadParams(); + + /** + * Updates the button status of the controller + * @param callback A CallbackStatus containing the button status + * @param index Button ID of the to be updated + */ + void SetButton(const Common::Input::CallbackStatus& callback, std::size_t index, + Common::UUID uuid); + + /** + * Updates the analog stick status of the controller + * @param callback A CallbackStatus containing the analog stick status + * @param index stick ID of the to be updated + */ + void SetStick(const Common::Input::CallbackStatus& callback, std::size_t index, + Common::UUID uuid); + + /** + * Triggers a callback that something has changed on the controller status + * @param type Input type of the event to trigger + * @param is_service_update indicates if this event should only be sent to HID services + */ + void TriggerOnChange(ControllerTriggerType type, bool is_hid_service_update); + + bool is_connected{false}; + bool is_configuring{false}; + + ButtonParams button_params; + StickParams stick_params; + ControllerMotionParams motion_params; + + ButtonDevices button_devices; + StickDevices stick_devices; + ControllerMotionDevices motion_devices; + + // Virtual gamepad related variables + ButtonParams virtual_button_params; + StickParams virtual_stick_params; + ButtonDevices virtual_button_devices; + StickDevices virtual_stick_devices; + + mutable std::mutex mutex; + mutable std::mutex callback_mutex; + std::unordered_map callback_list; + int last_callback_key = 0; + + // Stores the current status of all controller input + ControllerStatus controller; +}; + +} // namespace Core::HID diff --git a/src/core/hid/hid_core.cpp b/src/core/hid/hid_core.cpp new file mode 100644 index 000000000..347cf9e95 --- /dev/null +++ b/src/core/hid/hid_core.cpp @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "core/hid/emulated_console.h" +#include "core/hid/emulated_controller.h" +#include "core/hid/hid_core.h" + +namespace Core::HID { + +HIDCore::HIDCore() + : controller{std::make_unique()}, + console{std::make_unique()} {} + +HIDCore::~HIDCore() = default; + +EmulatedController* HIDCore::GetEmulatedController() { + return controller.get(); +} + +const EmulatedController* HIDCore::GetEmulatedController() const { + return controller.get(); +} + +EmulatedConsole* HIDCore::GetEmulatedConsole() { + return console.get(); +} + +const EmulatedConsole* HIDCore::GetEmulatedConsole() const { + return console.get(); +} + +void HIDCore::EnableAllControllerConfiguration() { + controller->EnableConfiguration(); +} + +void HIDCore::DisableAllControllerConfiguration() { + controller->DisableConfiguration(); +} + +void HIDCore::ReloadInputDevices() { + controller->ReloadFromSettings(); + console->ReloadFromSettings(); +} + +void HIDCore::UnloadInputDevices() { + controller->UnloadInput(); + console->UnloadInput(); +} + +} // namespace Core::HID diff --git a/src/core/hid/hid_core.h b/src/core/hid/hid_core.h new file mode 100644 index 000000000..50ec53c7e --- /dev/null +++ b/src/core/hid/hid_core.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_funcs.h" + +namespace Core::HID { +class EmulatedConsole; +class EmulatedController; +} // namespace Core::HID + +namespace Core::HID { + +class HIDCore { +public: + explicit HIDCore(); + ~HIDCore(); + + EmulatedController* GetEmulatedController(); + const EmulatedController* GetEmulatedController() const; + + EmulatedConsole* GetEmulatedConsole(); + const EmulatedConsole* GetEmulatedConsole() const; + + /// Sets all emulated controllers into configuring mode. + void EnableAllControllerConfiguration(); + + /// Sets all emulated controllers into normal mode. + void DisableAllControllerConfiguration(); + + /// Reloads all input devices from settings + void ReloadInputDevices(); + + /// Removes all callbacks from input common + void UnloadInputDevices(); + +private: + std::unique_ptr controller; + std::unique_ptr console; +}; + +} // namespace Core::HID diff --git a/src/core/hid/hid_types.h b/src/core/hid/hid_types.h new file mode 100644 index 000000000..009f4f00b --- /dev/null +++ b/src/core/hid/hid_types.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/bit_field.h" +#include "common/common_types.h" + +namespace Core::HID { + +/** + * Structure of a Pad controller state. + */ +struct PadState { + union { + u32 hex{}; + + BitField<0, 1, u32> a; + BitField<1, 1, u32> b; + BitField<2, 1, u32> select; + BitField<3, 1, u32> start; + BitField<4, 1, u32> right; + BitField<5, 1, u32> left; + BitField<6, 1, u32> up; + BitField<7, 1, u32> down; + BitField<8, 1, u32> r; + BitField<9, 1, u32> l; + BitField<10, 1, u32> x; + BitField<11, 1, u32> y; + BitField<12, 1, u32> debug; + BitField<13, 1, u32> gpio14; + + BitField<28, 1, u32> circle_right; + BitField<29, 1, u32> circle_left; + BitField<30, 1, u32> circle_up; + BitField<31, 1, u32> circle_down; + }; +}; + +} // namespace Core::HID diff --git a/src/core/hid/input_converter.cpp b/src/core/hid/input_converter.cpp new file mode 100644 index 000000000..860fd1cc2 --- /dev/null +++ b/src/core/hid/input_converter.cpp @@ -0,0 +1,340 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "common/input.h" +#include "core/hid/input_converter.h" + +namespace Core::HID { + +Common::Input::ButtonStatus TransformToButton(const Common::Input::CallbackStatus& callback) { + Common::Input::ButtonStatus status{}; + switch (callback.type) { + case Common::Input::InputType::Analog: + status.value = TransformToTrigger(callback).pressed.value; + status.toggle = callback.analog_status.properties.toggle; + break; + case Common::Input::InputType::Button: + status = callback.button_status; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to button not implemented", callback.type); + break; + } + + if (status.inverted) { + status.value = !status.value; + } + + return status; +} + +Common::Input::MotionStatus TransformToMotion(const Common::Input::CallbackStatus& callback) { + Common::Input::MotionStatus status{}; + switch (callback.type) { + case Common::Input::InputType::Button: { + Common::Input::AnalogProperties properties{ + .deadzone = 0.0f, + .range = 1.0f, + .offset = 0.0f, + }; + status.delta_timestamp = 5000; + status.force_update = true; + status.accel.x = { + .value = 0.0f, + .raw_value = 0.0f, + .properties = properties, + }; + status.accel.y = { + .value = 0.0f, + .raw_value = 0.0f, + .properties = properties, + }; + status.accel.z = { + .value = 0.0f, + .raw_value = -1.0f, + .properties = properties, + }; + status.gyro.x = { + .value = 0.0f, + .raw_value = 0.0f, + .properties = properties, + }; + status.gyro.y = { + .value = 0.0f, + .raw_value = 0.0f, + .properties = properties, + }; + status.gyro.z = { + .value = 0.0f, + .raw_value = 0.0f, + .properties = properties, + }; + if (TransformToButton(callback).value) { + std::random_device device; + std::mt19937 gen(device()); + std::uniform_int_distribution distribution(-5000, 5000); + status.accel.x.raw_value = static_cast(distribution(gen)) * 0.001f; + status.accel.y.raw_value = static_cast(distribution(gen)) * 0.001f; + status.accel.z.raw_value = static_cast(distribution(gen)) * 0.001f; + status.gyro.x.raw_value = static_cast(distribution(gen)) * 0.001f; + status.gyro.y.raw_value = static_cast(distribution(gen)) * 0.001f; + status.gyro.z.raw_value = static_cast(distribution(gen)) * 0.001f; + } + break; + } + case Common::Input::InputType::Motion: + status = callback.motion_status; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to motion not implemented", callback.type); + break; + } + SanitizeAnalog(status.accel.x, false); + SanitizeAnalog(status.accel.y, false); + SanitizeAnalog(status.accel.z, false); + SanitizeAnalog(status.gyro.x, false); + SanitizeAnalog(status.gyro.y, false); + SanitizeAnalog(status.gyro.z, false); + + return status; +} + +Common::Input::StickStatus TransformToStick(const Common::Input::CallbackStatus& callback) { + Common::Input::StickStatus status{}; + + switch (callback.type) { + case Common::Input::InputType::Stick: + status = callback.stick_status; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to stick not implemented", callback.type); + break; + } + + SanitizeStick(status.x, status.y, true); + const auto& properties_x = status.x.properties; + const auto& properties_y = status.y.properties; + const float x = status.x.value; + const float y = status.y.value; + + // Set directional buttons + status.right = x > properties_x.threshold; + status.left = x < -properties_x.threshold; + status.up = y > properties_y.threshold; + status.down = y < -properties_y.threshold; + + return status; +} + +Common::Input::TouchStatus TransformToTouch(const Common::Input::CallbackStatus& callback) { + Common::Input::TouchStatus status{}; + + switch (callback.type) { + case Common::Input::InputType::Touch: + status = callback.touch_status; + break; + case Common::Input::InputType::Stick: + status.x = callback.stick_status.x; + status.y = callback.stick_status.y; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to touch not implemented", callback.type); + break; + } + + SanitizeAnalog(status.x, true); + SanitizeAnalog(status.y, true); + float& x = status.x.value; + float& y = status.y.value; + + // Adjust if value is inverted + x = status.x.properties.inverted ? 1.0f + x : x; + y = status.y.properties.inverted ? 1.0f + y : y; + + // clamp value + x = std::clamp(x, 0.0f, 1.0f); + y = std::clamp(y, 0.0f, 1.0f); + + if (status.pressed.inverted) { + status.pressed.value = !status.pressed.value; + } + + return status; +} + +Common::Input::TriggerStatus TransformToTrigger(const Common::Input::CallbackStatus& callback) { + Common::Input::TriggerStatus status{}; + float& raw_value = status.analog.raw_value; + bool calculate_button_value = true; + + switch (callback.type) { + case Common::Input::InputType::Analog: + status.analog.properties = callback.analog_status.properties; + raw_value = callback.analog_status.raw_value; + break; + case Common::Input::InputType::Button: + status.analog.properties.range = 1.0f; + status.analog.properties.inverted = callback.button_status.inverted; + raw_value = callback.button_status.value ? 1.0f : 0.0f; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to trigger not implemented", callback.type); + break; + } + + SanitizeAnalog(status.analog, true); + const auto& properties = status.analog.properties; + float& value = status.analog.value; + + // Set button status + if (calculate_button_value) { + status.pressed.value = value > properties.threshold; + } + + // Adjust if value is inverted + value = properties.inverted ? 1.0f + value : value; + + // clamp value + value = std::clamp(value, 0.0f, 1.0f); + + return status; +} + +Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatus& callback) { + Common::Input::AnalogStatus status{}; + + switch (callback.type) { + case Common::Input::InputType::Analog: + status.properties = callback.analog_status.properties; + status.raw_value = callback.analog_status.raw_value; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to analog not implemented", callback.type); + break; + } + + SanitizeAnalog(status, false); + + // Adjust if value is inverted + status.value = status.properties.inverted ? -status.value : status.value; + + return status; +} + +void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) { + const auto& properties = analog.properties; + float& raw_value = analog.raw_value; + float& value = analog.value; + + if (!std::isnormal(raw_value)) { + raw_value = 0; + } + + // Apply center offset + raw_value -= properties.offset; + + // Set initial values to be formated + value = raw_value; + + // Calculate vector size + const float r = std::abs(value); + + // Return zero if value is smaller than the deadzone + if (r <= properties.deadzone || properties.deadzone == 1.0f) { + analog.value = 0; + return; + } + + // Adjust range of value + const float deadzone_factor = + 1.0f / r * (r - properties.deadzone) / (1.0f - properties.deadzone); + value = value * deadzone_factor / properties.range; + + // Invert direction if needed + if (properties.inverted) { + value = -value; + } + + // Clamp value + if (clamp_value) { + value = std::clamp(value, -1.0f, 1.0f); + } +} + +void SanitizeStick(Common::Input::AnalogStatus& analog_x, Common::Input::AnalogStatus& analog_y, + bool clamp_value) { + const auto& properties_x = analog_x.properties; + const auto& properties_y = analog_y.properties; + float& raw_x = analog_x.raw_value; + float& raw_y = analog_y.raw_value; + float& x = analog_x.value; + float& y = analog_y.value; + + if (!std::isnormal(raw_x)) { + raw_x = 0; + } + if (!std::isnormal(raw_y)) { + raw_y = 0; + } + + // Apply center offset + raw_x += properties_x.offset; + raw_y += properties_y.offset; + + // Apply X scale correction from offset + if (std::abs(properties_x.offset) < 0.75f) { + if (raw_x > 0) { + raw_x /= 1 + properties_x.offset; + } else { + raw_x /= 1 - properties_x.offset; + } + } + + // Apply Y scale correction from offset + if (std::abs(properties_y.offset) < 0.75f) { + if (raw_y > 0) { + raw_y /= 1 + properties_y.offset; + } else { + raw_y /= 1 - properties_y.offset; + } + } + + // Invert direction if needed + raw_x = properties_x.inverted ? -raw_x : raw_x; + raw_y = properties_y.inverted ? -raw_y : raw_y; + + // Set initial values to be formated + x = raw_x; + y = raw_y; + + // Calculate vector size + float r = x * x + y * y; + r = std::sqrt(r); + + // TODO(German77): Use deadzone and range of both axis + + // Return zero if values are smaller than the deadzone + if (r <= properties_x.deadzone || properties_x.deadzone >= 1.0f) { + x = 0; + y = 0; + return; + } + + // Adjust range of joystick + const float deadzone_factor = + 1.0f / r * (r - properties_x.deadzone) / (1.0f - properties_x.deadzone); + x = x * deadzone_factor / properties_x.range; + y = y * deadzone_factor / properties_x.range; + r = r * deadzone_factor / properties_x.range; + + // Normalize joystick + if (clamp_value && r > 1.0f) { + x /= r; + y /= r; + } +} + +} // namespace Core::HID diff --git a/src/core/hid/input_converter.h b/src/core/hid/input_converter.h new file mode 100644 index 000000000..8215e8255 --- /dev/null +++ b/src/core/hid/input_converter.h @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +namespace Common::Input { +struct CallbackStatus; +struct AnalogStatus; +struct ButtonStatus; +struct MotionStatus; +struct StickStatus; +struct TouchStatus; +struct TriggerStatus; +}; // namespace Common::Input + +namespace Core::HID { + +/** + * Converts raw input data into a valid button status. Applies invert properties to the output. + * + * @param callback Supported callbacks: Analog, Button, Trigger. + * @return A valid TouchStatus object. + */ +Common::Input::ButtonStatus TransformToButton(const Common::Input::CallbackStatus& callback); + +/** + * Converts raw input data into a valid motion status. + * + * @param callback Supported callbacks: Motion. + * @return A valid TouchStatus object. + */ +Common::Input::MotionStatus TransformToMotion(const Common::Input::CallbackStatus& callback); + +/** + * Converts raw input data into a valid stick status. Applies offset, deadzone, range and invert + * properties to the output. + * + * @param callback Supported callbacks: Stick. + * @return A valid StickStatus object. + */ +Common::Input::StickStatus TransformToStick(const Common::Input::CallbackStatus& callback); + +/** + * Converts raw input data into a valid touch status. + * + * @param callback Supported callbacks: Touch. + * @return A valid TouchStatus object. + */ +Common::Input::TouchStatus TransformToTouch(const Common::Input::CallbackStatus& callback); + +/** + * Converts raw input data into a valid trigger status. Applies offset, deadzone, range and + * invert properties to the output. Button status uses the threshold property if necessary. + * + * @param callback Supported callbacks: Analog, Button, Trigger. + * @return A valid TriggerStatus object. + */ +Common::Input::TriggerStatus TransformToTrigger(const Common::Input::CallbackStatus& callback); + +/** + * Converts raw input data into a valid analog status. Applies offset, deadzone, range and + * invert properties to the output. + * + * @param callback Supported callbacks: Analog. + * @return A valid AnalogStatus object. + */ +Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatus& callback); + +/** + * Converts raw analog data into a valid analog value + * @param analog An analog object containing raw data and properties + * @param clamp_value determines if the value needs to be clamped between -1.0f and 1.0f. + */ +void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value); + +/** + * Converts raw stick data into a valid stick value + * @param analog_x raw analog data and properties for the x-axis + * @param analog_y raw analog data and properties for the y-axis + * @param clamp_value bool that determines if the value needs to be clamped into the unit circle. + */ +void SanitizeStick(Common::Input::AnalogStatus& analog_x, Common::Input::AnalogStatus& analog_y, + bool clamp_value); + +} // namespace Core::HID diff --git a/src/core/hid/motion_input.cpp b/src/core/hid/motion_input.cpp new file mode 100644 index 000000000..b1f658e62 --- /dev/null +++ b/src/core/hid/motion_input.cpp @@ -0,0 +1,284 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/math_util.h" +#include "core/hid/motion_input.h" + +namespace Core::HID { + +MotionInput::MotionInput() { + // Initialize PID constants with default values + SetPID(0.3f, 0.005f, 0.0f); + SetGyroThreshold(0.007f); +} + +void MotionInput::SetPID(f32 new_kp, f32 new_ki, f32 new_kd) { + kp = new_kp; + ki = new_ki; + kd = new_kd; +} + +void MotionInput::SetAcceleration(const Common::Vec3f& acceleration) { + accel = acceleration; +} + +void MotionInput::SetGyroscope(const Common::Vec3f& gyroscope) { + gyro = gyroscope - gyro_bias; + + // Auto adjust drift to minimize drift + if (!IsMoving(0.1f)) { + gyro_bias = (gyro_bias * 0.9999f) + (gyroscope * 0.0001f); + } + + if (gyro.Length() < gyro_threshold) { + gyro = {}; + } else { + only_accelerometer = false; + } +} + +void MotionInput::SetQuaternion(const Common::Quaternion& quaternion) { + quat = quaternion; +} + +void MotionInput::SetGyroBias(const Common::Vec3f& bias) { + gyro_bias = bias; +} + +void MotionInput::SetGyroThreshold(f32 threshold) { + gyro_threshold = threshold; +} + +void MotionInput::EnableReset(bool reset) { + reset_enabled = reset; +} + +void MotionInput::ResetRotations() { + rotations = {}; +} + +bool MotionInput::IsMoving(f32 sensitivity) const { + return gyro.Length() >= sensitivity || accel.Length() <= 0.9f || accel.Length() >= 1.1f; +} + +bool MotionInput::IsCalibrated(f32 sensitivity) const { + return real_error.Length() < sensitivity; +} + +void MotionInput::UpdateRotation(u64 elapsed_time) { + const auto sample_period = static_cast(elapsed_time) / 1000000.0f; + if (sample_period > 0.1f) { + return; + } + rotations += gyro * sample_period; +} + +// Based on Madgwick's implementation of Mayhony's AHRS algorithm. +// https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs +void MotionInput::UpdateOrientation(u64 elapsed_time) { + if (!IsCalibrated(0.1f)) { + ResetOrientation(); + } + // Short name local variable for readability + f32 q1 = quat.w; + f32 q2 = quat.xyz[0]; + f32 q3 = quat.xyz[1]; + f32 q4 = quat.xyz[2]; + const auto sample_period = static_cast(elapsed_time) / 1000000.0f; + + // Ignore invalid elapsed time + if (sample_period > 0.1f) { + return; + } + + const auto normal_accel = accel.Normalized(); + auto rad_gyro = gyro * Common::PI * 2; + const f32 swap = rad_gyro.x; + rad_gyro.x = rad_gyro.y; + rad_gyro.y = -swap; + rad_gyro.z = -rad_gyro.z; + + // Clear gyro values if there is no gyro present + if (only_accelerometer) { + rad_gyro.x = 0; + rad_gyro.y = 0; + rad_gyro.z = 0; + } + + // Ignore drift correction if acceleration is not reliable + if (accel.Length() >= 0.75f && accel.Length() <= 1.25f) { + const f32 ax = -normal_accel.x; + const f32 ay = normal_accel.y; + const f32 az = -normal_accel.z; + + // Estimated direction of gravity + const f32 vx = 2.0f * (q2 * q4 - q1 * q3); + const f32 vy = 2.0f * (q1 * q2 + q3 * q4); + const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4; + + // Error is cross product between estimated direction and measured direction of gravity + const Common::Vec3f new_real_error = { + az * vx - ax * vz, + ay * vz - az * vy, + ax * vy - ay * vx, + }; + + derivative_error = new_real_error - real_error; + real_error = new_real_error; + + // Prevent integral windup + if (ki != 0.0f && !IsCalibrated(0.05f)) { + integral_error += real_error; + } else { + integral_error = {}; + } + + // Apply feedback terms + if (!only_accelerometer) { + rad_gyro += kp * real_error; + rad_gyro += ki * integral_error; + rad_gyro += kd * derivative_error; + } else { + // Give more weight to accelerometer values to compensate for the lack of gyro + rad_gyro += 35.0f * kp * real_error; + rad_gyro += 10.0f * ki * integral_error; + rad_gyro += 10.0f * kd * derivative_error; + + // Emulate gyro values for games that need them + gyro.x = -rad_gyro.y; + gyro.y = rad_gyro.x; + gyro.z = -rad_gyro.z; + UpdateRotation(elapsed_time); + } + } + + const f32 gx = rad_gyro.y; + const f32 gy = rad_gyro.x; + const f32 gz = rad_gyro.z; + + // Integrate rate of change of quaternion + const f32 pa = q2; + const f32 pb = q3; + const f32 pc = q4; + q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period); + q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period); + q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period); + q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period); + + quat.w = q1; + quat.xyz[0] = q2; + quat.xyz[1] = q3; + quat.xyz[2] = q4; + quat = quat.Normalized(); +} + +std::array MotionInput::GetOrientation() const { + const Common::Quaternion quad{ + .xyz = {-quat.xyz[1], -quat.xyz[0], -quat.w}, + .w = -quat.xyz[2], + }; + const std::array matrix4x4 = quad.ToMatrix(); + + return {Common::Vec3f(matrix4x4[0], matrix4x4[1], -matrix4x4[2]), + Common::Vec3f(matrix4x4[4], matrix4x4[5], -matrix4x4[6]), + Common::Vec3f(-matrix4x4[8], -matrix4x4[9], matrix4x4[10])}; +} + +Common::Vec3f MotionInput::GetAcceleration() const { + return accel; +} + +Common::Vec3f MotionInput::GetGyroscope() const { + return gyro; +} + +Common::Vec3f MotionInput::GetGyroBias() const { + return gyro_bias; +} + +Common::Quaternion MotionInput::GetQuaternion() const { + return quat; +} + +Common::Vec3f MotionInput::GetRotations() const { + return rotations; +} + +void MotionInput::ResetOrientation() { + if (!reset_enabled || only_accelerometer) { + return; + } + if (!IsMoving(0.5f) && accel.z <= -0.9f) { + ++reset_counter; + if (reset_counter > 900) { + quat.w = 0; + quat.xyz[0] = 0; + quat.xyz[1] = 0; + quat.xyz[2] = -1; + SetOrientationFromAccelerometer(); + integral_error = {}; + reset_counter = 0; + } + } else { + reset_counter = 0; + } +} + +void MotionInput::SetOrientationFromAccelerometer() { + int iterations = 0; + const f32 sample_period = 0.015f; + + const auto normal_accel = accel.Normalized(); + + while (!IsCalibrated(0.01f) && ++iterations < 100) { + // Short name local variable for readability + f32 q1 = quat.w; + f32 q2 = quat.xyz[0]; + f32 q3 = quat.xyz[1]; + f32 q4 = quat.xyz[2]; + + Common::Vec3f rad_gyro; + const f32 ax = -normal_accel.x; + const f32 ay = normal_accel.y; + const f32 az = -normal_accel.z; + + // Estimated direction of gravity + const f32 vx = 2.0f * (q2 * q4 - q1 * q3); + const f32 vy = 2.0f * (q1 * q2 + q3 * q4); + const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4; + + // Error is cross product between estimated direction and measured direction of gravity + const Common::Vec3f new_real_error = { + az * vx - ax * vz, + ay * vz - az * vy, + ax * vy - ay * vx, + }; + + derivative_error = new_real_error - real_error; + real_error = new_real_error; + + rad_gyro += 10.0f * kp * real_error; + rad_gyro += 5.0f * ki * integral_error; + rad_gyro += 10.0f * kd * derivative_error; + + const f32 gx = rad_gyro.y; + const f32 gy = rad_gyro.x; + const f32 gz = rad_gyro.z; + + // Integrate rate of change of quaternion + const f32 pa = q2; + const f32 pb = q3; + const f32 pc = q4; + q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period); + q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period); + q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period); + q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period); + + quat.w = q1; + quat.xyz[0] = q2; + quat.xyz[1] = q3; + quat.xyz[2] = q4; + quat = quat.Normalized(); + } +} +} // namespace Core::HID diff --git a/src/core/hid/motion_input.h b/src/core/hid/motion_input.h new file mode 100644 index 000000000..f5fd90db5 --- /dev/null +++ b/src/core/hid/motion_input.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" +#include "common/quaternion.h" +#include "common/vector_math.h" + +namespace Core::HID { + +class MotionInput { +public: + explicit MotionInput(); + + MotionInput(const MotionInput&) = default; + MotionInput& operator=(const MotionInput&) = default; + + MotionInput(MotionInput&&) = default; + MotionInput& operator=(MotionInput&&) = default; + + void SetPID(f32 new_kp, f32 new_ki, f32 new_kd); + void SetAcceleration(const Common::Vec3f& acceleration); + void SetGyroscope(const Common::Vec3f& gyroscope); + void SetQuaternion(const Common::Quaternion& quaternion); + void SetGyroBias(const Common::Vec3f& bias); + void SetGyroThreshold(f32 threshold); + + void EnableReset(bool reset); + void ResetRotations(); + + void UpdateRotation(u64 elapsed_time); + void UpdateOrientation(u64 elapsed_time); + + [[nodiscard]] std::array GetOrientation() const; + [[nodiscard]] Common::Vec3f GetAcceleration() const; + [[nodiscard]] Common::Vec3f GetGyroscope() const; + [[nodiscard]] Common::Vec3f GetGyroBias() const; + [[nodiscard]] Common::Vec3f GetRotations() const; + [[nodiscard]] Common::Quaternion GetQuaternion() const; + + [[nodiscard]] bool IsMoving(f32 sensitivity) const; + [[nodiscard]] bool IsCalibrated(f32 sensitivity) const; + +private: + void ResetOrientation(); + void SetOrientationFromAccelerometer(); + + // PID constants + f32 kp; + f32 ki; + f32 kd; + + // PID errors + Common::Vec3f real_error; + Common::Vec3f integral_error; + Common::Vec3f derivative_error; + + // Quaternion containing the device orientation + Common::Quaternion quat{{0.0f, 0.0f, -1.0f}, 0.0f}; + + // Number of full rotations in each axis + Common::Vec3f rotations; + + // Acceleration vector measurement in G force + Common::Vec3f accel; + + // Gyroscope vector measurement in radians/s. + Common::Vec3f gyro; + + // Vector to be substracted from gyro measurements + Common::Vec3f gyro_bias; + + // Minimum gyro amplitude to detect if the device is moving + f32 gyro_threshold = 0.0f; + + // Number of invalid sequential data + u32 reset_counter = 0; + + // If the provided data is invalid the device will be autocalibrated + bool reset_enabled = true; + + // Use accelerometer values to calculate position + bool only_accelerometer = true; +}; + +} // namespace Core::HID diff --git a/src/core/hle/service/ir/extra_hid.h b/src/core/hle/service/ir/extra_hid.h index d498c471f..5899f2b47 100644 --- a/src/core/hle/service/ir/extra_hid.h +++ b/src/core/hle/service/ir/extra_hid.h @@ -8,8 +8,8 @@ #include #include #include "common/bit_field.h" +#include "common/input.h" #include "common/swap.h" -#include "core/frontend/input.h" #include "core/hle/service/ir/ir_user.h" namespace Core { @@ -49,31 +49,20 @@ public: void OnDisconnect() override; void OnReceive(const std::vector& data) override; - /// Requests input devices reload from current settings. Called when the input settings change. - void RequestInputDevicesReload(); - private: void SendHIDStatus(); void HandleConfigureHIDPollingRequest(const std::vector& request); void HandleReadCalibrationDataRequest(const std::vector& request); - void LoadInputDevices(); Core::Timing& timing; u8 hid_period; Core::TimingEventType* hid_polling_callback_id; std::array calibration_data; - std::unique_ptr zl; - std::unique_ptr zr; - std::unique_ptr c_stick; - std::atomic is_device_reload_pending; template void serialize(Archive& ar, const unsigned int) { ar& hid_period; ar& calibration_data; // This isn't writeable for now, but might be in future - if (Archive::is_loading::value) { - LoadInputDevices(); // zl, zr, c_stick are loaded here - } } friend class boost::serialization::access; }; diff --git a/src/core/hle/service/ir/ir_rst.h b/src/core/hle/service/ir/ir_rst.h index 2514ab6f9..42b0613e6 100644 --- a/src/core/hle/service/ir/ir_rst.h +++ b/src/core/hle/service/ir/ir_rst.h @@ -8,8 +8,8 @@ #include #include "common/bit_field.h" #include "common/common_types.h" +#include "common/input.h" #include "common/swap.h" -#include "core/frontend/input.h" #include "core/hle/service/service.h" namespace Kernel { @@ -40,7 +40,6 @@ class IR_RST final : public ServiceFramework { public: explicit IR_RST(Core::System& system); ~IR_RST(); - void ReloadInputDevices(); private: /** @@ -72,8 +71,6 @@ private: */ void Shutdown(Kernel::HLERequestContext& ctx); - void LoadInputDevices(); - void UnloadInputDevices(); void UpdateCallback(std::uintptr_t user_data, s64 cycles_late); Core::System& system; @@ -81,10 +78,6 @@ private: std::shared_ptr shared_memory; u32 next_pad_index{0}; Core::TimingEventType* update_callback_id; - std::unique_ptr zl_button; - std::unique_ptr zr_button; - std::unique_ptr c_stick; - std::atomic is_device_reload_pending{false}; bool raw_c_stick{false}; int update_period{0}; diff --git a/src/core/hle/service/ir/ir_user.cpp b/src/core/hle/service/ir/ir_user.cpp index 1900db902..e0fa3a73c 100644 --- a/src/core/hle/service/ir/ir_user.cpp +++ b/src/core/hle/service/ir/ir_user.cpp @@ -473,10 +473,6 @@ IR_USER::~IR_USER() { } } -void IR_USER::ReloadInputDevices() { - extra_hid->RequestInputDevicesReload(); -} - IRDevice::IRDevice(SendFunc send_func_) : send_func(send_func_) {} IRDevice::~IRDevice() = default; diff --git a/src/core/hle/service/ir/ir_user.h b/src/core/hle/service/ir/ir_user.h index afb9be4f7..da6a27555 100644 --- a/src/core/hle/service/ir/ir_user.h +++ b/src/core/hle/service/ir/ir_user.h @@ -55,8 +55,6 @@ public: explicit IR_USER(Core::System& system); ~IR_USER(); - void ReloadInputDevices(); - private: /** * InitializeIrNopShared service function diff --git a/src/input_common/drivers/udp_client.h b/src/input_common/drivers/udp_client.h index cea9f579a..ce6c72d92 100644 --- a/src/input_common/drivers/udp_client.h +++ b/src/input_common/drivers/udp_client.h @@ -141,9 +141,6 @@ private: // Translates configuration to client number std::size_t GetClientNumber(std::string_view host, u16 port) const; - // Translates UDP battery level to input engine battery level - Common::Input::BatteryLevel GetBatteryLevel(Response::Battery battery) const; - void OnVersion(Response::Version); void OnPortInfo(Response::PortInfo); void OnPadData(Response::PadData, std::size_t client); diff --git a/src/input_common/input_mapping.cpp b/src/input_common/input_mapping.cpp index edd5287c1..dae687e4e 100644 --- a/src/input_common/input_mapping.cpp +++ b/src/input_common/input_mapping.cpp @@ -195,8 +195,7 @@ bool MappingFactory::IsDriverValid(const MappingData& data) const { return false; } // To prevent mapping with two devices we disable any UDP except motion - if (!Settings::values.enable_udp_controller && data.engine == "cemuhookudp" && - data.type != EngineInputType::Motion) { + if (data.engine == "cemuhookudp" && data.type != EngineInputType::Motion) { return false; } // The following drivers don't need to be mapped