76 changes: 76 additions & 0 deletions Source/Core/Common/Debug/CodeTrace.h
@@ -0,0 +1,76 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <optional>
#include <set>
#include <string>
#include <vector>

#include "Common/CommonTypes.h"

struct InstructionAttributes
{
u32 address = 0;
std::string instruction = "";
std::string reg0 = "";
std::string reg1 = "";
std::string reg2 = "";
std::string reg3 = "";
std::optional<u32> memory_target = std::nullopt;
u32 memory_target_size = 4;
bool is_store = false;
bool is_load = false;
};

struct TraceOutput
{
u32 address;
std::optional<u32> memory_target = std::nullopt;
std::string instruction;
};

struct AutoStepResults
{
std::vector<std::string> reg_tracked;
std::set<u32> mem_tracked;
u32 count = 0;
bool timed_out = false;
bool trackers_empty = false;
};

enum class HitType : u32
{
SKIP = (1 << 0), // Not a hit
OVERWRITE = (1 << 1), // Tracked value gets overwritten by untracked. Typically skipped.
MOVED = (1 << 2), // Target duplicated to another register, unchanged.
SAVELOAD = (1 << 3), // Target saved or loaded. Priority over Pointer.
POINTER = (1 << 4), // Target used as pointer/offset for save or load
PASSIVE = (1 << 5), // Conditional, etc, but not pointer. Unchanged
ACTIVE = (1 << 6), // Math, etc. Changed.
UPDATED = (1 << 7), // Masked or math without changing register.
};

class CodeTrace
{
public:
enum class AutoStop
{
Always,
Used,
Changed
};

void SetRegTracked(const std::string& reg);
AutoStepResults AutoStepping(bool continue_previous = false, AutoStop stop_on = AutoStop::Always);

private:
InstructionAttributes GetInstructionAttributes(const TraceOutput& line) const;
TraceOutput SaveCurrentInstruction() const;
HitType TraceLogic(const TraceOutput& current_instr, bool first_hit = false);

bool m_recording = false;
std::vector<std::string> m_reg_autotrack;
std::set<u32> m_mem_autotrack;
};
2 changes: 2 additions & 0 deletions Source/Core/DolphinLib.props
Expand Up @@ -37,6 +37,7 @@
<ClInclude Include="Common\Crypto\bn.h" />
<ClInclude Include="Common\Crypto\ec.h" />
<ClInclude Include="Common\Crypto\SHA1.h" />
<ClInclude Include="Common\Debug\CodeTrace.h" />
<ClInclude Include="Common\Debug\MemoryPatches.h" />
<ClInclude Include="Common\Debug\Threads.h" />
<ClInclude Include="Common\Debug\Watches.h" />
Expand Down Expand Up @@ -737,6 +738,7 @@
-->
<AdditionalOptions Condition="'$(Platform)'=='ARM64'">/d2ssa-peeps-post-color- %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<ClCompile Include="Common\Debug\CodeTrace.cpp" />
<ClCompile Include="Common\Debug\MemoryPatches.cpp" />
<ClCompile Include="Common\Debug\Watches.cpp" />
<ClCompile Include="Common\DynamicLibrary.cpp" />
Expand Down
101 changes: 101 additions & 0 deletions Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp
Expand Up @@ -14,6 +14,7 @@
#include <QInputDialog>
#include <QKeyEvent>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QResizeEvent>
Expand All @@ -23,6 +24,7 @@
#include <QWheelEvent>

#include "Common/Assert.h"
#include "Common/Debug/CodeTrace.h"
#include "Common/GekkoDisassembler.h"
#include "Common/StringUtil.h"
#include "Core/Core.h"
Expand Down Expand Up @@ -565,6 +567,26 @@ void CodeViewWidget::OnContextMenu()
auto* restore_action =
menu->addAction(tr("Restore instruction"), this, &CodeViewWidget::OnRestoreInstruction);

QString target;
if (addr == PC && running && Core::GetState() == Core::State::Paused)
{
const std::string line = PowerPC::debug_interface.Disassemble(PC);
const auto target_it = std::find(line.begin(), line.end(), '\t');
const auto target_end = std::find(target_it, line.end(), ',');

if (target_it != line.end() && target_end != line.end())
target = QString::fromStdString(std::string{target_it + 1, target_end});
}

auto* run_until_menu = menu->addMenu(tr("Run until (ignoring breakpoints)"));
run_until_menu->addAction(tr("%1's value is hit").arg(target), this,
[this] { AutoStep(CodeTrace::AutoStop::Always); });
run_until_menu->addAction(tr("%1's value is used").arg(target), this,
[this] { AutoStep(CodeTrace::AutoStop::Used); });
run_until_menu->addAction(tr("%1's value is changed").arg(target),
[this] { AutoStep(CodeTrace::AutoStop::Changed); });

run_until_menu->setEnabled(!target.isEmpty());
follow_branch_action->setEnabled(running && GetBranchFromAddress(addr));

for (auto* action : {copy_address_action, copy_line_action, copy_hex_action, function_action,
Expand All @@ -588,6 +610,85 @@ void CodeViewWidget::OnContextMenu()
Update();
}

void CodeViewWidget::AutoStep(CodeTrace::AutoStop option)
{
// Autosteps and follows value in the target (left-most) register. The Used and Changed options
// silently follows target through reshuffles in memory and registers and stops on use or update.

CodeTrace code_trace;
bool repeat = false;

QMessageBox msgbox(QMessageBox::NoIcon, tr("Run until"), {}, QMessageBox::Cancel);
QPushButton* run_button = msgbox.addButton(tr("Keep Running"), QMessageBox::AcceptRole);
// Not sure if we want default to be cancel. Spacebar can let you quickly continue autostepping if
// Yes.

do
{
// Run autostep then update codeview
const AutoStepResults results = code_trace.AutoStepping(repeat, option);
emit Host::GetInstance()->UpdateDisasmDialog();
repeat = true;

// Invalid instruction, 0 means no step executed.
if (results.count == 0)
return;

// Status report
if (results.reg_tracked.empty() && results.mem_tracked.empty())
{
QMessageBox::warning(
this, tr("Overwritten"),
tr("Target value was overwritten by current instruction.\nInstructions executed: %1")
.arg(QString::number(results.count)),
QMessageBox::Cancel);
return;
}
else if (results.timed_out)
{
// Can keep running and try again after a time out.
msgbox.setText(
tr("<font color='#ff0000'>AutoStepping timed out. Current instruction is irrelevant."));
}
else
{
msgbox.setText(tr("Value tracked to current instruction."));
}

// Mem_tracked needs to track each byte individually, so a tracked word-sized value would have
// four entries. The displayed memory list needs to be shortened so it's not a huge list of
// bytes. Assumes adjacent bytes represent a word or half-word and removes the redundant bytes.
std::set<u32> mem_out;
auto iter = results.mem_tracked.begin();

while (iter != results.mem_tracked.end())
{
const u32 address = *iter;
mem_out.insert(address);

for (u32 i = 1; i <= 3; i++)
{
if (results.mem_tracked.count(address + i))
iter++;
else
break;
}

iter++;
}

const QString msgtext =
tr("Instructions executed: %1\nValue contained in:\nRegisters: %2\nMemory: %3")
.arg(QString::number(results.count))
.arg(QString::fromStdString(fmt::format("{}", fmt::join(results.reg_tracked, ", "))))
.arg(QString::fromStdString(fmt::format("{:#x}", fmt::join(mem_out, ", "))));

msgbox.setInformativeText(msgtext);
msgbox.exec();

} while (msgbox.clickedButton() == (QAbstractButton*)run_button);
}

void CodeViewWidget::OnCopyAddress()
{
const u32 addr = GetContextAddress();
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinQt/Debugger/CodeViewWidget.h
Expand Up @@ -8,6 +8,7 @@
#include <QTableWidget>

#include "Common/CommonTypes.h"
#include "Common/Debug/CodeTrace.h"

class QKeyEvent;
class QMouseEvent;
Expand Down Expand Up @@ -68,6 +69,7 @@ class CodeViewWidget : public QTableWidget

void OnContextMenu();

void AutoStep(CodeTrace::AutoStop option = CodeTrace::AutoStop::Always);
void OnFollowBranch();
void OnCopyAddress();
void OnCopyTargetAddress();
Expand Down
38 changes: 38 additions & 0 deletions Source/Core/DolphinQt/Debugger/RegisterWidget.cpp
Expand Up @@ -8,9 +8,11 @@
#include <QActionGroup>
#include <QHeaderView>
#include <QMenu>
#include <QMessageBox>
#include <QTableWidget>
#include <QVBoxLayout>

#include "Common/Debug/CodeTrace.h"
#include "Core/Core.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/PowerPC/PowerPC.h"
Expand Down Expand Up @@ -164,6 +166,16 @@ void RegisterWidget::ShowContextMenu()
auto* view_double_column = menu->addAction(tr("All Double"));
view_double_column->setData(static_cast<int>(RegisterDisplay::Double));

if (type == RegisterType::gpr || type == RegisterType::fpr)
{
menu->addSeparator();

const std::string type_string =
fmt::format("{}{}", type == RegisterType::gpr ? "r" : "f", m_table->currentItem()->row());
menu->addAction(tr("Run until hit (ignoring breakpoints)"),
[this, type_string]() { AutoStep(type_string); });
}

for (auto* action : {view_hex, view_int, view_uint, view_float, view_double})
{
action->setCheckable(true);
Expand Down Expand Up @@ -269,6 +281,32 @@ void RegisterWidget::ShowContextMenu()
menu->exec(QCursor::pos());
}

void RegisterWidget::AutoStep(const std::string& reg) const
{
CodeTrace trace;
trace.SetRegTracked(reg);

QMessageBox msgbox(
QMessageBox::NoIcon, tr("Timed Out"),
tr("<font color='#ff0000'>AutoStepping timed out. Current instruction is irrelevant."),
QMessageBox::Cancel);
QPushButton* run_button = msgbox.addButton(tr("Keep Running"), QMessageBox::AcceptRole);

while (true)
{
const AutoStepResults results = trace.AutoStepping(true);
emit Host::GetInstance()->UpdateDisasmDialog();

if (!results.timed_out)
break;

// Can keep running and try again after a time out.
msgbox.exec();
if (msgbox.clickedButton() != (QAbstractButton*)run_button)
break;
}
}

void RegisterWidget::PopulateTable()
{
for (int i = 0; i < 32; i++)
Expand Down
1 change: 1 addition & 0 deletions Source/Core/DolphinQt/Debugger/RegisterWidget.h
Expand Up @@ -46,6 +46,7 @@ class RegisterWidget : public QDockWidget
void AddRegister(int row, int column, RegisterType type, std::string register_name,
std::function<u64()> get_reg, std::function<void(u64)> set_reg);

void AutoStep(const std::string& reg) const;
void Update();

QTableWidget* m_table;
Expand Down