diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 9d5137a4f..0198ada76 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 000000000..24128e38a --- /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 <QString> +#include <QTreeWidgetItem> +#include <fmt/format.h> +#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::IPCRecorder>()) { + + ui->setupUi(this); + qRegisterMetaType<IPCDebugger::RequestRecord>(); + + 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<u32>(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<std::size_t>(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 000000000..63b190790 --- /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 <map> +#include <memory> +#include <unordered_map> +#include <QDockWidget> +#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::IPCRecorder> 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<IPCDebugger::RequestRecord> 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 000000000..78b988fdc --- /dev/null +++ b/src/citra_qt/debugger/ipc/recorder.ui @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>IPCRecorder</class> + <widget class="QDockWidget" name="IPCRecorder"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>IPC Recorder</string> + </property> + <widget class="QWidget"> + <layout class="QVBoxLayout"> + <item> + <widget class="QCheckBox" name="enabled"> + <property name="text"> + <string>Enable Recording</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout"> + <item> + <widget class="QLabel"> + <property name="text"> + <string>Filter:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="filter"> + <property name="placeholderText"> + <string>Leave empty to disable filtering</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTreeWidget" name="main"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>#</string> + </property> + </column> + <column> + <property name="text"> + <string>Status</string> + </property> + </column> + <column> + <property name="text"> + <string>Service</string> + </property> + </column> + <column> + <property name="text"> + <string>Function</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout"> + <item> + <spacer> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="clearButton"> + <property name="text"> + <string>Clear</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index d1a31fa5c..49e4588ae 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 780589141..c5861cb2e 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;