mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-23 14:19:46 -06:00
Merge pull request #11497 from vyuuui/debugger_assembler_ui
Built-in assembler for debugger interface
This commit is contained in:
131
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.cpp
Normal file
131
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.cpp
Normal 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;
|
||||
}
|
36
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.h
Normal file
36
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.h
Normal 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;
|
||||
};
|
960
Source/Core/DolphinQt/Debugger/AssemblerWidget.cpp
Normal file
960
Source/Core/DolphinQt/Debugger/AssemblerWidget.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
100
Source/Core/DolphinQt/Debugger/AssemblerWidget.h
Normal file
100
Source/Core/DolphinQt/Debugger/AssemblerWidget.h
Normal 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;
|
||||
};
|
369
Source/Core/DolphinQt/Debugger/AssemblyEditor.cpp
Normal file
369
Source/Core/DolphinQt/Debugger/AssemblyEditor.cpp
Normal 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;
|
||||
}
|
||||
}
|
81
Source/Core/DolphinQt/Debugger/AssemblyEditor.h
Normal file
81
Source/Core/DolphinQt/Debugger/AssemblyEditor.h
Normal 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;
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,6 +95,8 @@ private:
|
||||
void OnInsertBLR();
|
||||
void OnInsertNOP();
|
||||
void OnReplaceInstruction();
|
||||
void OnAssembleInstruction();
|
||||
void DoPatchInstruction(bool assemble);
|
||||
void OnRestoreInstruction();
|
||||
|
||||
void CalculateBranchIndentation();
|
||||
|
261
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.cpp
Normal file
261
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.cpp
Normal 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);
|
||||
}
|
60
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.h
Normal file
60
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.h
Normal 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;
|
||||
};
|
Reference in New Issue
Block a user