From 9dc52577ea22510c9a043a76558c4d6562ce583e Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 7 Jul 2025 02:11:57 +0300 Subject: [PATCH] Qt: Initial CPU debugger widget implementation Co-Authored-By: liuk707 <62625900+liuk7071@users.noreply.github.com> --- CMakeLists.txt | 4 +- include/capstone.hpp | 2 +- include/emulator.hpp | 8 +- include/panda_qt/cpu_debugger.hpp | 38 +++ include/panda_qt/disabled_widget_overlay.hpp | 28 +++ include/panda_qt/main_window.hpp | 2 + src/panda_qt/cpu_debugger.cpp | 232 +++++++++++++++++++ src/panda_qt/main_window.cpp | 3 + 8 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 include/panda_qt/cpu_debugger.hpp create mode 100644 include/panda_qt/disabled_widget_overlay.hpp create mode 100644 src/panda_qt/cpu_debugger.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e50af1b8..bae21128 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 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/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 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/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}) diff --git a/include/capstone.hpp b/include/capstone.hpp index 32ca404f..14dd44b5 100644 --- a/include/capstone.hpp +++ b/include/capstone.hpp @@ -23,7 +23,7 @@ namespace Common { // pc: program counter of the instruction to disassemble // bytes: Byte representation of instruction // buffer: text buffer to output the disassembly too - usize disassemble(std::string& buffer, u32 pc, std::span bytes, u64 offset = 0) { + usize disassemble(std::string& buffer, u32 pc, std::span bytes, u64 offset = 0) { if (!initialized) { buffer = "Capstone was not properly initialized"; return 0; diff --git a/include/emulator.hpp b/include/emulator.hpp index 6b63a211..51242494 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -122,14 +122,16 @@ class Emulator { // Reloads some settings that require special handling, such as audio enable void reloadSettings(); + CPU& getCPU() { return cpu; } + Memory& getMemory() { return memory; } + Kernel& getKernel() { return kernel; } + Scheduler& getScheduler() { return scheduler; } + EmulatorConfig& getConfig() { return config; } Cheats& getCheats() { return cheats; } ServiceManager& getServiceManager() { return kernel.getServiceManager(); } LuaManager& getLua() { return lua; } - Scheduler& getScheduler() { return scheduler; } - Memory& getMemory() { return memory; } AudioDeviceInterface& getAudioDevice() { return audioDevice; } - Kernel& getKernel() { return kernel; } RendererType getRendererType() const { return config.rendererType; } Renderer* getRenderer() { return gpu.getRenderer(); } diff --git a/include/panda_qt/cpu_debugger.hpp b/include/panda_qt/cpu_debugger.hpp new file mode 100644 index 00000000..f6a6aa95 --- /dev/null +++ b/include/panda_qt/cpu_debugger.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include +#include + +#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); +}; diff --git a/include/panda_qt/disabled_widget_overlay.hpp b/include/panda_qt/disabled_widget_overlay.hpp new file mode 100644 index 00000000..12e7c26e --- /dev/null +++ b/include/panda_qt/disabled_widget_overlay.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +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); + } +}; \ No newline at end of file diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 715f73fb..c5032a87 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -17,6 +17,7 @@ #include "panda_qt/about_window.hpp" #include "panda_qt/cheats_window.hpp" #include "panda_qt/config_window.hpp" +#include "panda_qt/cpu_debugger.hpp" #include "panda_qt/patch_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/shader_editor.hpp" @@ -110,6 +111,7 @@ class MainWindow : public QMainWindow { TextEditorWindow* luaEditor; PatchWindow* patchWindow; ShaderEditorWindow* shaderEditor; + CPUDebugger* cpuDebugger; ThreadDebugger* threadDebugger; // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible diff --git a/src/panda_qt/cpu_debugger.cpp b/src/panda_qt/cpu_debugger.cpp new file mode 100644 index 00000000..37c2693c --- /dev/null +++ b/src/panda_qt/cpu_debugger.cpp @@ -0,0 +1,232 @@ +#include "panda_qt/cpu_debugger.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#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 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 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 gprs = cpu.regs(); + const std::span 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(fprs[i])); + } + for (int i = 10; i < 32; i++) { + text += fmt::format("f{:01d}: {:f}\n", i, Helpers::bit_cast(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(); +} \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index d29b9875..e5a2795d 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -66,6 +66,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); auto patchWindowAction = toolsMenu->addAction(tr("Open Patch Window")); 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 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(cheatsEditorAction, &QAction::triggered, this, [this]() { cheatsEditor->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(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); @@ -94,6 +96,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) luaEditor = new TextEditorWindow(this, "script.lua", ""); shaderEditor = new ShaderEditorWindow(this, "shader.glsl", ""); threadDebugger = new ThreadDebugger(emu, this); + cpuDebugger = new CPUDebugger(emu, this); shaderEditor->setEnable(emu->getRenderer()->supportsShaderReload()); if (shaderEditor->supported) {