diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index fac43c039..622793ec2 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -52,6 +52,10 @@ set(SRCS
gamepropertiesdialog.cpp
gamepropertiesdialog.h
gamepropertiesdialog.ui
+ gdbconnection.cpp
+ gdbconnection.h
+ gdbserver.cpp
+ gdbserver.h
generalsettingswidget.cpp
generalsettingswidget.h
generalsettingswidget.ui
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 67b74ccda..c9ea12336 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -75,6 +75,8 @@
+
+
@@ -119,6 +121,8 @@
+
+
@@ -221,6 +225,8 @@
+
+
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index 7e0a6b93a..88296c22a 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -31,6 +31,10 @@
+
+
+
+
@@ -101,6 +105,8 @@
+
+
diff --git a/src/duckstation-qt/gdbconnection.cpp b/src/duckstation-qt/gdbconnection.cpp
new file mode 100644
index 000000000..c81181ce4
--- /dev/null
+++ b/src/duckstation-qt/gdbconnection.cpp
@@ -0,0 +1,82 @@
+#include "gdbconnection.h"
+#include "qthostinterface.h"
+#include "common/log.h"
+#include "core/gdb_protocol.h"
+Log_SetChannel(GDBConnection);
+
+GDBConnection::GDBConnection(QObject *parent, int descriptor)
+ : QThread(parent), m_descriptor(descriptor)
+{
+ Log_InfoPrintf("(%u) Accepted new connection on GDB server", m_descriptor);
+
+ connect(&m_socket, &QTcpSocket::readyRead, this, &GDBConnection::receivedData);
+ connect(&m_socket, &QTcpSocket::disconnected, this, &GDBConnection::gotDisconnected);
+
+ if (m_socket.setSocketDescriptor(m_descriptor)) {
+ QtHostInterface::GetInstance()->pauseSystem(true, true);
+ }
+ else {
+ Log_ErrorPrintf("(%u) Failed to set socket descriptor: %s", m_descriptor, m_socket.errorString().toUtf8().constData());
+ }
+}
+
+void GDBConnection::gotDisconnected()
+{
+ Log_InfoPrintf("(%u) Client disconnected", m_descriptor);
+ this->exit(0);
+}
+
+void GDBConnection::receivedData()
+{
+ qint64 bytesRead;
+ char buffer[256];
+
+ while ((bytesRead = m_socket.read(buffer, sizeof(buffer))) > 0) {
+ for (char c : std::string_view(buffer, bytesRead)) {
+ m_readBuffer.push_back(c);
+
+ if (GDBProtocol::IsPacketInterrupt(m_readBuffer)) {
+ Log_DebugPrintf("(%u) > Interrupt request", m_descriptor);
+ QtHostInterface::GetInstance()->pauseSystem(true, true);
+ m_readBuffer.erase();
+ }
+ else if (GDBProtocol::IsPacketContinue(m_readBuffer)) {
+ Log_DebugPrintf("(%u) > Continue request", m_descriptor);
+ QtHostInterface::GetInstance()->pauseSystem(false, false);
+ m_readBuffer.erase();
+ }
+ else if (GDBProtocol::IsPacketComplete(m_readBuffer)) {
+ Log_DebugPrintf("(%u) > %s", m_descriptor, m_readBuffer.c_str());
+ writePacket(GDBProtocol::ProcessPacket(m_readBuffer));
+ m_readBuffer.erase();
+ }
+ }
+ }
+ if (bytesRead == -1) {
+ Log_ErrorPrintf("(%u) Failed to read from socket: %s", m_descriptor, m_socket.errorString().toUtf8().constData());
+ }
+}
+
+void GDBConnection::onEmulationPaused(bool paused)
+{
+ if (paused) {
+ if (m_seen_resume) {
+ m_seen_resume = false;
+ // Generate a stop reply packet, insert '?' command to generate it.
+ writePacket(GDBProtocol::ProcessPacket("$?#3f"));
+ }
+ }
+ else {
+ m_seen_resume = true;
+ // Send ack, in case GDB sent a continue request.
+ writePacket("+");
+ }
+}
+
+void GDBConnection::writePacket(std::string_view packet)
+{
+ Log_DebugPrintf("(%u) < %*s", m_descriptor, packet.length(), packet.data());
+ if (m_socket.write(packet.data(), packet.length()) == -1) {
+ Log_ErrorPrintf("(%u) Failed to write to socket: %s", m_descriptor, m_socket.errorString().toUtf8().constData());
+ }
+}
diff --git a/src/duckstation-qt/gdbconnection.h b/src/duckstation-qt/gdbconnection.h
new file mode 100644
index 000000000..b3917ec22
--- /dev/null
+++ b/src/duckstation-qt/gdbconnection.h
@@ -0,0 +1,24 @@
+#pragma once
+#include
+#include
+
+class GDBConnection : public QThread
+{
+ Q_OBJECT
+
+public:
+ GDBConnection(QObject *parent, int descriptor);
+
+public Q_SLOTS:
+ void gotDisconnected();
+ void receivedData();
+ void onEmulationPaused(bool paused);
+
+private:
+ void writePacket(std::string_view data);
+
+ int m_descriptor;
+ QTcpSocket m_socket;
+ std::string m_readBuffer;
+ bool m_seen_resume;
+};
diff --git a/src/duckstation-qt/gdbserver.cpp b/src/duckstation-qt/gdbserver.cpp
new file mode 100644
index 000000000..970f592e5
--- /dev/null
+++ b/src/duckstation-qt/gdbserver.cpp
@@ -0,0 +1,35 @@
+#include "gdbserver.h"
+#include "gdbconnection.h"
+#include "common/log.h"
+#include "qthostinterface.h"
+Log_SetChannel(GDBServer);
+
+GDBServer::GDBServer(QObject *parent, u16 port)
+ : QTcpServer(parent)
+{
+ if (listen(QHostAddress::LocalHost, port)) {
+ Log_InfoPrintf("GDB server listening on TCP port %u", port);
+ }
+ else {
+ Log_InfoPrintf("Failed to listen on TCP port %u for GDB server: %s", port, errorString().toUtf8().constData());
+ }
+}
+
+GDBServer::~GDBServer()
+{
+ Log_InfoPrint("GDB server stopped");
+ for (auto* thread : m_connections) {
+ thread->quit();
+ thread->wait();
+ delete thread;
+ }
+}
+
+void GDBServer::incomingConnection(qintptr descriptor)
+{
+ Log_InfoPrint("Accepted connection on GDB server");
+ GDBConnection *thread = new GDBConnection(this, descriptor);
+ connect(QtHostInterface::GetInstance(), &QtHostInterface::emulationPaused, thread, &GDBConnection::onEmulationPaused);
+ thread->start();
+ m_connections.push_back(thread);
+}
diff --git a/src/duckstation-qt/gdbserver.h b/src/duckstation-qt/gdbserver.h
new file mode 100644
index 000000000..d9cc7febf
--- /dev/null
+++ b/src/duckstation-qt/gdbserver.h
@@ -0,0 +1,19 @@
+#pragma once
+#include "core/types.h"
+#include "gdbconnection.h"
+#include
+
+class GDBServer : public QTcpServer
+{
+ Q_OBJECT
+
+public:
+ GDBServer(QObject* parent, u16 port);
+ ~GDBServer();
+
+protected:
+ void incomingConnection(qintptr socketDescriptor) override;
+
+private:
+ std::list m_connections;
+};
diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
index 427d8e4bd..f4fda8dea 100644
--- a/src/duckstation-qt/mainwindow.cpp
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -11,6 +11,7 @@
#include "gamelistsettingswidget.h"
#include "gamelistwidget.h"
#include "gamepropertiesdialog.h"
+#include "gdbserver.h"
#include "memorycardeditordialog.h"
#include "qtdisplaywidget.h"
#include "qthostinterface.h"
@@ -814,6 +815,16 @@ void MainWindow::updateEmulationActions(bool starting, bool running)
}
}
+ if (g_settings.debugging.enable_gdb_server) {
+ if (starting && !m_gdb_server) {
+ m_gdb_server = new GDBServer(this, g_settings.debugging.gdb_server_port);
+ }
+ else if (!running && m_gdb_server) {
+ delete m_gdb_server;
+ m_gdb_server = nullptr;
+ }
+ }
+
m_ui.statusBar->clearMessage();
}
diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h
index a5bdc1e56..19ba30612 100644
--- a/src/duckstation-qt/mainwindow.h
+++ b/src/duckstation-qt/mainwindow.h
@@ -21,6 +21,8 @@ class DebuggerWindow;
class HostDisplay;
struct GameListEntry;
+class GDBServer;
+
class MainWindow final : public QMainWindow
{
Q_OBJECT
@@ -145,4 +147,6 @@ private:
bool m_emulation_running = false;
bool m_was_paused_by_focus_loss = false;
+
+ GDBServer* m_gdb_server = nullptr;
};