First Commit

This commit is contained in:
2025-11-18 14:18:26 -07:00
parent 33eb6e3707
commit 27277ec342
6106 changed files with 3571167 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "BreakpointDialog.h"
#include "DebugTools/Breakpoints.h"
#include "QtUtils.h"
#include "QtHost.h"
#include <QtWidgets/QDialog>
#include <QtWidgets/QMessageBox>
BreakpointDialog::BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model)
: QDialog(parent)
, m_cpu(cpu)
, m_purpose(PURPOSE::CREATE)
, m_bpModel(model)
{
m_ui.setupUi(this);
m_ui.grpType->setEnabled(true);
m_ui.txtAddress->setEnabled(true);
connect(m_ui.rdoExecute, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
connect(m_ui.rdoMemory, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
}
BreakpointDialog::BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model, BreakpointMemcheck bp_mc, int rowIndex)
: QDialog(parent)
, m_cpu(cpu)
, m_purpose(PURPOSE::EDIT)
, m_bpModel(model)
, m_bp_mc(bp_mc)
, m_rowIndex(rowIndex)
{
m_ui.setupUi(this);
connect(m_ui.rdoExecute, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
connect(m_ui.rdoMemory, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
m_ui.rdoExecute->setChecked(true);
m_ui.chkEnable->setChecked(bp->enabled);
m_ui.txtAddress->setText(QtUtils::FilledQStringFromValue(bp->addr, 16));
m_ui.txtDescription->setText(QString::fromStdString(bp->description));
if (bp->hasCond)
m_ui.txtCondition->setText(QString::fromStdString(bp->cond.expressionString));
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
m_ui.rdoMemory->setChecked(true);
m_ui.txtAddress->setText(QtUtils::FilledQStringFromValue(mc->start, 16));
m_ui.txtSize->setText(QtUtils::FilledQStringFromValue(mc->end - mc->start, 16));
m_ui.txtDescription->setText(QString::fromStdString(mc->description));
m_ui.chkRead->setChecked(mc->memCond & MEMCHECK_READ);
m_ui.chkWrite->setChecked(mc->memCond & MEMCHECK_WRITE);
m_ui.chkChange->setChecked(mc->memCond & MEMCHECK_WRITE_ONCHANGE);
m_ui.chkEnable->setChecked(mc->result & MEMCHECK_BREAK);
m_ui.chkLog->setChecked(mc->result & MEMCHECK_LOG);
if (mc->hasCond)
m_ui.txtCondition->setText(QString::fromStdString(mc->cond.expressionString));
}
}
BreakpointDialog::~BreakpointDialog()
{
}
void BreakpointDialog::onRdoButtonToggled()
{
const bool isExecute = m_ui.rdoExecute->isChecked();
m_ui.grpMemory->setEnabled(!isExecute);
m_ui.chkLog->setEnabled(!isExecute);
}
void BreakpointDialog::accept()
{
std::string error;
if (m_purpose == PURPOSE::CREATE)
{
if (m_ui.rdoExecute->isChecked())
m_bp_mc = BreakPoint();
else if (m_ui.rdoMemory->isChecked())
m_bp_mc = MemCheck();
}
if (auto* bp = std::get_if<BreakPoint>(&m_bp_mc))
{
PostfixExpression expr;
u64 address;
if (!m_cpu->evaluateExpression(m_ui.txtAddress->text().toStdString().c_str(), address, error))
{
QMessageBox::warning(this, tr("Invalid Address"), QString::fromStdString(error));
return;
}
bp->addr = address;
bp->description = m_ui.txtDescription->text().toStdString();
bp->enabled = m_ui.chkEnable->isChecked();
if (!m_ui.txtCondition->text().isEmpty())
{
bp->hasCond = true;
bp->cond.debug = m_cpu;
if (!m_cpu->initExpression(m_ui.txtCondition->text().toStdString().c_str(), expr, error))
{
QMessageBox::warning(this, tr("Invalid Condition"), QString::fromStdString(error));
return;
}
bp->cond.expression = expr;
bp->cond.expressionString = m_ui.txtCondition->text().toStdString();
}
}
if (auto* mc = std::get_if<MemCheck>(&m_bp_mc))
{
u64 startAddress;
if (!m_cpu->evaluateExpression(m_ui.txtAddress->text().toStdString().c_str(), startAddress, error))
{
QMessageBox::warning(this, tr("Invalid Address"), QString::fromStdString(error));
return;
}
u64 size;
if (!m_cpu->evaluateExpression(m_ui.txtSize->text().toStdString().c_str(), size, error) || !size)
{
QMessageBox::warning(this, tr("Invalid Size"), QString::fromStdString(error));
return;
}
mc->start = startAddress;
mc->end = startAddress + size;
mc->description = m_ui.txtDescription->text().toStdString();
if (!m_ui.txtCondition->text().isEmpty())
{
mc->hasCond = true;
mc->cond.debug = m_cpu;
PostfixExpression expr;
if (!m_cpu->initExpression(m_ui.txtCondition->text().toStdString().c_str(), expr, error))
{
QMessageBox::warning(this, tr("Invalid Condition"), QString::fromStdString(error));
return;
}
mc->cond.expression = expr;
mc->cond.expressionString = m_ui.txtCondition->text().toStdString();
}
int condition = 0;
if (m_ui.chkRead->isChecked())
condition |= MEMCHECK_READ;
if (m_ui.chkWrite->isChecked())
condition |= MEMCHECK_WRITE;
if (m_ui.chkChange->isChecked())
condition |= MEMCHECK_WRITE_ONCHANGE;
mc->memCond = static_cast<MemCheckCondition>(condition);
int result = 0;
if (m_ui.chkEnable->isChecked())
result |= MEMCHECK_BREAK;
if (m_ui.chkLog->isChecked())
result |= MEMCHECK_LOG;
mc->result = static_cast<MemCheckResult>(result);
}
if (m_purpose == PURPOSE::EDIT)
{
m_bpModel.removeRows(m_rowIndex, 1);
}
m_bpModel.insertBreakpointRows(0, 1, {m_bp_mc});
QDialog::accept();
}

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_BreakpointDialog.h"
#include "BreakpointModel.h"
#include "DebugTools/Breakpoints.h"
#include <QtWidgets/QDialog>
class BreakpointDialog final : public QDialog
{
Q_OBJECT
public:
BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model);
BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model, BreakpointMemcheck bpmc, int rowIndex);
~BreakpointDialog();
public slots:
void onRdoButtonToggled();
void accept() override;
private:
enum class PURPOSE
{
CREATE,
EDIT
};
Ui::BreakpointDialog m_ui;
DebugInterface* m_cpu;
const PURPOSE m_purpose;
BreakpointModel& m_bpModel;
BreakpointMemcheck m_bp_mc;
int m_rowIndex;
};

View File

@@ -0,0 +1,348 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BreakpointDialog</class>
<widget class="QDialog" name="BreakpointDialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>375</width>
<height>300</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>375</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>375</width>
<height>300</height>
</size>
</property>
<property name="baseSize">
<size>
<width>375</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>Create / Modify Breakpoint</string>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>20</x>
<y>260</y>
<width>341</width>
<height>32</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QGroupBox" name="grpType">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>0</y>
<width>91</width>
<height>81</height>
</rect>
</property>
<property name="title">
<string>Type</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="1" column="0">
<widget class="QRadioButton" name="rdoExecute">
<property name="text">
<string>Execute</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="rdoMemory">
<property name="text">
<string>Memory</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="groupBox_2">
<property name="geometry">
<rect>
<x>110</x>
<y>10</y>
<width>250</width>
<height>79</height>
</rect>
</property>
<property name="title">
<string/>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtAddress">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>19</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Description</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="txtDescription">
<property name="minimumSize">
<size>
<width>0</width>
<height>19</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>19</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="grpMemory">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>110</x>
<y>100</y>
<width>251</width>
<height>91</height>
</rect>
</property>
<property name="title">
<string>Memory</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="chkRead">
<property name="text">
<string>Read</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkWrite">
<property name="text">
<string>Write</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkChange">
<property name="text">
<string>Change</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Size</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtSize">
<property name="minimumSize">
<size>
<width>0</width>
<height>19</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="grpExecute">
<property name="geometry">
<rect>
<x>110</x>
<y>190</y>
<width>251</width>
<height>61</height>
</rect>
</property>
<property name="title">
<string/>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Condition</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtCondition"/>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="groupBox_5">
<property name="geometry">
<rect>
<x>10</x>
<y>90</y>
<width>91</width>
<height>71</height>
</rect>
</property>
<property name="title">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="chkLog">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Log</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkEnable">
<property name="text">
<string>Enable</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>BreakpointDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>BreakpointDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,694 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "BreakpointModel.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "Debugger/DebuggerSettingsManager.h"
#include "DebugTools/DebugInterface.h"
#include "DebugTools/Breakpoints.h"
#include "DebugTools/DisassemblyManager.h"
#include "common/Console.h"
#include <QtWidgets/QMessageBox>
#include <algorithm>
std::map<BreakPointCpu, BreakpointModel*> BreakpointModel::s_instances;
BreakpointModel::BreakpointModel(DebugInterface& cpu, QObject* parent)
: QAbstractTableModel(parent)
, m_cpu(cpu)
{
if (m_cpu.getCpuType() == BREAKPOINT_EE)
{
connect(g_emu_thread, &EmuThread::onGameChanged, this, [this](const QString& title) {
if (title.isEmpty())
return;
if (rowCount() == 0)
DebuggerSettingsManager::loadGameSettings(this);
});
DebuggerSettingsManager::loadGameSettings(this);
}
connect(this, &BreakpointModel::dataChanged, this, &BreakpointModel::refreshData);
}
BreakpointModel* BreakpointModel::getInstance(DebugInterface& cpu)
{
auto iterator = s_instances.find(cpu.getCpuType());
if (iterator == s_instances.end())
iterator = s_instances.emplace(cpu.getCpuType(), new BreakpointModel(cpu)).first;
return iterator->second;
}
int BreakpointModel::rowCount(const QModelIndex&) const
{
return m_breakpoints.size();
}
int BreakpointModel::columnCount(const QModelIndex&) const
{
return BreakpointColumns::COLUMN_COUNT;
}
QVariant BreakpointModel::data(const QModelIndex& index, int role) const
{
const size_t row = static_cast<size_t>(index.row());
if (!index.isValid() || row >= m_breakpoints.size())
return QVariant();
const BreakpointMemcheck& bp_mc = m_breakpoints[row];
if (role == Qt::DisplayRole)
{
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::ENABLED:
return (bp->enabled) ? tr("Enabled") : tr("Disabled");
case BreakpointColumns::TYPE:
return tr("Execute");
case BreakpointColumns::OFFSET:
return QtUtils::FilledQStringFromValue(bp->addr, 16);
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(bp->description);
case BreakpointColumns::SIZE_LABEL:
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(bp->addr).name);
case BreakpointColumns::OPCODE:
// Note: Fix up the disassemblymanager so we can use it here, instead of calling a function through the disassemblyview (yuck)
return m_cpu.disasm(bp->addr, true).c_str();
case BreakpointColumns::CONDITION:
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
case BreakpointColumns::HITS:
return tr("--");
}
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::ENABLED:
return (mc->result & MEMCHECK_BREAK) ? tr("Enabled") : tr("Disabled");
case BreakpointColumns::TYPE:
{
QString type("");
type += (mc->memCond & MEMCHECK_READ) ? tr("Read") : "";
type += ((mc->memCond & MEMCHECK_READWRITE) == MEMCHECK_READWRITE) ? ", " : " ";
//: (C) = changes, as in "look for changes".
type += (mc->memCond & MEMCHECK_WRITE) ? (mc->memCond & MEMCHECK_WRITE_ONCHANGE) ? tr("Write(C)") : tr("Write") : "";
return type;
}
case BreakpointColumns::OFFSET:
return QtUtils::FilledQStringFromValue(mc->start, 16);
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(mc->description);
case BreakpointColumns::SIZE_LABEL:
return QString::number(mc->end - mc->start, 16);
case BreakpointColumns::OPCODE:
return tr("--"); // Our address is going to point to memory, no purpose in printing the op
case BreakpointColumns::CONDITION:
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
case BreakpointColumns::HITS:
return QString::number(mc->numHits);
}
}
}
else if (role == Qt::EditRole)
{
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::CONDITION:
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(bp->description);
}
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::CONDITION:
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(mc->description);
}
}
}
else if (role == BreakpointModel::DataRole)
{
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::ENABLED:
return static_cast<int>(bp->enabled);
case BreakpointColumns::TYPE:
return MEMCHECK_INVALID;
case BreakpointColumns::OFFSET:
return bp->addr;
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(bp->description);
case BreakpointColumns::SIZE_LABEL:
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(bp->addr).name);
case BreakpointColumns::OPCODE:
// Note: Fix up the disassemblymanager so we can use it here, instead of calling a function through the disassemblyview (yuck)
return m_cpu.disasm(bp->addr, false).c_str();
case BreakpointColumns::CONDITION:
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
case BreakpointColumns::HITS:
return 0;
}
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::ENABLED:
return (mc->result & MEMCHECK_BREAK);
case BreakpointColumns::TYPE:
return mc->memCond;
case BreakpointColumns::OFFSET:
return mc->start;
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(mc->description);
case BreakpointColumns::SIZE_LABEL:
return mc->end - mc->start;
case BreakpointColumns::OPCODE:
return "";
case BreakpointColumns::CONDITION:
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
case BreakpointColumns::HITS:
return mc->numHits;
}
}
}
else if (role == BreakpointModel::ExportRole)
{
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::ENABLED:
return static_cast<int>(bp->enabled);
case BreakpointColumns::TYPE:
return MEMCHECK_INVALID;
case BreakpointColumns::OFFSET:
return QtUtils::FilledQStringFromValue(bp->addr, 16);
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(bp->description);
case BreakpointColumns::SIZE_LABEL:
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(bp->addr).name);
case BreakpointColumns::OPCODE:
// Note: Fix up the disassemblymanager so we can use it here, instead of calling a function through the disassemblyview (yuck)
return m_cpu.disasm(bp->addr, false).c_str();
case BreakpointColumns::CONDITION:
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
case BreakpointColumns::HITS:
return 0;
}
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
switch (index.column())
{
case BreakpointColumns::TYPE:
return mc->memCond;
case BreakpointColumns::OFFSET:
return QtUtils::FilledQStringFromValue(mc->start, 16);
case BreakpointColumns::DESCRIPTION:
return QString::fromStdString(mc->description);
case BreakpointColumns::SIZE_LABEL:
return mc->end - mc->start;
case BreakpointColumns::OPCODE:
return "";
case BreakpointColumns::CONDITION:
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
case BreakpointColumns::HITS:
return mc->numHits;
case BreakpointColumns::ENABLED:
return (mc->result & MEMCHECK_BREAK);
}
}
}
else if (role == Qt::CheckStateRole)
{
if (index.column() == 0)
{
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
return bp->enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked;
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
return (mc->result & MEMCHECK_BREAK) ? Qt::CheckState::Checked : Qt::CheckState::Unchecked;
}
}
}
return QVariant();
}
QVariant BreakpointModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
{
switch (section)
{
case BreakpointColumns::TYPE:
//: Warning: limited space available. Abbreviate if needed.
return tr("TYPE");
case BreakpointColumns::OFFSET:
//: Warning: limited space available. Abbreviate if needed.
return tr("OFFSET");
case BreakpointColumns::DESCRIPTION:
return "DESCRIPTION";
case BreakpointColumns::SIZE_LABEL:
//: Warning: limited space available. Abbreviate if needed.
return tr("SIZE / LABEL");
case BreakpointColumns::OPCODE:
//: Warning: limited space available. Abbreviate if needed.
return tr("INSTRUCTION");
case BreakpointColumns::CONDITION:
//: Warning: limited space available. Abbreviate if needed.
return tr("CONDITION");
case BreakpointColumns::HITS:
//: Warning: limited space available. Abbreviate if needed.
return tr("HITS");
case BreakpointColumns::ENABLED:
//: Warning: limited space available. Abbreviate if needed.
return tr("X");
default:
return QVariant();
}
}
if (role == Qt::UserRole && orientation == Qt::Horizontal)
{
switch (section)
{
case BreakpointColumns::TYPE:
return "TYPE";
case BreakpointColumns::OFFSET:
return "OFFSET";
case BreakpointColumns::DESCRIPTION:
return "DESCRIPTION";
case BreakpointColumns::SIZE_LABEL:
return "SIZE / LABEL";
case BreakpointColumns::OPCODE:
return "INSTRUCTION";
case BreakpointColumns::CONDITION:
return "CONDITION";
case BreakpointColumns::HITS:
return "HITS";
case BreakpointColumns::ENABLED:
return "X";
default:
return QVariant();
}
}
return QVariant();
}
Qt::ItemFlags BreakpointModel::flags(const QModelIndex& index) const
{
switch (index.column())
{
case BreakpointColumns::CONDITION:
case BreakpointColumns::DESCRIPTION:
return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEditable;
case BreakpointColumns::TYPE:
case BreakpointColumns::OPCODE:
case BreakpointColumns::HITS:
case BreakpointColumns::OFFSET:
case BreakpointColumns::SIZE_LABEL:
return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsSelectable;
case BreakpointColumns::ENABLED:
return Qt::ItemFlag::ItemIsUserCheckable | Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsSelectable;
}
return index.flags();
}
bool BreakpointModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
const size_t row = static_cast<size_t>(index.row());
if (!index.isValid() || row >= m_breakpoints.size())
return false;
const BreakpointMemcheck& bp_mc = m_breakpoints[row];
if (role == Qt::CheckStateRole && index.column() == BreakpointColumns::ENABLED)
{
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
Host::RunOnCPUThread([cpu = this->m_cpu.getCpuType(), bp = *bp, enabled = value.toBool()] {
CBreakPoints::ChangeBreakPoint(cpu, bp.addr, enabled);
});
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
Host::RunOnCPUThread([cpu = this->m_cpu.getCpuType(), mc = *mc] {
CBreakPoints::ChangeMemCheck(cpu, mc.start, mc.end, mc.memCond,
MemCheckResult(mc.result ^ MEMCHECK_BREAK));
});
}
emit dataChanged(index, index);
return true;
}
else if (role == Qt::EditRole && index.column() == BreakpointColumns::CONDITION)
{
if (auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
const QString condValue = value.toString();
if (condValue.isEmpty())
{
if (bp->hasCond)
{
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp] {
CBreakPoints::ChangeBreakPointRemoveCond(cpu, bp->addr);
});
}
}
else
{
PostfixExpression expr;
std::string error;
if (!m_cpu.initExpression(condValue.toLocal8Bit().constData(), expr, error))
{
QMessageBox::warning(nullptr, "Condition Error", QString::fromStdString(error));
return false;
}
BreakPointCond cond;
cond.debug = &m_cpu;
cond.expression = expr;
cond.expressionString = condValue.toStdString();
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp, cond] {
CBreakPoints::ChangeBreakPointAddCond(cpu, bp->addr, cond);
});
}
}
else if (auto* mc = std::get_if<MemCheck>(&bp_mc))
{
const QString condValue = value.toString();
if (condValue.isEmpty())
{
if (mc->hasCond)
{
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc] {
CBreakPoints::ChangeMemCheckRemoveCond(cpu, mc->start, mc->end);
});
}
}
else
{
PostfixExpression expr;
std::string error;
if (!m_cpu.initExpression(condValue.toLocal8Bit().constData(), expr, error))
{
QMessageBox::warning(nullptr, "Condition Error", QString::fromStdString(error));
return false;
}
BreakPointCond cond;
cond.debug = &m_cpu;
cond.expression = expr;
cond.expressionString = condValue.toStdString();
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc, cond] {
CBreakPoints::ChangeMemCheckAddCond(cpu, mc->start, mc->end, cond);
});
}
}
emit dataChanged(index, index);
return true;
}
else if (role == Qt::EditRole && index.column() == BreakpointColumns::DESCRIPTION)
{
// Update BreakPoint description
if (auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
const QString descValue = value.toString();
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp, descValue] {
CBreakPoints::ChangeBreakPointDescription(cpu, bp->addr, descValue.toStdString());
});
}
// Update MemCheck description
else if (auto* mc = std::get_if<MemCheck>(&bp_mc))
{
const QString descValue = value.toString();
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc, descValue] {
CBreakPoints::ChangeMemCheckDescription(cpu, mc->start, mc->end, descValue.toStdString());
});
}
emit dataChanged(index, index);
return true;
}
return false;
}
bool BreakpointModel::removeRows(int row, int count, const QModelIndex& index)
{
const size_t begin_index = static_cast<size_t>(row);
const size_t end_index = static_cast<size_t>(row + count);
if (end_index > m_breakpoints.size())
return false;
beginRemoveRows(index, row, row + count - 1);
for (size_t i = begin_index; i < end_index; i++)
{
auto bp_mc = m_breakpoints.at(i);
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), addr = bp->addr] {
CBreakPoints::RemoveBreakPoint(cpu, addr);
});
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), start = mc->start, end = mc->end] {
CBreakPoints::RemoveMemCheck(cpu, start, end);
});
}
}
const auto begin = m_breakpoints.begin() + row;
const auto end = begin + count;
m_breakpoints.erase(begin, end);
endRemoveRows();
return true;
}
bool BreakpointModel::insertBreakpointRows(int row, int count, std::vector<BreakpointMemcheck> breakpoints, const QModelIndex& index)
{
if (breakpoints.size() != static_cast<size_t>(count))
return false;
beginInsertRows(index, row, row + (count - 1));
// After endInsertRows, Qt will try and validate our new rows
// Because we add the breakpoints off of the UI thread, our new rows may not be visible yet
// To prevent the (seemingly harmless?) warning emitted by enderInsertRows, add the breakpoints manually here as well
m_breakpoints.insert(m_breakpoints.begin(), breakpoints.begin(), breakpoints.end());
for (const auto& bp_mc : breakpoints)
{
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
{
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp = *bp] {
CBreakPoints::AddBreakPoint(cpu, bp.addr, false, bp.enabled);
CBreakPoints::ChangeBreakPointDescription(cpu, bp.addr, bp.description);
if (bp.hasCond)
{
CBreakPoints::ChangeBreakPointAddCond(cpu, bp.addr, bp.cond);
}
});
}
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
{
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc = *mc] {
CBreakPoints::AddMemCheck(cpu, mc.start, mc.end, mc.memCond, mc.result);
CBreakPoints::ChangeMemCheckDescription(cpu, mc.start, mc.end, mc.description);
if (mc.hasCond)
{
CBreakPoints::ChangeMemCheckAddCond(cpu, mc.start, mc.end, mc.cond);
}
});
}
}
endInsertRows();
return true;
}
void BreakpointModel::refreshData()
{
Host::RunOnCPUThread([this]() mutable {
std::vector<BreakpointMemcheck> all_breakpoints;
std::ranges::move(CBreakPoints::GetBreakpoints(m_cpu.getCpuType(), false), std::back_inserter(all_breakpoints));
std::ranges::move(CBreakPoints::GetMemChecks(m_cpu.getCpuType()), std::back_inserter(all_breakpoints));
QtHost::RunOnUIThread([this, breakpoints = std::move(all_breakpoints)]() mutable {
beginResetModel();
m_breakpoints = std::move(breakpoints);
endResetModel();
});
});
}
void BreakpointModel::loadBreakpointFromFieldList(QStringList fields)
{
std::string error;
bool ok;
if (fields.size() != BreakpointColumns::COLUMN_COUNT)
{
Console.WriteLn("Debugger Breakpoint Model: Invalid number of columns, skipping");
return;
}
const int type = fields[BreakpointColumns::TYPE].toUInt(&ok);
if (!ok)
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse type '%s', skipping",
fields[BreakpointColumns::TYPE].toUtf8().constData());
return;
}
// This is how we differentiate between breakpoints and memchecks
if (type == MEMCHECK_INVALID)
{
BreakPoint bp;
// Address
bp.addr = fields[BreakpointColumns::OFFSET].toUInt(&ok, 16);
if (!ok)
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping",
fields[BreakpointColumns::OFFSET].toUtf8().constData());
return;
}
// Condition
if (!fields[BreakpointColumns::CONDITION].isEmpty())
{
PostfixExpression expr;
bp.hasCond = true;
bp.cond.debug = &m_cpu;
if (!m_cpu.initExpression(fields[BreakpointColumns::CONDITION].toUtf8().constData(), expr, error))
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond '%s', skipping",
fields[BreakpointModel::CONDITION].toUtf8().constData());
return;
}
bp.cond.expression = expr;
bp.cond.expressionString = fields[BreakpointColumns::CONDITION].toStdString();
}
// Enabled
bp.enabled = fields[BreakpointColumns::ENABLED].toUInt(&ok);
if (!ok)
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse enable flag '%s', skipping",
fields[BreakpointColumns::ENABLED].toUtf8().constData());
return;
}
// Description
if (!fields[BreakpointColumns::DESCRIPTION].isEmpty())
{
bp.description = fields[BreakpointColumns::DESCRIPTION].toStdString();
}
insertBreakpointRows(0, 1, {bp});
}
else
{
MemCheck mc;
// Mode
if (type >= MEMCHECK_INVALID)
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond type '%s', skipping",
fields[BreakpointColumns::TYPE].toUtf8().constData());
return;
}
mc.memCond = static_cast<MemCheckCondition>(type);
// Address
QString test = fields[BreakpointColumns::OFFSET];
mc.start = fields[BreakpointColumns::OFFSET].toUInt(&ok, 16);
if (!ok)
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping",
fields[BreakpointColumns::OFFSET].toUtf8().constData());
return;
}
// Size
mc.end = fields[BreakpointColumns::SIZE_LABEL].toUInt(&ok) + mc.start;
if (!ok)
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse length '%s', skipping",
fields[BreakpointColumns::SIZE_LABEL].toUtf8().constData());
return;
}
// Condition
if (!fields[BreakpointColumns::CONDITION].isEmpty())
{
PostfixExpression expr;
mc.hasCond = true;
mc.cond.debug = &m_cpu;
if (!m_cpu.initExpression(fields[BreakpointColumns::CONDITION].toUtf8().constData(), expr, error))
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond '%s', skipping",
fields[BreakpointColumns::CONDITION].toUtf8().constData());
return;
}
mc.cond.expression = expr;
mc.cond.expressionString = fields[BreakpointColumns::CONDITION].toStdString();
}
// Result
const int result = fields[BreakpointColumns::ENABLED].toUInt(&ok);
if (!ok)
{
Console.WriteLn("Debugger Breakpoint Model: Failed to parse result flag '%s', skipping",
fields[BreakpointColumns::ENABLED].toUtf8().constData());
return;
}
mc.result = static_cast<MemCheckResult>(result);
// Description
if (!fields[BreakpointColumns::DESCRIPTION].isEmpty())
{
mc.description = fields[BreakpointColumns::DESCRIPTION].toStdString();
}
insertBreakpointRows(0, 1, {mc});
}
}
void BreakpointModel::clear()
{
beginResetModel();
m_breakpoints.clear();
endResetModel();
}

View File

@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtCore/QAbstractTableModel>
#include <QtWidgets/QHeaderView>
#include "DebugTools/DebugInterface.h"
#include "DebugTools/Breakpoints.h"
using BreakpointMemcheck = std::variant<BreakPoint, MemCheck>;
class BreakpointModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum BreakpointColumns : int
{
ENABLED = 0,
TYPE,
OFFSET,
DESCRIPTION,
SIZE_LABEL,
OPCODE,
CONDITION,
HITS,
COLUMN_COUNT
};
enum BreakpointRoles : int
{
DataRole = Qt::UserRole,
ExportRole = Qt::UserRole + 1,
};
static constexpr QHeaderView::ResizeMode HeaderResizeModes[BreakpointColumns::COLUMN_COUNT] = {
QHeaderView::ResizeMode::ResizeToContents,
QHeaderView::ResizeMode::ResizeToContents,
QHeaderView::ResizeMode::ResizeToContents,
QHeaderView::ResizeMode::ResizeToContents,
QHeaderView::ResizeMode::Stretch,
QHeaderView::ResizeMode::Stretch,
QHeaderView::ResizeMode::ResizeToContents,
QHeaderView::ResizeMode::ResizeToContents,
};
static BreakpointModel* getInstance(DebugInterface& cpu);
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
bool removeRows(int row, int count, const QModelIndex& index = QModelIndex()) override;
bool insertBreakpointRows(int row, int count, std::vector<BreakpointMemcheck> breakpoints, const QModelIndex& index = QModelIndex());
void loadBreakpointFromFieldList(QStringList breakpointFields);
BreakpointMemcheck at(int row) const { return m_breakpoints.at(row); };
void refreshData();
void clear();
private:
BreakpointModel(DebugInterface& cpu, QObject* parent = nullptr);
DebugInterface& m_cpu;
std::vector<BreakpointMemcheck> m_breakpoints;
static std::map<BreakPointCpu, BreakpointModel*> s_instances;
};

View File

@@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "BreakpointView.h"
#include "QtUtils.h"
#include "Debugger/DebuggerSettingsManager.h"
#include "BreakpointDialog.h"
#include "BreakpointModel.h"
#include <QtGui/QClipboard>
BreakpointView::BreakpointView(const DebuggerViewParameters& parameters)
: DebuggerView(parameters, DISALLOW_MULTIPLE_INSTANCES)
, m_model(BreakpointModel::getInstance(cpu()))
{
m_ui.setupUi(this);
m_ui.breakpointList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_ui.breakpointList, &QTableView::customContextMenuRequested, this, &BreakpointView::openContextMenu);
connect(m_ui.breakpointList, &QTableView::doubleClicked, this, &BreakpointView::onDoubleClicked);
m_ui.breakpointList->setModel(m_model);
this->resizeColumns();
}
void BreakpointView::onDoubleClicked(const QModelIndex& index)
{
if (index.isValid() && index.column() == BreakpointModel::OFFSET)
goToInDisassembler(m_model->data(index, BreakpointModel::DataRole).toUInt(), true);
}
void BreakpointView::openContextMenu(QPoint pos)
{
QMenu* menu = new QMenu(m_ui.breakpointList);
menu->setAttribute(Qt::WA_DeleteOnClose);
if (cpu().isAlive())
{
QAction* newAction = menu->addAction(tr("New"));
connect(newAction, &QAction::triggered, this, &BreakpointView::contextNew);
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
if (selModel->hasSelection())
{
QAction* editAction = menu->addAction(tr("Edit"));
connect(editAction, &QAction::triggered, this, &BreakpointView::contextEdit);
if (selModel->selectedIndexes().count() == 1)
{
QAction* copyAction = menu->addAction(tr("Copy"));
connect(copyAction, &QAction::triggered, this, &BreakpointView::contextCopy);
}
QAction* deleteAction = menu->addAction(tr("Delete"));
connect(deleteAction, &QAction::triggered, this, &BreakpointView::contextDelete);
}
}
menu->addSeparator();
if (m_model->rowCount() > 0)
{
QAction* actionExport = menu->addAction(tr("Copy all as CSV"));
connect(actionExport, &QAction::triggered, [this]() {
// It's important to use the Export Role here to allow pasting to be translation agnostic
QGuiApplication::clipboard()->setText(
QtUtils::AbstractItemModelToCSV(m_model, BreakpointModel::ExportRole, true));
});
}
if (cpu().isAlive())
{
QAction* actionImport = menu->addAction(tr("Paste from CSV"));
connect(actionImport, &QAction::triggered, this, &BreakpointView::contextPasteCSV);
if (cpu().getCpuType() == BREAKPOINT_EE)
{
QAction* actionLoad = menu->addAction(tr("Load from Settings"));
connect(actionLoad, &QAction::triggered, [this]() {
m_model->clear();
DebuggerSettingsManager::loadGameSettings(m_model);
});
QAction* actionSave = menu->addAction(tr("Save to Settings"));
connect(actionSave, &QAction::triggered, this, &BreakpointView::saveBreakpointsToDebuggerSettings);
}
}
menu->popup(m_ui.breakpointList->viewport()->mapToGlobal(pos));
}
void BreakpointView::contextCopy()
{
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
if (!selModel->hasSelection())
return;
QGuiApplication::clipboard()->setText(m_model->data(selModel->currentIndex()).toString());
}
void BreakpointView::contextDelete()
{
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
if (!selModel->hasSelection())
return;
QModelIndexList indices = selModel->selectedIndexes();
std::set<int> rows;
for (QModelIndex index : indices)
rows.emplace(index.row());
for (auto row = rows.rbegin(); row != rows.rend(); row++)
m_model->removeRows(*row, 1);
}
void BreakpointView::contextNew()
{
BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), *m_model);
connect(bpDialog, &QDialog::accepted, this, &BreakpointView::resizeColumns);
bpDialog->setAttribute(Qt::WA_DeleteOnClose);
bpDialog->show();
}
void BreakpointView::contextEdit()
{
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
if (!selModel->hasSelection())
return;
const int selectedRow = selModel->selectedIndexes().first().row();
auto bpObject = m_model->at(selectedRow);
BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), *m_model, bpObject, selectedRow);
connect(bpDialog, &QDialog::accepted, this, &BreakpointView::resizeColumns);
bpDialog->setAttribute(Qt::WA_DeleteOnClose);
bpDialog->show();
}
void BreakpointView::contextPasteCSV()
{
QString csv = QGuiApplication::clipboard()->text();
// Skip header
csv = csv.mid(csv.indexOf('\n') + 1);
for (const QString& line : csv.split('\n'))
{
QStringList fields;
// In order to handle text with commas in them we must wrap values in quotes to mark
// where a value starts and end so that text commas aren't identified as delimiters.
// So matches each quote pair, parse it out, and removes the quotes to get the value.
QRegularExpression eachQuotePair(R"("([^"]|\\.)*")");
QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line);
while (it.hasNext())
{
QRegularExpressionMatch match = it.next();
QString matchedValue = match.captured(0);
fields << matchedValue.mid(1, matchedValue.length() - 2);
}
m_model->loadBreakpointFromFieldList(fields);
}
}
void BreakpointView::saveBreakpointsToDebuggerSettings()
{
DebuggerSettingsManager::saveGameSettings(m_model);
}
void BreakpointView::resizeColumns()
{
for (std::size_t i = 0; auto mode : BreakpointModel::HeaderResizeModes)
{
m_ui.breakpointList->horizontalHeader()->setSectionResizeMode(i, mode);
i++;
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_BreakpointView.h"
#include "BreakpointModel.h"
#include "Debugger/DebuggerView.h"
#include "DebugTools/DebugInterface.h"
#include "DebugTools/DisassemblyManager.h"
#include <QtWidgets/QMenu>
#include <QtWidgets/QTabBar>
#include <QtGui/QPainter>
class BreakpointView : public DebuggerView
{
Q_OBJECT
public:
BreakpointView(const DebuggerViewParameters& parameters);
void onDoubleClicked(const QModelIndex& index);
void openContextMenu(QPoint pos);
void contextCopy();
void contextDelete();
void contextNew();
void contextEdit();
void contextPasteCSV();
void resizeColumns();
void saveBreakpointsToDebuggerSettings();
private:
Ui::BreakpointView m_ui;
BreakpointModel* m_model;
};

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BreakpointView</class>
<widget class="QWidget" name="BreakpointView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="breakpointList"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>