Skip to content

Commit 02a1ccb

Browse files
committed
Qt: Persist memory scanner watch list across instances
i.e. save it to a file.
1 parent 52d9f73 commit 02a1ccb

File tree

4 files changed

+204
-3
lines changed

4 files changed

+204
-3
lines changed

src/core/memory_scanner.cpp

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
1+
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com> and contributors.
22
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
33

44
#include "memory_scanner.h"
55
#include "bus.h"
66
#include "cpu_core.h"
77
#include "cpu_core_private.h"
88

9+
#include "common/error.h"
10+
#include "common/file_system.h"
911
#include "common/log.h"
12+
#include "common/path.h"
13+
#include "common/ryml_helpers.h"
1014

1115
#include "fmt/format.h"
1216

@@ -340,6 +344,7 @@ bool MemoryWatchList::AddEntry(std::string description, u32 address, MemoryAcces
340344
entry.freeze = freeze;
341345

342346
m_entries.push_back(std::move(entry));
347+
m_entries_changed = true;
343348
return true;
344349
}
345350

@@ -357,6 +362,7 @@ void MemoryWatchList::RemoveEntry(u32 index)
357362
return;
358363

359364
m_entries.erase(m_entries.begin() + index);
365+
m_entries_changed = true;
360366
}
361367

362368
bool MemoryWatchList::RemoveEntryByAddress(u32 address)
@@ -366,6 +372,7 @@ bool MemoryWatchList::RemoveEntryByAddress(u32 address)
366372
if (it->address == address)
367373
{
368374
m_entries.erase(it);
375+
m_entries_changed = true;
369376
return true;
370377
}
371378
}
@@ -389,6 +396,7 @@ void MemoryWatchList::SetEntryFreeze(u32 index, bool freeze)
389396

390397
Entry& entry = m_entries[index];
391398
entry.freeze = freeze;
399+
m_entries_changed = true;
392400
}
393401

394402
void MemoryWatchList::SetEntryValue(u32 index, u32 value)
@@ -427,6 +435,12 @@ void MemoryWatchList::UpdateValues()
427435
UpdateEntryValue(&entry);
428436
}
429437

438+
void MemoryWatchList::ClearEntries()
439+
{
440+
m_entries.clear();
441+
m_entries_changed = false;
442+
}
443+
430444
void MemoryWatchList::SetEntryValue(Entry* entry, u32 value)
431445
{
432446
switch (entry->size)
@@ -482,3 +496,91 @@ void MemoryWatchList::UpdateEntryValue(Entry* entry)
482496
if (entry->freeze && entry->changed)
483497
SetEntryValue(entry, old_value);
484498
}
499+
500+
bool MemoryWatchList::LoadFromFile(const char* path, Error* error)
501+
{
502+
std::optional<std::string> yaml_data = FileSystem::ReadFileToString(path, error);
503+
if (!yaml_data.has_value())
504+
{
505+
Error::AddPrefixFmt(error, "Failed to read {}: ", Path::GetFileName(path));
506+
return false;
507+
}
508+
509+
m_entries.clear();
510+
m_entries_changed = false;
511+
512+
const ryml::Tree yaml =
513+
ryml::parse_in_place(to_csubstr(path), c4::substr(reinterpret_cast<char*>(yaml_data->data()), yaml_data->size()));
514+
const ryml::ConstNodeRef root = yaml.rootref();
515+
516+
m_entries.reserve(root.num_children());
517+
for (const ryml::ConstNodeRef& child : root.cchildren())
518+
{
519+
Entry entry;
520+
std::string_view address;
521+
std::string_view size;
522+
std::optional<u32> parsed_address;
523+
if (!GetStringFromObject(child, "description", &entry.description) ||
524+
!GetStringFromObject(child, "address", &address) || !GetStringFromObject(child, "size", &size) ||
525+
!GetUIntFromObject(child, "isSigned", &entry.is_signed) || !GetUIntFromObject(child, "freeze", &entry.freeze) ||
526+
!(parsed_address = StringUtil::FromCharsWithOptionalBase<u32>(address)).has_value() ||
527+
(size != "byte" && size != "halfword" && size != "word"))
528+
{
529+
Error::SetStringView(error, "One or more required fields are missing in the memory watch entry.");
530+
m_entries.clear();
531+
return false;
532+
}
533+
534+
entry.address = parsed_address.value();
535+
if (size == "byte")
536+
entry.size = MemoryAccessSize::Byte;
537+
else if (size == "halfword")
538+
entry.size = MemoryAccessSize::HalfWord;
539+
else // if (size == "word")
540+
entry.size = MemoryAccessSize::Word;
541+
542+
entry.changed = false;
543+
UpdateEntryValue(&entry);
544+
545+
m_entries.push_back(std::move(entry));
546+
}
547+
548+
DEV_LOG("Loaded {} entries from {}", m_entries.size(), Path::GetFileName(path));
549+
return true;
550+
}
551+
552+
bool MemoryWatchList::SaveToFile(const char* path, Error* error)
553+
{
554+
std::string buf;
555+
auto appender = std::back_inserter(buf);
556+
557+
for (const Entry& entry : m_entries)
558+
{
559+
fmt::format_to(appender, "- description: {}\n", entry.description);
560+
fmt::format_to(appender, " address: 0x{:08x}\n", entry.address);
561+
fmt::format_to(appender, " size: {}\n",
562+
(entry.size == MemoryAccessSize::Byte) ?
563+
"byte" :
564+
((entry.size == MemoryAccessSize::HalfWord) ? "halfword" : "word"));
565+
fmt::format_to(appender, " isSigned: {}\n", entry.is_signed);
566+
fmt::format_to(appender, " freeze: {}\n", entry.freeze);
567+
}
568+
569+
// avoid rewriting if unchanged
570+
std::optional<std::string> current_file = FileSystem::ReadFileToString(path);
571+
if (current_file.has_value() && current_file.value() == buf)
572+
{
573+
DEV_LOG("Memory watch list unchanged, not saving to {}", Path::GetFileName(path));
574+
m_entries_changed = false;
575+
return true;
576+
}
577+
578+
if (!FileSystem::WriteStringToFile(path, buf, error))
579+
{
580+
Error::AddPrefixFmt(error, "Failed to write {}: ", Path::GetFileName(path));
581+
return false;
582+
}
583+
584+
m_entries_changed = false;
585+
return true;
586+
}

src/core/memory_scanner.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
1+
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com> and contributors.
22
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
33

44
#pragma once
@@ -10,6 +10,8 @@
1010
#include <string_view>
1111
#include <vector>
1212

13+
class Error;
14+
1315
class MemoryScan
1416
{
1517
public:
@@ -111,6 +113,7 @@ class MemoryWatchList
111113
const EntryVector& GetEntries() const { return m_entries; }
112114
const Entry& GetEntry(u32 index) const { return m_entries[index]; }
113115
u32 GetEntryCount() const { return static_cast<u32>(m_entries.size()); }
116+
bool HasEntriesChanged() const { return m_entries_changed; }
114117

115118
bool AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze);
116119
bool GetEntryFreeze(u32 index) const;
@@ -125,9 +128,15 @@ class MemoryWatchList
125128

126129
void UpdateValues();
127130

131+
void ClearEntries();
132+
133+
bool LoadFromFile(const char* path, Error* error);
134+
bool SaveToFile(const char* path, Error* error);
135+
128136
private:
129137
static void SetEntryValue(Entry* entry, u32 value);
130138
static void UpdateEntryValue(Entry* entry);
131139

132140
EntryVector m_entries;
141+
bool m_entries_changed = false;
133142
};

src/duckstation-qt/memoryscannerwindow.cpp

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
#include "core/bus.h"
99
#include "core/cpu_core.h"
1010
#include "core/host.h"
11+
#include "core/settings.h"
1112
#include "core/system.h"
1213

1314
#include "common/assert.h"
15+
#include "common/error.h"
16+
#include "common/file_system.h"
17+
#include "common/path.h"
1418
#include "common/string_util.h"
1519

1620
#include "fmt/format.h"
@@ -273,13 +277,23 @@ void MemoryScannerWindow::onSystemStarted()
273277
m_update_timer->start(SCAN_INTERVAL);
274278

275279
enableUi(true);
280+
281+
// this is a bit yuck, but the title is cleared by the time that onSystemDestroyed() is called,
282+
// which means we can't generate it there to save...
283+
m_watch_save_filename = QStringLiteral("%1.ini").arg(QtHost::GetCurrentGameTitle()).toStdString();
284+
Path::SanitizeFileName(&m_watch_save_filename);
285+
286+
reloadWatches();
276287
}
277288

278289
void MemoryScannerWindow::onSystemDestroyed()
279290
{
280291
if (m_update_timer->isActive())
281292
m_update_timer->stop();
282293

294+
clearWatches();
295+
m_watch_save_filename = {};
296+
283297
enableUi(false);
284298
}
285299

@@ -485,7 +499,7 @@ void MemoryScannerWindow::updateScanValue()
485499
}
486500

487501
QTableWidgetItem* MemoryScannerWindow::createValueItem(MemoryAccessSize size, u32 value, bool is_signed,
488-
bool editable) const
502+
bool editable) const
489503
{
490504
QTableWidgetItem* item;
491505
if (m_ui.scanValueBase->currentIndex() == 0)
@@ -648,3 +662,73 @@ void MemoryScannerWindow::updateScanUi()
648662
updateResultsValues();
649663
updateWatchValues();
650664
}
665+
666+
std::string MemoryScannerWindow::getWatchSavePath(bool saving)
667+
{
668+
std::string ret;
669+
670+
if (m_watch_save_filename.empty())
671+
return ret;
672+
673+
const std::string dir = Path::Combine(EmuFolders::DataRoot, "watches");
674+
if (saving && !FileSystem::DirectoryExists(dir.c_str()))
675+
{
676+
Error error;
677+
if (!FileSystem::CreateDirectory(dir.c_str(), false, &error))
678+
{
679+
QMessageBox::critical(
680+
this, windowTitle(),
681+
tr("Failed to create watches directory: %1").arg(QString::fromStdString(error.GetDescription())));
682+
return ret;
683+
}
684+
}
685+
686+
ret = Path::Combine(dir, m_watch_save_filename);
687+
return ret;
688+
}
689+
690+
void MemoryScannerWindow::saveWatches()
691+
{
692+
if (!m_watch.HasEntriesChanged())
693+
return;
694+
695+
const std::string path = getWatchSavePath(true);
696+
if (path.empty())
697+
return;
698+
699+
Error error;
700+
if (!m_watch.SaveToFile(path.c_str(), &error))
701+
{
702+
QMessageBox::critical(this, windowTitle(),
703+
tr("Failed to save watches to file: %1").arg(QString::fromStdString(error.GetDescription())));
704+
}
705+
}
706+
707+
void MemoryScannerWindow::reloadWatches()
708+
{
709+
saveWatches();
710+
711+
m_watch.ClearEntries();
712+
713+
const std::string path = getWatchSavePath(false);
714+
if (!path.empty() && FileSystem::FileExists(path.c_str()))
715+
{
716+
Error error;
717+
if (!m_watch.LoadFromFile(path.c_str(), &error))
718+
{
719+
QMessageBox::critical(
720+
this, windowTitle(),
721+
tr("Failed to load watches from file: %1").arg(QString::fromStdString(error.GetDescription())));
722+
}
723+
}
724+
725+
updateWatch();
726+
}
727+
728+
void MemoryScannerWindow::clearWatches()
729+
{
730+
saveWatches();
731+
732+
m_watch.ClearEntries();
733+
updateWatch();
734+
}

src/duckstation-qt/memoryscannerwindow.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,16 @@ private Q_SLOTS:
7272

7373
QTableWidgetItem* createValueItem(MemoryAccessSize size, u32 value, bool is_signed, bool editable) const;
7474

75+
std::string getWatchSavePath(bool saving);
76+
void saveWatches();
77+
void reloadWatches();
78+
void clearWatches();
79+
7580
Ui::MemoryScannerWindow m_ui;
7681

7782
MemoryScan m_scanner;
7883
MemoryWatchList m_watch;
7984

8085
QTimer* m_update_timer = nullptr;
86+
std::string m_watch_save_filename;
8187
};

0 commit comments

Comments
 (0)