diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 9d5137a4f0..0198ada766 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -93,6 +93,9 @@ add_executable(citra-qt debugger/ipc/record_dialog.cpp debugger/ipc/record_dialog.h debugger/ipc/record_dialog.ui + debugger/ipc/recorder.cpp + debugger/ipc/recorder.h + debugger/ipc/recorder.ui debugger/lle_service_modules.cpp debugger/lle_service_modules.h debugger/profiler.cpp diff --git a/src/citra_qt/debugger/ipc/recorder.cpp b/src/citra_qt/debugger/ipc/recorder.cpp new file mode 100644 index 0000000000..24128e38a1 --- /dev/null +++ b/src/citra_qt/debugger/ipc/recorder.cpp @@ -0,0 +1,183 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/debugger/ipc/record_dialog.h" +#include "citra_qt/debugger/ipc/recorder.h" +#include "common/assert.h" +#include "common/string_util.h" +#include "core/core.h" +#include "core/hle/kernel/ipc_debugger/recorder.h" +#include "core/hle/kernel/kernel.h" +#include "core/hle/service/sm/sm.h" +#include "ui_recorder.h" + +IPCRecorderWidget::IPCRecorderWidget(QWidget* parent) + : QDockWidget(parent), ui(std::make_unique()) { + + ui->setupUi(this); + qRegisterMetaType(); + + connect(ui->enabled, &QCheckBox::stateChanged, + [this](int new_state) { SetEnabled(new_state == Qt::Checked); }); + connect(ui->clearButton, &QPushButton::clicked, this, &IPCRecorderWidget::Clear); + connect(ui->filter, &QLineEdit::textChanged, this, &IPCRecorderWidget::ApplyFilterToAll); + connect(ui->main, &QTreeWidget::itemDoubleClicked, this, &IPCRecorderWidget::OpenRecordDialog); + connect(this, &IPCRecorderWidget::EntryUpdated, this, &IPCRecorderWidget::OnEntryUpdated); +} + +IPCRecorderWidget::~IPCRecorderWidget() = default; + +void IPCRecorderWidget::OnEmulationStarting() { + Clear(); + id_offset = 1; + + // Update the enabled status when the system is powered on. + SetEnabled(ui->enabled->isChecked()); +} + +QString IPCRecorderWidget::GetStatusStr(const IPCDebugger::RequestRecord& record) const { + switch (record.status) { + case IPCDebugger::RequestStatus::Invalid: + return tr("Invalid"); + case IPCDebugger::RequestStatus::Sent: + return tr("Sent"); + case IPCDebugger::RequestStatus::Handling: + return tr("Handling"); + case IPCDebugger::RequestStatus::Handled: + if (record.translated_reply_cmdbuf[1] == RESULT_SUCCESS.raw) { + return tr("Success"); + } + return tr("Error"); + case IPCDebugger::RequestStatus::HLEUnimplemented: + return tr("HLE Unimplemented"); + default: + UNREACHABLE(); + } +} + +void IPCRecorderWidget::OnEntryUpdated(IPCDebugger::RequestRecord record) { + if (record.id < id_offset) { // The record has already been deleted by 'Clear' + return; + } + + QString service = GetServiceName(record); + if (record.status == IPCDebugger::RequestStatus::Handling || + record.status == IPCDebugger::RequestStatus::Handled || + record.status == IPCDebugger::RequestStatus::HLEUnimplemented) { + + service = QStringLiteral("%1 (%2)").arg(service, record.is_hle ? tr("HLE") : tr("LLE")); + } + + QTreeWidgetItem item{ + {QString::number(record.id), GetStatusStr(record), service, GetFunctionName(record)}}; + + const int row_id = record.id - id_offset; + if (ui->main->invisibleRootItem()->childCount() > row_id) { + records[row_id] = record; + (*ui->main->invisibleRootItem()->child(row_id)) = item; + } else { + records.emplace_back(record); + ui->main->invisibleRootItem()->addChild(new QTreeWidgetItem(item)); + } + + if (record.status == IPCDebugger::RequestStatus::HLEUnimplemented || + (record.status == IPCDebugger::RequestStatus::Handled && + record.translated_reply_cmdbuf[1] != RESULT_SUCCESS.raw)) { // Unimplemented / Error + + auto* item = ui->main->invisibleRootItem()->child(row_id); + for (int column = 0; column < item->columnCount(); ++column) { + item->setBackgroundColor(column, QColor::fromRgb(255, 0, 0)); + } + } + + ApplyFilter(row_id); +} + +void IPCRecorderWidget::SetEnabled(bool enabled) { + if (!Core::System::GetInstance().IsPoweredOn()) { + return; + } + + auto& ipc_recorder = Core::System::GetInstance().Kernel().GetIPCRecorder(); + ipc_recorder.SetEnabled(enabled); + + if (enabled) { + handle = ipc_recorder.BindCallback( + [this](const IPCDebugger::RequestRecord& record) { emit EntryUpdated(record); }); + } else if (handle) { + ipc_recorder.UnbindCallback(handle); + } +} + +void IPCRecorderWidget::Clear() { + id_offset = records.size() + 1; + + records.clear(); + ui->main->invisibleRootItem()->takeChildren(); +} + +QString IPCRecorderWidget::GetServiceName(const IPCDebugger::RequestRecord& record) const { + if (Core::System::GetInstance().IsPoweredOn() && record.client_port.id != -1) { + const auto service_name = + Core::System::GetInstance().ServiceManager().GetServiceNameByPortId( + static_cast(record.client_port.id)); + + if (!service_name.empty()) { + return QString::fromStdString(service_name); + } + } + + // Get a similar result from the server session name + std::string session_name = record.server_session.name; + session_name = Common::ReplaceAll(session_name, "_Server", ""); + session_name = Common::ReplaceAll(session_name, "_Client", ""); + return QString::fromStdString(session_name); +} + +QString IPCRecorderWidget::GetFunctionName(const IPCDebugger::RequestRecord& record) const { + if (record.untranslated_request_cmdbuf.empty()) { // Cmdbuf is not yet available + return tr("Unknown"); + } + const QString header_code = + QStringLiteral("0x%1").arg(record.untranslated_request_cmdbuf[0], 8, 16, QLatin1Char('0')); + if (record.function_name.empty()) { + return header_code; + } + return QStringLiteral("%1 (%2)").arg(QString::fromStdString(record.function_name), header_code); +} + +void IPCRecorderWidget::ApplyFilter(int index) { + auto* item = ui->main->invisibleRootItem()->child(index); + const QString filter = ui->filter->text(); + if (filter.isEmpty()) { + item->setHidden(false); + return; + } + + for (int i = 0; i < item->columnCount(); ++i) { + if (item->text(i).contains(filter)) { + item->setHidden(false); + return; + } + } + + item->setHidden(true); +} + +void IPCRecorderWidget::ApplyFilterToAll() { + for (int i = 0; i < ui->main->invisibleRootItem()->childCount(); ++i) { + ApplyFilter(i); + } +} + +void IPCRecorderWidget::OpenRecordDialog(QTreeWidgetItem* item, [[maybe_unused]] int column) { + int index = ui->main->invisibleRootItem()->indexOfChild(item); + + RecordDialog dialog(this, records[static_cast(index)], item->text(2), + item->text(3)); + dialog.exec(); +} diff --git a/src/citra_qt/debugger/ipc/recorder.h b/src/citra_qt/debugger/ipc/recorder.h new file mode 100644 index 0000000000..63b1907901 --- /dev/null +++ b/src/citra_qt/debugger/ipc/recorder.h @@ -0,0 +1,52 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "core/hle/kernel/ipc_debugger/recorder.h" + +class QTreeWidgetItem; + +namespace Ui { +class IPCRecorder; +} + +class IPCRecorderWidget : public QDockWidget { + Q_OBJECT + +public: + explicit IPCRecorderWidget(QWidget* parent = nullptr); + ~IPCRecorderWidget(); + + void OnEmulationStarting(); + +signals: + void EntryUpdated(IPCDebugger::RequestRecord record); + +private: + QString GetStatusStr(const IPCDebugger::RequestRecord& record) const; + void OnEntryUpdated(IPCDebugger::RequestRecord record); + void SetEnabled(bool enabled); + void Clear(); + void ApplyFilter(int index); + void ApplyFilterToAll(); + QString GetServiceName(const IPCDebugger::RequestRecord& record) const; + QString GetFunctionName(const IPCDebugger::RequestRecord& record) const; + void OpenRecordDialog(QTreeWidgetItem* item, int column); + + std::unique_ptr ui; + IPCDebugger::CallbackHandle handle; + + // The offset between record id and row id, assuming record ids are assigned + // continuously and only the 'Clear' action can be performed, this is enough. + // The initial value is 1, which means record 1 = row 0. + int id_offset = 1; + std::vector records; +}; + +Q_DECLARE_METATYPE(IPCDebugger::RequestRecord); diff --git a/src/citra_qt/debugger/ipc/recorder.ui b/src/citra_qt/debugger/ipc/recorder.ui new file mode 100644 index 0000000000..78b988fdca --- /dev/null +++ b/src/citra_qt/debugger/ipc/recorder.ui @@ -0,0 +1,93 @@ + + + IPCRecorder + + + + 0 + 0 + 600 + 300 + + + + IPC Recorder + + + + + + + Enable Recording + + + + + + + + + Filter: + + + + + + + Leave empty to disable filtering + + + + + + + + + true + + + + # + + + + + Status + + + + + Service + + + + + Function + + + + + + + + + + Qt::Horizontal + + + + + + + Clear + + + + + + + + + + + diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index d1a31fa5c1..49e4588ae7 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -36,6 +36,7 @@ #include "citra_qt/debugger/graphics/graphics_surface.h" #include "citra_qt/debugger/graphics/graphics_tracing.h" #include "citra_qt/debugger/graphics/graphics_vertex_shader.h" +#include "citra_qt/debugger/ipc/recorder.h" #include "citra_qt/debugger/lle_service_modules.h" #include "citra_qt/debugger/profiler.h" #include "citra_qt/debugger/registers.h" @@ -328,6 +329,13 @@ void GMainWindow::InitializeDebugWidgets() { [this] { lleServiceModulesWidget->setDisabled(true); }); connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, [this] { lleServiceModulesWidget->setDisabled(false); }); + + ipcRecorderWidget = new IPCRecorderWidget(this); + addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); + ipcRecorderWidget->hide(); + debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, + &IPCRecorderWidget::OnEmulationStarting); } void GMainWindow::InitializeRecentFileMenuActions() { diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 780589141a..c5861cb2eb 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -30,6 +30,7 @@ class GraphicsBreakPointsWidget; class GraphicsTracingWidget; class GraphicsVertexShaderWidget; class GRenderWindow; +class IPCRecorderWidget; class LLEServiceModulesWidget; class MicroProfileDialog; class MultiplayerState; @@ -247,6 +248,7 @@ private: GraphicsBreakPointsWidget* graphicsBreakpointsWidget; GraphicsVertexShaderWidget* graphicsVertexShaderWidget; GraphicsTracingWidget* graphicsTracingWidget; + IPCRecorderWidget* ipcRecorderWidget; LLEServiceModulesWidget* lleServiceModulesWidget; WaitTreeWidget* waitTreeWidget; Updater* updater;