From 3b9490e633782177355d3c73662c4eab18e607eb Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 27 Mar 2024 19:11:47 +0000 Subject: [PATCH] Add controller support to Qt (#475) * Add controllers to Qt Co-Authored-By: Nadia Holmquist Pedersen <893884+nadiaholmquist@users.noreply.github.com> * Remove debug logs * Bonk --------- Co-authored-by: Nadia Holmquist Pedersen <893884+nadiaholmquist@users.noreply.github.com> --- include/panda_qt/main_window.hpp | 16 ++++- src/panda_qt/main_window.cpp | 117 ++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index c3f99c29..208da2c3 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -13,8 +15,8 @@ #include "emulator.hpp" #include "input_mappings.hpp" #include "panda_qt/about_window.hpp" -#include "panda_qt/config_window.hpp" #include "panda_qt/cheats_window.hpp" +#include "panda_qt/config_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" @@ -96,6 +98,10 @@ class MainWindow : public QMainWindow { TextEditorWindow* luaEditor; QMenuBar* menuBar = nullptr; + // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible + SDL_GameController* gameController = nullptr; + int gameControllerID = 0; + void swapEmuBuffer(); void emuThreadMainLoop(); void selectLuaFile(); @@ -104,6 +110,8 @@ class MainWindow : public QMainWindow { void openLuaEditor(); void openCheatsEditor(); void showAboutMenu(); + void initControllers(); + void pollControllers(); void sendMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message); @@ -111,6 +119,12 @@ class MainWindow : public QMainWindow { bool usingGL = false; bool usingVk = false; + // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard + // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state + // And so the user can still use the keyboard to control the analog + bool keyboardAnalogX = false; + bool keyboardAnalogY = false; + public: MainWindow(QApplication* app, QWidget* parent = nullptr); ~MainWindow(); diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 3ff1049c..a4fc20f0 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -97,6 +97,8 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) Helpers::panic("Unsupported graphics backend for Qt frontend!"); } + // We have to initialize controllers on the same thread they'll be polled in + initControllers(); emuThreadMainLoop(); }); } @@ -117,6 +119,8 @@ void MainWindow::emuThreadMainLoop() { } emu->runFrame(); + pollControllers(); + if (emu->romType != ROMType::None) { emu->getServiceManager().getHID().updateInputs(emu->getTicks()); } @@ -279,8 +283,21 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { case MessageType::Reset: emu->reset(Emulator::ReloadOption::Reload); break; case MessageType::PressKey: emu->getServiceManager().getHID().pressKey(message.key.key); break; case MessageType::ReleaseKey: emu->getServiceManager().getHID().releaseKey(message.key.key); break; - case MessageType::SetCirclePadX: emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); break; - case MessageType::SetCirclePadY: emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); break; + + // Track whether we're controlling the analog stick with our controller and update the CirclePad X/Y values in HID + // Controllers are polled on the emulator thread, so this message type is only used when the circlepad is changed via keyboard input + case MessageType::SetCirclePadX: { + keyboardAnalogX = message.circlepad.value != 0; + emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); + break; + } + + case MessageType::SetCirclePadY: { + keyboardAnalogY = message.circlepad.value != 0; + emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); + break; + } + case MessageType::PressTouchscreen: emu->getServiceManager().getHID().setTouchScreenPress(message.touchscreen.x, message.touchscreen.y); break; @@ -397,4 +414,100 @@ void MainWindow::editCheat(u32 handle, const std::vector& cheat, const message.cheat.c = c; sendMessage(message); +} + +void MainWindow::initControllers() { + // Make SDL use consistent positional button mapping + SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); + if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0) { + Helpers::warn("Failed to initialize SDL2 GameController: %s", SDL_GetError()); + return; + } + + if (SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { + gameController = SDL_GameControllerOpen(0); + + if (gameController != nullptr) { + SDL_Joystick* stick = SDL_GameControllerGetJoystick(gameController); + gameControllerID = SDL_JoystickInstanceID(stick); + } + } +} + +void MainWindow::pollControllers() { + // Update circlepad if a controller is plugged in + if (gameController != nullptr) { + HIDService& hid = emu->getServiceManager().getHID(); + const s16 stickX = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTX); + const s16 stickY = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTY); + constexpr s16 deadzone = 3276; + constexpr s16 maxValue = 0x9C; + constexpr s16 div = 0x8000 / maxValue; + + // Avoid overriding the keyboard's circlepad input + if (std::abs(stickX) < deadzone && !keyboardAnalogX) { + hid.setCirclepadX(0); + } else { + hid.setCirclepadX(stickX / div); + } + + if (std::abs(stickY) < deadzone && !keyboardAnalogY) { + hid.setCirclepadY(0); + } else { + hid.setCirclepadY(-(stickY / div)); + } + } + + SDL_Event event; + while (SDL_PollEvent(&event)) { + HIDService& hid = emu->getServiceManager().getHID(); + using namespace HID; + + switch (event.type) { + case SDL_CONTROLLERDEVICEADDED: + if (gameController == nullptr) { + gameController = SDL_GameControllerOpen(event.cdevice.which); + gameControllerID = event.cdevice.which; + } + break; + + case SDL_CONTROLLERDEVICEREMOVED: + if (event.cdevice.which == gameControllerID) { + SDL_GameControllerClose(gameController); + gameController = nullptr; + gameControllerID = 0; + } + break; + + case SDL_CONTROLLERBUTTONUP: + case SDL_CONTROLLERBUTTONDOWN: { + if (emu->romType == ROMType::None) break; + u32 key = 0; + + switch (event.cbutton.button) { + case SDL_CONTROLLER_BUTTON_A: key = Keys::B; break; + case SDL_CONTROLLER_BUTTON_B: key = Keys::A; break; + case SDL_CONTROLLER_BUTTON_X: key = Keys::Y; break; + case SDL_CONTROLLER_BUTTON_Y: key = Keys::X; break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: key = Keys::L; break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: key = Keys::R; break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: key = Keys::Left; break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: key = Keys::Right; break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: key = Keys::Up; break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: key = Keys::Down; break; + case SDL_CONTROLLER_BUTTON_BACK: key = Keys::Select; break; + case SDL_CONTROLLER_BUTTON_START: key = Keys::Start; break; + } + + if (key != 0) { + if (event.cbutton.state == SDL_PRESSED) { + hid.pressKey(key); + } else { + hid.releaseKey(key); + } + } + break; + } + } + } } \ No newline at end of file