From 52ccc609cd1f35ab6fea6deea61caea1c91ce190 Mon Sep 17 00:00:00 2001 From: Dan McCarthy Date: Thu, 18 Jan 2024 19:15:40 -0600 Subject: [PATCH] Debugger: Adds loading breakpoints/saved addresses from settings Adds `/inis/debuggersettings/` settings folder to contain settings specifically for the debugger. Adds functionality to manually save (to settings) Breakpoints/Saved addresses and automatically load them upon launching the debugger. --- .gitignore | 1 + pcsx2-qt/CMakeLists.txt | 2 + pcsx2-qt/Debugger/CpuWidget.cpp | 233 +++++++----------- pcsx2-qt/Debugger/CpuWidget.h | 3 + pcsx2-qt/Debugger/DebuggerSettingsManager.cpp | 164 ++++++++++++ pcsx2-qt/Debugger/DebuggerSettingsManager.h | 29 +++ pcsx2-qt/Debugger/Models/BreakpointModel.cpp | 126 ++++++++++ pcsx2-qt/Debugger/Models/BreakpointModel.h | 2 + .../Debugger/Models/SavedAddressesModel.cpp | 69 +++++- .../Debugger/Models/SavedAddressesModel.h | 4 +- pcsx2-qt/pcsx2-qt.vcxproj | 2 + pcsx2-qt/pcsx2-qt.vcxproj.filters | 6 + pcsx2/Config.h | 1 + pcsx2/Pcsx2Config.cpp | 4 + pcsx2/VMManager.cpp | 16 ++ pcsx2/VMManager.h | 6 + 16 files changed, 513 insertions(+), 155 deletions(-) create mode 100644 pcsx2-qt/Debugger/DebuggerSettingsManager.cpp create mode 100644 pcsx2-qt/Debugger/DebuggerSettingsManager.h diff --git a/.gitignore b/.gitignore index e08224536f..dbfa057e55 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ oprofile_data/ /bin/gamesettings /bin/help /bin/inis +/bin/inis/debuggersettings /bin/logs /bin/memcards /bin/plugins diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 5840b74403..3bc152df1f 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -149,6 +149,8 @@ target_sources(pcsx2-qt PRIVATE Debugger/CpuWidget.cpp Debugger/CpuWidget.h Debugger/CpuWidget.ui + Debugger/DebuggerSettingsManager.cpp + Debugger/DebuggerSettingsManager.h Debugger/DebuggerWindow.cpp Debugger/DebuggerWindow.h Debugger/DebuggerWindow.ui diff --git a/pcsx2-qt/Debugger/CpuWidget.cpp b/pcsx2-qt/Debugger/CpuWidget.cpp index 2e026e639d..961c0f1fa0 100644 --- a/pcsx2-qt/Debugger/CpuWidget.cpp +++ b/pcsx2-qt/Debugger/CpuWidget.cpp @@ -8,6 +8,7 @@ #include "Models/BreakpointModel.h" #include "Models/ThreadModel.h" #include "Models/SavedAddressesModel.h" +#include "Debugger/DebuggerSettingsManager.h" #include "DebugTools/DebugInterface.h" #include "DebugTools/Breakpoints.h" @@ -45,6 +46,15 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) m_ui.setupUi(this); connect(g_emu_thread, &EmuThread::onVMPaused, this, &CpuWidget::onVMPaused); + connect(g_emu_thread, &EmuThread::onGameChanged, [this](const QString& title) { + if (title.isEmpty()) + return; + // Don't overwrite users BPs/Saved Addresses unless they have a clean state. + if (m_bpModel.rowCount() == 0) + DebuggerSettingsManager::loadGameSettings(&m_bpModel); + if (m_savedAddressesModel.rowCount() == 0) + DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel); + }); connect(m_ui.registerWidget, &RegisterWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress); connect(m_ui.memoryviewWidget, &MemoryViewWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress); @@ -143,9 +153,12 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) m_ui.savedAddressesList->horizontalHeader()->setSectionResizeMode(i++, mode); } QTableView* savedAddressesTableView = m_ui.savedAddressesList; - connect(m_ui.savedAddressesList->model(), &QAbstractItemModel::dataChanged, [savedAddressesTableView](const QModelIndex& topLeft) { + connect(m_ui.savedAddressesList->model(), &QAbstractItemModel::dataChanged, [savedAddressesTableView](const QModelIndex& topLeft) { savedAddressesTableView->resizeColumnToContents(topLeft.column()); }); + + DebuggerSettingsManager::loadGameSettings(&m_bpModel); + DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel); } CpuWidget::~CpuWidget() = default; @@ -301,46 +314,63 @@ void CpuWidget::onBPListDoubleClicked(const QModelIndex& index) void CpuWidget::onBPListContextMenu(QPoint pos) { - if (!m_cpu.isAlive()) - return; - QMenu* contextMenu = new QMenu(tr("Breakpoint List Context Menu"), m_ui.breakpointList); - - QAction* newAction = new QAction(tr("New"), m_ui.breakpointList); - connect(newAction, &QAction::triggered, this, &CpuWidget::contextBPListNew); - contextMenu->addAction(newAction); - - const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel(); - - if (selModel->hasSelection()) + if (m_cpu.isAlive()) { - QAction* editAction = new QAction(tr("Edit"), m_ui.breakpointList); - connect(editAction, &QAction::triggered, this, &CpuWidget::contextBPListEdit); - contextMenu->addAction(editAction); - if (selModel->selectedIndexes().count() == 1) + QAction* newAction = new QAction(tr("New"), m_ui.breakpointList); + connect(newAction, &QAction::triggered, this, &CpuWidget::contextBPListNew); + contextMenu->addAction(newAction); + + const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel(); + + if (selModel->hasSelection()) { - QAction* copyAction = new QAction(tr("Copy"), m_ui.breakpointList); - connect(copyAction, &QAction::triggered, this, &CpuWidget::contextBPListCopy); - contextMenu->addAction(copyAction); - } + QAction* editAction = new QAction(tr("Edit"), m_ui.breakpointList); + connect(editAction, &QAction::triggered, this, &CpuWidget::contextBPListEdit); + contextMenu->addAction(editAction); - QAction* deleteAction = new QAction(tr("Delete"), m_ui.breakpointList); - connect(deleteAction, &QAction::triggered, this, &CpuWidget::contextBPListDelete); - contextMenu->addAction(deleteAction); + if (selModel->selectedIndexes().count() == 1) + { + QAction* copyAction = new QAction(tr("Copy"), m_ui.breakpointList); + connect(copyAction, &QAction::triggered, this, &CpuWidget::contextBPListCopy); + contextMenu->addAction(copyAction); + } + + QAction* deleteAction = new QAction(tr("Delete"), m_ui.breakpointList); + connect(deleteAction, &QAction::triggered, this, &CpuWidget::contextBPListDelete); + contextMenu->addAction(deleteAction); + } } contextMenu->addSeparator(); - QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.breakpointList); - connect(actionExport, &QAction::triggered, [this]() { - // It's important to use the Export Role here to allow pasting to be translation agnostic - QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.breakpointList->model(), BreakpointModel::ExportRole, true)); - }); - contextMenu->addAction(actionExport); + if (m_bpModel.rowCount() > 0) + { + QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.breakpointList); + connect(actionExport, &QAction::triggered, [this]() { + // It's important to use the Export Role here to allow pasting to be translation agnostic + QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.breakpointList->model(), BreakpointModel::ExportRole, true)); + }); + contextMenu->addAction(actionExport); + } - QAction* actionImport = new QAction(tr("Paste from CSV"), m_ui.breakpointList); - connect(actionImport, &QAction::triggered, this, &CpuWidget::contextBPListPasteCSV); - contextMenu->addAction(actionImport); + if (m_cpu.isAlive()) + { + QAction* actionImport = new QAction(tr("Paste from CSV"), m_ui.breakpointList); + connect(actionImport, &QAction::triggered, this, &CpuWidget::contextBPListPasteCSV); + contextMenu->addAction(actionImport); + + QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.breakpointList); + connect(actionLoad, &QAction::triggered, [this]() { + m_bpModel.clear(); + DebuggerSettingsManager::loadGameSettings(&m_bpModel); + }); + contextMenu->addAction(actionLoad); + + QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.breakpointList); + connect(actionSave, &QAction::triggered, this, &CpuWidget::saveBreakpointsToDebuggerSettings); + contextMenu->addAction(actionSave); + } contextMenu->popup(m_ui.breakpointList->viewport()->mapToGlobal(pos)); } @@ -407,7 +437,7 @@ void CpuWidget::contextBPListPasteCSV() // In order to handle text with commas in them we must wrap values in quotes to mark // where a value starts and end so that text commas aren't identified as delimiters. // So matches each quote pair, parse it out, and removes the quotes to get the value. - QRegularExpression eachQuotePair(R"("([^"]|\\.)*")"); + QRegularExpression eachQuotePair(R"("([^"]|\\.)*")"); QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line); while (it.hasNext()) { @@ -415,98 +445,7 @@ void CpuWidget::contextBPListPasteCSV() QString matchedValue = match.captured(0); fields << matchedValue.mid(1, matchedValue.length() - 2); } - - if (fields.size() != BreakpointModel::BreakpointColumns::COLUMN_COUNT) - { - Console.WriteLn("Debugger CSV Import: Invalid number of columns, skipping"); - continue; - } - - bool ok; - const int type = fields[BreakpointModel::BreakpointColumns::TYPE].toUInt(&ok); - if (!ok) - { - Console.WriteLn("Debugger CSV Import: Failed to parse type '%s', skipping", fields[BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData()); - continue; - } - - // This is how we differentiate between breakpoints and memchecks - if (type == MEMCHECK_INVALID) - { - BreakPoint bp; - - // Address - bp.addr = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16); - if (!ok) - { - Console.WriteLn("Debugger CSV Import: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData()); - continue; - } - - // Condition - if (!fields[BreakpointModel::BreakpointColumns::CONDITION].isEmpty()) - { - PostfixExpression expr; - bp.hasCond = true; - bp.cond.debug = &m_cpu; - - if (!m_cpu.initExpression(fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), expr)) - { - Console.WriteLn("Debugger CSV Import: Failed to parse cond '%s', skipping", fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData()); - continue; - } - bp.cond.expression = expr; - strncpy(&bp.cond.expressionString[0], fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), sizeof(bp.cond.expressionString)); - } - - // Enabled - bp.enabled = fields[BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok); - if (!ok) - { - Console.WriteLn("Debugger CSV Import: Failed to parse enable flag '%s', skipping", fields[BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData()); - continue; - } - - m_bpModel.insertBreakpointRows(0, 1, {bp}); - } - else - { - MemCheck mc; - // Mode - if (type >= MEMCHECK_INVALID) - { - Console.WriteLn("Debugger CSV Import: Failed to parse cond type '%s', skipping", fields [BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData()); - continue; - } - mc.cond = static_cast(type); - - // Address - mc.start = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16); - if (!ok) - { - Console.WriteLn("Debugger CSV Import: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData()); - continue; - } - - // Size - mc.end = fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUInt(&ok) + mc.start; - if (!ok) - { - Console.WriteLn("Debugger CSV Import: Failed to parse length '%s', skipping", fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUtf8().constData()); - continue; - } - - // Result - const int result = fields [BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok); - if (!ok) - { - Console.WriteLn("Debugger CSV Import: Failed to parse result flag '%s', skipping", fields [BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData()); - continue; - } - mc.result = static_cast(result); - - m_bpModel.insertBreakpointRows(0, 1, {mc}); - } + m_bpModel.loadBreakpointFromFieldList(fields); } } @@ -561,7 +500,19 @@ void CpuWidget::onSavedAddressesListContextMenu(QPoint pos) connect(actionImportCSV, &QAction::triggered, this, &CpuWidget::contextSavedAddressesListPasteCSV); contextMenu->addAction(actionImportCSV); - contextMenu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos)); + if (m_cpu.isAlive()) + { + QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.savedAddressesList); + connect(actionLoad, &QAction::triggered, [this]() { + m_savedAddressesModel.clear(); + DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel); + }); + contextMenu->addAction(actionLoad); + + QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.savedAddressesList); + connect(actionSave, &QAction::triggered, this, &CpuWidget::saveSavedAddressesToDebuggerSettings); + contextMenu->addAction(actionSave); + } if (isIndexValid) { @@ -571,6 +522,8 @@ void CpuWidget::onSavedAddressesListContextMenu(QPoint pos) }); contextMenu->addAction(deleteAction); } + + contextMenu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos)); } void CpuWidget::contextSavedAddressesListPasteCSV() @@ -594,24 +547,7 @@ void CpuWidget::contextSavedAddressesListPasteCSV() fields << matchedValue.mid(1, matchedValue.length() - 2); } - if (fields.size() != SavedAddressesModel::HeaderColumns::COLUMN_COUNT) - { - Console.WriteLn("Debugger CSV Import: Invalid number of columns, skipping"); - continue; - } - - bool ok; - const u32 address = fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUInt(&ok, 16); - if (!ok) - { - Console.WriteLn("Debugger CSV Import: Failed to parse address '%s', skipping", fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUtf8().constData()); - continue; - } - - const QString label = fields[SavedAddressesModel::HeaderColumns::LABEL]; - const QString description = fields[SavedAddressesModel::HeaderColumns::DESCRIPTION]; - const SavedAddressesModel::SavedAddress importedAddress = {address, label, description}; - m_savedAddressesModel.addRow(importedAddress); + m_savedAddressesModel.loadSavedAddressFromFieldList(fields); } } @@ -1392,3 +1328,12 @@ void CpuWidget::loadSearchResults() } } +void CpuWidget::saveBreakpointsToDebuggerSettings() +{ + DebuggerSettingsManager::saveGameSettings(&m_bpModel); +} + +void CpuWidget::saveSavedAddressesToDebuggerSettings() +{ + DebuggerSettingsManager::saveGameSettings(&m_savedAddressesModel); +} diff --git a/pcsx2-qt/Debugger/CpuWidget.h b/pcsx2-qt/Debugger/CpuWidget.h index 54a63d0558..ce12fc9854 100644 --- a/pcsx2-qt/Debugger/CpuWidget.h +++ b/pcsx2-qt/Debugger/CpuWidget.h @@ -118,6 +118,9 @@ public slots: void contextRemoveSearchResult(); void onListSearchResultsContextMenu(QPoint pos); + void saveBreakpointsToDebuggerSettings(); + void saveSavedAddressesToDebuggerSettings(); + private: std::vector m_registerTableViews; std::vector m_searchResults; diff --git a/pcsx2-qt/Debugger/DebuggerSettingsManager.cpp b/pcsx2-qt/Debugger/DebuggerSettingsManager.cpp new file mode 100644 index 0000000000..4151278a4b --- /dev/null +++ b/pcsx2-qt/Debugger/DebuggerSettingsManager.cpp @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-License-Identifier: LGPL-3.0+ + +#include "DebuggerSettingsManager.h" + +#include +#include +#include +#include + +#include "common/Console.h" +#include "fmt/core.h" +#include "VMManager.h" +#include "Models/BreakpointModel.h" + +std::mutex DebuggerSettingsManager::writeLock; +const QString DebuggerSettingsManager::settingsFileVersion = "0.00"; + +QJsonObject DebuggerSettingsManager::loadGameSettingsJSON() { + std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame(); + QFile file(QString::fromStdString(path)); + if (!file.open(QIODevice::ReadOnly)) + { + Console.WriteLnFmt("Debugger Settings Manager: No Debugger Settings file found for game at: '{}'", path); + return QJsonObject(); + } + QByteArray fileContent = file.readAll(); + file.close(); + + const QJsonDocument jsonDoc(QJsonDocument::fromJson(fileContent)); + if (jsonDoc.isNull() || !jsonDoc.isObject()) + { + Console.WriteLnFmt("Debugger Settings Manager: Failed to load contents of settings file for file at: '{}'", path); + return QJsonObject(); + } + + return jsonDoc.object(); +} + +void DebuggerSettingsManager::writeJSONToPath(std::string path, QJsonDocument jsonDocument) +{ + QFile file(QString::fromStdString(path)); + if (!file.open(QIODevice::WriteOnly)) + { + Console.WriteLnFmt("Debugger Settings Manager: Failed to write Debugger Settings file to path: '{}'", path); + return; + } + file.write(jsonDocument.toJson(QJsonDocument::Indented)); + file.close(); +} + +void DebuggerSettingsManager::loadGameSettings(BreakpointModel* bpModel) +{ + const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame(); + if (path.empty()) + return; + + const QJsonValue breakpointsValue = loadGameSettingsJSON().value("Breakpoints"); + const QString valueToLoad = breakpointsValue.toString(); + if (breakpointsValue.isUndefined() || !breakpointsValue.isArray()) + { + Console.WriteLnFmt("Debugger Settings Manager: Failed to read Breakpoints array from settings file: '{}'", path); + return; + } + + const QJsonArray breakpointsArray = breakpointsValue.toArray(); + for (u32 row = 0; row < breakpointsArray.size(); row++) + { + const QJsonValue rowValue = breakpointsArray.at(row); + if (rowValue.isUndefined() || !rowValue.isObject()) + { + Console.WriteLn("Debugger Settings Manager: Failed to load invalid Breakpoint object."); + continue; + } + const QJsonObject rowObject = rowValue.toObject(); + + QStringList fields; + u32 col = 0; + for (auto iter = rowObject.begin(); iter != rowObject.end(); iter++, col++) + { + QString headerColKey = bpModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString(); + fields << rowObject.value(headerColKey).toString(); + } + bpModel->loadBreakpointFromFieldList(fields); + } +} + +void DebuggerSettingsManager::loadGameSettings(SavedAddressesModel* savedAddressesModel) +{ + const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame(); + if (path.empty()) + return; + + const QJsonValue savedAddressesValue = loadGameSettingsJSON().value("SavedAddresses"); + QString valueToLoad = savedAddressesValue.toString(); + if (savedAddressesValue.isUndefined() || !savedAddressesValue.isArray()) + { + Console.WriteLnFmt("Debugger Settings Manager: Failed to read Saved Addresses array from settings file: '{}'", path); + return; + } + + const QJsonArray breakpointsArray = savedAddressesValue.toArray(); + + for (u32 row = 0; row < breakpointsArray.size(); row++) + { + const QJsonValue rowValue = breakpointsArray.at(row); + if (rowValue.isUndefined() || !rowValue.isObject()) + { + Console.WriteLn("Debugger Settings Manager: Failed to load invalid Breakpoint object."); + continue; + } + const QJsonObject rowObject = rowValue.toObject(); + QStringList fields; + u32 col = 0; + for (auto iter = rowObject.begin(); iter != rowObject.end(); iter++, col++) + { + QString headerColKey = savedAddressesModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString(); + fields << rowObject.value(headerColKey).toString(); + } + savedAddressesModel->loadSavedAddressFromFieldList(fields); + } +} + +void DebuggerSettingsManager::saveGameSettings(BreakpointModel* bpModel) +{ + saveGameSettings(bpModel, "Breakpoints", BreakpointModel::ExportRole); +} + +void DebuggerSettingsManager::saveGameSettings(SavedAddressesModel* savedAddressesModel) +{ + saveGameSettings(savedAddressesModel, "SavedAddresses", Qt::DisplayRole); +} + +void DebuggerSettingsManager::saveGameSettings(QAbstractTableModel* abstractTableModel, QString settingsKey, u32 role) +{ + const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame(); + if (path.empty()) + return; + + const std::lock_guard lock(writeLock); + QJsonObject loadedSettings = loadGameSettingsJSON(); + QJsonArray rowsArray; + QStringList keys; + for (u32 col = 0; col < abstractTableModel->columnCount(); ++col) + { + keys << abstractTableModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString(); + } + + for (u32 row = 0; row < abstractTableModel->rowCount(); row++) + { + QJsonObject rowObject; + for (u32 col = 0; col < abstractTableModel->columnCount(); col++) + { + const QModelIndex index = abstractTableModel->index(row, col); + const QString data = abstractTableModel->data(index, role).toString(); + rowObject.insert(keys[col], QJsonValue::fromVariant(data)); + } + rowsArray.append(rowObject); + } + loadedSettings.insert(settingsKey, rowsArray); + loadedSettings.insert("Version", settingsFileVersion); + QJsonDocument doc(loadedSettings); + writeJSONToPath(path, doc); +} diff --git a/pcsx2-qt/Debugger/DebuggerSettingsManager.h b/pcsx2-qt/Debugger/DebuggerSettingsManager.h new file mode 100644 index 0000000000..9a77a7c4c0 --- /dev/null +++ b/pcsx2-qt/Debugger/DebuggerSettingsManager.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-License-Identifier: LGPL-3.0+ + +#pragma once +#include + +#include + +#include "Models/BreakpointModel.h" +#include "Models/SavedAddressesModel.h" + +class DebuggerSettingsManager final +{ +public: + DebuggerSettingsManager(QWidget* parent = nullptr); + ~DebuggerSettingsManager(); + + static void loadGameSettings(BreakpointModel* bpModel); + static void loadGameSettings(SavedAddressesModel* savedAddressesModel); + static void saveGameSettings(BreakpointModel* bpModel); + static void saveGameSettings(SavedAddressesModel* savedAddressesModel); + static void saveGameSettings(QAbstractTableModel* abstractTableModel, QString settingsKey, u32 role); + +private: + static std::mutex writeLock; + static void writeJSONToPath(std::string path, QJsonDocument jsonDocument); + static QJsonObject loadGameSettingsJSON(); + const static QString settingsFileVersion; +}; diff --git a/pcsx2-qt/Debugger/Models/BreakpointModel.cpp b/pcsx2-qt/Debugger/Models/BreakpointModel.cpp index 3e9fe8d797..eeca3d6af9 100644 --- a/pcsx2-qt/Debugger/Models/BreakpointModel.cpp +++ b/pcsx2-qt/Debugger/Models/BreakpointModel.cpp @@ -6,6 +6,7 @@ #include "DebugTools/DebugInterface.h" #include "DebugTools/Breakpoints.h" #include "DebugTools/DisassemblyManager.h" +#include "common/Console.h" #include "QtHost.h" #include "QtUtils.h" @@ -226,6 +227,28 @@ QVariant BreakpointModel::headerData(int section, Qt::Orientation orientation, i return QVariant(); } } + if (role == Qt::UserRole && orientation == Qt::Horizontal) + { + switch (section) + { + case BreakpointColumns::TYPE: + return "TYPE"; + case BreakpointColumns::OFFSET: + return "OFFSET"; + case BreakpointColumns::SIZE_LABEL: + return "SIZE / LABEL"; + case BreakpointColumns::OPCODE: + return "INSTRUCTION"; + case BreakpointColumns::CONDITION: + return "CONDITION"; + case BreakpointColumns::HITS: + return "HITS"; + case BreakpointColumns::ENABLED: + return "X"; + default: + return QVariant(); + } + } return QVariant(); } @@ -402,3 +425,106 @@ void BreakpointModel::refreshData() endResetModel(); } + +void BreakpointModel::loadBreakpointFromFieldList(QStringList fields) +{ + bool ok; + if (fields.size() != BreakpointModel::BreakpointColumns::COLUMN_COUNT) + { + Console.WriteLn("Debugger Breakpoint Model: Invalid number of columns, skipping"); + return; + } + + const int type = fields[BreakpointModel::BreakpointColumns::TYPE].toUInt(&ok); + if (!ok) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse type '%s', skipping", fields[BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData()); + return; + } + + // This is how we differentiate between breakpoints and memchecks + if (type == MEMCHECK_INVALID) + { + BreakPoint bp; + + // Address + bp.addr = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16); + if (!ok) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData()); + return; + } + + // Condition + if (!fields[BreakpointModel::BreakpointColumns::CONDITION].isEmpty()) + { + PostfixExpression expr; + bp.hasCond = true; + bp.cond.debug = &m_cpu; + + if (!m_cpu.initExpression(fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), expr)) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond '%s', skipping", fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData()); + return; + } + bp.cond.expression = expr; + strncpy(&bp.cond.expressionString[0], fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), sizeof(bp.cond.expressionString)); + } + + // Enabled + bp.enabled = fields[BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok); + if (!ok) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse enable flag '%s', skipping", fields[BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData()); + return; + } + + insertBreakpointRows(0, 1, {bp}); + } + else + { + MemCheck mc; + // Mode + if (type >= MEMCHECK_INVALID) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond type '%s', skipping", fields[BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData()); + return; + } + mc.cond = static_cast(type); + + // Address + QString test = fields[BreakpointModel::BreakpointColumns::OFFSET]; + mc.start = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16); + if (!ok) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData()); + return; + } + + // Size + mc.end = fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUInt(&ok) + mc.start; + if (!ok) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse length '%s', skipping", fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUtf8().constData()); + return; + } + + // Result + const int result = fields[BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok); + if (!ok) + { + Console.WriteLn("Debugger Breakpoint Model: Failed to parse result flag '%s', skipping", fields[BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData()); + return; + } + mc.result = static_cast(result); + + insertBreakpointRows(0, 1, {mc}); + } +} + +void BreakpointModel::clear() +{ + beginResetModel(); + m_breakpoints.clear(); + endResetModel(); +} diff --git a/pcsx2-qt/Debugger/Models/BreakpointModel.h b/pcsx2-qt/Debugger/Models/BreakpointModel.h index 1a90c47f7e..b478bdff51 100644 --- a/pcsx2-qt/Debugger/Models/BreakpointModel.h +++ b/pcsx2-qt/Debugger/Models/BreakpointModel.h @@ -55,10 +55,12 @@ public: bool setData(const QModelIndex& index, const QVariant& value, int role) override; bool removeRows(int row, int count, const QModelIndex& index = QModelIndex()) override; bool insertBreakpointRows(int row, int count, std::vector breakpoints, const QModelIndex& index = QModelIndex()); + void loadBreakpointFromFieldList(QStringList breakpointFields); BreakpointMemcheck at(int row) const { return m_breakpoints.at(row); }; void refreshData(); + void clear(); private: DebugInterface& m_cpu; diff --git a/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp b/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp index ea4ba96ac6..a1fae76899 100644 --- a/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp +++ b/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp @@ -4,6 +4,8 @@ #include "PrecompiledHeader.h" #include "SavedAddressesModel.h" +#include "common/Console.h" + SavedAddressesModel::SavedAddressesModel(DebugInterface& cpu, QObject* parent) : QAbstractTableModel(parent) , m_cpu(cpu) @@ -96,20 +98,38 @@ bool SavedAddressesModel::setData(const QModelIndex& index, const QVariant& valu QVariant SavedAddressesModel::headerData(int section, Qt::Orientation orientation, int role) const { - if ((role != Qt::DisplayRole && role != Qt::EditRole) || orientation != Qt::Horizontal) + if (orientation != Qt::Horizontal) return QVariant(); - switch (section) + if (role == Qt::DisplayRole) { - case SavedAddressesModel::ADDRESS: - return tr("MEMORY ADDRESS"); - case SavedAddressesModel::LABEL: - return tr("LABEL"); - case SavedAddressesModel::DESCRIPTION: - return tr("DESCRIPTION"); - default: - return QVariant(); + switch (section) + { + case SavedAddressesModel::ADDRESS: + return tr("MEMORY ADDRESS"); + case SavedAddressesModel::LABEL: + return tr("LABEL"); + case SavedAddressesModel::DESCRIPTION: + return tr("DESCRIPTION"); + default: + return QVariant(); + } } + if (role == Qt::UserRole) + { + switch (section) + { + case SavedAddressesModel::ADDRESS: + return "MEMORY ADDRESS"; + case SavedAddressesModel::LABEL: + return "LABEL"; + case SavedAddressesModel::DESCRIPTION: + return "DESCRIPTION"; + default: + return QVariant(); + } + } + return QVariant(); } Qt::ItemFlags SavedAddressesModel::flags(const QModelIndex& index) const @@ -150,3 +170,32 @@ int SavedAddressesModel::columnCount(const QModelIndex&) const { return HeaderColumns::COLUMN_COUNT; } + +void SavedAddressesModel::loadSavedAddressFromFieldList(QStringList fields) +{ + if (fields.size() != SavedAddressesModel::HeaderColumns::COLUMN_COUNT) + { + Console.WriteLn("Debugger Saved Addresses Model: Invalid number of columns, skipping"); + return; + } + + bool ok; + const u32 address = fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUInt(&ok, 16); + if (!ok) + { + Console.WriteLn("Debugger Saved Addresses Model: Failed to parse address '%s', skipping", fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUtf8().constData()); + return; + } + + const QString label = fields[SavedAddressesModel::HeaderColumns::LABEL]; + const QString description = fields[SavedAddressesModel::HeaderColumns::DESCRIPTION]; + const SavedAddressesModel::SavedAddress importedAddress = {address, label, description}; + addRow(importedAddress); +} + +void SavedAddressesModel::clear() +{ + beginResetModel(); + m_savedAddresses.clear(); + endResetModel(); +} diff --git a/pcsx2-qt/Debugger/Models/SavedAddressesModel.h b/pcsx2-qt/Debugger/Models/SavedAddressesModel.h index f3b293b588..cb6b7f1821 100644 --- a/pcsx2-qt/Debugger/Models/SavedAddressesModel.h +++ b/pcsx2-qt/Debugger/Models/SavedAddressesModel.h @@ -20,7 +20,7 @@ public: QString description; }; - enum HeaderColumns : int + enum HeaderColumns: int { ADDRESS = 0, LABEL, @@ -45,6 +45,8 @@ public: void addRow(SavedAddress addresstoSave); bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; + void loadSavedAddressFromFieldList(QStringList fields); + void clear(); private: DebugInterface& m_cpu; diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index b72e7522f9..6266df4ce6 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -106,6 +106,7 @@ + @@ -196,6 +197,7 @@ + diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index c5b868ba42..3d7d58b695 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj.filters +++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters @@ -346,6 +346,9 @@ moc + + Debugger + @@ -365,6 +368,9 @@ Settings + + Debugger + diff --git a/pcsx2/Config.h b/pcsx2/Config.h index f196dda053..743cbccdd8 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -1210,6 +1210,7 @@ namespace EmuFolders extern std::string AppRoot; extern std::string DataRoot; extern std::string Settings; + extern std::string DebuggerSettings; extern std::string Bios; extern std::string Snapshots; extern std::string Savestates; diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 13a2ab2ff0..7a28f0d3a5 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -150,6 +150,7 @@ namespace EmuFolders std::string AppRoot; std::string DataRoot; std::string Settings; + std::string DebuggerSettings; std::string Bios; std::string Snapshots; std::string Savestates; @@ -2004,6 +2005,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si) Textures = LoadPathFromSettings(si, DataRoot, "Textures", "textures"); InputProfiles = LoadPathFromSettings(si, DataRoot, "InputProfiles", "inputprofiles"); Videos = LoadPathFromSettings(si, DataRoot, "Videos", "videos"); + DebuggerSettings = LoadPathFromSettings(si, Settings, "DebuggerSettings", "debuggersettings"); Console.WriteLn("BIOS Directory: %s", Bios.c_str()); Console.WriteLn("Snapshots Directory: %s", Snapshots.c_str()); @@ -2020,6 +2022,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si) Console.WriteLn("Textures Directory: %s", Textures.c_str()); Console.WriteLn("Input Profile Directory: %s", InputProfiles.c_str()); Console.WriteLn("Video Dumping Directory: %s", Videos.c_str()); + Console.WriteLn("Debugger Settings Directory: %s", DebuggerSettings.c_str()); } bool EmuFolders::EnsureFoldersExist() @@ -2035,6 +2038,7 @@ bool EmuFolders::EnsureFoldersExist() result = FileSystem::CreateDirectoryPath(Covers.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(GameSettings.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(UserResources.c_str(), false) && result; + result = FileSystem::CreateDirectoryPath(DebuggerSettings.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(Cache.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(Textures.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(InputProfiles.c_str(), false) && result; diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 657b5ca383..c04dc86960 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -729,6 +729,22 @@ std::string VMManager::GetInputProfilePath(const std::string_view& name) return Path::Combine(EmuFolders::InputProfiles, fmt::format("{}.ini", name)); } +std::string VMManager::GetDebuggerSettingsFilePath(const std::string_view& game_serial, u32 game_crc) +{ + std::string path; + if (!game_serial.empty() && game_crc != 0) + { + auto lock = Host::GetSettingsLock(); + return Path::Combine(EmuFolders::DebuggerSettings, fmt::format("{}_{:08X}.json", game_serial, game_crc)); + } + return path; +} + +std::string VMManager::GetDebuggerSettingsFilePathForCurrentGame() +{ + return GetDebuggerSettingsFilePath(s_disc_serial, s_current_crc); +} + void VMManager::Internal::UpdateEmuFolders() { const std::string old_cheats_directory(EmuFolders::Cheats); diff --git a/pcsx2/VMManager.h b/pcsx2/VMManager.h index e10b5693e0..5090a99298 100644 --- a/pcsx2/VMManager.h +++ b/pcsx2/VMManager.h @@ -196,6 +196,12 @@ namespace VMManager /// Returns the path for the input profile ini file with the specified name (may not exist). std::string GetInputProfilePath(const std::string_view& name); + /// Returns the path for the debugger settings json file for the specified game serial and CRC. + std::string GetDebuggerSettingsFilePath(const std::string_view& game_serial, u32 game_crc); + + /// Returns the path for the debugger settings json file for the current game. + std::string GetDebuggerSettingsFilePathForCurrentGame(); + /// Resizes the render window to the display size, with an optional scale. /// If the scale is set to 0, the internal resolution will be used, otherwise it is treated as a multiplier to 1x. void RequestDisplaySize(float scale = 0.0f);