diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 6ac1eb2db..0198ada76 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -90,6 +90,12 @@ add_executable(citra-qt debugger/graphics/graphics_tracing.h debugger/graphics/graphics_vertex_shader.cpp debugger/graphics/graphics_vertex_shader.h + 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/record_dialog.cpp b/src/citra_qt/debugger/ipc/record_dialog.cpp new file mode 100644 index 000000000..2a179e10f --- /dev/null +++ b/src/citra_qt/debugger/ipc/record_dialog.cpp @@ -0,0 +1,64 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "citra_qt/debugger/ipc/record_dialog.h" +#include "common/assert.h" +#include "core/hle/kernel/ipc_debugger/recorder.h" +#include "ui_record_dialog.h" + +QString RecordDialog::FormatObject(const IPCDebugger::ObjectInfo& object) const { + if (object.id == -1) { + return tr("null"); + } + + return QStringLiteral("%1 (0x%2)") + .arg(QString::fromStdString(object.name)) + .arg(object.id, 8, 16, QLatin1Char('0')); +} + +QString RecordDialog::FormatCmdbuf(const std::vector& cmdbuf) const { + QString result; + for (std::size_t i = 0; i < cmdbuf.size(); ++i) { + result.append( + QStringLiteral("[%1]: 0x%2\n").arg(i).arg(cmdbuf[i], 8, 16, QLatin1Char('0'))); + } + return result; +} + +RecordDialog::RecordDialog(QWidget* parent, const IPCDebugger::RequestRecord& record, + const QString& service, const QString& function) + : QDialog(parent), ui(std::make_unique()) { + + ui->setupUi(this); + + ui->clientProcess->setText(FormatObject(record.client_process)); + ui->clientThread->setText(FormatObject(record.client_thread)); + ui->clientSession->setText(FormatObject(record.client_session)); + + ui->serverProcess->setText(FormatObject(record.server_process)); + ui->serverThread->setText(FormatObject(record.server_thread)); + ui->serverSession->setText(FormatObject(record.server_session)); + + ui->clientPort->setText(FormatObject(record.client_port)); + ui->service->setText(std::move(service)); + ui->function->setText(std::move(function)); + + cmdbufs = {record.untranslated_request_cmdbuf, record.translated_request_cmdbuf, + record.untranslated_reply_cmdbuf, record.translated_reply_cmdbuf}; + + UpdateCmdbufDisplay(); + + connect(ui->cmdbufSelection, qOverload(&QComboBox::currentIndexChanged), this, + &RecordDialog::UpdateCmdbufDisplay); +} + +RecordDialog::~RecordDialog() = default; + +void RecordDialog::UpdateCmdbufDisplay() { + int index = ui->cmdbufSelection->currentIndex(); + + ASSERT_MSG(0 <= index && index <= 3, "Index out of bound"); + ui->cmdbuf->setPlainText(FormatCmdbuf(cmdbufs[index])); +} diff --git a/src/citra_qt/debugger/ipc/record_dialog.h b/src/citra_qt/debugger/ipc/record_dialog.h new file mode 100644 index 000000000..40618b925 --- /dev/null +++ b/src/citra_qt/debugger/ipc/record_dialog.h @@ -0,0 +1,36 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/common_types.h" + +namespace IPCDebugger { +struct ObjectInfo; +struct RequestRecord; +} // namespace IPCDebugger + +namespace Ui { +class RecordDialog; +} + +class RecordDialog : public QDialog { + Q_OBJECT + +public: + explicit RecordDialog(QWidget* parent, const IPCDebugger::RequestRecord& record, + const QString& service, const QString& function); + ~RecordDialog() override; + +private: + QString FormatObject(const IPCDebugger::ObjectInfo& object) const; + QString FormatCmdbuf(const std::vector& cmdbuf) const; + void UpdateCmdbufDisplay(); + + std::unique_ptr ui; + std::array, 4> cmdbufs; +}; diff --git a/src/citra_qt/debugger/ipc/record_dialog.ui b/src/citra_qt/debugger/ipc/record_dialog.ui new file mode 100644 index 000000000..0f5c7a435 --- /dev/null +++ b/src/citra_qt/debugger/ipc/record_dialog.ui @@ -0,0 +1,245 @@ + + + RecordDialog + + + View Record + + + + 0 + 0 + 800 + 500 + + + + + + + + + Client + + + + + + Process: + + + + + + + true + + + + + + + Thread: + + + + + + + true + + + + + + + Session: + + + + + + + true + + + + + + + + + + Server + + + + + + Process: + + + + + + + true + + + + + + + Thread: + + + + + + + true + + + + + + + Session: + + + + + + + true + + + + + + + + + + + + General + + + + + + Client Port: + + + + + + + true + + + + + + + Service: + + + + + + + true + + + + + + + Function: + + + + + + + true + + + + + + + + + + Command Buffer + + + + + + + + Select: + + + + + + + + Request Untranslated + + + + + Request Translated + + + + + Reply Untranslated + + + + + Reply Translated + + + + + + + + + + true + + + + + + + + + + + + Qt::Horizontal + + + + + + + OK + + + + + + + + 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 +#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 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 +#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 000000000..78b988fdc --- /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 1b27b1860..599420fae 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -40,6 +40,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" @@ -339,6 +340,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 c86d99b79..3ff40f976 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; @@ -254,6 +255,7 @@ private: GraphicsBreakPointsWidget* graphicsBreakpointsWidget; GraphicsVertexShaderWidget* graphicsVertexShaderWidget; GraphicsTracingWidget* graphicsTracingWidget; + IPCRecorderWidget* ipcRecorderWidget; LLEServiceModulesWidget* lleServiceModulesWidget; WaitTreeWidget* waitTreeWidget; Updater* updater; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 12e923e1e..5ec409f1e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -134,6 +134,8 @@ add_library(core STATIC hle/kernel/hle_ipc.h hle/kernel/ipc.cpp hle/kernel/ipc.h + hle/kernel/ipc_debugger/recorder.cpp + hle/kernel/ipc_debugger/recorder.h hle/kernel/kernel.cpp hle/kernel/kernel.h hle/kernel/memory.cpp diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp index cd2f9fc87..ab4ecfd05 100644 --- a/src/core/hle/kernel/hle_ipc.cpp +++ b/src/core/hle/kernel/hle_ipc.cpp @@ -10,6 +10,7 @@ #include "core/hle/kernel/event.h" #include "core/hle/kernel/handle_table.h" #include "core/hle/kernel/hle_ipc.h" +#include "core/hle/kernel/ipc_debugger/recorder.h" #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/process.h" @@ -107,6 +108,13 @@ ResultCode HLERequestContext::PopulateFromIncomingCommandBuffer(const u32_le* sr std::copy_n(src_cmdbuf, untranslated_size, cmd_buf.begin()); + const bool should_record = kernel.GetIPCRecorder().IsEnabled(); + + std::vector untranslated_cmdbuf; + if (should_record) { + untranslated_cmdbuf = std::vector{src_cmdbuf, src_cmdbuf + command_size}; + } + std::size_t i = untranslated_size; while (i < command_size) { u32 descriptor = cmd_buf[i] = src_cmdbuf[i]; @@ -160,6 +168,12 @@ ResultCode HLERequestContext::PopulateFromIncomingCommandBuffer(const u32_le* sr } } + if (should_record) { + std::vector translated_cmdbuf{cmd_buf.begin(), cmd_buf.begin() + command_size}; + kernel.GetIPCRecorder().SetRequestInfo(SharedFrom(thread), std::move(untranslated_cmdbuf), + std::move(translated_cmdbuf)); + } + return RESULT_SUCCESS; } @@ -173,6 +187,13 @@ ResultCode HLERequestContext::WriteToOutgoingCommandBuffer(u32_le* dst_cmdbuf, std::copy_n(cmd_buf.begin(), untranslated_size, dst_cmdbuf); + const bool should_record = kernel.GetIPCRecorder().IsEnabled(); + + std::vector untranslated_cmdbuf; + if (should_record) { + untranslated_cmdbuf = std::vector{cmd_buf.begin(), cmd_buf.begin() + command_size}; + } + std::size_t i = untranslated_size; while (i < command_size) { u32 descriptor = dst_cmdbuf[i] = cmd_buf[i]; @@ -225,6 +246,12 @@ ResultCode HLERequestContext::WriteToOutgoingCommandBuffer(u32_le* dst_cmdbuf, } } + if (should_record) { + std::vector translated_cmdbuf{dst_cmdbuf, dst_cmdbuf + command_size}; + kernel.GetIPCRecorder().SetReplyInfo(SharedFrom(thread), std::move(untranslated_cmdbuf), + std::move(translated_cmdbuf)); + } + return RESULT_SUCCESS; } @@ -233,6 +260,12 @@ MappedBuffer& HLERequestContext::GetMappedBuffer(u32 id_from_cmdbuf) { return request_mapped_buffers[id_from_cmdbuf]; } +void HLERequestContext::ReportUnimplemented() const { + if (kernel.GetIPCRecorder().IsEnabled()) { + kernel.GetIPCRecorder().SetHLEUnimplemented(SharedFrom(thread)); + } +} + MappedBuffer::MappedBuffer(Memory::MemorySystem& memory, const Process& process, u32 descriptor, VAddr address, u32 id) : memory(&memory), id(id), address(address), process(&process) { diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index f684985d0..26942fe6b 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -234,6 +234,9 @@ public: /// Writes data from this context back to the requesting process/thread. ResultCode WriteToOutgoingCommandBuffer(u32_le* dst_cmdbuf, Process& dst_process) const; + /// Reports an unimplemented function. + void ReportUnimplemented() const; + private: KernelSystem& kernel; std::array cmd_buf; diff --git a/src/core/hle/kernel/ipc.cpp b/src/core/hle/kernel/ipc.cpp index c9616e566..3dacb9831 100644 --- a/src/core/hle/kernel/ipc.cpp +++ b/src/core/hle/kernel/ipc.cpp @@ -8,6 +8,7 @@ #include "core/hle/ipc.h" #include "core/hle/kernel/handle_table.h" #include "core/hle/kernel/ipc.h" +#include "core/hle/kernel/ipc_debugger/recorder.h" #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/memory.h" #include "core/hle/kernel/process.h" @@ -16,7 +17,8 @@ namespace Kernel { -ResultCode TranslateCommandBuffer(Memory::MemorySystem& memory, std::shared_ptr src_thread, +ResultCode TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem& memory, + std::shared_ptr src_thread, std::shared_ptr dst_thread, VAddr src_address, VAddr dst_address, std::vector& mapped_buffer_context, @@ -37,6 +39,13 @@ ResultCode TranslateCommandBuffer(Memory::MemorySystem& memory, std::shared_ptr< std::array cmd_buf; memory.ReadBlock(*src_process, src_address, cmd_buf.data(), command_size * sizeof(u32)); + const bool should_record = kernel.GetIPCRecorder().IsEnabled(); + + std::vector untranslated_cmdbuf; + if (should_record) { + untranslated_cmdbuf = std::vector{cmd_buf.begin(), cmd_buf.begin() + command_size}; + } + std::size_t i = untranslated_size; while (i < command_size) { u32 descriptor = cmd_buf[i]; @@ -218,6 +227,17 @@ ResultCode TranslateCommandBuffer(Memory::MemorySystem& memory, std::shared_ptr< } } + if (should_record) { + std::vector translated_cmdbuf{cmd_buf.begin(), cmd_buf.begin() + command_size}; + if (reply) { + kernel.GetIPCRecorder().SetReplyInfo(dst_thread, std::move(untranslated_cmdbuf), + std::move(translated_cmdbuf)); + } else { + kernel.GetIPCRecorder().SetRequestInfo(src_thread, std::move(untranslated_cmdbuf), + std::move(translated_cmdbuf), dst_thread); + } + } + memory.WriteBlock(*dst_process, dst_address, cmd_buf.data(), command_size * sizeof(u32)); return RESULT_SUCCESS; diff --git a/src/core/hle/kernel/ipc.h b/src/core/hle/kernel/ipc.h index d84028767..b06079958 100644 --- a/src/core/hle/kernel/ipc.h +++ b/src/core/hle/kernel/ipc.h @@ -16,6 +16,8 @@ class MemorySystem; namespace Kernel { +class KernelSystem; + struct MappedBufferContext { IPC::MappedBufferPermissions permissions; u32 size; @@ -27,7 +29,8 @@ struct MappedBufferContext { }; /// Performs IPC command buffer translation from one process to another. -ResultCode TranslateCommandBuffer(Memory::MemorySystem& memory, std::shared_ptr src_thread, +ResultCode TranslateCommandBuffer(KernelSystem& system, Memory::MemorySystem& memory, + std::shared_ptr src_thread, std::shared_ptr dst_thread, VAddr src_address, VAddr dst_address, std::vector& mapped_buffer_context, diff --git a/src/core/hle/kernel/ipc_debugger/recorder.cpp b/src/core/hle/kernel/ipc_debugger/recorder.cpp new file mode 100644 index 000000000..968815c5b --- /dev/null +++ b/src/core/hle/kernel/ipc_debugger/recorder.cpp @@ -0,0 +1,165 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/hle/kernel/client_port.h" +#include "core/hle/kernel/client_session.h" +#include "core/hle/kernel/ipc_debugger/recorder.h" +#include "core/hle/kernel/process.h" +#include "core/hle/kernel/server_port.h" +#include "core/hle/kernel/server_session.h" +#include "core/hle/kernel/session.h" +#include "core/hle/kernel/thread.h" +#include "core/hle/service/service.h" + +namespace IPCDebugger { + +namespace { +ObjectInfo GetObjectInfo(const Kernel::Object* object) { + if (object == nullptr) { + return {}; + } + return {object->GetTypeName(), object->GetName(), static_cast(object->GetObjectId())}; +} + +ObjectInfo GetObjectInfo(const Kernel::Thread* thread) { + if (thread == nullptr) { + return {}; + } + return {thread->GetTypeName(), thread->GetName(), static_cast(thread->GetThreadId())}; +} + +ObjectInfo GetObjectInfo(const Kernel::Process* process) { + if (process == nullptr) { + return {}; + } + return {process->GetTypeName(), process->GetName(), static_cast(process->process_id)}; +} +} // namespace + +Recorder::Recorder() = default; +Recorder::~Recorder() = default; + +bool Recorder::IsEnabled() const { + return enabled.load(std::memory_order_relaxed); +} + +void Recorder::RegisterRequest(const std::shared_ptr& client_session, + const std::shared_ptr& client_thread) { + const u32 thread_id = client_thread->GetThreadId(); + + RequestRecord record = {/* id */ ++record_count, + /* status */ RequestStatus::Sent, + /* client_process */ GetObjectInfo(client_thread->owner_process), + /* client_thread */ GetObjectInfo(client_thread.get()), + /* client_session */ GetObjectInfo(client_session.get()), + /* client_port */ GetObjectInfo(client_session->parent->port.get()), + /* server_process */ {}, + /* server_thread */ {}, + /* server_session */ GetObjectInfo(client_session->parent->server)}; + record_map.insert_or_assign(thread_id, std::make_unique(record)); + client_session_map.insert_or_assign(thread_id, client_session); + + InvokeCallbacks(record); +} + +void Recorder::SetRequestInfo(const std::shared_ptr& client_thread, + std::vector untranslated_cmdbuf, + std::vector translated_cmdbuf, + const std::shared_ptr& server_thread) { + const u32 thread_id = client_thread->GetThreadId(); + if (!record_map.count(thread_id)) { + // This is possible when the recorder is enabled after application started + LOG_ERROR(Kernel, "No request is assoicated with the thread"); + return; + } + + auto& record = *record_map[thread_id]; + record.status = RequestStatus::Handling; + record.untranslated_request_cmdbuf = std::move(untranslated_cmdbuf); + record.translated_request_cmdbuf = std::move(translated_cmdbuf); + + if (server_thread) { + record.server_process = GetObjectInfo(server_thread->owner_process); + record.server_thread = GetObjectInfo(server_thread.get()); + } else { + record.is_hle = true; + } + + // Function name + ASSERT_MSG(client_session_map.count(thread_id), "Client session is missing"); + const auto& client_session = client_session_map[thread_id]; + if (client_session->parent->port && + client_session->parent->port->GetServerPort()->hle_handler) { + + record.function_name = std::dynamic_pointer_cast( + client_session->parent->port->GetServerPort()->hle_handler) + ->GetFunctionName(record.untranslated_request_cmdbuf[0]); + } + client_session_map.erase(thread_id); + + InvokeCallbacks(record); +} + +void Recorder::SetReplyInfo(const std::shared_ptr& client_thread, + std::vector untranslated_cmdbuf, + std::vector translated_cmdbuf) { + const u32 thread_id = client_thread->GetThreadId(); + if (!record_map.count(thread_id)) { + // This is possible when the recorder is enabled after application started + LOG_ERROR(Kernel, "No request is assoicated with the thread"); + return; + } + + auto& record = *record_map[thread_id]; + if (record.status != RequestStatus::HLEUnimplemented) { + record.status = RequestStatus::Handled; + } + + record.untranslated_reply_cmdbuf = std::move(untranslated_cmdbuf); + record.translated_reply_cmdbuf = std::move(translated_cmdbuf); + InvokeCallbacks(record); + + record_map.erase(thread_id); +} + +void Recorder::SetHLEUnimplemented(const std::shared_ptr& client_thread) { + const u32 thread_id = client_thread->GetThreadId(); + if (!record_map.count(thread_id)) { + // This is possible when the recorder is enabled after application started + LOG_ERROR(Kernel, "No request is assoicated with the thread"); + return; + } + + auto& record = *record_map[thread_id]; + record.status = RequestStatus::HLEUnimplemented; +} + +CallbackHandle Recorder::BindCallback(CallbackType callback) { + std::unique_lock lock(callback_mutex); + CallbackHandle handle = std::make_shared(callback); + callbacks.emplace(handle); + return handle; +} + +void Recorder::UnbindCallback(const CallbackHandle& handle) { + std::unique_lock lock(callback_mutex); + callbacks.erase(handle); +} + +void Recorder::InvokeCallbacks(const RequestRecord& request) { + { + std::shared_lock lock(callback_mutex); + for (const auto& iter : callbacks) { + (*iter)(request); + } + } +} + +void Recorder::SetEnabled(bool enabled_) { + enabled.store(enabled_, std::memory_order_relaxed); +} + +} // namespace IPCDebugger diff --git a/src/core/hle/kernel/ipc_debugger/recorder.h b/src/core/hle/kernel/ipc_debugger/recorder.h new file mode 100644 index 000000000..ebd8bf5d1 --- /dev/null +++ b/src/core/hle/kernel/ipc_debugger/recorder.h @@ -0,0 +1,129 @@ +// 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 +#include +#include +#include +#include "common/common_types.h" + +namespace Kernel { +class ClientSession; +class Thread; +} // namespace Kernel + +namespace IPCDebugger { + +/** + * Record of a kernel object, for debugging purposes. + */ +struct ObjectInfo { + std::string type; + std::string name; + int id = -1; +}; + +/** + * Status of a request. + */ +enum class RequestStatus { + Invalid, ///< Invalid status + Sent, ///< The request is sent to the kernel and is waiting to be handled + Handling, ///< The request is being handled + Handled, ///< The request is handled with reply sent + HLEUnimplemented, ///< The request is unimplemented by HLE, and unhandled +}; + +/** + * Record of an IPC request. + */ +struct RequestRecord { + int id; + RequestStatus status = RequestStatus::Invalid; + ObjectInfo client_process; + ObjectInfo client_thread; + ObjectInfo client_session; + ObjectInfo client_port; // Not available for portless + ObjectInfo server_process; // Only available for LLE requests + ObjectInfo server_thread; // Only available for LLE requests + ObjectInfo server_session; + std::string function_name; // Not available for LLE or portless + bool is_hle = false; + // Request info is only available when status is not `Invalid` or `Sent` + std::vector untranslated_request_cmdbuf; + std::vector translated_request_cmdbuf; + // Reply info is only available when status is `Handled` + std::vector untranslated_reply_cmdbuf; + std::vector translated_reply_cmdbuf; +}; + +using CallbackType = std::function; +using CallbackHandle = std::shared_ptr; + +class Recorder { +public: + explicit Recorder(); + ~Recorder(); + + /** + * Returns whether the recorder is enabled. + */ + bool IsEnabled() const; + + /** + * Registers a request into the recorder. The request is then assoicated with the client thread. + */ + void RegisterRequest(const std::shared_ptr& client_session, + const std::shared_ptr& client_thread); + + /** + * Sets the request information of the request record associated with the client thread. + * When the server thread is empty, the request will be considered HLE. + */ + void SetRequestInfo(const std::shared_ptr& client_thread, + std::vector untranslated_cmdbuf, std::vector translated_cmdbuf, + const std::shared_ptr& server_thread = {}); + + /** + * Sets the reply information of the request record assoicated with the client thread. + * The request is then unlinked from the client thread. + */ + void SetReplyInfo(const std::shared_ptr& client_thread, + std::vector untranslated_cmdbuf, std::vector translated_cmdbuf); + + /** + * Set the status of a record to HLEUnimplemented. + */ + void SetHLEUnimplemented(const std::shared_ptr& client_thread); + + /** + * Set the status of the debugger (enabled/disabled). + */ + void SetEnabled(bool enabled); + + CallbackHandle BindCallback(CallbackType callback); + void UnbindCallback(const CallbackHandle& handle); + +private: + void InvokeCallbacks(const RequestRecord& request); + + std::unordered_map> record_map; + int record_count{}; + + // Temporary client session map for function name handling + std::unordered_map> client_session_map; + + std::atomic_bool enabled{false}; + + std::set callbacks; + mutable std::shared_mutex callback_mutex; +}; + +} // namespace IPCDebugger diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index f480f6362..ceb2f14f5 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -5,6 +5,7 @@ #include "core/hle/kernel/client_port.h" #include "core/hle/kernel/config_mem.h" #include "core/hle/kernel/handle_table.h" +#include "core/hle/kernel/ipc_debugger/recorder.h" #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/memory.h" #include "core/hle/kernel/process.h" @@ -25,6 +26,7 @@ KernelSystem::KernelSystem(Memory::MemorySystem& memory, Core::Timing& timing, resource_limits = std::make_unique(*this); thread_manager = std::make_unique(*this); timer_manager = std::make_unique(timing); + ipc_recorder = std::make_unique(); } /// Shutdown the kernel @@ -87,6 +89,14 @@ const SharedPage::Handler& KernelSystem::GetSharedPageHandler() const { return *shared_page_handler; } +IPCDebugger::Recorder& KernelSystem::GetIPCRecorder() { + return *ipc_recorder; +} + +const IPCDebugger::Recorder& KernelSystem::GetIPCRecorder() const { + return *ipc_recorder; +} + void KernelSystem::AddNamedPort(std::string name, std::shared_ptr port) { named_ports.emplace(std::move(name), std::move(port)); } diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index 38045a425..58f63938b 100644 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h @@ -32,6 +32,10 @@ namespace Core { class Timing; } +namespace IPCDebugger { +class Recorder; +} + namespace Kernel { class AddressArbiter; @@ -222,6 +226,9 @@ public: SharedPage::Handler& GetSharedPageHandler(); const SharedPage::Handler& GetSharedPageHandler() const; + IPCDebugger::Recorder& GetIPCRecorder(); + const IPCDebugger::Recorder& GetIPCRecorder() const; + MemoryRegionInfo* GetMemoryRegion(MemoryRegion region); void HandleSpecialMapping(VMManager& address_space, const AddressMapping& mapping); @@ -274,6 +281,8 @@ private: std::unique_ptr config_mem_handler; std::unique_ptr shared_page_handler; + + std::unique_ptr ipc_recorder; }; } // namespace Kernel diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 88a0ff5ba..b5ebaf936 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -19,6 +19,7 @@ #include "core/hle/kernel/event.h" #include "core/hle/kernel/handle_table.h" #include "core/hle/kernel/ipc.h" +#include "core/hle/kernel/ipc_debugger/recorder.h" #include "core/hle/kernel/memory.h" #include "core/hle/kernel/mutex.h" #include "core/hle/kernel/process.h" @@ -387,7 +388,13 @@ ResultCode SVC::SendSyncRequest(Handle handle) { system.PrepareReschedule(); - return session->SendSyncRequest(SharedFrom(kernel.GetThreadManager().GetCurrentThread())); + auto thread = SharedFrom(kernel.GetThreadManager().GetCurrentThread()); + + if (kernel.GetIPCRecorder().IsEnabled()) { + kernel.GetIPCRecorder().RegisterRequest(session, thread); + } + + return session->SendSyncRequest(thread); } /// Close a handle @@ -593,7 +600,7 @@ ResultCode SVC::WaitSynchronizationN(s32* out, VAddr handles_address, s32 handle } } -static ResultCode ReceiveIPCRequest(Memory::MemorySystem& memory, +static ResultCode ReceiveIPCRequest(Kernel::KernelSystem& kernel, Memory::MemorySystem& memory, std::shared_ptr server_session, std::shared_ptr thread) { if (server_session->parent->client == nullptr) { @@ -603,9 +610,9 @@ static ResultCode ReceiveIPCRequest(Memory::MemorySystem& memory, VAddr target_address = thread->GetCommandBufferAddress(); VAddr source_address = server_session->currently_handling->GetCommandBufferAddress(); - ResultCode translation_result = - TranslateCommandBuffer(memory, server_session->currently_handling, thread, source_address, - target_address, server_session->mapped_buffer_context, false); + ResultCode translation_result = TranslateCommandBuffer( + kernel, memory, server_session->currently_handling, thread, source_address, target_address, + server_session->mapped_buffer_context, false); // If a translation error occurred, immediately resume the client thread. if (translation_result.IsError()) { @@ -670,9 +677,9 @@ ResultCode SVC::ReplyAndReceive(s32* index, VAddr handles_address, s32 handle_co VAddr source_address = thread->GetCommandBufferAddress(); VAddr target_address = request_thread->GetCommandBufferAddress(); - ResultCode translation_result = - TranslateCommandBuffer(memory, SharedFrom(thread), request_thread, source_address, - target_address, session->mapped_buffer_context, true); + ResultCode translation_result = TranslateCommandBuffer( + kernel, memory, SharedFrom(thread), request_thread, source_address, target_address, + session->mapped_buffer_context, true); // Note: The real kernel seems to always panic if the Server->Client buffer translation // fails for whatever reason. @@ -707,7 +714,7 @@ ResultCode SVC::ReplyAndReceive(s32* index, VAddr handles_address, s32 handle_co return RESULT_SUCCESS; auto server_session = static_cast(object); - return ReceiveIPCRequest(memory, SharedFrom(server_session), SharedFrom(thread)); + return ReceiveIPCRequest(kernel, memory, SharedFrom(server_session), SharedFrom(thread)); } // No objects were ready to be acquired, prepare to suspend the thread. @@ -723,9 +730,9 @@ ResultCode SVC::ReplyAndReceive(s32* index, VAddr handles_address, s32 handle_co thread->wait_objects = std::move(objects); - thread->wakeup_callback = [& memory = this->memory](ThreadWakeupReason reason, - std::shared_ptr thread, - std::shared_ptr object) { + thread->wakeup_callback = [& kernel = this->kernel, &memory = this->memory]( + ThreadWakeupReason reason, std::shared_ptr thread, + std::shared_ptr object) { ASSERT(thread->status == ThreadStatus::WaitSynchAny); ASSERT(reason == ThreadWakeupReason::Signal); @@ -733,7 +740,7 @@ ResultCode SVC::ReplyAndReceive(s32* index, VAddr handles_address, s32 handle_co if (object->GetHandleType() == HandleType::ServerSession) { auto server_session = DynamicObjectCast(object); - result = ReceiveIPCRequest(memory, server_session, thread); + result = ReceiveIPCRequest(kernel, memory, server_session, thread); } thread->SetWaitSynchronizationResult(result); diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp index 461d0930f..0310d4ad9 100644 --- a/src/core/hle/service/service.cpp +++ b/src/core/hle/service/service.cpp @@ -171,6 +171,7 @@ void ServiceFrameworkBase::HandleSyncRequest(Kernel::HLERequestContext& context) auto itr = handlers.find(header_code); const FunctionInfoBase* info = itr == handlers.end() ? nullptr : &itr->second; if (info == nullptr || info->handler_callback == nullptr) { + context.ReportUnimplemented(); return ReportUnimplementedFunction(context.CommandBuffer(), info); } @@ -179,6 +180,14 @@ void ServiceFrameworkBase::HandleSyncRequest(Kernel::HLERequestContext& context) handler_invoker(this, info->handler_callback, context); } +std::string ServiceFrameworkBase::GetFunctionName(u32 header) const { + if (!handlers.count(header)) { + return ""; + } + + return handlers.at(header).name; +} + //////////////////////////////////////////////////////////////////////////////////////////////////// // Module interface diff --git a/src/core/hle/service/service.h b/src/core/hle/service/service.h index 1d9e800e2..db6a0ad23 100644 --- a/src/core/hle/service/service.h +++ b/src/core/hle/service/service.h @@ -64,6 +64,9 @@ public: void HandleSyncRequest(Kernel::HLERequestContext& context) override; + /// Retrieves name of a function based on the header code. For IPC Recorder. + std::string GetFunctionName(u32 header) const; + protected: /// Member-function pointer type of SyncRequest handlers. template diff --git a/src/core/hle/service/sm/sm.cpp b/src/core/hle/service/sm/sm.cpp index e37c72cc1..3caca2f93 100644 --- a/src/core/hle/service/sm/sm.cpp +++ b/src/core/hle/service/sm/sm.cpp @@ -42,6 +42,7 @@ ResultVal> ServiceManager::RegisterService( auto [server_port, client_port] = system.Kernel().CreatePortPair(max_sessions, name); + registered_services_inverse.emplace(client_port->GetObjectId(), name); registered_services.emplace(std::move(name), std::move(client_port)); return MakeResult(std::move(server_port)); } @@ -65,4 +66,12 @@ ResultVal> ServiceManager::ConnectToServi return client_port->Connect(); } +std::string ServiceManager::GetServiceNameByPortId(u32 port) const { + if (registered_services_inverse.count(port)) { + return registered_services_inverse.at(port); + } + + return ""; +} + } // namespace Service::SM diff --git a/src/core/hle/service/sm/sm.h b/src/core/hle/service/sm/sm.h index dc26d8814..6e47fd152 100644 --- a/src/core/hle/service/sm/sm.h +++ b/src/core/hle/service/sm/sm.h @@ -51,6 +51,8 @@ public: unsigned int max_sessions); ResultVal> GetServicePort(const std::string& name); ResultVal> ConnectToService(const std::string& name); + // For IPC Recorder + std::string GetServiceNameByPortId(u32 port) const; template std::shared_ptr GetService(const std::string& service_name) const { @@ -74,6 +76,10 @@ private: /// Map of registered services, retrieved using GetServicePort or ConnectToService. std::unordered_map> registered_services; + + // For IPC Recorder + /// client port Object id -> service name + std::unordered_map registered_services_inverse; }; } // namespace Service::SM