From 8134c8a57211f54cf2109ae3c068c0c94370f786 Mon Sep 17 00:00:00 2001 From: mitaclaw <140017135+mitaclaw@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:36:02 -0800 Subject: [PATCH] BranchWatchDialog: A Total Replacement for CodeDiffDialog With a purpose-built Branch Watch feature built into the emulated system: BranchWatchDialog, replacing CodeDiffDialog, is now better than ever! --- Source/Core/Common/CommonPaths.h | 2 + Source/Core/Common/FileUtil.cpp | 6 + Source/Core/Common/FileUtil.h | 2 + Source/Core/Core/Boot/Boot_BS2Emu.cpp | 8 + Source/Core/Core/System.h | 3 + Source/Core/DolphinQt/CMakeLists.txt | 6 +- .../DolphinQt/Debugger/BranchWatchDialog.cpp | 1014 +++++++++++++++++ .../DolphinQt/Debugger/BranchWatchDialog.h | 114 ++ .../Debugger/BranchWatchTableModel.cpp | 502 ++++++++ .../Debugger/BranchWatchTableModel.h | 119 ++ .../DolphinQt/Debugger/CodeDiffDialog.cpp | 673 ----------- .../Core/DolphinQt/Debugger/CodeDiffDialog.h | 86 -- Source/Core/DolphinQt/Debugger/CodeWidget.cpp | 31 +- Source/Core/DolphinQt/Debugger/CodeWidget.h | 8 +- Source/Core/DolphinQt/DolphinQt.vcxproj | 6 +- Source/Core/UICommon/UICommon.cpp | 4 + 16 files changed, 1805 insertions(+), 779 deletions(-) create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchDialog.h create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h delete mode 100644 Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp delete mode 100644 Source/Core/DolphinQt/Debugger/CodeDiffDialog.h diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index bf30fb98a9..1dce5c47f5 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -75,6 +75,8 @@ #define DUMP_AUDIO_DIR "Audio" #define DUMP_DSP_DIR "DSP" #define DUMP_SSL_DIR "SSL" +#define DUMP_DEBUG_DIR "Debug" +#define DUMP_DEBUG_BRANCHWATCH_DIR "BranchWatch" #define LOGS_DIR "Logs" #define MAIL_LOGS_DIR "Mail" #define SHADERS_DIR "Shaders" diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 562e66ff56..f7a8555875 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -856,6 +856,9 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[D_DUMPTEXTURES_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_TEXTURES_DIR DIR_SEP; s_user_paths[D_DUMPDSP_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DSP_DIR DIR_SEP; s_user_paths[D_DUMPSSL_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_SSL_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_BRANCHWATCH_IDX] = + s_user_paths[D_DUMPDEBUG_IDX] + DUMP_DEBUG_BRANCHWATCH_DIR DIR_SEP; s_user_paths[D_LOGS_IDX] = s_user_paths[D_USER_IDX] + LOGS_DIR DIR_SEP; s_user_paths[D_MAILLOGS_IDX] = s_user_paths[D_LOGS_IDX] + MAIL_LOGS_DIR DIR_SEP; s_user_paths[D_THEMES_IDX] = s_user_paths[D_USER_IDX] + THEMES_DIR DIR_SEP; @@ -932,6 +935,9 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[D_DUMPTEXTURES_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_TEXTURES_DIR DIR_SEP; s_user_paths[D_DUMPDSP_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DSP_DIR DIR_SEP; s_user_paths[D_DUMPSSL_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_SSL_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_BRANCHWATCH_IDX] = + s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_BRANCHWATCH_DIR DIR_SEP; s_user_paths[F_MEM1DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM1_DUMP; s_user_paths[F_MEM2DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM2_DUMP; s_user_paths[F_ARAMDUMP_IDX] = s_user_paths[D_DUMP_IDX] + ARAM_DUMP; diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 94491cecfd..975ab55256 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -52,6 +52,8 @@ enum D_DUMPTEXTURES_IDX, D_DUMPDSP_IDX, D_DUMPSSL_IDX, + D_DUMPDEBUG_IDX, + D_DUMPDEBUG_BRANCHWATCH_IDX, D_LOAD_IDX, D_LOGS_IDX, D_MAILLOGS_IDX, diff --git a/Source/Core/Core/Boot/Boot_BS2Emu.cpp b/Source/Core/Core/Boot/Boot_BS2Emu.cpp index 09f9e5128a..d36b192bb5 100644 --- a/Source/Core/Core/Boot/Boot_BS2Emu.cpp +++ b/Source/Core/Core/Boot/Boot_BS2Emu.cpp @@ -19,6 +19,7 @@ #include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/HLE/HLE.h" #include "Core/HW/DVD/DVDInterface.h" #include "Core/HW/EXI/EXI_DeviceIPL.h" @@ -158,6 +159,11 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard auto& ppc_state = system.GetPPCState(); auto& mmu = system.GetMMU(); + auto& branch_watch = system.GetPowerPC().GetBranchWatch(); + + const bool resume_branch_watch = branch_watch.GetRecordingActive(); + if (system.IsBranchWatchIgnoreApploader()) + branch_watch.Pause(); // Call iAppLoaderEntry. DEBUG_LOG_FMT(BOOT, "Call iAppLoaderEntry"); @@ -220,6 +226,8 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard // return ppc_state.pc = ppc_state.gpr[3]; + branch_watch.SetRecordingActive(resume_branch_watch); + return true; } diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h index fe60eabf12..acaf2daad7 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -141,9 +141,11 @@ public: bool IsPauseOnPanicMode() const { return m_pause_on_panic_enabled; } bool IsMIOS() const { return m_is_mios; } bool IsWii() const { return m_is_wii; } + bool IsBranchWatchIgnoreApploader() { return m_branch_watch_ignore_apploader; } void SetIsMIOS(bool is_mios) { m_is_mios = is_mios; } void SetIsWii(bool is_wii) { m_is_wii = is_wii; } + void SetIsBranchWatchIgnoreApploader(bool enable) { m_branch_watch_ignore_apploader = enable; } SoundStream* GetSoundStream() const; void SetSoundStream(std::unique_ptr sound_stream); @@ -202,5 +204,6 @@ private: bool m_pause_on_panic_enabled = false; bool m_is_mios = false; bool m_is_wii = false; + bool m_branch_watch_ignore_apploader = false; }; } // namespace Core diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 8285c17d4a..437323fc74 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -206,12 +206,14 @@ add_executable(dolphin-emu Debugger/AssemblerWidget.h Debugger/AssemblyEditor.cpp Debugger/AssemblyEditor.h + Debugger/BranchWatchDialog.cpp + Debugger/BranchWatchDialog.h + Debugger/BranchWatchTableModel.cpp + Debugger/BranchWatchTableModel.h Debugger/BreakpointDialog.cpp Debugger/BreakpointDialog.h Debugger/BreakpointWidget.cpp Debugger/BreakpointWidget.h - Debugger/CodeDiffDialog.cpp - Debugger/CodeDiffDialog.h Debugger/CodeViewWidget.cpp Debugger/CodeViewWidget.h Debugger/CodeWidget.cpp 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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 + void OnToggled(bool enabled) + { + this->*member = enabled; + invalidateRowsFilter(); + } + template + void OnSymbolTextChanged(const QString& text) + { + this->*member = text; + invalidateRowsFilter(); + } + template 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 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().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().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 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(), 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()); + }); + 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(), 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(), 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(), 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()); +} diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchDialog.h b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.h new file mode 100644 index 0000000000..b167ca106d --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.h @@ -0,0 +1,114 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include +#include + +#include "Core/Core.h" + +namespace Core +{ +class BranchWatch; +class CPUThreadGuard; +class System; +} // namespace Core +class BranchWatchProxyModel; +class BranchWatchTableModel; +class CodeWidget; +class QAction; +class QMenu; +class QPoint; +class QPushButton; +class QStatusBar; +class QTableView; +class QTimer; +class QToolBar; +class QWidget; + +namespace BranchWatchTableModelColumn +{ +enum EnumType : int; +} +namespace BranchWatchTableModelUserRole +{ +enum EnumType : int; +} + +class BranchWatchDialog : public QDialog +{ + Q_OBJECT + + using Column = BranchWatchTableModelColumn::EnumType; + using UserRole = BranchWatchTableModelUserRole::EnumType; + +public: + explicit BranchWatchDialog(Core::System& system, Core::BranchWatch& branch_watch, + CodeWidget* code_widget, QWidget* parent = nullptr); + void done(int r) override; + int exec() override; + void open() override; + +private: + void OnStartPause(bool checked); + void OnClearBranchWatch(); + void OnSave(); + void OnSaveAs(); + void OnLoad(); + void OnLoadFrom(); + void OnCodePathWasTaken(); + void OnCodePathNotTaken(); + void OnBranchWasOverwritten(); + void OnBranchNotOverwritten(); + void OnWipeRecentHits(); + void OnWipeInspection(); + void OnTimeout(); + void OnEmulationStateChanged(Core::State new_state); + void OnHelp(); + void OnToggleAutoSave(bool checked); + void OnHideShowControls(bool checked); + void OnToggleIgnoreApploader(bool checked); + + void OnTableClicked(const QModelIndex& index); + void OnTableContextMenu(const QPoint& pos); + void OnTableHeaderContextMenu(const QPoint& pos); + void OnTableDelete(QModelIndexList index_list); + void OnTableDeleteKeypress(); + void OnTableSetBLR(QModelIndexList index_list); + void OnTableSetNOP(QModelIndexList index_list); + void OnTableCopyAddress(QModelIndexList index_list); + +public: + // TODO: Step doesn't cause EmulationStateChanged to be emitted, so it has to call this manually. + void Update(); + // TODO: There seems to be a lack of a ubiquitous signal for when symbols change. + void UpdateSymbols(); + +private: + void UpdateStatus(); + void Save(const Core::CPUThreadGuard& guard, const std::string& filepath); + void Load(const Core::CPUThreadGuard& guard, const std::string& filepath); + void AutoSave(const Core::CPUThreadGuard& guard); + + Core::System& m_system; + Core::BranchWatch& m_branch_watch; + CodeWidget* m_code_widget; + + QPushButton *m_btn_start_pause, *m_btn_clear_watch, *m_btn_path_was_taken, *m_btn_path_not_taken, + *m_btn_was_overwritten, *m_btn_not_overwritten, *m_btn_wipe_recent_hits; + QAction* m_act_autosave; + QMenu* m_mnu_column_visibility; + + QToolBar* m_control_toolbar; + QTableView* m_table_view; + BranchWatchProxyModel* m_table_proxy; + BranchWatchTableModel* m_table_model; + QStatusBar* m_status_bar; + QTimer* m_timer; + + std::optional m_autosave_filepath; +}; diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp new file mode 100644 index 0000000000..800973a6dd --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp @@ -0,0 +1,502 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/Debugger/BranchWatchTableModel.h" + +#include +#include +#include + +#include + +#include "Common/Assert.h" +#include "Common/GekkoDisassembler.h" +#include "Core/Debugger/BranchWatch.h" +#include "Core/PowerPC/PPCSymbolDB.h" + +QVariant BranchWatchTableModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + return DisplayRoleData(index); + case Qt::FontRole: + return FontRoleData(index); + case Qt::TextAlignmentRole: + return TextAlignmentRoleData(index); + case Qt::ForegroundRole: + return ForegroundRoleData(index); + case UserRole::ClickRole: + return ClickRoleData(index); + case UserRole::SortRole: + return SortRoleData(index); + } + return QVariant(); +} + +QVariant BranchWatchTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical || role != Qt::DisplayRole) + return QVariant(); + + static constexpr std::array headers = { + QT_TR_NOOP("Instr."), QT_TR_NOOP("Cond."), + 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")}; + return tr(headers[section]); +} + +int BranchWatchTableModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return static_cast(m_branch_watch.GetSelection().size()); +} + +int BranchWatchTableModel::columnCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return Column::NumberOfColumns; +} + +bool BranchWatchTableModel::removeRows(int row, int count, const QModelIndex& parent) +{ + if (parent.isValid() || row < 0) + return false; + if (count <= 0) + return true; + + auto& selection = m_branch_watch.GetSelection(); + beginRemoveRows(parent, row, row + count - 1); // Last is inclusive in Qt! + selection.erase(selection.begin() + row, selection.begin() + row + count); + m_symbol_list.remove(row, count); + endRemoveRows(); + return true; +} + +void BranchWatchTableModel::OnClearBranchWatch(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.Clear(guard); + m_symbol_list.clear(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnCodePathWasTaken(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateHasExecuted(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnCodePathNotTaken(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateNotExecuted(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnBranchWasOverwritten(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateWasOverwritten(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnBranchNotOverwritten(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateNotOverwritten(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnWipeRecentHits() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::DisplayRole}; + m_branch_watch.UpdateHitsSnapshot(); + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::RecentHits), createIndex(last, Column::RecentHits), + roles); +} + +void BranchWatchTableModel::OnWipeInspection() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + m_branch_watch.ClearSelectionInspection(); + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::Origin), createIndex(last, Column::Destination), roles); + emit dataChanged(createIndex(0, Column::OriginSymbol), createIndex(last, Column::DestinSymbol), + roles); +} + +void BranchWatchTableModel::OnDelete(QModelIndexList index_list) +{ + std::sort(index_list.begin(), index_list.end()); + // TODO C++20: std::ranges::reverse_view + for (auto iter = index_list.rbegin(); iter != index_list.rend(); ++iter) + { + if (!iter->isValid()) + continue; + removeRow(iter->row()); + } +} + +void BranchWatchTableModel::Save(const Core::CPUThreadGuard& guard, std::FILE* file) const +{ + m_branch_watch.Save(guard, file); +} + +void BranchWatchTableModel::Load(const Core::CPUThreadGuard& guard, std::FILE* file) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.Load(guard, file); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::UpdateSymbols() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::DisplayRole}; + PrefetchSymbols(); + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::OriginSymbol), createIndex(last, Column::DestinSymbol), + roles); +} + +void BranchWatchTableModel::UpdateHits() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::DisplayRole}; + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::RecentHits), createIndex(last, Column::TotalHits), roles); +} + +void BranchWatchTableModel::SetInspected(const QModelIndex& index) +{ + const int row = index.row(); + switch (index.column()) + { + case Column::Origin: + SetOriginInspected(m_branch_watch.GetSelection()[row].collection_ptr->first.origin_addr); + return; + case Column::Destination: + SetDestinInspected(m_branch_watch.GetSelection()[row].collection_ptr->first.destin_addr, false); + return; + case Column::OriginSymbol: + SetSymbolInspected(m_symbol_list[row].origin_addr.value(), false); + return; + case Column::DestinSymbol: + SetSymbolInspected(m_symbol_list[row].destin_addr.value(), false); + return; + } +} + +void BranchWatchTableModel::SetOriginInspected(u32 origin_addr) +{ + using Inspection = Core::BranchWatchSelectionInspection; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + + const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection(); + for (std::size_t i = 0; i < selection.size(); ++i) + { + if (selection[i].collection_ptr->first.origin_addr != origin_addr) + continue; + m_branch_watch.SetSelectedInspected(i, Inspection::SetOriginNOP); + const QModelIndex index = createIndex(static_cast(i), Column::Origin); + emit dataChanged(index, index, roles); + } +} + +void BranchWatchTableModel::SetDestinInspected(u32 destin_addr, bool nested) +{ + using Inspection = Core::BranchWatchSelectionInspection; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + + const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection(); + for (std::size_t i = 0; i < selection.size(); ++i) + { + if (selection[i].collection_ptr->first.destin_addr != destin_addr) + continue; + m_branch_watch.SetSelectedInspected(i, Inspection::SetDestinBLR); + const QModelIndex index = createIndex(static_cast(i), Column::Destination); + emit dataChanged(index, index, roles); + } + + if (nested) + return; + SetSymbolInspected(destin_addr, true); +} + +void BranchWatchTableModel::SetSymbolInspected(u32 symbol_addr, bool nested) +{ + using Inspection = Core::BranchWatchSelectionInspection; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + + for (qsizetype i = 0; i < m_symbol_list.size(); ++i) + { + const SymbolListValueType& value = m_symbol_list[i]; + if (value.origin_addr.isValid() && value.origin_addr.value() == symbol_addr) + { + m_branch_watch.SetSelectedInspected(i, Inspection::SetOriginSymbolBLR); + const QModelIndex index = createIndex(i, Column::OriginSymbol); + emit dataChanged(index, index, roles); + } + if (value.destin_addr.isValid() && value.destin_addr.value() == symbol_addr) + { + m_branch_watch.SetSelectedInspected(i, Inspection::SetDestinSymbolBLR); + const QModelIndex index = createIndex(i, Column::DestinSymbol); + emit dataChanged(index, index, roles); + } + } + + if (nested) + return; + SetDestinInspected(symbol_addr, true); +} + +void BranchWatchTableModel::PrefetchSymbols() +{ + if (m_branch_watch.GetRecordingPhase() != Core::BranchWatch::Phase::Reduction) + return; + + const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection(); + m_symbol_list.clear(); + m_symbol_list.reserve(selection.size()); + for (const Core::BranchWatch::Selection::value_type& value : selection) + { + const Core::BranchWatch::Collection::value_type* const kv = value.collection_ptr; + m_symbol_list.emplace_back(g_symbolDB.GetSymbolFromAddr(kv->first.origin_addr), + g_symbolDB.GetSymbolFromAddr(kv->first.destin_addr)); + } +} + +static QVariant GetValidSymbolStringVariant(const QVariant& symbol_name_v) +{ + if (symbol_name_v.isValid()) + return symbol_name_v; + return QStringLiteral(" --- "); +} + +static QString GetInstructionMnemonic(u32 hex) +{ + const std::string disas = Common::GekkoDisassembler::Disassemble(hex, 0); + const std::string::size_type split = disas.find('\t'); + // I wish I could disassemble just the mnemonic! + if (split == std::string::npos) + return QString::fromStdString(disas); + return QString::fromLatin1(disas.data(), split); +} + +static bool BranchIsUnconditional(UGeckoInstruction inst) +{ + if (inst.OPCD == 18) // bx + return true; + // If BranchWatch is doing its job, the input will be only bcx, bclrx, and bcctrx instructions. + DEBUG_ASSERT(inst.OPCD == 16 || (inst.OPCD == 19 && (inst.SUBOP10 == 16 || inst.SUBOP10 == 528))); + if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always + return true; + return false; +} + +static QString GetConditionString(const Core::BranchWatch::Selection::value_type& value, + const Core::BranchWatch::Collection::value_type* kv) +{ + if (value.condition == false) + return BranchWatchTableModel::tr("false"); + if (BranchIsUnconditional(kv->first.original_inst)) + return QStringLiteral(""); + return BranchWatchTableModel::tr("true"); +} + +QVariant BranchWatchTableModel::DisplayRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + case Column::OriginSymbol: + return GetValidSymbolStringVariant(m_symbol_list[index.row()].origin_name); + case Column::DestinSymbol: + return GetValidSymbolStringVariant(m_symbol_list[index.row()].destin_name); + } + const Core::BranchWatch::Selection::value_type& value = + m_branch_watch.GetSelection()[index.row()]; + const Core::BranchWatch::Collection::value_type* kv = value.collection_ptr; + switch (index.column()) + { + case Column::Instruction: + return GetInstructionMnemonic(kv->first.original_inst.hex); + case Column::Condition: + return GetConditionString(value, kv); + case Column::Origin: + return QString::number(kv->first.origin_addr, 16); + case Column::Destination: + return QString::number(kv->first.destin_addr, 16); + case Column::RecentHits: + return QString::number(kv->second.total_hits - kv->second.hits_snapshot); + case Column::TotalHits: + return QString::number(kv->second.total_hits); + } + return QVariant(); +} + +QVariant BranchWatchTableModel::FontRoleData(const QModelIndex& index) const +{ + m_font.setBold([&]() -> bool { + switch (index.column()) + { + using Inspection = Core::BranchWatchSelectionInspection; + case Column::Origin: + return (m_branch_watch.GetSelection()[index.row()].inspection & Inspection::SetOriginNOP) != + Inspection{}; + case Column::Destination: + return (m_branch_watch.GetSelection()[index.row()].inspection & Inspection::SetDestinBLR) != + Inspection{}; + case Column::OriginSymbol: + return (m_branch_watch.GetSelection()[index.row()].inspection & + Inspection::SetOriginSymbolBLR) != Inspection{}; + case Column::DestinSymbol: + return (m_branch_watch.GetSelection()[index.row()].inspection & + Inspection::SetDestinSymbolBLR) != Inspection{}; + } + // Importantly, this code path avoids subscripting the selection to get an inspection value. + return false; + }()); + return m_font; +} + +QVariant BranchWatchTableModel::TextAlignmentRoleData(const QModelIndex& index) const +{ + // Qt enums become QFlags when operators are used. QVariant's constructors don't support QFlags. + switch (index.column()) + { + case Column::Condition: + case Column::Origin: + case Column::Destination: + return Qt::AlignCenter; + case Column::RecentHits: + case Column::TotalHits: + return QVariant::fromValue(Qt::AlignRight | Qt::AlignVCenter); + case Column::Instruction: + case Column::OriginSymbol: + case Column::DestinSymbol: + return QVariant::fromValue(Qt::AlignLeft | Qt::AlignVCenter); + } + return QVariant(); +} + +QVariant BranchWatchTableModel::ForegroundRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + using Inspection = Core::BranchWatchSelectionInspection; + case Column::Origin: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetOriginNOP) != Inspection{} ? QBrush(Qt::red) : QVariant(); + } + case Column::Destination: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetDestinBLR) != Inspection{} ? QBrush(Qt::red) : QVariant(); + } + case Column::OriginSymbol: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetOriginSymbolBLR) != Inspection{} ? QBrush(Qt::red) : + QVariant(); + } + case Column::DestinSymbol: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetDestinSymbolBLR) != Inspection{} ? QBrush(Qt::red) : + QVariant(); + } + } + // Importantly, this code path avoids subscripting the selection to get an inspection value. + return QVariant(); +} + +QVariant BranchWatchTableModel::ClickRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + case Column::OriginSymbol: + return m_symbol_list[index.row()].origin_addr; + case Column::DestinSymbol: + return m_symbol_list[index.row()].destin_addr; + } + const Core::BranchWatch::Collection::value_type* kv = + m_branch_watch.GetSelection()[index.row()].collection_ptr; + switch (index.column()) + { + case Column::Instruction: + return kv->first.original_inst.hex; + case Column::Origin: + return kv->first.origin_addr; + case Column::Destination: + return kv->first.destin_addr; + } + return QVariant(); +} + +// 0 == false, 1 == true, 2 == unconditional +static int GetConditionInteger(const Core::BranchWatch::Selection::value_type& value, + const Core::BranchWatch::Collection::value_type* kv) +{ + if (value.condition == false) + return 0; + if (BranchIsUnconditional(kv->first.original_inst)) + return 2; + return 1; +} + +QVariant BranchWatchTableModel::SortRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + case Column::OriginSymbol: + return m_symbol_list[index.row()].origin_name; + case Column::DestinSymbol: + return m_symbol_list[index.row()].destin_name; + } + const Core::BranchWatch::Selection::value_type& selection_value = + m_branch_watch.GetSelection()[index.row()]; + const Core::BranchWatch::Collection::value_type* kv = selection_value.collection_ptr; + switch (index.column()) + { + // QVariant's ctor only supports (unsigned) int and (unsigned) long long for some stupid reason. + // std::size_t is unsigned long on some platforms, which results in an ambiguous conversion. + case Column::Instruction: + return GetInstructionMnemonic(kv->first.original_inst.hex); + case Column::Condition: + return GetConditionInteger(selection_value, kv); + case Column::Origin: + return kv->first.origin_addr; + case Column::Destination: + return kv->first.destin_addr; + case Column::RecentHits: + return qulonglong{kv->second.total_hits - kv->second.hits_snapshot}; + case Column::TotalHits: + return qulonglong{kv->second.total_hits}; + } + return QVariant(); +} diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h new file mode 100644 index 0000000000..7b3cf42bb1 --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h @@ -0,0 +1,119 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include + +#include "Common/SymbolDB.h" + +namespace Core +{ +class BranchWatch; +class CPUThreadGuard; +class System; +} // namespace Core + +namespace BranchWatchTableModelColumn +{ +enum EnumType : int +{ + Instruction = 0, + Condition, + Origin, + Destination, + RecentHits, + TotalHits, + OriginSymbol, + DestinSymbol, + NumberOfColumns, +}; +} + +namespace BranchWatchTableModelUserRole +{ +enum EnumType : int +{ + ClickRole = Qt::UserRole, + SortRole, +}; +} + +struct BranchWatchTableModelSymbolListValueType +{ + explicit BranchWatchTableModelSymbolListValueType(const Common::Symbol* const origin_symbol, + const Common::Symbol* const destin_symbol) + : origin_name(origin_symbol ? QString::fromStdString(origin_symbol->name) : QVariant{}), + origin_addr(origin_symbol ? origin_symbol->address : QVariant{}), + destin_name(destin_symbol ? QString::fromStdString(destin_symbol->name) : QVariant{}), + destin_addr(destin_symbol ? destin_symbol->address : QVariant{}) + { + } + QVariant origin_name, origin_addr; + QVariant destin_name, destin_addr; +}; + +class BranchWatchTableModel final : public QAbstractTableModel +{ + Q_OBJECT + +public: + using Column = BranchWatchTableModelColumn::EnumType; + using UserRole = BranchWatchTableModelUserRole::EnumType; + using SymbolListValueType = BranchWatchTableModelSymbolListValueType; + using SymbolList = QList; + + explicit BranchWatchTableModel(Core::System& system, Core::BranchWatch& branch_watch, + QObject* parent = nullptr) + : QAbstractTableModel(parent), m_system(system), m_branch_watch(branch_watch) + { + } + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex{}) const override; + int columnCount(const QModelIndex& parent = QModelIndex{}) const override; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex{}) override; + void setFont(const QFont& font) { m_font = font; } + + void OnClearBranchWatch(const Core::CPUThreadGuard& guard); + void OnCodePathWasTaken(const Core::CPUThreadGuard& guard); + void OnCodePathNotTaken(const Core::CPUThreadGuard& guard); + void OnBranchWasOverwritten(const Core::CPUThreadGuard& guard); + void OnBranchNotOverwritten(const Core::CPUThreadGuard& guard); + void OnWipeRecentHits(); + void OnWipeInspection(); + void OnDelete(QModelIndexList index_list); + + void Save(const Core::CPUThreadGuard& guard, std::FILE* file) const; + void Load(const Core::CPUThreadGuard& guard, std::FILE* file); + void UpdateSymbols(); + void UpdateHits(); + void SetInspected(const QModelIndex& index); + + const SymbolList& GetSymbolList() const { return m_symbol_list; } + +private: + void SetOriginInspected(u32 origin_addr); + void SetDestinInspected(u32 destin_addr, bool nested); + void SetSymbolInspected(u32 symbol_addr, bool nested); + void PrefetchSymbols(); + + [[nodiscard]] QVariant DisplayRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant FontRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant TextAlignmentRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant ForegroundRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant ClickRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant SortRoleData(const QModelIndex& index) const; + + Core::System& m_system; + Core::BranchWatch& m_branch_watch; + + SymbolList m_symbol_list; + mutable QFont m_font; +}; diff --git a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp b/Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp deleted file mode 100644 index 423036742d..0000000000 --- a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp +++ /dev/null @@ -1,673 +0,0 @@ -// Copyright 2022 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "DolphinQt/Debugger/CodeDiffDialog.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Common/FileUtil.h" -#include "Common/IOFile.h" -#include "Common/MsgHandler.h" -#include "Common/StringUtil.h" -#include "Core/ConfigManager.h" -#include "Core/Core.h" -#include "Core/Debugger/PPCDebugInterface.h" -#include "Core/HW/CPU.h" -#include "Core/PowerPC/JitInterface.h" -#include "Core/PowerPC/MMU.h" -#include "Core/PowerPC/PPCSymbolDB.h" -#include "Core/PowerPC/PowerPC.h" -#include "Core/PowerPC/Profiler.h" -#include "Core/System.h" - -#include "DolphinQt/Debugger/CodeWidget.h" -#include "DolphinQt/Host.h" -#include "DolphinQt/QtUtils/ModalMessageBox.h" -#include "DolphinQt/Settings.h" - -static const QString RECORD_BUTTON_STYLESHEET = QStringLiteral( - "QPushButton:checked { background-color: rgb(150, 0, 0); border-style: solid;" - "padding: 0px; border-width: 3px; border-color: rgb(150,0,0); color: rgb(255, 255, 255);}"); - -CodeDiffDialog::CodeDiffDialog(CodeWidget* parent) : QDialog(parent), m_code_widget(parent) -{ - setWindowTitle(tr("Code Diff Tool")); - CreateWidgets(); - auto& settings = Settings::GetQSettings(); - restoreGeometry(settings.value(QStringLiteral("diffdialog/geometry")).toByteArray()); - ConnectWidgets(); -} - -void CodeDiffDialog::reject() -{ - ClearData(); - auto& settings = Settings::GetQSettings(); - settings.setValue(QStringLiteral("diffdialog/geometry"), saveGeometry()); - QDialog::reject(); -} - -void CodeDiffDialog::CreateWidgets() -{ - bool running = Core::GetState() != Core::State::Uninitialized; - - auto* btns_layout = new QGridLayout; - m_exclude_btn = new QPushButton(tr("Code did not get executed")); - m_include_btn = new QPushButton(tr("Code has been executed")); - m_record_btn = new QPushButton(tr("Start Recording")); - m_record_btn->setCheckable(true); - m_record_btn->setStyleSheet(RECORD_BUTTON_STYLESHEET); - m_record_btn->setEnabled(running); - m_exclude_btn->setEnabled(false); - m_include_btn->setEnabled(false); - - btns_layout->addWidget(m_exclude_btn, 0, 0); - btns_layout->addWidget(m_include_btn, 0, 1); - btns_layout->addWidget(m_record_btn, 0, 2); - - auto* labels_layout = new QHBoxLayout; - m_exclude_size_label = new QLabel(tr("Excluded: 0")); - m_include_size_label = new QLabel(tr("Included: 0")); - - btns_layout->addWidget(m_exclude_size_label, 1, 0); - btns_layout->addWidget(m_include_size_label, 1, 1); - - m_matching_results_table = new QTableWidget(); - m_matching_results_table->setColumnCount(5); - m_matching_results_table->setHorizontalHeaderLabels( - {tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")}); - m_matching_results_table->setSelectionMode(QAbstractItemView::SingleSelection); - m_matching_results_table->setSelectionBehavior(QAbstractItemView::SelectRows); - m_matching_results_table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - m_matching_results_table->setContextMenuPolicy(Qt::CustomContextMenu); - m_matching_results_table->setColumnWidth(0, 60); - m_matching_results_table->setColumnWidth(1, 60); - m_matching_results_table->setColumnWidth(2, 4); - m_matching_results_table->setColumnWidth(3, 210); - m_matching_results_table->setColumnWidth(4, 65); - m_matching_results_table->setCornerButtonEnabled(false); - m_autosave_check = new QCheckBox(tr("Auto Save")); - m_save_btn = new QPushButton(tr("Save")); - m_save_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_save_btn->setEnabled(running); - m_load_btn = new QPushButton(tr("Load")); - m_load_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_load_btn->setEnabled(running); - m_reset_btn = new QPushButton(tr("Reset All")); - m_reset_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_help_btn = new QPushButton(tr("Help")); - m_help_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - auto* bottom_controls_layout = new QHBoxLayout; - bottom_controls_layout->addWidget(m_reset_btn, 0, Qt::AlignLeft); - bottom_controls_layout->addStretch(); - bottom_controls_layout->addWidget(m_autosave_check, 0, Qt::AlignRight); - bottom_controls_layout->addWidget(m_save_btn, 0, Qt::AlignRight); - bottom_controls_layout->addWidget(m_load_btn, 0, Qt::AlignRight); - bottom_controls_layout->addWidget(m_help_btn, 0, Qt::AlignRight); - - auto* layout = new QVBoxLayout(); - layout->addLayout(btns_layout); - layout->addLayout(labels_layout); - layout->addWidget(m_matching_results_table); - layout->addLayout(bottom_controls_layout); - - setLayout(layout); - resize(515, 400); -} - -void CodeDiffDialog::ConnectWidgets() -{ -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, - [this](Qt::ColorScheme colorScheme) { - m_record_btn->setStyleSheet(RECORD_BUTTON_STYLESHEET); - }); -#endif - connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, - [this](Core::State state) { UpdateButtons(state != Core::State::Uninitialized); }); - connect(m_record_btn, &QPushButton::toggled, this, &CodeDiffDialog::OnRecord); - connect(m_include_btn, &QPushButton::pressed, [this]() { Update(UpdateType::Include); }); - connect(m_exclude_btn, &QPushButton::pressed, [this]() { Update(UpdateType::Exclude); }); - connect(m_matching_results_table, &QTableWidget::itemClicked, [this]() { OnClickItem(); }); - connect(m_save_btn, &QPushButton::pressed, this, &CodeDiffDialog::SaveDataBackup); - connect(m_load_btn, &QPushButton::pressed, this, &CodeDiffDialog::LoadDataBackup); - connect(m_reset_btn, &QPushButton::pressed, this, &CodeDiffDialog::ClearData); - connect(m_help_btn, &QPushButton::pressed, this, &CodeDiffDialog::InfoDisp); - connect(m_matching_results_table, &CodeDiffDialog::customContextMenuRequested, this, - &CodeDiffDialog::OnContextMenu); -} - -void CodeDiffDialog::OnClickItem() -{ - UpdateItem(); - auto address = m_matching_results_table->currentItem()->data(Qt::UserRole).toUInt(); - m_code_widget->SetAddress(address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); -} - -void CodeDiffDialog::SaveDataBackup() -{ - if (Core::GetState() == Core::State::Uninitialized) - { - ModalMessageBox::information(this, tr("Code Diff Tool"), - tr("Emulation must be started before saving a file.")); - return; - } - - if (m_include.empty()) - return; - - std::string filename = - File::GetUserPath(D_LOGS_IDX) + SConfig::GetInstance().GetGameID() + "_CodeDiff.txt"; - File::IOFile f(filename, "w"); - if (!f) - { - ModalMessageBox::information( - this, tr("Code Diff Tool"), - tr("Failed to save file to: %1").arg(QString::fromStdString(filename))); - return; - } - - // Copy list of BLR tested functions: - std::set address_blr; - for (int i = 0; i < m_matching_results_table->rowCount(); i++) - { - if (m_matching_results_table->item(i, 4)->text() == QStringLiteral("X")) - address_blr.insert(m_matching_results_table->item(i, 4)->data(Qt::UserRole).toUInt()); - } - - for (const auto& line : m_include) - { - bool blr = address_blr.contains(line.addr); - f.WriteString( - fmt::format("{} {} {} {:d} {}\n", line.addr, line.hits, line.total_hits, blr, line.symbol)); - } -} - -void CodeDiffDialog::LoadDataBackup() -{ - if (Core::GetState() == Core::State::Uninitialized) - { - ModalMessageBox::information(this, tr("Code Diff Tool"), - tr("Emulation must be started before loading a file.")); - return; - } - - if (g_symbolDB.IsEmpty()) - { - ModalMessageBox::warning( - this, tr("Code Diff Tool"), - tr("Symbol map not found.\n\nIf one does not exist, you can generate one from " - "the Menu bar:\nSymbols -> Generate Symbols From ->\n\tAddress | Signature " - "Database | RSO Modules")); - return; - } - - std::string filename = - File::GetUserPath(D_LOGS_IDX) + SConfig::GetInstance().GetGameID() + "_CodeDiff.txt"; - File::IOFile f(filename, "r"); - if (!f) - { - ModalMessageBox::information( - this, tr("Code Diff Tool"), - tr("Failed to find or open file: %1").arg(QString::fromStdString(filename))); - return; - }; - - ClearData(); - - std::set blr_addresses; - char line[512]; - while (fgets(line, 512, f.GetHandle())) - { - bool blr = false; - Diff temp; - std::istringstream iss(line); - iss.imbue(std::locale::classic()); - iss >> temp.addr >> temp.hits >> temp.total_hits >> blr >> std::ws; - std::getline(iss, temp.symbol); - - if (blr) - blr_addresses.insert(temp.addr); - - m_include.push_back(std::move(temp)); - } - - Update(UpdateType::Backup); - - for (int i = 0; i < m_matching_results_table->rowCount(); i++) - { - if (blr_addresses.contains(m_matching_results_table->item(i, 4)->data(Qt::UserRole).toUInt())) - MarkRowBLR(i); - } -} - -void CodeDiffDialog::ClearData() -{ - if (m_record_btn->isChecked()) - m_record_btn->toggle(); - ClearBlockCache(); - m_matching_results_table->clear(); - m_matching_results_table->setRowCount(0); - m_matching_results_table->setHorizontalHeaderLabels( - {tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")}); - m_matching_results_table->setEditTriggers(QAbstractItemView::EditTrigger::NoEditTriggers); - m_exclude_size_label->setText(tr("Excluded: %1").arg(0)); - m_include_size_label->setText(tr("Included: %1").arg(0)); - m_exclude_btn->setEnabled(false); - m_include_btn->setEnabled(false); - m_include_active = false; - // Swap is used instead of clear for efficiency in the case of huge m_include/m_exclude - std::vector().swap(m_include); - std::vector().swap(m_exclude); - Core::System::GetInstance().GetJitInterface().SetProfilingState( - JitInterface::ProfilingState::Disabled); -} - -void CodeDiffDialog::ClearBlockCache() -{ - Core::State old_state = Core::GetState(); - - if (old_state == Core::State::Running) - Core::SetState(Core::State::Paused, false); - - Core::System::GetInstance().GetJitInterface().ClearCache(); - - if (old_state == Core::State::Running) - Core::SetState(Core::State::Running); -} - -void CodeDiffDialog::OnRecord(bool enabled) -{ - if (m_failed_requirements) - { - m_failed_requirements = false; - return; - } - - if (Core::GetState() == Core::State::Uninitialized) - { - ModalMessageBox::information(this, tr("Code Diff Tool"), - tr("Emulation must be started to record.")); - m_failed_requirements = true; - m_record_btn->setChecked(false); - return; - } - - if (g_symbolDB.IsEmpty()) - { - ModalMessageBox::warning( - this, tr("Code Diff Tool"), - tr("Symbol map not found.\n\nIf one does not exist, you can generate one from " - "the Menu bar:\nSymbols -> Generate Symbols From ->\n\tAddress | Signature " - "Database | RSO Modules")); - m_failed_requirements = true; - m_record_btn->setChecked(false); - return; - } - - JitInterface::ProfilingState state; - - if (enabled) - { - ClearBlockCache(); - m_record_btn->setText(tr("Stop Recording")); - state = JitInterface::ProfilingState::Enabled; - m_exclude_btn->setEnabled(true); - m_include_btn->setEnabled(true); - } - else - { - ClearBlockCache(); - m_record_btn->setText(tr("Start Recording")); - state = JitInterface::ProfilingState::Disabled; - m_exclude_btn->setEnabled(false); - m_include_btn->setEnabled(false); - } - - m_record_btn->update(); - Core::System::GetInstance().GetJitInterface().SetProfilingState(state); -} - -void CodeDiffDialog::OnInclude() -{ - const auto recorded_symbols = CalculateSymbolsFromProfile(); - - if (recorded_symbols.empty()) - return; - - if (m_include.empty() && m_exclude.empty()) - { - m_include = recorded_symbols; - m_include_active = true; - } - else if (m_include.empty()) - { - // If include becomes empty after having items on it, don't refill it until after a reset. - if (m_include_active) - return; - - // If we are building include for the first time and we have an exlcude list, then include = - // recorded - excluded. - m_include = recorded_symbols; - RemoveMatchingSymbolsFromIncludes(m_exclude); - m_include_active = true; - } - else - { - // If include already exists, keep items that are in both include and recorded. Exclude list - // becomes irrelevant. - RemoveMissingSymbolsFromIncludes(recorded_symbols); - } -} - -void CodeDiffDialog::OnExclude() -{ - const auto recorded_symbols = CalculateSymbolsFromProfile(); - if (m_include.empty() && m_exclude.empty()) - { - m_exclude = recorded_symbols; - } - else if (m_include.empty()) - { - // If there is only an exclude list, update it. - for (auto& iter : recorded_symbols) - { - auto pos = std::lower_bound(m_exclude.begin(), m_exclude.end(), iter.symbol); - - if (pos == m_exclude.end() || pos->symbol != iter.symbol) - m_exclude.insert(pos, iter); - } - } - else - { - // If include already exists, the exclude list will have been used to trim it, so the exclude - // list is now irrelevant, as anythng not on the include list is effectively excluded. - // Exclude/subtract recorded items from the include list. - RemoveMatchingSymbolsFromIncludes(recorded_symbols); - } -} - -std::vector CodeDiffDialog::CalculateSymbolsFromProfile() const -{ - Profiler::ProfileStats prof_stats; - auto& blockstats = prof_stats.block_stats; - Core::System::GetInstance().GetJitInterface().GetProfileResults(&prof_stats); - std::vector current; - current.reserve(20000); - - // Convert blockstats to smaller struct Diff. Exclude repeat functions via symbols. - for (const auto& iter : blockstats) - { - std::string symbol = g_symbolDB.GetDescription(iter.addr); - if (!std::any_of(current.begin(), current.end(), - [&symbol](const Diff& v) { return v.symbol == symbol; })) - { - current.push_back(Diff{ - .addr = iter.addr, - .symbol = std::move(symbol), - .hits = static_cast(iter.run_count), - .total_hits = static_cast(iter.run_count), - }); - } - } - - std::sort(current.begin(), current.end(), - [](const Diff& v1, const Diff& v2) { return (v1.symbol < v2.symbol); }); - - return current; -} - -void CodeDiffDialog::RemoveMissingSymbolsFromIncludes(const std::vector& symbol_diff) -{ - m_include.erase(std::remove_if(m_include.begin(), m_include.end(), - [&](const Diff& v) { - auto arg = std::none_of( - symbol_diff.begin(), symbol_diff.end(), [&](const Diff& p) { - return p.symbol == v.symbol || p.addr == v.addr; - }); - return arg; - }), - m_include.end()); - for (auto& original_includes : m_include) - { - auto pos = std::lower_bound(symbol_diff.begin(), symbol_diff.end(), original_includes.symbol); - if (pos != symbol_diff.end() && - (pos->symbol == original_includes.symbol || pos->addr == original_includes.addr)) - { - original_includes.total_hits += pos->hits; - original_includes.hits = pos->hits; - } - } -} - -void CodeDiffDialog::RemoveMatchingSymbolsFromIncludes(const std::vector& symbol_list) -{ - m_include.erase(std::remove_if(m_include.begin(), m_include.end(), - [&](const Diff& i) { - return std::any_of( - symbol_list.begin(), symbol_list.end(), [&](const Diff& s) { - return i.symbol == s.symbol || i.addr == s.addr; - }); - }), - m_include.end()); -} - -void CodeDiffDialog::Update(UpdateType type) -{ - // Wrap everything in a pause - Core::State old_state = Core::GetState(); - if (old_state == Core::State::Running) - Core::SetState(Core::State::Paused, false); - - // Main process - if (type == UpdateType::Include) - { - OnInclude(); - } - else if (type == UpdateType::Exclude) - { - OnExclude(); - } - - if (type != UpdateType::Backup && m_autosave_check->isChecked() && !m_include.empty()) - SaveDataBackup(); - - const auto create_item = [](const QString& string = {}, const u32 address = 0x00000000) { - QTableWidgetItem* item = new QTableWidgetItem(string); - item->setData(Qt::UserRole, address); - item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - return item; - }; - - int i = 0; - m_matching_results_table->clear(); - m_matching_results_table->setRowCount(i); - m_matching_results_table->setHorizontalHeaderLabels( - {tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")}); - - for (auto& iter : m_include) - { - m_matching_results_table->setRowCount(i + 1); - - QString fix_sym = QString::fromStdString(iter.symbol); - fix_sym.replace(QStringLiteral("\t"), QStringLiteral(" ")); - - m_matching_results_table->setItem( - i, 0, create_item(QStringLiteral("%1").arg(iter.addr, 1, 16), iter.addr)); - m_matching_results_table->setItem( - i, 1, create_item(QStringLiteral("%1").arg(iter.total_hits), iter.addr)); - m_matching_results_table->setItem(i, 2, - create_item(QStringLiteral("%1").arg(iter.hits), iter.addr)); - m_matching_results_table->setItem(i, 3, - create_item(QStringLiteral("%1").arg(fix_sym), iter.addr)); - m_matching_results_table->setItem(i, 4, create_item(QStringLiteral(""), iter.addr)); - i++; - } - - // If we have ruled out all functions from being included. - if (m_include_active && m_include.empty()) - { - m_matching_results_table->setRowCount(1); - m_matching_results_table->setItem(0, 3, create_item(tr("No possible functions left. Reset."))); - } - - m_exclude_size_label->setText(tr("Excluded: %1").arg(m_exclude.size())); - m_include_size_label->setText(tr("Included: %1").arg(m_include.size())); - - Core::System::GetInstance().GetJitInterface().ClearCache(); - if (old_state == Core::State::Running) - Core::SetState(Core::State::Running); -} - -void CodeDiffDialog::InfoDisp() -{ - ModalMessageBox::information( - this, tr("Code Diff Tool Help"), - tr("Used to find functions based on when they should be running.\nSimilar to Cheat Engine " - "Ultimap.\n" - "A symbol map must be loaded prior to use.\n" - "Include/Exclude lists will persist on ending/restarting emulation.\nThese lists " - "will not persist on Dolphin close." - "\n\n'Start Recording': " - "keeps track of what functions run.\n'Stop Recording': erases current " - "recording without any change to the lists.\n'Code did not get executed': click while " - "recording, will add recorded functions to an exclude " - "list, then reset the recording list.\n'Code has been executed': click while recording, " - "will add recorded function to an include list, then reset the recording list.\n\nAfter " - "you use " - "both exclude and include once, the exclude list will be subtracted from the include " - "list " - "and any includes left over will be displayed.\nYou can continue to use " - "'Code did not get executed'/'Code has been executed' to narrow down the " - "results.\n\n" - "Saving will store the current list in Dolphin's Log folder (File -> Open User " - "Folder)")); - ModalMessageBox::information( - this, tr("Code Diff Tool Help"), - tr("Example:\n" - "You want to find a function that runs when HP is modified.\n1. Start recording and " - "play the game without letting HP be modified, then press 'Code did not get " - "executed'.\n2. Immediately gain/lose HP and press 'Code has been executed'.\n3. Repeat " - "1 or 2 to " - "narrow down the results.\nIncludes (Code has been executed) should " - "have short recordings focusing on what you want.\n\nPressing 'Code has been " - "executed' twice will only keep functions that ran for both recordings. Hits will update " - "to reflect the last recording's " - "number of Hits. Total Hits will reflect the total number of " - "times a function has been executed until the lists are cleared with Reset.\n\nRight " - "click -> 'Set blr' will place a " - "blr at the top of the symbol.\n")); -} - -void CodeDiffDialog::OnContextMenu() -{ - if (m_matching_results_table->currentItem() == nullptr) - return; - UpdateItem(); - QMenu* menu = new QMenu(this); - menu->addAction(tr("&Go to start of function"), this, &CodeDiffDialog::OnGoTop); - menu->addAction(tr("Set &blr"), this, &CodeDiffDialog::OnSetBLR); - menu->addAction(tr("&Delete"), this, &CodeDiffDialog::OnDelete); - menu->exec(QCursor::pos()); -} - -void CodeDiffDialog::OnGoTop() -{ - auto item = m_matching_results_table->currentItem(); - if (!item) - return; - Common::Symbol* symbol = g_symbolDB.GetSymbolFromAddr(item->data(Qt::UserRole).toUInt()); - if (!symbol) - return; - m_code_widget->SetAddress(symbol->address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); -} - -void CodeDiffDialog::OnDelete() -{ - // Delete from include list and qtable widget - auto item = m_matching_results_table->currentItem(); - if (!item) - return; - int row = m_matching_results_table->row(item); - if (row == -1) - return; - // TODO: If/when sorting is ever added, .erase needs to find item position instead; leaving as is - // for performance - if (!m_include.empty()) - { - m_include.erase(m_include.begin() + row); - } - m_matching_results_table->removeRow(row); -} - -void CodeDiffDialog::OnSetBLR() -{ - auto item = m_matching_results_table->currentItem(); - if (!item) - return; - - Common::Symbol* symbol = g_symbolDB.GetSymbolFromAddr(item->data(Qt::UserRole).toUInt()); - if (!symbol) - return; - - MarkRowBLR(item->row()); - if (m_autosave_check->isChecked()) - SaveDataBackup(); - - { - auto& system = Core::System::GetInstance(); - Core::CPUThreadGuard guard(system); - system.GetPowerPC().GetDebugInterface().SetPatch(guard, symbol->address, 0x4E800020); - } - - m_code_widget->Update(); -} - -void CodeDiffDialog::MarkRowBLR(int row) -{ - m_matching_results_table->item(row, 0)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 1)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 2)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 3)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 4)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 4)->setText(QStringLiteral("X")); -} - -void CodeDiffDialog::UpdateItem() -{ - QTableWidgetItem* item = m_matching_results_table->currentItem(); - if (!item) - return; - - int row = m_matching_results_table->row(item); - if (row == -1) - return; - uint address = item->data(Qt::UserRole).toUInt(); - - auto symbolName = g_symbolDB.GetDescription(address); - if (symbolName == " --- ") - return; - - QString newName = - QString::fromStdString(symbolName).replace(QStringLiteral("\t"), QStringLiteral(" ")); - m_matching_results_table->item(row, 3)->setText(newName); -} - -void CodeDiffDialog::UpdateButtons(bool running) -{ - m_save_btn->setEnabled(running); - m_load_btn->setEnabled(running); - m_record_btn->setEnabled(running); -} diff --git a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.h b/Source/Core/DolphinQt/Debugger/CodeDiffDialog.h deleted file mode 100644 index c22c63e1ad..0000000000 --- a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.h +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2022 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include "Common/CommonTypes.h" - -class CodeWidget; -class QLabel; -class QPushButton; -class QCheckBox; -class QTableWidget; - -struct Diff -{ - u32 addr = 0; - std::string symbol; - u32 hits = 0; - u32 total_hits = 0; - - bool operator<(const std::string& val) const { return symbol < val; } -}; - -class CodeDiffDialog : public QDialog -{ - Q_OBJECT - -public: - explicit CodeDiffDialog(CodeWidget* parent); - void reject() override; - -private: - enum class UpdateType - { - Include, - Exclude, - Backup - }; - - void CreateWidgets(); - void ConnectWidgets(); - void SaveDataBackup(); - void LoadDataBackup(); - void ClearData(); - void ClearBlockCache(); - void OnClickItem(); - void OnRecord(bool enabled); - std::vector CalculateSymbolsFromProfile() const; - void OnInclude(); - void OnExclude(); - void RemoveMissingSymbolsFromIncludes(const std::vector& symbol_diff); - void RemoveMatchingSymbolsFromIncludes(const std::vector& symbol_list); - void Update(UpdateType type); - void InfoDisp(); - - void OnContextMenu(); - - void OnGoTop(); - void OnDelete(); - void OnSetBLR(); - - void MarkRowBLR(int row); - void UpdateItem(); - void UpdateButtons(bool running); - - QTableWidget* m_matching_results_table; - QCheckBox* m_autosave_check; - QLabel* m_exclude_size_label; - QLabel* m_include_size_label; - QPushButton* m_exclude_btn; - QPushButton* m_include_btn; - QPushButton* m_record_btn; - QPushButton* m_reset_btn; - QPushButton* m_save_btn; - QPushButton* m_load_btn; - QPushButton* m_help_btn; - CodeWidget* m_code_widget; - - std::vector m_exclude; - std::vector m_include; - bool m_failed_requirements = false; - bool m_include_active = false; -}; diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp index 0bf2937dbf..1b9989e401 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp @@ -27,6 +27,7 @@ #include "Core/PowerPC/PPCSymbolDB.h" #include "Core/PowerPC/PowerPC.h" #include "Core/System.h" +#include "DolphinQt/Debugger/BranchWatchDialog.h" #include "DolphinQt/Host.h" #include "DolphinQt/QtUtils/SetWindowDecorations.h" #include "DolphinQt/Settings.h" @@ -35,7 +36,10 @@ static const QString BOX_SPLITTER_STYLESHEET = QStringLiteral( "QSplitter::handle { border-top: 1px dashed black; width: 1px; margin-left: 10px; " "margin-right: 10px; }"); -CodeWidget::CodeWidget(QWidget* parent) : QDockWidget(parent), m_system(Core::System::GetInstance()) +CodeWidget::CodeWidget(QWidget* parent) + : QDockWidget(parent), m_system(Core::System::GetInstance()), + m_branch_watch_dialog( + new BranchWatchDialog(m_system, m_system.GetPowerPC().GetBranchWatch(), this)) { setWindowTitle(tr("Code")); setObjectName(QStringLiteral("code")); @@ -105,7 +109,7 @@ void CodeWidget::CreateWidgets() layout->setSpacing(0); m_search_address = new QLineEdit; - m_code_diff = new QPushButton(tr("Diff")); + m_branch_watch = new QPushButton(tr("Branch Watch")); m_code_view = new CodeViewWidget; m_search_address->setPlaceholderText(tr("Search Address")); @@ -149,7 +153,7 @@ void CodeWidget::CreateWidgets() m_code_splitter->addWidget(m_code_view); layout->addWidget(m_search_address, 0, 0); - layout->addWidget(m_code_diff, 0, 2); + layout->addWidget(m_branch_watch, 0, 2); layout->addWidget(m_code_splitter, 1, 0, -1, -1); QWidget* widget = new QWidget(this); @@ -181,7 +185,7 @@ void CodeWidget::ConnectWidgets() }); connect(m_search_callstack, &QLineEdit::textChanged, this, &CodeWidget::UpdateCallstack); - connect(m_code_diff, &QPushButton::pressed, this, &CodeWidget::OnDiff); + connect(m_branch_watch, &QPushButton::pressed, this, &CodeWidget::OnBranchWatchDialog); connect(m_symbols_list, &QListWidget::itemPressed, this, &CodeWidget::OnSelectSymbol); connect(m_callstack_list, &QListWidget::itemPressed, this, &CodeWidget::OnSelectCallstack); @@ -209,15 +213,11 @@ void CodeWidget::ConnectWidgets() connect(m_code_view, &CodeViewWidget::ShowMemory, this, &CodeWidget::ShowMemory); } -void CodeWidget::OnDiff() +void CodeWidget::OnBranchWatchDialog() { - if (!m_diff_dialog) - m_diff_dialog = new CodeDiffDialog(this); - m_diff_dialog->setWindowFlag(Qt::WindowMinimizeButtonHint); - SetQWidgetWindowDecorations(m_diff_dialog); - m_diff_dialog->show(); - m_diff_dialog->raise(); - m_diff_dialog->activateWindow(); + m_branch_watch_dialog->open(); + m_branch_watch_dialog->raise(); + m_branch_watch_dialog->activateWindow(); } void CodeWidget::OnSearchAddress() @@ -394,6 +394,10 @@ void CodeWidget::UpdateSymbols() } m_symbols_list->sortItems(); + + // TODO: There seems to be a lack of a ubiquitous signal for when symbols change. + // This is the best location to catch the signals from MenuBar and CodeViewWidget. + m_branch_watch_dialog->UpdateSymbols(); } void CodeWidget::UpdateFunctionCalls(const Common::Symbol* symbol) @@ -464,6 +468,9 @@ void CodeWidget::Step() power_pc.SetMode(old_mode); Core::DisplayMessage(tr("Step successful!").toStdString(), 2000); // Will get a UpdateDisasmDialog(), don't update the GUI here. + + // TODO: Step doesn't cause EmulationStateChanged to be emitted, so it has to call this manually. + m_branch_watch_dialog->Update(); } void CodeWidget::StepOver() diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.h b/Source/Core/DolphinQt/Debugger/CodeWidget.h index 1e933a8c70..e0a5679bfb 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.h +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.h @@ -7,9 +7,9 @@ #include #include "Common/CommonTypes.h" -#include "DolphinQt/Debugger/CodeDiffDialog.h" #include "DolphinQt/Debugger/CodeViewWidget.h" +class BranchWatchDialog; class QCloseEvent; class QLineEdit; class QShowEvent; @@ -41,7 +41,7 @@ public: void ShowPC(); void SetPC(); - void OnDiff(); + void OnBranchWatchDialog(); void ToggleBreakpoint(); void AddBreakpoint(); void SetAddress(u32 address, CodeViewWidget::SetAddressUpdate update); @@ -72,9 +72,9 @@ private: Core::System& m_system; - CodeDiffDialog* m_diff_dialog = nullptr; + BranchWatchDialog* m_branch_watch_dialog; QLineEdit* m_search_address; - QPushButton* m_code_diff; + QPushButton* m_branch_watch; QLineEdit* m_search_callstack; QListWidget* m_callstack_list; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 063a048880..4cdb14fb33 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -137,9 +137,10 @@ + + - @@ -349,9 +350,10 @@ + + - diff --git a/Source/Core/UICommon/UICommon.cpp b/Source/Core/UICommon/UICommon.cpp index eff685e5c7..5ff9364dd0 100644 --- a/Source/Core/UICommon/UICommon.cpp +++ b/Source/Core/UICommon/UICommon.cpp @@ -74,6 +74,8 @@ static void CreateDumpPath(std::string path) File::CreateFullPath(File::GetUserPath(D_DUMPFRAMES_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPOBJECTS_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPTEXTURES_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)); } static void CreateLoadPath(std::string path) @@ -253,6 +255,8 @@ void CreateDirectories() File::CreateFullPath(File::GetUserPath(D_DUMPDSP_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPSSL_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPTEXTURES_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)); File::CreateFullPath(File::GetUserPath(D_GAMESETTINGS_IDX)); File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX)); File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + USA_DIR DIR_SEP);