From ce5e66a2d5856fa8ad6b01c632d71a58eeafd84b Mon Sep 17 00:00:00 2001 From: Dan McCarthy Date: Mon, 12 Feb 2024 18:20:00 -0600 Subject: [PATCH] Debugger: Migrates Memory Search tab to it's own widget Moves the Memory Search fucntionality to it's own widget so as to not pollute the CpuWidget with search related functionality, especially as it is intended to grow in scope. CpuWidget is fairly general and as such everything tends to get tossed together which makes it harder to navigate/understand/maintain. --- pcsx2-qt/CMakeLists.txt | 3 + pcsx2-qt/Debugger/CpuWidget.cpp | 464 +-------------------- pcsx2-qt/Debugger/CpuWidget.h | 31 -- pcsx2-qt/Debugger/CpuWidget.ui | 220 +++------- pcsx2-qt/Debugger/MemorySearchWidget.cpp | 494 +++++++++++++++++++++++ pcsx2-qt/Debugger/MemorySearchWidget.h | 70 ++++ pcsx2-qt/Debugger/MemorySearchWidget.ui | 201 +++++++++ pcsx2-qt/pcsx2-qt.vcxproj | 6 + pcsx2-qt/pcsx2-qt.vcxproj.filters | 12 + 9 files changed, 838 insertions(+), 663 deletions(-) create mode 100644 pcsx2-qt/Debugger/MemorySearchWidget.cpp create mode 100644 pcsx2-qt/Debugger/MemorySearchWidget.h create mode 100644 pcsx2-qt/Debugger/MemorySearchWidget.ui diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 758d1024e3..6c1ccc2463 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -158,6 +158,9 @@ target_sources(pcsx2-qt PRIVATE Debugger/DisassemblyWidget.cpp Debugger/DisassemblyWidget.h Debugger/DisassemblyWidget.ui + Debugger/MemorySearchWidget.cpp + Debugger/MemorySearchWidget.h + Debugger/MemorySearchWidget.ui Debugger/MemoryViewWidget.cpp Debugger/MemoryViewWidget.h Debugger/MemoryViewWidget.ui diff --git a/pcsx2-qt/Debugger/CpuWidget.cpp b/pcsx2-qt/Debugger/CpuWidget.cpp index 65f44c746c..e76d853f54 100644 --- a/pcsx2-qt/Debugger/CpuWidget.cpp +++ b/pcsx2-qt/Debugger/CpuWidget.cpp @@ -33,8 +33,6 @@ using namespace QtUtils; using namespace MipsStackWalk; -using SearchComparison = CpuWidget::SearchComparison; -using SearchType = CpuWidget::SearchType; CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) : m_cpu(cpu) @@ -109,22 +107,6 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) connect(m_ui.btnRefreshFunctions, &QPushButton::clicked, [this] { updateFunctionList(); }); connect(m_ui.txtFuncSearch, &QLineEdit::textChanged, [this] { updateFunctionList(); }); - m_ui.listSearchResults->setContextMenuPolicy(Qt::CustomContextMenu); - connect(m_ui.btnSearch, &QPushButton::clicked, this, &CpuWidget::onSearchButtonClicked); - connect(m_ui.btnFilterSearch, &QPushButton::clicked, this, &CpuWidget::onSearchButtonClicked); - connect(m_ui.listSearchResults, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) - { - m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory); - m_ui.memoryviewWidget->gotoAddress(item->text().toUInt(nullptr, 16)); - }); - connect(m_ui.listSearchResults->verticalScrollBar(), &QScrollBar::valueChanged, this, &CpuWidget::onSearchResultsListScroll); - connect(m_ui.listSearchResults, &QListView::customContextMenuRequested, this, &CpuWidget::onListSearchResultsContextMenu); - connect(m_ui.cmbSearchType, &QComboBox::currentIndexChanged, [this](int i) { - if (i < 4) - m_ui.chkSearchHex->setEnabled(true); - else - m_ui.chkSearchHex->setEnabled(false); - }); m_ui.disassemblyWidget->SetCpu(&cpu); m_ui.registerWidget->SetCpu(&cpu); m_ui.memoryviewWidget->SetCpu(&cpu); @@ -140,11 +122,6 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) } this->repaint(); - // Ensures we don't retrigger the load results function unintentionally - m_resultsLoadTimer.setInterval(100); - m_resultsLoadTimer.setSingleShot(true); - connect(&m_resultsLoadTimer, &QTimer::timeout, this, &CpuWidget::loadSearchResults); - m_ui.savedAddressesList->setModel(&m_savedAddressesModel); m_ui.savedAddressesList->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_ui.savedAddressesList, &QTableView::customContextMenuRequested, this, &CpuWidget::onSavedAddressesListContextMenu); @@ -159,6 +136,12 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) DebuggerSettingsManager::loadGameSettings(&m_bpModel); DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel); + + connect(m_ui.memorySearchWidget, &MemorySearchWidget::addAddressToSavedAddressesList, this, &CpuWidget::addAddressToSavedAddressesList); + connect(m_ui.memorySearchWidget, &MemorySearchWidget::goToAddressInDisassemblyView, [this](u32 address) { m_ui.disassemblyWidget->gotoAddress(address); }); + connect(m_ui.memorySearchWidget, &MemorySearchWidget::goToAddressInMemoryView, m_ui.memoryviewWidget, &MemoryViewWidget::gotoAddress); + connect(m_ui.memorySearchWidget, &MemorySearchWidget::switchToMemoryViewTab, [this]() { m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory); }); + m_ui.memorySearchWidget->setCpu(&m_cpu); } CpuWidget::~CpuWidget() = default; @@ -168,6 +151,7 @@ void CpuWidget::paintEvent(QPaintEvent* event) m_ui.registerWidget->update(); m_ui.disassemblyWidget->update(); m_ui.memoryviewWidget->update(); + m_ui.memorySearchWidget->update(); } // The cpu shouldn't be alive when these are called @@ -568,41 +552,6 @@ void CpuWidget::addAddressToSavedAddressesList(u32 address) m_ui.savedAddressesList->edit(m_ui.savedAddressesList->model()->index(rowCount - 1, 1)); } -void CpuWidget::contextSearchResultGoToDisassembly() -{ - const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); - if (!selModel->hasSelection()) - return; - - m_ui.disassemblyWidget->gotoAddress(m_ui.listSearchResults->selectedItems().first()->data(Qt::UserRole).toUInt()); -} - -void CpuWidget::contextRemoveSearchResult() -{ - const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); - if (!selModel->hasSelection()) - return; - - const int selectedResultIndex = m_ui.listSearchResults->row(m_ui.listSearchResults->selectedItems().first()); - const auto* rowToRemove = m_ui.listSearchResults->takeItem(selectedResultIndex); - if (m_searchResults.size() > static_cast(selectedResultIndex) && m_searchResults.at(selectedResultIndex) == rowToRemove->data(Qt::UserRole).toUInt()) - { - m_searchResults.erase(m_searchResults.begin() + selectedResultIndex); - } - delete rowToRemove; -} - -void CpuWidget::contextCopySearchResultAddress() -{ - if (!m_ui.listSearchResults->selectionModel()->hasSelection()) - return; - - const u32 selectedResultIndex = m_ui.listSearchResults->row(m_ui.listSearchResults->selectedItems().first()); - const u32 rowAddress = m_ui.listSearchResults->item(selectedResultIndex)->data(Qt::UserRole).toUInt(); - const QString addressString = FilledQStringFromValue(rowAddress, 16); - QApplication::clipboard()->setText(addressString); -} - void CpuWidget::updateFunctionList(bool whenEmpty) { if (!m_cpu.isAlive()) @@ -897,36 +846,6 @@ void CpuWidget::updateStackFrames() m_stackModel.refreshData(); } -void CpuWidget::onListSearchResultsContextMenu(QPoint pos) -{ - QMenu* contextMenu = new QMenu(tr("Search Results List Context Menu"), m_ui.listSearchResults); - const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); - const auto listSearchResults = m_ui.listSearchResults; - - if (selModel->hasSelection()) - { - QAction* copyAddressAction = new QAction(tr("Copy Address"), m_ui.listSearchResults); - connect(copyAddressAction, &QAction::triggered, this, &CpuWidget::contextCopySearchResultAddress); - contextMenu->addAction(copyAddressAction); - - QAction* goToDisassemblyAction = new QAction(tr("Go to in Disassembly"), m_ui.listSearchResults); - connect(goToDisassemblyAction, &QAction::triggered, this, &CpuWidget::contextSearchResultGoToDisassembly); - contextMenu->addAction(goToDisassemblyAction); - - QAction* addToSavedAddressesAction = new QAction(tr("Add to Saved Memory Addresses"), m_ui.listSearchResults); - connect(addToSavedAddressesAction, &QAction::triggered, this, [this, listSearchResults]() { - addAddressToSavedAddressesList(listSearchResults->selectedItems().first()->data(Qt::UserRole).toUInt()); - }); - contextMenu->addAction(addToSavedAddressesAction); - - QAction* removeResultAction = new QAction(tr("Remove Result"), m_ui.listSearchResults); - connect(removeResultAction, &QAction::triggered, this, &CpuWidget::contextRemoveSearchResult); - contextMenu->addAction(removeResultAction); - } - - contextMenu->popup(m_ui.listSearchResults->viewport()->mapToGlobal(pos)); -} - void CpuWidget::onStackListContextMenu(QPoint pos) { if (!m_ui.stackList->selectionModel()->hasSelection()) @@ -974,375 +893,6 @@ void CpuWidget::onStackListDoubleClick(const QModelIndex& index) } } -template -static T readValueAtAddress(DebugInterface* cpu, u32 addr) -{ - T val = 0; - switch (sizeof(T)) - { - case sizeof(u8): - val = cpu->read8(addr); - break; - case sizeof(u16): - val = cpu->read16(addr); - break; - case sizeof(u32): - { - val = cpu->read32(addr); - break; - } - case sizeof(u64): - { - val = cpu->read64(addr); - break; - } - } - return val; -} - -template -static bool memoryValueComparator(SearchComparison searchComparison, T searchValue, T readValue) -{ - const bool isNotOperator = searchComparison == SearchComparison::NotEquals; - switch (searchComparison) - { - case SearchComparison::Equals: - case SearchComparison::NotEquals: - { - bool areValuesEqual = false; - if constexpr (std::is_same_v) - { - const T fTop = searchValue + 0.00001f; - const T fBottom = searchValue - 0.00001f; - const T memValue = std::bit_cast(readValue); - areValuesEqual = (fBottom < memValue && memValue < fTop); - } - else if constexpr (std::is_same_v) - { - const double dTop = searchValue + 0.00001f; - const double dBottom = searchValue - 0.00001f; - const double memValue = std::bit_cast(readValue); - areValuesEqual = (dBottom < memValue && memValue < dTop); - } - else - { - areValuesEqual = searchValue == readValue; - } - return isNotOperator ? !areValuesEqual : areValuesEqual; - break; - } - case SearchComparison::GreaterThan: - case SearchComparison::GreaterThanOrEqual: - case SearchComparison::LessThan: - case SearchComparison::LessThanOrEqual: - { - const bool hasEqualsCheck = searchComparison == SearchComparison::GreaterThanOrEqual || searchComparison == SearchComparison::LessThanOrEqual; - if (hasEqualsCheck && memoryValueComparator(SearchComparison::Equals, searchValue, readValue)) - return true; - - const bool isGreaterOperator = searchComparison == SearchComparison::GreaterThan || searchComparison == SearchComparison::GreaterThanOrEqual; - if (std::is_same_v) - { - const T fTop = searchValue + 0.00001f; - const T fBottom = searchValue - 0.00001f; - const T memValue = std::bit_cast(readValue); - const bool isGreater = memValue > fTop; - const bool isLesser = memValue < fBottom; - return isGreaterOperator ? isGreater : isLesser; - } - else if (std::is_same_v) - { - const double dTop = searchValue + 0.00001f; - const double dBottom = searchValue - 0.00001f; - const double memValue = std::bit_cast(readValue); - const bool isGreater = memValue > dTop; - const bool isLesser = memValue < dBottom; - return isGreaterOperator ? isGreater : isLesser; - } - - return isGreaterOperator ? (readValue > searchValue) : (readValue < searchValue); - } - default: - Console.Error("Debugger: Unknown type when doing memory search!"); - return false; - } -} - -template -std::vector searchWorker(DebugInterface* cpu, std::vector searchAddresses, SearchComparison searchComparison, u32 start, u32 end, T searchValue) -{ - std::vector hitAddresses; - const bool isSearchingRange = searchAddresses.size() <= 0; - if (isSearchingRange) - { - for (u32 addr = start; addr < end; addr += sizeof(T)) - { - if (!cpu->isValidAddress(addr)) - continue; - T readValue = readValueAtAddress(cpu, addr); - if (memoryValueComparator(searchComparison, searchValue, readValue)) - { - hitAddresses.push_back(addr); - } - } - } - else - { - for (const u32 addr : searchAddresses) - { - if (!cpu->isValidAddress(addr)) - continue; - T readValue = readValueAtAddress(cpu, addr); - if (memoryValueComparator(searchComparison, searchValue, readValue)) - { - hitAddresses.push_back(addr); - } - } - } - return hitAddresses; -} - -static bool compareByteArrayAtAddress(DebugInterface* cpu, SearchComparison searchComparison, u32 addr, QByteArray value) -{ - const bool isNotOperator = searchComparison == SearchComparison::NotEquals; - for (qsizetype i = 0; i < value.length(); i++) - { - const char nextByte = cpu->read8(addr + i); - switch (searchComparison) - { - case SearchComparison::Equals: - { - if (nextByte != value[i]) - return false; - break; - } - case SearchComparison::NotEquals: - { - if (nextByte != value[i]) - return true; - break; - } - default: - { - Console.Error("Debugger: Unknown search comparison when doing memory search"); - return false; - } - } - } - return !isNotOperator; -} - -static std::vector searchWorkerByteArray(DebugInterface* cpu, SearchComparison searchComparison, std::vector searchAddresses, u32 start, u32 end, QByteArray value) -{ - std::vector hitAddresses; - const bool isSearchingRange = searchAddresses.size() <= 0; - if (isSearchingRange) - { - for (u32 addr = start; addr < end; addr += 1) - { - if (compareByteArrayAtAddress(cpu, searchComparison, addr, value)) - { - hitAddresses.emplace_back(addr); - addr += value.length() - 1; - } - } - } - else - { - for (u32 addr : searchAddresses) - { - if (compareByteArrayAtAddress(cpu, searchComparison, addr, value)) - { - hitAddresses.emplace_back(addr); - } - } - } - return hitAddresses; -} - -std::vector startWorker(DebugInterface* cpu, const SearchType type, const SearchComparison searchComparison, std::vector searchAddresses, u32 start, u32 end, QString value, int base) -{ - const bool isSigned = value.startsWith("-"); - switch (type) - { - case SearchType::ByteType: - return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toShort(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toUShort(nullptr, base)); - case SearchType::Int16Type: - return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toShort(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toUShort(nullptr, base)); - case SearchType::Int32Type: - return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toInt(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toUInt(nullptr, base)); - case SearchType::Int64Type: - return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toLong(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toULongLong(nullptr, base)); - case SearchType::FloatType: - return searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toFloat()); - case SearchType::DoubleType: - return searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toDouble()); - case SearchType::StringType: - return searchWorkerByteArray(cpu, searchComparison, searchAddresses, start, end, value.toUtf8()); - case SearchType::ArrayType: - return searchWorkerByteArray(cpu, searchComparison, searchAddresses, start, end, QByteArray::fromHex(value.toUtf8())); - default: - Console.Error("Debugger: Unknown type when doing memory search!"); - break; - }; - return {}; -} - -void CpuWidget::onSearchButtonClicked() -{ - if (!m_cpu.isAlive()) - return; - - const SearchType searchType = static_cast(m_ui.cmbSearchType->currentIndex()); - const bool searchHex = m_ui.chkSearchHex->isChecked(); - - bool ok; - const u32 searchStart = m_ui.txtSearchStart->text().toUInt(&ok, 16); - - if (!ok) - { - QMessageBox::critical(this, tr("Debugger"), tr("Invalid start address")); - return; - } - - const u32 searchEnd = m_ui.txtSearchEnd->text().toUInt(&ok, 16); - - if (!ok) - { - QMessageBox::critical(this, tr("Debugger"), tr("Invalid end address")); - return; - } - - if (searchStart >= searchEnd) - { - QMessageBox::critical(this, tr("Debugger"), tr("Start address can't be equal to or greater than the end address")); - return; - } - - const QString searchValue = m_ui.txtSearchValue->text(); - const SearchComparison searchComparison = static_cast(m_ui.cmbSearchComparison->currentIndex()); - const bool isFilterSearch = sender() == m_ui.btnFilterSearch; - unsigned long long value; - - const bool isVariableSize = searchType == SearchType::ArrayType || searchType == SearchType::StringType; - if (isVariableSize && !isFilterSearch && searchComparison == SearchComparison::NotEquals) - { - QMessageBox::critical(this, tr("Debugger"), tr("Search types Array and String can use the Not Equals search comparison type with new searches.")); - return; - } - - if (isVariableSize && searchComparison != SearchComparison::Equals && searchComparison != SearchComparison::NotEquals) - { - QMessageBox::critical(this, tr("Debugger"), tr("Search types Array and String can only be used with Equals search comparisons.")); - return; - } - - switch (searchType) - { - case SearchType::ByteType: - case SearchType::Int16Type: - case SearchType::Int32Type: - case SearchType::Int64Type: - value = searchValue.toULongLong(&ok, searchHex ? 16 : 10); - break; - case SearchType::FloatType: - case SearchType::DoubleType: - searchValue.toDouble(&ok); - break; - case SearchType::StringType: - ok = !searchValue.isEmpty(); - break; - case SearchType::ArrayType: - ok = !searchValue.trimmed().isEmpty(); - break; - } - - if (!ok) - { - QMessageBox::critical(this, tr("Debugger"), tr("Invalid search value")); - return; - } - - switch (searchType) - { - case SearchType::ArrayType: - case SearchType::StringType: - case SearchType::DoubleType: - case SearchType::FloatType: - break; - case SearchType::Int64Type: - if (value <= std::numeric_limits::max()) - break; - case SearchType::Int32Type: - if (value <= std::numeric_limits::max()) - break; - case SearchType::Int16Type: - if (value <= std::numeric_limits::max()) - break; - case SearchType::ByteType: - if (value <= std::numeric_limits::max()) - break; - default: - QMessageBox::critical(this, tr("Debugger"), tr("Value is larger than type")); - return; - } - - QFutureWatcher>* workerWatcher = new QFutureWatcher>; - - connect(workerWatcher, &QFutureWatcher>::finished, [this, workerWatcher] { - m_ui.btnSearch->setDisabled(false); - - m_ui.listSearchResults->clear(); - const auto& results = workerWatcher->future().result(); - - m_searchResults = results; - loadSearchResults(); - m_ui.btnFilterSearch->setDisabled(m_ui.listSearchResults->count() == 0); - }); - - m_ui.btnSearch->setDisabled(true); - std::vector addresses; - if (isFilterSearch) - { - addresses = m_searchResults; - } - QFuture> workerFuture = - QtConcurrent::run(startWorker, &m_cpu, searchType, searchComparison, addresses, searchStart, searchEnd, searchValue, searchHex ? 16 : 10); - workerWatcher->setFuture(workerFuture); -} - -void CpuWidget::onSearchResultsListScroll(u32 value) -{ - bool hasResultsToLoad = static_cast(m_ui.listSearchResults->count()) < m_searchResults.size(); - bool scrolledSufficiently = value > (m_ui.listSearchResults->verticalScrollBar()->maximum() * 0.95); - - if (!m_resultsLoadTimer.isActive() && hasResultsToLoad && scrolledSufficiently) - { - // Load results once timer ends, allowing us to debounce repeated requests and only do one load. - m_resultsLoadTimer.start(); - } -} - -void CpuWidget::loadSearchResults() -{ - const u32 numLoaded = m_ui.listSearchResults->count(); - const u32 amountLeftToLoad = m_searchResults.size() - numLoaded; - if (amountLeftToLoad < 1) - return; - - const bool isFirstLoad = numLoaded == 0; - const u32 maxLoadAmount = isFirstLoad ? m_initialResultsLoadLimit : m_numResultsAddedPerLoad; - const u32 numToLoad = amountLeftToLoad > maxLoadAmount ? maxLoadAmount : amountLeftToLoad; - - for (u32 i = 0; i < numToLoad; i++) - { - u32 address = m_searchResults.at(numLoaded + i); - QListWidgetItem* item = new QListWidgetItem(QtUtils::FilledQStringFromValue(address, 16)); - item->setData(Qt::UserRole, address); - m_ui.listSearchResults->addItem(item); - } -} - void CpuWidget::saveBreakpointsToDebuggerSettings() { DebuggerSettingsManager::saveGameSettings(&m_bpModel); diff --git a/pcsx2-qt/Debugger/CpuWidget.h b/pcsx2-qt/Debugger/CpuWidget.h index be871df1be..3f3830d404 100644 --- a/pcsx2-qt/Debugger/CpuWidget.h +++ b/pcsx2-qt/Debugger/CpuWidget.h @@ -33,28 +33,8 @@ public: CpuWidget(QWidget* parent, DebugInterface& cpu); ~CpuWidget(); - enum class SearchType - { - ByteType, - Int16Type, - Int32Type, - Int64Type, - FloatType, - DoubleType, - StringType, - ArrayType - }; // Note: The order of these enum values must reflect the order in thee Search Comparison combobox. - enum class SearchComparison - { - Equals, - NotEquals, - GreaterThan, - GreaterThanOrEqual, - LessThan, - LessThanOrEqual - }; public slots: void paintEvent(QPaintEvent* event); @@ -111,20 +91,12 @@ public slots: m_ui.memoryviewWidget->update(); }; - void onSearchButtonClicked(); - void onSearchResultsListScroll(u32 value); - void loadSearchResults(); - void contextSearchResultGoToDisassembly(); - void contextRemoveSearchResult(); - void contextCopySearchResultAddress(); - void onListSearchResultsContextMenu(QPoint pos); void saveBreakpointsToDebuggerSettings(); void saveSavedAddressesToDebuggerSettings(); private: std::vector m_registerTableViews; - std::vector m_searchResults; QMenu* m_stacklistContextMenu = 0; QMenu* m_funclistContextMenu = 0; @@ -139,10 +111,7 @@ private: QSortFilterProxyModel m_threadProxyModel; StackModel m_stackModel; SavedAddressesModel m_savedAddressesModel; - QTimer m_resultsLoadTimer; bool m_demangleFunctions = true; bool m_moduleView = true; - u32 m_initialResultsLoadLimit = 20000; - u32 m_numResultsAddedPerLoad = 10000; }; diff --git a/pcsx2-qt/Debugger/CpuWidget.ui b/pcsx2-qt/Debugger/CpuWidget.ui index 4b3c849460..5389105ad1 100644 --- a/pcsx2-qt/Debugger/CpuWidget.ui +++ b/pcsx2-qt/Debugger/CpuWidget.ui @@ -176,185 +176,49 @@ - + Memory Search - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - - - - - Value - - - - - - - - - - Type - - - - - - - - 1 Byte (8 bits) - - - - - 2 Bytes (16 bits) - - - - - 4 Bytes (32 bits) - - - - - 8 Bytes (64 bits) - - - - - Float - - - - - Double - - - - - String - - - - - Array of byte - - - - - - - - Hex - - - - - - - - - - true - - - - - - - Search - - - - - - - false - - - Filter Search - - - - - - - - Equals - - - - - Not Equals - - - - - Greater Than - - - - - Greater Than Or Equal - - - - - Less Than - - - - - Less Than Or Equal - - - - - - - - Comparison - - - - - - - - - - - Start - - - - - - - 0x00 - - - - - - - End - - - - - - - 0x2000000 - - - - - - - + + + + 0 + 0 + + + + + Monospace + + + + Qt::StrongFocus + + + Qt::CustomContextMenu + + + true + + @@ -650,6 +514,12 @@
pcsx2-qt/Debugger/MemoryViewWidget.h
1 + + MemorySearchWidget + QWidget +
pcsx2-qt/Debugger/MemorySearchWidget.h
+ 1 +
diff --git a/pcsx2-qt/Debugger/MemorySearchWidget.cpp b/pcsx2-qt/Debugger/MemorySearchWidget.cpp new file mode 100644 index 0000000000..d3b0b71c4f --- /dev/null +++ b/pcsx2-qt/Debugger/MemorySearchWidget.cpp @@ -0,0 +1,494 @@ +// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-License-Identifier: LGPL-3.0+ + +#include "MemorySearchWidget.h" + +#include "DebugTools/DebugInterface.h" + +#include "QtUtils.h" + +#include "common/Console.h" + +#include +#include +#include +#include +#include +#include +#include + +using SearchComparison = MemorySearchWidget::SearchComparison; +using SearchType = MemorySearchWidget::SearchType; + +using namespace QtUtils; + +MemorySearchWidget::MemorySearchWidget(QWidget* parent) + : QWidget(parent) +{ + m_ui.setupUi(this); + this->repaint(); + + m_ui.listSearchResults->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_ui.btnSearch, &QPushButton::clicked, this, &MemorySearchWidget::onSearchButtonClicked); + connect(m_ui.btnFilterSearch, &QPushButton::clicked, this, &MemorySearchWidget::onSearchButtonClicked); + connect(m_ui.listSearchResults, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) // move back to cpu widget + { + emit switchToMemoryViewTab(); + emit goToAddressInMemoryView(item->text().toUInt(nullptr, 16)); + }); + connect(m_ui.listSearchResults->verticalScrollBar(), &QScrollBar::valueChanged, this, &MemorySearchWidget::onSearchResultsListScroll); + connect(m_ui.listSearchResults, &QListView::customContextMenuRequested, this, &MemorySearchWidget::onListSearchResultsContextMenu); + connect(m_ui.cmbSearchType, &QComboBox::currentIndexChanged, [this](int i) { + if (i < 4) + m_ui.chkSearchHex->setEnabled(true); + else + m_ui.chkSearchHex->setEnabled(false); + }); + + // Ensures we don't retrigger the load results function unintentionally + m_resultsLoadTimer.setInterval(100); + m_resultsLoadTimer.setSingleShot(true); + connect(&m_resultsLoadTimer, &QTimer::timeout, this, &MemorySearchWidget::loadSearchResults); +} + +void MemorySearchWidget::setCpu(DebugInterface* cpu) +{ + m_cpu = cpu; +} + +void MemorySearchWidget::contextSearchResultGoToDisassembly() +{ + const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); + if (!selModel->hasSelection()) + return; + + u32 selectedAddress = m_ui.listSearchResults->selectedItems().first()->data(Qt::UserRole).toUInt(); + emit goToAddressInDisassemblyView(selectedAddress); +} + +void MemorySearchWidget::contextRemoveSearchResult() +{ + const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); + if (!selModel->hasSelection()) + return; + + const int selectedResultIndex = m_ui.listSearchResults->row(m_ui.listSearchResults->selectedItems().first()); + const auto* rowToRemove = m_ui.listSearchResults->takeItem(selectedResultIndex); + if (m_searchResults.size() > static_cast(selectedResultIndex) && m_searchResults.at(selectedResultIndex) == rowToRemove->data(Qt::UserRole).toUInt()) + { + m_searchResults.erase(m_searchResults.begin() + selectedResultIndex); + } + delete rowToRemove; +} + +void MemorySearchWidget::contextCopySearchResultAddress() +{ + if (!m_ui.listSearchResults->selectionModel()->hasSelection()) + return; + + const u32 selectedResultIndex = m_ui.listSearchResults->row(m_ui.listSearchResults->selectedItems().first()); + const u32 rowAddress = m_ui.listSearchResults->item(selectedResultIndex)->data(Qt::UserRole).toUInt(); + const QString addressString = FilledQStringFromValue(rowAddress, 16); + QApplication::clipboard()->setText(addressString); +} + +void MemorySearchWidget::onListSearchResultsContextMenu(QPoint pos) +{ + QMenu* contextMenu = new QMenu(tr("Search Results List Context Menu"), m_ui.listSearchResults); + const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); + const auto listSearchResults = m_ui.listSearchResults; + + if (selModel->hasSelection()) + { + QAction* copyAddressAction = new QAction(tr("Copy Address"), m_ui.listSearchResults); + connect(copyAddressAction, &QAction::triggered, this, &MemorySearchWidget::contextCopySearchResultAddress); + contextMenu->addAction(copyAddressAction); + + QAction* goToDisassemblyAction = new QAction(tr("Go to in Disassembly"), m_ui.listSearchResults); + connect(goToDisassemblyAction, &QAction::triggered, this, &MemorySearchWidget::contextSearchResultGoToDisassembly); + contextMenu->addAction(goToDisassemblyAction); + + QAction* addToSavedAddressesAction = new QAction(tr("Add to Saved Memory Addresses"), m_ui.listSearchResults); + connect(addToSavedAddressesAction, &QAction::triggered, this, [this, listSearchResults]() { + u32 selectedAddress = listSearchResults->selectedItems().first()->data(Qt::UserRole).toUInt(); + emit addAddressToSavedAddressesList(selectedAddress); + }); + contextMenu->addAction(addToSavedAddressesAction); + + QAction* removeResultAction = new QAction(tr("Remove Result"), m_ui.listSearchResults); + connect(removeResultAction, &QAction::triggered, this, &MemorySearchWidget::contextRemoveSearchResult); + contextMenu->addAction(removeResultAction); + } + + contextMenu->popup(m_ui.listSearchResults->viewport()->mapToGlobal(pos)); +} + + +template +static T readValueAtAddress(DebugInterface* cpu, u32 addr) +{ + T val = 0; + switch (sizeof(T)) + { + case sizeof(u8): + val = cpu->read8(addr); + break; + case sizeof(u16): + val = cpu->read16(addr); + break; + case sizeof(u32): + { + val = cpu->read32(addr); + break; + } + case sizeof(u64): + { + val = cpu->read64(addr); + break; + } + } + return val; +} + +template +static bool memoryValueComparator(SearchComparison searchComparison, T searchValue, T readValue) +{ + const bool isNotOperator = searchComparison == SearchComparison::NotEquals; + switch (searchComparison) + { + case SearchComparison::Equals: + case SearchComparison::NotEquals: + { + bool areValuesEqual = false; + if constexpr (std::is_same_v) + { + const T fTop = searchValue + 0.00001f; + const T fBottom = searchValue - 0.00001f; + const T memValue = std::bit_cast(readValue); + areValuesEqual = (fBottom < memValue && memValue < fTop); + } + else if constexpr (std::is_same_v) + { + const double dTop = searchValue + 0.00001f; + const double dBottom = searchValue - 0.00001f; + const double memValue = std::bit_cast(readValue); + areValuesEqual = (dBottom < memValue && memValue < dTop); + } + else + { + areValuesEqual = searchValue == readValue; + } + return isNotOperator ? !areValuesEqual : areValuesEqual; + break; + } + case SearchComparison::GreaterThan: + case SearchComparison::GreaterThanOrEqual: + case SearchComparison::LessThan: + case SearchComparison::LessThanOrEqual: + { + const bool hasEqualsCheck = searchComparison == SearchComparison::GreaterThanOrEqual || searchComparison == SearchComparison::LessThanOrEqual; + if (hasEqualsCheck && memoryValueComparator(SearchComparison::Equals, searchValue, readValue)) + return true; + + const bool isGreaterOperator = searchComparison == SearchComparison::GreaterThan || searchComparison == SearchComparison::GreaterThanOrEqual; + if (std::is_same_v) + { + const T fTop = searchValue + 0.00001f; + const T fBottom = searchValue - 0.00001f; + const T memValue = std::bit_cast(readValue); + const bool isGreater = memValue > fTop; + const bool isLesser = memValue < fBottom; + return isGreaterOperator ? isGreater : isLesser; + } + else if (std::is_same_v) + { + const double dTop = searchValue + 0.00001f; + const double dBottom = searchValue - 0.00001f; + const double memValue = std::bit_cast(readValue); + const bool isGreater = memValue > dTop; + const bool isLesser = memValue < dBottom; + return isGreaterOperator ? isGreater : isLesser; + } + + return isGreaterOperator ? (readValue > searchValue) : (readValue < searchValue); + } + default: + Console.Error("Debugger: Unknown type when doing memory search!"); + return false; + } +} + +template +std::vector searchWorker(DebugInterface* cpu, std::vector searchAddresses, SearchComparison searchComparison, u32 start, u32 end, T searchValue) +{ + std::vector hitAddresses; + const bool isSearchingRange = searchAddresses.size() <= 0; + if (isSearchingRange) + { + for (u32 addr = start; addr < end; addr += sizeof(T)) + { + if (!cpu->isValidAddress(addr)) + continue; + T readValue = readValueAtAddress(cpu, addr); + if (memoryValueComparator(searchComparison, searchValue, readValue)) + { + hitAddresses.push_back(addr); + } + } + } + else + { + for (const u32 addr : searchAddresses) + { + if (!cpu->isValidAddress(addr)) + continue; + T readValue = readValueAtAddress(cpu, addr); + if (memoryValueComparator(searchComparison, searchValue, readValue)) + { + hitAddresses.push_back(addr); + } + } + } + return hitAddresses; +} + +static bool compareByteArrayAtAddress(DebugInterface* cpu, SearchComparison searchComparison, u32 addr, QByteArray value) +{ + const bool isNotOperator = searchComparison == SearchComparison::NotEquals; + for (qsizetype i = 0; i < value.length(); i++) + { + const char nextByte = cpu->read8(addr + i); + switch (searchComparison) + { + case SearchComparison::Equals: + { + if (nextByte != value[i]) + return false; + break; + } + case SearchComparison::NotEquals: + { + if (nextByte != value[i]) + return true; + break; + } + default: + { + Console.Error("Debugger: Unknown search comparison when doing memory search"); + return false; + } + } + } + return !isNotOperator; +} + +static std::vector searchWorkerByteArray(DebugInterface* cpu, SearchComparison searchComparison, std::vector searchAddresses, u32 start, u32 end, QByteArray value) +{ + std::vector hitAddresses; + const bool isSearchingRange = searchAddresses.size() <= 0; + if (isSearchingRange) + { + for (u32 addr = start; addr < end; addr += 1) + { + if (compareByteArrayAtAddress(cpu, searchComparison, addr, value)) + { + hitAddresses.emplace_back(addr); + addr += value.length() - 1; + } + } + } + else + { + for (u32 addr : searchAddresses) + { + if (compareByteArrayAtAddress(cpu, searchComparison, addr, value)) + { + hitAddresses.emplace_back(addr); + } + } + } + return hitAddresses; +} + +std::vector startWorker(DebugInterface* cpu, const SearchType type, const SearchComparison searchComparison, std::vector searchAddresses, u32 start, u32 end, QString value, int base) +{ + const bool isSigned = value.startsWith("-"); + switch (type) + { + case SearchType::ByteType: + return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toShort(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toUShort(nullptr, base)); + case SearchType::Int16Type: + return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toShort(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toUShort(nullptr, base)); + case SearchType::Int32Type: + return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toInt(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toUInt(nullptr, base)); + case SearchType::Int64Type: + return isSigned ? searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toLong(nullptr, base)) : searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toULongLong(nullptr, base)); + case SearchType::FloatType: + return searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toFloat()); + case SearchType::DoubleType: + return searchWorker(cpu, searchAddresses, searchComparison, start, end, value.toDouble()); + case SearchType::StringType: + return searchWorkerByteArray(cpu, searchComparison, searchAddresses, start, end, value.toUtf8()); + case SearchType::ArrayType: + return searchWorkerByteArray(cpu, searchComparison, searchAddresses, start, end, QByteArray::fromHex(value.toUtf8())); + default: + Console.Error("Debugger: Unknown type when doing memory search!"); + break; + }; + return {}; +} + +void MemorySearchWidget::onSearchButtonClicked() +{ + if (!m_cpu->isAlive()) + return; + + const SearchType searchType = static_cast(m_ui.cmbSearchType->currentIndex()); + const bool searchHex = m_ui.chkSearchHex->isChecked(); + + bool ok; + const u32 searchStart = m_ui.txtSearchStart->text().toUInt(&ok, 16); + + if (!ok) + { + QMessageBox::critical(this, tr("Debugger"), tr("Invalid start address")); + return; + } + + const u32 searchEnd = m_ui.txtSearchEnd->text().toUInt(&ok, 16); + + if (!ok) + { + QMessageBox::critical(this, tr("Debugger"), tr("Invalid end address")); + return; + } + + if (searchStart >= searchEnd) + { + QMessageBox::critical(this, tr("Debugger"), tr("Start address can't be equal to or greater than the end address")); + return; + } + + const QString searchValue = m_ui.txtSearchValue->text(); + const SearchComparison searchComparison = static_cast(m_ui.cmbSearchComparison->currentIndex()); + const bool isFilterSearch = sender() == m_ui.btnFilterSearch; + unsigned long long value; + + const bool isVariableSize = searchType == SearchType::ArrayType || searchType == SearchType::StringType; + if (isVariableSize && !isFilterSearch && searchComparison == SearchComparison::NotEquals) + { + QMessageBox::critical(this, tr("Debugger"), tr("Search types Array and String can use the Not Equals search comparison type with new searches.")); + return; + } + + if (isVariableSize && searchComparison != SearchComparison::Equals && searchComparison != SearchComparison::NotEquals) + { + QMessageBox::critical(this, tr("Debugger"), tr("Search types Array and String can only be used with Equals search comparisons.")); + return; + } + + switch (searchType) + { + case SearchType::ByteType: + case SearchType::Int16Type: + case SearchType::Int32Type: + case SearchType::Int64Type: + value = searchValue.toULongLong(&ok, searchHex ? 16 : 10); + break; + case SearchType::FloatType: + case SearchType::DoubleType: + searchValue.toDouble(&ok); + break; + case SearchType::StringType: + ok = !searchValue.isEmpty(); + break; + case SearchType::ArrayType: + ok = !searchValue.trimmed().isEmpty(); + break; + } + + if (!ok) + { + QMessageBox::critical(this, tr("Debugger"), tr("Invalid search value")); + return; + } + + switch (searchType) + { + case SearchType::ArrayType: + case SearchType::StringType: + case SearchType::DoubleType: + case SearchType::FloatType: + break; + case SearchType::Int64Type: + if (value <= std::numeric_limits::max()) + break; + case SearchType::Int32Type: + if (value <= std::numeric_limits::max()) + break; + case SearchType::Int16Type: + if (value <= std::numeric_limits::max()) + break; + case SearchType::ByteType: + if (value <= std::numeric_limits::max()) + break; + default: + QMessageBox::critical(this, tr("Debugger"), tr("Value is larger than type")); + return; + } + + QFutureWatcher>* workerWatcher = new QFutureWatcher>; + + connect(workerWatcher, &QFutureWatcher>::finished, [this, workerWatcher] { + m_ui.btnSearch->setDisabled(false); + + m_ui.listSearchResults->clear(); + const auto& results = workerWatcher->future().result(); + + m_searchResults = results; + loadSearchResults(); + m_ui.btnFilterSearch->setDisabled(m_ui.listSearchResults->count() == 0); + }); + + m_ui.btnSearch->setDisabled(true); + std::vector addresses; + if (isFilterSearch) + { + addresses = m_searchResults; + } + QFuture> workerFuture = + QtConcurrent::run(startWorker, m_cpu, searchType, searchComparison, addresses, searchStart, searchEnd, searchValue, searchHex ? 16 : 10); + workerWatcher->setFuture(workerFuture); +} + +void MemorySearchWidget::onSearchResultsListScroll(u32 value) +{ + bool hasResultsToLoad = static_cast(m_ui.listSearchResults->count()) < m_searchResults.size(); + bool scrolledSufficiently = value > (m_ui.listSearchResults->verticalScrollBar()->maximum() * 0.95); + + if (!m_resultsLoadTimer.isActive() && hasResultsToLoad && scrolledSufficiently) + { + // Load results once timer ends, allowing us to debounce repeated requests and only do one load. + m_resultsLoadTimer.start(); + } +} + +void MemorySearchWidget::loadSearchResults() +{ + const u32 numLoaded = m_ui.listSearchResults->count(); + const u32 amountLeftToLoad = m_searchResults.size() - numLoaded; + if (amountLeftToLoad < 1) + return; + + const bool isFirstLoad = numLoaded == 0; + const u32 maxLoadAmount = isFirstLoad ? m_initialResultsLoadLimit : m_numResultsAddedPerLoad; + const u32 numToLoad = amountLeftToLoad > maxLoadAmount ? maxLoadAmount : amountLeftToLoad; + + for (u32 i = 0; i < numToLoad; i++) + { + u32 address = m_searchResults.at(numLoaded + i); + QListWidgetItem* item = new QListWidgetItem(QtUtils::FilledQStringFromValue(address, 16)); + item->setData(Qt::UserRole, address); + m_ui.listSearchResults->addItem(item); + } +} diff --git a/pcsx2-qt/Debugger/MemorySearchWidget.h b/pcsx2-qt/Debugger/MemorySearchWidget.h new file mode 100644 index 0000000000..5cd3833dea --- /dev/null +++ b/pcsx2-qt/Debugger/MemorySearchWidget.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-License-Identifier: LGPL-3.0+ + +#pragma once + +#include "ui_MemorySearchWidget.h" + +#include "DebugTools/DebugInterface.h" + +#include +#include + +class MemorySearchWidget final : public QWidget +{ + Q_OBJECT + +public: + MemorySearchWidget(QWidget* parent); + ~MemorySearchWidget() = default; + void setCpu(DebugInterface* cpu); + + enum class SearchType + { + ByteType, + Int16Type, + Int32Type, + Int64Type, + FloatType, + DoubleType, + StringType, + ArrayType + }; + + // Note: The order of these enum values must reflect the order in thee Search Comparison combobox. + enum class SearchComparison + { + Equals, + NotEquals, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual + }; + +public slots: + void onSearchButtonClicked(); + void onSearchResultsListScroll(u32 value); + void loadSearchResults(); + void contextSearchResultGoToDisassembly(); + void contextRemoveSearchResult(); + void contextCopySearchResultAddress(); + void onListSearchResultsContextMenu(QPoint pos); + +signals: + void addAddressToSavedAddressesList(u32 address); + void goToAddressInDisassemblyView(u32 address); + void goToAddressInMemoryView(u32 address); + void switchToMemoryViewTab(); + +private: + std::vector m_searchResults; + + Ui::MemorySearchWidget m_ui; + + DebugInterface* m_cpu; + QTimer m_resultsLoadTimer; + + u32 m_initialResultsLoadLimit = 20000; + u32 m_numResultsAddedPerLoad = 10000; +}; diff --git a/pcsx2-qt/Debugger/MemorySearchWidget.ui b/pcsx2-qt/Debugger/MemorySearchWidget.ui new file mode 100644 index 0000000000..789724a035 --- /dev/null +++ b/pcsx2-qt/Debugger/MemorySearchWidget.ui @@ -0,0 +1,201 @@ + + + MemorySearchWidget + + + + 0 + 0 + 400 + 300 + + + + + Monospace + + + + + + + + + + + + Value + + + + + + + + + + Type + + + + + + + + 1 Byte (8 bits) + + + + + 2 Bytes (16 bits) + + + + + 4 Bytes (32 bits) + + + + + 8 Bytes (64 bits) + + + + + Float + + + + + Double + + + + + String + + + + + Array of byte + + + + + + + + Hex + + + + + + + + + + true + + + + + + + Search + + + + + + + false + + + Filter Search + + + + + + + + Equals + + + + + Not Equals + + + + + Greater Than + + + + + Greater Than Or Equal + + + + + Less Than + + + + + Less Than Or Equal + + + + + + + + Comparison + + + + + + + + + + + Start + + + + + + + 0x00 + + + + + + + End + + + + + + + 0x2000000 + + + + + + + + + + + + + diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 4c3983e8fb..d7f17e8172 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -103,6 +103,7 @@ + @@ -194,6 +195,7 @@ + @@ -252,6 +254,7 @@ + @@ -380,6 +383,9 @@ Document + + Document + Document diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index 793128e558..2836d7e82d 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj.filters +++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters @@ -272,6 +272,9 @@ Debugger + + Debugger + Debugger @@ -302,6 +305,9 @@ moc + + moc + moc @@ -483,6 +489,9 @@ Debugger + + Debugger + Debugger @@ -625,6 +634,9 @@ Debugger + + Debugger + Debugger