mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2024-11-15 05:47:56 -07:00
1019 lines
39 KiB
C++
1019 lines
39 KiB
C++
// Copyright 2024 Dolphin Emulator Project
|
||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
||
#include "DolphinQt/Debugger/BranchWatchDialog.h"
|
||
|
||
#include <algorithm>
|
||
#include <optional>
|
||
#include <utility>
|
||
|
||
#include <QApplication>
|
||
#include <QCheckBox>
|
||
#include <QClipboard>
|
||
#include <QGridLayout>
|
||
#include <QGroupBox>
|
||
#include <QHeaderView>
|
||
#include <QLineEdit>
|
||
#include <QMenu>
|
||
#include <QMenuBar>
|
||
#include <QPushButton>
|
||
#include <QShortcut>
|
||
#include <QSortFilterProxyModel>
|
||
#include <QStatusBar>
|
||
#include <QString>
|
||
#include <QTableView>
|
||
#include <QTimer>
|
||
#include <QToolBar>
|
||
#include <QVBoxLayout>
|
||
#include <QVariant>
|
||
#include <fmt/format.h>
|
||
|
||
#include "Common/Assert.h"
|
||
#include "Common/CommonFuncs.h"
|
||
#include "Common/CommonTypes.h"
|
||
#include "Common/FileUtil.h"
|
||
#include "Common/IOFile.h"
|
||
#include "Core/ConfigManager.h"
|
||
#include "Core/Core.h"
|
||
#include "Core/Debugger/BranchWatch.h"
|
||
#include "Core/Debugger/PPCDebugInterface.h"
|
||
#include "Core/PowerPC/Gekko.h"
|
||
#include "Core/PowerPC/PowerPC.h"
|
||
#include "Core/System.h"
|
||
#include "DolphinQt/Debugger/BranchWatchTableModel.h"
|
||
#include "DolphinQt/Debugger/CodeWidget.h"
|
||
#include "DolphinQt/QtUtils/DolphinFileDialog.h"
|
||
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
||
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
|
||
#include "DolphinQt/Settings.h"
|
||
|
||
class BranchWatchProxyModel final : public QSortFilterProxyModel
|
||
{
|
||
friend BranchWatchDialog;
|
||
|
||
public:
|
||
explicit BranchWatchProxyModel(const Core::BranchWatch& branch_watch, QObject* parent = nullptr)
|
||
: QSortFilterProxyModel(parent), m_branch_watch(branch_watch)
|
||
{
|
||
}
|
||
|
||
BranchWatchTableModel* sourceModel() const
|
||
{
|
||
return static_cast<BranchWatchTableModel*>(QSortFilterProxyModel::sourceModel());
|
||
}
|
||
void setSourceModel(BranchWatchTableModel* source_model)
|
||
{
|
||
QSortFilterProxyModel::setSourceModel(source_model);
|
||
}
|
||
|
||
// Virtual setSourceModel is forbidden for type-safety reasons. See sourceModel().
|
||
[[noreturn]] void setSourceModel(QAbstractItemModel* source_model) override { Crash(); }
|
||
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
|
||
|
||
template <bool BranchWatchProxyModel::*member>
|
||
void OnToggled(bool enabled)
|
||
{
|
||
this->*member = enabled;
|
||
invalidateRowsFilter();
|
||
}
|
||
template <QString BranchWatchProxyModel::*member>
|
||
void OnSymbolTextChanged(const QString& text)
|
||
{
|
||
this->*member = text;
|
||
invalidateRowsFilter();
|
||
}
|
||
template <std::optional<u32> BranchWatchProxyModel::*member>
|
||
void OnAddressTextChanged(const QString& text)
|
||
{
|
||
bool ok = false;
|
||
if (const u32 value = text.toUInt(&ok, 16); ok)
|
||
this->*member = value;
|
||
else
|
||
this->*member = std::nullopt;
|
||
invalidateRowsFilter();
|
||
}
|
||
void OnDelete(QModelIndexList index_list);
|
||
|
||
bool IsBranchTypeAllowed(UGeckoInstruction inst) const;
|
||
void SetInspected(const QModelIndex& index);
|
||
|
||
private:
|
||
const Core::BranchWatch& m_branch_watch;
|
||
|
||
QString m_origin_symbol_name = {}, m_destin_symbol_name = {};
|
||
std::optional<u32> m_origin_min, m_origin_max, m_destin_min, m_destin_max;
|
||
bool m_b = {}, m_bl = {}, m_bc = {}, m_bcl = {}, m_blr = {}, m_blrl = {}, m_bclr = {},
|
||
m_bclrl = {}, m_bctr = {}, m_bctrl = {}, m_bcctr = {}, m_bcctrl = {};
|
||
bool m_cond_true = {}, m_cond_false = {};
|
||
};
|
||
|
||
bool BranchWatchProxyModel::filterAcceptsRow(int source_row, const QModelIndex&) const
|
||
{
|
||
const Core::BranchWatch::Selection::value_type& value = m_branch_watch.GetSelection()[source_row];
|
||
if (value.condition)
|
||
{
|
||
if (!m_cond_true)
|
||
return false;
|
||
}
|
||
else if (!m_cond_false)
|
||
return false;
|
||
|
||
const Core::BranchWatchCollectionKey& k = value.collection_ptr->first;
|
||
if (!IsBranchTypeAllowed(k.original_inst))
|
||
return false;
|
||
|
||
if (m_origin_min.has_value() && k.origin_addr < m_origin_min.value())
|
||
return false;
|
||
if (m_origin_max.has_value() && k.origin_addr > m_origin_max.value())
|
||
return false;
|
||
if (m_destin_min.has_value() && k.destin_addr < m_destin_min.value())
|
||
return false;
|
||
if (m_destin_max.has_value() && k.destin_addr > m_destin_max.value())
|
||
return false;
|
||
|
||
if (!m_origin_symbol_name.isEmpty())
|
||
{
|
||
if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].origin_name;
|
||
!symbol_name_v.isValid() ||
|
||
!symbol_name_v.value<QString>().contains(m_origin_symbol_name, Qt::CaseInsensitive))
|
||
return false;
|
||
}
|
||
if (!m_destin_symbol_name.isEmpty())
|
||
{
|
||
if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].destin_name;
|
||
!symbol_name_v.isValid() ||
|
||
!symbol_name_v.value<QString>().contains(m_destin_symbol_name, Qt::CaseInsensitive))
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void BranchWatchProxyModel::OnDelete(QModelIndexList index_list)
|
||
{
|
||
std::transform(index_list.begin(), index_list.end(), index_list.begin(),
|
||
[this](const QModelIndex& index) { return mapToSource(index); });
|
||
sourceModel()->OnDelete(std::move(index_list));
|
||
}
|
||
|
||
static constexpr bool BranchSavesLR(UGeckoInstruction inst)
|
||
{
|
||
DEBUG_ASSERT(inst.OPCD == 18 || inst.OPCD == 16 ||
|
||
(inst.OPCD == 19 && (inst.SUBOP10 == 16 || inst.SUBOP10 == 528)));
|
||
// Every branch instruction uses the same LK field.
|
||
return inst.LK;
|
||
}
|
||
|
||
bool BranchWatchProxyModel::IsBranchTypeAllowed(UGeckoInstruction inst) const
|
||
{
|
||
const bool lr_saved = BranchSavesLR(inst);
|
||
switch (inst.OPCD)
|
||
{
|
||
case 18:
|
||
return lr_saved ? m_bl : m_b;
|
||
case 16:
|
||
return lr_saved ? m_bcl : m_bc;
|
||
case 19:
|
||
switch (inst.SUBOP10)
|
||
{
|
||
case 16:
|
||
if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always
|
||
return lr_saved ? m_blrl : m_blr;
|
||
return lr_saved ? m_bclrl : m_bclr;
|
||
case 528:
|
||
if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always
|
||
return lr_saved ? m_bctrl : m_bctr;
|
||
return lr_saved ? m_bcctrl : m_bcctr;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void BranchWatchProxyModel::SetInspected(const QModelIndex& index)
|
||
{
|
||
sourceModel()->SetInspected(mapToSource(index));
|
||
}
|
||
|
||
BranchWatchDialog::BranchWatchDialog(Core::System& system, Core::BranchWatch& branch_watch,
|
||
PPCSymbolDB& ppc_symbol_db, 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, &ppc_symbol_db]() {
|
||
auto* main_layout = new QVBoxLayout;
|
||
|
||
// Controls Toolbar (widgets are added later)
|
||
main_layout->addWidget(m_control_toolbar = new QToolBar);
|
||
|
||
// Branch Watch Table
|
||
main_layout->addWidget(m_table_view = [this, &ppc_symbol_db]() {
|
||
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, ppc_symbol_db));
|
||
m_table_proxy->setSortRole(UserRole::SortRole);
|
||
|
||
m_table_model->setFont(ui_settings.GetDebugFont());
|
||
connect(&ui_settings, &Settings::DebugFontChanged, m_table_model,
|
||
&BranchWatchTableModel::setFont);
|
||
|
||
auto* const table_view = new QTableView;
|
||
table_view->setModel(m_table_proxy);
|
||
table_view->setSortingEnabled(true);
|
||
table_view->sortByColumn(Column::Origin, Qt::AscendingOrder);
|
||
table_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||
table_view->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
table_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
table_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||
table_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
table_view->setCornerButtonEnabled(false);
|
||
table_view->verticalHeader()->hide();
|
||
|
||
QHeaderView* const horizontal_header = table_view->horizontalHeader();
|
||
horizontal_header->restoreState( // Restore column visibility state.
|
||
Settings::GetQSettings()
|
||
.value(QStringLiteral("branchwatchdialog/tableheader/state"))
|
||
.toByteArray());
|
||
horizontal_header->setContextMenuPolicy(Qt::CustomContextMenu);
|
||
horizontal_header->setStretchLastSection(true);
|
||
horizontal_header->setSectionsMovable(true);
|
||
horizontal_header->setFirstSectionMovable(true);
|
||
|
||
connect(table_view, &QTableView::clicked, this, &BranchWatchDialog::OnTableClicked);
|
||
connect(table_view, &QTableView::customContextMenuRequested, this,
|
||
&BranchWatchDialog::OnTableContextMenu);
|
||
connect(horizontal_header, &QHeaderView::customContextMenuRequested, this,
|
||
&BranchWatchDialog::OnTableHeaderContextMenu);
|
||
connect(new QShortcut(QKeySequence(Qt::Key_Delete), this), &QShortcut::activated, this,
|
||
&BranchWatchDialog::OnTableDeleteKeypress);
|
||
|
||
return table_view;
|
||
}());
|
||
|
||
m_mnu_column_visibility = [this]() {
|
||
static constexpr std::array<const char*, Column::NumberOfColumns> headers = {
|
||
QT_TR_NOOP("Instruction"), QT_TR_NOOP("Condition"), QT_TR_NOOP("Origin"),
|
||
QT_TR_NOOP("Destination"), QT_TR_NOOP("Recent Hits"), QT_TR_NOOP("Total Hits"),
|
||
QT_TR_NOOP("Origin Symbol"), QT_TR_NOOP("Destination Symbol")};
|
||
|
||
auto* const menu = new QMenu();
|
||
for (int column = 0; column < Column::NumberOfColumns; ++column)
|
||
{
|
||
QAction* action = menu->addAction(tr(headers[column]), [this, column](bool enabled) {
|
||
m_table_view->setColumnHidden(column, !enabled);
|
||
});
|
||
action->setChecked(!m_table_view->isColumnHidden(column));
|
||
action->setCheckable(true);
|
||
}
|
||
return menu;
|
||
}();
|
||
|
||
// Menu Bar
|
||
main_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
|
||
main_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& placeholder_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(placeholder_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 main_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());
|
||
}
|
||
|
||
BranchWatchDialog::~BranchWatchDialog()
|
||
{
|
||
SaveSettings();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
void BranchWatchDialog::hideEvent(QHideEvent* event)
|
||
{
|
||
if (m_timer->isActive())
|
||
m_timer->stop();
|
||
QDialog::hideEvent(event);
|
||
}
|
||
|
||
void BranchWatchDialog::showEvent(QShowEvent* event)
|
||
{
|
||
if (TimerCondition(m_branch_watch, Core::GetState()))
|
||
m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
|
||
QDialog::showEvent(event);
|
||
}
|
||
|
||
void BranchWatchDialog::OnStartPause(bool checked)
|
||
{
|
||
if (checked)
|
||
{
|
||
m_branch_watch.Start();
|
||
m_btn_start_pause->setText(tr("Pause Branch Watch"));
|
||
// Restart the timer if the situation calls for it, but always turn off single-shot.
|
||
m_timer->setSingleShot(false);
|
||
if (Core::GetState() > Core::State::Paused)
|
||
m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
|
||
}
|
||
else
|
||
{
|
||
m_branch_watch.Pause();
|
||
m_btn_start_pause->setText(tr("Start Branch Watch"));
|
||
// Schedule one last update in the future in case Branch Watch is in the middle of a hit.
|
||
if (Core::GetState() > Core::State::Paused)
|
||
m_timer->setInterval(BRANCH_WATCH_TOOL_TIMER_PAUSE_ONESHOT_MS);
|
||
m_timer->setSingleShot(true);
|
||
}
|
||
Update();
|
||
}
|
||
|
||
void BranchWatchDialog::OnClearBranchWatch()
|
||
{
|
||
{
|
||
const Core::CPUThreadGuard guard{m_system};
|
||
m_table_model->OnClearBranchWatch(guard);
|
||
AutoSave(guard);
|
||
}
|
||
m_btn_wipe_recent_hits->setEnabled(false);
|
||
UpdateStatus();
|
||
}
|
||
|
||
static std::string GetSnapshotDefaultFilepath()
|
||
{
|
||
return fmt::format("{}{}.txt", File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX),
|
||
SConfig::GetInstance().GetGameID());
|
||
}
|
||
|
||
void BranchWatchDialog::OnSave()
|
||
{
|
||
if (!m_branch_watch.CanSave())
|
||
{
|
||
ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!"));
|
||
return;
|
||
}
|
||
|
||
Save(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath());
|
||
}
|
||
|
||
void BranchWatchDialog::OnSaveAs()
|
||
{
|
||
if (!m_branch_watch.CanSave())
|
||
{
|
||
ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!"));
|
||
return;
|
||
}
|
||
|
||
const QString filepath = DolphinFileDialog::getSaveFileName(
|
||
this, tr("Save Branch Watch snapshot"),
|
||
QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
|
||
tr("Text file (*.txt);;All Files (*)"));
|
||
if (filepath.isEmpty())
|
||
return;
|
||
|
||
Save(Core::CPUThreadGuard{m_system}, filepath.toStdString());
|
||
}
|
||
|
||
void BranchWatchDialog::OnLoad()
|
||
{
|
||
Load(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath());
|
||
}
|
||
|
||
void BranchWatchDialog::OnLoadFrom()
|
||
{
|
||
const QString filepath = DolphinFileDialog::getOpenFileName(
|
||
this, tr("Load Branch Watch snapshot"),
|
||
QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
|
||
tr("Text file (*.txt);;All Files (*)"), nullptr, QFileDialog::Option::ReadOnly);
|
||
if (filepath.isEmpty())
|
||
return;
|
||
|
||
Load(Core::CPUThreadGuard{m_system}, filepath.toStdString());
|
||
}
|
||
|
||
void BranchWatchDialog::OnCodePathWasTaken()
|
||
{
|
||
{
|
||
const Core::CPUThreadGuard guard{m_system};
|
||
m_table_model->OnCodePathWasTaken(guard);
|
||
AutoSave(guard);
|
||
}
|
||
m_btn_wipe_recent_hits->setEnabled(true);
|
||
UpdateStatus();
|
||
}
|
||
|
||
void BranchWatchDialog::OnCodePathNotTaken()
|
||
{
|
||
{
|
||
const Core::CPUThreadGuard guard{m_system};
|
||
m_table_model->OnCodePathNotTaken(guard);
|
||
AutoSave(guard);
|
||
}
|
||
UpdateStatus();
|
||
}
|
||
|
||
void BranchWatchDialog::OnBranchWasOverwritten()
|
||
{
|
||
if (Core::GetState() == Core::State::Uninitialized)
|
||
{
|
||
ModalMessageBox::warning(this, tr("Error"), tr("Core is uninitialized."));
|
||
return;
|
||
}
|
||
{
|
||
const Core::CPUThreadGuard guard{m_system};
|
||
m_table_model->OnBranchWasOverwritten(guard);
|
||
AutoSave(guard);
|
||
}
|
||
UpdateStatus();
|
||
}
|
||
|
||
void BranchWatchDialog::OnBranchNotOverwritten()
|
||
{
|
||
if (Core::GetState() == Core::State::Uninitialized)
|
||
{
|
||
ModalMessageBox::warning(this, tr("Error"), tr("Core is uninitialized."));
|
||
return;
|
||
}
|
||
{
|
||
const Core::CPUThreadGuard guard{m_system};
|
||
m_table_model->OnBranchNotOverwritten(guard);
|
||
AutoSave(guard);
|
||
}
|
||
UpdateStatus();
|
||
}
|
||
|
||
void BranchWatchDialog::OnWipeRecentHits()
|
||
{
|
||
m_table_model->OnWipeRecentHits();
|
||
}
|
||
|
||
void BranchWatchDialog::OnWipeInspection()
|
||
{
|
||
m_table_model->OnWipeInspection();
|
||
}
|
||
|
||
void BranchWatchDialog::OnTimeout()
|
||
{
|
||
Update();
|
||
}
|
||
|
||
void BranchWatchDialog::OnEmulationStateChanged(Core::State new_state)
|
||
{
|
||
if (!isVisible())
|
||
return;
|
||
|
||
if (TimerCondition(m_branch_watch, new_state))
|
||
m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
|
||
else if (m_timer->isActive())
|
||
m_timer->stop();
|
||
Update();
|
||
}
|
||
|
||
void BranchWatchDialog::OnHelp()
|
||
{
|
||
ModalMessageBox::information(
|
||
this, tr("Branch Watch Tool Help (1/4)"),
|
||
tr("Branch Watch is a code-searching tool that can isolate branches tracked by the emulated "
|
||
"CPU by testing candidate branches with simple criteria. If you are familiar with Cheat "
|
||
"Engine's Ultimap, Branch Watch is similar to that.\n\n"
|
||
"Press the \"Start Branch Watch\" button to activate Branch Watch. Branch Watch persists "
|
||
"across emulation sessions, and a snapshot of your progress can be saved to and loaded "
|
||
"from the User Directory to persist after Dolphin Emulator is closed. \"Save As...\" and "
|
||
"\"Load From...\" actions are also available, and auto-saving can be enabled to save a "
|
||
"snapshot at every step of a search. The \"Pause Branch Watch\" button will halt Branch "
|
||
"Watch from tracking further branch hits until it is told to resume. Press the \"Clear "
|
||
"Branch Watch\" button to clear all candidates and return to the blacklist phase."));
|
||
ModalMessageBox::information(
|
||
this, tr("Branch Watch Tool Help (2/4)"),
|
||
tr("Branch Watch starts in the blacklist phase, meaning no candidates have been chosen yet, "
|
||
"but candidates found so far can be excluded from the candidacy by pressing the \"Code "
|
||
"Path Not Taken\", \"Branch Was Overwritten\", and \"Branch Not Overwritten\" buttons. "
|
||
"Once the \"Code Path Was Taken\" button is pressed for the first time, Branch Watch will "
|
||
"switch to the reduction phase, and the table will populate with all eligible "
|
||
"candidates."));
|
||
ModalMessageBox::information(
|
||
this, tr("Branch Watch Tool Help (3/4)"),
|
||
tr("Once in the reduction phase, it is time to start narrowing down the candidates shown in "
|
||
"the table. Further reduce the candidates by checking whether a code path was or was not "
|
||
"taken since the last time it was checked. It is also possible to reduce the candidates "
|
||
"by determining whether a branch instruction has or has not been overwritten since it was "
|
||
"first hit. Filter the candidates by branch kind, branch condition, origin or destination "
|
||
"address, and origin or destination symbol name.\n\n"
|
||
"After enough passes and experimentation, you may be able to find function calls and "
|
||
"conditional code paths that are only taken when an action is performed in the emulated "
|
||
"software."));
|
||
ModalMessageBox::information(
|
||
this, tr("Branch Watch Tool Help (4/4)"),
|
||
tr("Rows in the table can be left-clicked on the origin, destination, and symbol columns to "
|
||
"view the associated address in Code View. Right-clicking the selected row(s) will bring "
|
||
"up a context menu.\n\n"
|
||
"If the origin column of a row selection is right-clicked, an action to replace the "
|
||
"branch instruction at the origin(s) with a NOP instruction (No Operation), and an action "
|
||
"to copy the address(es) to the clipboard will be available.\n\n"
|
||
"If the destination column of a row selection is right-clicked, an action to replace the "
|
||
"instruction at the destination(s) with a BLR instruction (Branch to Link Register) will "
|
||
"be available, but only if the branch instruction at every origin saves the link "
|
||
"register, and an action to copy the address(es) to the clipboard will be available.\n\n"
|
||
"If the origin / destination symbol column of a row selection is right-clicked, an action "
|
||
"to replace the instruction(s) at the start of the symbol with a BLR instruction will be "
|
||
"available, but only if every origin / destination symbol is found.\n\n"
|
||
"All context menus have the action to delete the selected row(s) from the candidates."));
|
||
}
|
||
|
||
void BranchWatchDialog::OnToggleAutoSave(bool checked)
|
||
{
|
||
if (!checked)
|
||
return;
|
||
|
||
const QString filepath = DolphinFileDialog::getSaveFileName(
|
||
this, tr("Select Branch Watch snapshot auto-save file (for user folder location, cancel)"),
|
||
QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
|
||
tr("Text file (*.txt);;All Files (*)"));
|
||
if (filepath.isEmpty())
|
||
m_autosave_filepath = std::nullopt;
|
||
else
|
||
m_autosave_filepath = filepath.toStdString();
|
||
}
|
||
|
||
void BranchWatchDialog::OnHideShowControls(bool checked)
|
||
{
|
||
if (checked)
|
||
m_control_toolbar->hide();
|
||
else
|
||
m_control_toolbar->show();
|
||
}
|
||
|
||
void BranchWatchDialog::OnToggleIgnoreApploader(bool checked)
|
||
{
|
||
m_system.SetIsBranchWatchIgnoreApploader(checked);
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableClicked(const QModelIndex& index)
|
||
{
|
||
const QVariant v = m_table_proxy->data(index, UserRole::ClickRole);
|
||
switch (index.column())
|
||
{
|
||
case Column::OriginSymbol:
|
||
case Column::DestinSymbol:
|
||
if (!v.isValid())
|
||
return;
|
||
[[fallthrough]];
|
||
case Column::Origin:
|
||
case Column::Destination:
|
||
m_code_widget->SetAddress(v.value<u32>(), CodeViewWidget::SetAddressUpdate::WithDetailedUpdate);
|
||
return;
|
||
}
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableContextMenu(const QPoint& pos)
|
||
{
|
||
const QModelIndex index = m_table_view->indexAt(pos);
|
||
if (!index.isValid())
|
||
return;
|
||
QModelIndexList index_list = m_table_view->selectionModel()->selectedRows(index.column());
|
||
|
||
QMenu* const menu = new QMenu;
|
||
menu->addAction(tr("&Delete"), [this, index_list]() { OnTableDelete(std::move(index_list)); });
|
||
switch (index.column())
|
||
{
|
||
case Column::Origin:
|
||
{
|
||
QAction* const action = menu->addAction(tr("Insert &NOP"));
|
||
if (Core::GetState() != Core::State::Uninitialized)
|
||
connect(action, &QAction::triggered,
|
||
[this, index_list]() { OnTableSetNOP(std::move(index_list)); });
|
||
else
|
||
action->setEnabled(false);
|
||
menu->addAction(tr("&Copy Address"), [this, index_list = std::move(index_list)]() {
|
||
OnTableCopyAddress(std::move(index_list));
|
||
});
|
||
break;
|
||
}
|
||
case Column::Destination:
|
||
{
|
||
QAction* const action = menu->addAction(tr("Insert &BLR"));
|
||
const bool enable_action =
|
||
Core::GetState() != Core::State::Uninitialized &&
|
||
std::all_of(index_list.begin(), index_list.end(), [this](const QModelIndex& idx) {
|
||
const QModelIndex sibling = idx.siblingAtColumn(Column::Instruction);
|
||
return BranchSavesLR(m_table_proxy->data(sibling, UserRole::ClickRole).value<u32>());
|
||
});
|
||
if (enable_action)
|
||
connect(action, &QAction::triggered,
|
||
[this, index_list]() { OnTableSetBLR(std::move(index_list)); });
|
||
else
|
||
action->setEnabled(false);
|
||
menu->addAction(tr("&Copy Address"), [this, index_list = std::move(index_list)]() {
|
||
OnTableCopyAddress(std::move(index_list));
|
||
});
|
||
break;
|
||
}
|
||
case Column::OriginSymbol:
|
||
case Column::DestinSymbol:
|
||
{
|
||
QAction* const action = menu->addAction(tr("Insert &BLR at start"));
|
||
const bool enable_action =
|
||
Core::GetState() != Core::State::Uninitialized &&
|
||
std::all_of(index_list.begin(), index_list.end(), [this](const QModelIndex& idx) {
|
||
return m_table_proxy->data(idx, UserRole::ClickRole).isValid();
|
||
});
|
||
if (enable_action)
|
||
connect(action, &QAction::triggered, [this, index_list = std::move(index_list)]() {
|
||
OnTableSetBLR(std::move(index_list));
|
||
});
|
||
else
|
||
action->setEnabled(false);
|
||
break;
|
||
}
|
||
}
|
||
menu->exec(m_table_view->viewport()->mapToGlobal(pos));
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableHeaderContextMenu(const QPoint& pos)
|
||
{
|
||
m_mnu_column_visibility->exec(m_table_view->horizontalHeader()->mapToGlobal(pos));
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableDelete(QModelIndexList index_list)
|
||
{
|
||
m_table_proxy->OnDelete(std::move(index_list));
|
||
UpdateStatus();
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableDeleteKeypress()
|
||
{
|
||
OnTableDelete(m_table_view->selectionModel()->selectedRows());
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableSetBLR(QModelIndexList index_list)
|
||
{
|
||
for (const QModelIndex& index : index_list)
|
||
{
|
||
m_system.GetPowerPC().GetDebugInterface().SetPatch(
|
||
Core::CPUThreadGuard{m_system},
|
||
m_table_proxy->data(index, UserRole::ClickRole).value<u32>(), 0x4e800020);
|
||
m_table_proxy->SetInspected(index);
|
||
}
|
||
// TODO: This is not ideal. What I need is a signal for when memory has been changed by the GUI,
|
||
// but I cannot find one. UpdateDisasmDialog comes close, but does too much in one signal. For
|
||
// example, CodeViewWidget will scroll to the current PC when UpdateDisasmDialog is signaled. This
|
||
// seems like a pervasive issue. For example, modifying an instruction in the CodeViewWidget will
|
||
// not reflect in the MemoryViewWidget, and vice versa. Neither of these widgets changing memory
|
||
// will reflect in the JITWidget, either. At the very least, we can make sure the CodeWidget
|
||
// is updated in an acceptable way.
|
||
m_code_widget->Update();
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableSetNOP(QModelIndexList index_list)
|
||
{
|
||
for (const QModelIndex& index : index_list)
|
||
{
|
||
m_system.GetPowerPC().GetDebugInterface().SetPatch(
|
||
Core::CPUThreadGuard{m_system},
|
||
m_table_proxy->data(index, UserRole::ClickRole).value<u32>(), 0x60000000);
|
||
m_table_proxy->SetInspected(index);
|
||
}
|
||
// Same issue as OnSetBLR.
|
||
m_code_widget->Update();
|
||
}
|
||
|
||
void BranchWatchDialog::OnTableCopyAddress(QModelIndexList index_list)
|
||
{
|
||
auto iter = index_list.begin();
|
||
if (iter == index_list.end())
|
||
return;
|
||
|
||
QString text;
|
||
text.reserve(index_list.size() * 9 - 1);
|
||
while (true)
|
||
{
|
||
text.append(QString::number(m_table_proxy->data(*iter, UserRole::ClickRole).value<u32>(), 16));
|
||
if (++iter == index_list.end())
|
||
break;
|
||
text.append(QChar::fromLatin1('\n'));
|
||
}
|
||
QApplication::clipboard()->setText(text);
|
||
}
|
||
|
||
void BranchWatchDialog::SaveSettings()
|
||
{
|
||
auto& settings = Settings::GetQSettings();
|
||
settings.setValue(QStringLiteral("branchwatchdialog/geometry"), saveGeometry());
|
||
settings.setValue(QStringLiteral("branchwatchdialog/tableheader/state"),
|
||
m_table_view->horizontalHeader()->saveState());
|
||
}
|
||
|
||
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());
|
||
}
|