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>
This commit is contained in:
wheremyfoodat 2024-03-27 19:11:47 +00:00 committed by GitHub
parent 429dc2a944
commit 3b9490e633
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 3 deletions

View file

@ -1,5 +1,7 @@
#pragma once
#include <SDL.h>
#include <QApplication>
#include <QMenuBar>
#include <QtWidgets>
@ -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();

View file

@ -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<uint8_t>& 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;
}
}
}
}