From 92fb83b6ae1bf398796571e95c86a005e4efb271 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 6 Sep 2026 01:22:23 +1000 Subject: [PATCH] Qt: Save debugger breakpoints to inis --- src/core/cpu_core.cpp | 62 +++++++- src/core/cpu_core.h | 5 +- src/duckstation-qt/debuggerwindow.cpp | 198 ++++++++++++++++++++++++-- src/duckstation-qt/debuggerwindow.h | 9 ++ 4 files changed, 258 insertions(+), 16 deletions(-) diff --git a/src/core/cpu_core.cpp b/src/core/cpu_core.cpp index 267f5558f..2e96db45c 100644 --- a/src/core/cpu_core.cpp +++ b/src/core/cpu_core.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "cpu_core.h" @@ -16,12 +16,14 @@ #include "timing_event.h" #include "util/state_wrapper.h" +#include "util/translation.h" #include "common/align.h" #include "common/fastjmp.h" #include "common/file_system.h" #include "common/log.h" #include "common/path.h" +#include "common/string_util.h" #include "fmt/format.h" @@ -196,7 +198,7 @@ void CPU::Initialize() void CPU::Shutdown() { - ClearBreakpoints(); + ClearBreakpoints(true, true); StopTrace(); } @@ -2254,6 +2256,28 @@ const char* CPU::GetBreakpointTypeName(BreakpointType type) return names[static_cast(type)]; } +const char* CPU::GetBreakpointTypeDisplayName(BreakpointType type) +{ + static constexpr std::array(BreakpointType::Count)> names = {{ + TRANSLATE_DISAMBIG_NOOP("CPU", "Execute", "BreakpointType"), + TRANSLATE_DISAMBIG_NOOP("CPU", "Read", "BreakpointType"), + TRANSLATE_DISAMBIG_NOOP("CPU", "Write", "BreakpointType"), + }}; + return Host::TranslateToCString("CPU", names[static_cast(type)], "BreakpointType"); +} + +std::optional CPU::ParseBreakpointTypeName(std::string_view sv) +{ + for (u8 i = 0; i < static_cast(CPU::BreakpointType::Count); i++) + { + const CPU::BreakpointType type = static_cast(i); + if (StringUtil::EqualNoCase(sv, CPU::GetBreakpointTypeName(type))) + return type; + } + + return std::nullopt; +} + bool CPU::HasBreakpointAtAddress(BreakpointType type, VirtualMemoryAddress address) { for (Breakpoint& bp : GetBreakpointList(type)) @@ -2370,12 +2394,36 @@ bool CPU::RemoveBreakpoint(BreakpointType type, VirtualMemoryAddress address) return true; } -void CPU::ClearBreakpoints() +void CPU::ClearBreakpoints(bool include_auto_clear /*= false*/, bool include_callbacks /*= false*/) { - for (BreakpointList& bplist : s_locals.breakpoints) - bplist.clear(); - s_locals.breakpoint_counter = 0; - s_locals.last_breakpoint_check_pc = INVALID_BREAKPOINT_PC; + if (include_auto_clear && include_callbacks) + { + for (BreakpointList& bplist : s_locals.breakpoints) + bplist.clear(); + } + else + { + for (BreakpointList& bplist : s_locals.breakpoints) + { + for (auto iter = bplist.begin(); iter != bplist.end();) + { + if ((iter->callback && !include_callbacks) || (iter->auto_clear && !include_auto_clear)) + { + ++iter; + continue; + } + + iter = bplist.erase(iter); + } + } + } + + if (std::ranges::all_of(s_locals.breakpoints, [](const auto& bplist) { return bplist.empty(); })) + { + s_locals.breakpoint_counter = 0; + s_locals.last_breakpoint_check_pc = INVALID_BREAKPOINT_PC; + } + if (UpdateDebugDispatcherFlag()) System::InterruptExecution(); } diff --git a/src/core/cpu_core.h b/src/core/cpu_core.h index a4a48d0d1..3bc3af20a 100644 --- a/src/core/cpu_core.h +++ b/src/core/cpu_core.h @@ -13,6 +13,7 @@ #include #include #include +#include #include class StateWrapper; @@ -246,6 +247,8 @@ using BreakpointList = std::vector; // Breakpoints const char* GetBreakpointTypeName(BreakpointType type); +const char* GetBreakpointTypeDisplayName(BreakpointType type); +std::optional ParseBreakpointTypeName(std::string_view sv); bool HasAnyBreakpoints(); bool HasBreakpointAtAddress(BreakpointType type, VirtualMemoryAddress address); BreakpointList CopyBreakpointList(bool include_auto_clear = false, bool include_callbacks = false); @@ -253,7 +256,7 @@ bool AddBreakpoint(BreakpointType type, VirtualMemoryAddress address, bool auto_ bool AddBreakpointWithCallback(BreakpointType type, VirtualMemoryAddress address, BreakpointCallback callback); bool SetBreakpointEnabled(BreakpointType type, VirtualMemoryAddress address, bool enabled); bool RemoveBreakpoint(BreakpointType type, VirtualMemoryAddress address); -void ClearBreakpoints(); +void ClearBreakpoints(bool include_auto_clear = false, bool include_callbacks = false); bool AddStepOverBreakpoint(); bool AddStepOutBreakpoint(u32 max_instructions_to_search = 1000); void SetSingleStepFlag(); diff --git a/src/duckstation-qt/debuggerwindow.cpp b/src/duckstation-qt/debuggerwindow.cpp index 40ecf8eba..d1cd3c54c 100644 --- a/src/duckstation-qt/debuggerwindow.cpp +++ b/src/duckstation-qt/debuggerwindow.cpp @@ -11,11 +11,22 @@ #include "core/cpu_code_cache.h" #include "core/cpu_core_private.h" #include "core/cpu_disasm.h" +#include "core/settings.h" +#include "core/system.h" #include "common/assert.h" #include "common/error.h" +#include "common/file_system.h" #include "common/log.h" +#include "common/path.h" #include "common/small_string.h" +#include "common/string_util.h" + +#include "util/ini_settings_interface.h" + +#include + +#include #include #include @@ -32,6 +43,7 @@ using namespace Qt::StringLiterals; LOG_CHANNEL(Host); static constexpr int TIMER_REFRESH_INTERVAL_MS = 100; +static constexpr const char* BREAKPOINTS_SECTION = "Breakpoints"; DebuggerWindow::DebuggerWindow(QWidget* parent /* = nullptr */) : QMainWindow(parent), m_active_memory_region(Bus::MemoryRegion::Count) @@ -41,10 +53,16 @@ DebuggerWindow::DebuggerWindow(QWidget* parent /* = nullptr */) connectSignals(); createModels(); setMemoryViewRegion(Bus::MemoryRegion::RAM); - if (QtHost::IsSystemValid() && QtHost::IsSystemPaused()) - onSystemPaused(); - else if (QtHost::IsSystemValid()) - onSystemStarted(); + if (QtHost::IsSystemValid()) + { + m_game_serial = QtHost::GetCurrentGameSerial(); + loadGameSettings(false); + + if (QtHost::IsSystemPaused()) + onSystemPaused(); + else + onSystemStarted(); + } else onSystemDestroyed(); } @@ -53,16 +71,36 @@ DebuggerWindow::~DebuggerWindow() = default; void DebuggerWindow::onSystemStarted() { + if (const QString& serial = QtHost::GetCurrentGameSerial(); m_game_serial != serial) + { + m_game_serial = serial; + loadGameSettings(true); + } + setUIEnabled(false, true); } void DebuggerWindow::onSystemDestroyed() { + m_game_serial.clear(); + refreshBreakpointList({}); m_call_stack_model->clear(); m_threads_model->clear(); setUIEnabled(false, false); } +void DebuggerWindow::onSystemGameChanged(const QString& path, const QString& serial, const QString& title) +{ + Q_UNUSED(path); + Q_UNUSED(title); + + if (m_game_serial == serial) + return; + + m_game_serial = serial; + loadGameSettings(true); +} + void DebuggerWindow::onSystemPaused() { setUIEnabled(true, true); @@ -551,9 +589,10 @@ void DebuggerWindow::onMemorySearchStringChanged(const QString&) void DebuggerWindow::closeEvent(QCloseEvent* event) { - QtUtils::SaveWindowGeometry(this); g_core_thread->disconnect(this); - Host::RunOnCoreThread(&CPU::ClearBreakpoints); + Host::RunOnCoreThread([]() { CPU::ClearBreakpoints(true, false); }); + QtUtils::SaveWindowGeometry(this); + saveGameSettings(); QMainWindow::closeEvent(event); emit closed(); } @@ -593,6 +632,7 @@ void DebuggerWindow::connectSignals() connect(g_core_thread, &CoreThread::systemResumed, this, &DebuggerWindow::onSystemResumed); connect(g_core_thread, &CoreThread::systemStarted, this, &DebuggerWindow::onSystemStarted); connect(g_core_thread, &CoreThread::systemDestroyed, this, &DebuggerWindow::onSystemDestroyed); + connect(g_core_thread, &CoreThread::systemGameChanged, this, &DebuggerWindow::onSystemGameChanged); connect(m_ui.actionPause, &QAction::triggered, this, &DebuggerWindow::onPauseActionTriggered); connect(m_ui.actionRunToCursor, &QAction::triggered, this, &DebuggerWindow::onRunToCursorTriggered); @@ -770,7 +810,8 @@ void DebuggerWindow::toggleBreakpoint(VirtualMemoryAddress address) void DebuggerWindow::clearBreakpoints() { m_ui.codeView->clearBreakpoints(); - Host::RunOnCoreThread(&CPU::ClearBreakpoints); + refreshBreakpointList(CPU::BreakpointList()); + Host::RunOnCoreThread([]() { CPU::ClearBreakpoints(false, false); }); } bool DebuggerWindow::tryFollowLoadStore(VirtualMemoryAddress address) @@ -845,7 +886,7 @@ void DebuggerWindow::refreshBreakpointList(const CPU::BreakpointList& bps) item->setCheckState(0, bp.enabled ? Qt::Checked : Qt::Unchecked); item->setText(0, QString::asprintf("%u", bp.number)); item->setText(1, QString::asprintf("0x%08X", bp.address)); - item->setText(2, QString::fromUtf8(CPU::GetBreakpointTypeName(bp.type))); + item->setText(2, QString::fromUtf8(CPU::GetBreakpointTypeDisplayName(bp.type))); item->setText(3, QString::asprintf("%u", bp.hit_count)); item->setData(0, Qt::UserRole, bp.number); item->setData(1, Qt::UserRole, QVariant(static_cast(bp.address))); @@ -939,3 +980,144 @@ void Host::ReportDebuggerEvent(CPU::DebuggerEvent event, std::string_view messag }); } } + +void DebuggerWindow::loadGameSettings(bool clear_existing) +{ + // can't have breakpoints without serial + if (m_game_serial.isEmpty()) + { + saveGameSettings(); + m_settings.reset(); + + if (!clear_existing) + return; + + m_ui.codeView->clearBreakpoints(); + refreshBreakpointList(CPU::BreakpointList()); + Host::RunOnCoreThread([]() { CPU::ClearBreakpoints(false, false); }); + return; + } + + m_settings = std::make_unique( + Path::Combine(EmuFolders::GameSettings, fmt::format("{}.debugger.ini", m_game_serial.toStdString()))); + if (Error error; FileSystem::FileExists(m_settings->GetPath().c_str())) + { + INFO_LOG("Loading debugger settings from {}", Path::GetFileName(m_settings->GetPath())); + if (!m_settings->Load(&error)) + { + ERROR_LOG("Failed to load debugger settings from '{}': {}", Path::GetFileName(m_settings->GetPath()), + error.GetDescription()); + QtUtils::AsyncMessageBox( + this, QMessageBox::Critical, windowTitle(), + u"Failed to load debugger settings: %1"_s.arg(QtUtils::StringViewToQStringView(error.GetDescription()))); + } + } + + using QueuedBreakpoint = std::tuple; + std::vector queued_bps; + + for (const auto& [type_name, value] : m_settings->GetKeyValueList(BREAKPOINTS_SECTION)) + { + const std::optional type = CPU::ParseBreakpointTypeName(type_name); + const std::vector parts = StringUtil::SplitString(value, ',', false); + const std::optional address = + (parts.size() == 2) ? StringUtil::FromCharsWithOptionalBase(parts[0]) : std::nullopt; + const std::optional enabled = (parts.size() == 2) ? StringUtil::FromChars(parts[1]) : std::nullopt; + if (!type.has_value() || !address.has_value() || !enabled.has_value()) + { + WARNING_LOG("Ignoring invalid debugger breakpoint '{} = {}'.", type_name, value); + continue; + } + + const QueuedBreakpoint bp = std::make_tuple(type.value(), address.value(), enabled.value()); + if (std::ranges::find(queued_bps, bp) != queued_bps.end()) + { + WARNING_LOG("Ignoring duplicate debugger breakpoint '{} = {}'.", type_name, value); + continue; + } + + queued_bps.push_back(bp); + } + + if (!queued_bps.empty()) + reportMessage(tr("Loaded %1 saved breakpoints.").arg(queued_bps.size())); + + Host::RunOnCoreThread([win = QPointer(this), serial = m_game_serial, clear_existing, + queued_bps = std::move(queued_bps)]() mutable { + if (QtUtils::StringViewToQStringView(System::GetGameSerial()) != serial) + return; + + if (clear_existing) + CPU::ClearBreakpoints(false, false); + + for (const auto& [type, address, enabled] : queued_bps) + CPU::AddBreakpoint(type, address, false, enabled); + + Host::RunOnUIThread([win = std::move(win), serial = std::move(serial), bps = CPU::CopyBreakpointList()]() { + if (win && win->m_game_serial == serial) + win->refreshBreakpointList(bps); + }); + }); +} + +void DebuggerWindow::saveGameSettings() +{ + if (!m_settings) + return; + + // update the breakpoint list, we can yoink it back from the UI to avoid storing it again + if (const int count = m_ui.breakpointsWidget->topLevelItemCount(); count > 0) + { + INISettingsInterface::KeyValueList entries; + entries.reserve(static_cast(count)); + for (int i = 0; i < count; i++) + { + const QTreeWidgetItem* const item = m_ui.breakpointsWidget->topLevelItem(i); + bool address_valid, type_valid; + const u32 address = item->data(1, Qt::UserRole).toUInt(&address_valid); + const CPU::BreakpointType type = + static_cast(item->data(2, Qt::UserRole).toUInt(&type_valid)); + if (!address_valid || !type_valid) + continue; + + const bool enabled = (item->checkState(0) == Qt::Checked); + entries.emplace_back(CPU::GetBreakpointTypeName(type), fmt::format("0x{:08X},{}", address, enabled)); + } + + // assumes it won't set dirty if it is unchanged + m_settings->SetKeyValueList(BREAKPOINTS_SECTION, entries); + } + else + { + m_settings->ClearSection(BREAKPOINTS_SECTION); + } + + // don't save if unchanged + m_settings->RemoveEmptySections(); + if (!m_settings->IsDirty()) + return; + + if (m_settings->IsEmpty()) + { + INFO_LOG("Removing empty debugger settings {}", Path::GetFileName(m_settings->GetPath())); + if (FileSystem::FileExists(m_settings->GetPath().c_str())) + { + if (Error error; !FileSystem::DeleteFile(m_settings->GetPath().c_str(), &error)) + { + ERROR_LOG("Failed to delete debugger settings '{}': {}", Path::GetFileName(m_settings->GetPath()), + error.GetDescription()); + QtUtils::AsyncMessageBox( + this, QMessageBox::Critical, windowTitle(), + u"Failed to delete debugger settings: %1"_s.arg(QtUtils::StringViewToQStringView(error.GetDescription()))); + } + } + } + else if (Error error; !m_settings->Save(&error)) + { + ERROR_LOG("Failed to save debugger settings to '{}': {}", Path::GetFileName(m_settings->GetPath()), + error.GetDescription()); + QtUtils::AsyncMessageBox( + this, QMessageBox::Critical, windowTitle(), + u"Failed to save debugger settings: %1"_s.arg(QtUtils::StringViewToQStringView(error.GetDescription()))); + } +} diff --git a/src/duckstation-qt/debuggerwindow.h b/src/duckstation-qt/debuggerwindow.h index 015c248e9..13f0bf576 100644 --- a/src/duckstation-qt/debuggerwindow.h +++ b/src/duckstation-qt/debuggerwindow.h @@ -10,8 +10,11 @@ #include #include +#include #include +class INISettingsInterface; + namespace Bus { enum class MemoryRegion; } @@ -58,9 +61,12 @@ private: void refreshBreakpointList(const CPU::BreakpointList& bps); void addBreakpoint(CPU::BreakpointType type, u32 address); void removeBreakpoint(CPU::BreakpointType type, u32 address); + void loadGameSettings(bool clear_existing); + void saveGameSettings(); void onSystemStarted(); void onSystemDestroyed(); + void onSystemGameChanged(const QString& path, const QString& serial, const QString& title); void onSystemPaused(); void onSystemResumed(); @@ -103,4 +109,7 @@ private: Bus::MemoryRegion m_active_memory_region; PhysicalMemoryAddress m_next_memory_search_address = 0; + + QString m_game_serial; + std::unique_ptr m_settings; };