dolphin/Source/Core/DolphinQt/Debugger/BreakpointDialog.cpp
Martino Fontana bd3cf67cbc Debugger: Rework temporary breakpoints
Before:
1. In theory there could be multiple, but in practice they were (manually) cleared before creating one
2. (Some of) the conditions to clear one were either to reach it, to create a new one (due to the point above), or to step. This created weird behavior: let's say you Step Over a `bl` (thus creating a temporary breakpoint on `pc+4`), and you reached a regular breakpoint inside the `bl`. The temporary one would still be there: if you resumed, the emulation would still stop there, as a sort of Step Out. But, if before resuming, you made a Step, then it wouldn't do that.
3. The breakpoint widget had no idea concept of them, and will treat them as regular breakpoints. Also, they'll be shown only when the widget is updated in some other way, leading to more confusion.
4. Because only one breakpoint could exist per address, the creation of a temporary breakpoint on a top of a regular one would delete it and inherit its properties (e.g. being log-only). This could happen, for instance, if you Stepped Over a `bl` specifically, and pc+4 had a regular breakpoint.

Now there can only be one temporary breakpoint, which is automatically cleared whenever emulation is paused. So, removing some manual clearing from 1., and removing the weird behavior of 2. As it is stored in a separate variable, it won't be seen at all depending on the function used (fixing 3., and removing some checks in other places), and it won't replace a regular breakpoint, instead simply having priority (fixing 4.).
2024-07-05 21:33:22 +02:00

375 lines
14 KiB
C++

// Copyright 2017 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/BreakpointDialog.h"
#include <QButtonGroup>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QRadioButton>
#include <QVBoxLayout>
#include "Core/PowerPC/BreakPoints.h"
#include "Core/PowerPC/Expression.h"
#include "Core/PowerPC/PowerPC.h"
#include "DolphinQt/Debugger/BreakpointWidget.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
BreakpointDialog::BreakpointDialog(BreakpointWidget* parent)
: QDialog(parent), m_parent(parent), m_open_mode(OpenMode::New)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("New Breakpoint"));
CreateWidgets();
ConnectWidgets();
OnBPTypeChanged();
OnAddressTypeChanged();
}
BreakpointDialog::BreakpointDialog(BreakpointWidget* parent, const TBreakPoint* breakpoint)
: QDialog(parent), m_parent(parent), m_open_mode(OpenMode::EditBreakPoint)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Edit Breakpoint"));
CreateWidgets();
ConnectWidgets();
m_instruction_address->setText(QString::number(breakpoint->address, 16));
if (breakpoint->condition)
m_conditional->setText(QString::fromStdString(breakpoint->condition->GetText()));
m_do_break->setChecked(breakpoint->break_on_hit && !breakpoint->log_on_hit);
m_do_log->setChecked(!breakpoint->break_on_hit && breakpoint->log_on_hit);
m_do_log_and_break->setChecked(breakpoint->break_on_hit && breakpoint->log_on_hit);
OnBPTypeChanged();
OnAddressTypeChanged();
}
BreakpointDialog::BreakpointDialog(BreakpointWidget* parent, const TMemCheck* memcheck)
: QDialog(parent), m_parent(parent), m_open_mode(OpenMode::EditMemCheck)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Edit Breakpoint"));
CreateWidgets();
ConnectWidgets();
m_memory_address_from->setText(QString::number(memcheck->start_address, 16));
if (memcheck->is_ranged)
{
m_memory_use_address->setChecked(false);
m_memory_use_range->setChecked(true);
m_memory_address_to->setText(QString::number(memcheck->end_address, 16));
}
else
{
m_memory_address_to->setText(QString::number(memcheck->start_address + 1, 16));
}
if (memcheck->condition)
m_conditional->setText(QString::fromStdString(memcheck->condition->GetText()));
m_memory_on_read->setChecked(memcheck->is_break_on_read && !memcheck->is_break_on_write);
m_memory_on_write->setChecked(!memcheck->is_break_on_read && memcheck->is_break_on_write);
m_memory_on_read_and_write->setChecked(memcheck->is_break_on_read && memcheck->is_break_on_write);
m_do_break->setChecked(memcheck->break_on_hit && !memcheck->log_on_hit);
m_do_log->setChecked(!memcheck->break_on_hit && memcheck->log_on_hit);
m_do_log_and_break->setChecked(memcheck->break_on_hit && memcheck->log_on_hit);
OnBPTypeChanged();
OnAddressTypeChanged();
}
void BreakpointDialog::CreateWidgets()
{
m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel |
QDialogButtonBox::Help);
auto* type_group = new QButtonGroup(this);
// Instruction BP
auto* instruction_widget = new QWidget;
auto* instruction_layout = new QGridLayout;
m_instruction_bp = new QRadioButton(tr("Instruction Breakpoint"));
type_group->addButton(m_instruction_bp);
m_instruction_box = new QGroupBox;
m_instruction_address = new QLineEdit;
auto* instruction_data_layout = new QHBoxLayout;
m_instruction_box->setLayout(instruction_data_layout);
instruction_data_layout->addWidget(new QLabel(tr("Address:")));
instruction_data_layout->addWidget(m_instruction_address);
instruction_layout->addWidget(m_instruction_bp, 0, 0, 1, 1);
instruction_layout->addWidget(m_instruction_box, 1, 0, 1, 2);
instruction_widget->setLayout(instruction_layout);
// Memory BP
auto* memory_widget = new QWidget;
auto* memory_layout = new QGridLayout;
m_memory_bp = new QRadioButton(tr("Memory Breakpoint"));
type_group->addButton(m_memory_bp);
m_memory_box = new QGroupBox;
auto* memory_type_group = new QButtonGroup(this);
m_memory_use_address = new QRadioButton(tr("Address"));
m_memory_use_address->setChecked(true);
memory_type_group->addButton(m_memory_use_address);
// i18n: A range of memory addresses
m_memory_use_range = new QRadioButton(tr("Range"));
memory_type_group->addButton(m_memory_use_range);
m_memory_address_from = new QLineEdit;
m_memory_address_to = new QLineEdit;
m_memory_address_from_label = new QLabel; // Set by OnAddressTypeChanged
m_memory_address_to_label = new QLabel(tr("To:"));
// i18n: This is a selectable condition when adding a breakpoint
m_memory_on_read = new QRadioButton(tr("Read"));
// i18n: This is a selectable condition when adding a breakpoint
m_memory_on_write = new QRadioButton(tr("Write"));
// i18n: This is a selectable condition when adding a breakpoint
m_memory_on_read_and_write = new QRadioButton(tr("Read or Write"));
m_memory_on_write->setChecked(true);
// i18n: This is a selectable action when adding a breakpoint
m_do_log = new QRadioButton(tr("Write to Log"));
// i18n: This is a selectable action when adding a breakpoint
m_do_break = new QRadioButton(tr("Break"));
// i18n: This is a selectable action when adding a breakpoint
m_do_log_and_break = new QRadioButton(tr("Write to Log and Break"));
m_do_log_and_break->setChecked(true);
auto* memory_data_layout = new QGridLayout;
m_memory_box->setLayout(memory_data_layout);
memory_data_layout->addWidget(m_memory_use_address, 0, 0);
memory_data_layout->addWidget(m_memory_use_range, 0, 3);
memory_data_layout->addWidget(m_memory_address_from_label, 1, 0);
memory_data_layout->addWidget(m_memory_address_from, 1, 1);
memory_data_layout->addWidget(m_memory_address_to_label, 1, 2);
memory_data_layout->addWidget(m_memory_address_to, 1, 3);
// i18n: If a condition is set for a breakpoint, the condition becoming true is a prerequisite for
// triggering the breakpoint.
QGroupBox* condition_box = new QGroupBox(tr("Condition"));
auto* condition_layout = new QHBoxLayout;
condition_box->setLayout(condition_layout);
memory_data_layout->addWidget(condition_box, 2, 0, 1, -1);
condition_layout->addWidget(m_memory_on_read);
condition_layout->addWidget(m_memory_on_write);
condition_layout->addWidget(m_memory_on_read_and_write);
memory_layout->addWidget(m_memory_bp, 0, 0);
memory_layout->addWidget(m_memory_box, 1, 0);
memory_widget->setLayout(memory_layout);
QGroupBox* action_box = new QGroupBox(tr("Action"));
QHBoxLayout* conditional_layout = new QHBoxLayout;
m_conditional = new QLineEdit();
// i18n: If a condition is set for a breakpoint, the condition becoming true is a prerequisite for
// triggering the breakpoint.
conditional_layout->addWidget(new QLabel(tr("Condition:")));
conditional_layout->addWidget(m_conditional);
auto* action_layout = new QHBoxLayout;
action_layout->addWidget(m_do_log);
action_layout->addWidget(m_do_break);
action_layout->addWidget(m_do_log_and_break);
auto* action_vlayout = new QVBoxLayout;
action_vlayout->addLayout(conditional_layout);
action_vlayout->addLayout(action_layout);
action_box->setLayout(action_vlayout);
auto* layout = new QVBoxLayout;
layout->addWidget(instruction_widget);
layout->addWidget(memory_widget);
layout->addWidget(action_box);
layout->addWidget(m_buttons);
switch (m_open_mode)
{
case OpenMode::New:
m_instruction_bp->setChecked(true);
m_instruction_address->setFocus();
break;
case OpenMode::EditBreakPoint:
memory_widget->setVisible(false);
m_instruction_bp->setChecked(true);
m_instruction_address->setEnabled(false);
m_instruction_address->setFocus();
break;
case OpenMode::EditMemCheck:
instruction_widget->setVisible(false);
m_memory_bp->setChecked(true);
m_memory_address_from->setEnabled(false);
m_memory_address_to->setFocus();
break;
}
setLayout(layout);
}
void BreakpointDialog::ConnectWidgets()
{
connect(m_buttons, &QDialogButtonBox::accepted, this, &BreakpointDialog::accept);
connect(m_buttons, &QDialogButtonBox::rejected, this, &BreakpointDialog::reject);
connect(m_buttons, &QDialogButtonBox::helpRequested, this, &BreakpointDialog::ShowConditionHelp);
connect(m_instruction_bp, &QRadioButton::toggled, this, &BreakpointDialog::OnBPTypeChanged);
connect(m_memory_bp, &QRadioButton::toggled, this, &BreakpointDialog::OnBPTypeChanged);
connect(m_memory_use_address, &QRadioButton::toggled, this,
&BreakpointDialog::OnAddressTypeChanged);
connect(m_memory_use_range, &QRadioButton::toggled, this,
&BreakpointDialog::OnAddressTypeChanged);
}
void BreakpointDialog::OnBPTypeChanged()
{
m_instruction_box->setEnabled(m_instruction_bp->isChecked());
m_memory_box->setEnabled(m_memory_bp->isChecked());
}
void BreakpointDialog::OnAddressTypeChanged()
{
bool ranged = m_memory_use_range->isChecked();
m_memory_address_to->setHidden(!ranged);
m_memory_address_to_label->setHidden(!ranged);
m_memory_address_from_label->setText(ranged ? tr("From:") : tr("Address:"));
}
void BreakpointDialog::accept()
{
auto invalid_input = [this](const QString& field) {
ModalMessageBox::critical(this, tr("Error"),
tr("Invalid input for the field \"%1\"").arg(field));
};
bool instruction = m_instruction_bp->isChecked();
bool ranged = m_memory_use_range->isChecked();
// Triggers
bool on_read = m_memory_on_read->isChecked() || m_memory_on_read_and_write->isChecked();
bool on_write = m_memory_on_write->isChecked() || m_memory_on_read_and_write->isChecked();
// Actions
bool do_log = m_do_log->isChecked() || m_do_log_and_break->isChecked();
bool do_break = m_do_break->isChecked() || m_do_log_and_break->isChecked();
bool good;
const QString condition = m_conditional->text().trimmed();
if (!condition.isEmpty() && !Expression::TryParse(condition.toUtf8().constData()))
{
// i18n: If a condition is set for a breakpoint, the condition becoming true is a prerequisite
// for triggering the breakpoint.
invalid_input(tr("Condition"));
return;
}
if (instruction)
{
u32 address = m_instruction_address->text().toUInt(&good, 16);
if (!good)
{
invalid_input(tr("Address"));
return;
}
m_parent->AddBP(address, do_break, do_log, condition);
}
else
{
u32 from = m_memory_address_from->text().toUInt(&good, 16);
if (!good)
{
invalid_input(ranged ? tr("From") : tr("Address"));
return;
}
if (ranged)
{
u32 to = m_memory_address_to->text().toUInt(&good, 16);
if (!good)
{
invalid_input(tr("To"));
return;
}
m_parent->AddRangedMBP(from, to, on_read, on_write, do_log, do_break, condition);
}
else
{
m_parent->AddAddressMBP(from, on_read, on_write, do_log, do_break, condition);
}
}
QDialog::accept();
}
void BreakpointDialog::ShowConditionHelp()
{
const auto message = tr(
"Conditions:\n"
"Sets an expression that is evaluated when a breakpoint is hit. If the expression is false "
"or 0, the breakpoint is ignored until hit again. Statements should be separated by a comma. "
"Only the last statement will be used to determine what to do.\n"
"\n"
"Registers that can be referenced:\n"
"GPRs : r0..r31\n"
"FPRs : f0..f31\n"
"SPRs : xer, lr, ctr, dsisr, dar, dec, sdr1, srr0, srr1, tbl, tbu, pvr, sprg0..sprg3, ear, "
"ibat0u..ibat7u, ibat0l..ibat7l, dbat0u..dbat7u, dbat0l..dbat07, gqr0..gqr7, hid0, hid1, "
"hid2, hid4, iabr, dabr, wpar, dmau, dmal, ecid_u, ecid_m, ecid_l, upmc1..upmc4, usia, sia, "
"l2cr, ictc, mmcr0, mmcr1, pmc1..pmc4, thrm1..thrm3\n"
"Other : pc, msr\n"
"\n"
"Functions:\n"
"Set a register: r1 = 8\n"
"Casts: s8(0xff). Available: s8, u8, s16, u16, s32, u32\n"
"Callstack: callstack(0x80123456), callstack(\"anim\")\n"
"Compare Strings: streq(r3, \"abc\"). Both parameters can be addresses or string constants.\n"
"Read Memory: read_u32(0x80000000). Available: u8, s8, u16, s16, u32, s32, f32, f64\n"
"Write Memory: write_u32(r3, 0x80000000). Available: u8, u16, u32, f32, f64\n"
"*currently writing will always be triggered\n"
"\n"
"Operations:\n"
"Unary: -u, !u, ~u\n"
"Math: * / + -, power: **, remainder: %, shift: <<, >>\n"
"Compare: <, <=, >, >=, ==, !=, &&, ||\n"
"Bitwise: &, |, ^\n"
"\n"
"Examples:\n"
"r4 == 1\n"
"f0 == 1.0 && f2 < 10.0\n"
"r26 <= r0 && ((r5 + 3) & -4) * ((r6 + 3) & -4)* 4 > r0\n"
"p = r3 + 0x8, p == 0x8003510 && read_u32(p) != 0\n"
"Write and break: r4 = 8, 1\n"
"Write and continue: f3 = f1 + f2, 0\n"
"The condition must always be last\n\n"
"Strings should only be used in callstack() or streq() and \"quoted\". Do not assign strings "
"to a variable.\n"
"All variables will be printed in the Memory Interface log, if there's a hit or a NaN "
"result. To check for issues, assign a variable to your equation, so it can be printed.\n\n"
"Note: All values are internally converted to Doubles for calculations. It's possible for "
"them to go out of range or to become NaN. A warning will be given if NaN is returned, and "
"the var that became NaN will be logged.");
// i18n: The title for a dialog that shows help for how to use conditions. If a condition is set
// for a breakpoint, the condition becoming true is a prerequisite for triggering the breakpoint.
ModalMessageBox::information(this, tr("Conditional help"), message);
}