// 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 #include std::map 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(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(&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(&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(&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(&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(&bp_mc)) { switch (index.column()) { case BreakpointColumns::ENABLED: return static_cast(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(&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(&bp_mc)) { switch (index.column()) { case BreakpointColumns::ENABLED: return static_cast(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(&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(&bp_mc)) { return bp->enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked; } else if (const auto* mc = std::get_if(&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(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(&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(&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(&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(&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(&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(&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(row); const size_t end_index = static_cast(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(&bp_mc)) { Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), addr = bp->addr] { CBreakPoints::RemoveBreakPoint(cpu, addr); }); } else if (const auto* mc = std::get_if(&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 breakpoints, const QModelIndex& index) { if (breakpoints.size() != static_cast(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(&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(&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 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(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(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(); }