From c76cca874b0fc47e050a0466a704ecfc5aff2f7f Mon Sep 17 00:00:00 2001
From: chaoticgd <43898262+chaoticgd@users.noreply.github.com>
Date: Sat, 7 Dec 2024 17:36:29 +0000
Subject: [PATCH] Debugger: Redesign UI based on KDDockWidgets

---
 pcsx2-qt/CMakeLists.txt                       |  52 +-
 .../{ => Breakpoints}/BreakpointDialog.cpp    |   0
 .../{ => Breakpoints}/BreakpointDialog.h      |   4 +-
 .../{ => Breakpoints}/BreakpointDialog.ui     |   0
 .../BreakpointModel.cpp                       |   0
 .../{Models => Breakpoints}/BreakpointModel.h |   0
 .../Debugger/Breakpoints/BreakpointWidget.cpp | 183 +++++
 .../Debugger/Breakpoints/BreakpointWidget.h   |  41 +
 .../Debugger/Breakpoints/BreakpointWidget.ui  |  43 +
 pcsx2-qt/Debugger/CpuWidget.cpp               | 732 ------------------
 pcsx2-qt/Debugger/CpuWidget.h                 |  97 ---
 pcsx2-qt/Debugger/CpuWidget.ui                | 445 -----------
 pcsx2-qt/Debugger/DebuggerSettingsManager.cpp |   6 +-
 pcsx2-qt/Debugger/DebuggerSettingsManager.h   |   4 +-
 pcsx2-qt/Debugger/DebuggerWidget.cpp          |  31 +
 pcsx2-qt/Debugger/DebuggerWidget.h            |  28 +
 pcsx2-qt/Debugger/DebuggerWindow.cpp          |  50 +-
 pcsx2-qt/Debugger/DebuggerWindow.h            |  13 +-
 pcsx2-qt/Debugger/DebuggerWindow.ui           |  79 +-
 pcsx2-qt/Debugger/DisassemblyWidget.cpp       |  90 ++-
 pcsx2-qt/Debugger/DisassemblyWidget.h         |  13 +-
 pcsx2-qt/Debugger/DockManager.cpp             | 108 +++
 pcsx2-qt/Debugger/DockManager.h               |  42 +
 .../{ => Memory}/MemorySearchWidget.cpp       |  38 +-
 .../{ => Memory}/MemorySearchWidget.h         |  26 +-
 .../{ => Memory}/MemorySearchWidget.ui        |   0
 .../{ => Memory}/MemoryViewWidget.cpp         |  38 +-
 .../Debugger/{ => Memory}/MemoryViewWidget.h  |  13 +-
 .../Debugger/{ => Memory}/MemoryViewWidget.ui |   3 +
 .../SavedAddressesModel.cpp                   |   0
 .../{Models => Memory}/SavedAddressesModel.h  |   0
 .../Debugger/Memory/SavedAddressesWidget.cpp  | 158 ++++
 .../Debugger/Memory/SavedAddressesWidget.h    |  29 +
 .../Debugger/Memory/SavedAddressesWidget.ui   |  43 +
 pcsx2-qt/Debugger/RegisterWidget.cpp          |  99 ++-
 pcsx2-qt/Debugger/RegisterWidget.h            |  11 +-
 pcsx2-qt/Debugger/RegisterWidget.ui           |   6 +-
 pcsx2-qt/Debugger/{Models => }/StackModel.cpp |   0
 pcsx2-qt/Debugger/{Models => }/StackModel.h   |   0
 pcsx2-qt/Debugger/StackWidget.cpp             |  76 ++
 pcsx2-qt/Debugger/StackWidget.h               |  26 +
 pcsx2-qt/Debugger/StackWidget.ui              |  43 +
 .../Debugger/SymbolTree/SymbolTreeWidget.ui   |   4 +-
 .../Debugger/SymbolTree/SymbolTreeWidgets.cpp |  32 +-
 .../Debugger/SymbolTree/SymbolTreeWidgets.h   |  10 +-
 .../Debugger/{Models => }/ThreadModel.cpp     |   0
 pcsx2-qt/Debugger/{Models => }/ThreadModel.h  |   0
 pcsx2-qt/Debugger/ThreadWidget.cpp            |  75 ++
 pcsx2-qt/Debugger/ThreadWidget.h              |  28 +
 pcsx2-qt/Debugger/ThreadWidget.ui             |  43 +
 pcsx2-qt/MainWindow.cpp                       |   5 +
 pcsx2-qt/QtHost.h                             |   2 +-
 pcsx2-qt/pcsx2-qt.vcxproj                     |  80 +-
 pcsx2-qt/pcsx2-qt.vcxproj.filters             | 130 +++-
 54 files changed, 1475 insertions(+), 1604 deletions(-)
 rename pcsx2-qt/Debugger/{ => Breakpoints}/BreakpointDialog.cpp (100%)
 rename pcsx2-qt/Debugger/{ => Breakpoints}/BreakpointDialog.h (95%)
 rename pcsx2-qt/Debugger/{ => Breakpoints}/BreakpointDialog.ui (100%)
 rename pcsx2-qt/Debugger/{Models => Breakpoints}/BreakpointModel.cpp (100%)
 rename pcsx2-qt/Debugger/{Models => Breakpoints}/BreakpointModel.h (100%)
 create mode 100644 pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.cpp
 create mode 100644 pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.h
 create mode 100644 pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.ui
 delete mode 100644 pcsx2-qt/Debugger/CpuWidget.cpp
 delete mode 100644 pcsx2-qt/Debugger/CpuWidget.h
 delete mode 100644 pcsx2-qt/Debugger/CpuWidget.ui
 create mode 100644 pcsx2-qt/Debugger/DebuggerWidget.cpp
 create mode 100644 pcsx2-qt/Debugger/DebuggerWidget.h
 create mode 100644 pcsx2-qt/Debugger/DockManager.cpp
 create mode 100644 pcsx2-qt/Debugger/DockManager.h
 rename pcsx2-qt/Debugger/{ => Memory}/MemorySearchWidget.cpp (95%)
 rename pcsx2-qt/Debugger/{ => Memory}/MemorySearchWidget.h (90%)
 rename pcsx2-qt/Debugger/{ => Memory}/MemorySearchWidget.ui (100%)
 rename pcsx2-qt/Debugger/{ => Memory}/MemoryViewWidget.cpp (96%)
 rename pcsx2-qt/Debugger/{ => Memory}/MemoryViewWidget.h (93%)
 rename pcsx2-qt/Debugger/{ => Memory}/MemoryViewWidget.ui (81%)
 rename pcsx2-qt/Debugger/{Models => Memory}/SavedAddressesModel.cpp (100%)
 rename pcsx2-qt/Debugger/{Models => Memory}/SavedAddressesModel.h (100%)
 create mode 100644 pcsx2-qt/Debugger/Memory/SavedAddressesWidget.cpp
 create mode 100644 pcsx2-qt/Debugger/Memory/SavedAddressesWidget.h
 create mode 100644 pcsx2-qt/Debugger/Memory/SavedAddressesWidget.ui
 rename pcsx2-qt/Debugger/{Models => }/StackModel.cpp (100%)
 rename pcsx2-qt/Debugger/{Models => }/StackModel.h (100%)
 create mode 100644 pcsx2-qt/Debugger/StackWidget.cpp
 create mode 100644 pcsx2-qt/Debugger/StackWidget.h
 create mode 100644 pcsx2-qt/Debugger/StackWidget.ui
 rename pcsx2-qt/Debugger/{Models => }/ThreadModel.cpp (100%)
 rename pcsx2-qt/Debugger/{Models => }/ThreadModel.h (100%)
 create mode 100644 pcsx2-qt/Debugger/ThreadWidget.cpp
 create mode 100644 pcsx2-qt/Debugger/ThreadWidget.h
 create mode 100644 pcsx2-qt/Debugger/ThreadWidget.ui

diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt
index 2d8a1af252..1bdff961fc 100644
--- a/pcsx2-qt/CMakeLists.txt
+++ b/pcsx2-qt/CMakeLists.txt
@@ -158,37 +158,48 @@ target_sources(pcsx2-qt PRIVATE
 	Debugger/AnalysisOptionsDialog.cpp
 	Debugger/AnalysisOptionsDialog.h
 	Debugger/AnalysisOptionsDialog.ui
-	Debugger/CpuWidget.cpp
-	Debugger/CpuWidget.h
-	Debugger/CpuWidget.ui
 	Debugger/DebuggerSettingsManager.cpp
 	Debugger/DebuggerSettingsManager.h
+	Debugger/DebuggerWidget.cpp
+	Debugger/DebuggerWidget.h
 	Debugger/DebuggerWindow.cpp
 	Debugger/DebuggerWindow.h
 	Debugger/DebuggerWindow.ui
 	Debugger/DisassemblyWidget.cpp
 	Debugger/DisassemblyWidget.h
 	Debugger/DisassemblyWidget.ui
-	Debugger/MemorySearchWidget.cpp
-	Debugger/MemorySearchWidget.h
-	Debugger/MemorySearchWidget.ui
-	Debugger/MemoryViewWidget.cpp
-	Debugger/MemoryViewWidget.h
-	Debugger/MemoryViewWidget.ui
+	Debugger/DockManager.cpp
+	Debugger/DockManager.h
 	Debugger/RegisterWidget.cpp
 	Debugger/RegisterWidget.h
 	Debugger/RegisterWidget.ui
-	Debugger/BreakpointDialog.cpp
-	Debugger/BreakpointDialog.h
-	Debugger/BreakpointDialog.ui
-	Debugger/Models/BreakpointModel.cpp
-	Debugger/Models/BreakpointModel.h
-	Debugger/Models/ThreadModel.cpp
-	Debugger/Models/ThreadModel.h
-	Debugger/Models/StackModel.cpp
-	Debugger/Models/StackModel.h
-	Debugger/Models/SavedAddressesModel.cpp
-	Debugger/Models/SavedAddressesModel.h
+	Debugger/StackModel.cpp
+	Debugger/StackModel.h
+	Debugger/StackWidget.cpp
+	Debugger/StackWidget.h
+	Debugger/ThreadModel.cpp
+	Debugger/ThreadModel.h
+	Debugger/ThreadWidget.cpp
+	Debugger/ThreadWidget.h
+	Debugger/Breakpoints/BreakpointDialog.cpp
+	Debugger/Breakpoints/BreakpointDialog.h
+	Debugger/Breakpoints/BreakpointDialog.ui
+	Debugger/Breakpoints/BreakpointModel.cpp
+	Debugger/Breakpoints/BreakpointModel.h
+	Debugger/Breakpoints/BreakpointWidget.cpp
+	Debugger/Breakpoints/BreakpointWidget.h
+	Debugger/Breakpoints/BreakpointWidget.ui
+	Debugger/Memory/MemorySearchWidget.cpp
+	Debugger/Memory/MemorySearchWidget.h
+	Debugger/Memory/MemorySearchWidget.ui
+	Debugger/Memory/MemoryViewWidget.cpp
+	Debugger/Memory/MemoryViewWidget.h
+	Debugger/Memory/MemoryViewWidget.ui
+	Debugger/Memory/SavedAddressesModel.cpp
+	Debugger/Memory/SavedAddressesModel.h
+	Debugger/Memory/SavedAddressesWidget.cpp
+	Debugger/Memory/SavedAddressesWidget.h
+	Debugger/Memory/SavedAddressesWidget.ui
 	Debugger/SymbolTree/NewSymbolDialogs.cpp
 	Debugger/SymbolTree/NewSymbolDialogs.h
 	Debugger/SymbolTree/NewSymbolDialog.ui
@@ -232,6 +243,7 @@ target_link_libraries(pcsx2-qt PRIVATE
 	Qt6::Core
 	Qt6::Gui
 	Qt6::Widgets
+	KDAB::kddockwidgets
 )
 
 # Our Qt builds may have exceptions on, so force them off.
diff --git a/pcsx2-qt/Debugger/BreakpointDialog.cpp b/pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.cpp
similarity index 100%
rename from pcsx2-qt/Debugger/BreakpointDialog.cpp
rename to pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.cpp
diff --git a/pcsx2-qt/Debugger/BreakpointDialog.h b/pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.h
similarity index 95%
rename from pcsx2-qt/Debugger/BreakpointDialog.h
rename to pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.h
index 61fc3fea1c..d1b033e20d 100644
--- a/pcsx2-qt/Debugger/BreakpointDialog.h
+++ b/pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.h
@@ -5,9 +5,9 @@
 
 #include "ui_BreakpointDialog.h"
 
-#include "DebugTools/Breakpoints.h"
+#include "BreakpointModel.h"
 
-#include "Models/BreakpointModel.h"
+#include "DebugTools/Breakpoints.h"
 
 #include <QtWidgets/QDialog>
 
diff --git a/pcsx2-qt/Debugger/BreakpointDialog.ui b/pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.ui
similarity index 100%
rename from pcsx2-qt/Debugger/BreakpointDialog.ui
rename to pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.ui
diff --git a/pcsx2-qt/Debugger/Models/BreakpointModel.cpp b/pcsx2-qt/Debugger/Breakpoints/BreakpointModel.cpp
similarity index 100%
rename from pcsx2-qt/Debugger/Models/BreakpointModel.cpp
rename to pcsx2-qt/Debugger/Breakpoints/BreakpointModel.cpp
diff --git a/pcsx2-qt/Debugger/Models/BreakpointModel.h b/pcsx2-qt/Debugger/Breakpoints/BreakpointModel.h
similarity index 100%
rename from pcsx2-qt/Debugger/Models/BreakpointModel.h
rename to pcsx2-qt/Debugger/Breakpoints/BreakpointModel.h
diff --git a/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.cpp b/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.cpp
new file mode 100644
index 0000000000..4617a6e3e1
--- /dev/null
+++ b/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.cpp
@@ -0,0 +1,183 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "BreakpointWidget.h"
+
+#include "QtUtils.h"
+#include "Debugger/DebuggerSettingsManager.h"
+#include "BreakpointDialog.h"
+#include "BreakpointModel.h"
+
+#include <QClipboard>
+
+BreakpointWidget::BreakpointWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu, parent)
+	, m_model(cpu)
+{
+	m_ui.setupUi(this);
+
+	connect(m_ui.breakpointList, &QTableView::customContextMenuRequested, this, &BreakpointWidget::onContextMenu);
+	connect(m_ui.breakpointList, &QTableView::doubleClicked, this, &BreakpointWidget::onDoubleClicked);
+
+	m_ui.breakpointList->setModel(&m_model);
+	for (std::size_t i = 0; auto mode : BreakpointModel::HeaderResizeModes)
+	{
+		m_ui.breakpointList->horizontalHeader()->setSectionResizeMode(i, mode);
+		i++;
+	}
+
+	connect(&m_model, &BreakpointModel::dataChanged, &m_model, &BreakpointModel::refreshData);
+}
+
+void BreakpointWidget::onDoubleClicked(const QModelIndex& index)
+{
+	if (index.isValid() && index.column() == BreakpointModel::OFFSET)
+	{
+		not_yet_implemented();
+		//m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_model.data(index, BreakpointModel::DataRole).toUInt());
+	}
+}
+
+void BreakpointWidget::onContextMenu(QPoint pos)
+{
+	QMenu* contextMenu = new QMenu(tr("Breakpoint List Context Menu"), m_ui.breakpointList);
+	if (cpu().isAlive())
+	{
+
+		QAction* newAction = new QAction(tr("New"), m_ui.breakpointList);
+		connect(newAction, &QAction::triggered, this, &BreakpointWidget::contextNew);
+		contextMenu->addAction(newAction);
+
+		const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
+
+		if (selModel->hasSelection())
+		{
+			QAction* editAction = new QAction(tr("Edit"), m_ui.breakpointList);
+			connect(editAction, &QAction::triggered, this, &BreakpointWidget::contextEdit);
+			contextMenu->addAction(editAction);
+
+			if (selModel->selectedIndexes().count() == 1)
+			{
+				QAction* copyAction = new QAction(tr("Copy"), m_ui.breakpointList);
+				connect(copyAction, &QAction::triggered, this, &BreakpointWidget::contextCopy);
+				contextMenu->addAction(copyAction);
+			}
+
+			QAction* deleteAction = new QAction(tr("Delete"), m_ui.breakpointList);
+			connect(deleteAction, &QAction::triggered, this, &BreakpointWidget::contextDelete);
+			contextMenu->addAction(deleteAction);
+		}
+	}
+
+	contextMenu->addSeparator();
+	if (m_model.rowCount() > 0)
+	{
+		QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.breakpointList);
+		connect(actionExport, &QAction::triggered, [this]() {
+			// It's important to use the Export Role here to allow pasting to be translation agnostic
+			QGuiApplication::clipboard()->setText(
+				QtUtils::AbstractItemModelToCSV(m_ui.breakpointList->model(),
+					BreakpointModel::ExportRole, true));
+		});
+		contextMenu->addAction(actionExport);
+	}
+
+	if (cpu().isAlive())
+	{
+		QAction* actionImport = new QAction(tr("Paste from CSV"), m_ui.breakpointList);
+		connect(actionImport, &QAction::triggered, this, &BreakpointWidget::contextPasteCSV);
+		contextMenu->addAction(actionImport);
+
+		QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.breakpointList);
+		connect(actionLoad, &QAction::triggered, [this]() {
+			m_model.clear();
+			DebuggerSettingsManager::loadGameSettings(&m_model);
+		});
+		contextMenu->addAction(actionLoad);
+
+		QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.breakpointList);
+		connect(actionSave, &QAction::triggered, this, &BreakpointWidget::saveBreakpointsToDebuggerSettings);
+		contextMenu->addAction(actionSave);
+	}
+
+	contextMenu->popup(m_ui.breakpointList->viewport()->mapToGlobal(pos));
+}
+
+void BreakpointWidget::contextCopy()
+{
+	const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
+
+	if (!selModel->hasSelection())
+		return;
+
+	QGuiApplication::clipboard()->setText(m_model.data(selModel->currentIndex()).toString());
+}
+
+void BreakpointWidget::contextDelete()
+{
+	const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
+
+	if (!selModel->hasSelection())
+		return;
+
+	QModelIndexList rows = selModel->selectedIndexes();
+
+	std::sort(rows.begin(), rows.end(), [](const QModelIndex& a, const QModelIndex& b) {
+		return a.row() > b.row();
+	});
+
+	for (const QModelIndex& index : rows)
+	{
+		m_model.removeRows(index.row(), 1);
+	}
+}
+
+void BreakpointWidget::contextNew()
+{
+	BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), m_model);
+	bpDialog->show();
+}
+
+void BreakpointWidget::contextEdit()
+{
+	const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
+
+	if (!selModel->hasSelection())
+		return;
+
+	const int selectedRow = selModel->selectedIndexes().first().row();
+
+	auto bpObject = m_model.at(selectedRow);
+
+	BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), m_model, bpObject, selectedRow);
+	bpDialog->show();
+}
+
+void BreakpointWidget::contextPasteCSV()
+{
+	QString csv = QGuiApplication::clipboard()->text();
+	// Skip header
+	csv = csv.mid(csv.indexOf('\n') + 1);
+
+	for (const QString& line : csv.split('\n'))
+	{
+		QStringList fields;
+		// In order to handle text with commas in them we must wrap values in quotes to mark
+		// where a value starts and end so that text commas aren't identified as delimiters.
+		// So matches each quote pair, parse it out, and removes the quotes to get the value.
+		QRegularExpression eachQuotePair(R"("([^"]|\\.)*")");
+		QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line);
+		while (it.hasNext())
+		{
+			QRegularExpressionMatch match = it.next();
+			QString matchedValue = match.captured(0);
+			fields << matchedValue.mid(1, matchedValue.length() - 2);
+		}
+		m_model.loadBreakpointFromFieldList(fields);
+	}
+}
+
+void BreakpointWidget::saveBreakpointsToDebuggerSettings()
+{
+	DebuggerSettingsManager::saveGameSettings(&m_model);
+}
diff --git a/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.h b/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.h
new file mode 100644
index 0000000000..4bb750ccbc
--- /dev/null
+++ b/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.h
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "ui_BreakpointWidget.h"
+
+#include "BreakpointModel.h"
+
+#include "Debugger/DebuggerWidget.h"
+
+#include "DebugTools/DebugInterface.h"
+#include "DebugTools/DisassemblyManager.h"
+
+#include <QtWidgets/QMenu>
+#include <QtWidgets/QTabBar>
+#include <QtGui/QPainter>
+
+class BreakpointWidget : public DebuggerWidget
+{
+	Q_OBJECT
+
+public:
+	BreakpointWidget(DebugInterface& cpu, QWidget* parent = nullptr);
+
+	void onDoubleClicked(const QModelIndex& index);
+	void onContextMenu(QPoint pos);
+
+	void contextCopy();
+	void contextDelete();
+	void contextNew();
+	void contextEdit();
+	void contextPasteCSV();
+
+	void saveBreakpointsToDebuggerSettings();
+
+private:
+	Ui::BreakpointWidget m_ui;
+
+	BreakpointModel m_model;
+};
diff --git a/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.ui b/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.ui
new file mode 100644
index 0000000000..7dc2bf069c
--- /dev/null
+++ b/pcsx2-qt/Debugger/Breakpoints/BreakpointWidget.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BreakpointWidget</class>
+ <widget class="QWidget" name="BreakpointWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTableView" name="breakpointList">
+     <property name="contextMenuPolicy">
+      <enum>Qt::CustomContextMenu</enum>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/pcsx2-qt/Debugger/CpuWidget.cpp b/pcsx2-qt/Debugger/CpuWidget.cpp
deleted file mode 100644
index 76f49e67b2..0000000000
--- a/pcsx2-qt/Debugger/CpuWidget.cpp
+++ /dev/null
@@ -1,732 +0,0 @@
-// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
-// SPDX-License-Identifier: GPL-3.0+
-
-#include "CpuWidget.h"
-
-#include "DisassemblyWidget.h"
-#include "BreakpointDialog.h"
-#include "Models/BreakpointModel.h"
-#include "Models/ThreadModel.h"
-#include "Models/SavedAddressesModel.h"
-#include "Debugger/DebuggerSettingsManager.h"
-
-#include "DebugTools/DebugInterface.h"
-#include "DebugTools/Breakpoints.h"
-#include "DebugTools/MipsStackWalk.h"
-
-#include "QtUtils.h"
-
-#include "common/Console.h"
-
-#include <QtGui/QClipboard>
-#include <QtWidgets/QMessageBox>
-#include <QtConcurrent/QtConcurrent>
-#include <QtCore/QFutureWatcher>
-#include <QtCore/QRegularExpression>
-#include <QtCore/QRegularExpressionMatchIterator>
-#include <QtCore/QStringList>
-#include <QtWidgets/QScrollBar>
-
-using namespace QtUtils;
-using namespace MipsStackWalk;
-
-CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu)
-	: m_cpu(cpu)
-	, m_bpModel(cpu)
-	, m_threadModel(cpu)
-	, m_stackModel(cpu)
-	, m_savedAddressesModel(cpu)
-{
-	m_ui.setupUi(this);
-
-	connect(g_emu_thread, &EmuThread::onVMPaused, this, &CpuWidget::onVMPaused);
-	connect(g_emu_thread, &EmuThread::onGameChanged, this, [this](const QString& title) {
-		if (title.isEmpty())
-			return;
-		// Don't overwrite users BPs/Saved Addresses unless they have a clean state.
-		if (m_bpModel.rowCount() == 0)
-			DebuggerSettingsManager::loadGameSettings(&m_bpModel);
-		if (m_savedAddressesModel.rowCount() == 0)
-			DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel);
-	});
-
-	connect(m_ui.registerWidget, &RegisterWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress);
-	connect(m_ui.memoryviewWidget, &MemoryViewWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress);
-	connect(m_ui.memoryviewWidget, &MemoryViewWidget::addToSavedAddresses, this, &CpuWidget::addAddressToSavedAddressesList);
-
-	connect(m_ui.registerWidget, &RegisterWidget::gotoInMemory, this, &CpuWidget::onGotoInMemory);
-	connect(m_ui.disassemblyWidget, &DisassemblyWidget::gotoInMemory, this, &CpuWidget::onGotoInMemory);
-
-	connect(m_ui.memoryviewWidget, &MemoryViewWidget::VMUpdate, this, &CpuWidget::reloadCPUWidgets);
-	connect(m_ui.registerWidget, &RegisterWidget::VMUpdate, this, &CpuWidget::reloadCPUWidgets);
-	connect(m_ui.disassemblyWidget, &DisassemblyWidget::VMUpdate, this, &CpuWidget::reloadCPUWidgets);
-
-	connect(m_ui.disassemblyWidget, &DisassemblyWidget::breakpointsChanged, this, &CpuWidget::updateBreakpoints);
-
-	connect(m_ui.breakpointList, &QTableView::customContextMenuRequested, this, &CpuWidget::onBPListContextMenu);
-	connect(m_ui.breakpointList, &QTableView::doubleClicked, this, &CpuWidget::onBPListDoubleClicked);
-
-	m_ui.breakpointList->setModel(&m_bpModel);
-	for (std::size_t i = 0; auto mode : BreakpointModel::HeaderResizeModes)
-	{
-		m_ui.breakpointList->horizontalHeader()->setSectionResizeMode(i, mode);
-		i++;
-	}
-
-	connect(&m_bpModel, &BreakpointModel::dataChanged, this, &CpuWidget::updateBreakpoints);
-
-	connect(m_ui.threadList, &QTableView::customContextMenuRequested, this, &CpuWidget::onThreadListContextMenu);
-	connect(m_ui.threadList, &QTableView::doubleClicked, this, &CpuWidget::onThreadListDoubleClick);
-
-	m_threadProxyModel.setSourceModel(&m_threadModel);
-	m_threadProxyModel.setSortRole(Qt::UserRole);
-	m_ui.threadList->setModel(&m_threadProxyModel);
-	m_ui.threadList->setSortingEnabled(true);
-	m_ui.threadList->sortByColumn(ThreadModel::ThreadColumns::ID, Qt::SortOrder::AscendingOrder);
-	for (std::size_t i = 0; auto mode : ThreadModel::HeaderResizeModes)
-	{
-		m_ui.threadList->horizontalHeader()->setSectionResizeMode(i, mode);
-		i++;
-	}
-
-	connect(m_ui.stackList, &QTableView::customContextMenuRequested, this, &CpuWidget::onStackListContextMenu);
-	connect(m_ui.stackList, &QTableView::doubleClicked, this, &CpuWidget::onStackListDoubleClick);
-
-	m_ui.stackList->setModel(&m_stackModel);
-	for (std::size_t i = 0; auto mode : StackModel::HeaderResizeModes)
-	{
-		m_ui.stackList->horizontalHeader()->setSectionResizeMode(i, mode);
-		i++;
-	}
-
-	m_ui.disassemblyWidget->SetCpu(&cpu);
-	m_ui.registerWidget->SetCpu(&cpu);
-	m_ui.memoryviewWidget->SetCpu(&cpu);
-
-	this->repaint();
-
-	m_ui.savedAddressesList->setModel(&m_savedAddressesModel);
-	m_ui.savedAddressesList->setContextMenuPolicy(Qt::CustomContextMenu);
-	connect(m_ui.savedAddressesList, &QTableView::customContextMenuRequested, this, &CpuWidget::onSavedAddressesListContextMenu);
-	for (std::size_t i = 0; auto mode : SavedAddressesModel::HeaderResizeModes)
-	{
-		m_ui.savedAddressesList->horizontalHeader()->setSectionResizeMode(i++, mode);
-	}
-	QTableView* savedAddressesTableView = m_ui.savedAddressesList;
-	connect(m_ui.savedAddressesList->model(), &QAbstractItemModel::dataChanged, [savedAddressesTableView](const QModelIndex& topLeft) {
-		savedAddressesTableView->resizeColumnToContents(topLeft.column());
-	});
-
-	setupSymbolTrees();
-
-	DebuggerSettingsManager::loadGameSettings(&m_bpModel);
-	DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel);
-
-	connect(m_ui.memorySearchWidget, &MemorySearchWidget::addAddressToSavedAddressesList, this, &CpuWidget::addAddressToSavedAddressesList);
-	connect(m_ui.memorySearchWidget, &MemorySearchWidget::goToAddressInDisassemblyView,
-		[this](u32 address) { m_ui.disassemblyWidget->gotoAddress(address, true); });
-	connect(m_ui.memorySearchWidget, &MemorySearchWidget::goToAddressInMemoryView, m_ui.memoryviewWidget, &MemoryViewWidget::gotoAddress);
-	connect(m_ui.memorySearchWidget, &MemorySearchWidget::switchToMemoryViewTab,
-		[this]() { m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory); });
-	m_ui.memorySearchWidget->setCpu(&m_cpu);
-
-	m_refreshDebuggerTimer.setInterval(1000);
-	connect(&m_refreshDebuggerTimer, &QTimer::timeout, this, &CpuWidget::refreshDebugger);
-	m_refreshDebuggerTimer.start();
-}
-
-CpuWidget::~CpuWidget() = default;
-
-void CpuWidget::setupSymbolTrees()
-{
-	m_ui.tabFunctions->setLayout(new QVBoxLayout());
-	m_ui.tabGlobalVariables->setLayout(new QVBoxLayout());
-	m_ui.tabLocalVariables->setLayout(new QVBoxLayout());
-	m_ui.tabParameterVariables->setLayout(new QVBoxLayout());
-
-	m_ui.tabFunctions->layout()->setContentsMargins(0, 0, 0, 0);
-	m_ui.tabGlobalVariables->layout()->setContentsMargins(0, 0, 0, 0);
-	m_ui.tabLocalVariables->layout()->setContentsMargins(0, 0, 0, 0);
-	m_ui.tabParameterVariables->layout()->setContentsMargins(0, 0, 0, 0);
-
-	m_function_tree = new FunctionTreeWidget(m_cpu);
-	m_global_variable_tree = new GlobalVariableTreeWidget(m_cpu);
-	m_local_variable_tree = new LocalVariableTreeWidget(m_cpu);
-	m_parameter_variable_tree = new ParameterVariableTreeWidget(m_cpu);
-
-	m_function_tree->updateModel();
-	m_global_variable_tree->updateModel();
-	m_local_variable_tree->updateModel();
-	m_parameter_variable_tree->updateModel();
-
-	m_ui.tabFunctions->layout()->addWidget(m_function_tree);
-	m_ui.tabGlobalVariables->layout()->addWidget(m_global_variable_tree);
-	m_ui.tabLocalVariables->layout()->addWidget(m_local_variable_tree);
-	m_ui.tabParameterVariables->layout()->addWidget(m_parameter_variable_tree);
-
-	connect(m_function_tree, &SymbolTreeWidget::goToInDisassembly, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddressAndSetFocus);
-	connect(m_global_variable_tree, &SymbolTreeWidget::goToInDisassembly, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddressAndSetFocus);
-	connect(m_local_variable_tree, &SymbolTreeWidget::goToInDisassembly, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddressAndSetFocus);
-	connect(m_parameter_variable_tree, &SymbolTreeWidget::goToInDisassembly, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddressAndSetFocus);
-
-	connect(m_function_tree, &SymbolTreeWidget::goToInMemoryView, this, &CpuWidget::onGotoInMemory);
-	connect(m_global_variable_tree, &SymbolTreeWidget::goToInMemoryView, this, &CpuWidget::onGotoInMemory);
-	connect(m_local_variable_tree, &SymbolTreeWidget::goToInMemoryView, this, &CpuWidget::onGotoInMemory);
-	connect(m_parameter_variable_tree, &SymbolTreeWidget::goToInMemoryView, this, &CpuWidget::onGotoInMemory);
-
-	connect(m_function_tree, &SymbolTreeWidget::nameColumnClicked, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddressAndSetFocus);
-	connect(m_function_tree, &SymbolTreeWidget::locationColumnClicked, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddressAndSetFocus);
-}
-
-void CpuWidget::refreshDebugger()
-{
-	if (!m_cpu.isAlive())
-		return;
-
-	m_ui.registerWidget->update();
-	m_ui.disassemblyWidget->update();
-	m_ui.memoryviewWidget->update();
-	m_ui.memorySearchWidget->update();
-
-	m_function_tree->updateModel();
-	m_global_variable_tree->updateModel();
-	m_local_variable_tree->updateModel();
-	m_parameter_variable_tree->updateModel();
-}
-
-void CpuWidget::reloadCPUWidgets()
-{
-	updateThreads();
-	updateStackFrames();
-
-	m_ui.registerWidget->update();
-	m_ui.disassemblyWidget->update();
-	m_ui.memoryviewWidget->update();
-
-	m_function_tree->updateModel();
-	m_global_variable_tree->updateModel();
-	m_local_variable_tree->updateModel();
-	m_parameter_variable_tree->updateModel();
-}
-
-void CpuWidget::paintEvent(QPaintEvent* event)
-{
-	m_ui.registerWidget->update();
-	m_ui.disassemblyWidget->update();
-	m_ui.memoryviewWidget->update();
-	m_ui.memorySearchWidget->update();
-}
-
-// The cpu shouldn't be alive when these are called
-// But make sure it isn't just in case
-void CpuWidget::onStepInto()
-{
-	if (!m_cpu.isAlive() || !m_cpu.isCpuPaused())
-		return;
-
-	// Allow the cpu to skip this pc if it is a breakpoint
-	CBreakPoints::SetSkipFirst(m_cpu.getCpuType(), m_cpu.getPC());
-
-	const u32 pc = m_cpu.getPC();
-	const MIPSAnalyst::MipsOpcodeInfo info = MIPSAnalyst::GetOpcodeInfo(&m_cpu, pc);
-
-	u32 bpAddr = pc + 0x4; // Default to the next instruction
-
-	if (info.isBranch)
-	{
-		if (!info.isConditional)
-		{
-			bpAddr = info.branchTarget;
-		}
-		else
-		{
-			if (info.conditionMet)
-			{
-				bpAddr = info.branchTarget;
-			}
-			else
-			{
-				bpAddr = pc + (2 * 4); // Skip branch delay slot
-			}
-		}
-	}
-
-	if (info.isSyscall)
-		bpAddr = info.branchTarget; // Syscalls are always taken
-
-	Host::RunOnCPUThread([cpu = &m_cpu, bpAddr] {
-		CBreakPoints::AddBreakPoint(cpu->getCpuType(), bpAddr, true);
-		cpu->resumeCpu();
-	});
-
-	this->repaint();
-}
-
-void CpuWidget::onStepOut()
-{
-	if (!m_cpu.isAlive() || !m_cpu.isCpuPaused())
-		return;
-
-	// Allow the cpu to skip this pc if it is a breakpoint
-	CBreakPoints::SetSkipFirst(m_cpu.getCpuType(), m_cpu.getPC());
-
-	if (m_stackModel.rowCount() < 2)
-		return;
-
-	Host::RunOnCPUThread([cpu = &m_cpu, stackModel = &m_stackModel] {
-		CBreakPoints::AddBreakPoint(cpu->getCpuType(), stackModel->data(stackModel->index(1, StackModel::PC), Qt::UserRole).toUInt(), true);
-		cpu->resumeCpu();
-	});
-
-	this->repaint();
-}
-
-void CpuWidget::onStepOver()
-{
-	if (!m_cpu.isAlive() || !m_cpu.isCpuPaused())
-		return;
-
-	const u32 pc = m_cpu.getPC();
-	const MIPSAnalyst::MipsOpcodeInfo info = MIPSAnalyst::GetOpcodeInfo(&m_cpu, pc);
-
-	u32 bpAddr = pc + 0x4; // Default to the next instruction
-
-	if (info.isBranch)
-	{
-		if (!info.isConditional)
-		{
-			if (info.isLinkedBranch) // jal, jalr
-			{
-				// it's a function call with a delay slot - skip that too
-				bpAddr += 4;
-			}
-			else // j, ...
-			{
-				// in case of absolute branches, set the breakpoint at the branch target
-				bpAddr = info.branchTarget;
-			}
-		}
-		else // beq, ...
-		{
-			if (info.conditionMet)
-			{
-				bpAddr = info.branchTarget;
-			}
-			else
-			{
-				bpAddr = pc + (2 * 4); // Skip branch delay slot
-			}
-		}
-	}
-
-	Host::RunOnCPUThread([cpu = &m_cpu, bpAddr] {
-		CBreakPoints::AddBreakPoint(cpu->getCpuType(), bpAddr, true);
-		cpu->resumeCpu();
-	});
-
-	this->repaint();
-}
-
-void CpuWidget::onVMPaused()
-{
-	// Stops us from telling the disassembly dialog to jump somwhere because breakpoint code paused the core.
-	if (CBreakPoints::GetCorePaused())
-	{
-		CBreakPoints::SetCorePaused(false);
-	}
-	else
-	{
-		m_ui.disassemblyWidget->gotoProgramCounterOnPause();
-	}
-
-	reloadCPUWidgets();
-	this->repaint();
-}
-
-void CpuWidget::updateBreakpoints()
-{
-	m_bpModel.refreshData();
-}
-
-void CpuWidget::onBPListDoubleClicked(const QModelIndex& index)
-{
-	if (index.isValid())
-	{
-		if (index.column() == BreakpointModel::OFFSET)
-		{
-			m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_bpModel.data(index, BreakpointModel::DataRole).toUInt());
-		}
-	}
-}
-
-void CpuWidget::onBPListContextMenu(QPoint pos)
-{
-	QMenu* contextMenu = new QMenu(tr("Breakpoint List Context Menu"), m_ui.breakpointList);
-	if (m_cpu.isAlive())
-	{
-
-		QAction* newAction = new QAction(tr("New"), m_ui.breakpointList);
-		connect(newAction, &QAction::triggered, this, &CpuWidget::contextBPListNew);
-		contextMenu->addAction(newAction);
-
-		const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
-
-		if (selModel->hasSelection())
-		{
-			QAction* editAction = new QAction(tr("Edit"), m_ui.breakpointList);
-			connect(editAction, &QAction::triggered, this, &CpuWidget::contextBPListEdit);
-			contextMenu->addAction(editAction);
-
-			if (selModel->selectedIndexes().count() == 1)
-			{
-				QAction* copyAction = new QAction(tr("Copy"), m_ui.breakpointList);
-				connect(copyAction, &QAction::triggered, this, &CpuWidget::contextBPListCopy);
-				contextMenu->addAction(copyAction);
-			}
-
-			QAction* deleteAction = new QAction(tr("Delete"), m_ui.breakpointList);
-			connect(deleteAction, &QAction::triggered, this, &CpuWidget::contextBPListDelete);
-			contextMenu->addAction(deleteAction);
-		}
-	}
-
-	contextMenu->addSeparator();
-	if (m_bpModel.rowCount() > 0)
-	{
-		QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.breakpointList);
-		connect(actionExport, &QAction::triggered, [this]() {
-			// It's important to use the Export Role here to allow pasting to be translation agnostic
-			QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.breakpointList->model(), BreakpointModel::ExportRole, true));
-		});
-		contextMenu->addAction(actionExport);
-	}
-
-	if (m_cpu.isAlive())
-	{
-		QAction* actionImport = new QAction(tr("Paste from CSV"), m_ui.breakpointList);
-		connect(actionImport, &QAction::triggered, this, &CpuWidget::contextBPListPasteCSV);
-		contextMenu->addAction(actionImport);
-
-		QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.breakpointList);
-		connect(actionLoad, &QAction::triggered, [this]() {
-			m_bpModel.clear();
-			DebuggerSettingsManager::loadGameSettings(&m_bpModel);
-		});
-		contextMenu->addAction(actionLoad);
-
-		QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.breakpointList);
-		connect(actionSave, &QAction::triggered, this, &CpuWidget::saveBreakpointsToDebuggerSettings);
-		contextMenu->addAction(actionSave);
-	}
-
-	contextMenu->popup(m_ui.breakpointList->viewport()->mapToGlobal(pos));
-}
-
-void CpuWidget::onGotoInMemory(u32 address)
-{
-	m_ui.memoryviewWidget->gotoAddress(address);
-	m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory);
-}
-
-void CpuWidget::contextBPListCopy()
-{
-	const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
-
-	if (!selModel->hasSelection())
-		return;
-
-	QGuiApplication::clipboard()->setText(m_bpModel.data(selModel->currentIndex()).toString());
-}
-
-void CpuWidget::contextBPListDelete()
-{
-	const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
-
-	if (!selModel->hasSelection())
-		return;
-
-	QModelIndexList rows = selModel->selectedIndexes();
-
-	std::sort(rows.begin(), rows.end(), [](const QModelIndex& a, const QModelIndex& b) {
-		return a.row() > b.row();
-	});
-
-	for (const QModelIndex& index : rows)
-	{
-		m_bpModel.removeRows(index.row(), 1);
-	}
-}
-
-void CpuWidget::contextBPListNew()
-{
-	BreakpointDialog* bpDialog = new BreakpointDialog(this, &m_cpu, m_bpModel);
-	bpDialog->show();
-}
-
-void CpuWidget::contextBPListEdit()
-{
-	const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
-
-	if (!selModel->hasSelection())
-		return;
-
-	const int selectedRow = selModel->selectedIndexes().first().row();
-
-	auto bpObject = m_bpModel.at(selectedRow);
-
-	BreakpointDialog* bpDialog = new BreakpointDialog(this, &m_cpu, m_bpModel, bpObject, selectedRow);
-	bpDialog->show();
-}
-
-void CpuWidget::contextBPListPasteCSV()
-{
-	QString csv = QGuiApplication::clipboard()->text();
-	// Skip header
-	csv = csv.mid(csv.indexOf('\n') + 1);
-
-	for (const QString& line : csv.split('\n'))
-	{
-		QStringList fields;
-		// In order to handle text with commas in them we must wrap values in quotes to mark
-		// where a value starts and end so that text commas aren't identified as delimiters.
-		// So matches each quote pair, parse it out, and removes the quotes to get the value.
-		QRegularExpression eachQuotePair(R"("([^"]|\\.)*")");
-		QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line);
-		while (it.hasNext())
-		{
-			QRegularExpressionMatch match = it.next();
-			QString matchedValue = match.captured(0);
-			fields << matchedValue.mid(1, matchedValue.length() - 2);
-		}
-		m_bpModel.loadBreakpointFromFieldList(fields);
-	}
-}
-
-void CpuWidget::onSavedAddressesListContextMenu(QPoint pos)
-{
-	QMenu* contextMenu = new QMenu("Saved Addresses List Context Menu", m_ui.savedAddressesList);
-
-	QAction* newAction = new QAction(tr("New"), m_ui.savedAddressesList);
-	connect(newAction, &QAction::triggered, this, &CpuWidget::contextSavedAddressesListNew);
-	contextMenu->addAction(newAction);
-
-	const QModelIndex indexAtPos = m_ui.savedAddressesList->indexAt(pos);
-	const bool isIndexValid = indexAtPos.isValid();
-
-	if (isIndexValid)
-	{
-		if (m_cpu.isAlive())
-		{
-			QAction* goToAddressMemViewAction = new QAction(tr("Go to in Memory View"), m_ui.savedAddressesList);
-			connect(goToAddressMemViewAction, &QAction::triggered, this, [this, indexAtPos]() {
-				const QModelIndex rowAddressIndex = m_ui.savedAddressesList->model()->index(indexAtPos.row(), 0, QModelIndex());
-				m_ui.memoryviewWidget->gotoAddress(m_ui.savedAddressesList->model()->data(rowAddressIndex, Qt::UserRole).toUInt());
-				m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory);
-			});
-			contextMenu->addAction(goToAddressMemViewAction);
-
-			QAction* goToAddressDisassemblyAction = new QAction(tr("Go to in Disassembly"), m_ui.savedAddressesList);
-			connect(goToAddressDisassemblyAction, &QAction::triggered, this, [this, indexAtPos]() {
-				const QModelIndex rowAddressIndex = m_ui.savedAddressesList->model()->index(indexAtPos.row(), 0, QModelIndex());
-				m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_ui.savedAddressesList->model()->data(rowAddressIndex, Qt::UserRole).toUInt());
-			});
-			contextMenu->addAction(goToAddressDisassemblyAction);
-		}
-
-		QAction* copyAction = new QAction(indexAtPos.column() == 0 ? tr("Copy Address") : tr("Copy Text"), m_ui.savedAddressesList);
-		connect(copyAction, &QAction::triggered, [this, indexAtPos]() {
-			QGuiApplication::clipboard()->setText(m_ui.savedAddressesList->model()->data(indexAtPos, Qt::DisplayRole).toString());
-		});
-		contextMenu->addAction(copyAction);
-	}
-
-	if (m_ui.savedAddressesList->model()->rowCount() > 0)
-	{
-		QAction* actionExportCSV = new QAction(tr("Copy all as CSV"), m_ui.savedAddressesList);
-		connect(actionExportCSV, &QAction::triggered, [this]() {
-			QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.savedAddressesList->model(), Qt::DisplayRole, true));
-		});
-		contextMenu->addAction(actionExportCSV);
-	}
-
-	QAction* actionImportCSV = new QAction(tr("Paste from CSV"), m_ui.savedAddressesList);
-	connect(actionImportCSV, &QAction::triggered, this, &CpuWidget::contextSavedAddressesListPasteCSV);
-	contextMenu->addAction(actionImportCSV);
-
-	if (m_cpu.isAlive())
-	{
-		QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.savedAddressesList);
-		connect(actionLoad, &QAction::triggered, [this]() {
-			m_savedAddressesModel.clear();
-			DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel);
-		});
-		contextMenu->addAction(actionLoad);
-
-		QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.savedAddressesList);
-		connect(actionSave, &QAction::triggered, this, &CpuWidget::saveSavedAddressesToDebuggerSettings);
-		contextMenu->addAction(actionSave);
-	}
-
-	if (isIndexValid)
-	{
-		QAction* deleteAction = new QAction(tr("Delete"), m_ui.savedAddressesList);
-		connect(deleteAction, &QAction::triggered, this, [this, indexAtPos]() {
-			m_ui.savedAddressesList->model()->removeRows(indexAtPos.row(), 1);
-		});
-		contextMenu->addAction(deleteAction);
-	}
-
-	contextMenu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos));
-}
-
-void CpuWidget::contextSavedAddressesListPasteCSV()
-{
-	QString csv = QGuiApplication::clipboard()->text();
-	// Skip header
-	csv = csv.mid(csv.indexOf('\n') + 1);
-
-	for (const QString& line : csv.split('\n'))
-	{
-		QStringList fields;
-		// In order to handle text with commas in them we must wrap values in quotes to mark
-		// where a value starts and end so that text commas aren't identified as delimiters.
-		// So matches each quote pair, parse it out, and removes the quotes to get the value.
-		QRegularExpression eachQuotePair(R"("([^"]|\\.)*")");
-		QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line);
-		while (it.hasNext())
-		{
-			QRegularExpressionMatch match = it.next();
-			QString matchedValue = match.captured(0);
-			fields << matchedValue.mid(1, matchedValue.length() - 2);
-		}
-
-		m_savedAddressesModel.loadSavedAddressFromFieldList(fields);
-	}
-}
-
-void CpuWidget::contextSavedAddressesListNew()
-{
-	qobject_cast<SavedAddressesModel*>(m_ui.savedAddressesList->model())->addRow();
-	const u32 rowCount = m_ui.savedAddressesList->model()->rowCount();
-	m_ui.savedAddressesList->edit(m_ui.savedAddressesList->model()->index(rowCount - 1, 0));
-}
-
-void CpuWidget::addAddressToSavedAddressesList(u32 address)
-{
-	qobject_cast<SavedAddressesModel*>(m_ui.savedAddressesList->model())->addRow();
-	const u32 rowCount = m_ui.savedAddressesList->model()->rowCount();
-	const QModelIndex addressIndex = m_ui.savedAddressesList->model()->index(rowCount - 1, 0);
-	m_ui.tabWidget->setCurrentWidget(m_ui.tab_savedaddresses);
-	m_ui.savedAddressesList->model()->setData(addressIndex, address, Qt::UserRole);
-	m_ui.savedAddressesList->edit(m_ui.savedAddressesList->model()->index(rowCount - 1, 1));
-}
-
-void CpuWidget::updateThreads()
-{
-	m_threadModel.refreshData();
-}
-
-void CpuWidget::onThreadListContextMenu(QPoint pos)
-{
-	if (!m_ui.threadList->selectionModel()->hasSelection())
-		return;
-
-	QMenu* contextMenu = new QMenu(tr("Thread List Context Menu"), m_ui.threadList);
-
-	QAction* actionCopy = new QAction(tr("Copy"), m_ui.threadList);
-	connect(actionCopy, &QAction::triggered, [this]() {
-		const auto* selModel = m_ui.threadList->selectionModel();
-
-		if (!selModel->hasSelection())
-			return;
-
-		QGuiApplication::clipboard()->setText(m_ui.threadList->model()->data(selModel->currentIndex()).toString());
-	});
-	contextMenu->addAction(actionCopy);
-
-	contextMenu->addSeparator();
-
-	QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.threadList);
-	connect(actionExport, &QAction::triggered, [this]() {
-		QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.threadList->model()));
-	});
-	contextMenu->addAction(actionExport);
-
-	contextMenu->popup(m_ui.threadList->viewport()->mapToGlobal(pos));
-}
-
-void CpuWidget::onThreadListDoubleClick(const QModelIndex& index)
-{
-	switch (index.column())
-	{
-		case ThreadModel::ThreadColumns::ENTRY:
-			m_ui.memoryviewWidget->gotoAddress(m_ui.threadList->model()->data(index, Qt::UserRole).toUInt());
-			m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory);
-			break;
-		default: // Default to PC
-			m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_ui.threadList->model()->data(m_ui.threadList->model()->index(index.row(), ThreadModel::ThreadColumns::PC), Qt::UserRole).toUInt());
-			break;
-	}
-}
-
-void CpuWidget::updateStackFrames()
-{
-	m_stackModel.refreshData();
-}
-
-void CpuWidget::onStackListContextMenu(QPoint pos)
-{
-	if (!m_ui.stackList->selectionModel()->hasSelection())
-		return;
-
-	QMenu* contextMenu = new QMenu(tr("Stack List Context Menu"), m_ui.stackList);
-
-	QAction* actionCopy = new QAction(tr("Copy"), m_ui.stackList);
-	connect(actionCopy, &QAction::triggered, [this]() {
-		const auto* selModel = m_ui.stackList->selectionModel();
-
-		if (!selModel->hasSelection())
-			return;
-
-		QGuiApplication::clipboard()->setText(m_ui.stackList->model()->data(selModel->currentIndex()).toString());
-	});
-	contextMenu->addAction(actionCopy);
-
-	contextMenu->addSeparator();
-
-	QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.stackList);
-	connect(actionExport, &QAction::triggered, [this]() {
-		QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.stackList->model()));
-	});
-	contextMenu->addAction(actionExport);
-
-	contextMenu->popup(m_ui.stackList->viewport()->mapToGlobal(pos));
-}
-
-void CpuWidget::onStackListDoubleClick(const QModelIndex& index)
-{
-	switch (index.column())
-	{
-		case StackModel::StackModel::ENTRY:
-		case StackModel::StackModel::ENTRY_LABEL:
-			m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_ui.stackList->model()->data(m_ui.stackList->model()->index(index.row(), StackModel::StackColumns::ENTRY), Qt::UserRole).toUInt());
-			break;
-		case StackModel::StackModel::SP:
-			m_ui.memoryviewWidget->gotoAddress(m_ui.stackList->model()->data(index, Qt::UserRole).toUInt());
-			m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory);
-			break;
-		default: // Default to PC
-			m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_ui.stackList->model()->data(m_ui.stackList->model()->index(index.row(), StackModel::StackColumns::PC), Qt::UserRole).toUInt());
-			break;
-	}
-}
-
-void CpuWidget::saveBreakpointsToDebuggerSettings()
-{
-	DebuggerSettingsManager::saveGameSettings(&m_bpModel);
-}
-
-void CpuWidget::saveSavedAddressesToDebuggerSettings()
-{
-	DebuggerSettingsManager::saveGameSettings(&m_savedAddressesModel);
-}
diff --git a/pcsx2-qt/Debugger/CpuWidget.h b/pcsx2-qt/Debugger/CpuWidget.h
deleted file mode 100644
index 2f96ead065..0000000000
--- a/pcsx2-qt/Debugger/CpuWidget.h
+++ /dev/null
@@ -1,97 +0,0 @@
-// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
-// SPDX-License-Identifier: GPL-3.0+
-
-#pragma once
-
-#include "ui_CpuWidget.h"
-
-#include "DebugTools/DebugInterface.h"
-
-#include "Models/BreakpointModel.h"
-#include "Models/ThreadModel.h"
-#include "Models/StackModel.h"
-#include "Models/SavedAddressesModel.h"
-#include "Debugger/SymbolTree/SymbolTreeWidgets.h"
-
-#include "QtHost.h"
-#include <QtWidgets/QWidget>
-#include <QtWidgets/QTableWidget>
-#include <QtCore/QSortFilterProxyModel>
-#include <QtCore/QTimer>
-
-#include <vector>
-
-using namespace MipsStackWalk;
-
-class CpuWidget final : public QWidget
-{
-	Q_OBJECT
-
-public:
-	CpuWidget(QWidget* parent, DebugInterface& cpu);
-	~CpuWidget();
-
-public slots:
-	void paintEvent(QPaintEvent* event);
-
-	void onStepInto();
-	void onStepOver();
-	void onStepOut();
-
-	void onVMPaused();
-
-	void updateBreakpoints();
-	void onBPListDoubleClicked(const QModelIndex& index);
-	void onBPListContextMenu(QPoint pos);
-	void onGotoInMemory(u32 address);
-
-	void contextBPListCopy();
-	void contextBPListDelete();
-	void contextBPListNew();
-	void contextBPListEdit();
-	void contextBPListPasteCSV();
-
-	void onSavedAddressesListContextMenu(QPoint pos);
-	void contextSavedAddressesListPasteCSV();
-	void contextSavedAddressesListNew();
-	void addAddressToSavedAddressesList(u32 address);
-
-	void updateThreads();
-	void onThreadListDoubleClick(const QModelIndex& index);
-	void onThreadListContextMenu(QPoint pos);
-
-	void updateStackFrames();
-	void onStackListContextMenu(QPoint pos);
-	void onStackListDoubleClick(const QModelIndex& index);
-
-	void refreshDebugger();
-	void reloadCPUWidgets();
-
-	void saveBreakpointsToDebuggerSettings();
-	void saveSavedAddressesToDebuggerSettings();
-
-private:
-	void setupSymbolTrees();
-
-	std::vector<QTableWidget*> m_registerTableViews;
-
-	QMenu* m_stacklistContextMenu = 0;
-	QMenu* m_funclistContextMenu = 0;
-	QMenu* m_moduleTreeContextMenu = 0;
-	QTimer m_refreshDebuggerTimer;
-
-	Ui::CpuWidget m_ui;
-
-	DebugInterface& m_cpu;
-
-	BreakpointModel m_bpModel;
-	ThreadModel m_threadModel;
-	QSortFilterProxyModel m_threadProxyModel;
-	StackModel m_stackModel;
-	SavedAddressesModel m_savedAddressesModel;
-
-	FunctionTreeWidget* m_function_tree = nullptr;
-	GlobalVariableTreeWidget* m_global_variable_tree = nullptr;
-	LocalVariableTreeWidget* m_local_variable_tree = nullptr;
-	ParameterVariableTreeWidget* m_parameter_variable_tree = nullptr;
-};
diff --git a/pcsx2-qt/Debugger/CpuWidget.ui b/pcsx2-qt/Debugger/CpuWidget.ui
deleted file mode 100644
index 587fd31449..0000000000
--- a/pcsx2-qt/Debugger/CpuWidget.ui
+++ /dev/null
@@ -1,445 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>CpuWidget</class>
- <widget class="QWidget" name="CpuWidget">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>668</width>
-    <height>563</height>
-   </rect>
-  </property>
-  <property name="font">
-   <font>
-    <family>Monospace</family>
-   </font>
-  </property>
-  <property name="focusPolicy">
-   <enum>Qt::StrongFocus</enum>
-  </property>
-  <property name="windowTitle">
-   <string/>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <widget class="QSplitter" name="verticalSplitter">
-     <property name="orientation">
-      <enum>Qt::Vertical</enum>
-     </property>
-     <property name="childrenCollapsible">
-      <bool>false</bool>
-     </property>
-     <widget class="QSplitter" name="horizontalSplitter">
-      <property name="orientation">
-       <enum>Qt::Horizontal</enum>
-      </property>
-      <property name="childrenCollapsible">
-       <bool>false</bool>
-      </property>
-      <widget class="QTabWidget" name="tabWidgetRegFunc">
-       <property name="minimumSize">
-        <size>
-         <width>100</width>
-         <height>100</height>
-        </size>
-       </property>
-       <property name="currentIndex">
-        <number>0</number>
-       </property>
-       <widget class="QWidget" name="tabRegisters">
-        <attribute name="title">
-         <string>Registers</string>
-        </attribute>
-        <layout class="QHBoxLayout" name="registerLayout">
-         <property name="spacing">
-          <number>0</number>
-         </property>
-         <property name="leftMargin">
-          <number>0</number>
-         </property>
-         <property name="topMargin">
-          <number>0</number>
-         </property>
-         <property name="rightMargin">
-          <number>0</number>
-         </property>
-         <property name="bottomMargin">
-          <number>0</number>
-         </property>
-         <item>
-          <widget class="RegisterWidget" name="registerWidget" native="true">
-           <property name="sizePolicy">
-            <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
-             <horstretch>0</horstretch>
-             <verstretch>0</verstretch>
-            </sizepolicy>
-           </property>
-           <property name="minimumSize">
-            <size>
-             <width>320</width>
-             <height>100</height>
-            </size>
-           </property>
-           <property name="font">
-            <font>
-             <family>Monospace</family>
-            </font>
-           </property>
-           <property name="focusPolicy">
-            <enum>Qt::StrongFocus</enum>
-           </property>
-           <property name="contextMenuPolicy">
-            <enum>Qt::CustomContextMenu</enum>
-           </property>
-           <property name="autoFillBackground">
-            <bool>true</bool>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </widget>
-       <widget class="QWidget" name="tabFunctions">
-        <attribute name="title">
-         <string>Functions</string>
-        </attribute>
-       </widget>
-       <widget class="QWidget" name="tabMemorySearch">
-        <attribute name="title">
-         <string>Memory Search</string>
-        </attribute>
-        <layout class="QHBoxLayout" name="memorySearchLayout">
-         <property name="spacing">
-          <number>0</number>
-         </property>
-         <property name="leftMargin">
-          <number>0</number>
-         </property>
-         <property name="topMargin">
-          <number>0</number>
-         </property>
-         <property name="rightMargin">
-          <number>0</number>
-         </property>
-         <property name="bottomMargin">
-          <number>0</number>
-         </property>
-         <item>
-          <widget class="MemorySearchWidget" name="memorySearchWidget" native="true">
-           <property name="sizePolicy">
-            <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-             <horstretch>0</horstretch>
-             <verstretch>0</verstretch>
-            </sizepolicy>
-           </property>
-           <property name="font">
-            <font>
-             <family>Monospace</family>
-            </font>
-           </property>
-           <property name="focusPolicy">
-            <enum>Qt::StrongFocus</enum>
-           </property>
-           <property name="contextMenuPolicy">
-            <enum>Qt::CustomContextMenu</enum>
-           </property>
-           <property name="autoFillBackground">
-            <bool>true</bool>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </widget>
-      </widget>
-      <widget class="DisassemblyWidget" name="disassemblyWidget" native="true">
-       <property name="sizePolicy">
-        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
-         <horstretch>0</horstretch>
-         <verstretch>1</verstretch>
-        </sizepolicy>
-       </property>
-       <property name="minimumSize">
-        <size>
-         <width>100</width>
-         <height>100</height>
-        </size>
-       </property>
-       <property name="font">
-        <font>
-         <family>Monospace</family>
-        </font>
-       </property>
-       <property name="focusPolicy">
-        <enum>Qt::StrongFocus</enum>
-       </property>
-       <property name="contextMenuPolicy">
-        <enum>Qt::CustomContextMenu</enum>
-       </property>
-       <property name="autoFillBackground">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </widget>
-     <widget class="QTabWidget" name="tabWidget">
-      <property name="minimumSize">
-       <size>
-        <width>150</width>
-        <height>0</height>
-       </size>
-      </property>
-      <property name="tabPosition">
-       <enum>QTabWidget::North</enum>
-      </property>
-      <property name="currentIndex">
-       <number>0</number>
-      </property>
-      <widget class="QWidget" name="tab_memory">
-       <property name="sizePolicy">
-        <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-         <horstretch>0</horstretch>
-         <verstretch>0</verstretch>
-        </sizepolicy>
-       </property>
-       <attribute name="title">
-        <string>Memory</string>
-       </attribute>
-       <layout class="QHBoxLayout" name="memoryLayout">
-        <property name="spacing">
-         <number>6</number>
-        </property>
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="MemoryViewWidget" name="memoryviewWidget" native="true">
-          <property name="sizePolicy">
-           <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-            <horstretch>0</horstretch>
-            <verstretch>0</verstretch>
-           </sizepolicy>
-          </property>
-          <property name="minimumSize">
-           <size>
-            <width>250</width>
-            <height>100</height>
-           </size>
-          </property>
-          <property name="font">
-           <font>
-            <family>Monospace</family>
-           </font>
-          </property>
-          <property name="contextMenuPolicy">
-           <enum>Qt::CustomContextMenu</enum>
-          </property>
-          <property name="autoFillBackground">
-           <bool>true</bool>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </widget>
-      <widget class="QWidget" name="tab_breakpoints">
-       <attribute name="title">
-        <string>Breakpoints</string>
-       </attribute>
-       <layout class="QHBoxLayout" name="breakpointsLayout">
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="QTableView" name="breakpointList">
-          <property name="contextMenuPolicy">
-           <enum>Qt::CustomContextMenu</enum>
-          </property>
-          <property name="horizontalScrollBarPolicy">
-           <enum>Qt::ScrollBarAlwaysOff</enum>
-          </property>
-          <property name="gridStyle">
-           <enum>Qt::NoPen</enum>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </widget>
-      <widget class="QWidget" name="tab_threads">
-       <attribute name="title">
-        <string>Threads</string>
-       </attribute>
-       <layout class="QHBoxLayout" name="threadsLayout">
-        <property name="spacing">
-         <number>0</number>
-        </property>
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="QTableView" name="threadList">
-          <property name="contextMenuPolicy">
-           <enum>Qt::CustomContextMenu</enum>
-          </property>
-          <property name="selectionMode">
-           <enum>QAbstractItemView::SingleSelection</enum>
-          </property>
-          <property name="gridStyle">
-           <enum>Qt::NoPen</enum>
-          </property>
-          <property name="cornerButtonEnabled">
-           <bool>false</bool>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </widget>
-      <widget class="QWidget" name="tab_callstack">
-       <attribute name="title">
-        <string>Active Call Stack</string>
-       </attribute>
-       <layout class="QHBoxLayout" name="callStackLayout">
-        <property name="spacing">
-         <number>0</number>
-        </property>
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="QTableView" name="stackList">
-          <property name="contextMenuPolicy">
-           <enum>Qt::CustomContextMenu</enum>
-          </property>
-          <property name="horizontalScrollBarPolicy">
-           <enum>Qt::ScrollBarAlwaysOff</enum>
-          </property>
-          <property name="sizeAdjustPolicy">
-           <enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
-          </property>
-          <property name="selectionMode">
-           <enum>QAbstractItemView::SingleSelection</enum>
-          </property>
-          <property name="gridStyle">
-           <enum>Qt::NoPen</enum>
-          </property>
-          <property name="cornerButtonEnabled">
-           <bool>false</bool>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </widget>
-      <widget class="QWidget" name="tab_savedaddresses">
-       <attribute name="title">
-        <string>Saved Addresses</string>
-       </attribute>
-       <layout class="QHBoxLayout" name="savedAddressesLayout">
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="QTableView" name="savedAddressesList">
-          <property name="contextMenuPolicy">
-           <enum>Qt::CustomContextMenu</enum>
-          </property>
-          <property name="horizontalScrollBarPolicy">
-           <enum>Qt::ScrollBarAlwaysOff</enum>
-          </property>
-          <property name="gridStyle">
-           <enum>Qt::NoPen</enum>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </widget>
-      <widget class="QWidget" name="tabGlobalVariables">
-       <attribute name="title">
-        <string>Globals</string>
-       </attribute>
-      </widget>
-      <widget class="QWidget" name="tabLocalVariables">
-       <attribute name="title">
-        <string>Locals</string>
-       </attribute>
-      </widget>
-      <widget class="QWidget" name="tabParameterVariables">
-       <attribute name="title">
-        <string>Parameters</string>
-       </attribute>
-      </widget>
-     </widget>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>DisassemblyWidget</class>
-   <extends>QWidget</extends>
-   <header>pcsx2-qt/Debugger/DisassemblyWidget.h</header>
-   <container>1</container>
-  </customwidget>
-  <customwidget>
-   <class>RegisterWidget</class>
-   <extends>QWidget</extends>
-   <header>pcsx2-qt/Debugger/RegisterWidget.h</header>
-   <container>1</container>
-  </customwidget>
-  <customwidget>
-   <class>MemoryViewWidget</class>
-   <extends>QWidget</extends>
-   <header>pcsx2-qt/Debugger/MemoryViewWidget.h</header>
-   <container>1</container>
-  </customwidget>
-  <customwidget>
-   <class>MemorySearchWidget</class>
-   <extends>QWidget</extends>
-   <header>pcsx2-qt/Debugger/MemorySearchWidget.h</header>
-   <container>1</container>
-  </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
diff --git a/pcsx2-qt/Debugger/DebuggerSettingsManager.cpp b/pcsx2-qt/Debugger/DebuggerSettingsManager.cpp
index f4b7ace1ca..81a23c8858 100644
--- a/pcsx2-qt/Debugger/DebuggerSettingsManager.cpp
+++ b/pcsx2-qt/Debugger/DebuggerSettingsManager.cpp
@@ -10,12 +10,12 @@
 
 #include "common/Console.h"
 #include "VMManager.h"
-#include "Models/BreakpointModel.h"
 
 std::mutex DebuggerSettingsManager::writeLock;
 const QString DebuggerSettingsManager::settingsFileVersion = "0.00";
 
-QJsonObject DebuggerSettingsManager::loadGameSettingsJSON() {
+QJsonObject DebuggerSettingsManager::loadGameSettingsJSON()
+{
 	std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
 	QFile file(QString::fromStdString(path));
 	if (!file.open(QIODevice::ReadOnly))
@@ -134,7 +134,7 @@ void DebuggerSettingsManager::saveGameSettings(QAbstractTableModel* abstractTabl
 {
 	const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
 	if (path.empty())
-		return; 
+		return;
 
 	const std::lock_guard<std::mutex> lock(writeLock);
 	QJsonObject loadedSettings = loadGameSettingsJSON();
diff --git a/pcsx2-qt/Debugger/DebuggerSettingsManager.h b/pcsx2-qt/Debugger/DebuggerSettingsManager.h
index 4cbf373543..6c264f670c 100644
--- a/pcsx2-qt/Debugger/DebuggerSettingsManager.h
+++ b/pcsx2-qt/Debugger/DebuggerSettingsManager.h
@@ -6,8 +6,8 @@
 
 #include <mutex>
 
-#include "Models/BreakpointModel.h"
-#include "Models/SavedAddressesModel.h"
+#include "Breakpoints/BreakpointModel.h"
+#include "Memory/SavedAddressesModel.h"
 
 class DebuggerSettingsManager final
 {
diff --git a/pcsx2-qt/Debugger/DebuggerWidget.cpp b/pcsx2-qt/Debugger/DebuggerWidget.cpp
new file mode 100644
index 0000000000..c4d2148ba5
--- /dev/null
+++ b/pcsx2-qt/Debugger/DebuggerWidget.cpp
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "DebuggerWidget.h"
+
+#include "common/Assertions.h"
+
+DebuggerWidget::DebuggerWidget(DebugInterface* cpu, QWidget* parent)
+	: QWidget(parent)
+	, m_cpu(cpu)
+{
+}
+
+DebugInterface& DebuggerWidget::cpu() const
+{
+	pxAssertRel(m_cpu, "DebuggerWidget::cpu() called on object that doesn't have a CPU type set.");
+	return *m_cpu;
+}
+
+void DebuggerWidget::applyMonospaceFont()
+{
+	// Easiest way to handle cross platform monospace fonts
+	// There are issues related to TabWidget -> Children font inheritance otherwise
+#if defined(WIN32)
+	setStyleSheet(QStringLiteral("font: 10pt 'Lucida Console'"));
+#elif defined(__APPLE__)
+	setStyleSheet(QStringLiteral("font: 10pt 'Monaco'"));
+#else
+	setStyleSheet(QStringLiteral("font: 10pt 'Monospace'"));
+#endif
+}
diff --git a/pcsx2-qt/Debugger/DebuggerWidget.h b/pcsx2-qt/Debugger/DebuggerWidget.h
new file mode 100644
index 0000000000..53fec8c4b4
--- /dev/null
+++ b/pcsx2-qt/Debugger/DebuggerWidget.h
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "DebugTools/DebugInterface.h"
+
+#include <QtWidgets/QWidget>
+
+inline void not_yet_implemented()
+{
+	abort();
+}
+
+class DebuggerWidget : public QWidget
+{
+	Q_OBJECT
+
+protected:
+	DebuggerWidget(DebugInterface* cpu, QWidget* parent = nullptr);
+
+	DebugInterface& cpu() const;
+
+	void applyMonospaceFont();
+
+private:
+	DebugInterface* m_cpu;
+};
diff --git a/pcsx2-qt/Debugger/DebuggerWindow.cpp b/pcsx2-qt/Debugger/DebuggerWindow.cpp
index ee43eafa06..47be09fc85 100644
--- a/pcsx2-qt/Debugger/DebuggerWindow.cpp
+++ b/pcsx2-qt/Debugger/DebuggerWindow.cpp
@@ -12,20 +12,11 @@
 #include "AnalysisOptionsDialog.h"
 
 DebuggerWindow::DebuggerWindow(QWidget* parent)
-	: QMainWindow(parent)
+	: KDDockWidgets::QtWidgets::MainWindow(QStringLiteral("DebuggerWindow"), {}, parent)
+	, m_dock_manager(this)
 {
 	m_ui.setupUi(this);
 
-// Easiest way to handle cross platform monospace fonts
-// There are issues related to TabWidget -> Children font inheritance otherwise
-#if defined(WIN32)
-	m_ui.cpuTabs->setStyleSheet(QStringLiteral("font: 8pt 'Lucida Console'"));
-#elif defined(__APPLE__)
-	m_ui.cpuTabs->setStyleSheet(QStringLiteral("font: 10pt 'Monaco'"));
-#else
-	m_ui.cpuTabs->setStyleSheet(QStringLiteral("font: 8pt 'Monospace'"));
-#endif
-
 	connect(m_ui.actionRun, &QAction::triggered, this, &DebuggerWindow::onRunPause);
 	connect(m_ui.actionStepInto, &QAction::triggered, this, &DebuggerWindow::onStepInto);
 	connect(m_ui.actionStepOver, &QAction::triggered, this, &DebuggerWindow::onStepOver);
@@ -39,15 +30,18 @@ DebuggerWindow::DebuggerWindow(QWidget* parent)
 	onVMStateChanged(); // If we missed a state change while we weren't loaded
 
 	// We can't do this in the designer, but we want to right align the actionOnTop action in the toolbar
-	QWidget* spacer = new QWidget(this);
-	spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
-	m_ui.toolBar->insertWidget(m_ui.actionAnalyse, spacer);
+	//QWidget* spacer = new QWidget(this);
+	//spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+	//m_ui.toolBar->insertWidget(m_ui.actionAnalyse, spacer);
 
-	m_cpuWidget_r5900 = new CpuWidget(this, r5900Debug);
-	m_cpuWidget_r3000 = new CpuWidget(this, r3000Debug);
+	//m_ui.cpuTabs->addTab(m_cpuWidget_r5900, "R5900");
+	//m_ui.cpuTabs->addTab(m_cpuWidget_r3000, "R3000");
 
-	m_ui.cpuTabs->addTab(m_cpuWidget_r5900, "R5900");
-	m_ui.cpuTabs->addTab(m_cpuWidget_r3000, "R3000");
+	m_dock_manager.switchToLayout(0);
+
+	//QTabBar* tabs = new QTabBar();
+	//tabs->addTab("Test");
+	//m_ui.menuBar->layout()->addWidget(tabs);
 }
 
 DebuggerWindow::~DebuggerWindow() = default;
@@ -56,8 +50,8 @@ DebuggerWindow::~DebuggerWindow() = default;
 // Sorry colour blind people, but this is the best we can do for now
 void DebuggerWindow::setTabActiveStyle(BreakPointCpu enabledCpu)
 {
-	m_ui.cpuTabs->tabBar()->setTabTextColor(m_ui.cpuTabs->indexOf(m_cpuWidget_r5900), (enabledCpu == BREAKPOINT_EE) ? Qt::red : this->palette().text().color());
-	m_ui.cpuTabs->tabBar()->setTabTextColor(m_ui.cpuTabs->indexOf(m_cpuWidget_r3000), (enabledCpu == BREAKPOINT_IOP) ? Qt::red : this->palette().text().color());
+	//m_ui.cpuTabs->tabBar()->setTabTextColor(m_ui.cpuTabs->indexOf(m_cpuWidget_r5900), (enabledCpu == BREAKPOINT_EE) ? Qt::red : this->palette().text().color());
+	//m_ui.cpuTabs->tabBar()->setTabTextColor(m_ui.cpuTabs->indexOf(m_cpuWidget_r3000), (enabledCpu == BREAKPOINT_IOP) ? Qt::red : this->palette().text().color());
 }
 
 void DebuggerWindow::onVMStateChanged()
@@ -87,10 +81,10 @@ void DebuggerWindow::onVMStateChanged()
 			switch (triggeredCpu)
 			{
 				case BREAKPOINT_EE:
-					m_ui.cpuTabs->setCurrentWidget(m_cpuWidget_r5900);
+					//m_ui.cpuTabs->setCurrentWidget(m_cpuWidget_r5900);
 					break;
 				case BREAKPOINT_IOP:
-					m_ui.cpuTabs->setCurrentWidget(m_cpuWidget_r3000);
+					//m_ui.cpuTabs->setCurrentWidget(m_cpuWidget_r3000);
 					break;
 				default:
 					break;
@@ -115,20 +109,20 @@ void DebuggerWindow::onRunPause()
 
 void DebuggerWindow::onStepInto()
 {
-	CpuWidget* currentCpu = static_cast<CpuWidget*>(m_ui.cpuTabs->currentWidget());
-	currentCpu->onStepInto();
+	//CpuWidget* currentCpu = static_cast<CpuWidget*>(m_ui.cpuTabs->currentWidget());
+	//currentCpu->onStepInto();
 }
 
 void DebuggerWindow::onStepOver()
 {
-	CpuWidget* currentCpu = static_cast<CpuWidget*>(m_ui.cpuTabs->currentWidget());
-	currentCpu->onStepOver();
+	//CpuWidget* currentCpu = static_cast<CpuWidget*>(m_ui.cpuTabs->currentWidget());
+	//currentCpu->onStepOver();
 }
 
 void DebuggerWindow::onStepOut()
 {
-	CpuWidget* currentCpu = static_cast<CpuWidget*>(m_ui.cpuTabs->currentWidget());
-	currentCpu->onStepOut();
+	//CpuWidget* currentCpu = static_cast<CpuWidget*>(m_ui.cpuTabs->currentWidget());
+	//currentCpu->onStepOut();
 }
 
 void DebuggerWindow::onAnalyse()
diff --git a/pcsx2-qt/Debugger/DebuggerWindow.h b/pcsx2-qt/Debugger/DebuggerWindow.h
index 26b4b0fc5d..65df2dd71a 100644
--- a/pcsx2-qt/Debugger/DebuggerWindow.h
+++ b/pcsx2-qt/Debugger/DebuggerWindow.h
@@ -3,11 +3,13 @@
 
 #pragma once
 
-#include "CpuWidget.h"
-
 #include "ui_DebuggerWindow.h"
 
-class DebuggerWindow : public QMainWindow
+#include "DockManager.h"
+
+#include <kddockwidgets/MainWindow.h>
+
+class DebuggerWindow : public KDDockWidgets::QtWidgets::MainWindow
 {
 	Q_OBJECT
 
@@ -25,7 +27,7 @@ public slots:
 
 protected:
 	void showEvent(QShowEvent* event);
-	void hideEvent(QHideEvent *event);
+	void hideEvent(QHideEvent* event);
 
 private:
 	Ui::DebuggerWindow m_ui;
@@ -34,8 +36,7 @@ private:
 	QAction* m_actionStepOver;
 	QAction* m_actionStepOut;
 
-	CpuWidget* m_cpuWidget_r5900;
-	CpuWidget* m_cpuWidget_r3000;
+	DockManager m_dock_manager;
 
 	void setTabActiveStyle(BreakPointCpu toggledCPU);
 };
diff --git a/pcsx2-qt/Debugger/DebuggerWindow.ui b/pcsx2-qt/Debugger/DebuggerWindow.ui
index c668a6316b..c0bd9b9487 100644
--- a/pcsx2-qt/Debugger/DebuggerWindow.ui
+++ b/pcsx2-qt/Debugger/DebuggerWindow.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>800</width>
-    <height>600</height>
+    <width>1000</width>
+    <height>750</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -18,19 +18,12 @@
     <normalon>:/icons/AppIcon64.png</normalon>
    </iconset>
   </property>
-  <widget class="QWidget" name="centralwidget">
-   <layout class="QGridLayout" name="gridLayout">
-    <item row="0" column="0">
-     <widget class="QTabWidget" name="cpuTabs"/>
-    </item>
-   </layout>
-  </widget>
-  <widget class="QToolBar" name="toolBar">
+  <widget class="QToolBar" name="debugToolBar">
    <property name="contextMenuPolicy">
-    <enum>Qt::ContextMenuPolicy::PreventContextMenu</enum>
+    <enum>Qt::PreventContextMenu</enum>
    </property>
    <property name="movable">
-    <bool>false</bool>
+    <bool>true</bool>
    </property>
    <property name="iconSize">
     <size>
@@ -39,10 +32,10 @@
     </size>
    </property>
    <property name="toolButtonStyle">
-    <enum>Qt::ToolButtonStyle::ToolButtonTextBesideIcon</enum>
+    <enum>Qt::ToolButtonTextBesideIcon</enum>
    </property>
    <property name="floatable">
-    <bool>false</bool>
+    <bool>true</bool>
    </property>
    <attribute name="toolBarArea">
     <enum>TopToolBarArea</enum>
@@ -54,7 +47,63 @@
    <addaction name="actionStepInto"/>
    <addaction name="actionStepOver"/>
    <addaction name="actionStepOut"/>
-   <addaction name="actionAnalyse"/>
+  </widget>
+  <widget class="QMenuBar" name="menuBar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>1000</width>
+     <height>20</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuFile">
+    <property name="title">
+     <string>File</string>
+    </property>
+    <addaction name="actionAnalyse"/>
+   </widget>
+   <widget class="QMenu" name="menuDebug">
+    <property name="title">
+     <string>Debug</string>
+    </property>
+    <addaction name="actionRun"/>
+    <addaction name="actionStepInto"/>
+    <addaction name="actionStepOver"/>
+    <addaction name="actionStepOut"/>
+   </widget>
+   <widget class="QMenu" name="menuWindows">
+    <property name="title">
+     <string>Windows</string>
+    </property>
+   </widget>
+   <widget class="QMenu" name="menuLayouts">
+    <property name="title">
+     <string>Layouts</string>
+    </property>
+   </widget>
+   <widget class="QMenu" name="menuView">
+    <property name="title">
+     <string>View</string>
+    </property>
+    <addaction name="actionOnTop"/>
+   </widget>
+   <addaction name="menuFile"/>
+   <addaction name="menuView"/>
+   <addaction name="menuDebug"/>
+   <addaction name="menuWindows"/>
+   <addaction name="menuLayouts"/>
+  </widget>
+  <widget class="QToolBar" name="viewToolBar">
+   <property name="windowTitle">
+    <string>toolBar</string>
+   </property>
+   <attribute name="toolBarArea">
+    <enum>TopToolBarArea</enum>
+   </attribute>
+   <attribute name="toolBarBreak">
+    <bool>false</bool>
+   </attribute>
    <addaction name="actionOnTop"/>
   </widget>
   <action name="actionRun">
diff --git a/pcsx2-qt/Debugger/DisassemblyWidget.cpp b/pcsx2-qt/Debugger/DisassemblyWidget.cpp
index 097a5d566a..fb8f9b63f4 100644
--- a/pcsx2-qt/Debugger/DisassemblyWidget.cpp
+++ b/pcsx2-qt/Debugger/DisassemblyWidget.cpp
@@ -19,12 +19,16 @@
 
 using namespace QtUtils;
 
-DisassemblyWidget::DisassemblyWidget(QWidget* parent)
-	: QWidget(parent)
+DisassemblyWidget::DisassemblyWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu, parent)
 {
-	ui.setupUi(this);
+	m_ui.setupUi(this);
+
+	m_disassemblyManager.setCpu(&cpu);
 
 	connect(this, &DisassemblyWidget::customContextMenuRequested, this, &DisassemblyWidget::customMenuRequested);
+
+	applyMonospaceFont();
 }
 
 DisassemblyWidget::~DisassemblyWidget() = default;
@@ -46,7 +50,7 @@ void DisassemblyWidget::contextCopyInstructionText()
 
 void DisassemblyWidget::contextAssembleInstruction()
 {
-	if (!m_cpu->isCpuPaused())
+	if (!cpu().isCpuPaused())
 	{
 		QMessageBox::warning(this, tr("Assemble Error"), tr("Unable to change assembly while core is running"));
 		return;
@@ -63,7 +67,7 @@ void DisassemblyWidget::contextAssembleInstruction()
 
 	u32 encodedInstruction;
 	std::string errorText;
-	bool valid = MipsAssembleOpcode(instruction.toLocal8Bit().constData(), m_cpu, m_selectedAddressStart, encodedInstruction, errorText);
+	bool valid = MipsAssembleOpcode(instruction.toLocal8Bit().constData(), &cpu(), m_selectedAddressStart, encodedInstruction, errorText);
 
 	if (!valid)
 	{
@@ -72,7 +76,7 @@ void DisassemblyWidget::contextAssembleInstruction()
 	}
 	else
 	{
-		Host::RunOnCPUThread([this, start = m_selectedAddressStart, end = m_selectedAddressEnd, cpu = m_cpu, val = encodedInstruction] {
+		Host::RunOnCPUThread([this, start = m_selectedAddressStart, end = m_selectedAddressEnd, cpu = &cpu(), val = encodedInstruction] {
 			for (u32 i = start; i <= end; i += 4)
 			{
 				this->m_nopedInstructions.insert({i, cpu->read32(i)});
@@ -85,7 +89,7 @@ void DisassemblyWidget::contextAssembleInstruction()
 
 void DisassemblyWidget::contextNoopInstruction()
 {
-	Host::RunOnCPUThread([this, start = m_selectedAddressStart, end = m_selectedAddressEnd, cpu = m_cpu] {
+	Host::RunOnCPUThread([this, start = m_selectedAddressStart, end = m_selectedAddressEnd, cpu = &cpu()] {
 		for (u32 i = start; i <= end; i += 4)
 		{
 			this->m_nopedInstructions.insert({i, cpu->read32(i)});
@@ -97,7 +101,7 @@ void DisassemblyWidget::contextNoopInstruction()
 
 void DisassemblyWidget::contextRestoreInstruction()
 {
-	Host::RunOnCPUThread([this, start = m_selectedAddressStart, end = m_selectedAddressEnd, cpu = m_cpu] {
+	Host::RunOnCPUThread([this, start = m_selectedAddressStart, end = m_selectedAddressEnd, cpu = &cpu()] {
 		for (u32 i = start; i <= end; i += 4)
 		{
 			if (this->m_nopedInstructions.find(i) != this->m_nopedInstructions.end())
@@ -113,7 +117,7 @@ void DisassemblyWidget::contextRestoreInstruction()
 void DisassemblyWidget::contextRunToCursor()
 {
 	const u32 selectedAddressStart = m_selectedAddressStart;
-	Host::RunOnCPUThread([cpu = m_cpu, selectedAddressStart] {
+	Host::RunOnCPUThread([cpu = &cpu(), selectedAddressStart] {
 		CBreakPoints::AddBreakPoint(cpu->getCpuType(), selectedAddressStart, true);
 		cpu->resumeCpu();
 	});
@@ -121,17 +125,17 @@ void DisassemblyWidget::contextRunToCursor()
 
 void DisassemblyWidget::contextJumpToCursor()
 {
-	m_cpu->setPc(m_selectedAddressStart);
+	cpu().setPc(m_selectedAddressStart);
 	this->repaint();
 }
 
 void DisassemblyWidget::contextToggleBreakpoint()
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 
 	const u32 selectedAddressStart = m_selectedAddressStart;
-	const BreakPointCpu cpuType = m_cpu->getCpuType();
+	const BreakPointCpu cpuType = cpu().getCpuType();
 	if (CBreakPoints::IsAddressBreakPoint(cpuType, selectedAddressStart))
 	{
 		Host::RunOnCPUThread([cpuType, selectedAddressStart] { CBreakPoints::RemoveBreakPoint(cpuType, selectedAddressStart); });
@@ -171,7 +175,7 @@ void DisassemblyWidget::contextGoToAddress()
 
 	u64 address = 0;
 	std::string error;
-	if (!m_cpu->evaluateExpression(targetString.toStdString().c_str(), address, error))
+	if (!cpu().evaluateExpression(targetString.toStdString().c_str(), address, error))
 	{
 		QMessageBox::warning(this, tr("Cannot Go To"), QString::fromStdString(error));
 		return;
@@ -182,7 +186,7 @@ void DisassemblyWidget::contextGoToAddress()
 
 void DisassemblyWidget::contextAddFunction()
 {
-	NewFunctionDialog* dialog = new NewFunctionDialog(*m_cpu, this);
+	NewFunctionDialog* dialog = new NewFunctionDialog(cpu(), this);
 	dialog->setName(QString("func_%1").arg(m_selectedAddressStart, 8, 16, QChar('0')));
 	dialog->setAddress(m_selectedAddressStart);
 	if (m_selectedAddressEnd != m_selectedAddressStart)
@@ -193,13 +197,13 @@ void DisassemblyWidget::contextAddFunction()
 
 void DisassemblyWidget::contextCopyFunctionName()
 {
-	std::string name = m_cpu->GetSymbolGuardian().FunctionStartingAtAddress(m_selectedAddressStart).name;
+	std::string name = cpu().GetSymbolGuardian().FunctionStartingAtAddress(m_selectedAddressStart).name;
 	QGuiApplication::clipboard()->setText(QString::fromStdString(name));
 }
 
 void DisassemblyWidget::contextRemoveFunction()
 {
-	m_cpu->GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
+	cpu().GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
 		ccc::Function* curFunc = database.functions.symbol_overlapping_address(m_selectedAddressStart);
 		if (!curFunc)
 			return;
@@ -215,7 +219,7 @@ void DisassemblyWidget::contextRemoveFunction()
 
 void DisassemblyWidget::contextRenameFunction()
 {
-	const FunctionInfo curFunc = m_cpu->GetSymbolGuardian().FunctionOverlappingAddress(m_selectedAddressStart);
+	const FunctionInfo curFunc = cpu().GetSymbolGuardian().FunctionOverlappingAddress(m_selectedAddressStart);
 
 	if (!curFunc.address.valid())
 	{
@@ -236,17 +240,17 @@ void DisassemblyWidget::contextRenameFunction()
 		return;
 	}
 
-	m_cpu->GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
+	cpu().GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
 		database.functions.rename_symbol(curFunc.handle, newName.toStdString());
 	});
 }
 
 void DisassemblyWidget::contextStubFunction()
 {
-	FunctionInfo function = m_cpu->GetSymbolGuardian().FunctionOverlappingAddress(m_selectedAddressStart);
+	FunctionInfo function = cpu().GetSymbolGuardian().FunctionOverlappingAddress(m_selectedAddressStart);
 	u32 address = function.address.valid() ? function.address.value : m_selectedAddressStart;
 
-	Host::RunOnCPUThread([this, address, cpu = m_cpu] {
+	Host::RunOnCPUThread([this, address, cpu = &cpu()] {
 		this->m_stubbedFunctions.insert({address, {cpu->read32(address), cpu->read32(address + 4)}});
 		cpu->write32(address, 0x03E00008); // jr ra
 		cpu->write32(address + 4, 0x00000000); // nop
@@ -257,7 +261,7 @@ void DisassemblyWidget::contextStubFunction()
 void DisassemblyWidget::contextRestoreFunction()
 {
 	u32 address = m_selectedAddressStart;
-	m_cpu->GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
+	cpu().GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
 		const ccc::Function* function = database.functions.symbol_overlapping_address(m_selectedAddressStart);
 		if (function)
 			address = function->address().value;
@@ -266,7 +270,7 @@ void DisassemblyWidget::contextRestoreFunction()
 	auto stub = m_stubbedFunctions.find(address);
 	if (stub != m_stubbedFunctions.end())
 	{
-		Host::RunOnCPUThread([this, address, cpu = m_cpu, stub] {
+		Host::RunOnCPUThread([this, address, cpu = &cpu(), stub] {
 			auto [first_instruction, second_instruction] = stub->second;
 			cpu->write32(address, first_instruction);
 			cpu->write32(address + 4, second_instruction);
@@ -286,12 +290,6 @@ void DisassemblyWidget::contextShowOpcode()
 	this->repaint();
 }
 
-void DisassemblyWidget::SetCpu(DebugInterface* cpu)
-{
-	m_cpu = cpu;
-	m_disassemblyManager.setCpu(cpu);
-}
-
 QString DisassemblyWidget::GetLineDisasm(u32 address)
 {
 	DisassemblyLineInfo lineInfo;
@@ -322,7 +320,7 @@ void DisassemblyWidget::paintEvent(QPaintEvent* event)
 	bool inSelectionBlock = false;
 	bool alternate = m_visibleStart % 8;
 
-	const u32 curPC = m_cpu->getPC(); // Get the PC here, because it'll change when we are drawing and make it seem like there are two PCs
+	const u32 curPC = cpu().getPC(); // Get the PC here, because it'll change when we are drawing and make it seem like there are two PCs
 
 	for (u32 i = 0; i <= m_visibleRows; i++)
 	{
@@ -347,7 +345,7 @@ void DisassemblyWidget::paintEvent(QPaintEvent* event)
 
 		// Breakpoint marker
 		bool enabled;
-		if (CBreakPoints::IsAddressBreakPoint(m_cpu->getCpuType(), rowAddress, &enabled) && !CBreakPoints::IsTempBreakPoint(m_cpu->getCpuType(), rowAddress))
+		if (CBreakPoints::IsAddressBreakPoint(cpu().getCpuType(), rowAddress, &enabled) && !CBreakPoints::IsTempBreakPoint(cpu().getCpuType(), rowAddress))
 		{
 			if (enabled)
 			{
@@ -506,11 +504,11 @@ void DisassemblyWidget::mousePressEvent(QMouseEvent* event)
 
 void DisassemblyWidget::mouseDoubleClickEvent(QMouseEvent* event)
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 
 	const u32 selectedAddress = (static_cast<int>(event->position().y()) / m_rowHeight * 4) + m_visibleStart;
-	const BreakPointCpu cpuType = m_cpu->getCpuType();
+	const BreakPointCpu cpuType = cpu().getCpuType();
 	if (CBreakPoints::IsAddressBreakPoint(cpuType, selectedAddress))
 	{
 		Host::RunOnCPUThread([cpuType, selectedAddress] { CBreakPoints::RemoveBreakPoint(cpuType, selectedAddress); });
@@ -598,7 +596,7 @@ void DisassemblyWidget::keyPressEvent(QKeyEvent* event)
 			contextFollowBranch();
 			break;
 		case Qt::Key_Left:
-			gotoAddressAndSetFocus(m_cpu->getPC());
+			gotoAddressAndSetFocus(cpu().getPC());
 			break;
 		case Qt::Key_O:
 			m_showInstructionOpcode = !m_showInstructionOpcode;
@@ -610,7 +608,7 @@ void DisassemblyWidget::keyPressEvent(QKeyEvent* event)
 
 void DisassemblyWidget::customMenuRequested(QPoint pos)
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 
 	QMenu* contextMenu = new QMenu(this);
@@ -623,7 +621,7 @@ void DisassemblyWidget::customMenuRequested(QPoint pos)
 	contextMenu->addAction(action = new QAction(tr("&Copy Instruction Text"), this));
 	action->setShortcut(QKeySequence(Qt::Key_C));
 	connect(action, &QAction::triggered, this, &DisassemblyWidget::contextCopyInstructionText);
-	if (m_cpu->GetSymbolGuardian().FunctionExistsWithStartingAddress(m_selectedAddressStart))
+	if (cpu().GetSymbolGuardian().FunctionExistsWithStartingAddress(m_selectedAddressStart))
 	{
 		contextMenu->addAction(action = new QAction(tr("Copy Function Name"), this));
 		connect(action, &QAction::triggered, this, &DisassemblyWidget::contextCopyFunctionName);
@@ -695,18 +693,18 @@ inline QString DisassemblyWidget::DisassemblyStringFromAddress(u32 address, QFon
 {
 	DisassemblyLineInfo line;
 
-	if (!m_cpu->isValidAddress(address))
+	if (!cpu().isValidAddress(address))
 		return tr("%1 NOT VALID ADDRESS").arg(address, 8, 16, QChar('0')).toUpper();
 	// Todo? support non symbol view?
 	m_disassemblyManager.getLine(address, true, line);
 
-	const bool isConditional = line.info.isConditional && m_cpu->getPC() == address;
+	const bool isConditional = line.info.isConditional && cpu().getPC() == address;
 	const bool isConditionalMet = line.info.conditionMet;
-	const bool isCurrentPC = m_cpu->getPC() == address;
+	const bool isCurrentPC = cpu().getPC() == address;
 
-	FunctionInfo function = m_cpu->GetSymbolGuardian().FunctionStartingAtAddress(address);
-	SymbolInfo symbol = m_cpu->GetSymbolGuardian().SymbolStartingAtAddress(address);
-	const bool showOpcode = m_showInstructionOpcode && m_cpu->isAlive();
+	FunctionInfo function = cpu().GetSymbolGuardian().FunctionStartingAtAddress(address);
+	SymbolInfo symbol = cpu().GetSymbolGuardian().SymbolStartingAtAddress(address);
+	const bool showOpcode = m_showInstructionOpcode && cpu().isAlive();
 
 	QString lineString;
 	if (showOpcode)
@@ -739,7 +737,7 @@ inline QString DisassemblyWidget::DisassemblyStringFromAddress(u32 address, QFon
 
 	if (showOpcode)
 	{
-		const u32 opcode = m_cpu->read32(address);
+		const u32 opcode = cpu().read32(address);
 		lineString = lineString.arg(QtUtils::FilledQStringFromValue(opcode, 16));
 	}
 
@@ -789,7 +787,7 @@ QColor DisassemblyWidget::GetAddressFunctionColor(u32 address)
 	// Use the address to pick the colour since the value of the handle may
 	// change from run to run.
 	ccc::Address function_address =
-		m_cpu->GetSymbolGuardian().FunctionOverlappingAddress(address).address;
+		cpu().GetSymbolGuardian().FunctionOverlappingAddress(address).address;
 	if (!function_address.valid())
 		return palette().text().color();
 
@@ -817,7 +815,7 @@ QString DisassemblyWidget::FetchSelectionInfo(SelectionInfo selInfo)
 		}
 		else // INSTRUCTIONHEX
 		{
-			infoBlock += FilledQStringFromValue(m_cpu->read32(i), 16);
+			infoBlock += FilledQStringFromValue(cpu().read32(i), 16);
 		}
 	}
 	return infoBlock;
@@ -831,7 +829,7 @@ void DisassemblyWidget::gotoAddressAndSetFocus(u32 address)
 void DisassemblyWidget::gotoProgramCounterOnPause()
 {
 	if (m_goToProgramCounterOnPause)
-		gotoAddress(m_cpu->getPC(), false);
+		gotoAddress(cpu().getPC(), false);
 }
 
 void DisassemblyWidget::gotoAddress(u32 address, bool should_set_focus)
@@ -861,7 +859,7 @@ bool DisassemblyWidget::AddressCanRestore(u32 start, u32 end)
 
 bool DisassemblyWidget::FunctionCanRestore(u32 address)
 {
-	FunctionInfo function = m_cpu->GetSymbolGuardian().FunctionOverlappingAddress(address);
+	FunctionInfo function = cpu().GetSymbolGuardian().FunctionOverlappingAddress(address);
 	if (function.address.valid())
 		address = function.address.value;
 
diff --git a/pcsx2-qt/Debugger/DisassemblyWidget.h b/pcsx2-qt/Debugger/DisassemblyWidget.h
index 9d2670c487..85a51f692e 100644
--- a/pcsx2-qt/Debugger/DisassemblyWidget.h
+++ b/pcsx2-qt/Debugger/DisassemblyWidget.h
@@ -5,24 +5,22 @@
 
 #include "ui_DisassemblyWidget.h"
 
+#include "DebuggerWidget.h"
+
 #include "pcsx2/DebugTools/DebugInterface.h"
 #include "pcsx2/DebugTools/DisassemblyManager.h"
 
-#include <QtWidgets/QWidget>
 #include <QtWidgets/QMenu>
 #include <QtGui/QPainter>
 
-class DisassemblyWidget final : public QWidget
+class DisassemblyWidget final : public DebuggerWidget
 {
 	Q_OBJECT
 
 public:
-	DisassemblyWidget(QWidget* parent);
+	DisassemblyWidget(DebugInterface& cpu, QWidget* parent = nullptr);
 	~DisassemblyWidget();
 
-	// Required because our constructor needs to take no extra arguments.
-	void SetCpu(DebugInterface* cpu);
-
 	// Required for the breakpoint list (ugh wtf)
 	QString GetLineDisasm(u32 address);
 
@@ -69,9 +67,8 @@ signals:
 	void VMUpdate();
 
 private:
-	Ui::DisassemblyWidget ui;
+	Ui::DisassemblyWidget m_ui;
 
-	DebugInterface* m_cpu;
 	u32 m_visibleStart = 0x00336318; // The address of the first opcode shown(row 0)
 	u32 m_visibleRows;
 	u32 m_selectedAddressStart = 0;
diff --git a/pcsx2-qt/Debugger/DockManager.cpp b/pcsx2-qt/Debugger/DockManager.cpp
new file mode 100644
index 0000000000..409c3cece8
--- /dev/null
+++ b/pcsx2-qt/Debugger/DockManager.cpp
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "DockManager.h"
+
+#include "DebuggerWindow.h"
+#include "DisassemblyWidget.h"
+#include "RegisterWidget.h"
+#include "StackWidget.h"
+#include "ThreadWidget.h"
+#include "Breakpoints/BreakpointWidget.h"
+#include "Memory/MemorySearchWidget.h"
+#include "Memory/MemoryViewWidget.h"
+#include "Memory/SavedAddressesWidget.h"
+#include "SymbolTree/SymbolTreeWidgets.h"
+
+#include <kddockwidgets/Config.h>
+
+#include <QtCore/QTimer>
+#include <QtCore/QtTranslation>
+
+#define FOR_EACH_DEBUGGER_DOCK_WIDGET \
+	/* Top right. */ \
+	X(DisassemblyWidget, QT_TRANSLATE_NOOP("DockWidget", "Disassembly"), OnRight, Root) \
+	/* Bottom. */ \
+	X(MemoryViewWidget, QT_TRANSLATE_NOOP("DockWidget", "Memory"), OnBottom, DisassemblyWidget) \
+	X(BreakpointWidget, QT_TRANSLATE_NOOP("DockWidget", "Breakpoints"), None, MemoryViewWidget) \
+	X(ThreadWidget, QT_TRANSLATE_NOOP("DockWidget", "Threads"), None, MemoryViewWidget) \
+	X(StackWidget, QT_TRANSLATE_NOOP("DockWidget", "Stack"), None, MemoryViewWidget) \
+	X(SavedAddressesWidget, QT_TRANSLATE_NOOP("DockWidget", "Saved Addresses"), None, MemoryViewWidget) \
+	X(GlobalVariableTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Globals"), None, MemoryViewWidget) \
+	X(LocalVariableTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Locals"), None, MemoryViewWidget) \
+	X(ParameterVariableTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Parameters"), None, MemoryViewWidget) \
+	/* Top left. */ \
+	X(RegisterWidget, QT_TRANSLATE_NOOP("DockWidget", "Registers"), OnLeft, DisassemblyWidget) \
+	X(FunctionTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Functions"), None, RegisterWidget) \
+	X(MemorySearchWidget, QT_TRANSLATE_NOOP("DockWidget", "Memory Search"), None, RegisterWidget)
+
+DockManager::DockManager(DebuggerWindow* window)
+	: m_window(window)
+{
+	createDefaultLayout("R5900", r5900Debug);
+	//createDefaultLayout("R3000", r3000Debug);
+	loadLayouts();
+}
+
+void DockManager::configure_docking_system()
+{
+	KDDockWidgets::Config::self().setFlags(
+		KDDockWidgets::Config::Flag_HideTitleBarWhenTabsVisible |
+		KDDockWidgets::Config::Flag_AlwaysShowTabs |
+		KDDockWidgets::Config::Flag_AllowReorderTabs |
+		KDDockWidgets::Config::Flag_TabsHaveCloseButton |
+		KDDockWidgets::Config::Flag_TitleBarIsFocusable);
+}
+
+const std::vector<DockManager::Layout>& DockManager::layouts()
+{
+	return m_layouts;
+}
+
+void DockManager::switchToLayout(size_t layout)
+{
+	//m_layouts.at(m_current_layout).dock_manager->setParent(nullptr);
+	//m_window->setCentralWidget(m_layouts.at(layout).dock_manager);
+	//m_current_layout = layout;
+}
+
+size_t DockManager::cloneLayout(size_t existing_layout, std::string new_name)
+{
+	return 0;
+}
+
+bool DockManager::deleteLayout(size_t layout)
+{
+	return false;
+}
+
+void DockManager::loadLayouts()
+{
+}
+
+void DockManager::saveLayouts()
+{
+}
+
+size_t DockManager::createDefaultLayout(const char* name, DebugInterface& cpu)
+{
+	size_t index = m_layouts.size();
+
+	Layout& layout = m_layouts.emplace_back();
+	layout.name = name;
+	layout.cpu = cpu.getCpuType();
+	layout.user_defined = false;
+
+	KDDockWidgets::QtWidgets::DockWidget* dock_Root = nullptr;
+#define X(Type, title, Location, Parent) \
+	KDDockWidgets::QtWidgets::DockWidget* dock_##Type = new KDDockWidgets::QtWidgets::DockWidget(title); \
+	dock_##Type->setWidget(new Type(cpu)); \
+	if (KDDockWidgets::Location_##Location != KDDockWidgets::Location_None) \
+		m_window->addDockWidget(dock_##Type, KDDockWidgets::Location_##Location, dock_##Parent); \
+	else \
+		dock_##Parent->addDockWidgetAsTab(dock_##Type);
+	FOR_EACH_DEBUGGER_DOCK_WIDGET
+#undef X
+
+	return index;
+}
diff --git a/pcsx2-qt/Debugger/DockManager.h b/pcsx2-qt/Debugger/DockManager.h
new file mode 100644
index 0000000000..430908a8a1
--- /dev/null
+++ b/pcsx2-qt/Debugger/DockManager.h
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "DebugTools/DebugInterface.h"
+
+#include <kddockwidgets/MainWindow.h>
+#include <kddockwidgets/DockWidget.h>
+
+class DebuggerWindow;
+
+class DockManager
+{
+public:
+	struct Layout
+	{
+		std::string name;
+		BreakPointCpu cpu;
+		bool user_defined = false;
+	};
+
+	DockManager(DebuggerWindow* window);
+
+	static void configure_docking_system();
+
+	const std::vector<Layout>& layouts();
+	void switchToLayout(size_t layout);
+	size_t cloneLayout(size_t existing_layout, std::string new_name);
+	bool deleteLayout(size_t layout);
+
+	void loadLayouts();
+	void saveLayouts();
+
+protected:
+	size_t createDefaultLayout(const char* name, DebugInterface& cpu);
+
+	KDDockWidgets::QtWidgets::MainWindow* m_window;
+
+	std::vector<Layout> m_layouts;
+	size_t m_current_layout = 0;
+};
diff --git a/pcsx2-qt/Debugger/MemorySearchWidget.cpp b/pcsx2-qt/Debugger/Memory/MemorySearchWidget.cpp
similarity index 95%
rename from pcsx2-qt/Debugger/MemorySearchWidget.cpp
rename to pcsx2-qt/Debugger/Memory/MemorySearchWidget.cpp
index a3f1cc419c..cf629f2cca 100644
--- a/pcsx2-qt/Debugger/MemorySearchWidget.cpp
+++ b/pcsx2-qt/Debugger/Memory/MemorySearchWidget.cpp
@@ -23,8 +23,8 @@ using SearchResult = MemorySearchWidget::SearchResult;
 
 using namespace QtUtils;
 
-MemorySearchWidget::MemorySearchWidget(QWidget* parent)
-    : QWidget(parent)
+MemorySearchWidget::MemorySearchWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu, parent)
 {
 	m_ui.setupUi(this);
 	this->repaint();
@@ -32,8 +32,7 @@ MemorySearchWidget::MemorySearchWidget(QWidget* parent)
 	m_ui.listSearchResults->setContextMenuPolicy(Qt::CustomContextMenu);
 	connect(m_ui.btnSearch, &QPushButton::clicked, this, &MemorySearchWidget::onSearchButtonClicked);
 	connect(m_ui.btnFilterSearch, &QPushButton::clicked, this, &MemorySearchWidget::onSearchButtonClicked);
-	connect(m_ui.listSearchResults, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item)
-	{
+	connect(m_ui.listSearchResults, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) {
 		emit switchToMemoryViewTab();
 		emit goToAddressInMemoryView(item->text().toUInt(nullptr, 16));
 	});
@@ -48,11 +47,6 @@ MemorySearchWidget::MemorySearchWidget(QWidget* parent)
 	connect(&m_resultsLoadTimer, &QTimer::timeout, this, &MemorySearchWidget::loadSearchResults);
 }
 
-void MemorySearchWidget::setCpu(DebugInterface* cpu)
-{
-	m_cpu = cpu;
-}
-
 void MemorySearchWidget::contextSearchResultGoToDisassembly()
 {
 	const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel();
@@ -121,15 +115,15 @@ void MemorySearchWidget::onListSearchResultsContextMenu(QPoint pos)
 	contextMenu->popup(m_ui.listSearchResults->viewport()->mapToGlobal(pos));
 }
 
-template<typename T>
+template <typename T>
 T readValueAtAddress(DebugInterface* cpu, u32 addr);
-template<>
+template <>
 float readValueAtAddress<float>(DebugInterface* cpu, u32 addr)
 {
 	return std::bit_cast<float>(cpu->read32(addr));
 }
 
-template<>
+template <>
 double readValueAtAddress<double>(DebugInterface* cpu, u32 addr)
 {
 	return std::bit_cast<double>(cpu->read64(addr));
@@ -230,7 +224,7 @@ template <typename T>
 bool handleSearchComparison(SearchComparison searchComparison, u32 searchAddress, const SearchResult* priorResult, T searchValue, T readValue)
 {
 	const bool isNotOperator = searchComparison == SearchComparison::NotEquals || searchComparison == SearchComparison::NotChanged;
-	switch (searchComparison) 
+	switch (searchComparison)
 	{
 		case SearchComparison::Equals:
 		case SearchComparison::NotEquals:
@@ -302,7 +296,7 @@ void searchWorker(DebugInterface* cpu, std::vector<SearchResult>& searchResults,
 		{
 			if (!cpu->isValidAddress(addr))
 				continue;
-			
+
 			T readValue = readValueAtAddress<T>(cpu, addr);
 			if (handleSearchComparison(searchComparison, addr, nullptr, searchValue, readValue))
 			{
@@ -315,7 +309,7 @@ void searchWorker(DebugInterface* cpu, std::vector<SearchResult>& searchResults,
 		auto removeIt = std::remove_if(searchResults.begin(), searchResults.end(), [cpu, searchType, searchComparison, searchValue](SearchResult& searchResult) -> bool {
 			const u32 addr = searchResult.getAddress();
 			if (!cpu->isValidAddress(addr))
-					 return true;
+				return true;
 
 			const auto readValue = readValueAtAddress<T>(cpu, addr);
 
@@ -415,7 +409,7 @@ static void searchWorkerByteArray(DebugInterface* cpu, SearchType searchType, Se
 	}
 	else
 	{
-		auto removeIt = std::remove_if(searchResults.begin(), searchResults.end(), [ searchComparison, searchType, searchValue, cpu ](SearchResult& searchResult) -> bool {
+		auto removeIt = std::remove_if(searchResults.begin(), searchResults.end(), [searchComparison, searchType, searchValue, cpu](SearchResult& searchResult) -> bool {
 			const u32 addr = searchResult.getAddress();
 			if (!cpu->isValidAddress(addr))
 				return true;
@@ -476,7 +470,7 @@ std::vector<SearchResult> startWorker(DebugInterface* cpu, const SearchType type
 
 void MemorySearchWidget::onSearchButtonClicked()
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 
 	const SearchType searchType = getCurrentSearchType();
@@ -575,6 +569,12 @@ void MemorySearchWidget::onSearchButtonClicked()
 		}
 	}
 
+	if (!isFilterSearch && (searchComparison == SearchComparison::Changed || searchComparison == SearchComparison::ChangedBy || searchComparison == SearchComparison::Decreased || searchComparison == SearchComparison::DecreasedBy || searchComparison == SearchComparison::Increased || searchComparison == SearchComparison::IncreasedBy || searchComparison == SearchComparison::NotChanged))
+	{
+		QMessageBox::critical(this, tr("Debugger"), tr("This search comparison can only be used with filter searches."));
+		return;
+	}
+
 	QFutureWatcher<std::vector<SearchResult>>* workerWatcher = new QFutureWatcher<std::vector<SearchResult>>();
 	auto onSearchFinished = [this, workerWatcher] {
 		m_ui.btnSearch->setDisabled(false);
@@ -597,7 +597,7 @@ void MemorySearchWidget::onSearchButtonClicked()
 		m_searchResults.clear();
 	}
 
-	QFuture<std::vector<SearchResult>> workerFuture = QtConcurrent::run(startWorker, m_cpu, searchType, searchComparison, std::move(m_searchResults), searchStart, searchEnd, searchValue, searchHex ? 16 : 10);
+	QFuture<std::vector<SearchResult>> workerFuture = QtConcurrent::run(startWorker, &cpu(), searchType, searchComparison, std::move(m_searchResults), searchStart, searchEnd, searchValue, searchHex ? 16 : 10);
 	workerWatcher->setFuture(workerFuture);
 	connect(workerWatcher, &QFutureWatcher<std::vector<SearchResult>>::finished, onSearchFinished);
 	m_searchResults.clear();
@@ -708,7 +708,7 @@ void MemorySearchWidget::updateSearchComparisonSelections()
 std::vector<SearchComparison> MemorySearchWidget::getValidSearchComparisonsForState(SearchType type, std::vector<SearchResult>& existingResults)
 {
 	const bool hasResults = existingResults.size() > 0;
-	std::vector<SearchComparison> comparisons = { SearchComparison::Equals };
+	std::vector<SearchComparison> comparisons = {SearchComparison::Equals};
 
 	if (type == SearchType::ArrayType || type == SearchType::StringType)
 	{
diff --git a/pcsx2-qt/Debugger/MemorySearchWidget.h b/pcsx2-qt/Debugger/Memory/MemorySearchWidget.h
similarity index 90%
rename from pcsx2-qt/Debugger/MemorySearchWidget.h
rename to pcsx2-qt/Debugger/Memory/MemorySearchWidget.h
index 50f44e3286..38bf6ba6db 100644
--- a/pcsx2-qt/Debugger/MemorySearchWidget.h
+++ b/pcsx2-qt/Debugger/Memory/MemorySearchWidget.h
@@ -5,22 +5,23 @@
 
 #include "ui_MemorySearchWidget.h"
 
+#include "Debugger/DebuggerWidget.h"
+
 #include "DebugTools/DebugInterface.h"
 
 #include <QtWidgets/QWidget>
 #include <QtCore/QTimer>
 #include <QtCore/QMap>
 
-class MemorySearchWidget final : public QWidget
+class MemorySearchWidget final : public DebuggerWidget
 {
-    Q_OBJECT
+	Q_OBJECT
 
 public:
-	MemorySearchWidget(QWidget* parent);
-    ~MemorySearchWidget() = default;
-	void setCpu(DebugInterface* cpu);
+	MemorySearchWidget(DebugInterface& cpu, QWidget* parent = nullptr);
+	~MemorySearchWidget() = default;
 
-    enum class SearchType
+	enum class SearchType
 	{
 		ByteType,
 		Int16Type,
@@ -77,9 +78,11 @@ public:
 		{
 			return labelToEnumMap.value(comparisonLabel, SearchComparison::Invalid);
 		}
-		QString enumToLabel(SearchComparison comparison) {
+		QString enumToLabel(SearchComparison comparison)
+		{
 			return enumToLabelMap.value(comparison, "");
 		}
+
 	private:
 		QMap<SearchComparison, QString> enumToLabelMap;
 		QMap<QString, SearchComparison> labelToEnumMap;
@@ -100,7 +103,9 @@ public:
 	public:
 		SearchResult() {}
 		SearchResult(u32 address, const QVariant& value, SearchType type)
-			: address(address), value(value), type(type)
+			: address(address)
+			, value(value)
+			, type(type)
 		{
 		}
 		bool isIntegerValue() const { return type == SearchType::ByteType || type == SearchType::Int16Type || type == SearchType::Int32Type || type == SearchType::Int64Type; }
@@ -111,7 +116,7 @@ public:
 		SearchType getType() const { return type; }
 		QByteArray getArrayValue() const { return isArrayValue() ? value.toByteArray() : QByteArray(); }
 
-		template<typename T>
+		template <typename T>
 		T getValue() const
 		{
 			return value.value<T>();
@@ -139,14 +144,13 @@ private:
 	std::vector<SearchResult> m_searchResults;
 	SearchComparisonLabelMap m_searchComparisonLabelMap;
 	Ui::MemorySearchWidget m_ui;
-	DebugInterface* m_cpu;
 	QTimer m_resultsLoadTimer;
 
 	u32 m_initialResultsLoadLimit = 20000;
 	u32 m_numResultsAddedPerLoad = 10000;
 
 	void updateSearchComparisonSelections();
-	std::vector<SearchComparison> getValidSearchComparisonsForState(SearchType type, std::vector<SearchResult> &existingResults);
+	std::vector<SearchComparison> getValidSearchComparisonsForState(SearchType type, std::vector<SearchResult>& existingResults);
 	SearchType getCurrentSearchType();
 	SearchComparison getCurrentSearchComparison();
 	bool doesSearchComparisonTakeInput(SearchComparison comparison);
diff --git a/pcsx2-qt/Debugger/MemorySearchWidget.ui b/pcsx2-qt/Debugger/Memory/MemorySearchWidget.ui
similarity index 100%
rename from pcsx2-qt/Debugger/MemorySearchWidget.ui
rename to pcsx2-qt/Debugger/Memory/MemorySearchWidget.ui
diff --git a/pcsx2-qt/Debugger/MemoryViewWidget.cpp b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp
similarity index 96%
rename from pcsx2-qt/Debugger/MemoryViewWidget.cpp
rename to pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp
index 5753bcce5d..bbb25a1ec8 100644
--- a/pcsx2-qt/Debugger/MemoryViewWidget.cpp
+++ b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp
@@ -173,6 +173,10 @@ void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32
 
 void MemoryViewTable::SelectAt(QPoint pos)
 {
+	// Check if SelectAt was called before DrawTable.
+	if (rowHeight == 0)
+		return;
+
 	const u32 selectedRow = (pos.y() - 2) / (rowHeight);
 	const s32 x = pos.x();
 	const s32 avgSegmentWidth = segmentXAxis[1] - segmentXAxis[0];
@@ -447,37 +451,37 @@ bool MemoryViewTable::KeyPress(int key, QChar keychar)
 /*
 	MemoryViewWidget
 */
-MemoryViewWidget::MemoryViewWidget(QWidget* parent)
-	: QWidget(parent)
+MemoryViewWidget::MemoryViewWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu)
 	, m_table(this)
 {
 	ui.setupUi(this);
 	this->setFocusPolicy(Qt::FocusPolicy::ClickFocus);
 	connect(this, &MemoryViewWidget::customContextMenuRequested, this, &MemoryViewWidget::customMenuRequested);
+
+	m_table.SetCpu(&cpu);
+	m_table.UpdateStartAddress(0x480000);
+
+	applyMonospaceFont();
 }
 
 MemoryViewWidget::~MemoryViewWidget() = default;
 
-void MemoryViewWidget::SetCpu(DebugInterface* cpu)
-{
-	m_cpu = cpu;
-	m_table.SetCpu(cpu);
-	m_table.UpdateStartAddress(0x480000);
-}
-
 void MemoryViewWidget::paintEvent(QPaintEvent* event)
 {
-	if (!m_cpu->isAlive())
-		return;
-
 	QPainter painter(this);
 
+	painter.fillRect(rect(), palette().window());
+
+	if (!cpu().isAlive())
+		return;
+
 	m_table.DrawTable(painter, this->palette(), this->height());
 }
 
 void MemoryViewWidget::mousePressEvent(QMouseEvent* event)
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 
 	m_table.SelectAt(event->pos());
@@ -486,7 +490,7 @@ void MemoryViewWidget::mousePressEvent(QMouseEvent* event)
 
 void MemoryViewWidget::customMenuRequested(QPoint pos)
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 
 	if (!m_contextMenu)
@@ -571,7 +575,7 @@ void MemoryViewWidget::customMenuRequested(QPoint pos)
 
 void MemoryViewWidget::contextCopyByte()
 {
-	QApplication::clipboard()->setText(QString::number(m_cpu->read8(m_table.selectedAddress), 16).toUpper());
+	QApplication::clipboard()->setText(QString::number(cpu().read8(m_table.selectedAddress), 16).toUpper());
 }
 
 void MemoryViewWidget::contextCopySegment()
@@ -581,7 +585,7 @@ void MemoryViewWidget::contextCopySegment()
 
 void MemoryViewWidget::contextCopyCharacter()
 {
-	QApplication::clipboard()->setText(QChar::fromLatin1(m_cpu->read8(m_table.selectedAddress)).toUpper());
+	QApplication::clipboard()->setText(QChar::fromLatin1(cpu().read8(m_table.selectedAddress)).toUpper());
 }
 
 void MemoryViewWidget::contextPaste()
@@ -600,7 +604,7 @@ void MemoryViewWidget::contextGoToAddress()
 
 	u64 address = 0;
 	std::string error;
-	if (!m_cpu->evaluateExpression(targetString.toStdString().c_str(), address, error))
+	if (!cpu().evaluateExpression(targetString.toStdString().c_str(), address, error))
 	{
 		QMessageBox::warning(this, tr("Cannot Go To"), QString::fromStdString(error));
 		return;
diff --git a/pcsx2-qt/Debugger/MemoryViewWidget.h b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.h
similarity index 93%
rename from pcsx2-qt/Debugger/MemoryViewWidget.h
rename to pcsx2-qt/Debugger/Memory/MemoryViewWidget.h
index 03bc5baac8..e410e799a2 100644
--- a/pcsx2-qt/Debugger/MemoryViewWidget.h
+++ b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.h
@@ -3,7 +3,9 @@
 
 #pragma once
 
-#include "ui_RegisterWidget.h"
+#include "ui_MemoryViewWidget.h"
+
+#include "Debugger/DebuggerWidget.h"
 
 #include "DebugTools/DebugInterface.h"
 #include "DebugTools/DisassemblyManager.h"
@@ -105,16 +107,14 @@ public:
 };
 
 
-class MemoryViewWidget final : public QWidget
+class MemoryViewWidget final : public DebuggerWidget
 {
 	Q_OBJECT
 
 public:
-	MemoryViewWidget(QWidget* parent);
+	MemoryViewWidget(DebugInterface& cpu, QWidget* parent = nullptr);
 	~MemoryViewWidget();
 
-	void SetCpu(DebugInterface* cpu);
-
 protected:
 	void paintEvent(QPaintEvent* event);
 	void mousePressEvent(QMouseEvent* event);
@@ -138,7 +138,7 @@ signals:
 	void VMUpdate();
 
 private:
-	Ui::RegisterWidget ui;
+	Ui::MemoryViewWidget ui;
 
 	QMenu* m_contextMenu = 0x0;
 	QAction* m_actionLittleEndian;
@@ -147,6 +147,5 @@ private:
 	QAction* m_actionWORD;
 	QAction* m_actionDWORD;
 
-	DebugInterface* m_cpu;
 	MemoryViewTable m_table;
 };
diff --git a/pcsx2-qt/Debugger/MemoryViewWidget.ui b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.ui
similarity index 81%
rename from pcsx2-qt/Debugger/MemoryViewWidget.ui
rename to pcsx2-qt/Debugger/Memory/MemoryViewWidget.ui
index c9d13501bb..54103700aa 100644
--- a/pcsx2-qt/Debugger/MemoryViewWidget.ui
+++ b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.ui
@@ -10,6 +10,9 @@
     <height>300</height>
    </rect>
   </property>
+  <property name="contextMenuPolicy">
+   <enum>Qt::CustomContextMenu</enum>
+  </property>
   <property name="windowTitle">
    <string>Memory</string>
   </property>
diff --git a/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp b/pcsx2-qt/Debugger/Memory/SavedAddressesModel.cpp
similarity index 100%
rename from pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp
rename to pcsx2-qt/Debugger/Memory/SavedAddressesModel.cpp
diff --git a/pcsx2-qt/Debugger/Models/SavedAddressesModel.h b/pcsx2-qt/Debugger/Memory/SavedAddressesModel.h
similarity index 100%
rename from pcsx2-qt/Debugger/Models/SavedAddressesModel.h
rename to pcsx2-qt/Debugger/Memory/SavedAddressesModel.h
diff --git a/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.cpp b/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.cpp
new file mode 100644
index 0000000000..356c53f5c3
--- /dev/null
+++ b/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.cpp
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "SavedAddressesWidget.h"
+
+#include "QtUtils.h"
+#include "Debugger/DebuggerSettingsManager.h"
+
+#include <QClipboard>
+#include <QMenu>
+
+SavedAddressesWidget::SavedAddressesWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu, parent)
+	, m_model(cpu)
+{
+	//m_ui.savedAddressesList->setModel(&m_model);
+	//m_ui.savedAddressesList->setContextMenuPolicy(Qt::CustomContextMenu);
+	//connect(m_ui.savedAddressesList, &QTableView::customContextMenuRequested, this, &CpuWidget::onSavedAddressesListContextMenu);
+	//for (std::size_t i = 0; auto mode : SavedAddressesModel::HeaderResizeModes)
+	//{
+	//	m_ui.savedAddressesList->horizontalHeader()->setSectionResizeMode(i++, mode);
+	//}
+	//QTableView* savedAddressesTableView = m_ui.savedAddressesList;
+	//connect(m_ui.savedAddressesList->model(), &QAbstractItemModel::dataChanged, [savedAddressesTableView](const QModelIndex& topLeft) {
+	//	savedAddressesTableView->resizeColumnToContents(topLeft.column());
+	//});
+}
+
+void SavedAddressesWidget::onContextMenu(QPoint pos)
+{
+	QMenu* contextMenu = new QMenu("Saved Addresses List Context Menu", m_ui.savedAddressesList);
+
+	QAction* newAction = new QAction(tr("New"), m_ui.savedAddressesList);
+	connect(newAction, &QAction::triggered, this, &SavedAddressesWidget::contextNew);
+	contextMenu->addAction(newAction);
+
+	const QModelIndex indexAtPos = m_ui.savedAddressesList->indexAt(pos);
+	const bool isIndexValid = indexAtPos.isValid();
+
+	if (isIndexValid)
+	{
+		if (cpu().isAlive())
+		{
+			QAction* goToAddressMemViewAction = new QAction(tr("Go to in Memory View"), m_ui.savedAddressesList);
+			connect(goToAddressMemViewAction, &QAction::triggered, this, [this, indexAtPos]() {
+				const QModelIndex rowAddressIndex = m_ui.savedAddressesList->model()->index(indexAtPos.row(), 0, QModelIndex());
+				//m_ui.memoryviewWidget->gotoAddress(m_ui.savedAddressesList->model()->data(rowAddressIndex, Qt::UserRole).toUInt());
+				//m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory);
+				not_yet_implemented();
+			});
+			contextMenu->addAction(goToAddressMemViewAction);
+
+			QAction* goToAddressDisassemblyAction = new QAction(tr("Go to in Disassembly"), m_ui.savedAddressesList);
+			connect(goToAddressDisassemblyAction, &QAction::triggered, this, [this, indexAtPos]() {
+				const QModelIndex rowAddressIndex =
+					m_ui.savedAddressesList->model()->index(indexAtPos.row(), 0, QModelIndex());
+				//m_ui.disassemblyWidget->gotoAddressAndSetFocus(
+				//	m_ui.savedAddressesList->model()->data(rowAddressIndex, Qt::UserRole).toUInt());
+				not_yet_implemented();
+			});
+			contextMenu->addAction(goToAddressDisassemblyAction);
+		}
+
+		QAction* copyAction = new QAction(indexAtPos.column() == 0 ? tr("Copy Address") : tr("Copy Text"), m_ui.savedAddressesList);
+		connect(copyAction, &QAction::triggered, [this, indexAtPos]() {
+			QGuiApplication::clipboard()->setText(
+				m_ui.savedAddressesList->model()->data(indexAtPos, Qt::DisplayRole).toString());
+		});
+		contextMenu->addAction(copyAction);
+	}
+
+	if (m_ui.savedAddressesList->model()->rowCount() > 0)
+	{
+		QAction* actionExportCSV = new QAction(tr("Copy all as CSV"), m_ui.savedAddressesList);
+		connect(actionExportCSV, &QAction::triggered, [this]() {
+			QGuiApplication::clipboard()->setText(
+				QtUtils::AbstractItemModelToCSV(m_ui.savedAddressesList->model(), Qt::DisplayRole, true));
+		});
+		contextMenu->addAction(actionExportCSV);
+	}
+
+	QAction* actionImportCSV = new QAction(tr("Paste from CSV"), m_ui.savedAddressesList);
+	connect(actionImportCSV, &QAction::triggered, this, &SavedAddressesWidget::contextPasteCSV);
+	contextMenu->addAction(actionImportCSV);
+
+	if (cpu().isAlive())
+	{
+		QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.savedAddressesList);
+		connect(actionLoad, &QAction::triggered, [this]() {
+			m_model.clear();
+			DebuggerSettingsManager::loadGameSettings(&m_model);
+		});
+		contextMenu->addAction(actionLoad);
+
+		QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.savedAddressesList);
+		connect(actionSave, &QAction::triggered, this, &SavedAddressesWidget::saveToDebuggerSettings);
+		contextMenu->addAction(actionSave);
+	}
+
+	if (isIndexValid)
+	{
+		QAction* deleteAction = new QAction(tr("Delete"), m_ui.savedAddressesList);
+		connect(deleteAction, &QAction::triggered, this, [this, indexAtPos]() {
+			m_ui.savedAddressesList->model()->removeRows(indexAtPos.row(), 1);
+		});
+		contextMenu->addAction(deleteAction);
+	}
+
+	contextMenu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos));
+}
+
+void SavedAddressesWidget::contextPasteCSV()
+{
+	QString csv = QGuiApplication::clipboard()->text();
+	// Skip header
+	csv = csv.mid(csv.indexOf('\n') + 1);
+
+	for (const QString& line : csv.split('\n'))
+	{
+		QStringList fields;
+		// In order to handle text with commas in them we must wrap values in quotes to mark
+		// where a value starts and end so that text commas aren't identified as delimiters.
+		// So matches each quote pair, parse it out, and removes the quotes to get the value.
+		QRegularExpression eachQuotePair(R"("([^"]|\\.)*")");
+		QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line);
+		while (it.hasNext())
+		{
+			QRegularExpressionMatch match = it.next();
+			QString matchedValue = match.captured(0);
+			fields << matchedValue.mid(1, matchedValue.length() - 2);
+		}
+
+		m_model.loadSavedAddressFromFieldList(fields);
+	}
+}
+
+void SavedAddressesWidget::contextNew()
+{
+	qobject_cast<SavedAddressesModel*>(m_ui.savedAddressesList->model())->addRow();
+	const u32 rowCount = m_ui.savedAddressesList->model()->rowCount();
+	m_ui.savedAddressesList->edit(m_ui.savedAddressesList->model()->index(rowCount - 1, 0));
+}
+
+void SavedAddressesWidget::addAddress(u32 address)
+{
+	qobject_cast<SavedAddressesModel*>(m_ui.savedAddressesList->model())->addRow();
+	const u32 rowCount = m_ui.savedAddressesList->model()->rowCount();
+	const QModelIndex addressIndex = m_ui.savedAddressesList->model()->index(rowCount - 1, 0);
+	//m_ui.tabWidget->setCurrentWidget(m_ui.tab_savedaddresses);
+	not_yet_implemented();
+	m_ui.savedAddressesList->model()->setData(addressIndex, address, Qt::UserRole);
+	m_ui.savedAddressesList->edit(m_ui.savedAddressesList->model()->index(rowCount - 1, 1));
+}
+
+void SavedAddressesWidget::saveToDebuggerSettings()
+{
+	DebuggerSettingsManager::saveGameSettings(&m_model);
+}
diff --git a/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.h b/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.h
new file mode 100644
index 0000000000..ed73046c65
--- /dev/null
+++ b/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.h
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "ui_SavedAddressesWidget.h"
+
+#include "SavedAddressesModel.h"
+
+#include "Debugger/DebuggerWidget.h"
+
+class SavedAddressesWidget : public DebuggerWidget
+{
+	Q_OBJECT
+
+public:
+	SavedAddressesWidget(DebugInterface& cpu, QWidget* parent = nullptr);
+
+	void onContextMenu(QPoint pos);
+	void contextPasteCSV();
+	void contextNew();
+	void addAddress(u32 address);
+	void saveToDebuggerSettings();
+
+private:
+	Ui::SavedAddressesWidget m_ui;
+
+	SavedAddressesModel m_model;
+};
diff --git a/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.ui b/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.ui
new file mode 100644
index 0000000000..a8d756f623
--- /dev/null
+++ b/pcsx2-qt/Debugger/Memory/SavedAddressesWidget.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SavedAddressesWidget</class>
+ <widget class="QWidget" name="SavedAddressesWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Saved Addresses</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTableWidget" name="savedAddressesList">
+     <property name="contextMenuPolicy">
+      <enum>Qt::CustomContextMenu</enum>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/pcsx2-qt/Debugger/RegisterWidget.cpp b/pcsx2-qt/Debugger/RegisterWidget.cpp
index b0c80a0bd0..49e4fbf65c 100644
--- a/pcsx2-qt/Debugger/RegisterWidget.cpp
+++ b/pcsx2-qt/Debugger/RegisterWidget.cpp
@@ -20,8 +20,8 @@
 
 using namespace QtUtils;
 
-RegisterWidget::RegisterWidget(QWidget* parent)
-	: QWidget(parent)
+RegisterWidget::RegisterWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu)
 {
 	this->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
 
@@ -30,21 +30,19 @@ RegisterWidget::RegisterWidget(QWidget* parent)
 
 	connect(this, &RegisterWidget::customContextMenuRequested, this, &RegisterWidget::customMenuRequested);
 	connect(ui.registerTabs, &QTabBar::currentChanged, this, &RegisterWidget::tabCurrentChanged);
-};
 
-RegisterWidget::~RegisterWidget()
-{
-}
-
-void RegisterWidget::SetCpu(DebugInterface* cpu)
-{
-	m_cpu = cpu;
-	for (int i = 0; i < m_cpu->getRegisterCategoryCount(); i++)
+	for (int i = 0; i < cpu.getRegisterCategoryCount(); i++)
 	{
-		ui.registerTabs->addTab(m_cpu->getRegisterCategoryName(i));
+		ui.registerTabs->addTab(cpu.getRegisterCategoryName(i));
 	}
 
 	connect(ui.registerTabs, &QTabBar::currentChanged, [this]() { this->repaint(); });
+
+	applyMonospaceFont();
+}
+
+RegisterWidget::~RegisterWidget()
+{
 }
 
 void RegisterWidget::tabCurrentChanged(int cur)
@@ -54,9 +52,6 @@ void RegisterWidget::tabCurrentChanged(int cur)
 
 void RegisterWidget::paintEvent(QPaintEvent* event)
 {
-	if (!m_cpu)
-		return;
-
 	QPainter painter(this);
 	painter.setPen(this->palette().text().color());
 	m_renderStart = QPoint(0, ui.registerTabs->pos().y() + ui.registerTabs->size().height());
@@ -94,9 +89,9 @@ void RegisterWidget::paintEvent(QPaintEvent* event)
 	// off of that.
 	// Can probably constexpr the loop out as register names are known during runtime
 	int safeValueStartX = 0;
-	for (int i = 0; i < m_cpu->getRegisterCount(categoryIndex); i++)
+	for (int i = 0; i < cpu().getRegisterCount(categoryIndex); i++)
 	{
-		const int registerNameWidth = strlen(m_cpu->getRegisterName(categoryIndex, i));
+		const int registerNameWidth = strlen(cpu().getRegisterName(categoryIndex, i));
 		if (safeValueStartX < registerNameWidth)
 		{
 			safeValueStartX = registerNameWidth;
@@ -110,7 +105,7 @@ void RegisterWidget::paintEvent(QPaintEvent* event)
 	// Make it relative to where we start rendering
 	safeValueStartX += m_renderStart.x();
 
-	for (s32 i = 0; i < m_cpu->getRegisterCount(categoryIndex) - m_rowStart; i++)
+	for (s32 i = 0; i < cpu().getRegisterCount(categoryIndex) - m_rowStart; i++)
 	{
 		const s32 registerIndex = i + m_rowStart;
 		const int yStart = (i * m_rowHeight) + m_renderStart.y();
@@ -120,11 +115,11 @@ void RegisterWidget::paintEvent(QPaintEvent* event)
 
 		// Draw register name
 		painter.setPen(this->palette().text().color());
-		painter.drawText(m_renderStart.x() + painter.fontMetrics().averageCharWidth(), yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft, m_cpu->getRegisterName(categoryIndex, registerIndex));
+		painter.drawText(m_renderStart.x() + painter.fontMetrics().averageCharWidth(), yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft, cpu().getRegisterName(categoryIndex, registerIndex));
 
-		if (m_cpu->getRegisterSize(categoryIndex) == 128)
+		if (cpu().getRegisterSize(categoryIndex) == 128)
 		{
-			const u128 curRegister = m_cpu->getRegister(categoryIndex, registerIndex);
+			const u128 curRegister = cpu().getRegister(categoryIndex, registerIndex);
 
 			int regIndex = 3;
 			for (int j = 0; j < 4; j++)
@@ -136,7 +131,7 @@ void RegisterWidget::paintEvent(QPaintEvent* event)
 
 				if (categoryIndex == EECAT_VU0F && m_showVU0FFloat)
 					painter.drawText(m_fieldStartX[j], yStart, m_fieldWidth, m_rowHeight, Qt::AlignLeft,
-						painter.fontMetrics().elidedText(QString::number(std::bit_cast<float>(m_cpu->getRegister(categoryIndex, registerIndex)._u32[regIndex])), Qt::ElideRight, m_fieldWidth - painter.fontMetrics().averageCharWidth()));
+						painter.fontMetrics().elidedText(QString::number(std::bit_cast<float>(cpu().getRegister(categoryIndex, registerIndex)._u32[regIndex])), Qt::ElideRight, m_fieldWidth - painter.fontMetrics().averageCharWidth()));
 				else
 					painter.drawText(m_fieldStartX[j], yStart, m_fieldWidth, m_rowHeight,
 						Qt::AlignLeft, FilledQStringFromValue(curRegister._u32[regIndex], 16));
@@ -153,13 +148,13 @@ void RegisterWidget::paintEvent(QPaintEvent* event)
 
 			if (categoryIndex == EECAT_FPR && m_showFPRFloat)
 				painter.drawText(safeValueStartX, yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft,
-					QString("%1").arg(QString::number(std::bit_cast<float>(m_cpu->getRegister(categoryIndex, registerIndex)._u32[0]))).toUpper());
-			else if (m_cpu->getRegisterSize(categoryIndex) == 64)
+					QString("%1").arg(QString::number(std::bit_cast<float>(cpu().getRegister(categoryIndex, registerIndex)._u32[0]))).toUpper());
+			else if (cpu().getRegisterSize(categoryIndex) == 64)
 				painter.drawText(safeValueStartX, yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft,
-					FilledQStringFromValue(m_cpu->getRegister(categoryIndex, registerIndex).lo, 16));
+					FilledQStringFromValue(cpu().getRegister(categoryIndex, registerIndex).lo, 16));
 			else
 				painter.drawText(safeValueStartX, yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft,
-					FilledQStringFromValue(m_cpu->getRegister(categoryIndex, registerIndex)._u32[0], 16));
+					FilledQStringFromValue(cpu().getRegister(categoryIndex, registerIndex)._u32[0], 16));
 		}
 	}
 	painter.end();
@@ -171,7 +166,7 @@ void RegisterWidget::mousePressEvent(QMouseEvent* event)
 	m_selectedRow = static_cast<int>(((event->position().y() - m_renderStart.y()) / m_rowHeight)) + m_rowStart;
 
 	// For 128 bit types, support selecting segments
-	if (m_cpu->getRegisterSize(categoryIndex) == 128)
+	if (cpu().getRegisterSize(categoryIndex) == 128)
 	{
 		constexpr auto inRange = [](u32 low, u32 high, u32 val) {
 			return (low <= val && val <= high);
@@ -190,7 +185,7 @@ void RegisterWidget::mousePressEvent(QMouseEvent* event)
 
 void RegisterWidget::wheelEvent(QWheelEvent* event)
 {
-	if (event->angleDelta().y() < 0 && m_rowEnd < m_cpu->getRegisterCount(ui.registerTabs->currentIndex()))
+	if (event->angleDelta().y() < 0 && m_rowEnd < cpu().getRegisterCount(ui.registerTabs->currentIndex()))
 	{
 		m_rowStart += 1;
 	}
@@ -204,12 +199,12 @@ void RegisterWidget::wheelEvent(QWheelEvent* event)
 
 void RegisterWidget::mouseDoubleClickEvent(QMouseEvent* event)
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 	if (m_selectedRow > m_rowEnd) // Unsigned underflow; selectedRow will be > m_rowEnd (technically negative)
 		return;
 	const int categoryIndex = ui.registerTabs->currentIndex();
-	if (m_cpu->getRegisterSize(categoryIndex) == 128)
+	if (cpu().getRegisterSize(categoryIndex) == 128)
 		contextChangeSegment();
 	else
 		contextChangeValue();
@@ -217,7 +212,7 @@ void RegisterWidget::mouseDoubleClickEvent(QMouseEvent* event)
 
 void RegisterWidget::customMenuRequested(QPoint pos)
 {
-	if (!m_cpu->isAlive())
+	if (!cpu().isAlive())
 		return;
 
 	if (m_selectedRow > m_rowEnd) // Unsigned underflow; selectedRow will be > m_rowEnd (technically negative)
@@ -248,7 +243,7 @@ void RegisterWidget::customMenuRequested(QPoint pos)
 		m_contextMenu->addSeparator();
 	}
 
-	if (m_cpu->getRegisterSize(categoryIndex) == 128)
+	if (cpu().getRegisterSize(categoryIndex) == 128)
 	{
 		m_contextMenu->addAction(action = new QAction(tr("Copy Top Half"), this));
 		connect(action, &QAction::triggered, this, &RegisterWidget::contextCopyTop);
@@ -265,7 +260,7 @@ void RegisterWidget::customMenuRequested(QPoint pos)
 
 	m_contextMenu->addSeparator();
 
-	if (m_cpu->getRegisterSize(categoryIndex) == 128)
+	if (cpu().getRegisterSize(categoryIndex) == 128)
 	{
 		m_contextMenu->addAction(action = new QAction(tr("Change Top Half"), this));
 		connect(action, &QAction::triggered, this, &RegisterWidget::contextChangeTop);
@@ -295,7 +290,7 @@ void RegisterWidget::customMenuRequested(QPoint pos)
 void RegisterWidget::contextCopyValue()
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
-	const u128 val = m_cpu->getRegister(categoryIndex, m_selectedRow);
+	const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
 	if (CAT_SHOW_FLOAT)
 		QApplication::clipboard()->setText(QString("%1").arg(QString::number(std::bit_cast<float>(val._u32[0])).toUpper(), 16));
 	else
@@ -305,21 +300,21 @@ void RegisterWidget::contextCopyValue()
 void RegisterWidget::contextCopyTop()
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
-	const u128 val = m_cpu->getRegister(categoryIndex, m_selectedRow);
+	const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
 	QApplication::clipboard()->setText(FilledQStringFromValue(val.hi, 16));
 }
 
 void RegisterWidget::contextCopyBottom()
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
-	const u128 val = m_cpu->getRegister(categoryIndex, m_selectedRow);
+	const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
 	QApplication::clipboard()->setText(FilledQStringFromValue(val.lo, 16));
 }
 
 void RegisterWidget::contextCopySegment()
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
-	const u128 val = m_cpu->getRegister(categoryIndex, m_selectedRow);
+	const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
 	if (CAT_SHOW_FLOAT)
 		QApplication::clipboard()->setText(FilledQStringFromValue(std::bit_cast<float>(val._u32[3 - m_selected128Field]), 10));
 	else
@@ -330,7 +325,7 @@ bool RegisterWidget::contextFetchNewValue(u64& out, u64 currentValue, bool segme
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
 	const bool floatingPoint = CAT_SHOW_FLOAT && segment;
-	const int regSize = m_cpu->getRegisterSize(categoryIndex);
+	const int regSize = cpu().getRegisterSize(categoryIndex);
 	bool ok = false;
 
 	QString existingValue("%1");
@@ -341,7 +336,7 @@ bool RegisterWidget::contextFetchNewValue(u64& out, u64 currentValue, bool segme
 		existingValue = existingValue.arg(std::bit_cast<float>((u32)currentValue));
 
 	//: Changing the value in a CPU register (e.g. "Change t0")
-	QString input = QInputDialog::getText(this, tr("Change %1").arg(m_cpu->getRegisterName(categoryIndex, m_selectedRow)), "",
+	QString input = QInputDialog::getText(this, tr("Change %1").arg(cpu().getRegisterName(categoryIndex, m_selectedRow)), "",
 		QLineEdit::Normal, existingValue, &ok);
 
 	if (!ok)
@@ -373,9 +368,9 @@ void RegisterWidget::contextChangeValue()
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
 	u64 newVal;
-	if (contextFetchNewValue(newVal, m_cpu->getRegister(categoryIndex, m_selectedRow).lo))
+	if (contextFetchNewValue(newVal, cpu().getRegister(categoryIndex, m_selectedRow).lo))
 	{
-		m_cpu->setRegister(categoryIndex, m_selectedRow, u128::From64(newVal));
+		cpu().setRegister(categoryIndex, m_selectedRow, u128::From64(newVal));
 		VMUpdate();
 	}
 }
@@ -383,11 +378,11 @@ void RegisterWidget::contextChangeValue()
 void RegisterWidget::contextChangeTop()
 {
 	u64 newVal;
-	u128 oldVal = m_cpu->getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
+	u128 oldVal = cpu().getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
 	if (contextFetchNewValue(newVal, oldVal.hi))
 	{
 		oldVal.hi = newVal;
-		m_cpu->setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
+		cpu().setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
 		VMUpdate();
 	}
 }
@@ -395,11 +390,11 @@ void RegisterWidget::contextChangeTop()
 void RegisterWidget::contextChangeBottom()
 {
 	u64 newVal;
-	u128 oldVal = m_cpu->getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
+	u128 oldVal = cpu().getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
 	if (contextFetchNewValue(newVal, oldVal.lo))
 	{
 		oldVal.lo = newVal;
-		m_cpu->setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
+		cpu().setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
 		VMUpdate();
 	}
 }
@@ -407,11 +402,11 @@ void RegisterWidget::contextChangeBottom()
 void RegisterWidget::contextChangeSegment()
 {
 	u64 newVal;
-	u128 oldVal = m_cpu->getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
+	u128 oldVal = cpu().getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
 	if (contextFetchNewValue(newVal, oldVal._u32[3 - m_selected128Field], true))
 	{
 		oldVal._u32[3 - m_selected128Field] = (u32)newVal;
-		m_cpu->setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
+		cpu().setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
 		VMUpdate();
 	}
 }
@@ -419,15 +414,15 @@ void RegisterWidget::contextChangeSegment()
 void RegisterWidget::contextGotoDisasm()
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
-	u128 regVal = m_cpu->getRegister(categoryIndex, m_selectedRow);
+	u128 regVal = cpu().getRegister(categoryIndex, m_selectedRow);
 	u32 addr = 0;
 
-	if (m_cpu->getRegisterSize(categoryIndex) == 128)
+	if (cpu().getRegisterSize(categoryIndex) == 128)
 		addr = regVal._u32[3 - m_selected128Field];
 	else
 		addr = regVal._u32[0];
 
-	if (m_cpu->isValidAddress(addr))
+	if (cpu().isValidAddress(addr))
 		gotoInDisasm(addr);
 	else
 		QMessageBox::warning(this, tr("Invalid target address"), ("This register holds an invalid address."));
@@ -436,10 +431,10 @@ void RegisterWidget::contextGotoDisasm()
 void RegisterWidget::contextGotoMemory()
 {
 	const int categoryIndex = ui.registerTabs->currentIndex();
-	u128 regVal = m_cpu->getRegister(categoryIndex, m_selectedRow);
+	u128 regVal = cpu().getRegister(categoryIndex, m_selectedRow);
 	u32 addr = 0;
 
-	if (m_cpu->getRegisterSize(categoryIndex) == 128)
+	if (cpu().getRegisterSize(categoryIndex) == 128)
 		addr = regVal._u32[3 - m_selected128Field];
 	else
 		addr = regVal._u32[0];
diff --git a/pcsx2-qt/Debugger/RegisterWidget.h b/pcsx2-qt/Debugger/RegisterWidget.h
index cfe9cb4799..f114cca55e 100644
--- a/pcsx2-qt/Debugger/RegisterWidget.h
+++ b/pcsx2-qt/Debugger/RegisterWidget.h
@@ -5,24 +5,23 @@
 
 #include "ui_RegisterWidget.h"
 
+#include "DebuggerWidget.h"
+
 #include "DebugTools/DebugInterface.h"
 #include "DebugTools/DisassemblyManager.h"
 
-#include <QtWidgets/QWidget>
 #include <QtWidgets/QMenu>
 #include <QtWidgets/QTabBar>
 #include <QtGui/QPainter>
 
-class RegisterWidget final : public QWidget
+class RegisterWidget final : public DebuggerWidget
 {
 	Q_OBJECT
 
 public:
-	RegisterWidget(QWidget* parent);
+	RegisterWidget(DebugInterface& cpu, QWidget* parent = nullptr);
 	~RegisterWidget();
 
-	void SetCpu(DebugInterface* cpu);
-
 protected:
 	void paintEvent(QPaintEvent* event);
 	void mousePressEvent(QMouseEvent* event);
@@ -58,8 +57,6 @@ private:
 	// Returns true on success
 	bool contextFetchNewValue(u64& out, u64 currentValue, bool segment = false);
 
-	DebugInterface* m_cpu;
-
 	// Used for the height offset the tab bar creates
 	// because we share a widget
 	QPoint m_renderStart;
diff --git a/pcsx2-qt/Debugger/RegisterWidget.ui b/pcsx2-qt/Debugger/RegisterWidget.ui
index 63388d9f9d..fb5f5dd971 100644
--- a/pcsx2-qt/Debugger/RegisterWidget.ui
+++ b/pcsx2-qt/Debugger/RegisterWidget.ui
@@ -11,9 +11,9 @@
    </rect>
   </property>
   <property name="sizePolicy">
-   <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
-    <horstretch>1</horstretch>
-    <verstretch>1</verstretch>
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
    </sizepolicy>
   </property>
   <property name="minimumSize">
diff --git a/pcsx2-qt/Debugger/Models/StackModel.cpp b/pcsx2-qt/Debugger/StackModel.cpp
similarity index 100%
rename from pcsx2-qt/Debugger/Models/StackModel.cpp
rename to pcsx2-qt/Debugger/StackModel.cpp
diff --git a/pcsx2-qt/Debugger/Models/StackModel.h b/pcsx2-qt/Debugger/StackModel.h
similarity index 100%
rename from pcsx2-qt/Debugger/Models/StackModel.h
rename to pcsx2-qt/Debugger/StackModel.h
diff --git a/pcsx2-qt/Debugger/StackWidget.cpp b/pcsx2-qt/Debugger/StackWidget.cpp
new file mode 100644
index 0000000000..805376ead7
--- /dev/null
+++ b/pcsx2-qt/Debugger/StackWidget.cpp
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "StackWidget.h"
+
+#include "QtUtils.h"
+
+#include <QClipboard>
+#include <QMenu>
+
+StackWidget::StackWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu, parent)
+	, m_model(cpu)
+{
+	m_ui.setupUi(this);
+
+	connect(m_ui.stackList, &QTableView::customContextMenuRequested, this, &StackWidget::onContextMenu);
+	connect(m_ui.stackList, &QTableView::doubleClicked, this, &StackWidget::onDoubleClick);
+
+	m_ui.stackList->setModel(&m_model);
+	for (std::size_t i = 0; auto mode : StackModel::HeaderResizeModes)
+	{
+		m_ui.stackList->horizontalHeader()->setSectionResizeMode(i, mode);
+		i++;
+	}
+}
+
+void StackWidget::onContextMenu(QPoint pos)
+{
+	if (!m_ui.stackList->selectionModel()->hasSelection())
+		return;
+
+	QMenu* contextMenu = new QMenu(tr("Stack List Context Menu"), m_ui.stackList);
+
+	QAction* actionCopy = new QAction(tr("Copy"), m_ui.stackList);
+	connect(actionCopy, &QAction::triggered, [this]() {
+		const auto* selModel = m_ui.stackList->selectionModel();
+
+		if (!selModel->hasSelection())
+			return;
+
+		QGuiApplication::clipboard()->setText(m_ui.stackList->model()->data(selModel->currentIndex()).toString());
+	});
+	contextMenu->addAction(actionCopy);
+
+	contextMenu->addSeparator();
+
+	QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.stackList);
+	connect(actionExport, &QAction::triggered, [this]() {
+		QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.stackList->model()));
+	});
+	contextMenu->addAction(actionExport);
+
+	contextMenu->popup(m_ui.stackList->viewport()->mapToGlobal(pos));
+}
+
+void StackWidget::onDoubleClick(const QModelIndex& index)
+{
+	switch (index.column())
+	{
+		case StackModel::StackModel::ENTRY:
+		case StackModel::StackModel::ENTRY_LABEL:
+			//m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_ui.stackList->model()->data(m_ui.stackList->model()->index(index.row(), StackModel::StackColumns::ENTRY), Qt::UserRole).toUInt());
+			not_yet_implemented();
+			break;
+		case StackModel::StackModel::SP:
+			//m_ui.memoryviewWidget->gotoAddress(m_ui.stackList->model()->data(index, Qt::UserRole).toUInt());
+			//m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory);
+			not_yet_implemented();
+			break;
+		default: // Default to PC
+			//m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_ui.stackList->model()->data(m_ui.stackList->model()->index(index.row(), StackModel::StackColumns::PC), Qt::UserRole).toUInt());
+			not_yet_implemented();
+			break;
+	}
+}
diff --git a/pcsx2-qt/Debugger/StackWidget.h b/pcsx2-qt/Debugger/StackWidget.h
new file mode 100644
index 0000000000..32b5907da2
--- /dev/null
+++ b/pcsx2-qt/Debugger/StackWidget.h
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "ui_StackWidget.h"
+
+#include "StackModel.h"
+
+#include "DebuggerWidget.h"
+
+class StackWidget final : public DebuggerWidget
+{
+	Q_OBJECT
+
+public:
+	StackWidget(DebugInterface& cpu, QWidget* parent = nullptr);
+
+	void onContextMenu(QPoint pos);
+	void onDoubleClick(const QModelIndex& index);
+
+private:
+	Ui::StackWidget m_ui;
+
+	StackModel m_model;
+};
diff --git a/pcsx2-qt/Debugger/StackWidget.ui b/pcsx2-qt/Debugger/StackWidget.ui
new file mode 100644
index 0000000000..44830715af
--- /dev/null
+++ b/pcsx2-qt/Debugger/StackWidget.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>StackWidget</class>
+ <widget class="QWidget" name="StackWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Stack</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTableView" name="stackList">
+     <property name="contextMenuPolicy">
+      <enum>Qt::CustomContextMenu</enum>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidget.ui b/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidget.ui
index 4fb07245f4..de979d7e08 100644
--- a/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidget.ui
+++ b/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidget.ui
@@ -6,12 +6,12 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>419</width>
+    <width>400</width>
     <height>300</height>
    </rect>
   </property>
   <property name="windowTitle">
-   <string>Form</string>
+   <string>Symbol Tree</string>
   </property>
   <layout class="QVBoxLayout" name="vertical_layout">
    <property name="spacing">
diff --git a/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.cpp b/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.cpp
index a8b8ade04f..e86d8af394 100644
--- a/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.cpp
+++ b/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.cpp
@@ -14,8 +14,12 @@
 
 static bool testName(const QString& name, const QString& filter);
 
-SymbolTreeWidget::SymbolTreeWidget(u32 flags, s32 symbol_address_alignment, DebugInterface& cpu, QWidget* parent)
-	: QWidget(parent)
+SymbolTreeWidget::SymbolTreeWidget(
+	u32 flags,
+	s32 symbol_address_alignment,
+	DebugInterface& cpu,
+	QWidget* parent)
+	: DebuggerWidget(&cpu, parent)
 	, m_cpu(cpu)
 	, m_flags(flags)
 	, m_symbol_address_alignment(symbol_address_alignment)
@@ -661,7 +665,11 @@ SymbolTreeNode* SymbolTreeWidget::currentNode()
 // *****************************************************************************
 
 FunctionTreeWidget::FunctionTreeWidget(DebugInterface& cpu, QWidget* parent)
-	: SymbolTreeWidget(ALLOW_GROUPING | ALLOW_MANGLED_NAME_ACTIONS, 4, cpu, parent)
+	: SymbolTreeWidget(
+		  ALLOW_GROUPING | ALLOW_MANGLED_NAME_ACTIONS,
+		  4,
+		  cpu,
+		  parent)
 {
 }
 
@@ -745,7 +753,11 @@ void FunctionTreeWidget::onNewButtonPressed()
 // *****************************************************************************
 
 GlobalVariableTreeWidget::GlobalVariableTreeWidget(DebugInterface& cpu, QWidget* parent)
-	: SymbolTreeWidget(ALLOW_GROUPING | ALLOW_SORTING_BY_IF_TYPE_IS_KNOWN | ALLOW_TYPE_ACTIONS | ALLOW_MANGLED_NAME_ACTIONS, 1, cpu, parent)
+	: SymbolTreeWidget(
+		  ALLOW_GROUPING | ALLOW_SORTING_BY_IF_TYPE_IS_KNOWN | ALLOW_TYPE_ACTIONS | ALLOW_MANGLED_NAME_ACTIONS,
+		  1,
+		  cpu,
+		  parent)
 {
 }
 
@@ -884,7 +896,11 @@ void GlobalVariableTreeWidget::onNewButtonPressed()
 // *****************************************************************************
 
 LocalVariableTreeWidget::LocalVariableTreeWidget(DebugInterface& cpu, QWidget* parent)
-	: SymbolTreeWidget(ALLOW_TYPE_ACTIONS, 1, cpu, parent)
+	: SymbolTreeWidget(
+		  ALLOW_TYPE_ACTIONS,
+		  1,
+		  cpu,
+		  parent)
 {
 }
 
@@ -1009,7 +1025,11 @@ void LocalVariableTreeWidget::onNewButtonPressed()
 // *****************************************************************************
 
 ParameterVariableTreeWidget::ParameterVariableTreeWidget(DebugInterface& cpu, QWidget* parent)
-	: SymbolTreeWidget(ALLOW_TYPE_ACTIONS, 1, cpu, parent)
+	: SymbolTreeWidget(
+		  ALLOW_TYPE_ACTIONS,
+		  1,
+		  cpu,
+		  parent)
 {
 }
 
diff --git a/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.h b/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.h
index b74138bbc9..0186b46dbf 100644
--- a/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.h
+++ b/pcsx2-qt/Debugger/SymbolTree/SymbolTreeWidgets.h
@@ -3,7 +3,7 @@
 
 #pragma once
 
-#include <QtWidgets/QWidget>
+#include "Debugger/DebuggerWidget.h"
 #include "SymbolTreeModel.h"
 
 #include "ui_SymbolTreeWidget.h"
@@ -12,7 +12,7 @@ struct SymbolFilters;
 
 // A symbol tree widget with its associated refresh button, filter box and
 // right-click menu. Supports grouping, sorting and various other settings.
-class SymbolTreeWidget : public QWidget
+class SymbolTreeWidget : public DebuggerWidget
 {
 	Q_OBJECT
 
@@ -41,7 +41,11 @@ protected:
 		const ccc::SourceFile* source_file = nullptr;
 	};
 
-	SymbolTreeWidget(u32 flags, s32 symbol_address_alignment, DebugInterface& cpu, QWidget* parent = nullptr);
+	SymbolTreeWidget(
+		u32 flags,
+		s32 symbol_address_alignment,
+		DebugInterface& cpu,
+		QWidget* parent = nullptr);
 
 	void resizeEvent(QResizeEvent* event) override;
 
diff --git a/pcsx2-qt/Debugger/Models/ThreadModel.cpp b/pcsx2-qt/Debugger/ThreadModel.cpp
similarity index 100%
rename from pcsx2-qt/Debugger/Models/ThreadModel.cpp
rename to pcsx2-qt/Debugger/ThreadModel.cpp
diff --git a/pcsx2-qt/Debugger/Models/ThreadModel.h b/pcsx2-qt/Debugger/ThreadModel.h
similarity index 100%
rename from pcsx2-qt/Debugger/Models/ThreadModel.h
rename to pcsx2-qt/Debugger/ThreadModel.h
diff --git a/pcsx2-qt/Debugger/ThreadWidget.cpp b/pcsx2-qt/Debugger/ThreadWidget.cpp
new file mode 100644
index 0000000000..73401586ad
--- /dev/null
+++ b/pcsx2-qt/Debugger/ThreadWidget.cpp
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "ThreadWidget.h"
+
+#include "QtUtils.h"
+
+#include <QtGui/QClipboard>
+#include <QtWidgets/QMenu>
+
+ThreadWidget::ThreadWidget(DebugInterface& cpu, QWidget* parent)
+	: DebuggerWidget(&cpu, parent)
+	, m_model(cpu)
+{
+	m_ui.setupUi(this);
+
+	connect(m_ui.threadList, &QTableView::customContextMenuRequested, this, &ThreadWidget::onContextMenu);
+	connect(m_ui.threadList, &QTableView::doubleClicked, this, &ThreadWidget::onDoubleClick);
+
+	m_proxy_model.setSourceModel(&m_model);
+	m_proxy_model.setSortRole(Qt::UserRole);
+	m_ui.threadList->setModel(&m_proxy_model);
+	m_ui.threadList->setSortingEnabled(true);
+	m_ui.threadList->sortByColumn(ThreadModel::ThreadColumns::ID, Qt::SortOrder::AscendingOrder);
+	for (std::size_t i = 0; auto mode : ThreadModel::HeaderResizeModes)
+	{
+		m_ui.threadList->horizontalHeader()->setSectionResizeMode(i, mode);
+		i++;
+	}
+}
+
+void ThreadWidget::onContextMenu(QPoint pos)
+{
+	if (!m_ui.threadList->selectionModel()->hasSelection())
+		return;
+
+	QMenu* contextMenu = new QMenu(tr("Thread List Context Menu"), m_ui.threadList);
+
+	QAction* actionCopy = new QAction(tr("Copy"), m_ui.threadList);
+	connect(actionCopy, &QAction::triggered, [this]() {
+		const auto* selModel = m_ui.threadList->selectionModel();
+
+		if (!selModel->hasSelection())
+			return;
+
+		QGuiApplication::clipboard()->setText(m_ui.threadList->model()->data(selModel->currentIndex()).toString());
+	});
+	contextMenu->addAction(actionCopy);
+
+	contextMenu->addSeparator();
+
+	QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.threadList);
+	connect(actionExport, &QAction::triggered, [this]() {
+		QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.threadList->model()));
+	});
+	contextMenu->addAction(actionExport);
+
+	contextMenu->popup(m_ui.threadList->viewport()->mapToGlobal(pos));
+}
+
+void ThreadWidget::onDoubleClick(const QModelIndex& index)
+{
+	switch (index.column())
+	{
+		case ThreadModel::ThreadColumns::ENTRY:
+			not_yet_implemented();
+			//m_ui.memoryviewWidget->gotoAddress(m_ui.threadList->model()->data(index, Qt::UserRole).toUInt());
+			//m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory);
+			break;
+		default: // Default to PC
+			not_yet_implemented();
+			//m_ui.disassemblyWidget->gotoAddressAndSetFocus(m_ui.threadList->model()->data(m_ui.threadList->model()->index(index.row(), ThreadModel::ThreadColumns::PC), Qt::UserRole).toUInt());
+			break;
+	}
+}
diff --git a/pcsx2-qt/Debugger/ThreadWidget.h b/pcsx2-qt/Debugger/ThreadWidget.h
new file mode 100644
index 0000000000..f0d71064d7
--- /dev/null
+++ b/pcsx2-qt/Debugger/ThreadWidget.h
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "ui_ThreadWidget.h"
+
+#include "DebuggerWidget.h"
+#include "ThreadModel.h"
+
+#include <QSortFilterProxyModel>
+
+class ThreadWidget final : public DebuggerWidget
+{
+	Q_OBJECT
+
+public:
+	ThreadWidget(DebugInterface& cpu, QWidget* parent = nullptr);
+
+	void onContextMenu(QPoint pos);
+	void onDoubleClick(const QModelIndex& index);
+
+private:
+	Ui::ThreadWidget m_ui;
+
+	ThreadModel m_model;
+	QSortFilterProxyModel m_proxy_model;
+};
diff --git a/pcsx2-qt/Debugger/ThreadWidget.ui b/pcsx2-qt/Debugger/ThreadWidget.ui
new file mode 100644
index 0000000000..4d498c2146
--- /dev/null
+++ b/pcsx2-qt/Debugger/ThreadWidget.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ThreadWidget</class>
+ <widget class="QWidget" name="ThreadWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Threads</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTableView" name="threadList">
+     <property name="contextMenuPolicy">
+      <enum>Qt::CustomContextMenu</enum>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp
index bdac5bcce8..5c12a1b923 100644
--- a/pcsx2-qt/MainWindow.cpp
+++ b/pcsx2-qt/MainWindow.cpp
@@ -2782,8 +2782,13 @@ void MainWindow::doSettings(const char* category /* = nullptr */)
 DebuggerWindow* MainWindow::getDebuggerWindow()
 {
 	if (!m_debugger_window)
+	{
+		// Setup KDDockWidgets.
+		DockManager::configure_docking_system();
+
 		// Don't pass us (this) as the parent, otherwise the window is always on top of the mainwindow (on windows at least)
 		m_debugger_window = new DebuggerWindow(nullptr);
+	}
 
 	return m_debugger_window;
 }
diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h
index 1b89dc42e0..d15b8528a7 100644
--- a/pcsx2-qt/QtHost.h
+++ b/pcsx2-qt/QtHost.h
@@ -114,7 +114,7 @@ public Q_SLOTS:
 	void endCapture();
 	void setAudioOutputVolume(int volume, int fast_forward_volume);
 	void setAudioOutputMuted(bool muted);
-	
+
 Q_SIGNALS:
 	bool messageConfirmed(const QString& title, const QString& message);
 	void statusMessage(const QString& message);
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj
index 11d29cddeb..082edce0e4 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj
+++ b/pcsx2-qt/pcsx2-qt.vcxproj
@@ -49,7 +49,7 @@
       <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\ccc\src</AdditionalIncludeDirectories>
       <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)pcsx2</AdditionalIncludeDirectories>
       <!-- Needed for moc pch -->
-      <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(ProjectDir)\Settings;$(ProjectDir)\GameList;$(ProjectDir)\Tools\InputRecording;$(ProjectDir)\Debugger;$(ProjectDir)\Debugger\Models;$(ProjectDir)\Debugger\SymbolTree</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(ProjectDir)\Settings;$(ProjectDir)\GameList;$(ProjectDir)\Tools\InputRecording;$(ProjectDir)\Debugger;$(ProjectDir)\Debugger\Breakpoints;$(ProjectDir)\Debugger\Memory;$(ProjectDir)\Debugger\SymbolTree</AdditionalIncludeDirectories>
       <PrecompiledHeader>Use</PrecompiledHeader>
       <PrecompiledHeaderFile>PrecompiledHeader.h</PrecompiledHeaderFile>
       <ForcedIncludeFiles>PrecompiledHeader.h;%(ForcedIncludeFiles)</ForcedIncludeFiles>
@@ -110,18 +110,23 @@
     <ClCompile Include="Tools\InputRecording\InputRecordingViewer.cpp" />
     <ClCompile Include="Tools\InputRecording\NewInputRecordingDlg.cpp" />
     <ClCompile Include="Debugger\AnalysisOptionsDialog.cpp" />
-    <ClCompile Include="Debugger\CpuWidget.cpp" />
+    <ClCompile Include="Debugger\DebuggerWidget.cpp" />
     <ClCompile Include="Debugger\DebuggerWindow.cpp" />
     <ClCompile Include="Debugger\DisassemblyWidget.cpp" />
-    <ClCompile Include="Debugger\MemorySearchWidget.cpp" />
-    <ClCompile Include="Debugger\MemoryViewWidget.cpp" />
+    <ClCompile Include="Debugger\DockManager.cpp" />
     <ClCompile Include="Debugger\RegisterWidget.cpp" />
-    <ClCompile Include="Debugger\BreakpointDialog.cpp" />
     <ClCompile Include="Debugger\DebuggerSettingsManager.cpp" />
-    <ClCompile Include="Debugger\Models\BreakpointModel.cpp" />
-    <ClCompile Include="Debugger\Models\ThreadModel.cpp" />
-    <ClCompile Include="Debugger\Models\StackModel.cpp" />
-    <ClCompile Include="Debugger\Models\SavedAddressesModel.cpp" />
+    <ClCompile Include="Debugger\StackModel.cpp" />
+    <ClCompile Include="Debugger\StackWidget.cpp" />
+    <ClCompile Include="Debugger\ThreadModel.cpp" />
+    <ClCompile Include="Debugger\ThreadWidget.cpp" />
+    <ClCompile Include="Debugger\Breakpoints\BreakpointDialog.cpp" />
+    <ClCompile Include="Debugger\Breakpoints\BreakpointModel.cpp" />
+    <ClCompile Include="Debugger\Breakpoints\BreakpointWidget.cpp" />
+    <ClCompile Include="Debugger\Memory\MemorySearchWidget.cpp" />
+    <ClCompile Include="Debugger\Memory\MemoryViewWidget.cpp" />
+    <ClCompile Include="Debugger\Memory\SavedAddressesModel.cpp" />
+    <ClCompile Include="Debugger\Memory\SavedAddressesWidget.cpp" />
     <ClCompile Include="Settings\BIOSSettingsWidget.cpp" />
     <ClCompile Include="Settings\ControllerBindingWidget.cpp" />
     <ClCompile Include="Settings\ControllerGlobalSettingsWidget.cpp" />
@@ -212,18 +217,23 @@
     <QtMoc Include="Tools\InputRecording\InputRecordingViewer.h" />
     <ClInclude Include="QtUtils.h" />
     <QtMoc Include="Debugger\AnalysisOptionsDialog.h" />
-    <QtMoc Include="Debugger\CpuWidget.h" />
+    <QtMoc Include="Debugger\DebuggerWidget.h" />
     <QtMoc Include="Debugger\DebuggerWindow.h" />
     <QtMoc Include="Debugger\DisassemblyWidget.h" />
-    <QtMoc Include="Debugger\MemorySearchWidget.h" />
-    <QtMoc Include="Debugger\MemoryViewWidget.h" />
+    <QtMoc Include="Debugger\DockManager.h" />
     <QtMoc Include="Debugger\RegisterWidget.h" />
-    <QtMoc Include="Debugger\BreakpointDialog.h" />
+    <QtMoc Include="Debugger\StackModel.h" />
+    <QtMoc Include="Debugger\StackWidget.h" />
+    <QtMoc Include="Debugger\ThreadModel.h" />
+    <QtMoc Include="Debugger\ThreadWidget.h" />
     <ClInclude Include="Debugger\DebuggerSettingsManager.h" />
-    <QtMoc Include="Debugger\Models\BreakpointModel.h" />
-    <QtMoc Include="Debugger\Models\ThreadModel.h" />
-    <QtMoc Include="Debugger\Models\StackModel.h" />
-    <QtMoc Include="Debugger\Models\SavedAddressesModel.h" />
+    <QtMoc Include="Debugger\Breakpoints\BreakpointDialog.h" />
+    <QtMoc Include="Debugger\Breakpoints\BreakpointModel.h" />
+    <QtMoc Include="Debugger\Breakpoints\BreakpointWidget.h" />
+    <QtMoc Include="Debugger\Memory\MemorySearchWidget.h" />
+    <QtMoc Include="Debugger\Memory\MemoryViewWidget.h" />
+    <QtMoc Include="Debugger\Memory\SavedAddressesModel.h" />
+    <QtMoc Include="Debugger\Memory\SavedAddressesWidget.h" />
     <QtMoc Include="Settings\ControllerBindingWidget.h" />
     <QtMoc Include="Settings\ControllerGlobalSettingsWidget.h" />
     <ClInclude Include="Settings\MemoryCardConvertWorker.h" />
@@ -272,17 +282,22 @@
     <ClCompile Include="$(IntDir)Settings\moc_DebugAnalysisSettingsWidget.cpp" />
     <ClCompile Include="$(IntDir)Settings\moc_DebugSettingsWidget.cpp" />
     <ClCompile Include="$(IntDir)Debugger\moc_AnalysisOptionsDialog.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\moc_DebuggerWidget.cpp" />
     <ClCompile Include="$(IntDir)Debugger\moc_DebuggerWindow.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\moc_CpuWidget.cpp" />
     <ClCompile Include="$(IntDir)Debugger\moc_DisassemblyWidget.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\moc_DockManager.cpp" />
     <ClCompile Include="$(IntDir)Debugger\moc_RegisterWidget.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\moc_MemorySearchWidget.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\moc_MemoryViewWidget.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\moc_BreakpointDialog.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_BreakpointModel.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_ThreadModel.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_StackModel.cpp" />
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_SavedAddressesModel.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\moc_StackModel.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\moc_StackWidget.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\moc_ThreadModel.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\moc_ThreadWidget.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\Breakpoints\moc_BreakpointDialog.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\Breakpoints\moc_BreakpointModel.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\Breakpoints\moc_BreakpointWidget.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_MemorySearchWidget.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_MemoryViewWidget.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_SavedAddressesModel.cpp" />
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_SavedAddressesWidget.cpp" />
     <ClCompile Include="$(IntDir)Debugger\SymbolTree\moc_NewSymbolDialogs.cpp" />
     <ClCompile Include="$(IntDir)Debugger\SymbolTree\moc_SymbolTreeDelegates.cpp" />
     <ClCompile Include="$(IntDir)Debugger\SymbolTree\moc_SymbolTreeLocation.cpp" />
@@ -409,19 +424,22 @@
     <QtUi Include="Debugger\DisassemblyWidget.ui">
       <FileType>Document</FileType>
     </QtUi>
-    <QtUi Include="Debugger\CpuWidget.ui">
-      <FileType>Document</FileType>
-    </QtUi>
     <QtUi Include="Debugger\RegisterWidget.ui">
       <FileType>Document</FileType>
     </QtUi>
-    <QtUi Include="Debugger\MemorySearchWidget.ui">
+    <QtUi Include="Debugger\Breakpoints\BreakpointDialog.ui">
       <FileType>Document</FileType>
     </QtUi>
-    <QtUi Include="Debugger\MemoryViewWidget.ui">
+    <QtUi Include="Debugger\Breakpoints\BreakpointWidget.ui">
       <FileType>Document</FileType>
     </QtUi>
-    <QtUi Include="Debugger\BreakpointDialog.ui">
+    <QtUi Include="Debugger\Memory\MemorySearchWidget.ui">
+      <FileType>Document</FileType>
+    </QtUi>
+    <QtUi Include="Debugger\Memory\MemoryViewWidget.ui">
+      <FileType>Document</FileType>
+    </QtUi>
+    <QtUi Include="Debugger\Memory\SavedAddressesWidget.ui">
       <FileType>Document</FileType>
     </QtUi>
   </ItemGroup>
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters
index b583ecaaf3..5963e150f7 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj.filters
+++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters
@@ -272,43 +272,58 @@
     <ClCompile Include="Debugger\AnalysisOptionsDialog.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
+    <ClCompile Include="Debugger\DebuggerWidget.cpp">
+      <Filter>Debugger</Filter>
+    </ClCompile>
     <ClCompile Include="Debugger\DebuggerWindow.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
     <ClCompile Include="Debugger\DisassemblyWidget.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\CpuWidget.cpp">
+    <ClCompile Include="Debugger\DockManager.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
     <ClCompile Include="Debugger\RegisterWidget.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\MemorySearchWidget.cpp">
+    <ClCompile Include="Debugger\StackModel.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\MemoryViewWidget.cpp">
+    <ClCompile Include="Debugger\StackWidget.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\BreakpointDialog.cpp">
+    <ClCompile Include="Debugger\ThreadModel.cpp">
       <Filter>Debugger</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\Models\BreakpointModel.cpp">
-      <Filter>Debugger\Models</Filter>
+    <ClCompile Include="Debugger\ThreadWidget.cpp">
+      <Filter>Debugger</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\Models\ThreadModel.cpp">
-      <Filter>Debugger\Models</Filter>
+    <ClCompile Include="Debugger\Breakpoints\BreakpointDialog.cpp">
+      <Filter>Debugger\Breakpoints</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\Models\StackModel.cpp">
-      <Filter>Debugger\Models</Filter>
+    <ClCompile Include="Debugger\Breakpoints\BreakpointModel.cpp">
+      <Filter>Debugger\Breakpoints</Filter>
     </ClCompile>
-    <ClCompile Include="Debugger\Models\SavedAddressesModel.cpp">
-      <Filter>Debugger\Models</Filter>
+    <ClCompile Include="Debugger\Breakpoints\BreakpointWidget.cpp">
+      <Filter>Debugger\Breakpoints</Filter>
+    </ClCompile>
+    <ClCompile Include="Debugger\Memory\MemorySearchWidget.cpp">
+      <Filter>Debugger\Memory</Filter>
+    </ClCompile>
+    <ClCompile Include="Debugger\Memory\MemoryViewWidget.cpp">
+      <Filter>Debugger\Memory</Filter>
+    </ClCompile>
+    <ClCompile Include="Debugger\Memory\SavedAddressesModel.cpp">
+      <Filter>Debugger\Memory</Filter>
+    </ClCompile>
+    <ClCompile Include="Debugger\Memory\SavedAddressesWidget.cpp">
+      <Filter>Debugger\Memory</Filter>
     </ClCompile>
     <ClCompile Include="$(IntDir)Debugger\moc_AnalysisOptionsDialog.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\moc_CpuWidget.cpp">
+    <ClCompile Include="$(IntDir)Debugger\moc_DebuggerWidget.cpp">
       <Filter>moc</Filter>
     </ClCompile>
     <ClCompile Include="$(IntDir)Debugger\moc_DebuggerWindow.cpp">
@@ -317,28 +332,43 @@
     <ClCompile Include="$(IntDir)Debugger\moc_DisassemblyWidget.cpp">
       <Filter>moc</Filter>
     </ClCompile>
+    <ClCompile Include="$(IntDir)Debugger\moc_DockManager.cpp">
+      <Filter>moc</Filter>
+    </ClCompile>
     <ClCompile Include="$(IntDir)Debugger\moc_RegisterWidget.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\moc_MemorySearchWidget.cpp">
+    <ClCompile Include="$(IntDir)Debugger\moc_StackModel.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\moc_MemoryViewWidget.cpp">
+    <ClCompile Include="$(IntDir)Debugger\moc_StackWidget.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\moc_BreakpointDialog.cpp">
+    <ClCompile Include="$(IntDir)Debugger\moc_ThreadModel.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_BreakpointModel.cpp">
+    <ClCompile Include="$(IntDir)Debugger\moc_ThreadWidget.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_ThreadModel.cpp">
+    <ClCompile Include="$(IntDir)Debugger\Breakpoints\moc_BreakpointDialog.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_StackModel.cpp">
+    <ClCompile Include="$(IntDir)Debugger\Breakpoints\moc_BreakpointModel.cpp">
       <Filter>moc</Filter>
     </ClCompile>
-    <ClCompile Include="$(IntDir)Debugger\Models\moc_SavedAddressesModel.cpp">
+    <ClCompile Include="$(IntDir)Debugger\Breakpoints\moc_BreakpointWidget.cpp">
+      <Filter>moc</Filter>
+    </ClCompile>
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_MemorySearchWidget.cpp">
+      <Filter>moc</Filter>
+    </ClCompile>
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_MemoryViewWidget.cpp">
+      <Filter>moc</Filter>
+    </ClCompile>
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_SavedAddressesModel.cpp">
+      <Filter>moc</Filter>
+    </ClCompile>
+    <ClCompile Include="$(IntDir)Debugger\Memory\moc_SavedAddressesWidget.cpp">
       <Filter>moc</Filter>
     </ClCompile>
     <ClCompile Include="ColorPickerButton.cpp" />
@@ -541,38 +571,53 @@
     <QtMoc Include="Debugger\AnalysisOptionsDialog.h">
       <Filter>Debugger</Filter>
     </QtMoc>
+    <QtMoc Include="Debugger\DebuggerWidget.h">
+      <Filter>Debugger</Filter>
+    </QtMoc>
     <QtMoc Include="Debugger\DebuggerWindow.h">
       <Filter>Debugger</Filter>
     </QtMoc>
     <QtMoc Include="Debugger\DisassemblyWidget.h">
       <Filter>Debugger</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\CpuWidget.h">
+    <QtMoc Include="Debugger\DockManager.h">
       <Filter>Debugger</Filter>
     </QtMoc>
     <QtMoc Include="Debugger\RegisterWidget.h">
       <Filter>Debugger</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\MemorySearchWidget.h">
+    <QtMoc Include="Debugger\StackModel.h">
       <Filter>Debugger</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\MemoryViewWidget.h">
+    <QtMoc Include="Debugger\StackWidget.h">
       <Filter>Debugger</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\BreakpointDialog.h">
+    <QtMoc Include="Debugger\ThreadModel.h">
       <Filter>Debugger</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\Models\BreakpointModel.h">
-      <Filter>Debugger\Models</Filter>
+    <QtMoc Include="Debugger\ThreadWidget.h">
+      <Filter>Debugger</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\Models\ThreadModel.h">
-      <Filter>Debugger\Models</Filter>
+    <QtMoc Include="Debugger\Breakpoints\BreakpointDialog.h">
+      <Filter>Debugger\Breakpoints</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\Models\StackModel.h">
-      <Filter>Debugger\Models</Filter>
+    <QtMoc Include="Debugger\Breakpoints\BreakpointModel.h">
+      <Filter>Debugger\Breakpoints</Filter>
     </QtMoc>
-    <QtMoc Include="Debugger\Models\SavedAddressesModel.h">
-      <Filter>Debugger\Models</Filter>
+    <QtMoc Include="Debugger\Breakpoints\BreakpointWidget.h">
+      <Filter>Debugger\Breakpoints</Filter>
+    </QtMoc>
+    <QtMoc Include="Debugger\Memory\MemorySearchWidget.h">
+      <Filter>Debugger\Memory</Filter>
+    </QtMoc>
+    <QtMoc Include="Debugger\Memory\MemoryViewWidget.h">
+      <Filter>Debugger\Memory</Filter>
+    </QtMoc>
+    <QtMoc Include="Debugger\Memory\SavedAddressesModel.h">
+      <Filter>Debugger\Memory</Filter>
+    </QtMoc>
+    <QtMoc Include="Debugger\Memory\SavedAddressesWidget.h">
+      <Filter>Debugger\Memory</Filter>
     </QtMoc>
     <QtMoc Include="ColorPickerButton.h" />
     <QtMoc Include="SetupWizardDialog.h" />
@@ -698,20 +743,23 @@
     <QtUi Include="Debugger\DisassemblyWidget.ui">
       <Filter>Debugger</Filter>
     </QtUi>
-    <QtUi Include="Debugger\CpuWidget.ui">
-      <Filter>Debugger</Filter>
-    </QtUi>
     <QtUi Include="Debugger\RegisterWidget.ui">
       <Filter>Debugger</Filter>
     </QtUi>
-    <QtUi Include="Debugger\MemorySearchWidget.ui">
-      <Filter>Debugger</Filter>
+    <QtUi Include="Debugger\Breakpoints\BreakpointDialog.ui">
+      <Filter>Debugger\Breakpoints</Filter>
     </QtUi>
-    <QtUi Include="Debugger\MemoryViewWidget.ui">
-      <Filter>Debugger</Filter>
+    <QtUi Include="Debugger\Breakpoints\BreakpointWidget.ui">
+      <Filter>Debugger\Breakpoints</Filter>
     </QtUi>
-    <QtUi Include="Debugger\BreakpointDialog.ui">
-      <Filter>Debugger</Filter>
+    <QtUi Include="Debugger\Memory\MemorySearchWidget.ui">
+      <Filter>Debugger\Memory</Filter>
+    </QtUi>
+    <QtUi Include="Debugger\Memory\MemoryViewWidget.ui">
+      <Filter>Debugger\Memory</Filter>
+    </QtUi>
+    <QtUi Include="Debugger\Memory\SavedAddressesWidget.ui">
+      <Filter>Debugger\Memory</Filter>
     </QtUi>
     <QtUi Include="Settings\ControllerLEDSettingsDialog.ui">
       <Filter>Settings</Filter>