summaryrefslogtreecommitdiff
path: root/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp
diff options
context:
space:
mode:
authorAdmiral H. Curtiss <pikachu025@gmail.com>2024-03-02 14:13:54 +0100
committerGitHub <noreply@github.com>2024-03-02 14:13:54 +0100
commitf2e04e06030f4d9e3d120ba90d846600fb2897ce (patch)
treed43b588825360ecbd7e7366ba6e23e43cc3648e5 /Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp
parent8840f7f8bc218846ca5950b59da59997b5784d3a (diff)
parent8134c8a57211f54cf2109ae3c068c0c94370f786 (diff)
Merge pull request #12359 from mitaclaw/code-diff-dialog-refresh
BranchWatchDialog: A Total Replacement for CodeDiffDialog
Diffstat (limited to 'Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp')
-rw-r--r--Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp1014
1 files changed, 1014 insertions, 0 deletions
diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp
new file mode 100644
index 0000000000..f5e6a5acc3
--- /dev/null
+++ b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp
@@ -0,0 +1,1014 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "DolphinQt/Debugger/BranchWatchDialog.h"
+
+#include <algorithm>
+#include <optional>
+#include <utility>
+
+#include <QApplication>
+#include <QCheckBox>
+#include <QClipboard>
+#include <QGridLayout>
+#include <QGroupBox>
+#include <QHeaderView>
+#include <QLineEdit>
+#include <QMenu>
+#include <QMenuBar>
+#include <QPushButton>
+#include <QShortcut>
+#include <QSortFilterProxyModel>
+#include <QStatusBar>
+#include <QString>
+#include <QTableView>
+#include <QTimer>
+#include <QToolBar>
+#include <QVBoxLayout>
+#include <QVariant>
+#include <fmt/format.h>
+
+#include "Common/Assert.h"
+#include "Common/CommonFuncs.h"
+#include "Common/CommonTypes.h"
+#include "Common/FileUtil.h"
+#include "Common/IOFile.h"
+#include "Core/ConfigManager.h"
+#include "Core/Core.h"
+#include "Core/Debugger/BranchWatch.h"
+#include "Core/Debugger/PPCDebugInterface.h"
+#include "Core/PowerPC/Gekko.h"
+#include "Core/PowerPC/PowerPC.h"
+#include "Core/System.h"
+#include "DolphinQt/Debugger/BranchWatchTableModel.h"
+#include "DolphinQt/Debugger/CodeWidget.h"
+#include "DolphinQt/QtUtils/DolphinFileDialog.h"
+#include "DolphinQt/QtUtils/ModalMessageBox.h"
+#include "DolphinQt/QtUtils/SetWindowDecorations.h"
+#include "DolphinQt/Settings.h"
+
+class BranchWatchProxyModel final : public QSortFilterProxyModel
+{
+ friend BranchWatchDialog;
+
+public:
+ explicit BranchWatchProxyModel(const Core::BranchWatch& branch_watch, QObject* parent = nullptr)
+ : QSortFilterProxyModel(parent), m_branch_watch(branch_watch)
+ {
+ }
+
+ BranchWatchTableModel* sourceModel() const
+ {
+ return static_cast<BranchWatchTableModel*>(QSortFilterProxyModel::sourceModel());
+ }
+ void setSourceModel(BranchWatchTableModel* source_model)
+ {
+ QSortFilterProxyModel::setSourceModel(source_model);
+ }
+
+ // Virtual setSourceModel is forbidden for type-safety reasons. See sourceModel().
+ [[noreturn]] void setSourceModel(QAbstractItemModel* source_model) override { Crash(); }
+ bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
+
+ template <bool BranchWatchProxyModel::*member>
+ void OnToggled(bool enabled)
+ {
+ this->*member = enabled;
+ invalidateRowsFilter();
+ }
+ template <QString BranchWatchProxyModel::*member>
+ void OnSymbolTextChanged(const QString& text)
+ {
+ this->*member = text;
+ invalidateRowsFilter();
+ }
+ template <std::optional<u32> BranchWatchProxyModel::*member>
+ void OnAddressTextChanged(const QString& text)
+ {
+ bool ok = false;
+ if (const u32 value = text.toUInt(&ok, 16); ok)
+ this->*member = value;
+ else
+ this->*member = std::nullopt;
+ invalidateRowsFilter();
+ }
+ void OnDelete(QModelIndexList index_list);
+
+ bool IsBranchTypeAllowed(UGeckoInstruction inst) const;
+ void SetInspected(const QModelIndex& index);
+
+private:
+ const Core::BranchWatch& m_branch_watch;
+
+ QString m_origin_symbol_name = {}, m_destin_symbol_name = {};
+ std::optional<u32> m_origin_min, m_origin_max, m_destin_min, m_destin_max;
+ bool m_b = {}, m_bl = {}, m_bc = {}, m_bcl = {}, m_blr = {}, m_blrl = {}, m_bclr = {},
+ m_bclrl = {}, m_bctr = {}, m_bctrl = {}, m_bcctr = {}, m_bcctrl = {};
+ bool m_cond_true = {}, m_cond_false = {};
+};
+
+bool BranchWatchProxyModel::filterAcceptsRow(int source_row, const QModelIndex&) const
+{
+ const Core::BranchWatch::Selection::value_type& value = m_branch_watch.GetSelection()[source_row];
+ if (value.condition)
+ {
+ if (!m_cond_true)
+ return false;
+ }
+ else if (!m_cond_false)
+ return false;
+
+ const Core::BranchWatchCollectionKey& k = value.collection_ptr->first;
+ if (!IsBranchTypeAllowed(k.original_inst))
+ return false;
+
+ if (m_origin_min.has_value() && k.origin_addr < m_origin_min.value())
+ return false;
+ if (m_origin_max.has_value() && k.origin_addr > m_origin_max.value())
+ return false;
+ if (m_destin_min.has_value() && k.destin_addr < m_destin_min.value())
+ return false;
+ if (m_destin_max.has_value() && k.destin_addr > m_destin_max.value())
+ return false;
+
+ if (!m_origin_symbol_name.isEmpty())
+ {
+ if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].origin_name;
+ !symbol_name_v.isValid() ||
+ !symbol_name_v.value<QString>().contains(m_origin_symbol_name, Qt::CaseInsensitive))
+ return false;
+ }
+ if (!m_destin_symbol_name.isEmpty())
+ {
+ if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].destin_name;
+ !symbol_name_v.isValid() ||
+ !symbol_name_v.value<QString>().contains(m_destin_symbol_name, Qt::CaseInsensitive))
+ return false;
+ }
+ return true;
+}
+
+void BranchWatchProxyModel::OnDelete(QModelIndexList index_list)
+{
+ std::transform(index_list.begin(), index_list.end(), index_list.begin(),
+ [this](const QModelIndex& index) { return mapToSource(index); });
+ sourceModel()->OnDelete(std::move(index_list));
+}
+
+static constexpr bool BranchSavesLR(UGeckoInstruction inst)
+{
+ DEBUG_ASSERT(inst.OPCD == 18 || inst.OPCD == 16 ||
+ (inst.OPCD == 19 && (inst.SUBOP10 == 16 || inst.SUBOP10 == 528)));
+ // Every branch instruction uses the same LK field.
+ return inst.LK;
+}
+
+bool BranchWatchProxyModel::IsBranchTypeAllowed(UGeckoInstruction inst) const
+{
+ const bool lr_saved = BranchSavesLR(inst);
+ switch (inst.OPCD)
+ {
+ case 18:
+ return lr_saved ? m_bl : m_b;
+ case 16:
+ return lr_saved ? m_bcl : m_bc;
+ case 19:
+ switch (inst.SUBOP10)
+ {
+ case 16:
+ if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always
+ return lr_saved ? m_blrl : m_blr;
+ return lr_saved ? m_bclrl : m_bclr;
+ case 528:
+ if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always
+ return lr_saved ? m_bctrl : m_bctr;
+ return lr_saved ? m_bcctrl : m_bcctr;
+ }
+ }
+ return false;
+}
+
+void BranchWatchProxyModel::SetInspected(const QModelIndex& index)
+{
+ sourceModel()->SetInspected(mapToSource(index));
+}
+
+BranchWatchDialog::BranchWatchDialog(Core::System& system, Core::BranchWatch& branch_watch,
+ CodeWidget* code_widget, QWidget* parent)
+ : QDialog(parent), m_system(system), m_branch_watch(branch_watch), m_code_widget(code_widget)
+{
+ setWindowTitle(tr("Branch Watch Tool"));
+ setWindowFlags((windowFlags() | Qt::WindowMinMaxButtonsHint) & ~Qt::WindowContextHelpButtonHint);
+ SetQWidgetWindowDecorations(this);
+ setLayout([this]() {
+ auto* layout = new QVBoxLayout;
+
+ // Controls Toolbar (widgets are added later)
+ layout->addWidget(m_control_toolbar = new QToolBar);
+
+ // Branch Watch Table
+ layout->addWidget(m_table_view = [this]() {
+ const auto& ui_settings = Settings::Instance();
+
+ m_table_proxy = new BranchWatchProxyModel(m_branch_watch);
+ m_table_proxy->setSourceModel(m_table_model =
+ new BranchWatchTableModel(m_system, m_branch_watch));
+ m_table_proxy->setSortRole(UserRole::SortRole);
+
+ m_table_model->setFont(ui_settings.GetDebugFont());
+ connect(&ui_settings, &Settings::DebugFontChanged, m_table_model,
+ &BranchWatchTableModel::setFont);
+
+ auto* const table_view = new QTableView;
+ table_view->setModel(m_table_proxy);
+ table_view->setSortingEnabled(true);
+ table_view->sortByColumn(Column::Origin, Qt::AscendingOrder);
+ table_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ table_view->setSelectionBehavior(QAbstractItemView::SelectRows);
+ table_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ table_view->setContextMenuPolicy(Qt::CustomContextMenu);
+ table_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ table_view->setCornerButtonEnabled(false);
+ table_view->verticalHeader()->hide();
+
+ QHeaderView* const horizontal_header = table_view->horizontalHeader();
+ horizontal_header->restoreState( // Restore column visibility state.
+ Settings::GetQSettings()
+ .value(QStringLiteral("branchwatchdialog/tableheader/state"))
+ .toByteArray());
+ horizontal_header->setContextMenuPolicy(Qt::CustomContextMenu);
+ horizontal_header->setStretchLastSection(true);
+ horizontal_header->setSectionsMovable(true);
+ horizontal_header->setFirstSectionMovable(true);
+
+ connect(table_view, &QTableView::clicked, this, &BranchWatchDialog::OnTableClicked);
+ connect(table_view, &QTableView::customContextMenuRequested, this,
+ &BranchWatchDialog::OnTableContextMenu);
+ connect(horizontal_header, &QHeaderView::customContextMenuRequested, this,
+ &BranchWatchDialog::OnTableHeaderContextMenu);
+ connect(new QShortcut(QKeySequence(Qt::Key_Delete), this), &QShortcut::activated, this,
+ &BranchWatchDialog::OnTableDeleteKeypress);
+
+ return table_view;
+ }());
+
+ m_mnu_column_visibility = [this]() {
+ static constexpr std::array<const char*, Column::NumberOfColumns> headers = {
+ QT_TR_NOOP("Instruction"), QT_TR_NOOP("Condition"), QT_TR_NOOP("Origin"),
+ QT_TR_NOOP("Destination"), QT_TR_NOOP("Recent Hits"), QT_TR_NOOP("Total Hits"),
+ QT_TR_NOOP("Origin Symbol"), QT_TR_NOOP("Destination Symbol")};
+
+ auto* const menu = new QMenu();
+ for (int column = 0; column < Column::NumberOfColumns; ++column)
+ {
+ QAction* action = menu->addAction(tr(headers[column]), [this, column](bool enabled) {
+ m_table_view->setColumnHidden(column, !enabled);
+ });
+ action->setChecked(!m_table_view->isColumnHidden(column));
+ action->setCheckable(true);
+ }
+ return menu;
+ }();
+
+ // Menu Bar
+ layout->setMenuBar([this]() {
+ QMenuBar* const menu_bar = new QMenuBar;
+ menu_bar->setNativeMenuBar(false);
+
+ QMenu* const menu_file = new QMenu(tr("&File"), menu_bar);
+ menu_file->addAction(tr("&Save Branch Watch"), this, &BranchWatchDialog::OnSave);
+ menu_file->addAction(tr("Save Branch Watch &As..."), this, &BranchWatchDialog::OnSaveAs);
+ menu_file->addAction(tr("&Load Branch Watch"), this, &BranchWatchDialog::OnLoad);
+ menu_file->addAction(tr("Load Branch Watch &From..."), this, &BranchWatchDialog::OnLoadFrom);
+ m_act_autosave = menu_file->addAction(tr("A&uto Save"));
+ m_act_autosave->setCheckable(true);
+ connect(m_act_autosave, &QAction::toggled, this, &BranchWatchDialog::OnToggleAutoSave);
+ menu_bar->addMenu(menu_file);
+
+ QMenu* const menu_tool = new QMenu(tr("&Tool"), menu_bar);
+ menu_tool->setToolTipsVisible(true);
+ menu_tool->addAction(tr("Hide &Controls"), this, &BranchWatchDialog::OnHideShowControls)
+ ->setCheckable(true);
+ QAction* const act_ignore_apploader =
+ menu_tool->addAction(tr("Ignore &Apploader Branch Hits"));
+ act_ignore_apploader->setToolTip(
+ tr("This only applies to the initial boot of the emulated software."));
+ act_ignore_apploader->setChecked(m_system.IsBranchWatchIgnoreApploader());
+ act_ignore_apploader->setCheckable(true);
+ connect(act_ignore_apploader, &QAction::toggled, this,
+ &BranchWatchDialog::OnToggleIgnoreApploader);
+
+ menu_tool->addMenu(m_mnu_column_visibility)->setText(tr("Column &Visibility"));
+ menu_tool->addAction(tr("Wipe &Inspection Data"), this, &BranchWatchDialog::OnWipeInspection);
+ menu_tool->addAction(tr("&Help"), this, &BranchWatchDialog::OnHelp);
+
+ menu_bar->addMenu(menu_tool);
+
+ return menu_bar;
+ }());
+
+ // Status Bar
+ layout->addWidget(m_status_bar = []() {
+ auto* const status_bar = new QStatusBar;
+ status_bar->setSizeGripEnabled(false);
+ return status_bar;
+ }());
+
+ // Tool Controls
+ m_control_toolbar->addWidget([this]() {
+ auto* const layout = new QGridLayout;
+
+ layout->addWidget(m_btn_start_pause = new QPushButton(tr("Start Branch Watch")), 0, 0);
+ connect(m_btn_start_pause, &QPushButton::toggled, this, &BranchWatchDialog::OnStartPause);
+ m_btn_start_pause->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+ m_btn_start_pause->setCheckable(true);
+
+ layout->addWidget(m_btn_clear_watch = new QPushButton(tr("Clear Branch Watch")), 1, 0);
+ connect(m_btn_clear_watch, &QPushButton::pressed, this,
+ &BranchWatchDialog::OnClearBranchWatch);
+ m_btn_clear_watch->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+ layout->addWidget(m_btn_path_was_taken = new QPushButton(tr("Code Path Was Taken")), 0, 1);
+ connect(m_btn_path_was_taken, &QPushButton::pressed, this,
+ &BranchWatchDialog::OnCodePathWasTaken);
+ m_btn_path_was_taken->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+ layout->addWidget(m_btn_path_not_taken = new QPushButton(tr("Code Path Not Taken")), 1, 1);
+ connect(m_btn_path_not_taken, &QPushButton::pressed, this,
+ &BranchWatchDialog::OnCodePathNotTaken);
+ m_btn_path_not_taken->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+ auto* const group_box = new QGroupBox(tr("Tool Controls"));
+ group_box->setLayout(layout);
+ group_box->setAlignment(Qt::AlignHCenter);
+
+ return group_box;
+ }());
+
+ // Spacer
+ m_control_toolbar->addWidget([]() {
+ auto* const widget = new QWidget;
+ widget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
+ return widget;
+ }());
+
+ // Branch Type Filter Options
+ m_control_toolbar->addWidget([this]() {
+ auto* const layout = new QGridLayout;
+
+ const auto routine = [this, layout](const QString& text, const QString& tooltip, int row,
+ int column, void (BranchWatchProxyModel::*slot)(bool)) {
+ QCheckBox* const check_box = new QCheckBox(text);
+ check_box->setToolTip(tooltip);
+ layout->addWidget(check_box, row, column);
+ connect(check_box, &QCheckBox::toggled, [this, slot](bool checked) {
+ (m_table_proxy->*slot)(checked);
+ UpdateStatus();
+ });
+ check_box->setChecked(true);
+ };
+
+ // clang-format off
+ routine(QStringLiteral("b" ), tr("Branch" ), 0, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_b >);
+ routine(QStringLiteral("bl" ), tr("Branch (LR saved)" ), 0, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bl >);
+ routine(QStringLiteral("bc" ), tr("Branch Conditional" ), 0, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bc >);
+ routine(QStringLiteral("bcl" ), tr("Branch Conditional (LR saved)" ), 0, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcl >);
+ routine(QStringLiteral("blr" ), tr("Branch to Link Register" ), 1, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_blr >);
+ routine(QStringLiteral("blrl" ), tr("Branch to Link Register (LR saved)" ), 1, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_blrl >);
+ routine(QStringLiteral("bclr" ), tr("Branch Conditional to Link Register" ), 1, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bclr >);
+ routine(QStringLiteral("bclrl" ), tr("Branch Conditional to Link Register (LR saved)" ), 1, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bclrl >);
+ routine(QStringLiteral("bctr" ), tr("Branch to Count Register" ), 2, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bctr >);
+ routine(QStringLiteral("bctrl" ), tr("Branch to Count Register (LR saved)" ), 2, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bctrl >);
+ routine(QStringLiteral("bcctr" ), tr("Branch Conditional to Count Register" ), 2, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcctr >);
+ routine(QStringLiteral("bcctrl"), tr("Branch Conditional to Count Register (LR saved)"), 2, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcctrl>);
+ // clang-format on
+
+ auto* const group_box = new QGroupBox(tr("Branch Type"));
+ group_box->setLayout(layout);
+ group_box->setAlignment(Qt::AlignHCenter);
+
+ return group_box;
+ }());
+
+ // Origin and Destination Filter Options
+ m_control_toolbar->addWidget([this]() {
+ auto* const layout = new QGridLayout;
+
+ const auto routine = [this, layout](const QString& text, int row, int column, int width,
+ void (BranchWatchProxyModel::*slot)(const QString&)) {
+ QLineEdit* const line_edit = new QLineEdit;
+ layout->addWidget(line_edit, row, column, 1, width);
+ connect(line_edit, &QLineEdit::textChanged, [this, slot](const QString& text) {
+ (m_table_proxy->*slot)(text);
+ UpdateStatus();
+ });
+ line_edit->setPlaceholderText(text);
+ return line_edit;
+ };
+
+ // clang-format off
+ routine(tr("Origin Symbol" ), 0, 0, 1, &BranchWatchProxyModel::OnSymbolTextChanged<&BranchWatchProxyModel::m_origin_symbol_name>);
+ routine(tr("Origin Min" ), 1, 0, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_origin_min>)->setMaxLength(8);
+ routine(tr("Origin Max" ), 2, 0, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_origin_max>)->setMaxLength(8);
+ routine(tr("Destination Symbol"), 0, 1, 1, &BranchWatchProxyModel::OnSymbolTextChanged<&BranchWatchProxyModel::m_destin_symbol_name>);
+ routine(tr("Destination Min" ), 1, 1, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_destin_min>)->setMaxLength(8);
+ routine(tr("Destination Max" ), 2, 1, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_destin_max>)->setMaxLength(8);
+ // clang-format on
+
+ auto* const group_box = new QGroupBox(tr("Origin and Destination"));
+ group_box->setLayout(layout);
+ group_box->setAlignment(Qt::AlignHCenter);
+
+ return group_box;
+ }());
+
+ // Condition Filter Options
+ m_control_toolbar->addWidget([this]() {
+ auto* const layout = new QVBoxLayout;
+ layout->setAlignment(Qt::AlignHCenter);
+
+ const auto routine = [this, layout](const QString& text,
+ void (BranchWatchProxyModel::*slot)(bool)) {
+ QCheckBox* const check_box = new QCheckBox(text);
+ layout->addWidget(check_box);
+ connect(check_box, &QCheckBox::toggled, [this, slot](bool checked) {
+ (m_table_proxy->*slot)(checked);
+ UpdateStatus();
+ });
+ check_box->setChecked(true);
+ return check_box;
+ };
+
+ routine(QStringLiteral("true"),
+ &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_cond_true>)
+ ->setToolTip(tr("This will also filter unconditional branches.\n"
+ "To filter for or against unconditional branches,\n"
+ "use the Branch Type filter options."));
+ routine(QStringLiteral("false"),
+ &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_cond_false>);
+
+ auto* const group_box = new QGroupBox(tr("Condition"));
+ group_box->setLayout(layout);
+ group_box->setAlignment(Qt::AlignHCenter);
+
+ return group_box;
+ }());
+
+ // Misc. Controls
+ m_control_toolbar->addWidget([this]() {
+ auto* const layout = new QVBoxLayout;
+
+ layout->addWidget(m_btn_was_overwritten = new QPushButton(tr("Branch Was Overwritten")));
+ connect(m_btn_was_overwritten, &QPushButton::pressed, this,
+ &BranchWatchDialog::OnBranchWasOverwritten);
+ m_btn_was_overwritten->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+ layout->addWidget(m_btn_not_overwritten = new QPushButton(tr("Branch Not Overwritten")));
+ connect(m_btn_not_overwritten, &QPushButton::pressed, this,
+ &BranchWatchDialog::OnBranchNotOverwritten);
+ m_btn_not_overwritten->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+ layout->addWidget(m_btn_wipe_recent_hits = new QPushButton(tr("Wipe Recent Hits")));
+ connect(m_btn_wipe_recent_hits, &QPushButton::pressed, this,
+ &BranchWatchDialog::OnWipeRecentHits);
+ m_btn_wipe_recent_hits->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+ m_btn_wipe_recent_hits->setEnabled(false);
+
+ auto* const group_box = new QGroupBox(tr("Misc. Controls"));
+ group_box->setLayout(layout);
+ group_box->setAlignment(Qt::AlignHCenter);
+
+ return group_box;
+ }());
+
+ connect(m_timer = new QTimer, &QTimer::timeout, this, &BranchWatchDialog::OnTimeout);
+ connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
+ &BranchWatchDialog::OnEmulationStateChanged);
+ connect(m_table_proxy, &BranchWatchProxyModel::layoutChanged, this,
+ &BranchWatchDialog::UpdateStatus);
+
+ return layout;
+ }());
+
+ // FIXME: On Linux, Qt6 has recently been resetting column widths to their defaults in many
+ // unexpected ways. This affects all kinds of QTables in Dolphin's GUI, so to avoid it in
+ // this QTableView, I have deferred this operation. Any earlier, and this would be undone.
+ // SetQWidgetWindowDecorations was moved to before these operations for the same reason.
+ m_table_view->setColumnWidth(Column::Instruction, 50);
+ m_table_view->setColumnWidth(Column::Condition, 50);
+ m_table_view->setColumnWidth(Column::OriginSymbol, 250);
+ m_table_view->setColumnWidth(Column::DestinSymbol, 250);
+ // The default column width (100 units) is fine for the rest.
+
+ const auto& settings = Settings::GetQSettings();
+ restoreGeometry(settings.value(QStringLiteral("branchwatchdialog/geometry")).toByteArray());
+}
+
+void BranchWatchDialog::done(int r)
+{
+ if (m_timer->isActive())
+ m_timer->stop();
+ auto& settings = Settings::GetQSettings();
+ settings.setValue(QStringLiteral("branchwatchdialog/geometry"), saveGeometry());
+ settings.setValue(QStringLiteral("branchwatchdialog/tableheader/state"),
+ m_table_view->horizontalHeader()->saveState());
+ QDialog::done(r);
+}
+
+static constexpr int BRANCH_WATCH_TOOL_TIMER_DELAY_MS = 100;
+static constexpr int BRANCH_WATCH_TOOL_TIMER_PAUSE_ONESHOT_MS = 200;
+
+static bool TimerCondition(const Core::BranchWatch& branch_watch, Core::State state)
+{
+ return branch_watch.GetRecordingActive() && state > Core::State::Paused;
+}
+
+int BranchWatchDialog::exec()
+{
+ if (TimerCondition(m_branch_watch, Core::GetState()))
+ m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
+ return QDialog::exec();
+}
+
+void BranchWatchDialog::open()
+{
+ if (TimerCondition(m_branch_watch, Core::GetState()))
+ m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
+ QDialog::open();
+}
+
+void BranchWatchDialog::OnStartPause(bool checked)
+{
+ if (checked)
+ {
+ m_branch_watch.Start();
+ m_btn_start_pause->setText(tr("Pause Branch Watch"));
+ // Restart the timer if the situation calls for it, but always turn off single-shot.
+ m_timer->setSingleShot(false);
+ if (Core::GetState() > Core::State::Paused)
+ m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
+ }
+ else
+ {
+ m_branch_watch.Pause();
+ m_btn_start_pause->setText(tr("Start Branch Watch"));
+ // Schedule one last update in the future in case Branch Watch is in the middle of a hit.
+ if (Core::GetState() > Core::State::Paused)
+ m_timer->setInterval(BRANCH_WATCH_TOOL_TIMER_PAUSE_ONESHOT_MS);
+ m_timer->setSingleShot(true);
+ }
+ Update();
+}
+
+void BranchWatchDialog::OnClearBranchWatch()
+{
+ {
+ const Core::CPUThreadGuard guard{m_system};
+ m_table_model->OnClearBranchWatch(guard);
+ AutoSave(guard);
+ }
+ m_btn_wipe_recent_hits->setEnabled(false);
+ UpdateStatus();
+}
+
+static std::string GetSnapshotDefaultFilepath()
+{
+ return fmt::format("{}{}.txt", File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX),
+ SConfig::GetInstance().GetGameID());
+}
+
+void BranchWatchDialog::OnSave()
+{
+ if (!m_branch_watch.CanSave())
+ {
+ ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!"));
+ return;
+ }
+
+ Save(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath());
+}
+
+void BranchWatchDialog::OnSaveAs()
+{
+ if (!m_branch_watch.CanSave())
+ {
+ ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!"));
+ return;
+ }
+
+ const QString filepath = DolphinFileDialog::getSaveFileName(
+ this, tr("Save Branch Watch snapshot"),
+ QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
+ tr("Text file (*.txt);;All Files (*)"));
+ if (filepath.isEmpty())
+ return;
+
+ Save(Core::CPUThreadGuard{m_system}, filepath.toStdString());
+}
+
+void BranchWatchDialog::OnLoad()
+{
+ Load(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath());
+}
+
+void BranchWatchDialog::OnLoadFrom()
+{
+ const QString filepath = DolphinFileDialog::getOpenFileName(
+ this, tr("Load Branch Watch snapshot"),
+ QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
+ tr("Text file (*.txt);;All Files (*)"), nullptr, QFileDialog::Option::ReadOnly);
+ if (filepath.isEmpty())
+ return;
+
+ Load(Core::CPUThreadGuard{m_system}, filepath.toStdString());
+}
+
+void BranchWatchDialog::OnCodePathWasTaken()
+{
+ {
+ const Core::CPUThreadGuard guard{m_system};
+ m_table_model->OnCodePathWasTaken(guard);
+ AutoSave(guard);
+ }
+ m_btn_wipe_recent_hits->setEnabled(true);
+ UpdateStatus();
+}
+
+void BranchWatchDialog::OnCodePathNotTaken()
+{
+ {
+ const Core::CPUThreadGuard guard{m_system};
+ m_table_model->OnCodePathNotTaken(guard);
+ AutoSave(guard);
+ }
+ UpdateStatus();
+}
+
+void BranchWatchDialog::OnBranchWasOverwritten()
+{
+ if (Core::GetState() == Core::State::Uninitialized)
+ {
+ ModalMessageBox::warning(this, tr("Error"), tr("Core is uninitialized."));
+ return;
+ }
+ {
+ const Core::CPUThreadGuard guard{m_system};
+ m_table_model->OnBranchWasOverwritten(guard);
+ AutoSave(guard);
+ }
+ UpdateStatus();
+}
+
+void BranchWatchDialog::OnBranchNotOverwritten()
+{
+ if (Core::GetState() == Core::State::Uninitialized)
+ {
+ ModalMessageBox::warning(this, tr("Error"), tr("Core is uninitialized."));
+ return;
+ }
+ {
+ const Core::CPUThreadGuard guard{m_system};
+ m_table_model->OnBranchNotOverwritten(guard);
+ AutoSave(guard);
+ }
+ UpdateStatus();
+}
+
+void BranchWatchDialog::OnWipeRecentHits()
+{
+ m_table_model->OnWipeRecentHits();
+}
+
+void BranchWatchDialog::OnWipeInspection()
+{
+ m_table_model->OnWipeInspection();
+}
+
+void BranchWatchDialog::OnTimeout()
+{
+ Update();
+}
+
+void BranchWatchDialog::OnEmulationStateChanged(Core::State new_state)
+{
+ if (!isVisible())
+ return;
+
+ if (TimerCondition(m_branch_watch, new_state))
+ m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
+ else if (m_timer->isActive())
+ m_timer->stop();
+ Update();
+}
+
+void BranchWatchDialog::OnHelp()
+{
+ ModalMessageBox::information(
+ this, tr("Branch Watch Tool Help (1/4)"),
+ tr("Branch Watch is a code-searching tool that can isolate branches tracked by the emulated "
+ "CPU by testing candidate branches with simple criteria. If you are familiar with Cheat "
+ "Engine's Ultimap, Branch Watch is similar to that.\n\n"
+ "Press the \"Start Branch Watch\" button to activate Branch Watch. Branch Watch persists "
+ "across emulation sessions, and a snapshot of your progress can be saved to and loaded "
+ "from the User Directory to persist after Dolphin Emulator is closed. \"Save As...\" and "
+ "\"Load From...\" actions are also available, and auto-saving can be enabled to save a "
+ "snapshot at every step of a search. The \"Pause Branch Watch\" button will halt Branch "
+ "Watch from tracking further branch hits until it is told to resume. Press the \"Clear "
+ "Branch Watch\" button to clear all candidates and return to the blacklist phase."));
+ ModalMessageBox::information(
+ this, tr("Branch Watch Tool Help (2/4)"),
+ tr("Branch Watch starts in the blacklist phase, meaning no candidates have been chosen yet, "
+ "but candidates found so far can be excluded from the candidacy by pressing the \"Code "
+ "Path Not Taken\", \"Branch Was Overwritten\", and \"Branch Not Overwritten\" buttons. "
+ "Once the \"Code Path Was Taken\" button is pressed for the first time, Branch Watch will "
+ "switch to the reduction phase, and the table will populate with all eligible "
+ "candidates."));
+ ModalMessageBox::information(
+ this, tr("Branch Watch Tool Help (3/4)"),
+ tr("Once in the reduction phase, it is time to start narrowing down the candidates shown in "
+ "the table. Further reduce the candidates by checking whether a code path was or was not "
+ "taken since the last time it was checked. It is also possible to reduce the candidates "
+ "by determining whether a branch instruction has or has not been overwritten since it was "
+ "first hit. Filter the candidates by branch kind, branch condition, origin or destination "
+ "address, and origin or destination symbol name.\n\n"
+ "After enough passes and experimentation, you may be able to find function calls and "
+ "conditional code paths that are only taken when an action is performed in the emulated "
+ "software."));
+ ModalMessageBox::information(
+ this, tr("Branch Watch Tool Help (4/4)"),
+ tr("Rows in the table can be left-clicked on the origin, destination, and symbol columns to "
+ "view the associated address in Code View. Right-clicking the selected row(s) will bring "
+ "up a context menu.\n\n"
+ "If the origin column of a row selection is right-clicked, an action to replace the "
+ "branch instruction at the origin(s) with a NOP instruction (No Operation), and an action "
+ "to copy the address(es) to the clipboard will be available.\n\n"
+ "If the destination column of a row selection is right-clicked, an action to replace the "
+ "instruction at the destination(s) with a BLR instruction (Branch to Link Register) will "
+ "be available, but only if the branch instruction at every origin saves the link "
+ "register, and an action to copy the address(es) to the clipboard will be available.\n\n"
+ "If the origin / destination symbol column of a row selection is right-clicked, an action "
+ "to replace the instruction(s) at the start of the symbol with a BLR instruction will be "
+ "available, but only if every origin / destination symbol is found.\n\n"
+ "All context menus have the action to delete the selected row(s) from the candidates."));
+}
+
+void BranchWatchDialog::OnToggleAutoSave(bool checked)
+{
+ if (!checked)
+ return;
+
+ const QString filepath = DolphinFileDialog::getSaveFileName(
+ this, tr("Select Branch Watch snapshot auto-save file (for user folder location, cancel)"),
+ QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
+ tr("Text file (*.txt);;All Files (*)"));
+ if (filepath.isEmpty())
+ m_autosave_filepath = std::nullopt;
+ else
+ m_autosave_filepath = filepath.toStdString();
+}
+
+void BranchWatchDialog::OnHideShowControls(bool checked)
+{
+ if (checked)
+ m_control_toolbar->hide();
+ else
+ m_control_toolbar->show();
+}
+
+void BranchWatchDialog::OnToggleIgnoreApploader(bool checked)
+{
+ m_system.SetIsBranchWatchIgnoreApploader(checked);
+}
+
+void BranchWatchDialog::OnTableClicked(const QModelIndex& index)
+{
+ const QVariant v = m_table_proxy->data(index, UserRole::ClickRole);
+ switch (index.column())
+ {
+ case Column::OriginSymbol:
+ case Column::DestinSymbol:
+ if (!v.isValid())
+ return;
+ [[fallthrough]];
+ case Column::Origin:
+ case Column::Destination:
+ m_code_widget->SetAddress(v.value<u32>(), CodeViewWidget::SetAddressUpdate::WithDetailedUpdate);
+ return;
+ }
+}
+
+void BranchWatchDialog::OnTableContextMenu(const QPoint& pos)
+{
+ const QModelIndex index = m_table_view->indexAt(pos);
+ if (!index.isValid())
+ return;
+ QModelIndexList index_list = m_table_view->selectionModel()->selectedRows(index.column());
+
+ QMenu* const menu = new QMenu;
+ menu->addAction(tr("&Delete"), [this, index_list]() { OnTableDelete(std::move(index_list)); });
+ switch (index.column())
+ {
+ case Column::Origin:
+ {
+ QAction* const action = menu->addAction(tr("Insert &NOP"));
+ if (Core::GetState() != Core::State::Uninitialized)
+ connect(action, &QAction::triggered,
+ [this, index_list]() { OnTableSetNOP(std::move(index_list)); });
+ else
+ action->setEnabled(false);
+ menu->addAction(tr("&Copy Address"), [this, index_list = std::move(index_list)]() {
+ OnTableCopyAddress(std::move(index_list));
+ });
+ break;
+ }
+ case Column::Destination:
+ {
+ QAction* const action = menu->addAction(tr("Insert &BLR"));
+ const bool enable_action =
+ Core::GetState() != Core::State::Uninitialized &&
+ std::all_of(index_list.begin(), index_list.end(), [this](const QModelIndex& index) {
+ const QModelIndex sibling = index.siblingAtColumn(Column::Instruction);
+ return BranchSavesLR(m_table_proxy->data(sibling, UserRole::ClickRole).value<u32>());
+ });
+ if (enable_action)
+ connect(action, &QAction::triggered,
+ [this, index_list]() { OnTableSetBLR(std::move(index_list)); });
+ else
+ action->setEnabled(false);
+ menu->addAction(tr("&Copy Address"), [this, index_list = std::move(index_list)]() {
+ OnTableCopyAddress(std::move(index_list));
+ });
+ break;
+ }
+ case Column::OriginSymbol:
+ case Column::DestinSymbol:
+ {
+ QAction* const action = menu->addAction(tr("Insert &BLR at start"));
+ const bool enable_action =
+ Core::GetState() != Core::State::Uninitialized &&
+ std::all_of(index_list.begin(), index_list.end(), [this](const QModelIndex& index) {
+ return m_table_proxy->data(index, UserRole::ClickRole).isValid();
+ });
+ if (enable_action)
+ connect(action, &QAction::triggered, [this, index_list = std::move(index_list)]() {
+ OnTableSetBLR(std::move(index_list));
+ });
+ else
+ action->setEnabled(false);
+ break;
+ }
+ }
+ menu->exec(m_table_view->viewport()->mapToGlobal(pos));
+}
+
+void BranchWatchDialog::OnTableHeaderContextMenu(const QPoint& pos)
+{
+ m_mnu_column_visibility->exec(m_table_view->horizontalHeader()->mapToGlobal(pos));
+}
+
+void BranchWatchDialog::OnTableDelete(QModelIndexList index_list)
+{
+ m_table_proxy->OnDelete(std::move(index_list));
+ UpdateStatus();
+}
+
+void BranchWatchDialog::OnTableDeleteKeypress()
+{
+ OnTableDelete(m_table_view->selectionModel()->selectedRows());
+}
+
+void BranchWatchDialog::OnTableSetBLR(QModelIndexList index_list)
+{
+ for (const QModelIndex& index : index_list)
+ {
+ m_system.GetPowerPC().GetDebugInterface().SetPatch(
+ Core::CPUThreadGuard{m_system},
+ m_table_proxy->data(index, UserRole::ClickRole).value<u32>(), 0x4e800020);
+ m_table_proxy->SetInspected(index);
+ }
+ // TODO: This is not ideal. What I need is a signal for when memory has been changed by the GUI,
+ // but I cannot find one. UpdateDisasmDialog comes close, but does too much in one signal. For
+ // example, CodeViewWidget will scroll to the current PC when UpdateDisasmDialog is signaled. This
+ // seems like a pervasive issue. For example, modifying an instruction in the CodeViewWidget will
+ // not reflect in the MemoryViewWidget, and vice versa. Neither of these widgets changing memory
+ // will reflect in the JITWidget, either. At the very least, we can make sure the CodeWidget
+ // is updated in an acceptable way.
+ m_code_widget->Update();
+}
+
+void BranchWatchDialog::OnTableSetNOP(QModelIndexList index_list)
+{
+ for (const QModelIndex& index : index_list)
+ {
+ m_system.GetPowerPC().GetDebugInterface().SetPatch(
+ Core::CPUThreadGuard{m_system},
+ m_table_proxy->data(index, UserRole::ClickRole).value<u32>(), 0x60000000);
+ m_table_proxy->SetInspected(index);
+ }
+ // Same issue as OnSetBLR.
+ m_code_widget->Update();
+}
+
+void BranchWatchDialog::OnTableCopyAddress(QModelIndexList index_list)
+{
+ auto iter = index_list.begin();
+ if (iter == index_list.end())
+ return;
+
+ QString text;
+ text.reserve(index_list.size() * 9 - 1);
+ while (true)
+ {
+ text.append(QString::number(m_table_proxy->data(*iter, UserRole::ClickRole).value<u32>(), 16));
+ if (++iter == index_list.end())
+ break;
+ text.append(QChar::fromLatin1('\n'));
+ }
+ QApplication::clipboard()->setText(text);
+}
+
+void BranchWatchDialog::Update()
+{
+ if (m_branch_watch.GetRecordingPhase() == Core::BranchWatch::Phase::Blacklist)
+ UpdateStatus();
+ m_table_model->UpdateHits();
+}
+
+void BranchWatchDialog::UpdateSymbols()
+{
+ m_table_model->UpdateSymbols();
+}
+
+void BranchWatchDialog::UpdateStatus()
+{
+ switch (m_branch_watch.GetRecordingPhase())
+ {
+ case Core::BranchWatch::Phase::Blacklist:
+ {
+ const std::size_t candidate_size = m_branch_watch.GetCollectionSize();
+ const std::size_t blacklist_size = m_branch_watch.GetBlacklistSize();
+ if (blacklist_size == 0)
+ {
+ m_status_bar->showMessage(tr("Candidates: %1").arg(candidate_size));
+ return;
+ }
+ m_status_bar->showMessage(tr("Candidates: %1 | Excluded: %2 | Remaining: %3")
+ .arg(candidate_size)
+ .arg(blacklist_size)
+ .arg(candidate_size - blacklist_size));
+ return;
+ }
+ case Core::BranchWatch::Phase::Reduction:
+ {
+ const std::size_t candidate_size = m_branch_watch.GetSelection().size();
+ if (candidate_size == 0)
+ {
+ m_status_bar->showMessage(tr("Zero candidates remaining."));
+ return;
+ }
+ const std::size_t remaining_size = m_table_proxy->rowCount();
+ m_status_bar->showMessage(tr("Candidates: %1 | Filtered: %2 | Remaining: %3")
+ .arg(candidate_size)
+ .arg(candidate_size - remaining_size)
+ .arg(remaining_size));
+ return;
+ }
+ }
+}
+
+void BranchWatchDialog::Save(const Core::CPUThreadGuard& guard, const std::string& filepath)
+{
+ File::IOFile file(filepath, "w");
+ if (!file.IsOpen())
+ {
+ ModalMessageBox::warning(
+ this, tr("Error"),
+ tr("Failed to save Branch Watch snapshot \"%1\"").arg(QString::fromStdString(filepath)));
+ return;
+ }
+
+ m_table_model->Save(guard, file.GetHandle());
+}
+
+void BranchWatchDialog::Load(const Core::CPUThreadGuard& guard, const std::string& filepath)
+{
+ File::IOFile file(filepath, "r");
+ if (!file.IsOpen())
+ {
+ ModalMessageBox::warning(
+ this, tr("Error"),
+ tr("Failed to open Branch Watch snapshot \"%1\"").arg(QString::fromStdString(filepath)));
+ return;
+ }
+
+ m_table_model->Load(guard, file.GetHandle());
+ m_btn_wipe_recent_hits->setEnabled(m_branch_watch.GetRecordingPhase() ==
+ Core::BranchWatch::Phase::Reduction);
+}
+
+void BranchWatchDialog::AutoSave(const Core::CPUThreadGuard& guard)
+{
+ if (!m_act_autosave->isChecked() || !m_branch_watch.CanSave())
+ return;
+ Save(guard, m_autosave_filepath ? m_autosave_filepath.value() : GetSnapshotDefaultFilepath());
+}