// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: GPL-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 SearchResult = MemorySearchWidget::SearchResult; 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) { 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, &MemorySearchWidget::onSearchTypeChanged); // 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); u32 address = rowToRemove->data(Qt::UserRole).toUInt(); if (m_searchResults.size() > static_cast(selectedResultIndex) && m_searchResults.at(selectedResultIndex).getAddress() == address) { 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 T readValueAtAddress(DebugInterface* cpu, u32 addr); template<> float readValueAtAddress(DebugInterface* cpu, u32 addr) { return std::bit_cast(cpu->read32(addr)); } template<> double readValueAtAddress(DebugInterface* cpu, u32 addr) { return std::bit_cast(cpu->read64(addr)); } template 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; areValuesEqual = (fBottom < readValue && readValue < fTop); } else if constexpr (std::is_same_v) { const double dTop = searchValue + 0.00001f; const double dBottom = searchValue - 0.00001f; areValuesEqual = (dBottom < readValue && readValue < 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 bool isGreater = readValue > fTop; const bool isLesser = readValue < fBottom; return isGreaterOperator ? isGreater : isLesser; } else if (std::is_same_v) { const double dTop = searchValue + 0.00001f; const double dBottom = searchValue - 0.00001f; const bool isGreater = readValue > dTop; const bool isLesser = readValue < dBottom; return isGreaterOperator ? isGreater : isLesser; } return isGreaterOperator ? (readValue > searchValue) : (readValue < searchValue); } default: Console.Error("Debugger: Unknown type when doing memory search!"); return false; } } // Handles the comparison of the read value against either the search value, or if existing searchResults are available, the value at the same address in the searchResultsMap template bool handleSearchComparison(SearchComparison searchComparison, u32 searchAddress, const SearchResult* priorResult, T searchValue, T readValue) { const bool isNotOperator = searchComparison == SearchComparison::NotEquals || searchComparison == SearchComparison::NotChanged; switch (searchComparison) { case SearchComparison::Equals: case SearchComparison::NotEquals: case SearchComparison::GreaterThan: case SearchComparison::GreaterThanOrEqual: case SearchComparison::LessThan: case SearchComparison::LessThanOrEqual: { return memoryValueComparator(searchComparison, searchValue, readValue); break; } case SearchComparison::Increased: { const T priorValue = priorResult->getValue(); return memoryValueComparator(SearchComparison::GreaterThan, priorValue, readValue); break; } case SearchComparison::IncreasedBy: { const T priorValue = priorResult->getValue(); const T expectedIncrease = searchValue + priorValue; return memoryValueComparator(SearchComparison::Equals, readValue, expectedIncrease); break; } case SearchComparison::Decreased: { const T priorValue = priorResult->getValue(); return memoryValueComparator(SearchComparison::LessThan, priorValue, readValue); break; } case SearchComparison::DecreasedBy: { const T priorValue = priorResult->getValue(); const T expectedDecrease = priorValue - searchValue; return memoryValueComparator(SearchComparison::Equals, readValue, expectedDecrease); break; } case SearchComparison::Changed: case SearchComparison::NotChanged: { const T priorValue = priorResult->getValue(); return memoryValueComparator(isNotOperator ? SearchComparison::Equals : SearchComparison::NotEquals, priorValue, readValue); break; } case SearchComparison::ChangedBy: { const T priorValue = priorResult->getValue(); const T expectedIncrease = searchValue + priorValue; const T expectedDecrease = priorValue - searchValue; return memoryValueComparator(SearchComparison::Equals, readValue, expectedIncrease) || memoryValueComparator(SearchComparison::Equals, readValue, expectedDecrease); } default: Console.Error("Debugger: Unknown type when doing memory search!"); return false; } } template void searchWorker(DebugInterface* cpu, std::vector& searchResults, SearchType searchType, SearchComparison searchComparison, u32 start, u32 end, T searchValue) { const bool isSearchingRange = searchResults.size() <= 0; if (isSearchingRange) { for (u32 addr = start; addr < end; addr += sizeof(T)) { if (!cpu->isValidAddress(addr)) continue; T readValue = readValueAtAddress(cpu, addr); if (handleSearchComparison(searchComparison, addr, nullptr, searchValue, readValue)) { searchResults.push_back(MemorySearchWidget::SearchResult(addr, QVariant::fromValue(readValue), searchType)); } } } else { auto removeIt = std::remove_if(searchResults.begin(), searchResults.end(), [cpu, searchType, searchComparison, searchValue](SearchResult& searchResult) -> bool { const u32 addr = searchResult.getAddress(); if (!cpu->isValidAddress(addr)) return true; const auto readValue = readValueAtAddress(cpu, addr); const bool doesMatch = handleSearchComparison(searchComparison, addr, &searchResult, searchValue, readValue); if (!doesMatch) searchResult = MemorySearchWidget::SearchResult(addr, QVariant::fromValue(readValue), searchType); return !doesMatch; }); searchResults.erase(removeIt, searchResults.end()); } } 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; } bool handleArraySearchComparison(DebugInterface* cpu, SearchComparison searchComparison, u32 searchAddress, SearchResult* priorResult, QByteArray searchValue) { const bool isNotOperator = searchComparison == SearchComparison::NotEquals || searchComparison == SearchComparison::NotChanged; switch (searchComparison) { case SearchComparison::Equals: case SearchComparison::NotEquals: { return compareByteArrayAtAddress(cpu, searchComparison, searchAddress, searchValue); break; } case SearchComparison::Changed: case SearchComparison::NotChanged: { QByteArray priorValue = priorResult->getArrayValue(); return compareByteArrayAtAddress(cpu, isNotOperator ? SearchComparison::Equals : SearchComparison::NotEquals, searchAddress, priorValue); break; } default: { Console.Error("Debugger: Unknown search comparison when doing memory search"); return false; } } // Default to no match found unless the comparison is a NotEquals return isNotOperator; } static QByteArray readArrayAtAddress(DebugInterface* cpu, u32 address, u32 length) { QByteArray readArray; for (u32 i = address; i < address + length; i++) { readArray.append(cpu->read8(i)); } return readArray; } static void searchWorkerByteArray(DebugInterface* cpu, SearchType searchType, SearchComparison searchComparison, std::vector& searchResults, u32 start, u32 end, QByteArray searchValue) { const bool isSearchingRange = searchResults.size() <= 0; if (isSearchingRange) { for (u32 addr = start; addr < end; addr += 1) { if (!cpu->isValidAddress(addr)) continue; if (handleArraySearchComparison(cpu, searchComparison, addr, nullptr, searchValue)) { searchResults.push_back(MemorySearchWidget::SearchResult(addr, searchValue, searchType)); addr += searchValue.length() - 1; } } } else { auto removeIt = std::remove_if(searchResults.begin(), searchResults.end(), [ searchComparison, searchType, searchValue, cpu ](SearchResult& searchResult) -> bool { const u32 addr = searchResult.getAddress(); if (!cpu->isValidAddress(addr)) return true; const bool doesMatch = handleArraySearchComparison(cpu, searchComparison, addr, &searchResult, searchValue); if (doesMatch) { QByteArray matchValue; if (searchComparison == SearchComparison::Equals) matchValue = searchValue; else if (searchComparison == SearchComparison::NotChanged) matchValue = searchResult.getArrayValue(); else matchValue = readArrayAtAddress(cpu, addr, searchValue.length() - 1); searchResult = MemorySearchWidget::SearchResult(addr, matchValue, searchType); } return !doesMatch; }); searchResults.erase(removeIt, searchResults.end()); } } std::vector startWorker(DebugInterface* cpu, const SearchType type, const SearchComparison comparison, std::vector searchResults, u32 start, u32 end, QString value, int base) { const bool isSigned = value.startsWith("-"); switch (type) { case SearchType::ByteType: isSigned ? searchWorker(cpu, searchResults, type, comparison, start, end, value.toShort(nullptr, base)) : searchWorker(cpu, searchResults, type, comparison, start, end, value.toUShort(nullptr, base)); break; case SearchType::Int16Type: isSigned ? searchWorker(cpu, searchResults, type, comparison, start, end, value.toShort(nullptr, base)) : searchWorker(cpu, searchResults, type, comparison, start, end, value.toUShort(nullptr, base)); break; case SearchType::Int32Type: isSigned ? searchWorker(cpu, searchResults, type, comparison, start, end, value.toInt(nullptr, base)) : searchWorker(cpu, searchResults, type, comparison, start, end, value.toUInt(nullptr, base)); break; case SearchType::Int64Type: isSigned ? searchWorker(cpu, searchResults, type, comparison, start, end, value.toLong(nullptr, base)) : searchWorker(cpu, searchResults, type, comparison, start, end, value.toULongLong(nullptr, base)); break; case SearchType::FloatType: searchWorker(cpu, searchResults, type, comparison, start, end, value.toFloat()); break; case SearchType::DoubleType: searchWorker(cpu, searchResults, type, comparison, start, end, value.toDouble()); break; case SearchType::StringType: searchWorkerByteArray(cpu, type, comparison, searchResults, start, end, value.toUtf8()); break; case SearchType::ArrayType: searchWorkerByteArray(cpu, type, comparison, searchResults, start, end, QByteArray::fromHex(value.toUtf8())); break; default: Console.Error("Debugger: Unknown type when doing memory search!"); return {}; }; return searchResults; } void MemorySearchWidget::onSearchButtonClicked() { if (!m_cpu->isAlive()) return; const SearchType searchType = getCurrentSearchType(); 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 = getCurrentSearchComparison(); const bool isFilterSearch = sender() == m_ui.btnFilterSearch; unsigned long long value; 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; } if (!isFilterSearch && (searchComparison == SearchComparison::Changed || searchComparison == SearchComparison::ChangedBy || searchComparison == SearchComparison::Decreased || searchComparison == SearchComparison::DecreasedBy || searchComparison == SearchComparison::Increased || searchComparison == SearchComparison::IncreasedBy || searchComparison == SearchComparison::NotChanged)) { QMessageBox::critical(this, tr("Debugger"), tr("This search comparison can only be used with filter searches.")); return; } QFutureWatcher>* workerWatcher = new QFutureWatcher>(); auto onSearchFinished = [this, workerWatcher] { m_ui.btnSearch->setDisabled(false); m_ui.listSearchResults->clear(); const auto& results = workerWatcher->future().result(); m_searchResults = std::move(results); loadSearchResults(); m_ui.resultsCountLabel->setText(QString(tr("%0 results found")).arg(m_searchResults.size())); m_ui.btnFilterSearch->setDisabled(m_ui.listSearchResults->count() == 0); updateSearchComparisonSelections(); delete workerWatcher; }; connect(workerWatcher, &QFutureWatcher>::finished, onSearchFinished); m_ui.btnSearch->setDisabled(true); if (!isFilterSearch) { m_searchResults.clear(); } QFuture> workerFuture = QtConcurrent::run(startWorker, m_cpu, searchType, searchComparison, std::move(m_searchResults), searchStart, searchEnd, searchValue, searchHex ? 16 : 10); workerWatcher->setFuture(workerFuture); connect(workerWatcher, &QFutureWatcher>::finished, onSearchFinished); m_searchResults.clear(); m_ui.resultsCountLabel->setText(tr("Searching...")); m_ui.resultsCountLabel->setVisible(true); } void MemorySearchWidget::onSearchResultsListScroll(u32 value) { const bool hasResultsToLoad = static_cast(m_ui.listSearchResults->count()) < m_searchResults.size(); const 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++) { const u32 address = m_searchResults.at(numLoaded + i).getAddress(); QListWidgetItem* item = new QListWidgetItem(QtUtils::FilledQStringFromValue(address, 16)); item->setData(Qt::UserRole, address); m_ui.listSearchResults->addItem(item); } } SearchType MemorySearchWidget::getCurrentSearchType() { return static_cast(m_ui.cmbSearchType->currentIndex()); } SearchComparison MemorySearchWidget::getCurrentSearchComparison() { // Note: The index can't be converted directly to the enum value since we change what comparisons are shown. return m_searchComparisonLabelMap.labelToEnum(m_ui.cmbSearchComparison->currentText()); } void MemorySearchWidget::onSearchTypeChanged(int newIndex) { if (newIndex < 4) m_ui.chkSearchHex->setEnabled(true); else m_ui.chkSearchHex->setEnabled(false); // Clear existing search results when the comparison type changes if (m_searchResults.size() > 0 && (int)(m_searchResults.front().getType()) != newIndex) { m_searchResults.clear(); m_ui.btnSearch->setDisabled(false); m_ui.btnFilterSearch->setDisabled(true); } updateSearchComparisonSelections(); } void MemorySearchWidget::updateSearchComparisonSelections() { const QString selectedComparisonLabel = m_ui.cmbSearchComparison->currentText(); const SearchComparison selectedComparison = m_searchComparisonLabelMap.labelToEnum(selectedComparisonLabel); const std::vector comparisons = getValidSearchComparisonsForState(getCurrentSearchType(), m_searchResults); m_ui.cmbSearchComparison->clear(); for (const SearchComparison comparison : comparisons) { m_ui.cmbSearchComparison->addItem(m_searchComparisonLabelMap.enumToLabel(comparison)); } // Preserve selection if applicable if (selectedComparison == SearchComparison::Invalid) return; if (std::find(comparisons.begin(), comparisons.end(), selectedComparison) != comparisons.end()) m_ui.cmbSearchComparison->setCurrentText(selectedComparisonLabel); } std::vector MemorySearchWidget::getValidSearchComparisonsForState(SearchType type, std::vector& existingResults) { const bool hasResults = existingResults.size() > 0; std::vector comparisons = { SearchComparison::Equals }; if (type == SearchType::ArrayType || type == SearchType::StringType) { if (hasResults && existingResults.front().isArrayValue()) { comparisons.push_back(SearchComparison::NotEquals); comparisons.push_back(SearchComparison::Changed); comparisons.push_back(SearchComparison::NotChanged); } return comparisons; } comparisons.push_back(SearchComparison::NotEquals); comparisons.push_back(SearchComparison::GreaterThan); comparisons.push_back(SearchComparison::GreaterThanOrEqual); comparisons.push_back(SearchComparison::LessThan); comparisons.push_back(SearchComparison::LessThanOrEqual); if (hasResults && existingResults.front().getType() == type) { comparisons.push_back(SearchComparison::Increased); comparisons.push_back(SearchComparison::IncreasedBy); comparisons.push_back(SearchComparison::Decreased); comparisons.push_back(SearchComparison::DecreasedBy); comparisons.push_back(SearchComparison::Changed); comparisons.push_back(SearchComparison::ChangedBy); comparisons.push_back(SearchComparison::NotChanged); } return comparisons; }