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