diff --git a/src/duckstation-qt/debuggercodeview.cpp b/src/duckstation-qt/debuggercodeview.cpp index e12cca359..370060c95 100644 --- a/src/duckstation-qt/debuggercodeview.cpp +++ b/src/duckstation-qt/debuggercodeview.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -231,13 +232,35 @@ std::optional DebuggerCodeView::getSelectedAddress() const return m_selected_address; } +std::optional> DebuggerCodeView::getSelectedAddressRange() const +{ + if (!m_has_selection) + return std::nullopt; + + return std::make_pair(std::min(m_selection_anchor_address, m_selected_address), + std::max(m_selection_anchor_address, m_selected_address)); +} + +bool DebuggerCodeView::isAddressSelected(VirtualMemoryAddress address) const +{ + const std::optional> range = getSelectedAddressRange(); + return range.has_value() && address >= range->first && address <= range->second; +} + void DebuggerCodeView::setSelectedAddress(VirtualMemoryAddress address) { m_selected_address = address; + m_selection_anchor_address = address; m_has_selection = true; viewport()->update(); } +void DebuggerCodeView::setSelectionEndAddress(VirtualMemoryAddress address) +{ + m_selected_address = address; + viewport()->update(); +} + VirtualMemoryAddress DebuggerCodeView::getAddressAtPoint(const QPoint& point) const { const int top_row = getRowForAddress(m_top_address); @@ -269,7 +292,7 @@ void DebuggerCodeView::paintEvent(QPaintEvent* event) if (y + m_row_height < visible_rect.top() || y > visible_rect.bottom()) continue; - const bool is_selected = (m_has_selection && address == m_selected_address); + const bool is_selected = isAddressSelected(address); const bool is_pc = (address == m_last_pc); drawInstruction(painter, address, y, is_selected, is_pc); @@ -581,7 +604,10 @@ void DebuggerCodeView::mousePressEvent(QMouseEvent* event) if (event->button() == Qt::LeftButton) { const VirtualMemoryAddress address = getAddressAtPoint(event->pos()); - setSelectedAddress(address); + if (m_has_selection && (event->modifiers() & Qt::ShiftModifier)) + setSelectionEndAddress(address); + else + setSelectedAddress(address); } QAbstractScrollArea::mousePressEvent(event); @@ -592,7 +618,7 @@ void DebuggerCodeView::mouseMoveEvent(QMouseEvent* event) if (event->buttons() & Qt::LeftButton) { const VirtualMemoryAddress address = getAddressAtPoint(event->pos()); - setSelectedAddress(address); + setSelectionEndAddress(address); } QAbstractScrollArea::mouseMoveEvent(event); } @@ -631,35 +657,49 @@ void DebuggerCodeView::contextMenuEvent(QContextMenuEvent* event) void DebuggerCodeView::keyPressEvent(QKeyEvent* event) { + if (m_has_selection && event->matches(QKeySequence::Copy)) + { + emit copyActivated(); + event->accept(); + return; + } + if (!m_has_selection) { QAbstractScrollArea::keyPressEvent(event); return; } - VirtualMemoryAddress new_address = m_selected_address; + int row_delta; switch (event->key()) { case Qt::Key_Up: - new_address -= CPU::INSTRUCTION_SIZE; + row_delta = -1; break; case Qt::Key_Down: - new_address += CPU::INSTRUCTION_SIZE; + row_delta = 1; break; case Qt::Key_PageUp: - new_address -= CPU::INSTRUCTION_SIZE * getVisibleRowCount(); + row_delta = -getVisibleRowCount(); break; case Qt::Key_PageDown: - new_address += CPU::INSTRUCTION_SIZE * getVisibleRowCount(); + row_delta = getVisibleRowCount(); break; default: QAbstractScrollArea::keyPressEvent(event); return; } + const int total_rows = static_cast((m_code_region_end - m_code_region_start) / CPU::INSTRUCTION_SIZE); + const int new_row = std::clamp(getRowForAddress(m_selected_address) + row_delta, 0, total_rows - 1); + const VirtualMemoryAddress new_address = getAddressForRow(new_row); scrollToAddress(new_address, false); - setSelectedAddress(new_address); + if (event->modifiers() & Qt::ShiftModifier) + setSelectionEndAddress(new_address); + else + setSelectedAddress(new_address); + event->accept(); } void DebuggerCodeView::wheelEvent(QWheelEvent* event) diff --git a/src/duckstation-qt/debuggercodeview.h b/src/duckstation-qt/debuggercodeview.h index 9a128c4f5..cd990f941 100644 --- a/src/duckstation-qt/debuggercodeview.h +++ b/src/duckstation-qt/debuggercodeview.h @@ -15,6 +15,7 @@ #include #include +#include #include class DebuggerCodeView : public QAbstractScrollArea @@ -30,6 +31,8 @@ public: void scrollToAddress(VirtualMemoryAddress address, bool center = false); std::optional getSelectedAddress() const; + std::optional> getSelectedAddressRange() const; + bool isAddressSelected(VirtualMemoryAddress address) const; void setSelectedAddress(VirtualMemoryAddress address); VirtualMemoryAddress getAddressAtPoint(const QPoint& point) const; @@ -48,6 +51,7 @@ Q_SIGNALS: void toggleBreakpointActivated(VirtualMemoryAddress address); void addressActivated(VirtualMemoryAddress address); void commentActivated(VirtualMemoryAddress address); + void copyActivated(); void contextMenuRequested(const QPoint& point, VirtualMemoryAddress address); protected: @@ -99,11 +103,13 @@ private: int getVisibleRowCount() const; VirtualMemoryAddress getFirstVisibleAddress() const; VirtualMemoryAddress getLastVisibleAddress() const; + void setSelectionEndAddress(VirtualMemoryAddress address); int m_row_height = 1; int m_char_width = 0; VirtualMemoryAddress m_selected_address = 0; + VirtualMemoryAddress m_selection_anchor_address = 0; bool m_has_selection = false; std::vector m_branch_arrows; diff --git a/src/duckstation-qt/debuggerwindow.cpp b/src/duckstation-qt/debuggerwindow.cpp index 903fe7b34..abfb6569b 100644 --- a/src/duckstation-qt/debuggerwindow.cpp +++ b/src/duckstation-qt/debuggerwindow.cpp @@ -23,6 +23,7 @@ #include "common/string_util.h" #include "util/ini_settings_interface.h" +#include "util/translation.h" #include @@ -340,7 +341,11 @@ void DebuggerWindow::onCodeViewCommentActivated(VirtualMemoryAddress address) void DebuggerWindow::onCodeViewContextMenuRequested(const QPoint& pt) { const VirtualMemoryAddress address = m_ui.codeView->getAddressAtPoint(pt); - m_ui.codeView->setSelectedAddress(address); + if (!m_ui.codeView->isAddressSelected(address)) + m_ui.codeView->setSelectedAddress(address); + + const auto [selection_start, selection_end] = m_ui.codeView->getSelectedAddressRange().value(); + const u32 selection_count = ((selection_end - selection_start) / CPU::INSTRUCTION_SIZE) + 1; QMenu* const menu = QtUtils::NewPopupMenu(this); menu->addAction(QStringLiteral("0x%1").arg(static_cast(address), 8, 16, QChar('0')))->setEnabled(false); @@ -364,11 +369,17 @@ void DebuggerWindow::onCodeViewContextMenuRequested(const QPoint& pt) [this, address]() { startPatchInstruction(address); }); patch_action->setEnabled(can_patch); - QAction* const nop_action = menu->addAction(QIcon(u":/icons/monochrome/svg/trash-fill.svg"_s), tr("&Nop Instruction"), - [this, address]() { patchInstruction(address, 0); }); + QAction* const nop_action = + menu->addAction(QIcon(u":/icons/monochrome/svg/trash-fill.svg"_s), + tr("&Nop %n Instruction(s)", nullptr, static_cast(selection_count)), + [this, selection_start, selection_end]() { patchInstructions(selection_start, selection_end, 0); }); nop_action->setEnabled(can_patch); menu->addSeparator(); + menu->addAction(QIcon(u":/icons/monochrome/svg/file-copy-line.svg"_s), + tr("&Copy %n Instruction(s)", nullptr, static_cast(selection_count)), this, + &DebuggerWindow::copySelectedCodeToClipboard); + menu->addAction(QIcon(u":/icons/monochrome/svg/debugger-go-to-address.svg"_s), tr("View in &Dump"), [this, address]() { scrollToMemoryAddress(address); }); @@ -437,7 +448,7 @@ void DebuggerWindow::startPatchInstruction(VirtualMemoryAddress address) if (CPU::AssembleInstruction(&replacement_bits, address, std::string_view(text_utf8.constData(), text_utf8.size()), &error)) { - patchInstruction(address, replacement_bits); + patchInstructions(address, address, replacement_bits); return; } @@ -445,32 +456,104 @@ void DebuggerWindow::startPatchInstruction(VirtualMemoryAddress address) } } -void DebuggerWindow::patchInstruction(VirtualMemoryAddress address, u32 bits) +void DebuggerWindow::patchInstructions(VirtualMemoryAddress start_address, VirtualMemoryAddress end_address, u32 bits) { - Host::RunOnCoreThread([win = QPointer(this), address, bits]() mutable { - const bool success = CPU::SafeWriteMemoryWord(address, bits); - if (success) - CPU::InvalidateICacheAt(address); + Host::RunOnCoreThread([win = QPointer(this), start_address, end_address, bits]() mutable { + u32 success_count = 0; + std::optional first_failed_address; + for (VirtualMemoryAddress address = start_address;; address += CPU::INSTRUCTION_SIZE) + { + if (CPU::SafeWriteMemoryWord(address, bits)) + { + CPU::InvalidateICacheAt(address); + success_count++; + } + else if (!first_failed_address.has_value()) + { + first_failed_address = address; + } + + if (address == end_address) + break; + } - Host::RunOnUIThread([win = std::move(win), address, success]() { + const u32 instruction_count = ((end_address - start_address) / CPU::INSTRUCTION_SIZE) + 1; + Host::RunOnUIThread([win = std::move(win), start_address, success_count, instruction_count, + first_failed_address]() { if (!win) return; - if (!success) + if (success_count > 0) + { + win->m_ui.codeView->refreshView(); + win->m_ui.memoryView->forceRefresh(); + } + + if (first_failed_address.has_value()) { QtUtils::AsyncMessageBox( win, QMessageBox::Critical, win->windowTitle(), - tr("Failed to write patched instruction to 0x%1.").arg(static_cast(address), 8, 16, QChar('0'))); - return; + tr("Failed to write one or more patched instructions. Patched %1 of %2 instructions; the first failure was " + "at 0x%3.") + .arg(success_count) + .arg(instruction_count) + .arg(static_cast(first_failed_address.value()), 8, 16, QChar('0'))); + win->reportMessage(tr("Patched %1 of %2 selected instructions.").arg(success_count).arg(instruction_count)); + } + else if (instruction_count == 1) + { + win->reportMessage(tr("Patched instruction at 0x%1.").arg(static_cast(start_address), 8, 16, QChar('0'))); + } + else + { + win->reportMessage(tr("Patched %1 instructions.").arg(instruction_count)); } - - win->m_ui.codeView->refreshView(); - win->m_ui.memoryView->forceRefresh(); - win->reportMessage(tr("Patched instruction at 0x%1.").arg(static_cast(address), 8, 16, QChar('0'))); }); }); } +void DebuggerWindow::copySelectedCodeToClipboard() +{ + const std::optional> range = + m_ui.codeView->getSelectedAddressRange(); + if (!range.has_value()) + return; + + Host::RunOnCoreThread([range = range.value()]() { + if (!System::IsValid()) + return; + + SmallString text; + u32 instruction_count = 0; + for (VirtualMemoryAddress address = range.first;; address += CPU::INSTRUCTION_SIZE) + { + if (instruction_count > 0) + text.append('\n'); + + text.append_format("0x{:08X} ", address); + if (u32 instruction_bits; CPU::SafeReadInstruction(address, &instruction_bits)) + { + SmallString disassembly; + CPU::DisassembleInstruction(&disassembly, address, instruction_bits); + text.append(disassembly); + } + else + { + text.append(""); + } + + instruction_count++; + if (address == range.second) + break; + } + + Host::CopyTextToClipboard(text); + Host::ReportDebuggerEvent(CPU::DebuggerEvent::Message, + TRANSLATE_PLURAL_SSTR("DebuggerWindow", "Copied %n instruction(s) to the clipboard.", + "CopyRange", static_cast(instruction_count))); + }); +} + void DebuggerWindow::onMemorySearchTriggered() { m_ui.memoryView->clearHighlightRange(); @@ -651,6 +734,7 @@ void DebuggerWindow::connectSignals() connect(m_ui.codeView, &DebuggerCodeView::toggleBreakpointActivated, this, &DebuggerWindow::onCodeViewToggleBreakpointActivated); connect(m_ui.codeView, &DebuggerCodeView::commentActivated, this, &DebuggerWindow::onCodeViewCommentActivated); + connect(m_ui.codeView, &DebuggerCodeView::copyActivated, this, &DebuggerWindow::copySelectedCodeToClipboard); connect(m_ui.codeView, &QWidget::customContextMenuRequested, this, &DebuggerWindow::onCodeViewContextMenuRequested); connect(m_ui.callStackView, &QTreeView::doubleClicked, this, &DebuggerWindow::onCallStackItemDoubleClicked); connect(m_ui.stackView, &QTreeView::doubleClicked, this, &DebuggerWindow::onStackItemDoubleClicked); @@ -1040,7 +1124,7 @@ void DebuggerWindow::loadGameSettings(bool clear_existing) } if (!queued_bps.empty()) - reportMessage(tr("Loaded %n saved breakpoint(s).", nullptr, queued_bps.size())); + reportMessage(tr("Loaded %n saved breakpoint(s).", nullptr, static_cast(queued_bps.size()))); Host::RunOnCoreThread([win = QPointer(this), serial = m_game_serial, clear_existing, queued_bps = std::move(queued_bps)]() mutable { diff --git a/src/duckstation-qt/debuggerwindow.h b/src/duckstation-qt/debuggerwindow.h index 13f0bf576..a1c5cca37 100644 --- a/src/duckstation-qt/debuggerwindow.h +++ b/src/duckstation-qt/debuggerwindow.h @@ -49,7 +49,8 @@ private: void saveCurrentState(); void setMemoryViewRegion(Bus::MemoryRegion region); void startPatchInstruction(VirtualMemoryAddress address); - void patchInstruction(VirtualMemoryAddress address, u32 bits); + void patchInstructions(VirtualMemoryAddress start_address, VirtualMemoryAddress end_address, u32 bits); + void copySelectedCodeToClipboard(); void toggleBreakpoint(VirtualMemoryAddress address); void clearBreakpoints(); bool tryFollowLoadStore(VirtualMemoryAddress address); diff --git a/src/duckstation-qt/resources/duckstation-qt.qrc b/src/duckstation-qt/resources/duckstation-qt.qrc index b03cf4146..567235339 100644 --- a/src/duckstation-qt/resources/duckstation-qt.qrc +++ b/src/duckstation-qt/resources/duckstation-qt.qrc @@ -103,6 +103,7 @@ icons/monochrome/svg/export-line.svg icons/monochrome/svg/file-add-line.svg icons/monochrome/svg/file-check-line.svg + icons/monochrome/svg/file-copy-line.svg icons/monochrome/svg/file-forbid-line.svg icons/monochrome/svg/file-line.svg icons/monochrome/svg/file-list-line.svg diff --git a/src/duckstation-qt/resources/icons/monochrome/svg/file-copy-line.svg b/src/duckstation-qt/resources/icons/monochrome/svg/file-copy-line.svg new file mode 100644 index 000000000..9e82aa2c0 --- /dev/null +++ b/src/duckstation-qt/resources/icons/monochrome/svg/file-copy-line.svg @@ -0,0 +1 @@ + \ No newline at end of file