Merge pull request #11497 from vyuuui/debugger_assembler_ui

Built-in assembler for debugger interface
This commit is contained in:
Tilka
2023-12-16 21:15:31 +00:00
committed by GitHub
120 changed files with 9774 additions and 11 deletions

View File

@ -0,0 +1,131 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/AssembleInstructionDialog.h"
#include <QDialogButtonBox>
#include <QFontDatabase>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include "Common/Assembler/GekkoAssembler.h"
#include "Common/StringUtil.h"
namespace
{
QString HtmlFormatErrorLoc(const Common::GekkoAssembler::AssemblerError& err)
{
const QString error = QStringLiteral("<span style=\"color: red; font-weight: bold\">%1</span>")
.arg(QObject::tr("Error"));
// i18n: '%1' is the translation of 'Error'
return QObject::tr("%1 in column %2").arg(error).arg(err.col + 1);
}
QString HtmlFormatErrorLine(const Common::GekkoAssembler::AssemblerError& err)
{
const QString line_pre_error =
QString::fromStdString(std::string(err.error_line.substr(0, err.col))).toHtmlEscaped();
const QString line_error =
QString::fromStdString(std::string(err.error_line.substr(err.col, err.len))).toHtmlEscaped();
const QString line_post_error =
QString::fromStdString(std::string(err.error_line.substr(err.col + err.len))).toHtmlEscaped();
return QStringLiteral("%1<u><span style=\"color:red; font-weight:bold\">%2</span></u>%3")
.arg(line_pre_error)
.arg(line_error)
.arg(line_post_error);
}
} // namespace
AssembleInstructionDialog::AssembleInstructionDialog(QWidget* parent, u32 address, u32 value)
: QDialog(parent), m_code(value), m_address(address)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowModality(Qt::WindowModal);
setWindowTitle(tr("Instruction"));
CreateWidgets();
ConnectWidgets();
}
void AssembleInstructionDialog::CreateWidgets()
{
auto* layout = new QVBoxLayout;
m_input_edit = new QLineEdit;
m_error_loc_label = new QLabel;
m_error_line_label = new QLabel;
m_msg_label = new QLabel(tr("No input"));
m_button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
m_error_line_label->setFont(QFont(QFontDatabase::systemFont(QFontDatabase::FixedFont).family()));
m_input_edit->setFont(QFont(QFontDatabase::systemFont(QFontDatabase::FixedFont).family()));
layout->addWidget(m_error_loc_label);
layout->addWidget(m_input_edit);
layout->addWidget(m_error_line_label);
layout->addWidget(m_msg_label);
layout->addWidget(m_button_box);
m_input_edit->setText(QStringLiteral(".4byte 0x%1").arg(m_code, 8, 16, QLatin1Char('0')));
setLayout(layout);
OnEditChanged();
}
void AssembleInstructionDialog::ConnectWidgets()
{
connect(m_button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_input_edit, &QLineEdit::textChanged, this, &AssembleInstructionDialog::OnEditChanged);
}
void AssembleInstructionDialog::OnEditChanged()
{
using namespace Common::GekkoAssembler;
std::string line = m_input_edit->text().toStdString();
Common::ToLower(&line);
FailureOr<std::vector<CodeBlock>> asm_result = Assemble(line, m_address);
if (IsFailure(asm_result))
{
m_button_box->button(QDialogButtonBox::Ok)->setEnabled(false);
const AssemblerError& failure = GetFailure(asm_result);
m_error_loc_label->setText(HtmlFormatErrorLoc(failure));
m_error_line_label->setText(HtmlFormatErrorLine(failure));
m_msg_label->setText(QString::fromStdString(failure.message).toHtmlEscaped());
}
else if (GetT(asm_result).empty() || GetT(asm_result)[0].instructions.empty())
{
m_button_box->button(QDialogButtonBox::Ok)->setEnabled(false);
m_error_loc_label->setText(
QStringLiteral("<span style=\"color: red; font-weight: bold\">%1</span>").arg(tr("Error")));
m_error_line_label->clear();
m_msg_label->setText(tr("No input"));
}
else
{
m_button_box->button(QDialogButtonBox::Ok)->setEnabled(true);
m_code = 0;
const std::vector<u8>& block_bytes = GetT(asm_result)[0].instructions;
for (size_t i = 0; i < 4 && i < block_bytes.size(); i++)
{
m_code = (m_code << 8) | block_bytes[i];
}
m_error_loc_label->setText(
QStringLiteral("<span style=\"color: green; font-weight: bold\">%1</span>").arg(tr("OK")));
m_error_line_label->clear();
m_msg_label->setText(tr("Instruction: %1").arg(m_code, 8, 16, QLatin1Char('0')));
}
}
u32 AssembleInstructionDialog::GetCode() const
{
return m_code;
}

View File

@ -0,0 +1,36 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include "Common/CommonTypes.h"
class QDialogButtonBox;
class QLabel;
class QLineEdit;
class AssembleInstructionDialog : public QDialog
{
Q_OBJECT
public:
explicit AssembleInstructionDialog(QWidget* parent, u32 address, u32 value);
u32 GetCode() const;
private:
void CreateWidgets();
void ConnectWidgets();
void OnEditChanged();
u32 m_code;
u32 m_address;
QLineEdit* m_input_edit;
QLabel* m_error_loc_label;
QLabel* m_error_line_label;
QLabel* m_msg_label;
QDialogButtonBox* m_button_box;
};

View File

@ -0,0 +1,960 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/AssemblerWidget.h"
#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QComboBox>
#include <QFont>
#include <QFontDatabase>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QScrollBar>
#include <QShortcut>
#include <QStyle>
#include <QTabWidget>
#include <QTextBlock>
#include <QTextEdit>
#include <QToolBar>
#include <QToolButton>
#include <filesystem>
#include <fmt/format.h>
#include "Common/Assert.h"
#include "Common/FileUtil.h"
#include "Core/Core.h"
#include "Core/PowerPC/MMU.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "DolphinQt/Debugger/AssemblyEditor.h"
#include "DolphinQt/QtUtils/DolphinFileDialog.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"
namespace
{
using namespace Common::GekkoAssembler;
QString HtmlFormatErrorLoc(const AssemblerError& err)
{
const QString error = QStringLiteral("<span style=\"color: red; font-weight: bold\">%1</span>")
.arg(QObject::tr("Error"));
// i18n: '%1' is the translation of 'Error'
return QObject::tr("%1 on line %1 column %2").arg(error).arg(err.line + 1).arg(err.col + 1);
}
QString HtmlFormatErrorLine(const AssemblerError& err)
{
const QString line_pre_error =
QString::fromStdString(std::string(err.error_line.substr(0, err.col))).toHtmlEscaped();
const QString line_error =
QString::fromStdString(std::string(err.error_line.substr(err.col, err.len))).toHtmlEscaped();
const QString line_post_error =
QString::fromStdString(std::string(err.error_line.substr(err.col + err.len))).toHtmlEscaped();
return QStringLiteral("<span style=\"font-family:'monospace';font-size:16px\">"
"<pre>%1<u><span style=\"color:red;font-weight:bold\">%2</span></u>%3</pre>"
"</span>")
.arg(line_pre_error)
.arg(line_error)
.arg(line_post_error);
}
QString HtmlFormatMessage(const AssemblerError& err)
{
return QStringLiteral("<span>%1</span>").arg(QString::fromStdString(err.message).toHtmlEscaped());
}
void DeserializeBlock(const CodeBlock& blk, std::ostringstream& out_str, bool pad4)
{
size_t i = 0;
for (; i < blk.instructions.size(); i++)
{
out_str << fmt::format("{:02x}", blk.instructions[i]);
if (i % 8 == 7)
{
out_str << '\n';
}
else if (i % 4 == 3)
{
out_str << ' ';
}
}
if (pad4)
{
bool did_pad = false;
for (; i % 4 != 0; i++)
{
out_str << "00";
did_pad = true;
}
if (did_pad)
{
out_str << (i % 8 == 0 ? '\n' : ' ');
}
}
else if (i % 8 != 7)
{
out_str << '\n';
}
}
void DeserializeToRaw(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
out_str << fmt::format("# Block {:08x}\n", blk.block_address);
DeserializeBlock(blk, out_str, false);
}
}
void DeserializeToAr(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
size_t i = 0;
for (; i < blk.instructions.size() - 3; i += 4)
{
// type=NormalCode, subtype=SUB_RAM_WRITE, size=32bit
const u32 ar_addr = ((blk.block_address + i) & 0x1ffffff) | 0x04000000;
out_str << fmt::format("{:08x} {:02x}{:02x}{:02x}{:02x}\n", ar_addr, blk.instructions[i],
blk.instructions[i + 1], blk.instructions[i + 2],
blk.instructions[i + 3]);
}
for (; i < blk.instructions.size(); i++)
{
// type=NormalCode, subtype=SUB_RAM_WRITE, size=8bit
const u32 ar_addr = ((blk.block_address + i) & 0x1ffffff);
out_str << fmt::format("{:08x} 000000{:02x}\n", ar_addr, blk.instructions[i]);
}
}
}
void DeserializeToGecko(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
DeserializeToAr(blocks, out_str);
}
void DeserializeToGeckoExec(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
u32 nlines = 1 + static_cast<u32>((blk.instructions.size() - 1) / 8);
bool ret_on_newline = false;
if (blk.instructions.size() % 8 == 0 || blk.instructions.size() % 8 > 4)
{
// Append extra line for blr
nlines++;
ret_on_newline = true;
}
out_str << fmt::format("c0000000 {:08x}\n", nlines);
DeserializeBlock(blk, out_str, true);
if (ret_on_newline)
{
out_str << "4e800020 00000000\n";
}
else
{
out_str << "4e800020\n";
}
}
}
void DeserializeToGeckoTramp(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
const u32 inject_addr = (blk.block_address & 0x1ffffff) | 0x02000000;
u32 nlines = 1 + static_cast<u32>((blk.instructions.size() - 1) / 8);
bool padding_on_newline = false;
if (blk.instructions.size() % 8 == 0 || blk.instructions.size() % 8 > 4)
{
// Append extra line for nop+branchback
nlines++;
padding_on_newline = true;
}
out_str << fmt::format("c{:07x} {:08x}\n", inject_addr, nlines);
DeserializeBlock(blk, out_str, true);
if (padding_on_newline)
{
out_str << "60000000 00000000\n";
}
else
{
out_str << "00000000\n";
}
}
}
} // namespace
AssemblerWidget::AssemblerWidget(QWidget* parent)
: QDockWidget(parent), m_system(Core::System::GetInstance()), m_unnamed_editor_count(0),
m_net_zoom_delta(0)
{
{
QPalette base_palette;
m_dark_scheme = base_palette.color(QPalette::WindowText).value() >
base_palette.color(QPalette::Window).value();
}
setWindowTitle(tr("Assembler"));
setObjectName(QStringLiteral("assemblerwidget"));
setHidden(!Settings::Instance().IsAssemblerVisible() ||
!Settings::Instance().IsDebugModeEnabled());
this->setVisible(true);
CreateWidgets();
restoreGeometry(
Settings::GetQSettings().value(QStringLiteral("assemblerwidget/geometry")).toByteArray());
setFloating(Settings::GetQSettings().value(QStringLiteral("assemblerwidget/floating")).toBool());
connect(&Settings::Instance(), &Settings::AssemblerVisibilityChanged, this,
[this](bool visible) { setHidden(!visible); });
connect(&Settings::Instance(), &Settings::DebugModeToggled, this, [this](bool enabled) {
setHidden(!enabled || !Settings::Instance().IsAssemblerVisible());
});
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
&AssemblerWidget::OnEmulationStateChanged);
connect(&Settings::Instance(), &Settings::ThemeChanged, this, &AssemblerWidget::UpdateIcons);
connect(m_asm_tabs, &QTabWidget::tabCloseRequested, this, &AssemblerWidget::OnTabClose);
auto* save_shortcut = new QShortcut(QKeySequence::Save, this);
// Save should only activate if the active tab is in focus
save_shortcut->connect(save_shortcut, &QShortcut::activated, this, [this] {
if (m_asm_tabs->currentIndex() != -1 && m_asm_tabs->currentWidget()->hasFocus())
{
OnSave();
}
});
auto* zoom_in_shortcut = new QShortcut(QKeySequence::ZoomIn, this);
zoom_in_shortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_in_shortcut, &QShortcut::activated, this, &AssemblerWidget::OnZoomIn);
auto* zoom_out_shortcut = new QShortcut(QKeySequence::ZoomOut, this);
zoom_out_shortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_out_shortcut, &QShortcut::activated, this, &AssemblerWidget::OnZoomOut);
auto* zoom_in_alternate = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Equal), this);
zoom_in_alternate->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_in_alternate, &QShortcut::activated, this, &AssemblerWidget::OnZoomIn);
auto* zoom_out_alternate = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Underscore), this);
zoom_out_alternate->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_out_alternate, &QShortcut::activated, this, &AssemblerWidget::OnZoomOut);
auto* zoom_reset = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_0), this);
zoom_reset->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_reset, &QShortcut::activated, this, &AssemblerWidget::OnZoomReset);
ConnectWidgets();
UpdateIcons();
}
void AssemblerWidget::closeEvent(QCloseEvent*)
{
Settings::Instance().SetAssemblerVisible(false);
}
bool AssemblerWidget::ApplicationCloseRequest()
{
int num_unsaved = 0;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
if (GetEditor(i)->IsDirty())
{
num_unsaved++;
}
}
if (num_unsaved > 0)
{
const int result = ModalMessageBox::question(
this, tr("Unsaved Changes"),
tr("You have %1 unsaved assembly tabs open\n\n"
"Do you want to save all and exit?")
.arg(num_unsaved),
QMessageBox::YesToAll | QMessageBox::NoToAll | QMessageBox::Cancel, QMessageBox::Cancel);
switch (result)
{
case QMessageBox::YesToAll:
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* editor = GetEditor(i);
if (editor->IsDirty())
{
if (!SaveEditor(editor))
{
return false;
}
}
}
return true;
case QMessageBox::NoToAll:
return true;
case QMessageBox::Cancel:
return false;
}
}
return true;
}
AssemblerWidget::~AssemblerWidget()
{
auto& settings = Settings::GetQSettings();
settings.setValue(QStringLiteral("assemblerwidget/geometry"), saveGeometry());
settings.setValue(QStringLiteral("assemblerwidget/floating"), isFloating());
}
void AssemblerWidget::CreateWidgets()
{
m_asm_tabs = new QTabWidget;
m_toolbar = new QToolBar;
m_output_type = new QComboBox;
m_output_box = new QPlainTextEdit;
m_error_box = new QTextEdit;
m_address_line = new QLineEdit;
m_copy_output_button = new QPushButton;
m_asm_tabs->setTabsClosable(true);
// Initialize toolbar and actions
// m_toolbar->setIconSize(QSize(32, 32));
m_toolbar->setContentsMargins(0, 0, 0, 0);
m_toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_open = m_toolbar->addAction(tr("Open"), this, &AssemblerWidget::OnOpen);
m_new = m_toolbar->addAction(tr("New"), this, &AssemblerWidget::OnNew);
m_assemble = m_toolbar->addAction(tr("Assemble"), this, [this] {
std::vector<CodeBlock> unused;
OnAssemble(&unused);
});
m_inject = m_toolbar->addAction(tr("Inject"), this, &AssemblerWidget::OnInject);
m_save = m_toolbar->addAction(tr("Save"), this, &AssemblerWidget::OnSave);
m_inject->setEnabled(false);
m_save->setEnabled(false);
m_assemble->setEnabled(false);
// Initialize input, output, error text areas
auto palette = m_output_box->palette();
if (m_dark_scheme)
{
palette.setColor(QPalette::Base, QColor::fromRgb(76, 76, 76));
}
else
{
palette.setColor(QPalette::Base, QColor::fromRgb(180, 180, 180));
}
m_output_box->setPalette(palette);
m_error_box->setPalette(palette);
QFont mono_font(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
QFont error_font(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family());
mono_font.setPointSize(12);
error_font.setPointSize(12);
QFontMetrics mono_metrics(mono_font);
QFontMetrics err_metrics(mono_font);
m_output_box->setFont(mono_font);
m_error_box->setFont(error_font);
m_output_box->setReadOnly(true);
m_error_box->setReadOnly(true);
const int output_area_width = mono_metrics.horizontalAdvance(QLatin1Char('0')) * OUTPUT_BOX_WIDTH;
m_error_box->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
m_error_box->setFixedHeight(err_metrics.height() * 3 + mono_metrics.height());
m_output_box->setFixedWidth(output_area_width);
m_error_box->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
// Initialize output format selection box
m_output_type->addItem(tr("Raw"));
m_output_type->addItem(tr("AR Code"));
m_output_type->addItem(tr("Gecko (04)"));
m_output_type->addItem(tr("Gecko (C0)"));
m_output_type->addItem(tr("Gecko (C2)"));
// Setup layouts
auto* addr_input_layout = new QHBoxLayout;
addr_input_layout->addWidget(new QLabel(tr("Base Address")));
addr_input_layout->addWidget(m_address_line);
auto* output_extra_layout = new QHBoxLayout;
output_extra_layout->addWidget(m_output_type);
output_extra_layout->addWidget(m_copy_output_button);
QWidget* address_input_box = new QWidget();
address_input_box->setLayout(addr_input_layout);
addr_input_layout->setContentsMargins(0, 0, 0, 0);
QWidget* output_extra_box = new QWidget();
output_extra_box->setFixedWidth(output_area_width);
output_extra_box->setLayout(output_extra_layout);
output_extra_layout->setContentsMargins(0, 0, 0, 0);
auto* assembler_layout = new QGridLayout;
assembler_layout->setSpacing(0);
assembler_layout->setContentsMargins(5, 0, 5, 5);
assembler_layout->addWidget(m_toolbar, 0, 0, 1, 2);
{
auto* input_group = new QGroupBox(tr("Input"));
auto* layout = new QVBoxLayout;
input_group->setLayout(layout);
layout->addWidget(m_asm_tabs);
layout->addWidget(address_input_box);
assembler_layout->addWidget(input_group, 1, 0, 1, 1);
}
{
auto* output_group = new QGroupBox(tr("Output"));
auto* layout = new QGridLayout;
output_group->setLayout(layout);
layout->addWidget(m_output_box, 0, 0);
layout->addWidget(output_extra_box, 1, 0);
assembler_layout->addWidget(output_group, 1, 1, 1, 1);
output_group->setSizePolicy(
QSizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Expanding));
}
{
auto* error_group = new QGroupBox(tr("Error Log"));
auto* layout = new QHBoxLayout;
error_group->setLayout(layout);
layout->addWidget(m_error_box);
assembler_layout->addWidget(error_group, 2, 0, 1, 2);
error_group->setSizePolicy(
QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Fixed));
}
QWidget* widget = new QWidget;
widget->setLayout(assembler_layout);
setWidget(widget);
}
void AssemblerWidget::ConnectWidgets()
{
m_output_box->connect(m_output_box, &QPlainTextEdit::updateRequest, this, [this] {
if (m_output_box->verticalScrollBar()->isVisible())
{
m_output_box->setFixedWidth(m_output_box->fontMetrics().horizontalAdvance(QLatin1Char('0')) *
OUTPUT_BOX_WIDTH +
m_output_box->style()->pixelMetric(QStyle::PM_ScrollBarExtent));
}
else
{
m_output_box->setFixedWidth(m_output_box->fontMetrics().horizontalAdvance(QLatin1Char('0')) *
OUTPUT_BOX_WIDTH);
}
});
m_copy_output_button->connect(m_copy_output_button, &QPushButton::released, this,
&AssemblerWidget::OnCopyOutput);
m_address_line->connect(m_address_line, &QLineEdit::textChanged, this,
&AssemblerWidget::OnBaseAddressChanged);
m_asm_tabs->connect(m_asm_tabs, &QTabWidget::currentChanged, this, &AssemblerWidget::OnTabChange);
}
void AssemblerWidget::OnAssemble(std::vector<CodeBlock>* asm_out)
{
if (m_asm_tabs->currentIndex() == -1)
{
return;
}
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
AsmKind kind = AsmKind::Raw;
m_error_box->clear();
m_output_box->clear();
switch (m_output_type->currentIndex())
{
case 0:
kind = AsmKind::Raw;
break;
case 1:
kind = AsmKind::ActionReplay;
break;
case 2:
kind = AsmKind::Gecko;
break;
case 3:
kind = AsmKind::GeckoExec;
break;
case 4:
kind = AsmKind::GeckoTrampoline;
break;
}
bool good;
u32 base_address = m_address_line->text().toUInt(&good, 16);
if (!good)
{
base_address = 0;
const QString warning =
QStringLiteral("<span style=\"color:#ffcc00\">%1</span>").arg(tr("Warning"));
// i18n: '%1' is the translation of 'Warning'
m_error_box->append(tr("%1 invalid base address, defaulting to 0").arg(warning));
}
const std::string contents = active_editor->toPlainText().toStdString();
auto result = Assemble(contents, base_address);
if (IsFailure(result))
{
m_error_box->clear();
asm_out->clear();
const AssemblerError& error = GetFailure(result);
m_error_box->append(HtmlFormatErrorLoc(error));
m_error_box->append(HtmlFormatErrorLine(error));
m_error_box->append(HtmlFormatMessage(error));
asm_out->clear();
return;
}
auto& blocks = GetT(result);
std::ostringstream str_contents;
switch (kind)
{
case AsmKind::Raw:
DeserializeToRaw(blocks, str_contents);
break;
case AsmKind::ActionReplay:
DeserializeToAr(blocks, str_contents);
break;
case AsmKind::Gecko:
DeserializeToGecko(blocks, str_contents);
break;
case AsmKind::GeckoExec:
DeserializeToGeckoExec(blocks, str_contents);
break;
case AsmKind::GeckoTrampoline:
DeserializeToGeckoTramp(blocks, str_contents);
break;
}
m_output_box->appendPlainText(QString::fromStdString(str_contents.str()));
m_output_box->moveCursor(QTextCursor::MoveOperation::Start);
m_output_box->ensureCursorVisible();
*asm_out = std::move(GetT(result));
}
void AssemblerWidget::OnCopyOutput()
{
QApplication::clipboard()->setText(m_output_box->toPlainText());
}
void AssemblerWidget::OnOpen()
{
const std::string default_dir = File::GetUserPath(D_ASM_ROOT_IDX);
const QStringList paths = DolphinFileDialog::getOpenFileNames(
this, tr("Select a File"), QString::fromStdString(default_dir),
QStringLiteral("%1 (*.s *.S *.asm);;%2 (*)")
.arg(tr("All Assembly files"))
.arg(tr("All Files")));
if (paths.isEmpty())
{
return;
}
std::optional<int> show_index;
for (auto path : paths)
{
show_index = std::nullopt;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* editor = GetEditor(i);
if (editor->PathsMatch(path))
{
show_index = i;
break;
}
}
if (!show_index)
{
NewEditor(path);
}
}
if (show_index)
{
m_asm_tabs->setCurrentIndex(*show_index);
}
}
void AssemblerWidget::OnNew()
{
NewEditor();
}
void AssemblerWidget::OnInject()
{
Core::CPUThreadGuard guard(m_system);
std::vector<CodeBlock> asm_result;
OnAssemble(&asm_result);
for (const auto& blk : asm_result)
{
if (!PowerPC::MMU::HostIsRAMAddress(guard, blk.block_address) || blk.instructions.empty())
{
continue;
}
m_system.GetPowerPC().GetDebugInterface().SetPatch(guard, blk.block_address, blk.instructions);
}
}
void AssemblerWidget::OnSave()
{
if (m_asm_tabs->currentIndex() == -1)
{
return;
}
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
SaveEditor(active_editor);
}
void AssemblerWidget::OnZoomIn()
{
if (m_asm_tabs->currentIndex() != -1)
{
ZoomAllEditors(2);
}
}
void AssemblerWidget::OnZoomOut()
{
if (m_asm_tabs->currentIndex() != -1)
{
ZoomAllEditors(-2);
}
}
void AssemblerWidget::OnZoomReset()
{
if (m_asm_tabs->currentIndex() != -1)
{
ZoomAllEditors(-m_net_zoom_delta);
}
}
void AssemblerWidget::OnBaseAddressChanged()
{
if (m_asm_tabs->currentIndex() == -1)
{
return;
}
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
active_editor->SetBaseAddress(m_address_line->text());
}
void AssemblerWidget::OnTabChange(int index)
{
if (index == -1)
{
m_address_line->clear();
return;
}
AsmEditor* active_editor = GetEditor(index);
m_address_line->setText(active_editor->BaseAddress());
}
QString AssemblerWidget::TabTextForEditor(AsmEditor* editor, bool with_dirty)
{
ASSERT(editor != nullptr);
QString dirtyFlag = QStringLiteral();
if (editor->IsDirty() && with_dirty)
{
dirtyFlag = QStringLiteral(" *");
}
if (editor->Path().isEmpty())
{
if (editor->EditorNum() == 0)
{
return tr("New File%1").arg(dirtyFlag);
}
return tr("New File (%1)%2").arg(editor->EditorNum() + 1).arg(dirtyFlag);
}
return tr("%1%2").arg(editor->EditorTitle()).arg(dirtyFlag);
}
AsmEditor* AssemblerWidget::GetEditor(int idx)
{
return qobject_cast<AsmEditor*>(m_asm_tabs->widget(idx));
}
void AssemblerWidget::NewEditor(const QString& path)
{
AsmEditor* new_editor =
new AsmEditor(path, path.isEmpty() ? AllocateTabNum() : INVALID_EDITOR_NUM, m_dark_scheme);
if (!path.isEmpty() && !new_editor->LoadFromPath())
{
ModalMessageBox::warning(this, tr("Failed to open file"),
tr("Failed to read the contents of file\n\n"
"\"%1\"")
.arg(path));
delete new_editor;
return;
}
const int tab_idx = m_asm_tabs->addTab(new_editor, QStringLiteral());
new_editor->connect(new_editor, &AsmEditor::PathChanged, this, [this] {
AsmEditor* updated_tab = qobject_cast<AsmEditor*>(sender());
DisambiguateTabTitles(updated_tab);
UpdateTabText(updated_tab);
});
new_editor->connect(new_editor, &AsmEditor::DirtyChanged, this,
[this] { UpdateTabText(qobject_cast<AsmEditor*>(sender())); });
new_editor->connect(new_editor, &AsmEditor::ZoomRequested, this,
&AssemblerWidget::ZoomAllEditors);
new_editor->Zoom(m_net_zoom_delta);
DisambiguateTabTitles(new_editor);
m_asm_tabs->setTabText(tab_idx, TabTextForEditor(new_editor, true));
if (m_save && m_assemble)
{
m_save->setEnabled(true);
m_assemble->setEnabled(true);
}
m_asm_tabs->setCurrentIndex(tab_idx);
}
bool AssemblerWidget::SaveEditor(AsmEditor* editor)
{
QString save_path = editor->Path();
if (save_path.isEmpty())
{
const std::string default_dir = File::GetUserPath(D_ASM_ROOT_IDX);
const QString asm_filter = QStringLiteral("%1 (*.S)").arg(tr("Assembly File"));
const QString all_filter = QStringLiteral("%2 (*)").arg(tr("All Files"));
QString selected_filter;
save_path = DolphinFileDialog::getSaveFileName(
this, tr("Save File to"), QString::fromStdString(default_dir),
QStringLiteral("%1;;%2").arg(asm_filter).arg(all_filter), &selected_filter);
if (save_path.isEmpty())
{
return false;
}
if (selected_filter == asm_filter &&
std::filesystem::path(save_path.toStdString()).extension().empty())
{
save_path.append(QStringLiteral(".S"));
}
}
editor->SaveFile(save_path);
return true;
}
void AssemblerWidget::OnEmulationStateChanged(Core::State state)
{
m_inject->setEnabled(state != Core::State::Uninitialized);
}
void AssemblerWidget::OnTabClose(int index)
{
ASSERT(index < m_asm_tabs->count());
AsmEditor* editor = GetEditor(index);
if (editor->IsDirty())
{
const int result = ModalMessageBox::question(
this, tr("Unsaved Changes"),
tr("There are unsaved changes in \"%1\".\n\n"
"Do you want to save before closing?")
.arg(TabTextForEditor(editor, false)),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Cancel);
switch (result)
{
case QMessageBox::Yes:
if (editor->IsDirty())
{
if (!SaveEditor(editor))
{
return;
}
}
break;
case QMessageBox::No:
break;
case QMessageBox::Cancel:
return;
}
}
CloseTab(index, editor);
}
void AssemblerWidget::CloseTab(int index, AsmEditor* editor)
{
FreeTabNum(editor->EditorNum());
m_asm_tabs->removeTab(index);
editor->deleteLater();
DisambiguateTabTitles(nullptr);
if (m_asm_tabs->count() == 0 && m_save && m_assemble)
{
m_save->setEnabled(false);
m_assemble->setEnabled(false);
}
}
int AssemblerWidget::AllocateTabNum()
{
auto min_it = std::min_element(m_free_editor_nums.begin(), m_free_editor_nums.end());
if (min_it == m_free_editor_nums.end())
{
return m_unnamed_editor_count++;
}
const int min = *min_it;
m_free_editor_nums.erase(min_it);
return min;
}
void AssemblerWidget::FreeTabNum(int num)
{
if (num != INVALID_EDITOR_NUM)
{
m_free_editor_nums.push_back(num);
}
}
void AssemblerWidget::UpdateTabText(AsmEditor* editor)
{
int tab_idx = 0;
for (; tab_idx < m_asm_tabs->count(); tab_idx++)
{
if (m_asm_tabs->widget(tab_idx) == editor)
{
break;
}
}
ASSERT(tab_idx < m_asm_tabs->count());
m_asm_tabs->setTabText(tab_idx, TabTextForEditor(editor, true));
}
void AssemblerWidget::DisambiguateTabTitles(AsmEditor* new_tab)
{
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* check = GetEditor(i);
if (check->IsAmbiguous())
{
// Could group all editors with matching titles in a linked list
// but tracking that nicely without dangling pointers feels messy
bool still_ambiguous = false;
for (int j = 0; j < m_asm_tabs->count(); j++)
{
AsmEditor* against = GetEditor(j);
if (j != i && check->FileName() == against->FileName())
{
if (!against->IsAmbiguous())
{
against->SetAmbiguous(true);
UpdateTabText(against);
}
still_ambiguous = true;
}
}
if (!still_ambiguous)
{
check->SetAmbiguous(false);
UpdateTabText(check);
}
}
}
if (new_tab != nullptr)
{
bool is_ambiguous = false;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* against = GetEditor(i);
if (new_tab != against && against->FileName() == new_tab->FileName())
{
against->SetAmbiguous(true);
UpdateTabText(against);
is_ambiguous = true;
}
}
if (is_ambiguous)
{
new_tab->SetAmbiguous(true);
UpdateTabText(new_tab);
}
}
}
void AssemblerWidget::UpdateIcons()
{
m_new->setIcon(Resources::GetThemeIcon("assembler_new"));
m_open->setIcon(Resources::GetThemeIcon("assembler_openasm"));
m_save->setIcon(Resources::GetThemeIcon("assembler_save"));
m_assemble->setIcon(Resources::GetThemeIcon("assembler_assemble"));
m_inject->setIcon(Resources::GetThemeIcon("assembler_inject"));
m_copy_output_button->setIcon(Resources::GetThemeIcon("assembler_clipboard"));
}
void AssemblerWidget::ZoomAllEditors(int amount)
{
if (amount != 0)
{
m_net_zoom_delta += amount;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
GetEditor(i)->Zoom(amount);
}
}
}

View File

@ -0,0 +1,100 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDockWidget>
#include "Common/Assembler/GekkoAssembler.h"
#include "Core/Core.h"
class QTabWidget;
class AsmEditor;
class QAction;
class QComboBox;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
class QTextEdit;
class QToolBar;
namespace Core
{
class System;
} // namespace Core
class AssemblerWidget : public QDockWidget
{
Q_OBJECT
public:
explicit AssemblerWidget(QWidget* parent);
bool ApplicationCloseRequest();
~AssemblerWidget();
protected:
void closeEvent(QCloseEvent*);
private:
enum class AsmKind
{
Raw,
ActionReplay,
Gecko,
GeckoExec,
GeckoTrampoline
};
static constexpr int OUTPUT_BOX_WIDTH = 18;
void CreateWidgets();
void ConnectWidgets();
void OnEditChanged();
void OnAssemble(std::vector<Common::GekkoAssembler::CodeBlock>* asm_out);
void OnCopyOutput();
void OnOpen();
void OnNew();
void OnInject();
void OnSave();
void OnZoomIn();
void OnZoomOut();
void OnZoomReset();
void OnBaseAddressChanged();
void OnTabChange(int index);
QString TabTextForEditor(AsmEditor* editor, bool with_dirty);
AsmEditor* GetEditor(int idx);
void NewEditor(const QString& path = QStringLiteral());
bool SaveEditor(AsmEditor* editor);
void OnEmulationStateChanged(Core::State state);
void OnTabClose(int index);
void CloseTab(int index, AsmEditor* editor);
int AllocateTabNum();
void FreeTabNum(int num);
void UpdateTabText(AsmEditor* editor);
void DisambiguateTabTitles(AsmEditor* editor);
void UpdateIcons();
void ZoomAllEditors(int amount);
static constexpr int INVALID_EDITOR_NUM = -1;
Core::System& m_system;
QTabWidget* m_asm_tabs;
QPlainTextEdit* m_output_box;
QComboBox* m_output_type;
QPushButton* m_copy_output_button;
QTextEdit* m_error_box;
QLineEdit* m_address_line;
QToolBar* m_toolbar;
QAction* m_open;
QAction* m_new;
QAction* m_assemble;
QAction* m_inject;
QAction* m_save;
std::list<int> m_free_editor_nums;
int m_unnamed_editor_count;
int m_net_zoom_delta;
bool m_dark_scheme;
};

View File

@ -0,0 +1,369 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/AssemblyEditor.h"
#include <QFile>
#include <QPainter>
#include <QTextBlock>
#include <QToolTip>
#include <filesystem>
#include "Common/Assembler/GekkoParser.h"
#include "Common/StringUtil.h"
#include "DolphinQt/Debugger/GekkoSyntaxHighlight.h"
QSize AsmEditor::LineNumberArea::sizeHint() const
{
return QSize(asm_editor->LineNumberAreaWidth(), 0);
}
void AsmEditor::LineNumberArea::paintEvent(QPaintEvent* event)
{
asm_editor->LineNumberAreaPaintEvent(event);
}
AsmEditor::AsmEditor(const QString& path, int editor_num, bool dark_scheme, QWidget* parent)
: QPlainTextEdit(parent), m_path(path), m_base_address(QStringLiteral("0")),
m_editor_num(editor_num), m_dirty(false), m_dark_scheme(dark_scheme)
{
if (!m_path.isEmpty())
{
m_filename =
QString::fromStdString(std::filesystem::path(m_path.toStdString()).filename().string());
}
m_line_number_area = new LineNumberArea(this);
m_highlighter = new GekkoSyntaxHighlight(document(), currentCharFormat(), dark_scheme);
m_last_block = textCursor().block();
QFont mono_font(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
mono_font.setPointSize(12);
setFont(mono_font);
m_line_number_area->setFont(mono_font);
UpdateLineNumberAreaWidth(0);
HighlightCurrentLine();
setMouseTracking(true);
connect(this, &AsmEditor::blockCountChanged, this, &AsmEditor::UpdateLineNumberAreaWidth);
connect(this, &AsmEditor::updateRequest, this, &AsmEditor::UpdateLineNumberArea);
connect(this, &AsmEditor::cursorPositionChanged, this, &AsmEditor::HighlightCurrentLine);
connect(this, &AsmEditor::textChanged, this, [this] {
m_dirty = true;
emit DirtyChanged();
});
}
int AsmEditor::LineNumberAreaWidth()
{
int num_digits = 1;
for (int max = qMax(1, blockCount()); max >= 10; max /= 10, ++num_digits)
{
}
return 3 + CharWidth() * qMax(2, num_digits);
}
void AsmEditor::SetBaseAddress(const QString& ba)
{
if (ba != m_base_address)
{
m_base_address = ba;
m_dirty = true;
emit DirtyChanged();
}
}
bool AsmEditor::LoadFromPath()
{
QFile file(m_path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
return false;
}
const std::string base_addr_line = file.readLine().toStdString();
std::string base_address = "";
for (size_t i = 0; i < base_addr_line.length(); i++)
{
if (std::isspace(base_addr_line[i]))
{
continue;
}
else if (base_addr_line[i] == '#')
{
base_address = base_addr_line.substr(i + 1);
break;
}
else
{
break;
}
}
if (base_address.empty())
{
file.seek(0);
}
else
{
StringPopBackIf(&base_address, '\n');
if (base_address.empty())
{
base_address = "0";
}
m_base_address = QString::fromStdString(base_address);
}
const bool old_block = blockSignals(true);
setPlainText(QString::fromStdString(file.readAll().toStdString()));
blockSignals(old_block);
return true;
}
bool AsmEditor::PathsMatch(const QString& path) const
{
if (m_path.isEmpty() || path.isEmpty())
{
return false;
}
return std::filesystem::path(m_path.toStdString()) == std::filesystem::path(path.toStdString());
}
void AsmEditor::Zoom(int amount)
{
if (amount > 0)
{
zoomIn(amount);
}
else
{
zoomOut(-amount);
}
m_line_number_area->setFont(font());
}
bool AsmEditor::SaveFile(const QString& save_path)
{
QFile file(save_path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
{
return false;
}
if (m_path != save_path)
{
m_path = save_path;
m_filename =
QString::fromStdString(std::filesystem::path(m_path.toStdString()).filename().string());
emit PathChanged();
}
if (file.write(QStringLiteral("#%1\n").arg(m_base_address).toUtf8()) == -1)
{
return false;
}
if (file.write(toPlainText().toUtf8()) == -1)
{
return false;
}
m_dirty = false;
emit DirtyChanged();
return true;
}
void AsmEditor::UpdateLineNumberAreaWidth(int)
{
setViewportMargins(LineNumberAreaWidth(), 0, 0, 0);
}
void AsmEditor::UpdateLineNumberArea(const QRect& rect, int dy)
{
if (dy != 0)
{
m_line_number_area->scroll(0, dy);
}
else
{
m_line_number_area->update(0, rect.y(), m_line_number_area->width(), rect.height());
}
if (rect.contains(viewport()->rect()))
{
UpdateLineNumberAreaWidth(0);
}
}
int AsmEditor::CharWidth() const
{
return fontMetrics().horizontalAdvance(QLatin1Char(' '));
}
void AsmEditor::resizeEvent(QResizeEvent* e)
{
QPlainTextEdit::resizeEvent(e);
const QRect cr = contentsRect();
m_line_number_area->setGeometry(QRect(cr.left(), cr.top(), LineNumberAreaWidth(), cr.height()));
}
void AsmEditor::paintEvent(QPaintEvent* event)
{
QPlainTextEdit::paintEvent(event);
QPainter painter(viewport());
QTextCursor tc(document());
QPen p = QPen(Qt::red);
p.setStyle(Qt::PenStyle::SolidLine);
p.setWidth(1);
painter.setPen(p);
const int width = CharWidth();
for (QTextBlock blk = firstVisibleBlock(); blk.isVisible() && blk.isValid(); blk = blk.next())
{
if (blk.userData() == nullptr)
{
continue;
}
BlockInfo* info = static_cast<BlockInfo*>(blk.userData());
if (info->error_at_eol)
{
tc.setPosition(blk.position() + blk.length() - 1);
tc.clearSelection();
const QRect qr = cursorRect(tc);
painter.drawLine(qr.x(), qr.y() + qr.height(), qr.x() + width, qr.y() + qr.height());
}
}
}
bool AsmEditor::event(QEvent* e)
{
if (e->type() == QEvent::ToolTip)
{
QHelpEvent* he = static_cast<QHelpEvent*>(e);
QTextCursor hover_cursor = cursorForPosition(he->pos());
QTextBlock hover_block = hover_cursor.block();
BlockInfo* info = static_cast<BlockInfo*>(hover_block.userData());
if (info == nullptr || !info->error)
{
QToolTip::hideText();
return true;
}
QRect check_rect;
if (info->error_at_eol)
{
hover_cursor.setPosition(hover_block.position() +
static_cast<int>(info->error->col + info->error->len));
const QRect cursor_left = cursorRect(hover_cursor);
const int area_width = CharWidth();
check_rect = QRect(cursor_left.x() + LineNumberAreaWidth(), cursor_left.y(),
cursor_left.x() + area_width, cursor_left.height());
}
else
{
hover_cursor.setPosition(hover_block.position() + static_cast<int>(info->error->col));
const QRect cursor_left = cursorRect(hover_cursor);
hover_cursor.setPosition(hover_block.position() +
static_cast<int>(info->error->col + info->error->len));
const QRect cursor_right = cursorRect(hover_cursor);
check_rect = QRect(cursor_left.x() + LineNumberAreaWidth(), cursor_left.y(),
cursor_right.x() - cursor_left.x(), cursor_left.height());
}
if (check_rect.contains(he->pos()))
{
QToolTip::showText(he->globalPos(), QString::fromStdString(info->error->message));
}
else
{
QToolTip::hideText();
}
return true;
}
return QPlainTextEdit::event(e);
}
void AsmEditor::keyPressEvent(QKeyEvent* event)
{
// HACK: Change shift+enter to enter to keep lines as blocks
if (event->modifiers() & Qt::ShiftModifier &&
(event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return))
{
event->setModifiers(event->modifiers() & ~Qt::ShiftModifier);
}
QPlainTextEdit::keyPressEvent(event);
}
void AsmEditor::wheelEvent(QWheelEvent* event)
{
QPlainTextEdit::wheelEvent(event);
if (event->modifiers() & Qt::ControlModifier)
{
auto delta = static_cast<int>(std::round((event->angleDelta().y() / 120.0)));
if (delta != 0)
{
emit ZoomRequested(delta);
}
}
}
void AsmEditor::HighlightCurrentLine()
{
const bool old_state = blockSignals(true);
if (m_last_block.blockNumber() != textCursor().blockNumber())
{
m_highlighter->SetMode(2);
m_highlighter->rehighlightBlock(m_last_block);
m_last_block = textCursor().block();
}
m_highlighter->SetCursorLoc(textCursor().positionInBlock());
m_highlighter->SetMode(1);
m_highlighter->rehighlightBlock(textCursor().block());
m_highlighter->SetMode(0);
blockSignals(old_state);
}
void AsmEditor::LineNumberAreaPaintEvent(QPaintEvent* event)
{
QPainter painter(m_line_number_area);
if (m_dark_scheme)
{
painter.fillRect(event->rect(), QColor::fromRgb(76, 76, 76));
}
else
{
painter.fillRect(event->rect(), QColor::fromRgb(180, 180, 180));
}
QTextBlock block = firstVisibleBlock();
int block_num = block.blockNumber();
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
int bottom = top + qRound(blockBoundingRect(block).height());
while (block.isValid() && top <= event->rect().bottom())
{
if (block.isVisible() && bottom >= event->rect().top())
{
const QString num = QString::number(block_num + 1);
painter.drawText(0, top, m_line_number_area->width(), fontMetrics().height(), Qt::AlignRight,
num);
}
block = block.next();
top = bottom;
bottom = top + qRound(blockBoundingRect(block).height());
++block_num;
}
}

View File

@ -0,0 +1,81 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QPlainTextEdit>
#include <QTextBlock>
class QWidget;
class QPaintEvent;
class QResizeEvent;
class QRect;
class QWheelEvent;
class GekkoSyntaxHighlight;
class AsmEditor : public QPlainTextEdit
{
Q_OBJECT;
public:
AsmEditor(const QString& file_path, int editor_num, bool dark_scheme, QWidget* parent = nullptr);
void LineNumberAreaPaintEvent(QPaintEvent* event);
int LineNumberAreaWidth();
const QString& Path() const { return m_path; }
const QString& FileName() const { return m_filename; }
const QString& EditorTitle() const { return m_title_ambiguous ? Path() : FileName(); }
const QString& BaseAddress() const { return m_base_address; }
void SetBaseAddress(const QString& ba);
void SetAmbiguous(bool b) { m_title_ambiguous = b; }
int EditorNum() const { return m_editor_num; }
bool LoadFromPath();
bool IsDirty() const { return m_dirty; }
bool IsAmbiguous() const { return m_title_ambiguous; }
bool PathsMatch(const QString& path) const;
void Zoom(int amount);
public slots:
bool SaveFile(const QString& save_path);
signals:
void PathChanged();
void DirtyChanged();
void ZoomRequested(int amount);
protected:
void resizeEvent(QResizeEvent* event) override;
void paintEvent(QPaintEvent* event) override;
bool event(QEvent* e) override;
void keyPressEvent(QKeyEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
private:
void UpdateLineNumberAreaWidth(int new_block_count);
void HighlightCurrentLine();
void UpdateLineNumberArea(const QRect& rect, int dy);
int CharWidth() const;
class LineNumberArea : public QWidget
{
public:
LineNumberArea(AsmEditor* editor) : QWidget(editor), asm_editor(editor) {}
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent* event) override;
private:
AsmEditor* asm_editor;
};
QWidget* m_line_number_area;
GekkoSyntaxHighlight* m_highlighter;
QString m_path;
QString m_filename;
QString m_base_address;
const int m_editor_num;
bool m_dirty;
QTextBlock m_last_block;
bool m_title_ambiguous;
bool m_dark_scheme;
};

View File

@ -35,6 +35,7 @@
#include "Core/PowerPC/PPCSymbolDB.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "DolphinQt/Debugger/AssembleInstructionDialog.h"
#include "DolphinQt/Debugger/PatchInstructionDialog.h"
#include "DolphinQt/Host.h"
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
@ -597,6 +598,8 @@ void CodeViewWidget::OnContextMenu()
auto* insert_nop_action = menu->addAction(tr("Insert &nop"), this, &CodeViewWidget::OnInsertNOP);
auto* replace_action =
menu->addAction(tr("Re&place instruction"), this, &CodeViewWidget::OnReplaceInstruction);
auto* assemble_action =
menu->addAction(tr("Assemble instruction"), this, &CodeViewWidget::OnAssembleInstruction);
auto* restore_action =
menu->addAction(tr("Restore instruction"), this, &CodeViewWidget::OnRestoreInstruction);
@ -637,8 +640,9 @@ void CodeViewWidget::OnContextMenu()
run_until_menu->setEnabled(!target.isEmpty());
follow_branch_action->setEnabled(follow_branch_enabled);
for (auto* action : {copy_address_action, copy_line_action, copy_hex_action, function_action,
ppc_action, insert_blr_action, insert_nop_action, replace_action})
for (auto* action :
{copy_address_action, copy_line_action, copy_hex_action, function_action, ppc_action,
insert_blr_action, insert_nop_action, replace_action, assemble_action})
{
action->setEnabled(running);
}
@ -997,8 +1001,17 @@ void CodeViewWidget::OnSetSymbolEndAddress()
void CodeViewWidget::OnReplaceInstruction()
{
Core::CPUThreadGuard guard(m_system);
DoPatchInstruction(false);
}
void CodeViewWidget::OnAssembleInstruction()
{
DoPatchInstruction(true);
}
void CodeViewWidget::DoPatchInstruction(bool assemble)
{
Core::CPUThreadGuard guard(m_system);
const u32 addr = GetContextAddress();
if (!PowerPC::MMU::HostIsInstructionRAMAddress(guard, addr))
@ -1010,13 +1023,26 @@ void CodeViewWidget::OnReplaceInstruction()
return;
auto& debug_interface = m_system.GetPowerPC().GetDebugInterface();
PatchInstructionDialog dialog(this, addr, debug_interface.ReadInstruction(guard, addr));
SetQWidgetWindowDecorations(&dialog);
if (dialog.exec() == QDialog::Accepted)
if (assemble)
{
debug_interface.SetPatch(guard, addr, dialog.GetCode());
Update(&guard);
AssembleInstructionDialog dialog(this, addr, debug_interface.ReadInstruction(guard, addr));
SetQWidgetWindowDecorations(&dialog);
if (dialog.exec() == QDialog::Accepted)
{
debug_interface.SetPatch(guard, addr, dialog.GetCode());
Update(&guard);
}
}
else
{
PatchInstructionDialog dialog(this, addr, debug_interface.ReadInstruction(guard, addr));
SetQWidgetWindowDecorations(&dialog);
if (dialog.exec() == QDialog::Accepted)
{
debug_interface.SetPatch(guard, addr, dialog.GetCode());
Update(&guard);
}
}
}

View File

@ -95,6 +95,8 @@ private:
void OnInsertBLR();
void OnInsertNOP();
void OnReplaceInstruction();
void OnAssembleInstruction();
void DoPatchInstruction(bool assemble);
void OnRestoreInstruction();
void CalculateBranchIndentation();

View File

@ -0,0 +1,261 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/GekkoSyntaxHighlight.h"
#include "Common/Assembler/GekkoParser.h"
#include <QLabel>
#include <QPalette>
namespace
{
using namespace Common::GekkoAssembler;
using namespace Common::GekkoAssembler::detail;
class HighlightParsePlugin : public ParsePlugin
{
public:
virtual ~HighlightParsePlugin() = default;
std::vector<std::pair<int, int>>&& MoveParens() { return std::move(m_matched_parens); }
std::vector<std::tuple<int, int, HighlightFormat>>&& MoveFormatting()
{
return std::move(m_formatting);
}
void OnDirectivePre(GekkoDirective) override { HighlightCurToken(HighlightFormat::Directive); }
void OnInstructionPre(const ParseInfo&, bool) override
{
HighlightCurToken(HighlightFormat::Mnemonic);
}
void OnTerminal(Terminal type, const AssemblerToken& val) override
{
switch (type)
{
case Terminal::Id:
HighlightCurToken(HighlightFormat::Symbol);
break;
case Terminal::Hex:
case Terminal::Dec:
case Terminal::Oct:
case Terminal::Bin:
case Terminal::Flt:
HighlightCurToken(HighlightFormat::Immediate);
break;
case Terminal::GPR:
HighlightCurToken(HighlightFormat::GPR);
break;
case Terminal::FPR:
HighlightCurToken(HighlightFormat::GPR);
break;
case Terminal::SPR:
HighlightCurToken(HighlightFormat::SPR);
break;
case Terminal::CRField:
HighlightCurToken(HighlightFormat::CRField);
break;
case Terminal::Lt:
case Terminal::Gt:
case Terminal::Eq:
case Terminal::So:
HighlightCurToken(HighlightFormat::CRFlag);
break;
case Terminal::Str:
HighlightCurToken(HighlightFormat::Str);
break;
default:
break;
}
}
void OnHiaddr(std::string_view) override
{
HighlightCurToken(HighlightFormat::Symbol);
auto&& [ha_pos, ha_tok] = m_owner->lexer.LookaheadTagRef(2);
m_formatting.emplace_back(static_cast<int>(ha_pos.col),
static_cast<int>(ha_tok.token_val.length()), HighlightFormat::HaLa);
}
void OnLoaddr(std::string_view id) override { OnHiaddr(id); }
void OnOpenParen(ParenType type) override
{
m_paren_stack.push_back(static_cast<int>(m_owner->lexer.ColNumber()));
}
void OnCloseParen(ParenType type) override
{
if (m_paren_stack.empty())
{
return;
}
m_matched_parens.emplace_back(m_paren_stack.back(),
static_cast<int>(m_owner->lexer.ColNumber()));
m_paren_stack.pop_back();
}
void OnError() override
{
m_formatting.emplace_back(static_cast<int>(m_owner->error->col),
static_cast<int>(m_owner->error->len), HighlightFormat::Error);
}
void OnLabelDecl(std::string_view name) override
{
const int len = static_cast<int>(m_owner->lexer.LookaheadRef().token_val.length());
const int off = static_cast<int>(m_owner->lexer.ColNumber());
m_formatting.emplace_back(len, off, HighlightFormat::Symbol);
}
void OnVarDecl(std::string_view name) override { OnLabelDecl(name); }
private:
std::vector<int> m_paren_stack;
std::vector<std::pair<int, int>> m_matched_parens;
std::vector<std::tuple<int, int, HighlightFormat>> m_formatting;
void HighlightCurToken(HighlightFormat format)
{
const int len = static_cast<int>(m_owner->lexer.LookaheadRef().token_val.length());
const int off = static_cast<int>(m_owner->lexer.ColNumber());
m_formatting.emplace_back(off, len, format);
}
};
} // namespace
void GekkoSyntaxHighlight::highlightBlock(const QString& text)
{
BlockInfo* info = static_cast<BlockInfo*>(currentBlockUserData());
if (info == nullptr)
{
info = new BlockInfo;
setCurrentBlockUserData(info);
}
qsizetype comment_idx = text.indexOf(QLatin1Char('#'));
if (comment_idx != -1)
{
HighlightSubstr(comment_idx, text.length() - comment_idx, HighlightFormat::Comment);
}
if (m_mode == 0)
{
HighlightParsePlugin plugin;
ParseWithPlugin(&plugin, text.toStdString());
info->block_format = plugin.MoveFormatting();
info->parens = plugin.MoveParens();
info->error = std::move(plugin.Error());
info->error_at_eol = info->error && info->error->len == 0;
}
else if (m_mode == 1)
{
auto paren_it = std::find_if(info->parens.begin(), info->parens.end(),
[this](const std::pair<int, int>& p) {
return p.first == m_cursor_loc || p.second == m_cursor_loc;
});
if (paren_it != info->parens.end())
{
HighlightSubstr(paren_it->first, 1, HighlightFormat::Paren);
HighlightSubstr(paren_it->second, 1, HighlightFormat::Paren);
}
}
for (auto&& [off, len, format] : info->block_format)
{
HighlightSubstr(off, len, format);
}
}
GekkoSyntaxHighlight::GekkoSyntaxHighlight(QTextDocument* document, QTextCharFormat base_format,
bool dark_scheme)
: QSyntaxHighlighter(document), m_base_format(base_format)
{
QPalette base_scheme;
m_theme_idx = dark_scheme ? 1 : 0;
}
void GekkoSyntaxHighlight::HighlightSubstr(int start, int len, HighlightFormat format)
{
QTextCharFormat hl_format = m_base_format;
const QColor DIRECTIVE_COLOR[2] = {QColor(0x9d, 0x00, 0x06),
QColor(0xfb, 0x49, 0x34)}; // Gruvbox darkred
const QColor MNEMONIC_COLOR[2] = {QColor(0x79, 0x74, 0x0e),
QColor(0xb8, 0xbb, 0x26)}; // Gruvbox darkgreen
const QColor IMM_COLOR[2] = {QColor(0xb5, 0x76, 0x14),
QColor(0xfa, 0xbd, 0x2f)}; // Gruvbox darkyellow
const QColor BUILTIN_COLOR[2] = {QColor(0x07, 0x66, 0x78),
QColor(0x83, 0xa5, 0x98)}; // Gruvbox darkblue
const QColor HA_LA_COLOR[2] = {QColor(0xaf, 0x3a, 0x03),
QColor(0xfe, 0x80, 0x19)}; // Gruvbox darkorange
const QColor HOVER_BG_COLOR[2] = {QColor(0xd5, 0xc4, 0xa1),
QColor(0x50, 0x49, 0x45)}; // Gruvbox bg2
const QColor STRING_COLOR[2] = {QColor(0x98, 0x97, 0x1a),
QColor(0x98, 0x97, 0x1a)}; // Gruvbox green
const QColor COMMENT_COLOR[2] = {QColor(0x68, 0x9d, 0x6a),
QColor(0x68, 0x9d, 0x6a)}; // Gruvbox aqua
switch (format)
{
case HighlightFormat::Directive:
hl_format.setForeground(DIRECTIVE_COLOR[m_theme_idx]);
break;
case HighlightFormat::Mnemonic:
hl_format.setForeground(MNEMONIC_COLOR[m_theme_idx]);
break;
case HighlightFormat::Symbol:
break;
case HighlightFormat::Immediate:
hl_format.setForeground(IMM_COLOR[m_theme_idx]);
break;
case HighlightFormat::GPR:
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
break;
case HighlightFormat::FPR:
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
break;
case HighlightFormat::SPR:
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
break;
case HighlightFormat::CRField:
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
break;
case HighlightFormat::CRFlag:
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
break;
case HighlightFormat::Str:
hl_format.setForeground(STRING_COLOR[m_theme_idx]);
break;
case HighlightFormat::HaLa:
hl_format.setForeground(HA_LA_COLOR[m_theme_idx]);
break;
case HighlightFormat::Paren:
hl_format.setBackground(HOVER_BG_COLOR[m_theme_idx]);
break;
case HighlightFormat::Default:
hl_format.clearForeground();
hl_format.clearBackground();
break;
case HighlightFormat::Comment:
hl_format.setForeground(COMMENT_COLOR[m_theme_idx]);
break;
case HighlightFormat::Error:
hl_format.setUnderlineColor(Qt::red);
hl_format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
break;
}
setFormat(start, len, hl_format);
}

View File

@ -0,0 +1,60 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QSyntaxHighlighter>
#include <QTextCharFormat>
#include <optional>
#include "Common/Assembler/AssemblerShared.h"
enum class HighlightFormat
{
Directive,
Mnemonic,
Symbol,
Immediate,
GPR,
FPR,
SPR,
CRField,
CRFlag,
Str,
HaLa,
Paren,
Default,
Comment,
Error,
};
struct BlockInfo : public QTextBlockUserData
{
std::vector<std::tuple<int, int, HighlightFormat>> block_format;
std::vector<std::pair<int, int>> parens;
std::optional<Common::GekkoAssembler::AssemblerError> error;
bool error_at_eol = false;
};
class GekkoSyntaxHighlight : public QSyntaxHighlighter
{
Q_OBJECT;
public:
explicit GekkoSyntaxHighlight(QTextDocument* document, QTextCharFormat base_format,
bool dark_scheme);
void HighlightSubstr(int start, int len, HighlightFormat format);
void SetMode(int mode) { m_mode = mode; }
void SetCursorLoc(int loc) { m_cursor_loc = loc; }
protected:
void highlightBlock(const QString& line) override;
private:
int m_mode = 0;
int m_cursor_loc = 0;
QTextCharFormat m_base_format;
int m_theme_idx = 0;
};