diff --git a/CMakeLists.txt b/CMakeLists.txt index a0de7271..f0acd3bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -314,7 +314,7 @@ else() message(FATAL_ERROR "Currently unsupported CPU architecture") endif() -add_subdirectory(third_party/teakra EXCLUDE_FROM_ALL) +add_subdirectory(third_party/teakra) add_subdirectory(third_party/fdk-aac) set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) @@ -686,8 +686,8 @@ set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERN ${AUDIO_SOURCE_FILES} ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) target_sources(AlberCore PRIVATE ${ALL_SOURCES}) -target_link_libraries(AlberCore PRIVATE dynarmic glad resources_console_fonts teakra fdk-aac) -target_link_libraries(AlberCore PUBLIC glad capstone fmt::fmt) +target_link_libraries(AlberCore PRIVATE dynarmic glad resources_console_fonts fdk-aac) +target_link_libraries(AlberCore PUBLIC glad capstone fmt::fmt teakra) if(ENABLE_DISCORD_RPC AND NOT ANDROID) target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") @@ -733,12 +733,13 @@ 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/cpu_debugger.cpp + src/panda_qt/thread_debugger.cpp src/panda_qt/cpu_debugger.cpp src/panda_qt/dsp_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/cpu_debugger.hpp include/panda_qt/disabled_widget_overlay.hpp + include/panda_qt/thread_debugger.hpp include/panda_qt/cpu_debugger.hpp include/panda_qt/dsp_debugger.hpp + include/panda_qt/disabled_widget_overlay.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/audio/dsp_core.hpp b/include/audio/dsp_core.hpp index f180e717..00c45527 100644 --- a/include/audio/dsp_core.hpp +++ b/include/audio/dsp_core.hpp @@ -63,6 +63,8 @@ namespace Audio { Samples& getSamples() { return sampleBuffer; } virtual void setAudioEnabled(bool enable) { audioEnabled = enable; } + + virtual u32 getPC() { return 0; } }; std::unique_ptr makeDSPCore(EmulatorConfig& config, Memory& mem, Scheduler& scheduler, DSPService& dspService); diff --git a/include/audio/teakra_core.hpp b/include/audio/teakra_core.hpp index a9d39414..5b4c2f8c 100644 --- a/include/audio/teakra_core.hpp +++ b/include/audio/teakra_core.hpp @@ -90,6 +90,7 @@ namespace Audio { void setAudioEnabled(bool enable) override; u8* getDspMemory() override { return teakra.GetDspMemory().data(); } + u32 getPC() override; u16 recvData(u32 regId) override { return teakra.RecvData(regId); } bool recvDataIsReady(u32 regId) override { return teakra.RecvDataIsReady(regId); } diff --git a/include/emulator.hpp b/include/emulator.hpp index 51242494..bed01937 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -126,6 +126,7 @@ class Emulator { Memory& getMemory() { return memory; } Kernel& getKernel() { return kernel; } Scheduler& getScheduler() { return scheduler; } + Audio::DSPCore* getDSP() { return dsp.get(); } EmulatorConfig& getConfig() { return config; } Cheats& getCheats() { return cheats; } diff --git a/include/panda_qt/dsp_debugger.hpp b/include/panda_qt/dsp_debugger.hpp new file mode 100644 index 00000000..10f3acb1 --- /dev/null +++ b/include/panda_qt/dsp_debugger.hpp @@ -0,0 +1,45 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "capstone.hpp" +#include "emulator.hpp" +#include "panda_qt/disabled_widget_overlay.hpp" + +class DSPDebugger : public QWidget { + Q_OBJECT + Emulator* emu; + + QListWidget* disasmListWidget; + QScrollBar* verticalScrollBar; + QPlainTextEdit* registerTextEdit; + QTimer* updateTimer; + QLineEdit* addressInput; + + DisabledWidgetOverlay* disabledOverlay; + + bool enabled = false; + bool followPC = false; + Common::CapstoneDisassembler disassembler; + + public: + DSPDebugger(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(); + void scrollToPC(); + + bool eventFilter(QObject* obj, QEvent* event) override; + void showEvent(QShowEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; +}; diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index c5032a87..80b4a7f1 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -18,6 +18,7 @@ #include "panda_qt/cheats_window.hpp" #include "panda_qt/config_window.hpp" #include "panda_qt/cpu_debugger.hpp" +#include "panda_qt/dsp_debugger.hpp" #include "panda_qt/patch_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/shader_editor.hpp" @@ -112,6 +113,7 @@ class MainWindow : public QMainWindow { PatchWindow* patchWindow; ShaderEditorWindow* shaderEditor; CPUDebugger* cpuDebugger; + DSPDebugger* dspDebugger; 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/core/audio/teakra_core.cpp b/src/core/audio/teakra_core.cpp index c9c92902..7b73d5cd 100644 --- a/src/core/audio/teakra_core.cpp +++ b/src/core/audio/teakra_core.cpp @@ -7,6 +7,10 @@ #include "services/dsp.hpp" +#undef Assert +#undef UNREACHABLE +#include "teakra/impl/register.h" + using namespace Audio; struct Dsp1 { @@ -343,3 +347,5 @@ void TeakraDSP::unloadComponent() { teakra.RecvData(2); running = false; } + +u32 TeakraDSP::getPC() { return teakra.GetRegisterState().pc; } \ No newline at end of file diff --git a/src/panda_qt/dsp_debugger.cpp b/src/panda_qt/dsp_debugger.cpp new file mode 100644 index 00000000..326c8a2b --- /dev/null +++ b/src/panda_qt/dsp_debugger.cpp @@ -0,0 +1,242 @@ +#include "panda_qt/dsp_debugger.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "teakra/disassembler.h" + +// TODO: Make this actually thread-safe by having it only work when paused +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}; +} + +DSPDebugger::DSPDebugger(Emulator* emulator, QWidget* parent) : emu(emulator), disassembler(CS_ARCH_ARM, CS_MODE_ARM), QWidget(parent, Qt::Window) { + setWindowTitle(tr("DSP debugger")); + resize(1000, 600); + + QGridLayout* gridLayout = new QGridLayout(this); + QHBoxLayout* horizontalLayout = new QHBoxLayout(); + + // Set up the top line widgets + QPushButton* goToAddressButton = new QPushButton(tr("Go to address"), this); + QPushButton* goToPCButton = new QPushButton(tr("Go to PC"), this); + QCheckBox* followPCCheckBox = new QCheckBox(tr("Follow PC"), this); + addressInput = new QLineEdit(this); + + horizontalLayout->addWidget(goToAddressButton); + horizontalLayout->addWidget(goToPCButton); + horizontalLayout->addWidget(followPCCheckBox); + horizontalLayout->addWidget(addressInput); + + followPCCheckBox->setChecked(followPC); + connect(followPCCheckBox, &QCheckBox::toggled, this, [&](bool checked) { followPC = checked; }); + + addressInput->setPlaceholderText(tr("Address to jump to")); + addressInput->setMaximumWidth(150); + + gridLayout->addLayout(horizontalLayout, 0, 0); + + // Disassembly list on the left, scrollbar in the middle, register view on the right + disasmListWidget = new QListWidget(this); + gridLayout->addWidget(disasmListWidget, 1, 0); + + verticalScrollBar = new QScrollBar(Qt::Vertical, this); + gridLayout->addWidget(verticalScrollBar, 1, 1); + + registerTextEdit = new QPlainTextEdit(this); + registerTextEdit->setEnabled(true); + registerTextEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + registerTextEdit->setMaximumWidth(800); + gridLayout->addWidget(registerTextEdit, 1, 2); + + // Setup overlay for when the widget is disabled + disabledOverlay = new DisabledWidgetOverlay(this, tr("Pause the emulator to use the DSP Debugger")); + disabledOverlay->resize(size()); // Fill the whole screen + disabledOverlay->raise(); + disabledOverlay->hide(); + + // Use a monospace font for the disassembly to align it + QFont mono_font = QFont("Courier New"); + mono_font.setStyleHint(QFont::Monospace); + disasmListWidget->setFont(mono_font); + registerTextEdit->setFont(mono_font); + + // Forward scrolling from the list widget to our scrollbar + disasmListWidget->installEventFilter(this); + + // Annoyingly, due to a Qt limitation we can't set it to U32_MAX + verticalScrollBar->setRange(0, std::numeric_limits::max()); + verticalScrollBar->setSingleStep(8); + verticalScrollBar->setPageStep(getLinesInViewport(disasmListWidget)); + verticalScrollBar->show(); + connect(verticalScrollBar, &QScrollBar::valueChanged, this, &DSPDebugger::updateDisasm); + registerTextEdit->setReadOnly(true); + + connect(goToPCButton, &QPushButton::clicked, this, [&]() { scrollToPC(); }); + + // We have a QTimer that triggers every 500ms to update our widget when it's active + updateTimer = new QTimer(this); + connect(updateTimer, &QTimer::timeout, this, &DSPDebugger::update); + + // Go to address when the "Go to address" button is pressed, or when we press enter inside the address input box + connect(goToAddressButton, &QPushButton::clicked, this, [&]() { + QString text = addressInput->text().trimmed(); + + bool validAddr = false; + u32 addr = text.toUInt(&validAddr, 16); // Parse address as hex + if (validAddr) { + verticalScrollBar->setValue(addr); + } else { + addressInput->setText(tr("Invalid hexadecimal address")); + } + }); + connect(addressInput, &QLineEdit::returnPressed, goToAddressButton, &QPushButton::click); + + disable(); + hide(); +} + +void DSPDebugger::enable() { + enabled = true; + + disabledOverlay->hide(); + scrollToPC(); + + // Update the widget every 500ms + updateTimer->start(500); + update(); +} + +void DSPDebugger::disable() { + enabled = false; + + updateTimer->stop(); + disabledOverlay->show(); +} + +void DSPDebugger::update() { + if (enabled) { + if (followPC) { + scrollToPC(); + } + + updateDisasm(); + updateRegisters(); + } +} + +void DSPDebugger::updateDisasm() { + int currentRow = disasmListWidget->currentRow(); + disasmListWidget->clear(); + + auto [firstLine, lineCount] = getVisibleLineRange(disasmListWidget, verticalScrollBar); + const u32 startPC = (firstLine + 1) & ~1; // Align PC to 2 bytes + + auto DSP = emu->getDSP(); + auto dspRam = DSP->getDspMemory(); + auto readByte = [&](u32 addr) { + if (addr >= 256_KB) return u8(0); + + return dspRam[addr]; + }; + + auto readWord = [&](u32 addr) { + u16 lsb = u16(readByte(addr)); + u16 msb = u16(readByte(addr + 1)); + return u16(lsb | (msb << 8)); + }; + + auto& mem = emu->getMemory(); + u32 pc = DSP->getPC(); + + std::string disassembly; + + for (u32 addr = startPC, instructionCount = 0; instructionCount < lineCount; instructionCount++) { + const u16 instruction = readWord(addr); + const bool needExpansion = Teakra::Disassembler::NeedExpansion(instruction); + + const u16 expansion = needExpansion ? readWord(addr + 2) : u16(0); + addr += needExpansion ? sizeof(u32) : sizeof(u16); + + std::string disassembly = Teakra::Disassembler::Do(instruction, expansion); + disassembly = fmt::format("{:08X} | {}", addr, disassembly); + + QListWidgetItem* item = new QListWidgetItem(QString::fromStdString(disassembly)); + if (addr == pc) { + item->setBackground(Qt::darkGreen); + } + disasmListWidget->addItem(item); + } + + disasmListWidget->setCurrentRow(currentRow); +} + +void DSPDebugger::scrollToPC() { + u32 pc = emu->getDSP()->getPC(); + verticalScrollBar->setValue(pc); +} + +void DSPDebugger::updateRegisters() { registerTextEdit->setPlainText(QString::fromStdString("")); } + +bool DSPDebugger::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 DSPDebugger::showEvent(QShowEvent* event) { + QWidget::showEvent(event); + + enable(); +} + +// Scroll 1 instruction up or down when the arrow keys are pressed and we're at the edge of the disassembly list +void DSPDebugger::keyPressEvent(QKeyEvent* event) { + constexpr usize instructionSize = sizeof(u16); + + 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 DSPDebugger::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 e5a2795d..e8971878 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -9,6 +9,7 @@ #include "cheats.hpp" #include "input_mappings.hpp" +#include "panda_qt/dsp_debugger.hpp" #include "sdl_sensors.hpp" #include "services/dsp.hpp" #include "version.hpp" @@ -67,6 +68,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) 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 dspDebuggerAction = toolsMenu->addAction(tr("Open DSP Debugger")); auto threadDebuggerAction = toolsMenu->addAction(tr("Open Thread Debugger")); auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware")); @@ -76,6 +78,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) 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(dspDebuggerAction, &QAction::triggered, this, [this]() { dspDebugger->show(); }); connect(threadDebuggerAction, &QAction::triggered, this, [this]() { threadDebugger->show(); }); connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); @@ -97,6 +100,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) shaderEditor = new ShaderEditorWindow(this, "shader.glsl", ""); threadDebugger = new ThreadDebugger(emu, this); cpuDebugger = new CPUDebugger(emu, this); + dspDebugger = new DSPDebugger(emu, this); shaderEditor->setEnable(emu->getRenderer()->supportsShaderReload()); if (shaderEditor->supported) {