From 02a1ccbcdc72a04cfb624dc06d45b5c0dd3d202e Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 30 Jul 2025 22:21:33 +1000 Subject: [PATCH] Qt: Persist memory scanner watch list across instances i.e. save it to a file. --- src/core/memory_scanner.cpp | 104 ++++++++++++++++++++- src/core/memory_scanner.h | 11 ++- src/duckstation-qt/memoryscannerwindow.cpp | 86 ++++++++++++++++- src/duckstation-qt/memoryscannerwindow.h | 6 ++ 4 files changed, 204 insertions(+), 3 deletions(-) diff --git a/src/core/memory_scanner.cpp b/src/core/memory_scanner.cpp index f141a8d00..c2dd509c5 100644 --- a/src/core/memory_scanner.cpp +++ b/src/core/memory_scanner.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "memory_scanner.h" @@ -6,7 +6,11 @@ #include "cpu_core.h" #include "cpu_core_private.h" +#include "common/error.h" +#include "common/file_system.h" #include "common/log.h" +#include "common/path.h" +#include "common/ryml_helpers.h" #include "fmt/format.h" @@ -340,6 +344,7 @@ bool MemoryWatchList::AddEntry(std::string description, u32 address, MemoryAcces entry.freeze = freeze; m_entries.push_back(std::move(entry)); + m_entries_changed = true; return true; } @@ -357,6 +362,7 @@ void MemoryWatchList::RemoveEntry(u32 index) return; m_entries.erase(m_entries.begin() + index); + m_entries_changed = true; } bool MemoryWatchList::RemoveEntryByAddress(u32 address) @@ -366,6 +372,7 @@ bool MemoryWatchList::RemoveEntryByAddress(u32 address) if (it->address == address) { m_entries.erase(it); + m_entries_changed = true; return true; } } @@ -389,6 +396,7 @@ void MemoryWatchList::SetEntryFreeze(u32 index, bool freeze) Entry& entry = m_entries[index]; entry.freeze = freeze; + m_entries_changed = true; } void MemoryWatchList::SetEntryValue(u32 index, u32 value) @@ -427,6 +435,12 @@ void MemoryWatchList::UpdateValues() UpdateEntryValue(&entry); } +void MemoryWatchList::ClearEntries() +{ + m_entries.clear(); + m_entries_changed = false; +} + void MemoryWatchList::SetEntryValue(Entry* entry, u32 value) { switch (entry->size) @@ -482,3 +496,91 @@ void MemoryWatchList::UpdateEntryValue(Entry* entry) if (entry->freeze && entry->changed) SetEntryValue(entry, old_value); } + +bool MemoryWatchList::LoadFromFile(const char* path, Error* error) +{ + std::optional yaml_data = FileSystem::ReadFileToString(path, error); + if (!yaml_data.has_value()) + { + Error::AddPrefixFmt(error, "Failed to read {}: ", Path::GetFileName(path)); + return false; + } + + m_entries.clear(); + m_entries_changed = false; + + const ryml::Tree yaml = + ryml::parse_in_place(to_csubstr(path), c4::substr(reinterpret_cast(yaml_data->data()), yaml_data->size())); + const ryml::ConstNodeRef root = yaml.rootref(); + + m_entries.reserve(root.num_children()); + for (const ryml::ConstNodeRef& child : root.cchildren()) + { + Entry entry; + std::string_view address; + std::string_view size; + std::optional parsed_address; + if (!GetStringFromObject(child, "description", &entry.description) || + !GetStringFromObject(child, "address", &address) || !GetStringFromObject(child, "size", &size) || + !GetUIntFromObject(child, "isSigned", &entry.is_signed) || !GetUIntFromObject(child, "freeze", &entry.freeze) || + !(parsed_address = StringUtil::FromCharsWithOptionalBase(address)).has_value() || + (size != "byte" && size != "halfword" && size != "word")) + { + Error::SetStringView(error, "One or more required fields are missing in the memory watch entry."); + m_entries.clear(); + return false; + } + + entry.address = parsed_address.value(); + if (size == "byte") + entry.size = MemoryAccessSize::Byte; + else if (size == "halfword") + entry.size = MemoryAccessSize::HalfWord; + else // if (size == "word") + entry.size = MemoryAccessSize::Word; + + entry.changed = false; + UpdateEntryValue(&entry); + + m_entries.push_back(std::move(entry)); + } + + DEV_LOG("Loaded {} entries from {}", m_entries.size(), Path::GetFileName(path)); + return true; +} + +bool MemoryWatchList::SaveToFile(const char* path, Error* error) +{ + std::string buf; + auto appender = std::back_inserter(buf); + + for (const Entry& entry : m_entries) + { + fmt::format_to(appender, "- description: {}\n", entry.description); + fmt::format_to(appender, " address: 0x{:08x}\n", entry.address); + fmt::format_to(appender, " size: {}\n", + (entry.size == MemoryAccessSize::Byte) ? + "byte" : + ((entry.size == MemoryAccessSize::HalfWord) ? "halfword" : "word")); + fmt::format_to(appender, " isSigned: {}\n", entry.is_signed); + fmt::format_to(appender, " freeze: {}\n", entry.freeze); + } + + // avoid rewriting if unchanged + std::optional current_file = FileSystem::ReadFileToString(path); + if (current_file.has_value() && current_file.value() == buf) + { + DEV_LOG("Memory watch list unchanged, not saving to {}", Path::GetFileName(path)); + m_entries_changed = false; + return true; + } + + if (!FileSystem::WriteStringToFile(path, buf, error)) + { + Error::AddPrefixFmt(error, "Failed to write {}: ", Path::GetFileName(path)); + return false; + } + + m_entries_changed = false; + return true; +} diff --git a/src/core/memory_scanner.h b/src/core/memory_scanner.h index 0adbb5cac..13c593269 100644 --- a/src/core/memory_scanner.h +++ b/src/core/memory_scanner.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -10,6 +10,8 @@ #include #include +class Error; + class MemoryScan { public: @@ -111,6 +113,7 @@ public: const EntryVector& GetEntries() const { return m_entries; } const Entry& GetEntry(u32 index) const { return m_entries[index]; } u32 GetEntryCount() const { return static_cast(m_entries.size()); } + bool HasEntriesChanged() const { return m_entries_changed; } bool AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze); bool GetEntryFreeze(u32 index) const; @@ -125,9 +128,15 @@ public: void UpdateValues(); + void ClearEntries(); + + bool LoadFromFile(const char* path, Error* error); + bool SaveToFile(const char* path, Error* error); + private: static void SetEntryValue(Entry* entry, u32 value); static void UpdateEntryValue(Entry* entry); EntryVector m_entries; + bool m_entries_changed = false; }; diff --git a/src/duckstation-qt/memoryscannerwindow.cpp b/src/duckstation-qt/memoryscannerwindow.cpp index 3c53a2170..2e594e115 100644 --- a/src/duckstation-qt/memoryscannerwindow.cpp +++ b/src/duckstation-qt/memoryscannerwindow.cpp @@ -8,9 +8,13 @@ #include "core/bus.h" #include "core/cpu_core.h" #include "core/host.h" +#include "core/settings.h" #include "core/system.h" #include "common/assert.h" +#include "common/error.h" +#include "common/file_system.h" +#include "common/path.h" #include "common/string_util.h" #include "fmt/format.h" @@ -273,6 +277,13 @@ void MemoryScannerWindow::onSystemStarted() m_update_timer->start(SCAN_INTERVAL); enableUi(true); + + // this is a bit yuck, but the title is cleared by the time that onSystemDestroyed() is called, + // which means we can't generate it there to save... + m_watch_save_filename = QStringLiteral("%1.ini").arg(QtHost::GetCurrentGameTitle()).toStdString(); + Path::SanitizeFileName(&m_watch_save_filename); + + reloadWatches(); } void MemoryScannerWindow::onSystemDestroyed() @@ -280,6 +291,9 @@ void MemoryScannerWindow::onSystemDestroyed() if (m_update_timer->isActive()) m_update_timer->stop(); + clearWatches(); + m_watch_save_filename = {}; + enableUi(false); } @@ -485,7 +499,7 @@ void MemoryScannerWindow::updateScanValue() } QTableWidgetItem* MemoryScannerWindow::createValueItem(MemoryAccessSize size, u32 value, bool is_signed, - bool editable) const + bool editable) const { QTableWidgetItem* item; if (m_ui.scanValueBase->currentIndex() == 0) @@ -648,3 +662,73 @@ void MemoryScannerWindow::updateScanUi() updateResultsValues(); updateWatchValues(); } + +std::string MemoryScannerWindow::getWatchSavePath(bool saving) +{ + std::string ret; + + if (m_watch_save_filename.empty()) + return ret; + + const std::string dir = Path::Combine(EmuFolders::DataRoot, "watches"); + if (saving && !FileSystem::DirectoryExists(dir.c_str())) + { + Error error; + if (!FileSystem::CreateDirectory(dir.c_str(), false, &error)) + { + QMessageBox::critical( + this, windowTitle(), + tr("Failed to create watches directory: %1").arg(QString::fromStdString(error.GetDescription()))); + return ret; + } + } + + ret = Path::Combine(dir, m_watch_save_filename); + return ret; +} + +void MemoryScannerWindow::saveWatches() +{ + if (!m_watch.HasEntriesChanged()) + return; + + const std::string path = getWatchSavePath(true); + if (path.empty()) + return; + + Error error; + if (!m_watch.SaveToFile(path.c_str(), &error)) + { + QMessageBox::critical(this, windowTitle(), + tr("Failed to save watches to file: %1").arg(QString::fromStdString(error.GetDescription()))); + } +} + +void MemoryScannerWindow::reloadWatches() +{ + saveWatches(); + + m_watch.ClearEntries(); + + const std::string path = getWatchSavePath(false); + if (!path.empty() && FileSystem::FileExists(path.c_str())) + { + Error error; + if (!m_watch.LoadFromFile(path.c_str(), &error)) + { + QMessageBox::critical( + this, windowTitle(), + tr("Failed to load watches from file: %1").arg(QString::fromStdString(error.GetDescription()))); + } + } + + updateWatch(); +} + +void MemoryScannerWindow::clearWatches() +{ + saveWatches(); + + m_watch.ClearEntries(); + updateWatch(); +} diff --git a/src/duckstation-qt/memoryscannerwindow.h b/src/duckstation-qt/memoryscannerwindow.h index 08769f28b..e847f95d0 100644 --- a/src/duckstation-qt/memoryscannerwindow.h +++ b/src/duckstation-qt/memoryscannerwindow.h @@ -72,10 +72,16 @@ private: QTableWidgetItem* createValueItem(MemoryAccessSize size, u32 value, bool is_signed, bool editable) const; + std::string getWatchSavePath(bool saving); + void saveWatches(); + void reloadWatches(); + void clearWatches(); + Ui::MemoryScannerWindow m_ui; MemoryScan m_scanner; MemoryWatchList m_watch; QTimer* m_update_timer = nullptr; + std::string m_watch_save_filename; };