Qt: Save debugger breakpoints to inis

pull/3794/head
Stenzek 3 weeks ago
parent 3eee1e61e2
commit 92fb83b6ae
No known key found for this signature in database

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// 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<size_t>(type)];
}
const char* CPU::GetBreakpointTypeDisplayName(BreakpointType type)
{
static constexpr std::array<const char*, static_cast<u32>(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<size_t>(type)], "BreakpointType");
}
std::optional<CPU::BreakpointType> CPU::ParseBreakpointTypeName(std::string_view sv)
{
for (u8 i = 0; i < static_cast<u8>(CPU::BreakpointType::Count); i++)
{
const CPU::BreakpointType type = static_cast<CPU::BreakpointType>(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();
}

@ -13,6 +13,7 @@
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
class StateWrapper;
@ -246,6 +247,8 @@ using BreakpointList = std::vector<Breakpoint>;
// Breakpoints
const char* GetBreakpointTypeName(BreakpointType type);
const char* GetBreakpointTypeDisplayName(BreakpointType type);
std::optional<BreakpointType> 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();

@ -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 <fmt/format.h>
#include <tuple>
#include <QtCore/QPointer>
#include <QtCore/QSignalBlocker>
@ -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<uint>(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<INISettingsInterface>(
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<CPU::BreakpointType, VirtualMemoryAddress, bool>;
std::vector<QueuedBreakpoint> queued_bps;
for (const auto& [type_name, value] : m_settings->GetKeyValueList(BREAKPOINTS_SECTION))
{
const std::optional<CPU::BreakpointType> type = CPU::ParseBreakpointTypeName(type_name);
const std::vector<std::string_view> parts = StringUtil::SplitString(value, ',', false);
const std::optional<u32> address =
(parts.size() == 2) ? StringUtil::FromCharsWithOptionalBase<u32>(parts[0]) : std::nullopt;
const std::optional<bool> enabled = (parts.size() == 2) ? StringUtil::FromChars<bool>(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<DebuggerWindow>(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<size_t>(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<CPU::BreakpointType>(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())));
}
}

@ -10,8 +10,11 @@
#include <QtCore/QTimer>
#include <QtWidgets/QMainWindow>
#include <memory>
#include <optional>
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<INISettingsInterface> m_settings;
};

Loading…
Cancel
Save