Qt: Persist memory scanner watch list across instances
i.e. save it to a file.
This commit is contained in:
parent
52d9f73f98
commit
02a1ccbcdc
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com> 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<std::string> 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<char*>(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<u32> 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<u32>(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<std::string> 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;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com> and contributors.
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
|
@ -10,6 +10,8 @@
|
|||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
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<u32>(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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue