mirror of
https://github.com/citra-emu/citra.git
synced 2024-12-18 15:00:04 +00:00
citra_qt/debugger: Add recorder widget
This widget provides a simple list of recorded requests as well as a simple filter and the 'Clear' button. Requests with status 'HLE Unimplemented' or 'Error' will be marked out with the red color. This widget handles retrival of service name. For reasons refer to a previous commit message. Added the widget to the Debugging menu, pretty much the same as other debugging widgets.
This commit is contained in:
parent
45930e0621
commit
42cefdbff0
@ -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
|
||||
|
183
src/citra_qt/debugger/ipc/recorder.cpp
Normal file
183
src/citra_qt/debugger/ipc/recorder.cpp
Normal file
@ -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();
|
||||
}
|
52
src/citra_qt/debugger/ipc/recorder.h
Normal file
52
src/citra_qt/debugger/ipc/recorder.h
Normal file
@ -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);
|
93
src/citra_qt/debugger/ipc/recorder.ui
Normal file
93
src/citra_qt/debugger/ipc/recorder.ui
Normal file
@ -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>
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user