@@ -0,0 +1,154 @@
// Copyright 2020 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <array>
#include <string>
#include <type_traits>

#include "Common/CommonTypes.h"
#include "Common/Debug/Threads.h"

namespace Common::Debug
{
template <class C>
struct OSQueue
{
u32 head;
u32 tail;
};
template <class C>
struct OSLink
{
u32 next;
u32 prev;
};

struct OSMutex;
struct OSThread;

using OSThreadQueue = OSQueue<OSThread>;
using OSThreadLink = OSLink<OSThread>;

using OSMutexQueue = OSQueue<OSMutex>;
using OSMutexLink = OSLink<OSMutex>;

struct OSContext
{
enum class State : u16
{
HasFPU = 1,
HasException = 2,
};
std::array<u32, 32> gpr;
u32 cr;
u32 lr;
u32 ctr;
u32 xer;
std::array<double, 32> fpr;
u64 fpscr;
u32 srr0;
u32 srr1;
u16 dummy;
State state;
std::array<u32, 8> gqr;
u32 psf_padding;
std::array<double, 32> psf;

void Read(u32 addr);
};

static_assert(std::is_trivially_copyable_v<OSContext>);
static_assert(std::is_standard_layout_v<OSContext>);
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<u32, 2> 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<OSThread>);
static_assert(std::is_standard_layout_v<OSThread>);
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<OSMutex>);
static_assert(std::is_standard_layout_v<OSMutex>);
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
@@ -0,0 +1,63 @@
// Copyright 2020 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <array>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "Common/CommonTypes.h"

namespace Common::Debug
{
struct PartialContext
{
std::optional<std::array<u32, 32>> gpr;
std::optional<u32> cr;
std::optional<u32> lr;
std::optional<u32> ctr;
std::optional<u32> xer;
std::optional<std::array<double, 32>> fpr;
std::optional<u64> fpscr;
std::optional<u32> srr0;
std::optional<u32> srr1;
std::optional<u16> dummy;
std::optional<u16> state;
std::optional<std::array<u32, 8>> gqr;
std::optional<std::array<double, 32>> 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<std::unique_ptr<ThreadView>>;

} // namespace Common::Debug
@@ -9,6 +9,7 @@
#include <vector>

#include "Common/CommonTypes.h"
#include "Common/Debug/Threads.h"

namespace Common::Debug
{
@@ -51,6 +52,9 @@ class DebugInterface
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
{
@@ -767,4 +767,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>
@@ -11,6 +11,7 @@
#include <fmt/format.h>

#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<Common::Debug::OSThreadView>(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<Common::Debug::OSThreadView>(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
@@ -52,6 +52,9 @@ class PPCDebugInterface final : public Common::DebugInterface
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;
@@ -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.
@@ -53,6 +53,9 @@ class DSPDebugInterface final : public Common::DebugInterface
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;
@@ -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

Large diffs are not rendered by default.

@@ -0,0 +1,64 @@
// Copyright 2020 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <QDockWidget>

#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;
};
@@ -348,4 +348,5 @@ void WatchWidget::AddWatchBreakpoint(int row)
void WatchWidget::AddWatch(QString name, u32 addr)
{
PowerPC::debug_interface.SetWatch(addr, name.toStdString());
Update();
}
@@ -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
{
@@ -152,6 +152,7 @@
<QtMoc Include="Debugger\NewBreakpointDialog.h" />
<QtMoc Include="Debugger\PatchInstructionDialog.h" />
<QtMoc Include="Debugger\RegisterWidget.h" />
<QtMoc Include="Debugger\ThreadWidget.h" />
<QtMoc Include="Debugger\WatchWidget.h" />
<QtMoc Include="GCMemcardCreateNewDialog.h" />
<QtMoc Include="GCMemcardManager.h" />
@@ -296,6 +297,7 @@
<ClCompile Include="$(QtMocOutPrefix)PostProcessingConfigWindow.cpp" />
<ClCompile Include="$(QtMocOutPrefix)PropertiesDialog.cpp" />
<ClCompile Include="$(QtMocOutPrefix)RegisterWidget.cpp" />
<ClCompile Include="$(QtMocOutPrefix)ThreadWidget.cpp" />
<ClCompile Include="$(QtMocOutPrefix)RenderWidget.cpp" />
<ClCompile Include="$(QtMocOutPrefix)SearchBar.cpp" />
<ClCompile Include="$(QtMocOutPrefix)Settings.cpp" />
@@ -385,6 +387,7 @@
<ClCompile Include="Debugger\MemoryWidget.cpp" />
<ClCompile Include="Debugger\MemoryViewWidget.cpp" />
<ClCompile Include="Debugger\NetworkWidget.cpp" />
<ClCompile Include="Debugger\ThreadWidget.cpp" />
<ClCompile Include="DiscordHandler.cpp" />
<ClCompile Include="DiscordJoinRequestDialog.cpp" />
<ClCompile Include="FIFO\FIFOAnalyzer.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);
@@ -39,6 +39,7 @@ class RegisterWidget;
class RenderWidget;
class SearchBar;
class SettingsWindow;
class ThreadWidget;
class ToolBar;
class WatchWidget;
class WiiTASInputWindow;
@@ -228,6 +229,7 @@ class MainWindow final : public QMainWindow
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;
@@ -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"));
@@ -233,6 +233,7 @@ class MenuBar final : public QMenuBar
// 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;
@@ -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();
@@ -118,6 +118,8 @@ class Settings final : public QObject
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 @@ class Settings final : public QObject
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);