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);