Qt: Initial CPU debugger widget implementation

Co-Authored-By: liuk707 <62625900+liuk7071@users.noreply.github.com>
This commit is contained in:
wheremyfoodat 2025-07-07 02:11:57 +03:00
parent 8e20bd6220
commit 9dc52577ea
8 changed files with 311 additions and 6 deletions

View file

@ -733,12 +733,12 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp
src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp
src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp src/panda_qt/shader_editor.cpp src/panda_qt/translations.cpp src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp src/panda_qt/shader_editor.cpp src/panda_qt/translations.cpp
src/panda_qt/thread_debugger.cpp src/panda_qt/thread_debugger.cpp src/panda_qt/cpu_debugger.cpp
) )
set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp
include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp
include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp include/panda_qt/shader_editor.hpp include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp include/panda_qt/shader_editor.hpp
include/panda_qt/thread_debugger.hpp include/panda_qt/thread_debugger.hpp include/panda_qt/cpu_debugger.hpp include/panda_qt/disabled_widget_overlay.hpp
) )
source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES})

View file

@ -23,7 +23,7 @@ namespace Common {
// pc: program counter of the instruction to disassemble // pc: program counter of the instruction to disassemble
// bytes: Byte representation of instruction // bytes: Byte representation of instruction
// buffer: text buffer to output the disassembly too // buffer: text buffer to output the disassembly too
usize disassemble(std::string& buffer, u32 pc, std::span<u8> bytes, u64 offset = 0) { usize disassemble(std::string& buffer, u32 pc, std::span<const u8> bytes, u64 offset = 0) {
if (!initialized) { if (!initialized) {
buffer = "Capstone was not properly initialized"; buffer = "Capstone was not properly initialized";
return 0; return 0;

View file

@ -122,14 +122,16 @@ class Emulator {
// Reloads some settings that require special handling, such as audio enable // Reloads some settings that require special handling, such as audio enable
void reloadSettings(); void reloadSettings();
CPU& getCPU() { return cpu; }
Memory& getMemory() { return memory; }
Kernel& getKernel() { return kernel; }
Scheduler& getScheduler() { return scheduler; }
EmulatorConfig& getConfig() { return config; } EmulatorConfig& getConfig() { return config; }
Cheats& getCheats() { return cheats; } Cheats& getCheats() { return cheats; }
ServiceManager& getServiceManager() { return kernel.getServiceManager(); } ServiceManager& getServiceManager() { return kernel.getServiceManager(); }
LuaManager& getLua() { return lua; } LuaManager& getLua() { return lua; }
Scheduler& getScheduler() { return scheduler; }
Memory& getMemory() { return memory; }
AudioDeviceInterface& getAudioDevice() { return audioDevice; } AudioDeviceInterface& getAudioDevice() { return audioDevice; }
Kernel& getKernel() { return kernel; }
RendererType getRendererType() const { return config.rendererType; } RendererType getRendererType() const { return config.rendererType; }
Renderer* getRenderer() { return gpu.getRenderer(); } Renderer* getRenderer() { return gpu.getRenderer(); }

View file

@ -0,0 +1,38 @@
#pragma once
#include <QListWidget>
#include <QPlainTextEdit>
#include <QScrollBar>
#include <QWidget>
#include "emulator.hpp"
#include "panda_qt/disabled_widget_overlay.hpp"
class CPUDebugger : public QWidget {
Q_OBJECT
Emulator* emu;
QListWidget* disasmListWidget;
QScrollBar* verticalScrollBar;
QPlainTextEdit* registerTextEdit;
DisabledWidgetOverlay* disabledOverlay;
bool enabled = false;
public:
CPUDebugger(Emulator* emulator, QWidget* parent = nullptr);
void enable();
void disable();
private:
// Update the state of the disassembler. Qt events should always call update, not updateDisasm/updateRegister
// As update properly handles thread safety
void update();
void updateDisasm();
void updateRegisters();
bool eventFilter(QObject* obj, QEvent* event) override;
void showEvent(QShowEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void keyPressEvent(QKeyEvent* event);
};

View file

@ -0,0 +1,28 @@
#pragma once
#include <QtWidgets>
class DisabledWidgetOverlay : public QWidget {
Q_OBJECT
public:
DisabledWidgetOverlay(QWidget *parent = nullptr, QString overlayText = tr("This widget is disabled")) : text(overlayText), QWidget(parent) {
setVisible(false);
}
private:
QString text;
void paintEvent(QPaintEvent *) override {
QPainter painter = QPainter(this);
painter.fillRect(rect(), QColor(60, 60, 60, 128));
painter.setPen(Qt::gray);
QFont font = painter.font();
font.setBold(true);
font.setPointSize(18);
painter.setFont(font);
painter.drawText(rect(), Qt::AlignCenter, text);
}
};

View file

@ -17,6 +17,7 @@
#include "panda_qt/about_window.hpp" #include "panda_qt/about_window.hpp"
#include "panda_qt/cheats_window.hpp" #include "panda_qt/cheats_window.hpp"
#include "panda_qt/config_window.hpp" #include "panda_qt/config_window.hpp"
#include "panda_qt/cpu_debugger.hpp"
#include "panda_qt/patch_window.hpp" #include "panda_qt/patch_window.hpp"
#include "panda_qt/screen.hpp" #include "panda_qt/screen.hpp"
#include "panda_qt/shader_editor.hpp" #include "panda_qt/shader_editor.hpp"
@ -110,6 +111,7 @@ class MainWindow : public QMainWindow {
TextEditorWindow* luaEditor; TextEditorWindow* luaEditor;
PatchWindow* patchWindow; PatchWindow* patchWindow;
ShaderEditorWindow* shaderEditor; ShaderEditorWindow* shaderEditor;
CPUDebugger* cpuDebugger;
ThreadDebugger* threadDebugger; ThreadDebugger* threadDebugger;
// We use SDL's game controller API since it's the sanest API that supports as many controllers as possible // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible

View file

@ -0,0 +1,232 @@
#include "panda_qt/cpu_debugger.hpp"
#include <fmt/format.h>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QListWidget>
#include <QPlainTextEdit>
#include <QPushButton>
#include <span>
#include <utility>
#include "capstone.hpp"
static int getLinesInViewport(QListWidget* listWidget) {
auto viewportHeight = listWidget->viewport()->height();
QFontMetrics fm = QFontMetrics(listWidget->font());
auto lineHeight = fm.height();
return int(viewportHeight / lineHeight);
}
static std::pair<int, int> getVisibleLineRange(QListWidget* listWidget, QScrollBar* scrollBar) {
int firstLine = scrollBar->value();
int lineCount = getLinesInViewport(listWidget);
return {firstLine, lineCount};
}
CPUDebugger::CPUDebugger(Emulator* emulator, QWidget* parent) : emu(emulator), QWidget(parent, Qt::Window) {
setWindowTitle(tr("CPU debugger"));
resize(1000, 600);
// Main grid layout
QGridLayout* gridLayout = new QGridLayout(this);
// Top row: buttons in a horizontal layout
QHBoxLayout* horizontalLayout = new QHBoxLayout();
QPushButton* stepButton = new QPushButton(tr("Step"), this);
QPushButton* goToAddressButton = new QPushButton(tr("Go to address"), this);
QPushButton* goToPCButton = new QPushButton(tr("Go to PC"), this);
horizontalLayout->addWidget(stepButton);
horizontalLayout->addWidget(goToAddressButton);
horizontalLayout->addWidget(goToPCButton);
gridLayout->addLayout(horizontalLayout, 0, 0);
// Disassembly list on the left
disasmListWidget = new QListWidget(this);
gridLayout->addWidget(disasmListWidget, 1, 0);
// Vertical scroll bar in the middle
verticalScrollBar = new QScrollBar(Qt::Vertical, this);
gridLayout->addWidget(verticalScrollBar, 1, 1);
// Register view on the right
registerTextEdit = new QPlainTextEdit(this);
registerTextEdit->setEnabled(true);
registerTextEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
registerTextEdit->setMaximumWidth(800);
gridLayout->addWidget(registerTextEdit, 1, 2);
// Setup disabled widget overlay
disabledOverlay = new DisabledWidgetOverlay(this, tr("Pause the emulator to use the CPU Debugger"));
disabledOverlay->resize(size()); // Fill the whole screen
disabledOverlay->raise();
disabledOverlay->hide();
// Monospace font
QFont mono_font = QFont("Courier New");
mono_font.setStyleHint(QFont::Monospace);
disasmListWidget->setFont(mono_font);
registerTextEdit->setFont(mono_font);
// To forward scrolling from the list widget to the external scrollbar
disasmListWidget->installEventFilter(this);
// Setup scroll bar
verticalScrollBar->setRange(0, INT32_MAX);
verticalScrollBar->setSingleStep(8);
verticalScrollBar->setPageStep(getLinesInViewport(disasmListWidget));
verticalScrollBar->show();
connect(verticalScrollBar, &QScrollBar::valueChanged, this, &CPUDebugger::updateDisasm);
registerTextEdit->setReadOnly(true);
connect(goToPCButton, &QPushButton::clicked, this, [&]() {
u32 pc = emu->getCPU().getReg(15);
verticalScrollBar->setValue(pc);
});
disable();
hide();
}
void CPUDebugger::enable() {
enabled = true;
auto pc = emu->getCPU().getReg(15);
disabledOverlay->hide();
verticalScrollBar->setValue(pc);
update();
}
void CPUDebugger::disable() {
enabled = false;
disabledOverlay->show();
}
void CPUDebugger::update() {
if (enabled) {
updateDisasm();
updateRegisters();
}
}
void CPUDebugger::updateDisasm() {
int currentRow = disasmListWidget->currentRow();
disasmListWidget->clear();
auto [firstLine, lineCount] = getVisibleLineRange(disasmListWidget, verticalScrollBar);
const u32 startPC = (firstLine + 3) & ~3; // Align PC to 4 bytes
const u32 endPC = startPC + lineCount * sizeof(u32);
auto& cpu = emu->getCPU();
auto& mem = emu->getMemory();
u32 pc = cpu.getReg(15);
Common::CapstoneDisassembler disassembler(CS_ARCH_ARM, CS_MODE_ARM);
std::string disassembly;
for (u32 addr = startPC; addr < endPC; addr += sizeof(u32)) {
if (auto pointer = (u32*)mem.getReadPointer(addr)) {
const u32 instruction = *pointer;
// Convert instruction to byte array to pass to Capstone
const std::array<u8, 4> bytes = {
u8(instruction & 0xff),
u8((instruction >> 8) & 0xff),
u8((instruction >> 16) & 0xff),
u8((instruction >> 24) & 0xff),
};
disassembler.disassemble(disassembly, pc, std::span(bytes));
disassembly = fmt::format("{:08X} | {}", addr, disassembly);
QListWidgetItem* item = new QListWidgetItem(QString::fromStdString(disassembly));
if (addr == pc) {
item->setBackground(Qt::darkGreen);
}
disasmListWidget->addItem(item);
} else
disasmListWidget->addItem(QString::fromStdString(fmt::format("{:08X} | ???", addr)));
}
disasmListWidget->setCurrentRow(currentRow);
}
void CPUDebugger::updateRegisters() {
auto& cpu = emu->getCPU();
const std::span<u32, 16> gprs = cpu.regs();
const std::span<u32, 32> fprs = cpu.fprs();
const u32 pc = gprs[15];
const u32 cpsr = cpu.getCPSR();
const u32 fpscr = cpu.getFPSCR();
std::string text = "";
text.reserve(2048);
text += fmt::format("PC: {:08X}\nCPSR: {:08X}\nFPSCR: {:08X}\n", pc, cpsr, fpscr);
text += "\nGeneral Purpose Registers\n";
for (int i = 0; i < 10; i++) {
text += fmt::format("r{:01d}: 0x{:08X}\n", i, gprs[i]);
}
for (int i = 10; i < 16; i++) {
text += fmt::format("r{:02d}: 0x{:08X}\n", i, gprs[i]);
}
text += "\nFloating Point Registers\n";
for (int i = 0; i < 10; i++) {
text += fmt::format("f{:01d}: {:f}\n", i, Helpers::bit_cast<float, u32>(fprs[i]));
}
for (int i = 10; i < 32; i++) {
text += fmt::format("f{:01d}: {:f}\n", i, Helpers::bit_cast<float, u32>(fprs[i]));
}
registerTextEdit->setPlainText(QString::fromStdString(text));
}
bool CPUDebugger::eventFilter(QObject* obj, QEvent* event) {
// Forward scroll events from the list widget to the scrollbar
if (obj == disasmListWidget && event->type() == QEvent::Wheel) {
QWheelEvent* wheelEvent = (QWheelEvent*)event;
int wheelSteps = wheelEvent->angleDelta().y() / 60;
int newScrollValue = verticalScrollBar->value() - wheelSteps;
newScrollValue = qBound(verticalScrollBar->minimum(), newScrollValue, verticalScrollBar->maximum());
verticalScrollBar->setValue(newScrollValue);
return true;
}
return QWidget::eventFilter(obj, event);
}
void CPUDebugger::showEvent(QShowEvent* event) {
QWidget::showEvent(event);
enable();
}
void CPUDebugger::keyPressEvent(QKeyEvent* event) {
constexpr usize instructionSize = sizeof(u32);
if (event->key() == Qt::Key_Up) {
verticalScrollBar->setValue(verticalScrollBar->value() - instructionSize);
} else if (event->key() == Qt::Key_Down) {
verticalScrollBar->setValue(verticalScrollBar->value() + instructionSize);
} else {
QWidget::keyPressEvent(event);
}
}
void CPUDebugger::resizeEvent(QResizeEvent* event) {
QWidget::resizeEvent(event);
disabledOverlay->resize(event->size());
verticalScrollBar->setPageStep(getLinesInViewport(disasmListWidget));
update();
}

View file

@ -66,6 +66,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor"));
auto patchWindowAction = toolsMenu->addAction(tr("Open Patch Window")); auto patchWindowAction = toolsMenu->addAction(tr("Open Patch Window"));
auto shaderEditorAction = toolsMenu->addAction(tr("Open Shader Editor")); auto shaderEditorAction = toolsMenu->addAction(tr("Open Shader Editor"));
auto cpuDebuggerAction = toolsMenu->addAction(tr("Open CPU Debugger"));
auto threadDebuggerAction = toolsMenu->addAction(tr("Open Thread Debugger")); auto threadDebuggerAction = toolsMenu->addAction(tr("Open Thread Debugger"));
auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware")); auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware"));
@ -74,6 +75,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
connect(shaderEditorAction, &QAction::triggered, this, [this]() { shaderEditor->show(); }); connect(shaderEditorAction, &QAction::triggered, this, [this]() { shaderEditor->show(); });
connect(cheatsEditorAction, &QAction::triggered, this, [this]() { cheatsEditor->show(); }); connect(cheatsEditorAction, &QAction::triggered, this, [this]() { cheatsEditor->show(); });
connect(patchWindowAction, &QAction::triggered, this, [this]() { patchWindow->show(); }); connect(patchWindowAction, &QAction::triggered, this, [this]() { patchWindow->show(); });
connect(cpuDebuggerAction, &QAction::triggered, this, [this]() { cpuDebugger->show(); });
connect(threadDebuggerAction, &QAction::triggered, this, [this]() { threadDebugger->show(); }); connect(threadDebuggerAction, &QAction::triggered, this, [this]() { threadDebugger->show(); });
connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware);
@ -94,6 +96,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
luaEditor = new TextEditorWindow(this, "script.lua", ""); luaEditor = new TextEditorWindow(this, "script.lua", "");
shaderEditor = new ShaderEditorWindow(this, "shader.glsl", ""); shaderEditor = new ShaderEditorWindow(this, "shader.glsl", "");
threadDebugger = new ThreadDebugger(emu, this); threadDebugger = new ThreadDebugger(emu, this);
cpuDebugger = new CPUDebugger(emu, this);
shaderEditor->setEnable(emu->getRenderer()->supportsShaderReload()); shaderEditor->setEnable(emu->getRenderer()->supportsShaderReload());
if (shaderEditor->supported) { if (shaderEditor->supported) {