mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-26 15:55:31 +01:00
72cf2bdb87
Some pieces of code are calling IsRunning because there's some particular action that only makes sense when emulation is running, for instance showing the state of the emulated CPU. IsRunning is appropriate to use for this. Then there are pieces of code that are calling IsRunning because there's some particular thing they must avoid doing e.g. when the CPU thread is running or IOS is running. IsRunning isn't quite appropriate for this. Such code should also be checking for the states Starting and Stopping. Keep in mind that: * When the state is Starting, the state can asynchronously change to Running at any time. * When we try to stop the core, the state gets set to Stopping before we take any action to actually stop things. This commit adds a new method Core::IsUninitialized, and changes all callers of IsRunning and GetState that look to me like they should be changed.
958 lines
26 KiB
C++
958 lines
26 KiB
C++
// 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)
|
|
{
|
|
return QObject::tr("<span style=\"color: red; font-weight: bold\">Error</span> on line %1 col %2")
|
|
.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;
|
|
m_error_box->append(
|
|
tr("<span style=\"color:#ffcc00\">Warning</span> invalid base address, defaulting to 0"));
|
|
}
|
|
|
|
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 result;
|
|
if (!editor->Path().isEmpty())
|
|
result = editor->EditorTitle();
|
|
else if (editor->EditorNum() == 0)
|
|
result = tr("New File");
|
|
else
|
|
result = tr("New File (%1)").arg(editor->EditorNum() + 1);
|
|
|
|
if (with_dirty && editor->IsDirty())
|
|
{
|
|
// i18n: This asterisk is added to the title of an editor to indicate that it has unsaved
|
|
// changes
|
|
result = tr("%1 *").arg(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
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%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::Running || state == Core::State::Paused);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|