summaryrefslogtreecommitdiff
path: root/src/citron/debugger
diff options
context:
space:
mode:
authorZephyron <zephyron@citron-emu.org>2024-12-31 16:19:25 +1000
committerZephyron <zephyron@citron-emu.org>2024-12-31 16:19:25 +1000
commit9427e27e24a7135880ee2881c3c44988e174b41a (patch)
tree83f0062a35be144f6b162eaa823c5b3c7620146e /src/citron/debugger
parentb35ae725d20960411e8588b11c12a2d55f86c9d0 (diff)
chore: update project branding to citron
Diffstat (limited to 'src/citron/debugger')
-rw-r--r--src/citron/debugger/console.cpp49
-rw-r--r--src/citron/debugger/console.h13
-rw-r--r--src/citron/debugger/controller.cpp116
-rw-r--r--src/citron/debugger/controller.h56
-rw-r--r--src/citron/debugger/profiler.cpp229
-rw-r--r--src/citron/debugger/profiler.h27
-rw-r--r--src/citron/debugger/wait_tree.cpp431
-rw-r--r--src/citron/debugger/wait_tree.h188
8 files changed, 1109 insertions, 0 deletions
diff --git a/src/citron/debugger/console.cpp b/src/citron/debugger/console.cpp
new file mode 100644
index 000000000..1c1342ff1
--- /dev/null
+++ b/src/citron/debugger/console.cpp
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifdef _WIN32
+#include <windows.h>
+
+#include <wincon.h>
+#endif
+
+#include "common/logging/backend.h"
+#include "yuzu/debugger/console.h"
+#include "yuzu/uisettings.h"
+
+namespace Debugger {
+void ToggleConsole() {
+ static bool console_shown = false;
+ if (console_shown == UISettings::values.show_console.GetValue()) {
+ return;
+ } else {
+ console_shown = UISettings::values.show_console.GetValue();
+ }
+
+ using namespace Common::Log;
+#if defined(_WIN32) && !defined(_DEBUG)
+ FILE* temp;
+ if (UISettings::values.show_console) {
+ if (AllocConsole()) {
+ // The first parameter for freopen_s is a out parameter, so we can just ignore it
+ freopen_s(&temp, "CONIN$", "r", stdin);
+ freopen_s(&temp, "CONOUT$", "w", stdout);
+ freopen_s(&temp, "CONOUT$", "w", stderr);
+ SetConsoleOutputCP(65001);
+ SetColorConsoleBackendEnabled(true);
+ }
+ } else {
+ if (FreeConsole()) {
+ // In order to close the console, we have to also detach the streams on it.
+ // Just redirect them to NUL if there is no console window
+ SetColorConsoleBackendEnabled(false);
+ freopen_s(&temp, "NUL", "r", stdin);
+ freopen_s(&temp, "NUL", "w", stdout);
+ freopen_s(&temp, "NUL", "w", stderr);
+ }
+ }
+#else
+ SetColorConsoleBackendEnabled(UISettings::values.show_console.GetValue());
+#endif
+}
+} // namespace Debugger
diff --git a/src/citron/debugger/console.h b/src/citron/debugger/console.h
new file mode 100644
index 000000000..2491d1ec1
--- /dev/null
+++ b/src/citron/debugger/console.h
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+namespace Debugger {
+
+/**
+ * Uses the WINAPI to hide or show the stderr console. This function is a placeholder until we can
+ * get a real qt logging window which would work for all platforms.
+ */
+void ToggleConsole();
+} // namespace Debugger
diff --git a/src/citron/debugger/controller.cpp b/src/citron/debugger/controller.cpp
new file mode 100644
index 000000000..216d2974d
--- /dev/null
+++ b/src/citron/debugger/controller.cpp
@@ -0,0 +1,116 @@
+// SPDX-FileCopyrightText: 2015 Citra Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <QAction>
+#include <QLayout>
+#include <QString>
+#include "common/settings.h"
+#include "hid_core/frontend/emulated_controller.h"
+#include "hid_core/hid_core.h"
+#include "input_common/drivers/tas_input.h"
+#include "input_common/main.h"
+#include "yuzu/configuration/configure_input_player_widget.h"
+#include "yuzu/debugger/controller.h"
+
+ControllerDialog::ControllerDialog(Core::HID::HIDCore& hid_core_,
+ std::shared_ptr<InputCommon::InputSubsystem> input_subsystem_,
+ QWidget* parent)
+ : QWidget(parent, Qt::Dialog), hid_core{hid_core_}, input_subsystem{input_subsystem_} {
+ setObjectName(QStringLiteral("Controller"));
+ setWindowTitle(tr("Controller P1"));
+ resize(500, 350);
+ setMinimumSize(500, 350);
+ // Enable the maximize button
+ setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint);
+
+ widget = new PlayerControlPreview(this);
+ refreshConfiguration();
+ QLayout* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(widget);
+ setLayout(layout);
+
+ // Configure focus so that widget is focusable and the dialog automatically forwards focus to
+ // it.
+ setFocusProxy(widget);
+ widget->setFocusPolicy(Qt::StrongFocus);
+ widget->setFocus();
+}
+
+void ControllerDialog::refreshConfiguration() {
+ UnloadController();
+ auto* player_1 = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
+ auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
+ // Display the correct controller
+ controller = handheld->IsConnected() ? handheld : player_1;
+
+ Core::HID::ControllerUpdateCallback engine_callback{
+ .on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdate(type); },
+ .is_npad_service = true,
+ };
+ callback_key = controller->SetCallback(engine_callback);
+ widget->SetController(controller);
+ is_controller_set = true;
+}
+
+QAction* ControllerDialog::toggleViewAction() {
+ if (toggle_view_action == nullptr) {
+ toggle_view_action = new QAction(tr("&Controller P1"), this);
+ toggle_view_action->setCheckable(true);
+ toggle_view_action->setChecked(isVisible());
+ connect(toggle_view_action, &QAction::toggled, this, &ControllerDialog::setVisible);
+ }
+
+ return toggle_view_action;
+}
+
+void ControllerDialog::UnloadController() {
+ widget->UnloadController();
+ if (is_controller_set) {
+ controller->DeleteCallback(callback_key);
+ is_controller_set = false;
+ }
+}
+
+void ControllerDialog::showEvent(QShowEvent* ev) {
+ if (toggle_view_action) {
+ toggle_view_action->setChecked(isVisible());
+ }
+ QWidget::showEvent(ev);
+}
+
+void ControllerDialog::hideEvent(QHideEvent* ev) {
+ if (toggle_view_action) {
+ toggle_view_action->setChecked(isVisible());
+ }
+ QWidget::hideEvent(ev);
+}
+
+void ControllerDialog::ControllerUpdate(Core::HID::ControllerTriggerType type) {
+ // TODO(german77): Remove TAS from here
+ switch (type) {
+ case Core::HID::ControllerTriggerType::Button:
+ case Core::HID::ControllerTriggerType::Stick: {
+ const auto buttons_values = controller->GetButtonsValues();
+ const auto stick_values = controller->GetSticks();
+ u64 buttons = 0;
+ std::size_t index = 0;
+ for (const auto& button : buttons_values) {
+ buttons |= button.value ? 1LLU << index : 0;
+ index++;
+ }
+ const InputCommon::TasInput::TasAnalog left_axis = {
+ .x = stick_values.left.x / 32767.f,
+ .y = stick_values.left.y / 32767.f,
+ };
+ const InputCommon::TasInput::TasAnalog right_axis = {
+ .x = stick_values.right.x / 32767.f,
+ .y = stick_values.right.y / 32767.f,
+ };
+ input_subsystem->GetTas()->RecordInput(buttons, left_axis, right_axis);
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/src/citron/debugger/controller.h b/src/citron/debugger/controller.h
new file mode 100644
index 000000000..9651dfaa9
--- /dev/null
+++ b/src/citron/debugger/controller.h
@@ -0,0 +1,56 @@
+// SPDX-FileCopyrightText: 2015 Citra Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QWidget>
+
+class QAction;
+class QHideEvent;
+class QShowEvent;
+class PlayerControlPreview;
+
+namespace InputCommon {
+class InputSubsystem;
+}
+
+namespace Core::HID {
+class HIDCore;
+class EmulatedController;
+enum class ControllerTriggerType;
+} // namespace Core::HID
+
+class ControllerDialog : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit ControllerDialog(Core::HID::HIDCore& hid_core_,
+ std::shared_ptr<InputCommon::InputSubsystem> input_subsystem_,
+ QWidget* parent = nullptr);
+
+ /// Returns a QAction that can be used to toggle visibility of this dialog.
+ QAction* toggleViewAction();
+
+ /// Reloads the widget to apply any changes in the configuration
+ void refreshConfiguration();
+
+ /// Disables events from the emulated controller
+ void UnloadController();
+
+protected:
+ void showEvent(QShowEvent* ev) override;
+ void hideEvent(QHideEvent* ev) override;
+
+private:
+ /// Redirects input from the widget to the TAS driver
+ void ControllerUpdate(Core::HID::ControllerTriggerType type);
+
+ int callback_key;
+ bool is_controller_set{};
+ Core::HID::EmulatedController* controller;
+
+ QAction* toggle_view_action = nullptr;
+ PlayerControlPreview* widget;
+ Core::HID::HIDCore& hid_core;
+ std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
+};
diff --git a/src/citron/debugger/profiler.cpp b/src/citron/debugger/profiler.cpp
new file mode 100644
index 000000000..493ee0b17
--- /dev/null
+++ b/src/citron/debugger/profiler.cpp
@@ -0,0 +1,229 @@
+// SPDX-FileCopyrightText: 2015 Citra Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <QAction>
+#include <QLayout>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QString>
+#include <QTimer>
+#include "common/common_types.h"
+#include "common/microprofile.h"
+#include "yuzu/debugger/profiler.h"
+#include "yuzu/util/util.h"
+
+// Include the implementation of the UI in this file. This isn't in microprofile.cpp because the
+// non-Qt frontends don't need it (and don't implement the UI drawing hooks either).
+#if MICROPROFILE_ENABLED
+#define MICROPROFILEUI_IMPL 1
+#include "common/microprofileui.h"
+
+class MicroProfileWidget : public QWidget {
+public:
+ MicroProfileWidget(QWidget* parent = nullptr);
+
+protected:
+ void paintEvent(QPaintEvent* ev) override;
+ void showEvent(QShowEvent* ev) override;
+ void hideEvent(QHideEvent* ev) override;
+
+ void mouseMoveEvent(QMouseEvent* ev) override;
+ void mousePressEvent(QMouseEvent* ev) override;
+ void mouseReleaseEvent(QMouseEvent* ev) override;
+ void wheelEvent(QWheelEvent* ev) override;
+
+ void keyPressEvent(QKeyEvent* ev) override;
+ void keyReleaseEvent(QKeyEvent* ev) override;
+
+private:
+ /// This timer is used to redraw the widget's contents continuously. To save resources, it only
+ /// runs while the widget is visible.
+ QTimer update_timer;
+ /// Scale the coordinate system appropriately when dpi != 96.
+ qreal x_scale = 1.0, y_scale = 1.0;
+};
+
+#endif
+
+MicroProfileDialog::MicroProfileDialog(QWidget* parent) : QWidget(parent, Qt::Dialog) {
+ setObjectName(QStringLiteral("MicroProfile"));
+ setWindowTitle(tr("&MicroProfile"));
+ resize(1000, 600);
+ // Enable the maximize button
+ setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint);
+
+#if MICROPROFILE_ENABLED
+
+ MicroProfileWidget* widget = new MicroProfileWidget(this);
+
+ QLayout* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(widget);
+ setLayout(layout);
+
+ // Configure focus so that widget is focusable and the dialog automatically forwards focus to
+ // it.
+ setFocusProxy(widget);
+ widget->setFocusPolicy(Qt::StrongFocus);
+ widget->setFocus();
+#endif
+}
+
+QAction* MicroProfileDialog::toggleViewAction() {
+ if (toggle_view_action == nullptr) {
+ toggle_view_action = new QAction(windowTitle(), this);
+ toggle_view_action->setCheckable(true);
+ toggle_view_action->setChecked(isVisible());
+ connect(toggle_view_action, &QAction::toggled, this, &MicroProfileDialog::setVisible);
+ }
+
+ return toggle_view_action;
+}
+
+void MicroProfileDialog::showEvent(QShowEvent* ev) {
+ if (toggle_view_action) {
+ toggle_view_action->setChecked(isVisible());
+ }
+ QWidget::showEvent(ev);
+}
+
+void MicroProfileDialog::hideEvent(QHideEvent* ev) {
+ if (toggle_view_action) {
+ toggle_view_action->setChecked(isVisible());
+ }
+ QWidget::hideEvent(ev);
+}
+
+#if MICROPROFILE_ENABLED
+
+/// There's no way to pass a user pointer to MicroProfile, so this variable is used to make the
+/// QPainter available inside the drawing callbacks.
+static QPainter* mp_painter = nullptr;
+
+MicroProfileWidget::MicroProfileWidget(QWidget* parent) : QWidget(parent) {
+ // Send mouse motion events even when not dragging.
+ setMouseTracking(true);
+
+ MicroProfileSetDisplayMode(1); // Timers screen
+ MicroProfileInitUI();
+
+ connect(&update_timer, &QTimer::timeout, this, qOverload<>(&MicroProfileWidget::update));
+}
+
+void MicroProfileWidget::paintEvent(QPaintEvent* ev) {
+ QPainter painter(this);
+
+ // The units used by Microprofile for drawing are based in pixels on a 96 dpi display.
+ x_scale = qreal(painter.device()->logicalDpiX()) / 96.0;
+ y_scale = qreal(painter.device()->logicalDpiY()) / 96.0;
+ painter.scale(x_scale, y_scale);
+
+ painter.setBackground(Qt::black);
+ painter.eraseRect(rect());
+
+ QFont font = GetMonospaceFont();
+ font.setPixelSize(MICROPROFILE_TEXT_HEIGHT);
+ painter.setFont(font);
+
+ mp_painter = &painter;
+ MicroProfileDraw(rect().width() / x_scale, rect().height() / y_scale);
+ mp_painter = nullptr;
+}
+
+void MicroProfileWidget::showEvent(QShowEvent* ev) {
+ update_timer.start(15); // ~60 Hz
+ QWidget::showEvent(ev);
+}
+
+void MicroProfileWidget::hideEvent(QHideEvent* ev) {
+ update_timer.stop();
+ QWidget::hideEvent(ev);
+}
+
+void MicroProfileWidget::mouseMoveEvent(QMouseEvent* ev) {
+ const auto mouse_position = ev->pos();
+ MicroProfileMousePosition(mouse_position.x() / x_scale, mouse_position.y() / y_scale, 0);
+ ev->accept();
+}
+
+void MicroProfileWidget::mousePressEvent(QMouseEvent* ev) {
+ const auto mouse_position = ev->pos();
+ MicroProfileMousePosition(mouse_position.x() / x_scale, mouse_position.y() / y_scale, 0);
+ MicroProfileMouseButton(ev->buttons() & Qt::LeftButton, ev->buttons() & Qt::RightButton);
+ ev->accept();
+}
+
+void MicroProfileWidget::mouseReleaseEvent(QMouseEvent* ev) {
+ const auto mouse_position = ev->pos();
+ MicroProfileMousePosition(mouse_position.x() / x_scale, mouse_position.y() / y_scale, 0);
+ MicroProfileMouseButton(ev->buttons() & Qt::LeftButton, ev->buttons() & Qt::RightButton);
+ ev->accept();
+}
+
+void MicroProfileWidget::wheelEvent(QWheelEvent* ev) {
+ const auto wheel_position = ev->position().toPoint();
+ MicroProfileMousePosition(wheel_position.x() / x_scale, wheel_position.y() / y_scale,
+ ev->angleDelta().y() / 120);
+ ev->accept();
+}
+
+void MicroProfileWidget::keyPressEvent(QKeyEvent* ev) {
+ if (ev->key() == Qt::Key_Control) {
+ // Inform MicroProfile that the user is holding Ctrl.
+ MicroProfileModKey(1);
+ }
+ QWidget::keyPressEvent(ev);
+}
+
+void MicroProfileWidget::keyReleaseEvent(QKeyEvent* ev) {
+ if (ev->key() == Qt::Key_Control) {
+ MicroProfileModKey(0);
+ }
+ QWidget::keyReleaseEvent(ev);
+}
+
+// These functions are called by MicroProfileDraw to draw the interface elements on the screen.
+
+void MicroProfileDrawText(int x, int y, u32 hex_color, const char* text, u32 text_length) {
+ // hex_color does not include an alpha, so it must be assumed to be 255
+ mp_painter->setPen(QColor::fromRgb(hex_color));
+
+ // It's impossible to draw a string using a monospaced font with a fixed width per cell in a
+ // way that's reliable across different platforms and fonts as far as I (yuriks) can tell, so
+ // draw each character individually in order to precisely control the text advance.
+ for (u32 i = 0; i < text_length; ++i) {
+ // Position the text baseline 1 pixel above the bottom of the text cell, this gives nice
+ // vertical alignment of text for a wide range of tested fonts.
+ mp_painter->drawText(x, y + MICROPROFILE_TEXT_HEIGHT - 2, QString{QLatin1Char{text[i]}});
+ x += MICROPROFILE_TEXT_WIDTH + 1;
+ }
+}
+
+void MicroProfileDrawBox(int left, int top, int right, int bottom, u32 hex_color,
+ MicroProfileBoxType type) {
+ QColor color = QColor::fromRgba(hex_color);
+ QBrush brush = color;
+ if (type == MicroProfileBoxTypeBar) {
+ QLinearGradient gradient(left, top, left, bottom);
+ gradient.setColorAt(0.f, color.lighter(125));
+ gradient.setColorAt(1.f, color.darker(125));
+ brush = gradient;
+ }
+ mp_painter->fillRect(left, top, right - left, bottom - top, brush);
+}
+
+void MicroProfileDrawLine2D(u32 vertices_length, float* vertices, u32 hex_color) {
+ // Temporary vector used to convert between the float array and QPointF. Marked static to reuse
+ // the allocation across calls.
+ static std::vector<QPointF> point_buf;
+
+ for (u32 i = 0; i < vertices_length; ++i) {
+ point_buf.emplace_back(vertices[i * 2 + 0], vertices[i * 2 + 1]);
+ }
+
+ // hex_color does not include an alpha, so it must be assumed to be 255
+ mp_painter->setPen(QColor::fromRgb(hex_color));
+ mp_painter->drawPolyline(point_buf.data(), vertices_length);
+ point_buf.clear();
+}
+#endif
diff --git a/src/citron/debugger/profiler.h b/src/citron/debugger/profiler.h
new file mode 100644
index 000000000..4c8ccd3c2
--- /dev/null
+++ b/src/citron/debugger/profiler.h
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: 2015 Citra Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QWidget>
+
+class QAction;
+class QHideEvent;
+class QShowEvent;
+
+class MicroProfileDialog : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit MicroProfileDialog(QWidget* parent = nullptr);
+
+ /// Returns a QAction that can be used to toggle visibility of this dialog.
+ QAction* toggleViewAction();
+
+protected:
+ void showEvent(QShowEvent* ev) override;
+ void hideEvent(QHideEvent* ev) override;
+
+private:
+ QAction* toggle_view_action = nullptr;
+};
diff --git a/src/citron/debugger/wait_tree.cpp b/src/citron/debugger/wait_tree.cpp
new file mode 100644
index 000000000..c05a05057
--- /dev/null
+++ b/src/citron/debugger/wait_tree.cpp
@@ -0,0 +1,431 @@
+// SPDX-FileCopyrightText: 2016 Citra Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <array>
+#include <fmt/format.h>
+
+#include "yuzu/debugger/wait_tree.h"
+#include "yuzu/uisettings.h"
+
+#include "core/arm/debug.h"
+#include "core/core.h"
+#include "core/hle/kernel/k_class_token.h"
+#include "core/hle/kernel/k_handle_table.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/kernel/k_readable_event.h"
+#include "core/hle/kernel/k_scheduler.h"
+#include "core/hle/kernel/k_synchronization_object.h"
+#include "core/hle/kernel/k_thread.h"
+#include "core/hle/kernel/svc_common.h"
+#include "core/hle/kernel/svc_types.h"
+#include "core/memory.h"
+
+namespace {
+
+constexpr std::array<std::array<Qt::GlobalColor, 2>, 10> WaitTreeColors{{
+ {Qt::GlobalColor::darkGreen, Qt::GlobalColor::green},
+ {Qt::GlobalColor::darkBlue, Qt::GlobalColor::cyan},
+ {Qt::GlobalColor::lightGray, Qt::GlobalColor::lightGray},
+ {Qt::GlobalColor::lightGray, Qt::GlobalColor::lightGray},
+ {Qt::GlobalColor::darkRed, Qt::GlobalColor::red},
+ {Qt::GlobalColor::darkYellow, Qt::GlobalColor::yellow},
+ {Qt::GlobalColor::red, Qt::GlobalColor::red},
+ {Qt::GlobalColor::darkCyan, Qt::GlobalColor::cyan},
+ {Qt::GlobalColor::gray, Qt::GlobalColor::gray},
+}};
+
+bool IsDarkTheme() {
+ const auto& theme = UISettings::values.theme;
+ return theme == std::string("qdarkstyle") || theme == std::string("qdarkstyle_midnight_blue") ||
+ theme == std::string("colorful_dark") || theme == std::string("colorful_midnight_blue");
+}
+
+} // namespace
+
+WaitTreeItem::WaitTreeItem() = default;
+WaitTreeItem::~WaitTreeItem() = default;
+
+QColor WaitTreeItem::GetColor() const {
+ if (IsDarkTheme()) {
+ return QColor(Qt::GlobalColor::white);
+ } else {
+ return QColor(Qt::GlobalColor::black);
+ }
+}
+
+std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeItem::GetChildren() const {
+ return {};
+}
+
+void WaitTreeItem::Expand() {
+ if (IsExpandable() && !expanded) {
+ children = GetChildren();
+ for (std::size_t i = 0; i < children.size(); ++i) {
+ children[i]->parent = this;
+ children[i]->row = i;
+ }
+ expanded = true;
+ }
+}
+
+WaitTreeItem* WaitTreeItem::Parent() const {
+ return parent;
+}
+
+const std::vector<std::unique_ptr<WaitTreeItem>>& WaitTreeItem::Children() const {
+ return children;
+}
+
+bool WaitTreeItem::IsExpandable() const {
+ return false;
+}
+
+std::size_t WaitTreeItem::Row() const {
+ return row;
+}
+
+std::vector<std::unique_ptr<WaitTreeThread>> WaitTreeItem::MakeThreadItemList(
+ Core::System& system) {
+ std::vector<std::unique_ptr<WaitTreeThread>> item_list;
+ std::size_t row = 0;
+ auto add_threads = [&](const std::vector<Kernel::KThread*>& threads) {
+ for (std::size_t i = 0; i < threads.size(); ++i) {
+ if (threads[i]->GetThreadType() == Kernel::ThreadType::User) {
+ item_list.push_back(std::make_unique<WaitTreeThread>(*threads[i], system));
+ item_list.back()->row = row;
+ }
+ ++row;
+ }
+ };
+
+ add_threads(system.GlobalSchedulerContext().GetThreadList());
+
+ return item_list;
+}
+
+WaitTreeText::WaitTreeText(QString t) : text(std::move(t)) {}
+WaitTreeText::~WaitTreeText() = default;
+
+QString WaitTreeText::GetText() const {
+ return text;
+}
+
+WaitTreeCallstack::WaitTreeCallstack(const Kernel::KThread& thread_, Core::System& system_)
+ : thread{thread_}, system{system_} {}
+WaitTreeCallstack::~WaitTreeCallstack() = default;
+
+QString WaitTreeCallstack::GetText() const {
+ return tr("Call stack");
+}
+
+std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeCallstack::GetChildren() const {
+ std::vector<std::unique_ptr<WaitTreeItem>> list;
+
+ if (thread.GetThreadType() != Kernel::ThreadType::User) {
+ return list;
+ }
+
+ if (thread.GetOwnerProcess() == nullptr || !thread.GetOwnerProcess()->Is64Bit()) {
+ return list;
+ }
+
+ auto backtrace = Core::GetBacktraceFromContext(thread.GetOwnerProcess(), thread.GetContext());
+
+ for (auto& entry : backtrace) {
+ std::string s = fmt::format("{:20}{:016X} {:016X} {:016X} {}", entry.module, entry.address,
+ entry.original_address, entry.offset, entry.name);
+ list.push_back(std::make_unique<WaitTreeText>(QString::fromStdString(s)));
+ }
+
+ return list;
+}
+
+WaitTreeSynchronizationObject::WaitTreeSynchronizationObject(
+ const Kernel::KSynchronizationObject& object_, Core::System& system_)
+ : object{object_}, system{system_} {}
+WaitTreeSynchronizationObject::~WaitTreeSynchronizationObject() = default;
+
+WaitTreeExpandableItem::WaitTreeExpandableItem() = default;
+WaitTreeExpandableItem::~WaitTreeExpandableItem() = default;
+
+bool WaitTreeExpandableItem::IsExpandable() const {
+ return true;
+}
+
+QString WaitTreeSynchronizationObject::GetText() const {
+ return tr("[%1] %2")
+ .arg(object.GetId())
+ .arg(QString::fromStdString(object.GetTypeObj().GetName()));
+}
+
+std::unique_ptr<WaitTreeSynchronizationObject> WaitTreeSynchronizationObject::make(
+ const Kernel::KSynchronizationObject& object, Core::System& system) {
+ const auto type =
+ static_cast<Kernel::KClassTokenGenerator::ObjectType>(object.GetTypeObj().GetClassToken());
+ switch (type) {
+ case Kernel::KClassTokenGenerator::ObjectType::KReadableEvent:
+ return std::make_unique<WaitTreeEvent>(static_cast<const Kernel::KReadableEvent&>(object),
+ system);
+ case Kernel::KClassTokenGenerator::ObjectType::KThread:
+ return std::make_unique<WaitTreeThread>(static_cast<const Kernel::KThread&>(object),
+ system);
+ default:
+ return std::make_unique<WaitTreeSynchronizationObject>(object, system);
+ }
+}
+
+std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeSynchronizationObject::GetChildren() const {
+ std::vector<std::unique_ptr<WaitTreeItem>> list;
+
+ auto threads = object.GetWaitingThreadsForDebugging();
+ if (threads.empty()) {
+ list.push_back(std::make_unique<WaitTreeText>(tr("waited by no thread")));
+ } else {
+ list.push_back(std::make_unique<WaitTreeThreadList>(std::move(threads), system));
+ }
+
+ return list;
+}
+
+WaitTreeThread::WaitTreeThread(const Kernel::KThread& thread, Core::System& system_)
+ : WaitTreeSynchronizationObject(thread, system_), system{system_} {}
+WaitTreeThread::~WaitTreeThread() = default;
+
+QString WaitTreeThread::GetText() const {
+ const auto& thread = static_cast<const Kernel::KThread&>(object);
+ QString status;
+ switch (thread.GetState()) {
+ case Kernel::ThreadState::Runnable:
+ if (!thread.IsSuspended()) {
+ status = tr("runnable");
+ } else {
+ status = tr("paused");
+ }
+ break;
+ case Kernel::ThreadState::Waiting:
+ switch (thread.GetWaitReasonForDebugging()) {
+ case Kernel::ThreadWaitReasonForDebugging::Sleep:
+ status = tr("sleeping");
+ break;
+ case Kernel::ThreadWaitReasonForDebugging::IPC:
+ status = tr("waiting for IPC reply");
+ break;
+ case Kernel::ThreadWaitReasonForDebugging::Synchronization:
+ status = tr("waiting for objects");
+ break;
+ case Kernel::ThreadWaitReasonForDebugging::ConditionVar:
+ status = tr("waiting for condition variable");
+ break;
+ case Kernel::ThreadWaitReasonForDebugging::Arbitration:
+ status = tr("waiting for address arbiter");
+ break;
+ case Kernel::ThreadWaitReasonForDebugging::Suspended:
+ status = tr("waiting for suspend resume");
+ break;
+ default:
+ status = tr("waiting");
+ break;
+ }
+ break;
+ case Kernel::ThreadState::Initialized:
+ status = tr("initialized");
+ break;
+ case Kernel::ThreadState::Terminated:
+ status = tr("terminated");
+ break;
+ default:
+ status = tr("unknown");
+ break;
+ }
+
+ const auto& context = thread.GetContext();
+ const QString pc_info = tr(" PC = 0x%1 LR = 0x%2")
+ .arg(context.pc, 8, 16, QLatin1Char{'0'})
+ .arg(context.lr, 8, 16, QLatin1Char{'0'});
+ return QStringLiteral("%1%2 (%3) ")
+ .arg(WaitTreeSynchronizationObject::GetText(), pc_info, status);
+}
+
+QColor WaitTreeThread::GetColor() const {
+ const std::size_t color_index = IsDarkTheme() ? 1 : 0;
+
+ const auto& thread = static_cast<const Kernel::KThread&>(object);
+ switch (thread.GetState()) {
+ case Kernel::ThreadState::Runnable:
+ if (!thread.IsSuspended()) {
+ return QColor(WaitTreeColors[0][color_index]);
+ } else {
+ return QColor(WaitTreeColors[2][color_index]);
+ }
+ case Kernel::ThreadState::Waiting:
+ switch (thread.GetWaitReasonForDebugging()) {
+ case Kernel::ThreadWaitReasonForDebugging::IPC:
+ return QColor(WaitTreeColors[4][color_index]);
+ case Kernel::ThreadWaitReasonForDebugging::Sleep:
+ return QColor(WaitTreeColors[5][color_index]);
+ case Kernel::ThreadWaitReasonForDebugging::Synchronization:
+ case Kernel::ThreadWaitReasonForDebugging::ConditionVar:
+ case Kernel::ThreadWaitReasonForDebugging::Arbitration:
+ case Kernel::ThreadWaitReasonForDebugging::Suspended:
+ return QColor(WaitTreeColors[6][color_index]);
+ break;
+ default:
+ return QColor(WaitTreeColors[3][color_index]);
+ }
+ case Kernel::ThreadState::Initialized:
+ return QColor(WaitTreeColors[7][color_index]);
+ case Kernel::ThreadState::Terminated:
+ return QColor(WaitTreeColors[8][color_index]);
+ default:
+ return WaitTreeItem::GetColor();
+ }
+}
+
+std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeThread::GetChildren() const {
+ std::vector<std::unique_ptr<WaitTreeItem>> list(WaitTreeSynchronizationObject::GetChildren());
+
+ const auto& thread = static_cast<const Kernel::KThread&>(object);
+
+ QString processor;
+ switch (thread.GetActiveCore()) {
+ case Kernel::Svc::IdealCoreUseProcessValue:
+ processor = tr("ideal");
+ break;
+ default:
+ processor = tr("core %1").arg(thread.GetActiveCore());
+ break;
+ }
+
+ list.push_back(std::make_unique<WaitTreeText>(tr("processor = %1").arg(processor)));
+ list.push_back(std::make_unique<WaitTreeText>(
+ tr("affinity mask = %1").arg(thread.GetAffinityMask().GetAffinityMask())));
+ list.push_back(std::make_unique<WaitTreeText>(tr("thread id = %1").arg(thread.GetThreadId())));
+ list.push_back(std::make_unique<WaitTreeText>(tr("priority = %1(current) / %2(normal)")
+ .arg(thread.GetPriority())
+ .arg(thread.GetBasePriority())));
+ list.push_back(std::make_unique<WaitTreeText>(
+ tr("last running ticks = %1").arg(thread.GetLastScheduledTick())));
+
+ list.push_back(std::make_unique<WaitTreeCallstack>(thread, system));
+
+ return list;
+}
+
+WaitTreeEvent::WaitTreeEvent(const Kernel::KReadableEvent& object_, Core::System& system_)
+ : WaitTreeSynchronizationObject(object_, system_) {}
+WaitTreeEvent::~WaitTreeEvent() = default;
+
+WaitTreeThreadList::WaitTreeThreadList(std::vector<Kernel::KThread*>&& list, Core::System& system_)
+ : thread_list(std::move(list)), system{system_} {}
+WaitTreeThreadList::~WaitTreeThreadList() = default;
+
+QString WaitTreeThreadList::GetText() const {
+ return tr("waited by thread");
+}
+
+std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeThreadList::GetChildren() const {
+ std::vector<std::unique_ptr<WaitTreeItem>> list(thread_list.size());
+ std::transform(thread_list.begin(), thread_list.end(), list.begin(),
+ [this](const auto& t) { return std::make_unique<WaitTreeThread>(*t, system); });
+ return list;
+}
+
+WaitTreeModel::WaitTreeModel(Core::System& system_, QObject* parent)
+ : QAbstractItemModel(parent), system{system_} {}
+WaitTreeModel::~WaitTreeModel() = default;
+
+QModelIndex WaitTreeModel::index(int row, int column, const QModelIndex& parent) const {
+ if (!hasIndex(row, column, parent))
+ return {};
+
+ if (parent.isValid()) {
+ WaitTreeItem* parent_item = static_cast<WaitTreeItem*>(parent.internalPointer());
+ parent_item->Expand();
+ return createIndex(row, column, parent_item->Children()[row].get());
+ }
+
+ return createIndex(row, column, thread_items[row].get());
+}
+
+QModelIndex WaitTreeModel::parent(const QModelIndex& index) const {
+ if (!index.isValid())
+ return {};
+
+ WaitTreeItem* parent_item = static_cast<WaitTreeItem*>(index.internalPointer())->Parent();
+ if (!parent_item) {
+ return QModelIndex();
+ }
+ return createIndex(static_cast<int>(parent_item->Row()), 0, parent_item);
+}
+
+int WaitTreeModel::rowCount(const QModelIndex& parent) const {
+ if (!parent.isValid())
+ return static_cast<int>(thread_items.size());
+
+ WaitTreeItem* parent_item = static_cast<WaitTreeItem*>(parent.internalPointer());
+ parent_item->Expand();
+ return static_cast<int>(parent_item->Children().size());
+}
+
+int WaitTreeModel::columnCount(const QModelIndex&) const {
+ return 1;
+}
+
+QVariant WaitTreeModel::data(const QModelIndex& index, int role) const {
+ if (!index.isValid())
+ return {};
+
+ switch (role) {
+ case Qt::DisplayRole:
+ return static_cast<WaitTreeItem*>(index.internalPointer())->GetText();
+ case Qt::ForegroundRole:
+ return static_cast<WaitTreeItem*>(index.internalPointer())->GetColor();
+ default:
+ return {};
+ }
+}
+
+void WaitTreeModel::ClearItems() {
+ thread_items.clear();
+}
+
+void WaitTreeModel::InitItems() {
+ thread_items = WaitTreeItem::MakeThreadItemList(system);
+}
+
+WaitTreeWidget::WaitTreeWidget(Core::System& system_, QWidget* parent)
+ : QDockWidget(tr("&Wait Tree"), parent), system{system_} {
+ setObjectName(QStringLiteral("WaitTreeWidget"));
+ view = new QTreeView(this);
+ view->setHeaderHidden(true);
+ setWidget(view);
+ setEnabled(false);
+}
+
+WaitTreeWidget::~WaitTreeWidget() = default;
+
+void WaitTreeWidget::OnDebugModeEntered() {
+ if (!system.IsPoweredOn())
+ return;
+ model->InitItems();
+ view->setModel(model);
+ setEnabled(true);
+}
+
+void WaitTreeWidget::OnDebugModeLeft() {
+ setEnabled(false);
+ view->setModel(nullptr);
+ model->ClearItems();
+}
+
+void WaitTreeWidget::OnEmulationStarting(EmuThread* emu_thread) {
+ model = new WaitTreeModel(system, this);
+ view->setModel(model);
+ setEnabled(false);
+}
+
+void WaitTreeWidget::OnEmulationStopping() {
+ view->setModel(nullptr);
+ delete model;
+ setEnabled(false);
+}
diff --git a/src/citron/debugger/wait_tree.h b/src/citron/debugger/wait_tree.h
new file mode 100644
index 000000000..23c329fbe
--- /dev/null
+++ b/src/citron/debugger/wait_tree.h
@@ -0,0 +1,188 @@
+// SPDX-FileCopyrightText: 2016 Citra Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+#include <QDockWidget>
+#include <QTreeView>
+
+#include "common/common_types.h"
+#include "core/hle/kernel/k_auto_object.h"
+#include "core/hle/kernel/svc_common.h"
+
+class EmuThread;
+
+namespace Core {
+class System;
+}
+
+namespace Kernel {
+class KHandleTable;
+class KReadableEvent;
+class KSynchronizationObject;
+class KThread;
+} // namespace Kernel
+
+class WaitTreeThread;
+
+class WaitTreeItem : public QObject {
+ Q_OBJECT
+public:
+ WaitTreeItem();
+ ~WaitTreeItem() override;
+
+ virtual bool IsExpandable() const;
+ virtual std::vector<std::unique_ptr<WaitTreeItem>> GetChildren() const;
+ virtual QString GetText() const = 0;
+ virtual QColor GetColor() const;
+
+ void Expand();
+ WaitTreeItem* Parent() const;
+ const std::vector<std::unique_ptr<WaitTreeItem>>& Children() const;
+ std::size_t Row() const;
+ static std::vector<std::unique_ptr<WaitTreeThread>> MakeThreadItemList(Core::System& system);
+
+private:
+ std::size_t row;
+ bool expanded = false;
+ WaitTreeItem* parent = nullptr;
+ std::vector<std::unique_ptr<WaitTreeItem>> children;
+};
+
+class WaitTreeText : public WaitTreeItem {
+ Q_OBJECT
+public:
+ explicit WaitTreeText(QString text);
+ ~WaitTreeText() override;
+
+ QString GetText() const override;
+
+private:
+ QString text;
+};
+
+class WaitTreeExpandableItem : public WaitTreeItem {
+ Q_OBJECT
+public:
+ WaitTreeExpandableItem();
+ ~WaitTreeExpandableItem() override;
+
+ bool IsExpandable() const override;
+};
+
+class WaitTreeCallstack : public WaitTreeExpandableItem {
+ Q_OBJECT
+public:
+ explicit WaitTreeCallstack(const Kernel::KThread& thread_, Core::System& system_);
+ ~WaitTreeCallstack() override;
+
+ QString GetText() const override;
+ std::vector<std::unique_ptr<WaitTreeItem>> GetChildren() const override;
+
+private:
+ const Kernel::KThread& thread;
+
+ Core::System& system;
+};
+
+class WaitTreeSynchronizationObject : public WaitTreeExpandableItem {
+ Q_OBJECT
+public:
+ explicit WaitTreeSynchronizationObject(const Kernel::KSynchronizationObject& object_,
+ Core::System& system_);
+ ~WaitTreeSynchronizationObject() override;
+
+ static std::unique_ptr<WaitTreeSynchronizationObject> make(
+ const Kernel::KSynchronizationObject& object, Core::System& system);
+ QString GetText() const override;
+ std::vector<std::unique_ptr<WaitTreeItem>> GetChildren() const override;
+
+protected:
+ const Kernel::KSynchronizationObject& object;
+
+private:
+ Core::System& system;
+};
+
+class WaitTreeThread : public WaitTreeSynchronizationObject {
+ Q_OBJECT
+public:
+ explicit WaitTreeThread(const Kernel::KThread& thread, Core::System& system_);
+ ~WaitTreeThread() override;
+
+ QString GetText() const override;
+ QColor GetColor() const override;
+ std::vector<std::unique_ptr<WaitTreeItem>> GetChildren() const override;
+
+private:
+ Core::System& system;
+};
+
+class WaitTreeEvent : public WaitTreeSynchronizationObject {
+ Q_OBJECT
+public:
+ explicit WaitTreeEvent(const Kernel::KReadableEvent& object_, Core::System& system_);
+ ~WaitTreeEvent() override;
+};
+
+class WaitTreeThreadList : public WaitTreeExpandableItem {
+ Q_OBJECT
+public:
+ explicit WaitTreeThreadList(std::vector<Kernel::KThread*>&& list, Core::System& system_);
+ ~WaitTreeThreadList() override;
+
+ QString GetText() const override;
+ std::vector<std::unique_ptr<WaitTreeItem>> GetChildren() const override;
+
+private:
+ std::vector<Kernel::KThread*> thread_list;
+
+ Core::System& system;
+};
+
+class WaitTreeModel : public QAbstractItemModel {
+ Q_OBJECT
+
+public:
+ explicit WaitTreeModel(Core::System& system_, QObject* parent = nullptr);
+ ~WaitTreeModel() override;
+
+ QVariant data(const QModelIndex& index, int role) const override;
+ QModelIndex index(int row, int column, const QModelIndex& parent) const override;
+ QModelIndex parent(const QModelIndex& index) const override;
+ int rowCount(const QModelIndex& parent) const override;
+ int columnCount(const QModelIndex& parent) const override;
+
+ void ClearItems();
+ void InitItems();
+
+private:
+ std::vector<std::unique_ptr<WaitTreeThread>> thread_items;
+
+ Core::System& system;
+};
+
+class WaitTreeWidget : public QDockWidget {
+ Q_OBJECT
+
+public:
+ explicit WaitTreeWidget(Core::System& system_, QWidget* parent = nullptr);
+ ~WaitTreeWidget() override;
+
+public slots:
+ void OnDebugModeEntered();
+ void OnDebugModeLeft();
+
+ void OnEmulationStarting(EmuThread* emu_thread);
+ void OnEmulationStopping();
+
+private:
+ QTreeView* view;
+ WaitTreeModel* model;
+
+ Core::System& system;
+};