diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt
index de68a145d7..401f41cfa1 100644
--- a/Source/Core/Common/CMakeLists.txt
+++ b/Source/Core/Common/CMakeLists.txt
@@ -35,6 +35,9 @@ add_library(common
Crypto/ec.h
Debug/MemoryPatches.cpp
Debug/MemoryPatches.h
+ Debug/OSThread.cpp
+ Debug/OSThread.h
+ Debug/Threads.h
Debug/Watches.cpp
Debug/Watches.h
DebugInterface.h
diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj
index 2f05fd2999..0d8212ac39 100644
--- a/Source/Core/Common/Common.vcxproj
+++ b/Source/Core/Common/Common.vcxproj
@@ -76,6 +76,8 @@
+
+
@@ -217,6 +219,7 @@
+
@@ -310,4 +313,4 @@
-
+
\ No newline at end of file
diff --git a/Source/Core/Common/Common.vcxproj.filters b/Source/Core/Common/Common.vcxproj.filters
index b7a205b4a4..57f4224417 100644
--- a/Source/Core/Common/Common.vcxproj.filters
+++ b/Source/Core/Common/Common.vcxproj.filters
@@ -274,6 +274,12 @@
Debug
+
+ Debug
+
+
+ Debug
+
GL\GLInterface
@@ -359,6 +365,9 @@
Debug
+
+ Debug
+
GL\GLInterface
diff --git a/Source/Core/Common/Debug/OSThread.cpp b/Source/Core/Common/Debug/OSThread.cpp
new file mode 100644
index 0000000000..809e5adccd
--- /dev/null
+++ b/Source/Core/Common/Debug/OSThread.cpp
@@ -0,0 +1,204 @@
+// Copyright 2020 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "Common/Debug/OSThread.h"
+
+#include
+#include
+
+#include "Core/PowerPC/MMU.h"
+
+// Context offsets based on the following functions:
+// - OSSaveContext
+// - OSSaveFPUContext
+// - OSDumpContext
+// - OSClearContext
+// - OSExceptionVector
+void Common::Debug::OSContext::Read(u32 addr)
+{
+ for (std::size_t i = 0; i < gpr.size(); i++)
+ gpr[i] = PowerPC::HostRead_U32(addr + u32(i * sizeof(int)));
+ cr = PowerPC::HostRead_U32(addr + 0x80);
+ lr = PowerPC::HostRead_U32(addr + 0x84);
+ ctr = PowerPC::HostRead_U32(addr + 0x88);
+ xer = PowerPC::HostRead_U32(addr + 0x8C);
+ for (std::size_t i = 0; i < fpr.size(); i++)
+ fpr[i] = PowerPC::HostRead_F64(addr + 0x90 + u32(i * sizeof(double)));
+ fpscr = PowerPC::HostRead_U64(addr + 0x190);
+ srr0 = PowerPC::HostRead_U32(addr + 0x198);
+ srr1 = PowerPC::HostRead_U32(addr + 0x19c);
+ dummy = PowerPC::HostRead_U16(addr + 0x1a0);
+ state = static_cast(PowerPC::HostRead_U16(addr + 0x1a2));
+ for (std::size_t i = 0; i < gqr.size(); i++)
+ gqr[i] = PowerPC::HostRead_U32(addr + 0x1a4 + u32(i * sizeof(int)));
+ psf_padding = 0;
+ for (std::size_t i = 0; i < psf.size(); i++)
+ psf[i] = PowerPC::HostRead_F64(addr + 0x1c8 + u32(i * sizeof(double)));
+}
+
+// Mutex offsets based on the following functions:
+// - OSInitMutex
+// - OSLockMutex
+// - __OSUnlockAllMutex
+void Common::Debug::OSMutex::Read(u32 addr)
+{
+ thread_queue.head = PowerPC::HostRead_U32(addr);
+ thread_queue.tail = PowerPC::HostRead_U32(addr + 0x4);
+ owner_addr = PowerPC::HostRead_U32(addr + 0x8);
+ lock_count = PowerPC::HostRead_U32(addr + 0xc);
+ link.next = PowerPC::HostRead_U32(addr + 0x10);
+ link.prev = PowerPC::HostRead_U32(addr + 0x14);
+}
+
+// Thread offsets based on the following functions:
+// - OSCreateThread
+// - OSIsThreadTerminated
+// - OSJoinThread
+// - OSSuspendThread
+// - OSSetPriority
+// - OSExitThread
+// - OSLockMutex
+// - __OSUnlockAllMutex
+// - __OSThreadInit
+// - OSSetThreadSpecific
+// - SOInit (for errno)
+void Common::Debug::OSThread::Read(u32 addr)
+{
+ context.Read(addr);
+ state = PowerPC::HostRead_U16(addr + 0x2c8);
+ is_detached = PowerPC::HostRead_U16(addr + 0x2ca);
+ suspend = PowerPC::HostRead_U32(addr + 0x2cc);
+ effective_priority = PowerPC::HostRead_U32(addr + 0x2d0);
+ base_priority = PowerPC::HostRead_U32(addr + 0x2d4);
+ exit_code_addr = PowerPC::HostRead_U32(addr + 0x2d8);
+
+ queue_addr = PowerPC::HostRead_U32(addr + 0x2dc);
+ queue_link.next = PowerPC::HostRead_U32(addr + 0x2e0);
+ queue_link.prev = PowerPC::HostRead_U32(addr + 0x2e4);
+
+ join_queue.head = PowerPC::HostRead_U32(addr + 0x2e8);
+ join_queue.tail = PowerPC::HostRead_U32(addr + 0x2ec);
+
+ mutex_addr = PowerPC::HostRead_U32(addr + 0x2f0);
+ mutex_queue.head = PowerPC::HostRead_U32(addr + 0x2f4);
+ mutex_queue.tail = PowerPC::HostRead_U32(addr + 0x2f8);
+
+ thread_link.next = PowerPC::HostRead_U32(addr + 0x2fc);
+ thread_link.prev = PowerPC::HostRead_U32(addr + 0x300);
+
+ stack_addr = PowerPC::HostRead_U32(addr + 0x304);
+ stack_end = PowerPC::HostRead_U32(addr + 0x308);
+ error = PowerPC::HostRead_U32(addr + 0x30c);
+ specific[0] = PowerPC::HostRead_U32(addr + 0x310);
+ specific[1] = PowerPC::HostRead_U32(addr + 0x314);
+}
+
+bool Common::Debug::OSThread::IsValid() const
+{
+ return PowerPC::HostIsRAMAddress(stack_end) && PowerPC::HostRead_U32(stack_end) == STACK_MAGIC;
+}
+
+Common::Debug::OSThreadView::OSThreadView(u32 addr)
+{
+ m_address = addr;
+ m_thread.Read(addr);
+}
+
+const Common::Debug::OSThread& Common::Debug::OSThreadView::Data() const
+{
+ return m_thread;
+}
+
+Common::Debug::PartialContext Common::Debug::OSThreadView::GetContext() const
+{
+ PartialContext context;
+
+ if (!IsValid())
+ return context;
+
+ context.gpr = m_thread.context.gpr;
+ context.cr = m_thread.context.cr;
+ context.lr = m_thread.context.lr;
+ context.ctr = m_thread.context.ctr;
+ context.xer = m_thread.context.xer;
+ context.fpr = m_thread.context.fpr;
+ context.fpscr = m_thread.context.fpscr;
+ context.srr0 = m_thread.context.srr0;
+ context.srr1 = m_thread.context.srr1;
+ context.dummy = m_thread.context.dummy;
+ context.state = static_cast(m_thread.context.state);
+ context.gqr = m_thread.context.gqr;
+ context.psf = m_thread.context.psf;
+
+ return context;
+}
+
+u32 Common::Debug::OSThreadView::GetAddress() const
+{
+ return m_address;
+}
+
+u16 Common::Debug::OSThreadView::GetState() const
+{
+ return m_thread.state;
+}
+
+bool Common::Debug::OSThreadView::IsSuspended() const
+{
+ return m_thread.suspend > 0;
+}
+
+bool Common::Debug::OSThreadView::IsDetached() const
+{
+ return m_thread.is_detached != 0;
+}
+
+s32 Common::Debug::OSThreadView::GetBasePriority() const
+{
+ return m_thread.base_priority;
+}
+
+s32 Common::Debug::OSThreadView::GetEffectivePriority() const
+{
+ return m_thread.effective_priority;
+}
+
+u32 Common::Debug::OSThreadView::GetStackStart() const
+{
+ return m_thread.stack_addr;
+}
+
+u32 Common::Debug::OSThreadView::GetStackEnd() const
+{
+ return m_thread.stack_end;
+}
+
+std::size_t Common::Debug::OSThreadView::GetStackSize() const
+{
+ return GetStackStart() - GetStackEnd();
+}
+
+s32 Common::Debug::OSThreadView::GetErrno() const
+{
+ return m_thread.error;
+}
+
+std::string Common::Debug::OSThreadView::GetSpecific() const
+{
+ std::string specific;
+
+ for (u32 addr : m_thread.specific)
+ {
+ if (!PowerPC::HostIsRAMAddress(addr))
+ break;
+ specific += fmt::format("{:08x} \"{}\"\n", addr, PowerPC::HostGetString(addr));
+ }
+
+ return specific;
+}
+
+bool Common::Debug::OSThreadView::IsValid() const
+{
+ return m_thread.IsValid();
+}
diff --git a/Source/Core/Common/Debug/OSThread.h b/Source/Core/Common/Debug/OSThread.h
new file mode 100644
index 0000000000..7a588c670e
--- /dev/null
+++ b/Source/Core/Common/Debug/OSThread.h
@@ -0,0 +1,154 @@
+// Copyright 2020 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "Common/CommonTypes.h"
+#include "Common/Debug/Threads.h"
+
+namespace Common::Debug
+{
+template
+struct OSQueue
+{
+ u32 head;
+ u32 tail;
+};
+template
+struct OSLink
+{
+ u32 next;
+ u32 prev;
+};
+
+struct OSMutex;
+struct OSThread;
+
+using OSThreadQueue = OSQueue;
+using OSThreadLink = OSLink;
+
+using OSMutexQueue = OSQueue;
+using OSMutexLink = OSLink;
+
+struct OSContext
+{
+ enum class State : u16
+ {
+ HasFPU = 1,
+ HasException = 2,
+ };
+ std::array gpr;
+ u32 cr;
+ u32 lr;
+ u32 ctr;
+ u32 xer;
+ std::array fpr;
+ u64 fpscr;
+ u32 srr0;
+ u32 srr1;
+ u16 dummy;
+ State state;
+ std::array gqr;
+ u32 psf_padding;
+ std::array psf;
+
+ void Read(u32 addr);
+};
+
+static_assert(std::is_trivially_copyable_v);
+static_assert(std::is_standard_layout_v);
+static_assert(offsetof(OSContext, cr) == 0x80);
+static_assert(offsetof(OSContext, fpscr) == 0x190);
+static_assert(offsetof(OSContext, gqr) == 0x1a4);
+static_assert(offsetof(OSContext, psf) == 0x1c8);
+
+struct OSThread
+{
+ OSContext context;
+
+ u16 state; // Thread state (ready, running, waiting, moribund)
+ u16 is_detached; // Is thread detached
+ s32 suspend; // Suspended if greater than zero
+ s32 effective_priority; // Effective priority
+ s32 base_priority; // Base priority
+ u32 exit_code_addr; // Exit value address
+
+ u32 queue_addr; // Address of the queue the thread is on
+ OSThreadLink queue_link; // Used to traverse the thread queue
+ // OSSleepThread uses it to insert the current thread at the end of the thread queue
+
+ OSThreadQueue join_queue; // Threads waiting to be joined
+
+ u32 mutex_addr; // Mutex waiting
+ OSMutexQueue mutex_queue; // Mutex owned
+
+ OSThreadLink thread_link; // Link containing all active threads
+
+ // The STACK_MAGIC is written at stack_end
+ u32 stack_addr;
+ u32 stack_end;
+
+ s32 error; // errno value
+ std::array specific; // Pointers to data (can be used to store thread names)
+
+ static constexpr u32 STACK_MAGIC = 0xDEADBABE;
+ void Read(u32 addr);
+ bool IsValid() const;
+};
+
+static_assert(std::is_trivially_copyable_v);
+static_assert(std::is_standard_layout_v);
+static_assert(offsetof(OSThread, state) == 0x2c8);
+static_assert(offsetof(OSThread, mutex_addr) == 0x2f0);
+static_assert(offsetof(OSThread, stack_addr) == 0x304);
+static_assert(offsetof(OSThread, specific) == 0x310);
+
+struct OSMutex
+{
+ OSThreadQueue thread_queue; // Threads waiting to own the mutex
+ u32 owner_addr; // Thread owning the mutex
+ s32 lock_count; // Mutex lock count
+ OSMutexLink link; // Used to traverse the thread's mutex queue
+ // OSLockMutex uses it to insert the acquired mutex at the end of the queue
+
+ void Read(u32 addr);
+};
+
+static_assert(std::is_trivially_copyable_v);
+static_assert(std::is_standard_layout_v);
+static_assert(offsetof(OSMutex, owner_addr) == 0x8);
+static_assert(offsetof(OSMutex, link) == 0x10);
+
+class OSThreadView : public Common::Debug::ThreadView
+{
+public:
+ explicit OSThreadView(u32 addr);
+ ~OSThreadView() = default;
+
+ const OSThread& Data() const;
+
+ PartialContext GetContext() const override;
+ u32 GetAddress() const override;
+ u16 GetState() const override;
+ bool IsSuspended() const override;
+ bool IsDetached() const override;
+ s32 GetBasePriority() const override;
+ s32 GetEffectivePriority() const override;
+ u32 GetStackStart() const override;
+ u32 GetStackEnd() const override;
+ std::size_t GetStackSize() const override;
+ s32 GetErrno() const override;
+ std::string GetSpecific() const override;
+ bool IsValid() const override;
+
+private:
+ u32 m_address = 0;
+ OSThread m_thread;
+};
+
+} // namespace Common::Debug
diff --git a/Source/Core/Common/Debug/Threads.h b/Source/Core/Common/Debug/Threads.h
new file mode 100644
index 0000000000..a4f84ab058
--- /dev/null
+++ b/Source/Core/Common/Debug/Threads.h
@@ -0,0 +1,63 @@
+// Copyright 2020 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "Common/CommonTypes.h"
+
+namespace Common::Debug
+{
+struct PartialContext
+{
+ std::optional> gpr;
+ std::optional cr;
+ std::optional lr;
+ std::optional ctr;
+ std::optional xer;
+ std::optional> fpr;
+ std::optional fpscr;
+ std::optional srr0;
+ std::optional srr1;
+ std::optional dummy;
+ std::optional state;
+ std::optional> gqr;
+ std::optional> psf;
+};
+
+class ThreadView
+{
+public:
+ virtual ~ThreadView() = default;
+
+ enum class API
+ {
+ OSThread, // Nintendo SDK thread
+ LWPThread, // devkitPro libogc thread
+ };
+
+ virtual PartialContext GetContext() const = 0;
+ virtual u32 GetAddress() const = 0;
+ virtual u16 GetState() const = 0;
+ virtual bool IsSuspended() const = 0;
+ virtual bool IsDetached() const = 0;
+ virtual s32 GetBasePriority() const = 0;
+ virtual s32 GetEffectivePriority() const = 0;
+ virtual u32 GetStackStart() const = 0;
+ virtual u32 GetStackEnd() const = 0;
+ virtual std::size_t GetStackSize() const = 0;
+ virtual s32 GetErrno() const = 0;
+ // Implementation specific, used to store arbitrary data
+ virtual std::string GetSpecific() const = 0;
+ virtual bool IsValid() const = 0;
+};
+
+using Threads = std::vector>;
+
+} // namespace Common::Debug
diff --git a/Source/Core/Common/DebugInterface.h b/Source/Core/Common/DebugInterface.h
index fd9d93f8f9..bdbe927e39 100644
--- a/Source/Core/Common/DebugInterface.h
+++ b/Source/Core/Common/DebugInterface.h
@@ -9,6 +9,7 @@
#include
#include "Common/CommonTypes.h"
+#include "Common/Debug/Threads.h"
namespace Common::Debug
{
@@ -51,6 +52,9 @@ public:
virtual void RemovePatch(std::size_t index) = 0;
virtual void ClearPatches() = 0;
+ // Threads
+ virtual Debug::Threads GetThreads() const = 0;
+
virtual std::string Disassemble(u32 /*address*/) const { return "NODEBUGGER"; }
virtual std::string GetRawMemoryString(int /*memory*/, u32 /*address*/) const
{
diff --git a/Source/Core/Core/Core.vcxproj b/Source/Core/Core/Core.vcxproj
index b884249266..deeab45e20 100644
--- a/Source/Core/Core/Core.vcxproj
+++ b/Source/Core/Core/Core.vcxproj
@@ -767,4 +767,4 @@
-
+
\ No newline at end of file
diff --git a/Source/Core/Core/Debugger/PPCDebugInterface.cpp b/Source/Core/Core/Debugger/PPCDebugInterface.cpp
index 6dc34917a8..e5b296e612 100644
--- a/Source/Core/Core/Debugger/PPCDebugInterface.cpp
+++ b/Source/Core/Core/Debugger/PPCDebugInterface.cpp
@@ -11,6 +11,7 @@
#include
#include "Common/Align.h"
+#include "Common/Debug/OSThread.h"
#include "Common/GekkoDisassembler.h"
#include "Core/Core.h"
@@ -164,6 +165,42 @@ void PPCDebugInterface::ClearPatches()
m_patches.ClearPatches();
}
+Common::Debug::Threads PPCDebugInterface::GetThreads() const
+{
+ Common::Debug::Threads threads;
+
+ constexpr u32 ACTIVE_QUEUE_HEAD_ADDR = 0x800000dc;
+ if (!PowerPC::HostIsRAMAddress(ACTIVE_QUEUE_HEAD_ADDR))
+ return threads;
+ u32 addr = PowerPC::HostRead_U32(ACTIVE_QUEUE_HEAD_ADDR);
+ if (!PowerPC::HostIsRAMAddress(addr))
+ return threads;
+
+ auto active_thread = std::make_unique(addr);
+ if (!active_thread->IsValid())
+ return threads;
+ addr = active_thread->Data().thread_link.prev;
+
+ const auto insert_threads = [&threads](u32 addr, auto get_next_addr) {
+ while (addr != 0 && PowerPC::HostIsRAMAddress(addr))
+ {
+ auto thread = std::make_unique(addr);
+ if (!thread->IsValid())
+ break;
+ addr = get_next_addr(*thread);
+ threads.emplace_back(std::move(thread));
+ }
+ };
+
+ insert_threads(addr, [](const auto& thread) { return thread.Data().thread_link.prev; });
+ std::reverse(threads.begin(), threads.end());
+ addr = active_thread->Data().thread_link.next;
+ threads.emplace_back(std::move(active_thread));
+ insert_threads(addr, [](const auto& thread) { return thread.Data().thread_link.next; });
+
+ return threads;
+}
+
std::string PPCDebugInterface::Disassemble(u32 address) const
{
// PowerPC::HostRead_U32 seemed to crash on shutdown
diff --git a/Source/Core/Core/Debugger/PPCDebugInterface.h b/Source/Core/Core/Debugger/PPCDebugInterface.h
index 54b4f7e867..bf17934f0e 100644
--- a/Source/Core/Core/Debugger/PPCDebugInterface.h
+++ b/Source/Core/Core/Debugger/PPCDebugInterface.h
@@ -52,6 +52,9 @@ public:
void RemovePatch(std::size_t index) override;
void ClearPatches() override;
+ // Threads
+ Common::Debug::Threads GetThreads() const override;
+
std::string Disassemble(u32 address) const override;
std::string GetRawMemoryString(int memory, u32 address) const override;
bool IsAlive() const override;
diff --git a/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.cpp b/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.cpp
index ae2c567dc6..a96cf27ef3 100644
--- a/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.cpp
+++ b/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.cpp
@@ -140,6 +140,11 @@ void DSPDebugInterface::ClearPatches()
m_patches.ClearPatches();
}
+Common::Debug::Threads DSPDebugInterface::GetThreads() const
+{
+ return {};
+}
+
std::string DSPDebugInterface::Disassemble(u32 address) const
{
// we'll treat addresses as line numbers.
diff --git a/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.h b/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.h
index 078b3299c6..1fa80012e4 100644
--- a/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.h
+++ b/Source/Core/Core/HW/DSPLLE/DSPDebugInterface.h
@@ -53,6 +53,9 @@ public:
bool HasEnabledPatch(u32 address) const override;
void ClearPatches() override;
+ // Threads
+ Common::Debug::Threads GetThreads() const override;
+
std::string Disassemble(u32 address) const override;
std::string GetRawMemoryString(int memory, u32 address) const override;
bool IsAlive() const override;
diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index 1121424812..3ef723300d 100644
--- a/Source/Core/DolphinQt/CMakeLists.txt
+++ b/Source/Core/DolphinQt/CMakeLists.txt
@@ -188,6 +188,8 @@ add_executable(dolphin-emu
Debugger/RegisterColumn.h
Debugger/RegisterWidget.cpp
Debugger/RegisterWidget.h
+ Debugger/ThreadWidget.cpp
+ Debugger/ThreadWidget.h
Debugger/WatchWidget.cpp
Debugger/WatchWidget.h
GameList/GameList.cpp
diff --git a/Source/Core/DolphinQt/Debugger/ThreadWidget.cpp b/Source/Core/DolphinQt/Debugger/ThreadWidget.cpp
new file mode 100644
index 0000000000..3d943b2f0c
--- /dev/null
+++ b/Source/Core/DolphinQt/Debugger/ThreadWidget.cpp
@@ -0,0 +1,475 @@
+// Copyright 2020 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "DolphinQt/Debugger/ThreadWidget.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "Common/BitUtils.h"
+#include "Core/Core.h"
+#include "Core/PowerPC/MMU.h"
+#include "Core/PowerPC/PowerPC.h"
+#include "DolphinQt/Host.h"
+#include "DolphinQt/Settings.h"
+
+ThreadWidget::ThreadWidget(QWidget* parent) : QDockWidget(parent)
+{
+ setWindowTitle(tr("Threads"));
+ setObjectName(QStringLiteral("threads"));
+
+ setHidden(!Settings::Instance().IsThreadsVisible() || !Settings::Instance().IsDebugModeEnabled());
+
+ setAllowedAreas(Qt::AllDockWidgetAreas);
+
+ CreateWidgets();
+
+ auto& settings = Settings::GetQSettings();
+
+ restoreGeometry(settings.value(QStringLiteral("threadwidget/geometry")).toByteArray());
+ // macOS: setHidden() needs to be evaluated before setFloating() for proper window presentation
+ // according to Settings
+ setFloating(settings.value(QStringLiteral("threadwidget/floating")).toBool());
+
+ ConnectWidgets();
+
+ connect(Host::GetInstance(), &Host::UpdateDisasmDialog, this, &ThreadWidget::Update);
+
+ connect(&Settings::Instance(), &Settings::ThreadsVisibilityChanged,
+ [this](bool visible) { setHidden(!visible); });
+
+ connect(&Settings::Instance(), &Settings::DebugModeToggled, [this](bool enabled) {
+ setHidden(!enabled || !Settings::Instance().IsThreadsVisible());
+ });
+}
+
+ThreadWidget::~ThreadWidget()
+{
+ auto& settings = Settings::GetQSettings();
+
+ settings.setValue(QStringLiteral("threadwidget/geometry"), saveGeometry());
+ settings.setValue(QStringLiteral("threadwidget/floating"), isFloating());
+}
+
+void ThreadWidget::closeEvent(QCloseEvent*)
+{
+ Settings::Instance().SetThreadsVisible(false);
+}
+
+void ThreadWidget::showEvent(QShowEvent* event)
+{
+ Update();
+}
+
+void ThreadWidget::CreateWidgets()
+{
+ m_state = new QGroupBox(tr("State"));
+
+ auto* state_layout = new QHBoxLayout;
+ m_state->setLayout(state_layout);
+ state_layout->addWidget(CreateContextGroup());
+ state_layout->addWidget(CreateActiveThreadQueueGroup());
+ state_layout->setContentsMargins(2, 2, 2, 2);
+ state_layout->setSpacing(1);
+
+ auto* widget = new QWidget;
+ auto* layout = new QVBoxLayout;
+ widget->setLayout(layout);
+ layout->addWidget(m_state);
+ layout->addWidget(CreateThreadGroup());
+ layout->addWidget(CreateThreadContextGroup());
+ layout->addWidget(CreateThreadCallstackGroup());
+ layout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding));
+ setWidget(widget);
+
+ Update();
+ UpdateThreadContext({});
+}
+
+void ThreadWidget::ConnectWidgets()
+{
+ connect(m_thread_table->selectionModel(), &QItemSelectionModel::selectionChanged,
+ [this](const QItemSelection& selected, const QItemSelection& deselected) {
+ const auto indexes = selected.indexes();
+ const int row = indexes.empty() ? -1 : indexes.first().row();
+ OnSelectionChanged(row);
+ });
+ connect(m_context_table, &QTableWidget::customContextMenuRequested,
+ [this] { ShowContextMenu(m_context_table); });
+ connect(m_callstack_table, &QTableWidget::customContextMenuRequested,
+ [this] { ShowContextMenu(m_callstack_table); });
+}
+
+void ThreadWidget::ShowContextMenu(QTableWidget* table)
+{
+ const auto* item = static_cast(table->currentItem());
+ if (item == nullptr)
+ return;
+
+ bool ok;
+ const u32 addr = item->text().toUInt(&ok, 16);
+ if (!ok)
+ return;
+
+ QMenu* menu = new QMenu(this);
+ const QString watch_name = QStringLiteral("thread_context_%1").arg(addr, 8, 16, QLatin1Char('0'));
+ menu->addAction(tr("Add &breakpoint"), this, [this, addr] { emit RequestBreakpoint(addr); });
+ menu->addAction(tr("Add memory breakpoint"), this,
+ [this, addr] { emit RequestMemoryBreakpoint(addr); });
+ menu->addAction(tr("Add to &watch"), this,
+ [this, addr, watch_name] { emit RequestWatch(watch_name, addr); });
+ menu->addAction(tr("View &memory"), this, [this, addr] { emit RequestViewInMemory(addr); });
+ menu->addAction(tr("View &code"), this, [this, addr] { emit RequestViewInCode(addr); });
+ menu->exec(QCursor::pos());
+}
+
+QLineEdit* ThreadWidget::CreateLineEdit() const
+{
+ QLineEdit* line_edit = new QLineEdit(QLatin1Literal("00000000"));
+ line_edit->setReadOnly(true);
+ line_edit->setFixedWidth(line_edit->fontMetrics().width(QLatin1Literal(" 00000000 ")));
+ return line_edit;
+}
+
+QGroupBox* ThreadWidget::CreateContextGroup()
+{
+ QGroupBox* context_group = new QGroupBox(tr("Thread context"));
+ QGridLayout* context_layout = new QGridLayout;
+ context_group->setLayout(context_layout);
+ context_layout->addWidget(new QLabel(tr("Current context")), 0, 0);
+ m_current_context = CreateLineEdit();
+ context_layout->addWidget(m_current_context, 0, 1);
+ context_layout->addWidget(new QLabel(tr("Current thread")), 1, 0);
+ m_current_thread = CreateLineEdit();
+ context_layout->addWidget(m_current_thread, 1, 1);
+ context_layout->addWidget(new QLabel(tr("Default thread")), 2, 0);
+ m_default_thread = CreateLineEdit();
+ context_layout->addWidget(m_default_thread, 2, 1);
+ context_layout->setSpacing(1);
+ return context_group;
+}
+
+QGroupBox* ThreadWidget::CreateActiveThreadQueueGroup()
+{
+ QGroupBox* thread_queue_group = new QGroupBox(tr("Active thread queue"));
+ auto* thread_queue_layout = new QGridLayout;
+ thread_queue_group->setLayout(thread_queue_layout);
+ thread_queue_layout->addWidget(new QLabel(tr("Head")), 0, 0);
+ m_queue_head = CreateLineEdit();
+ thread_queue_layout->addWidget(m_queue_head, 0, 1);
+ thread_queue_layout->addWidget(new QLabel(tr("Tail")), 1, 0);
+ m_queue_tail = CreateLineEdit();
+ thread_queue_layout->addWidget(m_queue_tail, 1, 1);
+ thread_queue_layout->setSpacing(1);
+ return thread_queue_group;
+}
+
+QGroupBox* ThreadWidget::CreateThreadGroup()
+{
+ QGroupBox* thread_group = new QGroupBox(tr("Active threads"));
+ QGridLayout* thread_layout = new QGridLayout;
+ thread_group->setLayout(thread_layout);
+
+ m_thread_table = new QTableWidget();
+ QStringList header{tr("Address"),
+ tr("State"),
+ tr("Detached"),
+ tr("Suspended"),
+ QStringLiteral("%1\n(%2)").arg(tr("Base priority"), tr("Effective priority")),
+ QStringLiteral("%1\n%2").arg(tr("Stack end"), tr("Stack start")),
+ tr("errno"),
+ tr("Specific")};
+ m_thread_table->setColumnCount(header.size());
+
+ m_thread_table->setHorizontalHeaderLabels(header);
+ m_thread_table->setTabKeyNavigation(false);
+ m_thread_table->verticalHeader()->setVisible(false);
+ m_thread_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_thread_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_thread_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_thread_table->setWordWrap(false);
+
+ thread_layout->addWidget(m_thread_table, 0, 0);
+ thread_layout->setSpacing(1);
+ return thread_group;
+}
+
+QGroupBox* ThreadWidget::CreateThreadContextGroup()
+{
+ QGroupBox* thread_context_group = new QGroupBox(tr("Selected thread context"));
+ QGridLayout* thread_context_layout = new QGridLayout;
+ thread_context_group->setLayout(thread_context_layout);
+
+ m_context_table = new QTableWidget();
+ m_context_table->setColumnCount(8); // GPR, FPR, PSF, (GQR+others)
+ m_context_table->setRowCount(32);
+ m_context_table->setTabKeyNavigation(false);
+ m_context_table->horizontalHeader()->setVisible(false);
+ m_context_table->verticalHeader()->setVisible(false);
+ m_context_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_context_table->setSelectionBehavior(QAbstractItemView::SelectItems);
+ m_context_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_context_table->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ thread_context_layout->addWidget(m_context_table, 0, 0);
+ thread_context_layout->setSpacing(1);
+ return thread_context_group;
+}
+
+QGroupBox* ThreadWidget::CreateThreadCallstackGroup()
+{
+ QGroupBox* thread_callstack_group = new QGroupBox(tr("Selected thread callstack"));
+ QGridLayout* thread_callstack_layout = new QGridLayout;
+ thread_callstack_group->setLayout(thread_callstack_layout);
+
+ m_callstack_table = new QTableWidget();
+ QStringList header{tr("Address"), tr("Back Chain"), tr("LR Save"), tr("Description")};
+ m_callstack_table->setColumnCount(header.size());
+
+ m_callstack_table->setHorizontalHeaderLabels(header);
+ m_callstack_table->setRowCount(0);
+ m_callstack_table->setTabKeyNavigation(false);
+ m_callstack_table->verticalHeader()->setVisible(false);
+ m_callstack_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_callstack_table->setSelectionBehavior(QAbstractItemView::SelectItems);
+ m_callstack_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_callstack_table->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ thread_callstack_layout->addWidget(m_callstack_table, 0, 0);
+ thread_callstack_layout->setSpacing(1);
+ return thread_callstack_group;
+}
+
+void ThreadWidget::Update()
+{
+ if (!isVisible())
+ return;
+
+ const auto state = Core::GetState();
+ if (state == Core::State::Stopping)
+ {
+ m_thread_table->setRowCount(0);
+ UpdateThreadContext({});
+ UpdateThreadCallstack({});
+ }
+ if (state != Core::State::Paused)
+ return;
+
+ const auto format_hex = [](u32 value) {
+ return QStringLiteral("%1").arg(value, 8, 16, QLatin1Char('0'));
+ };
+ const auto format_f64_as_u64 = [](double value) {
+ return QStringLiteral("%1").arg(Common::BitCast(value), 16, 16, QLatin1Char('0'));
+ };
+ const auto format_hex_from = [&format_hex](u32 addr) {
+ addr = PowerPC::HostIsRAMAddress(addr) ? PowerPC::HostRead_U32(addr) : 0;
+ return format_hex(addr);
+ };
+ const auto get_state = [](u16 state) {
+ QString state_name;
+ switch (state)
+ {
+ case 1:
+ state_name = tr("READY");
+ break;
+ case 2:
+ state_name = tr("RUNNING");
+ break;
+ case 4:
+ state_name = tr("WAITING");
+ break;
+ case 8:
+ state_name = tr("MORIBUND");
+ break;
+ default:
+ state_name = tr("UNKNOWN");
+ }
+ return QStringLiteral("%1 (%2)").arg(QString::number(state), state_name);
+ };
+ const auto get_priority = [](u16 base, u16 effective) {
+ return QStringLiteral("%1 (%2)").arg(QString::number(base), QString::number(effective));
+ };
+ const auto get_stack = [](u32 end, u32 start) {
+ return QStringLiteral("%1\n%2")
+ .arg(end, 8, 16, QLatin1Char('0'))
+ .arg(start, 8, 16, QLatin1Char('0'));
+ };
+
+ // YAGCD - Section 4.2.1.4 Dolphin OS Globals
+ m_current_context->setText(format_hex_from(0x800000D4));
+ m_current_thread->setText(format_hex_from(0x800000E4));
+ m_default_thread->setText(format_hex_from(0x800000D8));
+
+ m_queue_head->setText(format_hex_from(0x800000DC));
+ m_queue_tail->setText(format_hex_from(0x800000E0));
+
+ // Thread group
+ m_threads = PowerPC::debug_interface.GetThreads();
+ int i = 0;
+ m_thread_table->setRowCount(i);
+ for (const auto& thread : m_threads)
+ {
+ m_thread_table->insertRow(i);
+ m_thread_table->setItem(i, 0, new QTableWidgetItem(format_hex(thread->GetAddress())));
+ m_thread_table->setItem(i, 1, new QTableWidgetItem(get_state(thread->GetState())));
+ m_thread_table->setItem(i, 2, new QTableWidgetItem(QString::number(thread->IsDetached())));
+ m_thread_table->setItem(i, 3, new QTableWidgetItem(QString::number(thread->IsSuspended())));
+ m_thread_table->setItem(i, 4,
+ new QTableWidgetItem(get_priority(thread->GetBasePriority(),
+ thread->GetEffectivePriority())));
+ m_thread_table->setItem(
+ i, 5, new QTableWidgetItem(get_stack(thread->GetStackEnd(), thread->GetStackStart())));
+ m_thread_table->setItem(i, 6, new QTableWidgetItem(QString::number(thread->GetErrno())));
+ m_thread_table->setItem(i, 7,
+ new QTableWidgetItem(QString::fromStdString(thread->GetSpecific())));
+ i += 1;
+ }
+ m_thread_table->resizeColumnsToContents();
+ m_thread_table->resizeRowsToContents();
+
+ // Thread's context group
+ UpdateThreadContext({});
+}
+
+void ThreadWidget::UpdateThreadContext(const Common::Debug::PartialContext& context)
+{
+ const auto format_hex = [](const std::optional& value) {
+ if (!value)
+ return QString{};
+ return QStringLiteral("%1").arg(*value, 8, 16, QLatin1Char('0'));
+ };
+ const auto format_hex_idx = [](const auto& table, std::size_t index) {
+ if (!table || index >= table->size())
+ return QString{};
+ return QStringLiteral("%1").arg(table->at(index), 8, 16, QLatin1Char('0'));
+ };
+ const auto format_f64_as_u64_idx = [](const auto& table, std::size_t index) {
+ if (!table || index >= table->size())
+ return QString{};
+ return QStringLiteral("%1").arg(Common::BitCast(table->at(index)), 16, 16,
+ QLatin1Char('0'));
+ };
+
+ m_context_table->setRowCount(0);
+ for (int i = 0; i < 32; i++)
+ {
+ m_context_table->insertRow(i);
+ m_context_table->setItem(i, 0, new QTableWidgetItem(QStringLiteral("GPR%1").arg(i)));
+ m_context_table->setItem(i, 1, new QTableWidgetItem(format_hex_idx(context.gpr, i)));
+ m_context_table->setItem(i, 2, new QTableWidgetItem(QStringLiteral("FPR%1").arg(i)));
+ m_context_table->setItem(i, 3, new QTableWidgetItem(format_f64_as_u64_idx(context.fpr, i)));
+ m_context_table->setItem(i, 4, new QTableWidgetItem(QStringLiteral("PSF%1").arg(i)));
+ m_context_table->setItem(i, 5, new QTableWidgetItem(format_f64_as_u64_idx(context.psf, i)));
+
+ if (i < 8)
+ {
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QStringLiteral("GQR%1").arg(i)));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex_idx(context.gqr, i)));
+ continue;
+ }
+ switch (i)
+ {
+ case 8:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("CR")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.cr)));
+ break;
+ case 9:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("LR")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.lr)));
+ break;
+ case 10:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("CTR")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.ctr)));
+ break;
+ case 11:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("XER")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.xer)));
+ break;
+ case 12:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("FPSCR")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.fpscr)));
+ break;
+ case 13:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("SRR0")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.srr0)));
+ break;
+ case 14:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("SRR1")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.srr1)));
+ break;
+ case 15:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("DUMMY")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.dummy)));
+ break;
+ case 16:
+ m_context_table->setItem(i, 6, new QTableWidgetItem(QLatin1Literal("STATE")));
+ m_context_table->setItem(i, 7, new QTableWidgetItem(format_hex(context.state)));
+ break;
+ default:
+ for (int j = 6; j <= 7; j++)
+ {
+ auto* disabled_item = new QTableWidgetItem();
+ auto flags = disabled_item->flags();
+ flags &= ~Qt::ItemIsSelectable;
+ flags &= ~Qt::ItemIsEditable;
+ flags &= ~Qt::ItemIsEnabled;
+ disabled_item->setFlags(flags);
+ disabled_item->setBackground(Qt::gray);
+ m_context_table->setItem(i, j, disabled_item);
+ }
+ }
+ }
+ m_context_table->resizeColumnsToContents();
+}
+
+void ThreadWidget::UpdateThreadCallstack(const Common::Debug::PartialContext& context)
+{
+ m_callstack_table->setRowCount(0);
+
+ if (!context.gpr)
+ return;
+
+ const auto format_hex = [](u32 value) {
+ return QStringLiteral("%1").arg(value, 8, 16, QLatin1Char('0'));
+ };
+
+ u32 sp = context.gpr->at(1);
+ for (int i = 0; i < 16; i++)
+ {
+ if (sp == 0 || sp == 0xffffffff || !PowerPC::HostIsRAMAddress(sp))
+ break;
+ m_callstack_table->insertRow(i);
+ m_callstack_table->setItem(i, 0, new QTableWidgetItem(format_hex(sp)));
+ if (PowerPC::HostIsRAMAddress(sp + 4))
+ {
+ const u32 lr_save = PowerPC::HostRead_U32(sp + 4);
+ m_callstack_table->setItem(i, 2, new QTableWidgetItem(format_hex(lr_save)));
+ m_callstack_table->setItem(i, 3,
+ new QTableWidgetItem(QString::fromStdString(
+ PowerPC::debug_interface.GetDescription(lr_save))));
+ }
+ else
+ {
+ m_callstack_table->setItem(i, 2, new QTableWidgetItem(QLatin1Literal("--------")));
+ }
+ sp = PowerPC::HostRead_U32(sp);
+ m_callstack_table->setItem(i, 1, new QTableWidgetItem(format_hex(sp)));
+ }
+}
+
+void ThreadWidget::OnSelectionChanged(int row)
+{
+ Common::Debug::PartialContext context;
+
+ if (row >= 0 && row < m_threads.size())
+ context = m_threads[row]->GetContext();
+
+ UpdateThreadContext(context);
+ UpdateThreadCallstack(context);
+}
diff --git a/Source/Core/DolphinQt/Debugger/ThreadWidget.h b/Source/Core/DolphinQt/Debugger/ThreadWidget.h
new file mode 100644
index 0000000000..a0d183ebe7
--- /dev/null
+++ b/Source/Core/DolphinQt/Debugger/ThreadWidget.h
@@ -0,0 +1,64 @@
+// Copyright 2020 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+
+#include "Common/CommonTypes.h"
+#include "Common/Debug/Threads.h"
+
+class QCloseEvent;
+class QGroupBox;
+class QLineEdit;
+class QShowEvent;
+class QTableWidget;
+
+class ThreadWidget : public QDockWidget
+{
+ Q_OBJECT
+public:
+ explicit ThreadWidget(QWidget* parent = nullptr);
+ ~ThreadWidget();
+
+signals:
+ void RequestBreakpoint(u32 addr);
+ void RequestMemoryBreakpoint(u32 addr);
+ void RequestWatch(QString name, u32 addr);
+ void RequestViewInCode(u32 addr);
+ void RequestViewInMemory(u32 addr);
+
+protected:
+ void closeEvent(QCloseEvent*) override;
+ void showEvent(QShowEvent* event) override;
+
+private:
+ void CreateWidgets();
+ void ConnectWidgets();
+
+ QLineEdit* CreateLineEdit() const;
+ QGroupBox* CreateContextGroup();
+ QGroupBox* CreateActiveThreadQueueGroup();
+ QGroupBox* CreateThreadGroup();
+ QGroupBox* CreateThreadContextGroup();
+ QGroupBox* CreateThreadCallstackGroup();
+
+ void ShowContextMenu(QTableWidget* table);
+
+ void Update();
+ void UpdateThreadContext(const Common::Debug::PartialContext& context);
+ void UpdateThreadCallstack(const Common::Debug::PartialContext& context);
+ void OnSelectionChanged(int row);
+
+ QGroupBox* m_state;
+ QLineEdit* m_current_context;
+ QLineEdit* m_current_thread;
+ QLineEdit* m_default_thread;
+ QLineEdit* m_queue_head;
+ QLineEdit* m_queue_tail;
+ QTableWidget* m_thread_table;
+ QTableWidget* m_context_table;
+ QTableWidget* m_callstack_table;
+ Common::Debug::Threads m_threads;
+};
diff --git a/Source/Core/DolphinQt/Debugger/WatchWidget.cpp b/Source/Core/DolphinQt/Debugger/WatchWidget.cpp
index 7870334227..dc450ca603 100644
--- a/Source/Core/DolphinQt/Debugger/WatchWidget.cpp
+++ b/Source/Core/DolphinQt/Debugger/WatchWidget.cpp
@@ -348,4 +348,5 @@ void WatchWidget::AddWatchBreakpoint(int row)
void WatchWidget::AddWatch(QString name, u32 addr)
{
PowerPC::debug_interface.SetWatch(addr, name.toStdString());
+ Update();
}
diff --git a/Source/Core/DolphinQt/Debugger/WatchWidget.h b/Source/Core/DolphinQt/Debugger/WatchWidget.h
index 4741a5ed0a..cea8343ec5 100644
--- a/Source/Core/DolphinQt/Debugger/WatchWidget.h
+++ b/Source/Core/DolphinQt/Debugger/WatchWidget.h
@@ -9,11 +9,11 @@
#include "Common/CommonTypes.h"
class QAction;
+class QCloseEvent;
+class QShowEvent;
class QTableWidget;
class QTableWidgetItem;
class QToolBar;
-class QCloseEvent;
-class QShowEvent;
class WatchWidget : public QDockWidget
{
diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj
index 936a412a7e..0fdfc53118 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -152,6 +152,7 @@
+
@@ -296,6 +297,7 @@
+
@@ -385,6 +387,7 @@
+
diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp
index 270f4394c6..fe74c680fa 100644
--- a/Source/Core/DolphinQt/MainWindow.cpp
+++ b/Source/Core/DolphinQt/MainWindow.cpp
@@ -73,6 +73,7 @@
#include "DolphinQt/Debugger/MemoryWidget.h"
#include "DolphinQt/Debugger/NetworkWidget.h"
#include "DolphinQt/Debugger/RegisterWidget.h"
+#include "DolphinQt/Debugger/ThreadWidget.h"
#include "DolphinQt/Debugger/WatchWidget.h"
#include "DolphinQt/DiscordHandler.h"
#include "DolphinQt/FIFO/FIFOPlayerWindow.h"
@@ -392,20 +393,33 @@ void MainWindow::CreateComponents()
m_memory_widget = new MemoryWidget(this);
m_network_widget = new NetworkWidget(this);
m_register_widget = new RegisterWidget(this);
+ m_thread_widget = new ThreadWidget(this);
m_watch_widget = new WatchWidget(this);
m_breakpoint_widget = new BreakpointWidget(this);
m_code_widget = new CodeWidget(this);
m_cheats_manager = new CheatsManager(this);
- connect(m_watch_widget, &WatchWidget::RequestMemoryBreakpoint,
- [this](u32 addr) { m_breakpoint_widget->AddAddressMBP(addr); });
- connect(m_register_widget, &RegisterWidget::RequestMemoryBreakpoint,
- [this](u32 addr) { m_breakpoint_widget->AddAddressMBP(addr); });
- connect(m_register_widget, &RegisterWidget::RequestViewInMemory, m_memory_widget,
- [this](u32 addr) { m_memory_widget->SetAddress(addr); });
- connect(m_register_widget, &RegisterWidget::RequestViewInCode, m_code_widget, [this](u32 addr) {
+ const auto request_watch = [this](QString name, u32 addr) {
+ m_watch_widget->AddWatch(name, addr);
+ };
+ const auto request_breakpoint = [this](u32 addr) { m_breakpoint_widget->AddBP(addr); };
+ const auto request_memory_breakpoint = [this](u32 addr) {
+ m_breakpoint_widget->AddAddressMBP(addr);
+ };
+ const auto request_view_in_memory = [this](u32 addr) { m_memory_widget->SetAddress(addr); };
+ const auto request_view_in_code = [this](u32 addr) {
m_code_widget->SetAddress(addr, CodeViewWidget::SetAddressUpdate::WithUpdate);
- });
+ };
+
+ connect(m_watch_widget, &WatchWidget::RequestMemoryBreakpoint, request_memory_breakpoint);
+ connect(m_register_widget, &RegisterWidget::RequestMemoryBreakpoint, request_memory_breakpoint);
+ connect(m_register_widget, &RegisterWidget::RequestViewInMemory, request_view_in_memory);
+ connect(m_register_widget, &RegisterWidget::RequestViewInCode, request_view_in_code);
+ connect(m_thread_widget, &ThreadWidget::RequestBreakpoint, request_breakpoint);
+ connect(m_thread_widget, &ThreadWidget::RequestMemoryBreakpoint, request_memory_breakpoint);
+ connect(m_thread_widget, &ThreadWidget::RequestWatch, request_watch);
+ connect(m_thread_widget, &ThreadWidget::RequestViewInMemory, request_view_in_memory);
+ connect(m_thread_widget, &ThreadWidget::RequestViewInCode, request_view_in_code);
connect(m_code_widget, &CodeWidget::BreakpointsChanged, m_breakpoint_widget,
&BreakpointWidget::Update);
@@ -642,6 +656,7 @@ void MainWindow::ConnectStack()
addDockWidget(Qt::LeftDockWidgetArea, m_log_config_widget);
addDockWidget(Qt::LeftDockWidgetArea, m_code_widget);
addDockWidget(Qt::LeftDockWidgetArea, m_register_widget);
+ addDockWidget(Qt::LeftDockWidgetArea, m_thread_widget);
addDockWidget(Qt::LeftDockWidgetArea, m_watch_widget);
addDockWidget(Qt::LeftDockWidgetArea, m_breakpoint_widget);
addDockWidget(Qt::LeftDockWidgetArea, m_memory_widget);
@@ -651,6 +666,7 @@ void MainWindow::ConnectStack()
tabifyDockWidget(m_log_widget, m_log_config_widget);
tabifyDockWidget(m_log_widget, m_code_widget);
tabifyDockWidget(m_log_widget, m_register_widget);
+ tabifyDockWidget(m_log_widget, m_thread_widget);
tabifyDockWidget(m_log_widget, m_watch_widget);
tabifyDockWidget(m_log_widget, m_breakpoint_widget);
tabifyDockWidget(m_log_widget, m_memory_widget);
diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h
index d84edb5af7..63141ec2d3 100644
--- a/Source/Core/DolphinQt/MainWindow.h
+++ b/Source/Core/DolphinQt/MainWindow.h
@@ -39,6 +39,7 @@ class RegisterWidget;
class RenderWidget;
class SearchBar;
class SettingsWindow;
+class ThreadWidget;
class ToolBar;
class WatchWidget;
class WiiTASInputWindow;
@@ -228,6 +229,7 @@ private:
MemoryWidget* m_memory_widget;
NetworkWidget* m_network_widget;
RegisterWidget* m_register_widget;
+ ThreadWidget* m_thread_widget;
WatchWidget* m_watch_widget;
CheatsManager* m_cheats_manager;
QByteArray m_render_widget_geometry;
diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp
index 25a7a1dd5b..453260ea91 100644
--- a/Source/Core/DolphinQt/MenuBar.cpp
+++ b/Source/Core/DolphinQt/MenuBar.cpp
@@ -168,6 +168,7 @@ void MenuBar::OnDebugModeToggled(bool enabled)
// View
m_show_code->setVisible(enabled);
m_show_registers->setVisible(enabled);
+ m_show_threads->setVisible(enabled);
m_show_watch->setVisible(enabled);
m_show_breakpoints->setVisible(enabled);
m_show_memory->setVisible(enabled);
@@ -445,6 +446,14 @@ void MenuBar::AddViewMenu()
connect(&Settings::Instance(), &Settings::RegistersVisibilityChanged, m_show_registers,
&QAction::setChecked);
+ m_show_threads = view_menu->addAction(tr("&Threads"));
+ m_show_threads->setCheckable(true);
+ m_show_threads->setChecked(Settings::Instance().IsThreadsVisible());
+
+ connect(m_show_threads, &QAction::toggled, &Settings::Instance(), &Settings::SetThreadsVisible);
+ connect(&Settings::Instance(), &Settings::ThreadsVisibilityChanged, m_show_threads,
+ &QAction::setChecked);
+
// i18n: This kind of "watch" is used for watching emulated memory.
// It's not related to timekeeping devices.
m_show_watch = view_menu->addAction(tr("&Watch"));
diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h
index 71c24fc837..41811b4a09 100644
--- a/Source/Core/DolphinQt/MenuBar.h
+++ b/Source/Core/DolphinQt/MenuBar.h
@@ -233,6 +233,7 @@ private:
// View
QAction* m_show_code;
QAction* m_show_registers;
+ QAction* m_show_threads;
QAction* m_show_watch;
QAction* m_show_breakpoints;
QAction* m_show_memory;
diff --git a/Source/Core/DolphinQt/Settings.cpp b/Source/Core/DolphinQt/Settings.cpp
index 9571ce055c..0cf5c9c4f8 100644
--- a/Source/Core/DolphinQt/Settings.cpp
+++ b/Source/Core/DolphinQt/Settings.cpp
@@ -364,6 +364,20 @@ void Settings::SetRegistersVisible(bool enabled)
}
}
+bool Settings::IsThreadsVisible() const
+{
+ return GetQSettings().value(QStringLiteral("debugger/showthreads")).toBool();
+}
+
+void Settings::SetThreadsVisible(bool enabled)
+{
+ if (IsThreadsVisible() == enabled)
+ return;
+
+ GetQSettings().setValue(QStringLiteral("debugger/showthreads"), enabled);
+ emit ThreadsVisibilityChanged(enabled);
+}
+
bool Settings::IsRegistersVisible() const
{
return GetQSettings().value(QStringLiteral("debugger/showregisters")).toBool();
diff --git a/Source/Core/DolphinQt/Settings.h b/Source/Core/DolphinQt/Settings.h
index 0af6357515..eb7f7ab4ae 100644
--- a/Source/Core/DolphinQt/Settings.h
+++ b/Source/Core/DolphinQt/Settings.h
@@ -118,6 +118,8 @@ public:
bool IsDebugModeEnabled() const;
void SetRegistersVisible(bool enabled);
bool IsRegistersVisible() const;
+ void SetThreadsVisible(bool enabled);
+ bool IsThreadsVisible() const;
void SetWatchVisible(bool enabled);
bool IsWatchVisible() const;
void SetBreakpointsVisible(bool enabled);
@@ -161,6 +163,7 @@ signals:
void VolumeChanged(int volume);
void NANDRefresh();
void RegistersVisibilityChanged(bool visible);
+ void ThreadsVisibilityChanged(bool visible);
void LogVisibilityChanged(bool visible);
void LogConfigVisibilityChanged(bool visible);
void ToolBarVisibilityChanged(bool visible);