diff --git a/README.md b/README.md index 144e4e6bf..08d92e04d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c ## Latest News +- 2020/12/16: Integrated CPU debugger added in Qt frontend. - 2020/12/13: Button layout for the touchscreen controller in the Android version can now be customized. - 2020/12/10: Translation support added for Android version. Currently Brazillian Portuguese, Italian, and Dutch are available. - 2020/11/27: Cover support added for game list in Android version. Procedure is the same as the desktop version, except you should place cover images in `/duckstation/covers` (see [Adding Game Covers](https://github.com/stenzek/duckstation/wiki/Adding-Game-Covers)). diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index a8a5aeff3..d817340da 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -29,6 +29,11 @@ set(SRCS consolesettingswidget.ui controllersettingswidget.cpp controllersettingswidget.h + debuggermodels.cpp + debuggermodels.h + debuggerwindow.cpp + debuggerwindow.h + debuggerwindow.ui displaysettingswidget.cpp displaysettingswidget.h displaysettingswidget.ui @@ -68,6 +73,8 @@ set(SRCS memorycardeditordialog.ui memorycardsettingswidget.cpp memorycardsettingswidget.h + memoryviewwidget.cpp + memoryviewwidget.h postprocessingchainconfigwidget.cpp postprocessingchainconfigwidget.h postprocessingchainconfigwidget.ui diff --git a/src/duckstation-qt/debuggermodels.cpp b/src/duckstation-qt/debuggermodels.cpp new file mode 100644 index 000000000..651e200cc --- /dev/null +++ b/src/duckstation-qt/debuggermodels.cpp @@ -0,0 +1,417 @@ +#include "debuggermodels.h" +#include "common/log.h" +#include "core/cpu_core.h" +#include "core/cpu_core_private.h" +#include "core/cpu_disasm.h" +#include +#include +#include +#include +Log_SetChannel(DebuggerModels); + +static constexpr int NUM_COLUMNS = 5; +static constexpr int STACK_RANGE = 128; +static constexpr u32 STACK_VALUE_SIZE = sizeof(u32); + +DebuggerCodeModel::DebuggerCodeModel(QObject* parent /*= nullptr*/) : QAbstractTableModel(parent) +{ + resetCodeView(0); + m_pc_pixmap = QIcon(QStringLiteral(":/icons/debug-pc.png")).pixmap(QSize(12, 12)); + m_breakpoint_pixmap = QIcon(QStringLiteral(":/icons/media-record.png")).pixmap(QSize(12, 12)); +} + +DebuggerCodeModel::~DebuggerCodeModel() {} + +int DebuggerCodeModel::rowCount(const QModelIndex& parent /*= QModelIndex()*/) const +{ + return static_cast((m_code_region_end - m_code_region_start) / CPU::INSTRUCTION_SIZE); +} + +int DebuggerCodeModel::columnCount(const QModelIndex& parent /*= QModelIndex()*/) const +{ + return NUM_COLUMNS; +} + +int DebuggerCodeModel::getRowForAddress(VirtualMemoryAddress address) const +{ + return static_cast((address - m_code_region_start) / CPU::INSTRUCTION_SIZE); +} + +VirtualMemoryAddress DebuggerCodeModel::getAddressForRow(int row) const +{ + return m_code_region_start + (static_cast(row) * CPU::INSTRUCTION_SIZE); +} + +VirtualMemoryAddress DebuggerCodeModel::getAddressForIndex(QModelIndex index) const +{ + return getAddressForRow(index.row()); +} + +int DebuggerCodeModel::getRowForPC() const +{ + return getRowForAddress(m_last_pc); +} + +QVariant DebuggerCodeModel::data(const QModelIndex& index, int role /*= Qt::DisplayRole*/) const +{ + if (index.column() < 0 || index.column() >= NUM_COLUMNS) + return QVariant(); + + if (role == Qt::DisplayRole) + { + const VirtualMemoryAddress address = getAddressForRow(index.row()); + switch (index.column()) + { + case 0: + // breakpoint + return QVariant(); + + case 1: + { + // Address + return QVariant(QString::asprintf("0x%08X", address)); + } + + case 2: + { + // Bytes + u32 instruction_bits; + if (!CPU::SafeReadInstruction(address, &instruction_bits)) + return QStringLiteral(""); + + return QString::asprintf("%08X", instruction_bits); + } + + case 3: + { + // Instruction + u32 instruction_bits; + if (!CPU::SafeReadInstruction(address, &instruction_bits)) + return QStringLiteral(""); + + Log_DevPrintf("Disassemble %08X", address); + SmallString str; + CPU::DisassembleInstruction(&str, address, instruction_bits); + return QString::fromUtf8(str.GetCharArray(), static_cast(str.GetLength())); + } + + case 4: + { + // Comment + if (address != m_last_pc) + return QVariant(); + + u32 instruction_bits; + if (!CPU::SafeReadInstruction(address, &instruction_bits)) + return QStringLiteral(""); + + TinyString str; + CPU::DisassembleInstructionComment(&str, address, instruction_bits, &CPU::g_state.regs); + return QString::fromUtf8(str.GetCharArray(), static_cast(str.GetLength())); + } + + default: + return QVariant(); + } + } + else if (role == Qt::DecorationRole) + { + if (index.column() == 0) + { + // breakpoint + const VirtualMemoryAddress address = getAddressForRow(index.row()); + if (m_last_pc == address) + return m_pc_pixmap; + else if (hasBreakpointAtAddress(address)) + return m_breakpoint_pixmap; + } + + return QVariant(); + } + else if (role == Qt::BackgroundRole) + { + const VirtualMemoryAddress address = getAddressForRow(index.row()); + + // breakpoint + if (hasBreakpointAtAddress(address)) + return QVariant(QColor(171, 97, 107)); + + // if (address == m_last_pc) + // return QApplication::palette().toolTipBase(); + if (address == m_last_pc) + return QColor(100, 100, 0); + else + return QVariant(); + } + else if (role == Qt::ForegroundRole) + { + const VirtualMemoryAddress address = getAddressForRow(index.row()); + + // if (address == m_last_pc) + // return QApplication::palette().toolTipText(); + if (address == m_last_pc || hasBreakpointAtAddress(address)) + return QColor(Qt::white); + else + return QVariant(); + } + else + { + return QVariant(); + } +} + +QVariant DebuggerCodeModel::headerData(int section, Qt::Orientation orientation, int role /*= Qt::DisplayRole*/) const +{ + if (orientation != Qt::Horizontal) + return QVariant(); + + if (role != Qt::DisplayRole) + return QVariant(); + + static const char* header_names[] = {"", "Address", "Bytes", "Instruction", "Comment"}; + if (section < 0 || section >= countof(header_names)) + return QVariant(); + + return header_names[section]; +} + +bool DebuggerCodeModel::updateRegion(VirtualMemoryAddress address) +{ + CPU::Segment segment = CPU::GetSegmentForAddress(address); + std::optional region = Bus::GetMemoryRegionForAddress(CPU::VirtualAddressToPhysical(address)); + if (!region.has_value() || (address >= m_code_region_start && address < m_code_region_end)) + return false; + + static constexpr unsigned NUM_INSTRUCTIONS_BEFORE = 4096; + static constexpr unsigned NUM_INSTRUCTIONS_AFTER = 4096; + static constexpr unsigned NUM_BYTES_BEFORE = NUM_INSTRUCTIONS_BEFORE * sizeof(CPU::Instruction); + static constexpr unsigned NUM_BYTES_AFTER = NUM_INSTRUCTIONS_AFTER * sizeof(CPU::Instruction); + + const VirtualMemoryAddress start_address = + CPU::PhysicalAddressToVirtual(Bus::GetMemoryRegionStart(region.value()), segment); + const VirtualMemoryAddress end_address = + CPU::PhysicalAddressToVirtual(Bus::GetMemoryRegionEnd(region.value()), segment); + + beginResetModel(); + m_code_region_start = ((address - start_address) < NUM_BYTES_BEFORE) ? start_address : (address - NUM_BYTES_BEFORE); + m_code_region_end = ((end_address - address) < NUM_BYTES_AFTER) ? end_address : (address + NUM_BYTES_AFTER); + m_current_segment = segment; + m_current_code_region = region.value(); + endResetModel(); + return true; +} + +bool DebuggerCodeModel::emitDataChangedForAddress(VirtualMemoryAddress address) +{ + CPU::Segment segment = CPU::GetSegmentForAddress(address); + std::optional region = Bus::GetMemoryRegionForAddress(CPU::VirtualAddressToPhysical(address)); + if (!region.has_value() || segment != m_current_segment || region != m_current_code_region) + return false; + + const int row = getRowForAddress(address); + emit dataChanged(index(row, 0), index(row, NUM_COLUMNS - 1)); + return true; +} + +bool DebuggerCodeModel::hasBreakpointAtAddress(VirtualMemoryAddress address) const +{ + return std::find(m_breakpoints.begin(), m_breakpoints.end(), address) != m_breakpoints.end(); +} + +void DebuggerCodeModel::resetCodeView(VirtualMemoryAddress start_address) +{ + updateRegion(start_address); +} + +void DebuggerCodeModel::setPC(VirtualMemoryAddress pc) +{ + const VirtualMemoryAddress prev_pc = m_last_pc; + + m_last_pc = pc; + if (!updateRegion(pc)) + { + emitDataChangedForAddress(prev_pc); + emitDataChangedForAddress(pc); + } +} + +void DebuggerCodeModel::ensureAddressVisible(VirtualMemoryAddress address) +{ + updateRegion(address); +} + +void DebuggerCodeModel::setBreakpointList(std::vector bps) +{ + clearBreakpoints(); + + m_breakpoints = std::move(bps); + for (VirtualMemoryAddress bp : m_breakpoints) + emitDataChangedForAddress(bp); +} + +void DebuggerCodeModel::clearBreakpoints() +{ + std::vector old_bps(std::move(m_breakpoints)); + + for (VirtualMemoryAddress old_bp : old_bps) + emitDataChangedForAddress(old_bp); +} + +void DebuggerCodeModel::setBreakpointState(VirtualMemoryAddress address, bool enabled) +{ + if (enabled) + { + if (std::find(m_breakpoints.begin(), m_breakpoints.end(), address) != m_breakpoints.end()) + return; + + m_breakpoints.push_back(address); + emitDataChangedForAddress(address); + } + else + { + auto it = std::find(m_breakpoints.begin(), m_breakpoints.end(), address); + if (it == m_breakpoints.end()) + return; + + m_breakpoints.erase(it); + emitDataChangedForAddress(address); + } +} + +DebuggerRegistersModel::DebuggerRegistersModel(QObject* parent /*= nullptr*/) : QAbstractListModel(parent) {} + +DebuggerRegistersModel::~DebuggerRegistersModel() {} + +int DebuggerRegistersModel::rowCount(const QModelIndex& parent /*= QModelIndex()*/) const +{ + return static_cast(CPU::Reg::count); +} + +int DebuggerRegistersModel::columnCount(const QModelIndex& parent /*= QModelIndex()*/) const +{ + return 2; +} + +QVariant DebuggerRegistersModel::data(const QModelIndex& index, int role /*= Qt::DisplayRole*/) const +{ + u32 reg_index = static_cast(index.row()); + if (reg_index >= static_cast(CPU::Reg::count)) + return QVariant(); + + if (index.column() < 0 || index.column() > 1) + return QVariant(); + + switch (index.column()) + { + case 0: // address + { + if (role == Qt::DisplayRole) + return QString::fromUtf8(CPU::GetRegName(static_cast(reg_index))); + } + break; + + case 1: // data + { + if (role == Qt::DisplayRole) + { + return QString::asprintf("0x%08X", CPU::g_state.regs.r[reg_index]); + } + else if (role == Qt::ForegroundRole) + { + if (CPU::g_state.regs.r[reg_index] != m_old_reg_values[reg_index]) + return QColor(255, 50, 50); + } + } + break; + + default: + break; + } + + return QVariant(); +} + +QVariant DebuggerRegistersModel::headerData(int section, Qt::Orientation orientation, + int role /*= Qt::DisplayRole*/) const +{ + if (orientation != Qt::Horizontal) + return QVariant(); + + if (role != Qt::DisplayRole) + return QVariant(); + + static const char* header_names[] = {"Register", "Value"}; + if (section < 0 || section >= countof(header_names)) + return QVariant(); + + return header_names[section]; +} + +void DebuggerRegistersModel::invalidateView() +{ + beginResetModel(); + endResetModel(); +} + +void DebuggerRegistersModel::saveCurrentValues() +{ + for (u32 i = 0; i < static_cast(CPU::Reg::count); i++) + m_old_reg_values[i] = CPU::g_state.regs.r[i]; +} + +DebuggerStackModel::DebuggerStackModel(QObject* parent /*= nullptr*/) : QAbstractListModel(parent) {} + +DebuggerStackModel::~DebuggerStackModel() {} + +int DebuggerStackModel::rowCount(const QModelIndex& parent /*= QModelIndex()*/) const +{ + return STACK_RANGE * 2; +} + +int DebuggerStackModel::columnCount(const QModelIndex& parent /*= QModelIndex()*/) const +{ + return 2; +} + +QVariant DebuggerStackModel::data(const QModelIndex& index, int role /*= Qt::DisplayRole*/) const +{ + if (index.column() < 0 || index.column() > 1) + return QVariant(); + + if (role != Qt::DisplayRole) + return QVariant(); + + const u32 sp = CPU::g_state.regs.sp; + const VirtualMemoryAddress address = + (sp - static_cast(STACK_RANGE * STACK_VALUE_SIZE)) + static_cast(index.row()) * STACK_VALUE_SIZE; + + if (index.column() == 0) + return QString::asprintf("0x%08X", address); + + u32 value; + if (!CPU::SafeReadMemoryWord(address, &value)) + return QStringLiteral(""); + + return QString::asprintf("0x%08X", ZeroExtend32(value)); +} + +QVariant DebuggerStackModel::headerData(int section, Qt::Orientation orientation, int role /*= Qt::DisplayRole*/) const +{ + if (orientation != Qt::Horizontal) + return QVariant(); + + if (role != Qt::DisplayRole) + return QVariant(); + + static const char* header_names[] = {"Address", "Value"}; + if (section < 0 || section >= countof(header_names)) + return QVariant(); + + return header_names[section]; +} + +void DebuggerStackModel::invalidateView() +{ + beginResetModel(); + endResetModel(); +} diff --git a/src/duckstation-qt/debuggermodels.h b/src/duckstation-qt/debuggermodels.h new file mode 100644 index 000000000..fcbbb5f7d --- /dev/null +++ b/src/duckstation-qt/debuggermodels.h @@ -0,0 +1,84 @@ +#pragma once +#include "core/bus.h" +#include "core/cpu_types.h" +#include +#include +#include +#include + +class DebuggerCodeModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + DebuggerCodeModel(QObject* parent = nullptr); + virtual ~DebuggerCodeModel(); + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Returns the row for this instruction pointer + void resetCodeView(VirtualMemoryAddress start_address); + int getRowForAddress(VirtualMemoryAddress address) const; + int getRowForPC() const; + VirtualMemoryAddress getAddressForRow(int row) const; + VirtualMemoryAddress getAddressForIndex(QModelIndex index) const; + void setPC(VirtualMemoryAddress pc); + void ensureAddressVisible(VirtualMemoryAddress address); + void setBreakpointList(std::vector bps); + void setBreakpointState(VirtualMemoryAddress address, bool enabled); + void clearBreakpoints(); + +private: + bool updateRegion(VirtualMemoryAddress address); + bool emitDataChangedForAddress(VirtualMemoryAddress address); + bool hasBreakpointAtAddress(VirtualMemoryAddress address) const; + + Bus::MemoryRegion m_current_code_region = Bus::MemoryRegion::Count; + CPU::Segment m_current_segment = CPU::Segment::KUSEG; + VirtualMemoryAddress m_code_region_start = 0; + VirtualMemoryAddress m_code_region_end = 0; + VirtualMemoryAddress m_last_pc = 0; + std::vector m_breakpoints; + + QPixmap m_pc_pixmap; + QPixmap m_breakpoint_pixmap; +}; + +class DebuggerRegistersModel : public QAbstractListModel +{ + Q_OBJECT + +public: + DebuggerRegistersModel(QObject* parent = nullptr); + virtual ~DebuggerRegistersModel(); + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void invalidateView(); + void saveCurrentValues(); + +private: + u32 m_old_reg_values[static_cast(CPU::Reg::count)] = {}; +}; + +class DebuggerStackModel : public QAbstractListModel +{ + Q_OBJECT + +public: + DebuggerStackModel(QObject* parent = nullptr); + virtual ~DebuggerStackModel(); + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void invalidateView(); +}; diff --git a/src/duckstation-qt/debuggerwindow.cpp b/src/duckstation-qt/debuggerwindow.cpp new file mode 100644 index 000000000..90017665c --- /dev/null +++ b/src/duckstation-qt/debuggerwindow.cpp @@ -0,0 +1,597 @@ +#include "debuggerwindow.h" +#include "core/cpu_core_private.h" +#include "debuggermodels.h" +#include "qthostinterface.h" +#include +#include +#include +#include +#include + +DebuggerWindow::DebuggerWindow(QWidget* parent /* = nullptr */) + : QMainWindow(parent), m_active_memory_region(Bus::MemoryRegion::Count) +{ + m_ui.setupUi(this); + setupAdditionalUi(); + connectSignals(); + createModels(); + setMemoryViewRegion(Bus::MemoryRegion::RAM); + setUIEnabled(false); +} + +DebuggerWindow::~DebuggerWindow() {} + +void DebuggerWindow::onEmulationPaused(bool paused) +{ + if (paused) + { + setUIEnabled(true); + refreshAll(); + refreshBreakpointList(); + } + else + { + setUIEnabled(false); + } + + { + QSignalBlocker sb(m_ui.actionPause); + m_ui.actionPause->setChecked(paused); + } +} + +void DebuggerWindow::onDebuggerMessageReported(const QString& message) +{ + m_ui.statusbar->showMessage(message, 0); +} + +void DebuggerWindow::refreshAll() +{ + m_registers_model->invalidateView(); + m_stack_model->invalidateView(); + m_ui.memoryView->repaint(); + + m_code_model->setPC(CPU::g_state.regs.pc); + scrollToPC(); +} + +void DebuggerWindow::scrollToPC() +{ + return scrollToCodeAddress(CPU::g_state.regs.pc); +} + +void DebuggerWindow::scrollToCodeAddress(VirtualMemoryAddress address) +{ + m_code_model->ensureAddressVisible(address); + + int row = m_code_model->getRowForAddress(address); + if (row >= 0) + { + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); + m_ui.codeView->scrollTo(m_code_model->index(row, 0)); + } +} + +void DebuggerWindow::onPauseActionToggled(bool paused) +{ + if (!paused) + { + m_registers_model->saveCurrentValues(); + setUIEnabled(false); + } + + QtHostInterface::GetInstance()->pauseSystem(paused); +} + +void DebuggerWindow::onRunToCursorTriggered() +{ + std::optional addr = getSelectedCodeAddress(); + if (!addr.has_value()) + { + QMessageBox::critical(this, windowTitle(), tr("No address selected.")); + return; + } + + CPU::AddBreakpoint(addr.value(), true, true); + QtHostInterface::GetInstance()->pauseSystem(false); +} + +void DebuggerWindow::onGoToPCTriggered() +{ + scrollToPC(); +} + +void DebuggerWindow::onGoToAddressTriggered() +{ + std::optional address = promptForAddress(tr("Enter code address:")); + if (!address.has_value()) + return; + + scrollToCodeAddress(address.value()); +} + +void DebuggerWindow::onDumpAddressTriggered() +{ + std::optional address = promptForAddress(tr("Enter memory address:")); + if (!address.has_value()) + return; + + scrollToMemoryAddress(address.value()); +} + +void DebuggerWindow::onFollowAddressTriggered() +{ + // +} + +void DebuggerWindow::onAddBreakpointTriggered() +{ + std::optional address = promptForAddress(tr("Enter code address:")); + if (!address.has_value()) + return; + + if (CPU::HasBreakpointAtAddress(address.value())) + { + QMessageBox::critical(this, windowTitle(), tr("A breakpoint already exists at this address.")); + return; + } + + toggleBreakpoint(address.value()); +} + +void DebuggerWindow::onToggleBreakpointTriggered() +{ + std::optional address = getSelectedCodeAddress(); + if (!address.has_value()) + return; + + toggleBreakpoint(address.value()); +} + +void DebuggerWindow::onClearBreakpointsTriggered() +{ + clearBreakpoints(); +} + +void DebuggerWindow::onStepIntoActionTriggered() +{ + Assert(System::IsPaused()); + m_registers_model->saveCurrentValues(); + QtHostInterface::GetInstance()->singleStepCPU(); + refreshAll(); +} + +void DebuggerWindow::onStepOverActionTriggered() +{ + Assert(System::IsPaused()); + if (!CPU::AddStepOverBreakpoint()) + { + onStepIntoActionTriggered(); + return; + } + + // unpause to let it run to the breakpoint + m_registers_model->saveCurrentValues(); + QtHostInterface::GetInstance()->pauseSystem(false); +} + +void DebuggerWindow::onStepOutActionTriggered() +{ + Assert(System::IsPaused()); + if (!CPU::AddStepOutBreakpoint()) + { + QMessageBox::critical(this, tr("Debugger"), tr("Failed to add step-out breakpoint, are you in a valid function?")); + return; + } + + // unpause to let it run to the breakpoint + m_registers_model->saveCurrentValues(); + QtHostInterface::GetInstance()->pauseSystem(false); +} + +void DebuggerWindow::onCodeViewItemActivated(QModelIndex index) +{ + if (!index.isValid()) + return; + + const VirtualMemoryAddress address = m_code_model->getAddressForIndex(index); + switch (index.column()) + { + case 0: // breakpoint + case 3: // disassembly + toggleBreakpoint(address); + break; + + case 1: // address + case 2: // bytes + scrollToMemoryAddress(address); + break; + + case 4: // comment + tryFollowLoadStore(address); + break; + } +} + +void DebuggerWindow::onMemorySearchTriggered() +{ + m_ui.memoryView->clearHighlightRange(); + + const QString pattern_str = m_ui.memorySearchString->text(); + if (pattern_str.isEmpty()) + return; + + std::vector pattern; + std::vector mask; + u8 spattern = 0; + u8 smask = 0; + bool msb = false; + + pattern.reserve(static_cast(pattern_str.length()) / 2); + mask.reserve(static_cast(pattern_str.length()) / 2); + + for (int i = 0; i < pattern_str.length(); i++) + { + const QChar ch = pattern_str[i]; + if (ch == ' ') + continue; + + if (ch == '?') + { + spattern = (spattern << 4); + smask = (smask << 4); + } + else if (ch.isDigit()) + { + spattern = (spattern << 4) | static_cast(ch.digitValue()); + smask = (smask << 4) | 0xF; + } + else if (ch.unicode() >= 'a' && ch.unicode() <= 'f') + { + spattern = (spattern << 4) | (0xA + static_cast(ch.unicode() - 'a')); + smask = (smask << 4) | 0xF; + } + else if (ch.unicode() >= 'A' && ch.unicode() <= 'F') + { + spattern = (spattern << 4) | (0xA + static_cast(ch.unicode() - 'A')); + smask = (smask << 4) | 0xF; + } + else + { + QMessageBox::critical(this, windowTitle(), + tr("Invalid search pattern. It should contain hex digits or question marks.")); + return; + } + + if (msb) + { + pattern.push_back(spattern); + mask.push_back(smask); + spattern = 0; + smask = 0; + } + + msb = !msb; + } + + if (msb) + { + // partial byte on the end + spattern = (spattern << 4); + smask = (smask << 4); + pattern.push_back(spattern); + mask.push_back(smask); + } + + if (pattern.empty()) + { + QMessageBox::critical(this, windowTitle(), + tr("Invalid search pattern. It should contain hex digits or question marks.")); + return; + } + + std::optional found_address = + Bus::SearchMemory(m_next_memory_search_address, pattern.data(), mask.data(), static_cast(pattern.size())); + bool wrapped_around = false; + if (!found_address.has_value()) + { + found_address = Bus::SearchMemory(0, pattern.data(), mask.data(), static_cast(pattern.size())); + if (!found_address.has_value()) + { + m_ui.statusbar->showMessage(tr("Pattern not found.")); + return; + } + + wrapped_around = true; + } + + m_next_memory_search_address = found_address.value() + 1; + if (scrollToMemoryAddress(found_address.value())) + { + const size_t highlight_offset = found_address.value() - m_ui.memoryView->addressOffset(); + m_ui.memoryView->setHighlightRange(highlight_offset, highlight_offset + pattern.size()); + } + + if (wrapped_around) + { + m_ui.statusbar->showMessage(tr("Pattern found at 0x%1 (passed the end of memory).") + .arg(static_cast(found_address.value()), 8, 10, static_cast('0'))); + } + else + { + m_ui.statusbar->showMessage( + tr("Pattern found at 0x%1.").arg(static_cast(found_address.value()), 8, 10, static_cast('0'))); + } +} + +void DebuggerWindow::onMemorySearchStringChanged(const QString&) +{ + m_next_memory_search_address = 0; +} + +void DebuggerWindow::closeEvent(QCloseEvent* event) +{ + QMainWindow::closeEvent(event); + QtHostInterface::GetInstance()->pauseSystem(true, true); + CPU::ClearBreakpoints(); + QtHostInterface::GetInstance()->pauseSystem(false); + emit closed(); +} + +void DebuggerWindow::setupAdditionalUi() +{ +#ifdef WIN32 + QFont fixedFont; + fixedFont.setFamily(QStringLiteral("Consolas")); + fixedFont.setFixedPitch(true); + fixedFont.setStyleHint(QFont::TypeWriter); + fixedFont.setPointSize(10); +#else + const QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); +#endif + m_ui.codeView->setFont(fixedFont); + m_ui.registerView->setFont(fixedFont); + m_ui.memoryView->setFont(fixedFont); + m_ui.stackView->setFont(fixedFont); + + setCentralWidget(nullptr); + delete m_ui.centralwidget; +} + +void DebuggerWindow::connectSignals() +{ + QtHostInterface* hi = QtHostInterface::GetInstance(); + connect(hi, &QtHostInterface::emulationPaused, this, &DebuggerWindow::onEmulationPaused); + connect(hi, &QtHostInterface::debuggerMessageReported, this, &DebuggerWindow::onDebuggerMessageReported); + + connect(m_ui.actionPause, &QAction::toggled, this, &DebuggerWindow::onPauseActionToggled); + connect(m_ui.actionRunToCursor, &QAction::triggered, this, &DebuggerWindow::onRunToCursorTriggered); + connect(m_ui.actionGoToPC, &QAction::triggered, this, &DebuggerWindow::onGoToPCTriggered); + connect(m_ui.actionGoToAddress, &QAction::triggered, this, &DebuggerWindow::onGoToAddressTriggered); + connect(m_ui.actionDumpAddress, &QAction::triggered, this, &DebuggerWindow::onDumpAddressTriggered); + connect(m_ui.actionStepInto, &QAction::triggered, this, &DebuggerWindow::onStepIntoActionTriggered); + connect(m_ui.actionStepOver, &QAction::triggered, this, &DebuggerWindow::onStepOverActionTriggered); + connect(m_ui.actionStepOut, &QAction::triggered, this, &DebuggerWindow::onStepOutActionTriggered); + connect(m_ui.actionAddBreakpoint, &QAction::triggered, this, &DebuggerWindow::onAddBreakpointTriggered); + connect(m_ui.actionToggleBreakpoint, &QAction::triggered, this, &DebuggerWindow::onToggleBreakpointTriggered); + connect(m_ui.actionClearBreakpoints, &QAction::triggered, this, &DebuggerWindow::onClearBreakpointsTriggered); + connect(m_ui.actionClose, &QAction::triggered, this, &DebuggerWindow::close); + + connect(m_ui.codeView, &QTreeView::activated, this, &DebuggerWindow::onCodeViewItemActivated); + + connect(m_ui.memoryRegionRAM, &QRadioButton::clicked, [this]() { setMemoryViewRegion(Bus::MemoryRegion::RAM); }); + connect(m_ui.memoryRegionEXP1, &QRadioButton::clicked, [this]() { setMemoryViewRegion(Bus::MemoryRegion::EXP1); }); + connect(m_ui.memoryRegionScratchpad, &QRadioButton::clicked, + [this]() { setMemoryViewRegion(Bus::MemoryRegion::Scratchpad); }); + connect(m_ui.memoryRegionBIOS, &QRadioButton::clicked, [this]() { setMemoryViewRegion(Bus::MemoryRegion::BIOS); }); + + connect(m_ui.memorySearch, &QPushButton::clicked, this, &DebuggerWindow::onMemorySearchTriggered); + connect(m_ui.memorySearchString, &QLineEdit::textChanged, this, &DebuggerWindow::onMemorySearchStringChanged); +} + +void DebuggerWindow::disconnectSignals() +{ + QtHostInterface* hi = QtHostInterface::GetInstance(); + hi->disconnect(this); +} + +void DebuggerWindow::createModels() +{ + m_code_model = std::make_unique(); + m_ui.codeView->setModel(m_code_model.get()); + + // set default column width in code view + m_ui.codeView->setColumnWidth(0, 40); + m_ui.codeView->setColumnWidth(1, 80); + m_ui.codeView->setColumnWidth(2, 80); + m_ui.codeView->setColumnWidth(3, 250); + m_ui.codeView->setColumnWidth(4, m_ui.codeView->width() - (40 + 80 + 80 + 250)); + + m_registers_model = std::make_unique(); + m_ui.registerView->setModel(m_registers_model.get()); + // m_ui->registerView->resizeRowsToContents(); + + m_stack_model = std::make_unique(); + m_ui.stackView->setModel(m_stack_model.get()); + + m_ui.breakpointsWidget->setColumnWidth(0, 50); + m_ui.breakpointsWidget->setColumnWidth(1, 80); + m_ui.breakpointsWidget->setColumnWidth(2, 40); + m_ui.breakpointsWidget->setRootIsDecorated(false); +} + +void DebuggerWindow::setUIEnabled(bool enabled) +{ + // Disable all UI elements that depend on execution state + m_ui.codeView->setEnabled(enabled); + m_ui.registerView->setEnabled(enabled); + m_ui.stackView->setEnabled(enabled); + m_ui.memoryView->setEnabled(enabled); + m_ui.actionRunToCursor->setEnabled(enabled); + m_ui.actionAddBreakpoint->setEnabled(enabled); + m_ui.actionToggleBreakpoint->setEnabled(enabled); + m_ui.actionClearBreakpoints->setEnabled(enabled); + m_ui.actionDumpAddress->setEnabled(enabled); + m_ui.actionStepInto->setEnabled(enabled); + m_ui.actionStepOver->setEnabled(enabled); + m_ui.actionStepOut->setEnabled(enabled); + m_ui.actionGoToAddress->setEnabled(enabled); + m_ui.actionGoToPC->setEnabled(enabled); + m_ui.memoryRegionRAM->setEnabled(enabled); + m_ui.memoryRegionEXP1->setEnabled(enabled); + m_ui.memoryRegionScratchpad->setEnabled(enabled); + m_ui.memoryRegionBIOS->setEnabled(enabled); +} + +void DebuggerWindow::setMemoryViewRegion(Bus::MemoryRegion region) +{ + if (m_active_memory_region == region) + return; + + m_active_memory_region = region; + + switch (region) + { + case Bus::MemoryRegion::RAM: + case Bus::MemoryRegion::RAMMirror1: + case Bus::MemoryRegion::RAMMirror2: + case Bus::MemoryRegion::RAMMirror3: + m_ui.memoryView->setData(Bus::GetMemoryRegionStart(region), Bus::g_ram, Bus::RAM_SIZE); + break; + + case Bus::MemoryRegion::Scratchpad: + m_ui.memoryView->setData(CPU::DCACHE_LOCATION, CPU::g_state.dcache.data(), CPU::DCACHE_SIZE); + break; + + case Bus::MemoryRegion::BIOS: + m_ui.memoryView->setData(Bus::BIOS_BASE, Bus::g_bios, Bus::BIOS_SIZE); + break; + + case Bus::MemoryRegion::EXP1: + default: + // TODO + m_ui.memoryView->setData(Bus::EXP1_BASE, nullptr, 0); + break; + } + +#define SET_REGION_RADIO_BUTTON(name, rb_region) \ + do \ + { \ + QSignalBlocker sb(name); \ + name->setChecked(region == rb_region); \ + } while (0) + + SET_REGION_RADIO_BUTTON(m_ui.memoryRegionRAM, Bus::MemoryRegion::RAM); + SET_REGION_RADIO_BUTTON(m_ui.memoryRegionEXP1, Bus::MemoryRegion::EXP1); + SET_REGION_RADIO_BUTTON(m_ui.memoryRegionScratchpad, Bus::MemoryRegion::Scratchpad); + SET_REGION_RADIO_BUTTON(m_ui.memoryRegionBIOS, Bus::MemoryRegion::BIOS); + +#undef SET_REGION_REGION_BUTTON + + m_ui.memoryView->repaint(); +} + +void DebuggerWindow::toggleBreakpoint(VirtualMemoryAddress address) +{ + const bool new_bp_state = !CPU::HasBreakpointAtAddress(address); + if (new_bp_state) + { + if (!CPU::AddBreakpoint(address, false)) + return; + } + else + { + if (!CPU::RemoveBreakpoint(address)) + return; + } + + m_code_model->setBreakpointState(address, new_bp_state); + refreshBreakpointList(); +} + +void DebuggerWindow::clearBreakpoints() +{ + m_code_model->clearBreakpoints(); + CPU::ClearBreakpoints(); +} + +std::optional DebuggerWindow::promptForAddress(const QString& label) +{ + const QString address_str(QInputDialog::getText(this, windowTitle(), tr("Enter memory address:"))); + if (address_str.isEmpty()) + return std::nullopt; + + bool ok; + uint address; + if (address_str.startsWith("0x")) + address = address_str.midRef(2).toUInt(&ok, 16); + else if (address_str[0] == '0') + address = address_str.midRef(2).toUInt(&ok, 8); + else + address = address_str.midRef(2).toUInt(&ok, 8); + + if (!ok) + { + QMessageBox::critical(this, windowTitle(), + tr("Invalid address. It should be in hex (0x12345678) or decimal (12345678)")); + return std::nullopt; + } + + return address; +} + +std::optional DebuggerWindow::getSelectedCodeAddress() +{ + QItemSelectionModel* sel_model = m_ui.codeView->selectionModel(); + const QModelIndexList indices(sel_model->selectedIndexes()); + if (indices.empty()) + return std::nullopt; + + return m_code_model->getAddressForIndex(indices[0]); +} + +bool DebuggerWindow::tryFollowLoadStore(VirtualMemoryAddress address) +{ + CPU::Instruction inst; + if (!CPU::SafeReadInstruction(address, &inst.bits)) + return false; + + const std::optional ea = GetLoadStoreEffectiveAddress(inst, &CPU::g_state.regs); + if (!ea.has_value()) + return false; + + scrollToMemoryAddress(ea.value()); + return true; +} + +bool DebuggerWindow::scrollToMemoryAddress(VirtualMemoryAddress address) +{ + const PhysicalMemoryAddress phys_address = CPU::VirtualAddressToPhysical(address); + std::optional region = Bus::GetMemoryRegionForAddress(phys_address); + if (!region.has_value()) + return false; + + setMemoryViewRegion(region.value()); + + const PhysicalMemoryAddress offset = phys_address - Bus::GetMemoryRegionStart(region.value()); + m_ui.memoryView->scrolltoOffset(offset); + return true; +} + +void DebuggerWindow::refreshBreakpointList() +{ + while (m_ui.breakpointsWidget->topLevelItemCount() > 0) + delete m_ui.breakpointsWidget->takeTopLevelItem(0); + + const CPU::BreakpointList bps(CPU::GetBreakpointList()); + for (const CPU::Breakpoint& bp : bps) + { + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, bp.enabled ? Qt::Checked : Qt::Unchecked); + item->setText(0, QString::asprintf("%u", bp.number)); + item->setText(1, QString::asprintf("0x%08X", bp.address)); + item->setText(2, QString::asprintf("%u", bp.hit_count)); + m_ui.breakpointsWidget->addTopLevelItem(item); + } +} diff --git a/src/duckstation-qt/debuggerwindow.h b/src/duckstation-qt/debuggerwindow.h new file mode 100644 index 000000000..dd952efca --- /dev/null +++ b/src/duckstation-qt/debuggerwindow.h @@ -0,0 +1,79 @@ +#pragma once +#include "core/types.h" +#include "ui_debuggerwindow.h" +#include +#include +#include + +namespace Bus { +enum class MemoryRegion; +} + +class DebuggerCodeModel; +class DebuggerRegistersModel; +class DebuggerStackModel; + +class DebuggerWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit DebuggerWindow(QWidget* parent = nullptr); + ~DebuggerWindow(); + +Q_SIGNALS: + void closed(); + +protected: + void closeEvent(QCloseEvent* event); + +private Q_SLOTS: + void onEmulationPaused(bool paused); + void onDebuggerMessageReported(const QString& message); + + void refreshAll(); + + void scrollToPC(); + + void onPauseActionToggled(bool paused); + void onRunToCursorTriggered(); + void onGoToPCTriggered(); + void onGoToAddressTriggered(); + void onDumpAddressTriggered(); + void onFollowAddressTriggered(); + void onAddBreakpointTriggered(); + void onToggleBreakpointTriggered(); + void onClearBreakpointsTriggered(); + void onStepIntoActionTriggered(); + void onStepOverActionTriggered(); + void onStepOutActionTriggered(); + void onCodeViewItemActivated(QModelIndex index); + void onMemorySearchTriggered(); + void onMemorySearchStringChanged(const QString&); + +private: + void setupAdditionalUi(); + void connectSignals(); + void disconnectSignals(); + void createModels(); + void setUIEnabled(bool enabled); + void setMemoryViewRegion(Bus::MemoryRegion region); + void toggleBreakpoint(VirtualMemoryAddress address); + void clearBreakpoints(); + std::optional promptForAddress(const QString& label); + std::optional getSelectedCodeAddress(); + bool tryFollowLoadStore(VirtualMemoryAddress address); + void scrollToCodeAddress(VirtualMemoryAddress address); + bool scrollToMemoryAddress(VirtualMemoryAddress address); + void refreshBreakpointList(); + + Ui::DebuggerWindow m_ui; + + std::unique_ptr m_code_model; + std::unique_ptr m_registers_model; + std::unique_ptr m_stack_model; + + Bus::MemoryRegion m_active_memory_region; + + PhysicalMemoryAddress m_next_memory_search_address = 0; +}; diff --git a/src/duckstation-qt/debuggerwindow.ui b/src/duckstation-qt/debuggerwindow.ui new file mode 100644 index 000000000..1cbf98e86 --- /dev/null +++ b/src/duckstation-qt/debuggerwindow.ui @@ -0,0 +1,477 @@ + + + DebuggerWindow + + + + 0 + 0 + 1210 + 800 + + + + CPU Debugger + + + true + + + + + + 0 + 0 + 1210 + 21 + + + + + &Debug + + + + + + + + + + + + + + + + + Breakpoints + + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + + + + + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Disassembly + + + 4 + + + + + 0 + 0 + + + + false + + + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Registers + + + 4 + + + + + 0 + 0 + + + + + 220 + 16777215 + + + + + + + + 1 + 0 + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Memory + + + 8 + + + + + + + + 0 + 0 + + + + + + + + + + RAM + + + true + + + + + + + Scratchpad + + + + + + + EXP1 + + + + + + + BIOS + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Search + + + + + + + + + + + + + + 0 + 0 + + + + + 200 + 524287 + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Breakpoints + + + 8 + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + 20 + + + + # + + + + + Address + + + + + Hit Count + + + + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Stack + + + 8 + + + + + 220 + 16777215 + + + + + + + true + + + + :/icons/media-playback-pause.png:/icons/media-playback-pause.png + + + Pause/Continue + + + &Pause/Continue + + + F5 + + + + + + :/icons/debug-step-into-instruction.png:/icons/debug-step-into-instruction.png + + + Step Into + + + &Step Into + + + F11 + + + + + + :/icons/debug-step-over.png:/icons/debug-step-over.png + + + Step Over + + + Step &Over + + + F10 + + + + + + :/icons/media-record@2x.png:/icons/media-record@2x.png + + + Toggle Breakpoint + + + Toggle &Breakpoint + + + F9 + + + + + &Close + + + + + + :/icons/debug-step-out.png:/icons/debug-step-out.png + + + Step Out + + + Step O&ut + + + Ctrl+F11 + + + + + + :/icons/debug-run-cursor.png:/icons/debug-run-cursor.png + + + Run To Cursor + + + &Run To Cursor + + + Ctrl+F10 + + + + + + :/icons/edit-clear-16.png:/icons/edit-clear-16.png + + + Clear Breakpoints + + + &Clear Breakpoints + + + Ctrl+Del + + + + + + :/icons/list-add.png:/icons/list-add.png + + + Add Breakpoint + + + Add &Breakpoint + + + Ctrl+F9 + + + + + + :/icons/document-open.png:/icons/document-open.png + + + Go To PC + + + &Go To PC + + + Ctrl+P + + + + + + :/icons/applications-system.png:/icons/applications-system.png + + + Go To Address + + + Go To &Address + + + Ctrl+G + + + + + + :/icons/antialias-icon.png:/icons/antialias-icon.png + + + &Dump Address + + + Ctrl+D + + + + + + MemoryViewWidget + QWidget +
memoryviewwidget.h
+ 1 +
+
+ + + + +
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 8fba6ee4f..67b74ccda 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -59,6 +59,8 @@ + + @@ -68,6 +70,7 @@ + @@ -106,7 +109,10 @@ + + + @@ -216,6 +222,8 @@ + + @@ -223,6 +231,7 @@ + @@ -251,35 +260,48 @@ Document + + + Document + + Document + + Document + + + Document + Document + + Document + + + Document + + + Document + Document Document + + Document + Document - - Document - - - - - - - - - + @@ -778,4 +800,4 @@ - \ No newline at end of file + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 00abb1d1f..7e0a6b93a 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -65,6 +65,12 @@ + + + + + + @@ -111,6 +117,9 @@ + + + @@ -142,46 +151,47 @@ + + + resources + + + + + translations + + translations + + + translations + translations + + translations + + + translations + + + translations + translations translations + + translations + translations - - - translations - - - translations - - - translations - - - translations - - - translations - - - translations - - - - - resources - - \ No newline at end of file diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index e8cf59136..427d8e4bd 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -6,6 +6,7 @@ #include "core/host_display.h" #include "core/settings.h" #include "core/system.h" +#include "debuggerwindow.h" #include "frontend-common/game_list.h" #include "gamelistsettingswidget.h" #include "gamelistwidget.h" @@ -60,6 +61,8 @@ MainWindow::~MainWindow() { Assert(!m_display_widget); m_host_interface->setMainWindow(nullptr); + + Assert(!m_debugger_window); } void MainWindow::reportError(const QString& message) @@ -311,6 +314,12 @@ void MainWindow::onEmulationStopped() delete m_cheat_manager_dialog; m_cheat_manager_dialog = nullptr; } + + if (m_debugger_window) + { + delete m_debugger_window; + m_debugger_window = nullptr; + } } void MainWindow::onEmulationPaused(bool paused) @@ -761,6 +770,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running) m_ui.menuChangeDisc->setDisabled(starting || !running); m_ui.menuCheats->setDisabled(starting || !running); m_ui.actionCheatManager->setDisabled(starting || !running); + m_ui.actionCPUDebugger->setDisabled(starting || !running); m_ui.actionSaveState->setDisabled(starting || !running); m_ui.menuSaveState->setDisabled(starting || !running); @@ -850,7 +860,7 @@ void MainWindow::connectSignals() [this]() { getSettingsDialog()->getGameListSettingsWidget()->addSearchDirectory(this); }); connect(m_ui.actionPowerOff, &QAction::triggered, m_host_interface, &QtHostInterface::powerOffSystem); connect(m_ui.actionReset, &QAction::triggered, m_host_interface, &QtHostInterface::resetSystem); - connect(m_ui.actionPause, &QAction::toggled, m_host_interface, &QtHostInterface::pauseSystem); + connect(m_ui.actionPause, &QAction::toggled, [this](bool active) { m_host_interface->pauseSystem(active); }); connect(m_ui.actionScreenshot, &QAction::triggered, m_host_interface, &QtHostInterface::saveScreenshot); connect(m_ui.actionScanForNewGames, &QAction::triggered, this, [this]() { m_host_interface->refreshGameList(false, false); }); @@ -899,6 +909,7 @@ void MainWindow::connectSignals() connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); connect(m_ui.actionCheatManager, &QAction::triggered, this, &MainWindow::onToolsCheatManagerTriggered); + connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::onToolsCPUDebuggerTriggered); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles); connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() { @@ -1325,6 +1336,26 @@ void MainWindow::onToolsCheatManagerTriggered() m_cheat_manager_dialog->show(); } +void MainWindow::onToolsCPUDebuggerTriggered() +{ + if (!m_debugger_window) + { + m_debugger_window = new DebuggerWindow(); + m_debugger_window->setWindowIcon(windowIcon()); + connect(m_debugger_window, &DebuggerWindow::closed, this, &MainWindow::onCPUDebuggerClosed); + } + + m_debugger_window->show(); + m_host_interface->pauseSystem(true); +} + +void MainWindow::onCPUDebuggerClosed() +{ + Assert(m_debugger_window); + m_debugger_window->deleteLater(); + m_debugger_window = nullptr; +} + void MainWindow::onToolsOpenDataDirectoryTriggered() { QtUtils::OpenURL(this, QUrl::fromLocalFile(m_host_interface->getUserDirectoryRelativePath(QString()))); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 7d55c64b5..f0f5cc3f5 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -16,6 +16,7 @@ class QtDisplayWidget; class AutoUpdaterDialog; class MemoryCardEditorDialog; class CheatManagerDialog; +class DebuggerWindow; class HostDisplay; struct GameListEntry; @@ -83,12 +84,14 @@ private Q_SLOTS: void onCheckForUpdatesActionTriggered(); void onToolsMemoryCardEditorTriggered(); void onToolsCheatManagerTriggered(); + void onToolsCPUDebuggerTriggered(); void onToolsOpenDataDirectoryTriggered(); void onGameListEntrySelected(const GameListEntry* entry); void onGameListEntryDoubleClicked(const GameListEntry* entry); void onGameListContextMenuRequested(const QPoint& point, const GameListEntry* entry); void onGameListSetCoverImageRequested(const GameListEntry* entry); + void onCPUDebuggerClosed(); void checkForUpdates(bool display_message); void onUpdateCheckComplete(); @@ -137,6 +140,7 @@ private: AutoUpdaterDialog* m_auto_updater_dialog = nullptr; MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr; CheatManagerDialog* m_cheat_manager_dialog = nullptr; + DebuggerWindow* m_debugger_window = nullptr; bool m_emulation_running = false; bool m_was_paused_by_focus_loss = false; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index b187f9437..b1eba0743 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -176,6 +176,8 @@ + + @@ -736,6 +738,11 @@ C&heat Manager + + + CPU D&ebugger + + Game &Grid diff --git a/src/duckstation-qt/memoryviewwidget.cpp b/src/duckstation-qt/memoryviewwidget.cpp new file mode 100644 index 000000000..d6e8dcbc3 --- /dev/null +++ b/src/duckstation-qt/memoryviewwidget.cpp @@ -0,0 +1,234 @@ +#include "memoryviewwidget.h" +#include +#include + +MemoryViewWidget::MemoryViewWidget(QWidget* parent /* = nullptr */, size_t address_offset /* = 0 */, + const void* data_ptr /* = nullptr */, size_t data_size /* = 0 */) + : QAbstractScrollArea(parent) +{ + m_bytes_per_line = 16; + + updateMetrics(); + + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &MemoryViewWidget::adjustContent); + connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &MemoryViewWidget::adjustContent); + + if (data_ptr) + setData(address_offset, data_ptr, data_size); +} + +MemoryViewWidget::~MemoryViewWidget() = default; + +int MemoryViewWidget::addressWidth() const +{ + return (8 * m_char_width) + m_char_width; +} + +int MemoryViewWidget::hexWidth() const +{ + return (m_bytes_per_line * 4) * m_char_width; +} + +int MemoryViewWidget::asciiWidth() const +{ + return (m_bytes_per_line * 2 + 1) * m_char_width; +} + +void MemoryViewWidget::updateMetrics() +{ + m_char_width = fontMetrics().horizontalAdvance(QChar('0')); + m_char_height = fontMetrics().height(); +} + +void MemoryViewWidget::setData(size_t address_offset, const void* data_ptr, size_t data_size) +{ + m_data = data_ptr; + m_data_size = data_size; + m_address_offset = address_offset; + adjustContent(); +} + +void MemoryViewWidget::setHighlightRange(size_t start, size_t end) +{ + m_highlight_start = start; + m_highlight_end = end; + viewport()->update(); +} + +void MemoryViewWidget::clearHighlightRange() +{ + m_highlight_start = 0; + m_highlight_end = 0; + viewport()->update(); +} + +void MemoryViewWidget::scrolltoOffset(size_t offset) +{ + const unsigned row = static_cast(offset / m_bytes_per_line); + verticalScrollBar()->setSliderPosition(static_cast(row)); + horizontalScrollBar()->setSliderPosition(0); +} + +void MemoryViewWidget::scrollToAddress(size_t address) +{ + const unsigned row = static_cast((address - m_start_offset) / m_bytes_per_line); + verticalScrollBar()->setSliderPosition(static_cast(row)); + horizontalScrollBar()->setSliderPosition(0); +} + +void MemoryViewWidget::setFont(const QFont& font) +{ + QAbstractScrollArea::setFont(font); + updateMetrics(); +} + +void MemoryViewWidget::resizeEvent(QResizeEvent*) +{ + adjustContent(); +} + +template +static bool RangesOverlap(T x1, T x2, T y1, T y2) +{ + return (x2 >= y1 && x1 < y2); +} + +void MemoryViewWidget::paintEvent(QPaintEvent*) +{ + QPainter painter(viewport()); + painter.setFont(font()); + if (!m_data) + return; + + const QColor highlight_color(100, 100, 0); + const int offsetX = horizontalScrollBar()->value(); + + int y = m_char_height; + QString address; + + painter.setPen(viewport()->palette().color(QPalette::WindowText)); + + y += m_char_height; + + const unsigned num_rows = static_cast(m_end_offset - m_start_offset) / m_bytes_per_line; + for (unsigned row = 0; row <= num_rows; row++) + { + const size_t data_offset = m_start_offset + (row * m_bytes_per_line); + const unsigned row_address = static_cast(m_address_offset + data_offset); + const int draw_x = m_char_width / 2 - offsetX; + if (RangesOverlap(data_offset, data_offset + m_bytes_per_line, m_highlight_start, m_highlight_end)) + painter.fillRect(0, y - m_char_height + 3, addressWidth(), m_char_height, highlight_color); + + const QString address_text(QString::asprintf("%08X", row_address)); + painter.drawText(draw_x, y, address_text); + y += m_char_height; + } + + int x; + int lx = addressWidth(); + painter.drawLine(lx - offsetX, 0, lx - offsetX, height()); + y = m_char_height; + + // hex data + const int HEX_CHAR_WIDTH = 4 * m_char_width; + + x = lx - offsetX; + for (unsigned col = 0; col < m_bytes_per_line; col++) + { + if ((col % 2) != 0) + painter.fillRect(x, 0, HEX_CHAR_WIDTH, height(), viewport()->palette().color(QPalette::AlternateBase)); + + x += HEX_CHAR_WIDTH; + } + + y = m_char_height; + x = lx - offsetX + m_char_width; + for (unsigned col = 0; col < m_bytes_per_line; col++) + { + painter.drawText(x, y, QString::asprintf("%02X", col)); + x += HEX_CHAR_WIDTH; + } + + painter.drawLine(0, y + 3, width(), y + 3); + y += m_char_height; + + size_t offset = m_start_offset; + for (unsigned row = 0; row <= num_rows; row++) + { + x = lx - offsetX + m_char_width; + for (unsigned col = 0; col < m_bytes_per_line && offset < m_data_size; col++, offset++) + { + unsigned char value; + std::memcpy(&value, static_cast(m_data) + offset, sizeof(value)); + if (offset >= m_highlight_start && offset < m_highlight_end) + painter.fillRect(x - m_char_width, y - m_char_height + 3, HEX_CHAR_WIDTH, m_char_height, highlight_color); + + painter.drawText(x, y, QString::asprintf("%02X", value)); + x += HEX_CHAR_WIDTH; + } + y += m_char_height; + } + + lx = addressWidth() + hexWidth(); + painter.drawLine(lx - offsetX, 0, lx - offsetX, height()); + + lx += m_char_width; + + y = m_char_height; + x = (lx - offsetX); + for (unsigned col = 0; col < m_bytes_per_line; col++) + { + const QChar ch = (col < 0xA) ? (static_cast('0' + col)) : (static_cast('A' + (col - 0xA))); + painter.drawText(x, y, ch); + x += 2 * m_char_width; + } + + y += m_char_height; + + offset = m_start_offset; + for (unsigned row = 0; row <= num_rows; row++) + { + x = lx - offsetX; + for (unsigned col = 0; col < m_bytes_per_line && offset < m_data_size; col++, offset++) + { + unsigned char value; + std::memcpy(&value, static_cast(m_data) + offset, sizeof(value)); + if (offset >= m_highlight_start && offset < m_highlight_end) + painter.fillRect(x, y - m_char_height + 3, 2 * m_char_width, m_char_height, highlight_color); + + if (!std::isprint(value)) + value = '.'; + painter.drawText(x, y, static_cast(value)); + x += 2 * m_char_width; + } + y += m_char_height; + } +} + +void MemoryViewWidget::adjustContent() +{ + if (!m_data) + { + setEnabled(false); + return; + } + + setEnabled(true); + + int w = addressWidth() + hexWidth() + asciiWidth(); + horizontalScrollBar()->setRange(0, w - viewport()->width()); + horizontalScrollBar()->setPageStep(viewport()->width()); + + m_rows_visible = viewport()->height() / m_char_height; + int val = verticalScrollBar()->value(); + m_start_offset = (size_t)val * m_bytes_per_line; + m_end_offset = m_start_offset + m_rows_visible * m_bytes_per_line - 1; + if (m_end_offset >= m_data_size) + m_end_offset = m_data_size - 1; + + const int lineCount = static_cast(m_data_size / m_bytes_per_line); + verticalScrollBar()->setRange(0, lineCount - m_rows_visible); + verticalScrollBar()->setPageStep(m_rows_visible); + + viewport()->update(); +} \ No newline at end of file diff --git a/src/duckstation-qt/memoryviewwidget.h b/src/duckstation-qt/memoryviewwidget.h new file mode 100644 index 000000000..760124989 --- /dev/null +++ b/src/duckstation-qt/memoryviewwidget.h @@ -0,0 +1,53 @@ +#pragma once +#include + +// Based on https://stackoverflow.com/questions/46375673/how-can-realize-my-own-memory-viewer-by-qt + +class MemoryViewWidget : public QAbstractScrollArea +{ +public: + Q_OBJECT +public: + MemoryViewWidget(QWidget* parent = nullptr, size_t address_offset = 0, const void* data_ptr = nullptr, + size_t data_size = 0); + ~MemoryViewWidget(); + + size_t addressOffset() const { return m_address_offset; } + + void setData(size_t address_offset, const void* data_ptr, size_t data_size); + void setHighlightRange(size_t start, size_t end); + void clearHighlightRange(); + void scrolltoOffset(size_t offset); + void scrollToAddress(size_t address); + void setFont(const QFont& font); + +protected: + void paintEvent(QPaintEvent*); + void resizeEvent(QResizeEvent*); + +private Q_SLOTS: + void adjustContent(); + +private: + int addressWidth() const; + int hexWidth() const; + int asciiWidth() const; + void updateMetrics(); + + const void* m_data; + size_t m_data_size; + size_t m_address_offset; + + size_t m_start_offset; + size_t m_end_offset; + + size_t m_highlight_start = 0; + size_t m_highlight_end = 0; + + unsigned m_bytes_per_line; + + int m_char_width; + int m_char_height; + + int m_rows_visible; +}; \ No newline at end of file diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index d852cedf7..f95d64381 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -821,11 +821,13 @@ void QtHostInterface::resetSystem() HostInterface::ResetSystem(); } -void QtHostInterface::pauseSystem(bool paused) +void QtHostInterface::pauseSystem(bool paused, bool wait_until_paused /* = false */) { if (!isOnWorkerThread()) { - QMetaObject::invokeMethod(this, "pauseSystem", Qt::QueuedConnection, Q_ARG(bool, paused)); + QMetaObject::invokeMethod(this, "pauseSystem", + wait_until_paused ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, + Q_ARG(bool, paused), Q_ARG(bool, wait_until_paused)); return; } @@ -1222,6 +1224,21 @@ void QtHostInterface::stopDumpingAudio() StopDumpingAudio(); } +void QtHostInterface::singleStepCPU() +{ + if (!isOnWorkerThread()) + { + QMetaObject::invokeMethod(this, "singleStepCPU", Qt::BlockingQueuedConnection); + return; + } + + if (!System::IsValid()) + return; + + System::SingleStepCPU(); + renderDisplay(); +} + void QtHostInterface::dumpRAM(const QString& filename) { if (!isOnWorkerThread()) diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h index d2f63b3cc..d7ceda635 100644 --- a/src/duckstation-qt/qthostinterface.h +++ b/src/duckstation-qt/qthostinterface.h @@ -155,7 +155,7 @@ public Q_SLOTS: void powerOffSystem(); void synchronousPowerOffSystem(); void resetSystem(); - void pauseSystem(bool paused); + void pauseSystem(bool paused, bool wait_until_paused = false); void changeDisc(const QString& new_disc_filename); void changeDiscFromPlaylist(quint32 index); void loadState(const QString& filename); @@ -165,6 +165,7 @@ public Q_SLOTS: void setAudioOutputMuted(bool muted); void startDumpingAudio(); void stopDumpingAudio(); + void singleStepCPU(); void dumpRAM(const QString& filename); void saveScreenshot(); void redrawDisplayWindow(); diff --git a/src/duckstation-qt/resources/icons/debug-execute-from-cursor.png b/src/duckstation-qt/resources/icons/debug-execute-from-cursor.png new file mode 100644 index 000000000..d78d4db2d Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-execute-from-cursor.png differ diff --git a/src/duckstation-qt/resources/icons/debug-execute-to-cursor.png b/src/duckstation-qt/resources/icons/debug-execute-to-cursor.png new file mode 100644 index 000000000..d998a154c Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-execute-to-cursor.png differ diff --git a/src/duckstation-qt/resources/icons/debug-pc.png b/src/duckstation-qt/resources/icons/debug-pc.png new file mode 100644 index 000000000..95fe6b279 Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-pc.png differ diff --git a/src/duckstation-qt/resources/icons/debug-pc@2x.png b/src/duckstation-qt/resources/icons/debug-pc@2x.png new file mode 100644 index 000000000..467b34fcd Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-pc@2x.png differ diff --git a/src/duckstation-qt/resources/icons/debug-run-cursor.png b/src/duckstation-qt/resources/icons/debug-run-cursor.png new file mode 100644 index 000000000..224c22546 Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-run-cursor.png differ diff --git a/src/duckstation-qt/resources/icons/debug-run.png b/src/duckstation-qt/resources/icons/debug-run.png new file mode 100644 index 000000000..bfde67c6a Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-run.png differ diff --git a/src/duckstation-qt/resources/icons/debug-step-instruction.png b/src/duckstation-qt/resources/icons/debug-step-instruction.png new file mode 100644 index 000000000..ccc49e415 Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-step-instruction.png differ diff --git a/src/duckstation-qt/resources/icons/debug-step-into-instruction.png b/src/duckstation-qt/resources/icons/debug-step-into-instruction.png new file mode 100644 index 000000000..4f9818cea Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-step-into-instruction.png differ diff --git a/src/duckstation-qt/resources/icons/debug-step-into.png b/src/duckstation-qt/resources/icons/debug-step-into.png new file mode 100644 index 000000000..7e3e990a0 Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-step-into.png differ diff --git a/src/duckstation-qt/resources/icons/debug-step-out.png b/src/duckstation-qt/resources/icons/debug-step-out.png new file mode 100644 index 000000000..b28908f7b Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-step-out.png differ diff --git a/src/duckstation-qt/resources/icons/debug-step-over.png b/src/duckstation-qt/resources/icons/debug-step-over.png new file mode 100644 index 000000000..21459f496 Binary files /dev/null and b/src/duckstation-qt/resources/icons/debug-step-over.png differ diff --git a/src/duckstation-qt/resources/resources.qrc b/src/duckstation-qt/resources/resources.qrc index fa40983fc..3c23c6016 100644 --- a/src/duckstation-qt/resources/resources.qrc +++ b/src/duckstation-qt/resources/resources.qrc @@ -24,6 +24,17 @@ icons/conical-flask-red.png icons/conical-flask-red@2x.png icons/cover-placeholder.png + icons/debug-execute-from-cursor.png + icons/debug-execute-to-cursor.png + icons/debug-run-cursor.png + icons/debug-run.png + icons/debug-pc.png + icons/debug-pc@2x.png + icons/debug-step-instruction.png + icons/debug-step-into-instruction.png + icons/debug-step-into.png + icons/debug-step-out.png + icons/debug-step-over.png icons/document-open.png icons/document-open@2x.png icons/document-save.png