summaryrefslogtreecommitdiffstats
path: root/src/citra_qt/debugger/graphics
diff options
context:
space:
mode:
Diffstat (limited to 'src/citra_qt/debugger/graphics')
-rw-r--r--src/citra_qt/debugger/graphics/graphics.cpp77
-rw-r--r--src/citra_qt/debugger/graphics/graphics.h41
-rw-r--r--src/citra_qt/debugger/graphics/graphics_breakpoint_observer.cpp27
-rw-r--r--src/citra_qt/debugger/graphics/graphics_breakpoint_observer.h33
-rw-r--r--src/citra_qt/debugger/graphics/graphics_breakpoints.cpp213
-rw-r--r--src/citra_qt/debugger/graphics/graphics_breakpoints.h46
-rw-r--r--src/citra_qt/debugger/graphics/graphics_breakpoints_p.h36
-rw-r--r--src/citra_qt/debugger/graphics/graphics_cmdlists.cpp259
-rw-r--r--src/citra_qt/debugger/graphics/graphics_cmdlists.h61
-rw-r--r--src/citra_qt/debugger/graphics/graphics_surface.cpp710
-rw-r--r--src/citra_qt/debugger/graphics/graphics_surface.h118
-rw-r--r--src/citra_qt/debugger/graphics/graphics_tracing.cpp178
-rw-r--r--src/citra_qt/debugger/graphics/graphics_tracing.h33
-rw-r--r--src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp629
-rw-r--r--src/citra_qt/debugger/graphics/graphics_vertex_shader.h87
15 files changed, 2548 insertions, 0 deletions
diff --git a/src/citra_qt/debugger/graphics/graphics.cpp b/src/citra_qt/debugger/graphics/graphics.cpp
new file mode 100644
index 000000000..6a76adeae
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics.cpp
@@ -0,0 +1,77 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QListView>
+#include "citra_qt/debugger/graphics/graphics.h"
+#include "citra_qt/util/util.h"
+
+extern GraphicsDebugger g_debugger;
+
+GPUCommandStreamItemModel::GPUCommandStreamItemModel(QObject* parent)
+ : QAbstractListModel(parent), command_count(0) {
+ connect(this, SIGNAL(GXCommandFinished(int)), this, SLOT(OnGXCommandFinishedInternal(int)));
+}
+
+int GPUCommandStreamItemModel::rowCount(const QModelIndex& parent) const {
+ return command_count;
+}
+
+QVariant GPUCommandStreamItemModel::data(const QModelIndex& index, int role) const {
+ if (!index.isValid())
+ return QVariant();
+
+ int command_index = index.row();
+ const Service::GSP::Command& command = GetDebugger()->ReadGXCommandHistory(command_index);
+ if (role == Qt::DisplayRole) {
+ std::map<Service::GSP::CommandId, const char*> command_names = {
+ {Service::GSP::CommandId::REQUEST_DMA, "REQUEST_DMA"},
+ {Service::GSP::CommandId::SUBMIT_GPU_CMDLIST, "SUBMIT_GPU_CMDLIST"},
+ {Service::GSP::CommandId::SET_MEMORY_FILL, "SET_MEMORY_FILL"},
+ {Service::GSP::CommandId::SET_DISPLAY_TRANSFER, "SET_DISPLAY_TRANSFER"},
+ {Service::GSP::CommandId::SET_TEXTURE_COPY, "SET_TEXTURE_COPY"},
+ {Service::GSP::CommandId::CACHE_FLUSH, "CACHE_FLUSH"},
+ };
+ const u32* command_data = reinterpret_cast<const u32*>(&command);
+ QString str = QString("%1 %2 %3 %4 %5 %6 %7 %8 %9")
+ .arg(command_names[command.id])
+ .arg(command_data[0], 8, 16, QLatin1Char('0'))
+ .arg(command_data[1], 8, 16, QLatin1Char('0'))
+ .arg(command_data[2], 8, 16, QLatin1Char('0'))
+ .arg(command_data[3], 8, 16, QLatin1Char('0'))
+ .arg(command_data[4], 8, 16, QLatin1Char('0'))
+ .arg(command_data[5], 8, 16, QLatin1Char('0'))
+ .arg(command_data[6], 8, 16, QLatin1Char('0'))
+ .arg(command_data[7], 8, 16, QLatin1Char('0'));
+ return QVariant(str);
+ } else {
+ return QVariant();
+ }
+}
+
+void GPUCommandStreamItemModel::GXCommandProcessed(int total_command_count) {
+ emit GXCommandFinished(total_command_count);
+}
+
+void GPUCommandStreamItemModel::OnGXCommandFinishedInternal(int total_command_count) {
+ if (total_command_count == 0)
+ return;
+
+ int prev_command_count = command_count;
+ command_count = total_command_count;
+ emit dataChanged(index(prev_command_count, 0), index(total_command_count - 1, 0));
+}
+
+GPUCommandStreamWidget::GPUCommandStreamWidget(QWidget* parent)
+ : QDockWidget(tr("Graphics Debugger"), parent) {
+ setObjectName("GraphicsDebugger");
+
+ GPUCommandStreamItemModel* command_model = new GPUCommandStreamItemModel(this);
+ g_debugger.RegisterObserver(command_model);
+
+ QListView* command_list = new QListView;
+ command_list->setModel(command_model);
+ command_list->setFont(GetMonospaceFont());
+
+ setWidget(command_list);
+}
diff --git a/src/citra_qt/debugger/graphics/graphics.h b/src/citra_qt/debugger/graphics/graphics.h
new file mode 100644
index 000000000..8837fb792
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics.h
@@ -0,0 +1,41 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QDockWidget>
+#include "video_core/gpu_debugger.h"
+
+class GPUCommandStreamItemModel : public QAbstractListModel,
+ public GraphicsDebugger::DebuggerObserver {
+ Q_OBJECT
+
+public:
+ explicit GPUCommandStreamItemModel(QObject* parent);
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
+public:
+ void GXCommandProcessed(int total_command_count) override;
+
+public slots:
+ void OnGXCommandFinishedInternal(int total_command_count);
+
+signals:
+ void GXCommandFinished(int total_command_count);
+
+private:
+ int command_count;
+};
+
+class GPUCommandStreamWidget : public QDockWidget {
+ Q_OBJECT
+
+public:
+ GPUCommandStreamWidget(QWidget* parent = nullptr);
+
+private:
+};
diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.cpp b/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.cpp
new file mode 100644
index 000000000..dc6070dea
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.cpp
@@ -0,0 +1,27 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QMetaType>
+#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h"
+
+BreakPointObserverDock::BreakPointObserverDock(std::shared_ptr<Pica::DebugContext> debug_context,
+ const QString& title, QWidget* parent)
+ : QDockWidget(title, parent), BreakPointObserver(debug_context) {
+ qRegisterMetaType<Pica::DebugContext::Event>("Pica::DebugContext::Event");
+
+ connect(this, SIGNAL(Resumed()), this, SLOT(OnResumed()));
+
+ // NOTE: This signal is emitted from a non-GUI thread, but connect() takes
+ // care of delaying its handling to the GUI thread.
+ connect(this, SIGNAL(BreakPointHit(Pica::DebugContext::Event, void*)), this,
+ SLOT(OnBreakPointHit(Pica::DebugContext::Event, void*)), Qt::BlockingQueuedConnection);
+}
+
+void BreakPointObserverDock::OnPicaBreakPointHit(Pica::DebugContext::Event event, void* data) {
+ emit BreakPointHit(event, data);
+}
+
+void BreakPointObserverDock::OnPicaResume() {
+ emit Resumed();
+}
diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.h b/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.h
new file mode 100644
index 000000000..e77df4f5b
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.h
@@ -0,0 +1,33 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QDockWidget>
+#include "video_core/debug_utils/debug_utils.h"
+
+/**
+ * Utility class which forwards calls to OnPicaBreakPointHit and OnPicaResume to public slots.
+ * This is because the Pica breakpoint callbacks are called from a non-GUI thread, while
+ * the widget usually wants to perform reactions in the GUI thread.
+ */
+class BreakPointObserverDock : public QDockWidget,
+ protected Pica::DebugContext::BreakPointObserver {
+ Q_OBJECT
+
+public:
+ BreakPointObserverDock(std::shared_ptr<Pica::DebugContext> debug_context, const QString& title,
+ QWidget* parent = nullptr);
+
+ void OnPicaBreakPointHit(Pica::DebugContext::Event event, void* data) override;
+ void OnPicaResume() override;
+
+private slots:
+ virtual void OnBreakPointHit(Pica::DebugContext::Event event, void* data) = 0;
+ virtual void OnResumed() = 0;
+
+signals:
+ void Resumed();
+ void BreakPointHit(Pica::DebugContext::Event event, void* data);
+};
diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoints.cpp b/src/citra_qt/debugger/graphics/graphics_breakpoints.cpp
new file mode 100644
index 000000000..030828ba8
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_breakpoints.cpp
@@ -0,0 +1,213 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QLabel>
+#include <QMetaType>
+#include <QPushButton>
+#include <QTreeView>
+#include <QVBoxLayout>
+#include "citra_qt/debugger/graphics/graphics_breakpoints.h"
+#include "citra_qt/debugger/graphics/graphics_breakpoints_p.h"
+#include "common/assert.h"
+
+BreakPointModel::BreakPointModel(std::shared_ptr<Pica::DebugContext> debug_context, QObject* parent)
+ : QAbstractListModel(parent), context_weak(debug_context),
+ at_breakpoint(debug_context->at_breakpoint),
+ active_breakpoint(debug_context->active_breakpoint) {}
+
+int BreakPointModel::columnCount(const QModelIndex& parent) const {
+ return 1;
+}
+
+int BreakPointModel::rowCount(const QModelIndex& parent) const {
+ return static_cast<int>(Pica::DebugContext::Event::NumEvents);
+}
+
+QVariant BreakPointModel::data(const QModelIndex& index, int role) const {
+ const auto event = static_cast<Pica::DebugContext::Event>(index.row());
+
+ switch (role) {
+ case Qt::DisplayRole: {
+ if (index.column() == 0) {
+ static const std::map<Pica::DebugContext::Event, QString> map = {
+ {Pica::DebugContext::Event::PicaCommandLoaded, tr("Pica command loaded")},
+ {Pica::DebugContext::Event::PicaCommandProcessed, tr("Pica command processed")},
+ {Pica::DebugContext::Event::IncomingPrimitiveBatch, tr("Incoming primitive batch")},
+ {Pica::DebugContext::Event::FinishedPrimitiveBatch, tr("Finished primitive batch")},
+ {Pica::DebugContext::Event::VertexShaderInvocation, tr("Vertex shader invocation")},
+ {Pica::DebugContext::Event::IncomingDisplayTransfer,
+ tr("Incoming display transfer")},
+ {Pica::DebugContext::Event::GSPCommandProcessed, tr("GSP command processed")},
+ {Pica::DebugContext::Event::BufferSwapped, tr("Buffers swapped")},
+ };
+
+ DEBUG_ASSERT(map.size() == static_cast<size_t>(Pica::DebugContext::Event::NumEvents));
+ return (map.find(event) != map.end()) ? map.at(event) : QString();
+ }
+
+ break;
+ }
+
+ case Qt::CheckStateRole: {
+ if (index.column() == 0)
+ return data(index, Role_IsEnabled).toBool() ? Qt::Checked : Qt::Unchecked;
+ break;
+ }
+
+ case Qt::BackgroundRole: {
+ if (at_breakpoint && index.row() == static_cast<int>(active_breakpoint)) {
+ return QBrush(QColor(0xE0, 0xE0, 0x10));
+ }
+ break;
+ }
+
+ case Role_IsEnabled: {
+ auto context = context_weak.lock();
+ return context && context->breakpoints[(int)event].enabled;
+ }
+
+ default:
+ break;
+ }
+ return QVariant();
+}
+
+Qt::ItemFlags BreakPointModel::flags(const QModelIndex& index) const {
+ if (!index.isValid())
+ return 0;
+
+ Qt::ItemFlags flags = Qt::ItemIsEnabled;
+ if (index.column() == 0)
+ flags |= Qt::ItemIsUserCheckable;
+ return flags;
+}
+
+bool BreakPointModel::setData(const QModelIndex& index, const QVariant& value, int role) {
+ const auto event = static_cast<Pica::DebugContext::Event>(index.row());
+
+ switch (role) {
+ case Qt::CheckStateRole: {
+ if (index.column() != 0)
+ return false;
+
+ auto context = context_weak.lock();
+ if (!context)
+ return false;
+
+ context->breakpoints[(int)event].enabled = value == Qt::Checked;
+ QModelIndex changed_index = createIndex(index.row(), 0);
+ emit dataChanged(changed_index, changed_index);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void BreakPointModel::OnBreakPointHit(Pica::DebugContext::Event event) {
+ auto context = context_weak.lock();
+ if (!context)
+ return;
+
+ active_breakpoint = context->active_breakpoint;
+ at_breakpoint = context->at_breakpoint;
+ emit dataChanged(createIndex(static_cast<int>(event), 0),
+ createIndex(static_cast<int>(event), 0));
+}
+
+void BreakPointModel::OnResumed() {
+ auto context = context_weak.lock();
+ if (!context)
+ return;
+
+ at_breakpoint = context->at_breakpoint;
+ emit dataChanged(createIndex(static_cast<int>(active_breakpoint), 0),
+ createIndex(static_cast<int>(active_breakpoint), 0));
+ active_breakpoint = context->active_breakpoint;
+}
+
+GraphicsBreakPointsWidget::GraphicsBreakPointsWidget(
+ std::shared_ptr<Pica::DebugContext> debug_context, QWidget* parent)
+ : QDockWidget(tr("Pica Breakpoints"), parent),
+ Pica::DebugContext::BreakPointObserver(debug_context) {
+ setObjectName("PicaBreakPointsWidget");
+
+ status_text = new QLabel(tr("Emulation running"));
+ resume_button = new QPushButton(tr("Resume"));
+ resume_button->setEnabled(false);
+
+ breakpoint_model = new BreakPointModel(debug_context, this);
+ breakpoint_list = new QTreeView;
+ breakpoint_list->setRootIsDecorated(false);
+ breakpoint_list->setHeaderHidden(true);
+ breakpoint_list->setModel(breakpoint_model);
+
+ qRegisterMetaType<Pica::DebugContext::Event>("Pica::DebugContext::Event");
+
+ connect(breakpoint_list, SIGNAL(doubleClicked(const QModelIndex&)), this,
+ SLOT(OnItemDoubleClicked(const QModelIndex&)));
+
+ connect(resume_button, SIGNAL(clicked()), this, SLOT(OnResumeRequested()));
+
+ connect(this, SIGNAL(BreakPointHit(Pica::DebugContext::Event, void*)), this,
+ SLOT(OnBreakPointHit(Pica::DebugContext::Event, void*)), Qt::BlockingQueuedConnection);
+ connect(this, SIGNAL(Resumed()), this, SLOT(OnResumed()));
+
+ connect(this, SIGNAL(BreakPointHit(Pica::DebugContext::Event, void*)), breakpoint_model,
+ SLOT(OnBreakPointHit(Pica::DebugContext::Event)), Qt::BlockingQueuedConnection);
+ connect(this, SIGNAL(Resumed()), breakpoint_model, SLOT(OnResumed()));
+
+ connect(this, SIGNAL(BreakPointsChanged(const QModelIndex&, const QModelIndex&)),
+ breakpoint_model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)));
+
+ QWidget* main_widget = new QWidget;
+ auto main_layout = new QVBoxLayout;
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(status_text);
+ sub_layout->addWidget(resume_button);
+ main_layout->addLayout(sub_layout);
+ }
+ main_layout->addWidget(breakpoint_list);
+ main_widget->setLayout(main_layout);
+
+ setWidget(main_widget);
+}
+
+void GraphicsBreakPointsWidget::OnPicaBreakPointHit(Event event, void* data) {
+ // Process in GUI thread
+ emit BreakPointHit(event, data);
+}
+
+void GraphicsBreakPointsWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) {
+ status_text->setText(tr("Emulation halted at breakpoint"));
+ resume_button->setEnabled(true);
+}
+
+void GraphicsBreakPointsWidget::OnPicaResume() {
+ // Process in GUI thread
+ emit Resumed();
+}
+
+void GraphicsBreakPointsWidget::OnResumed() {
+ status_text->setText(tr("Emulation running"));
+ resume_button->setEnabled(false);
+}
+
+void GraphicsBreakPointsWidget::OnResumeRequested() {
+ if (auto context = context_weak.lock())
+ context->Resume();
+}
+
+void GraphicsBreakPointsWidget::OnItemDoubleClicked(const QModelIndex& index) {
+ if (!index.isValid())
+ return;
+
+ QModelIndex check_index = breakpoint_list->model()->index(index.row(), 0);
+ QVariant enabled = breakpoint_list->model()->data(check_index, Qt::CheckStateRole);
+ QVariant new_state = Qt::Unchecked;
+ if (enabled == Qt::Unchecked)
+ new_state = Qt::Checked;
+ breakpoint_list->model()->setData(check_index, new_state, Qt::CheckStateRole);
+}
diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoints.h b/src/citra_qt/debugger/graphics/graphics_breakpoints.h
new file mode 100644
index 000000000..bec72a2db
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_breakpoints.h
@@ -0,0 +1,46 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QDockWidget>
+#include "video_core/debug_utils/debug_utils.h"
+
+class QLabel;
+class QPushButton;
+class QTreeView;
+
+class BreakPointModel;
+
+class GraphicsBreakPointsWidget : public QDockWidget, Pica::DebugContext::BreakPointObserver {
+ Q_OBJECT
+
+ using Event = Pica::DebugContext::Event;
+
+public:
+ explicit GraphicsBreakPointsWidget(std::shared_ptr<Pica::DebugContext> debug_context,
+ QWidget* parent = nullptr);
+
+ void OnPicaBreakPointHit(Pica::DebugContext::Event event, void* data) override;
+ void OnPicaResume() override;
+
+public slots:
+ void OnBreakPointHit(Pica::DebugContext::Event event, void* data);
+ void OnItemDoubleClicked(const QModelIndex&);
+ void OnResumeRequested();
+ void OnResumed();
+
+signals:
+ void Resumed();
+ void BreakPointHit(Pica::DebugContext::Event event, void* data);
+ void BreakPointsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight);
+
+private:
+ QLabel* status_text;
+ QPushButton* resume_button;
+
+ BreakPointModel* breakpoint_model;
+ QTreeView* breakpoint_list;
+};
diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoints_p.h b/src/citra_qt/debugger/graphics/graphics_breakpoints_p.h
new file mode 100644
index 000000000..dc64706bd
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_breakpoints_p.h
@@ -0,0 +1,36 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QAbstractListModel>
+#include "video_core/debug_utils/debug_utils.h"
+
+class BreakPointModel : public QAbstractListModel {
+ Q_OBJECT
+
+public:
+ enum {
+ Role_IsEnabled = Qt::UserRole,
+ };
+
+ BreakPointModel(std::shared_ptr<Pica::DebugContext> context, QObject* parent);
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ Qt::ItemFlags flags(const QModelIndex& index) const override;
+
+ bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
+
+public slots:
+ void OnBreakPointHit(Pica::DebugContext::Event event);
+ void OnResumed();
+
+private:
+ std::weak_ptr<Pica::DebugContext> context_weak;
+ bool at_breakpoint;
+ Pica::DebugContext::Event active_breakpoint;
+};
diff --git a/src/citra_qt/debugger/graphics/graphics_cmdlists.cpp b/src/citra_qt/debugger/graphics/graphics_cmdlists.cpp
new file mode 100644
index 000000000..dab529e3a
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_cmdlists.cpp
@@ -0,0 +1,259 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QApplication>
+#include <QClipboard>
+#include <QComboBox>
+#include <QHeaderView>
+#include <QLabel>
+#include <QListView>
+#include <QMainWindow>
+#include <QPushButton>
+#include <QSpinBox>
+#include <QTreeView>
+#include <QVBoxLayout>
+#include "citra_qt/debugger/graphics/graphics_cmdlists.h"
+#include "citra_qt/util/spinbox.h"
+#include "citra_qt/util/util.h"
+#include "common/vector_math.h"
+#include "video_core/debug_utils/debug_utils.h"
+#include "video_core/pica.h"
+#include "video_core/pica_state.h"
+
+namespace {
+QImage LoadTexture(const u8* src, const Pica::DebugUtils::TextureInfo& info) {
+ QImage decoded_image(info.width, info.height, QImage::Format_ARGB32);
+ for (int y = 0; y < info.height; ++y) {
+ for (int x = 0; x < info.width; ++x) {
+ Math::Vec4<u8> color = Pica::DebugUtils::LookupTexture(src, x, y, info, true);
+ decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), color.a()));
+ }
+ }
+
+ return decoded_image;
+}
+
+class TextureInfoWidget : public QWidget {
+public:
+ TextureInfoWidget(const u8* src, const Pica::DebugUtils::TextureInfo& info,
+ QWidget* parent = nullptr)
+ : QWidget(parent) {
+ QLabel* image_widget = new QLabel;
+ QPixmap image_pixmap = QPixmap::fromImage(LoadTexture(src, info));
+ image_pixmap = image_pixmap.scaled(200, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ image_widget->setPixmap(image_pixmap);
+
+ QVBoxLayout* layout = new QVBoxLayout;
+ layout->addWidget(image_widget);
+ setLayout(layout);
+ }
+};
+} // Anonymous namespace
+
+GPUCommandListModel::GPUCommandListModel(QObject* parent) : QAbstractListModel(parent) {}
+
+int GPUCommandListModel::rowCount(const QModelIndex& parent) const {
+ return static_cast<int>(pica_trace.writes.size());
+}
+
+int GPUCommandListModel::columnCount(const QModelIndex& parent) const {
+ return 4;
+}
+
+QVariant GPUCommandListModel::data(const QModelIndex& index, int role) const {
+ if (!index.isValid())
+ return QVariant();
+
+ const auto& write = pica_trace.writes[index.row()];
+
+ if (role == Qt::DisplayRole) {
+ switch (index.column()) {
+ case 0:
+ return QString::fromLatin1(Pica::Regs::GetCommandName(write.cmd_id).c_str());
+ case 1:
+ return QString("%1").arg(write.cmd_id, 3, 16, QLatin1Char('0'));
+ case 2:
+ return QString("%1").arg(write.mask, 4, 2, QLatin1Char('0'));
+ case 3:
+ return QString("%1").arg(write.value, 8, 16, QLatin1Char('0'));
+ }
+ } else if (role == CommandIdRole) {
+ return QVariant::fromValue<int>(write.cmd_id);
+ }
+
+ return QVariant();
+}
+
+QVariant GPUCommandListModel::headerData(int section, Qt::Orientation orientation, int role) const {
+ switch (role) {
+ case Qt::DisplayRole: {
+ switch (section) {
+ case 0:
+ return tr("Command Name");
+ case 1:
+ return tr("Register");
+ case 2:
+ return tr("Mask");
+ case 3:
+ return tr("New Value");
+ }
+
+ break;
+ }
+ }
+
+ return QVariant();
+}
+
+void GPUCommandListModel::OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace& trace) {
+ beginResetModel();
+
+ pica_trace = trace;
+
+ endResetModel();
+}
+
+#define COMMAND_IN_RANGE(cmd_id, reg_name) \
+ (cmd_id >= PICA_REG_INDEX(reg_name) && \
+ cmd_id < PICA_REG_INDEX(reg_name) + sizeof(decltype(Pica::g_state.regs.reg_name)) / 4)
+
+void GPUCommandListWidget::OnCommandDoubleClicked(const QModelIndex& index) {
+ const unsigned int command_id =
+ list_widget->model()->data(index, GPUCommandListModel::CommandIdRole).toUInt();
+ if (COMMAND_IN_RANGE(command_id, texture0) || COMMAND_IN_RANGE(command_id, texture1) ||
+ COMMAND_IN_RANGE(command_id, texture2)) {
+
+ unsigned texture_index;
+ if (COMMAND_IN_RANGE(command_id, texture0)) {
+ texture_index = 0;
+ } else if (COMMAND_IN_RANGE(command_id, texture1)) {
+ texture_index = 1;
+ } else if (COMMAND_IN_RANGE(command_id, texture2)) {
+ texture_index = 2;
+ } else {
+ UNREACHABLE_MSG("Unknown texture command");
+ }
+
+ const auto texture = Pica::g_state.regs.GetTextures()[texture_index];
+ const auto config = texture.config;
+ const auto format = texture.format;
+ const auto info = Pica::DebugUtils::TextureInfo::FromPicaRegister(config, format);
+
+ // TODO: Open a surface debugger
+ }
+}
+
+void GPUCommandListWidget::SetCommandInfo(const QModelIndex& index) {
+ QWidget* new_info_widget = nullptr;
+
+ const unsigned int command_id =
+ list_widget->model()->data(index, GPUCommandListModel::CommandIdRole).toUInt();
+ if (COMMAND_IN_RANGE(command_id, texture0) || COMMAND_IN_RANGE(command_id, texture1) ||
+ COMMAND_IN_RANGE(command_id, texture2)) {
+
+ unsigned texture_index;
+ if (COMMAND_IN_RANGE(command_id, texture0)) {
+ texture_index = 0;
+ } else if (COMMAND_IN_RANGE(command_id, texture1)) {
+ texture_index = 1;
+ } else {
+ texture_index = 2;
+ }
+
+ const auto texture = Pica::g_state.regs.GetTextures()[texture_index];
+ const auto config = texture.config;
+ const auto format = texture.format;
+
+ const auto info = Pica::DebugUtils::TextureInfo::FromPicaRegister(config, format);
+ const u8* src = Memory::GetPhysicalPointer(config.GetPhysicalAddress());
+ new_info_widget = new TextureInfoWidget(src, info);
+ }
+ if (command_info_widget) {
+ delete command_info_widget;
+ command_info_widget = nullptr;
+ }
+ if (new_info_widget) {
+ widget()->layout()->addWidget(new_info_widget);
+ command_info_widget = new_info_widget;
+ }
+}
+#undef COMMAND_IN_RANGE
+
+GPUCommandListWidget::GPUCommandListWidget(QWidget* parent)
+ : QDockWidget(tr("Pica Command List"), parent) {
+ setObjectName("Pica Command List");
+ GPUCommandListModel* model = new GPUCommandListModel(this);
+
+ QWidget* main_widget = new QWidget;
+
+ list_widget = new QTreeView;
+ list_widget->setModel(model);
+ list_widget->setFont(GetMonospaceFont());
+ list_widget->setRootIsDecorated(false);
+ list_widget->setUniformRowHeights(true);
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+ list_widget->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+#else
+ list_widget->header()->setResizeMode(QHeaderView::ResizeToContents);
+#endif
+
+ connect(list_widget->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), this,
+ SLOT(SetCommandInfo(const QModelIndex&)));
+ connect(list_widget, SIGNAL(doubleClicked(const QModelIndex&)), this,
+ SLOT(OnCommandDoubleClicked(const QModelIndex&)));
+
+ toggle_tracing = new QPushButton(tr("Start Tracing"));
+ QPushButton* copy_all = new QPushButton(tr("Copy All"));
+
+ connect(toggle_tracing, SIGNAL(clicked()), this, SLOT(OnToggleTracing()));
+ connect(this, SIGNAL(TracingFinished(const Pica::DebugUtils::PicaTrace&)), model,
+ SLOT(OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace&)));
+
+ connect(copy_all, SIGNAL(clicked()), this, SLOT(CopyAllToClipboard()));
+
+ command_info_widget = nullptr;
+
+ QVBoxLayout* main_layout = new QVBoxLayout;
+ main_layout->addWidget(list_widget);
+ {
+ QHBoxLayout* sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(toggle_tracing);
+ sub_layout->addWidget(copy_all);
+ main_layout->addLayout(sub_layout);
+ }
+ main_widget->setLayout(main_layout);
+
+ setWidget(main_widget);
+}
+
+void GPUCommandListWidget::OnToggleTracing() {
+ if (!Pica::DebugUtils::IsPicaTracing()) {
+ Pica::DebugUtils::StartPicaTracing();
+ toggle_tracing->setText(tr("Finish Tracing"));
+ } else {
+ pica_trace = Pica::DebugUtils::FinishPicaTracing();
+ emit TracingFinished(*pica_trace);
+ toggle_tracing->setText(tr("Start Tracing"));
+ }
+}
+
+void GPUCommandListWidget::CopyAllToClipboard() {
+ QClipboard* clipboard = QApplication::clipboard();
+ QString text;
+
+ QAbstractItemModel* model = static_cast<QAbstractItemModel*>(list_widget->model());
+
+ for (int row = 0; row < model->rowCount({}); ++row) {
+ for (int col = 0; col < model->columnCount({}); ++col) {
+ QModelIndex index = model->index(row, col);
+ text += model->data(index).value<QString>();
+ text += '\t';
+ }
+ text += '\n';
+ }
+
+ clipboard->setText(text);
+}
diff --git a/src/citra_qt/debugger/graphics/graphics_cmdlists.h b/src/citra_qt/debugger/graphics/graphics_cmdlists.h
new file mode 100644
index 000000000..8f40b94c5
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_cmdlists.h
@@ -0,0 +1,61 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QDockWidget>
+#include "video_core/debug_utils/debug_utils.h"
+#include "video_core/gpu_debugger.h"
+
+class QPushButton;
+class QTreeView;
+
+class GPUCommandListModel : public QAbstractListModel {
+ Q_OBJECT
+
+public:
+ enum {
+ CommandIdRole = Qt::UserRole,
+ };
+
+ explicit GPUCommandListModel(QObject* parent);
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+
+public slots:
+ void OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace& trace);
+
+private:
+ Pica::DebugUtils::PicaTrace pica_trace;
+};
+
+class GPUCommandListWidget : public QDockWidget {
+ Q_OBJECT
+
+public:
+ explicit GPUCommandListWidget(QWidget* parent = nullptr);
+
+public slots:
+ void OnToggleTracing();
+ void OnCommandDoubleClicked(const QModelIndex&);
+
+ void SetCommandInfo(const QModelIndex&);
+
+ void CopyAllToClipboard();
+
+signals:
+ void TracingFinished(const Pica::DebugUtils::PicaTrace&);
+
+private:
+ std::unique_ptr<Pica::DebugUtils::PicaTrace> pica_trace;
+
+ QTreeView* list_widget;
+ QWidget* command_info_widget;
+ QPushButton* toggle_tracing;
+};
diff --git a/src/citra_qt/debugger/graphics/graphics_surface.cpp b/src/citra_qt/debugger/graphics/graphics_surface.cpp
new file mode 100644
index 000000000..4efd95d3c
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_surface.cpp
@@ -0,0 +1,710 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QBoxLayout>
+#include <QComboBox>
+#include <QDebug>
+#include <QFileDialog>
+#include <QLabel>
+#include <QMouseEvent>
+#include <QPushButton>
+#include <QScrollArea>
+#include <QSpinBox>
+#include "citra_qt/debugger/graphics/graphics_surface.h"
+#include "citra_qt/util/spinbox.h"
+#include "common/color.h"
+#include "core/hw/gpu.h"
+#include "core/memory.h"
+#include "video_core/pica.h"
+#include "video_core/pica_state.h"
+#include "video_core/utils.h"
+
+SurfacePicture::SurfacePicture(QWidget* parent, GraphicsSurfaceWidget* surface_widget_)
+ : QLabel(parent), surface_widget(surface_widget_) {}
+SurfacePicture::~SurfacePicture() {}
+
+void SurfacePicture::mousePressEvent(QMouseEvent* event) {
+ // Only do something while the left mouse button is held down
+ if (!(event->buttons() & Qt::LeftButton))
+ return;
+
+ if (pixmap() == nullptr)
+ return;
+
+ if (surface_widget)
+ surface_widget->Pick(event->x() * pixmap()->width() / width(),
+ event->y() * pixmap()->height() / height());
+}
+
+void SurfacePicture::mouseMoveEvent(QMouseEvent* event) {
+ // We also want to handle the event if the user moves the mouse while holding down the LMB
+ mousePressEvent(event);
+}
+
+GraphicsSurfaceWidget::GraphicsSurfaceWidget(std::shared_ptr<Pica::DebugContext> debug_context,
+ QWidget* parent)
+ : BreakPointObserverDock(debug_context, tr("Pica Surface Viewer"), parent),
+ surface_source(Source::ColorBuffer) {
+ setObjectName("PicaSurface");
+
+ surface_source_list = new QComboBox;
+ surface_source_list->addItem(tr("Color Buffer"));
+ surface_source_list->addItem(tr("Depth Buffer"));
+ surface_source_list->addItem(tr("Stencil Buffer"));
+ surface_source_list->addItem(tr("Texture 0"));
+ surface_source_list->addItem(tr("Texture 1"));
+ surface_source_list->addItem(tr("Texture 2"));
+ surface_source_list->addItem(tr("Custom"));
+ surface_source_list->setCurrentIndex(static_cast<int>(surface_source));
+
+ surface_address_control = new CSpinBox;
+ surface_address_control->SetBase(16);
+ surface_address_control->SetRange(0, 0xFFFFFFFF);
+ surface_address_control->SetPrefix("0x");
+
+ unsigned max_dimension = 16384; // TODO: Find actual maximum
+
+ surface_width_control = new QSpinBox;
+ surface_width_control->setRange(0, max_dimension);
+
+ surface_height_control = new QSpinBox;
+ surface_height_control->setRange(0, max_dimension);
+
+ surface_picker_x_control = new QSpinBox;
+ surface_picker_x_control->setRange(0, max_dimension - 1);
+
+ surface_picker_y_control = new QSpinBox;
+ surface_picker_y_control->setRange(0, max_dimension - 1);
+
+ surface_format_control = new QComboBox;
+
+ // Color formats sorted by Pica texture format index
+ surface_format_control->addItem(tr("RGBA8"));
+ surface_format_control->addItem(tr("RGB8"));
+ surface_format_control->addItem(tr("RGB5A1"));
+ surface_format_control->addItem(tr("RGB565"));
+ surface_format_control->addItem(tr("RGBA4"));
+ surface_format_control->addItem(tr("IA8"));
+ surface_format_control->addItem(tr("RG8"));
+ surface_format_control->addItem(tr("I8"));
+ surface_format_control->addItem(tr("A8"));
+ surface_format_control->addItem(tr("IA4"));
+ surface_format_control->addItem(tr("I4"));
+ surface_format_control->addItem(tr("A4"));
+ surface_format_control->addItem(tr("ETC1"));
+ surface_format_control->addItem(tr("ETC1A4"));
+ surface_format_control->addItem(tr("D16"));
+ surface_format_control->addItem(tr("D24"));
+ surface_format_control->addItem(tr("D24X8"));
+ surface_format_control->addItem(tr("X24S8"));
+ surface_format_control->addItem(tr("Unknown"));
+
+ surface_info_label = new QLabel();
+ surface_info_label->setWordWrap(true);
+
+ surface_picture_label = new SurfacePicture(0, this);
+ surface_picture_label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ surface_picture_label->setAlignment(Qt::AlignLeft | Qt::AlignTop);
+ surface_picture_label->setScaledContents(false);
+
+ auto scroll_area = new QScrollArea();
+ scroll_area->setBackgroundRole(QPalette::Dark);
+ scroll_area->setWidgetResizable(false);
+ scroll_area->setWidget(surface_picture_label);
+
+ save_surface = new QPushButton(QIcon::fromTheme("document-save"), tr("Save"));
+
+ // Connections
+ connect(this, SIGNAL(Update()), this, SLOT(OnUpdate()));
+ connect(surface_source_list, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(OnSurfaceSourceChanged(int)));
+ connect(surface_address_control, SIGNAL(ValueChanged(qint64)), this,
+ SLOT(OnSurfaceAddressChanged(qint64)));
+ connect(surface_width_control, SIGNAL(valueChanged(int)), this,
+ SLOT(OnSurfaceWidthChanged(int)));
+ connect(surface_height_control, SIGNAL(valueChanged(int)), this,
+ SLOT(OnSurfaceHeightChanged(int)));
+ connect(surface_format_control, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(OnSurfaceFormatChanged(int)));
+ connect(surface_picker_x_control, SIGNAL(valueChanged(int)), this,
+ SLOT(OnSurfacePickerXChanged(int)));
+ connect(surface_picker_y_control, SIGNAL(valueChanged(int)), this,
+ SLOT(OnSurfacePickerYChanged(int)));
+ connect(save_surface, SIGNAL(clicked()), this, SLOT(SaveSurface()));
+
+ auto main_widget = new QWidget;
+ auto main_layout = new QVBoxLayout;
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(new QLabel(tr("Source:")));
+ sub_layout->addWidget(surface_source_list);
+ main_layout->addLayout(sub_layout);
+ }
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(new QLabel(tr("Physical Address:")));
+ sub_layout->addWidget(surface_address_control);
+ main_layout->addLayout(sub_layout);
+ }
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(new QLabel(tr("Width:")));
+ sub_layout->addWidget(surface_width_control);
+ main_layout->addLayout(sub_layout);
+ }
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(new QLabel(tr("Height:")));
+ sub_layout->addWidget(surface_height_control);
+ main_layout->addLayout(sub_layout);
+ }
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(new QLabel(tr("Format:")));
+ sub_layout->addWidget(surface_format_control);
+ main_layout->addLayout(sub_layout);
+ }
+ main_layout->addWidget(scroll_area);
+
+ auto info_layout = new QHBoxLayout;
+ {
+ auto xy_layout = new QVBoxLayout;
+ {
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(new QLabel(tr("X:")));
+ sub_layout->addWidget(surface_picker_x_control);
+ xy_layout->addLayout(sub_layout);
+ }
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(new QLabel(tr("Y:")));
+ sub_layout->addWidget(surface_picker_y_control);
+ xy_layout->addLayout(sub_layout);
+ }
+ }
+ info_layout->addLayout(xy_layout);
+ surface_info_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
+ info_layout->addWidget(surface_info_label);
+ }
+ main_layout->addLayout(info_layout);
+
+ main_layout->addWidget(save_surface);
+ main_widget->setLayout(main_layout);
+ setWidget(main_widget);
+
+ // Load current data - TODO: Make sure this works when emulation is not running
+ if (debug_context && debug_context->at_breakpoint) {
+ emit Update();
+ widget()->setEnabled(debug_context->at_breakpoint);
+ } else {
+ widget()->setEnabled(false);
+ }
+}
+
+void GraphicsSurfaceWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) {
+ emit Update();
+ widget()->setEnabled(true);
+}
+
+void GraphicsSurfaceWidget::OnResumed() {
+ widget()->setEnabled(false);
+}
+
+void GraphicsSurfaceWidget::OnSurfaceSourceChanged(int new_value) {
+ surface_source = static_cast<Source>(new_value);
+ emit Update();
+}
+
+void GraphicsSurfaceWidget::OnSurfaceAddressChanged(qint64 new_value) {
+ if (surface_address != new_value) {
+ surface_address = static_cast<unsigned>(new_value);
+
+ surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom));
+ emit Update();
+ }
+}
+
+void GraphicsSurfaceWidget::OnSurfaceWidthChanged(int new_value) {
+ if (surface_width != static_cast<unsigned>(new_value)) {
+ surface_width = static_cast<unsigned>(new_value);
+
+ surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom));
+ emit Update();
+ }
+}
+
+void GraphicsSurfaceWidget::OnSurfaceHeightChanged(int new_value) {
+ if (surface_height != static_cast<unsigned>(new_value)) {
+ surface_height = static_cast<unsigned>(new_value);
+
+ surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom));
+ emit Update();
+ }
+}
+
+void GraphicsSurfaceWidget::OnSurfaceFormatChanged(int new_value) {
+ if (surface_format != static_cast<Format>(new_value)) {
+ surface_format = static_cast<Format>(new_value);
+
+ surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom));
+ emit Update();
+ }
+}
+
+void GraphicsSurfaceWidget::OnSurfacePickerXChanged(int new_value) {
+ if (surface_picker_x != new_value) {
+ surface_picker_x = new_value;
+ Pick(surface_picker_x, surface_picker_y);
+ }
+}
+
+void GraphicsSurfaceWidget::OnSurfacePickerYChanged(int new_value) {
+ if (surface_picker_y != new_value) {
+ surface_picker_y = new_value;
+ Pick(surface_picker_x, surface_picker_y);
+ }
+}
+
+void GraphicsSurfaceWidget::Pick(int x, int y) {
+ surface_picker_x_control->setValue(x);
+ surface_picker_y_control->setValue(y);
+
+ if (x < 0 || x >= surface_width || y < 0 || y >= surface_height) {
+ surface_info_label->setText(tr("Pixel out of bounds"));
+ surface_info_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ return;
+ }
+
+ u8* buffer = Memory::GetPhysicalPointer(surface_address);
+ if (buffer == nullptr) {
+ surface_info_label->setText(tr("(unable to access pixel data)"));
+ surface_info_label->setAlignment(Qt::AlignCenter);
+ return;
+ }
+
+ unsigned nibbles_per_pixel = GraphicsSurfaceWidget::NibblesPerPixel(surface_format);
+ unsigned stride = nibbles_per_pixel * surface_width / 2;
+
+ unsigned bytes_per_pixel;
+ bool nibble_mode = (nibbles_per_pixel == 1);
+ if (nibble_mode) {
+ // As nibbles are contained in a byte we still need to access one byte per nibble
+ bytes_per_pixel = 1;
+ } else {
+ bytes_per_pixel = nibbles_per_pixel / 2;
+ }
+
+ const u32 coarse_y = y & ~7;
+ u32 offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * stride;
+ const u8* pixel = buffer + (nibble_mode ? (offset / 2) : offset);
+
+ auto GetText = [offset](Format format, const u8* pixel) {
+ switch (format) {
+ case Format::RGBA8: {
+ auto value = Color::DecodeRGBA8(pixel) / 255.0f;
+ return QString("Red: %1, Green: %2, Blue: %3, Alpha: %4")
+ .arg(QString::number(value.r(), 'f', 2))
+ .arg(QString::number(value.g(), 'f', 2))
+ .arg(QString::number(value.b(), 'f', 2))
+ .arg(QString::number(value.a(), 'f', 2));
+ }
+ case Format::RGB8: {
+ auto value = Color::DecodeRGB8(pixel) / 255.0f;
+ return QString("Red: %1, Green: %2, Blue: %3")
+ .arg(QString::number(value.r(), 'f', 2))
+ .arg(QString::number(value.g(), 'f', 2))
+ .arg(QString::number(value.b(), 'f', 2));
+ }
+ case Format::RGB5A1: {
+ auto value = Color::DecodeRGB5A1(pixel) / 255.0f;
+ return QString("Red: %1, Green: %2, Blue: %3, Alpha: %4")
+ .arg(QString::number(value.r(), 'f', 2))
+ .arg(QString::number(value.g(), 'f', 2))
+ .arg(QString::number(value.b(), 'f', 2))
+ .arg(QString::number(value.a(), 'f', 2));
+ }
+ case Format::RGB565: {
+ auto value = Color::DecodeRGB565(pixel) / 255.0f;
+ return QString("Red: %1, Green: %2, Blue: %3")
+ .arg(QString::number(value.r(), 'f', 2))
+ .arg(QString::number(value.g(), 'f', 2))
+ .arg(QString::number(value.b(), 'f', 2));
+ }
+ case Format::RGBA4: {
+ auto value = Color::DecodeRGBA4(pixel) / 255.0f;
+ return QString("Red: %1, Green: %2, Blue: %3, Alpha: %4")
+ .arg(QString::number(value.r(), 'f', 2))
+ .arg(QString::number(value.g(), 'f', 2))
+ .arg(QString::number(value.b(), 'f', 2))
+ .arg(QString::number(value.a(), 'f', 2));
+ }
+ case Format::IA8:
+ return QString("Index: %1, Alpha: %2").arg(pixel[0]).arg(pixel[1]);
+ case Format::RG8: {
+ auto value = Color::DecodeRG8(pixel) / 255.0f;
+ return QString("Red: %1, Green: %2")
+ .arg(QString::number(value.r(), 'f', 2))
+ .arg(QString::number(value.g(), 'f', 2));
+ }
+ case Format::I8:
+ return QString("Index: %1").arg(*pixel);
+ case Format::A8:
+ return QString("Alpha: %1").arg(QString::number(*pixel / 255.0f, 'f', 2));
+ case Format::IA4:
+ return QString("Index: %1, Alpha: %2").arg(*pixel & 0xF).arg((*pixel & 0xF0) >> 4);
+ case Format::I4: {
+ u8 i = (*pixel >> ((offset % 2) ? 4 : 0)) & 0xF;
+ return QString("Index: %1").arg(i);
+ }
+ case Format::A4: {
+ u8 a = (*pixel >> ((offset % 2) ? 4 : 0)) & 0xF;
+ return QString("Alpha: %1").arg(QString::number(a / 15.0f, 'f', 2));
+ }
+ case Format::ETC1:
+ case Format::ETC1A4:
+ // TODO: Display block information or channel values?
+ return QString("Compressed data");
+ case Format::D16: {
+ auto value = Color::DecodeD16(pixel);
+ return QString("Depth: %1").arg(QString::number(value / (float)0xFFFF, 'f', 4));
+ }
+ case Format::D24: {
+ auto value = Color::DecodeD24(pixel);
+ return QString("Depth: %1").arg(QString::number(value / (float)0xFFFFFF, 'f', 4));
+ }
+ case Format::D24X8:
+ case Format::X24S8: {
+ auto values = Color::DecodeD24S8(pixel);
+ return QString("Depth: %1, Stencil: %2")
+ .arg(QString::number(values[0] / (float)0xFFFFFF, 'f', 4))
+ .arg(values[1]);
+ }
+ case Format::Unknown:
+ return QString("Unknown format");
+ default:
+ return QString("Unhandled format");
+ }
+ return QString("");
+ };
+
+ QString nibbles = "";
+ for (unsigned i = 0; i < nibbles_per_pixel; i++) {
+ unsigned nibble_index = i;
+ if (nibble_mode) {
+ nibble_index += (offset % 2) ? 0 : 1;
+ }
+ u8 byte = pixel[nibble_index / 2];
+ u8 nibble = (byte >> ((nibble_index % 2) ? 0 : 4)) & 0xF;
+ nibbles.append(QString::number(nibble, 16).toUpper());
+ }
+
+ surface_info_label->setText(
+ QString("Raw: 0x%3\n(%4)").arg(nibbles).arg(GetText(surface_format, pixel)));
+ surface_info_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+}
+
+void GraphicsSurfaceWidget::OnUpdate() {
+ QPixmap pixmap;
+
+ switch (surface_source) {
+ case Source::ColorBuffer: {
+ // TODO: Store a reference to the registers in the debug context instead of accessing them
+ // directly...
+
+ const auto& framebuffer = Pica::g_state.regs.framebuffer;
+
+ surface_address = framebuffer.GetColorBufferPhysicalAddress();
+ surface_width = framebuffer.GetWidth();
+ surface_height = framebuffer.GetHeight();
+
+ switch (framebuffer.color_format) {
+ case Pica::Regs::ColorFormat::RGBA8:
+ surface_format = Format::RGBA8;
+ break;
+
+ case Pica::Regs::ColorFormat::RGB8:
+ surface_format = Format::RGB8;
+ break;
+
+ case Pica::Regs::ColorFormat::RGB5A1:
+ surface_format = Format::RGB5A1;
+ break;
+
+ case Pica::Regs::ColorFormat::RGB565:
+ surface_format = Format::RGB565;
+ break;
+
+ case Pica::Regs::ColorFormat::RGBA4:
+ surface_format = Format::RGBA4;
+ break;
+
+ default:
+ surface_format = Format::Unknown;
+ break;
+ }
+
+ break;
+ }
+
+ case Source::DepthBuffer: {
+ const auto& framebuffer = Pica::g_state.regs.framebuffer;
+
+ surface_address = framebuffer.GetDepthBufferPhysicalAddress();
+ surface_width = framebuffer.GetWidth();
+ surface_height = framebuffer.GetHeight();
+
+ switch (framebuffer.depth_format) {
+ case Pica::Regs::DepthFormat::D16:
+ surface_format = Format::D16;
+ break;
+
+ case Pica::Regs::DepthFormat::D24:
+ surface_format = Format::D24;
+ break;
+
+ case Pica::Regs::DepthFormat::D24S8:
+ surface_format = Format::D24X8;
+ break;
+
+ default:
+ surface_format = Format::Unknown;
+ break;
+ }
+
+ break;
+ }
+
+ case Source::StencilBuffer: {
+ const auto& framebuffer = Pica::g_state.regs.framebuffer;
+
+ surface_address = framebuffer.GetDepthBufferPhysicalAddress();
+ surface_width = framebuffer.GetWidth();
+ surface_height = framebuffer.GetHeight();
+
+ switch (framebuffer.depth_format) {
+ case Pica::Regs::DepthFormat::D24S8:
+ surface_format = Format::X24S8;
+ break;
+
+ default:
+ surface_format = Format::Unknown;
+ break;
+ }
+
+ break;
+ }
+
+ case Source::Texture0:
+ case Source::Texture1:
+ case Source::Texture2: {
+ unsigned texture_index;
+ if (surface_source == Source::Texture0)
+ texture_index = 0;
+ else if (surface_source == Source::Texture1)
+ texture_index = 1;
+ else if (surface_source == Source::Texture2)
+ texture_index = 2;
+ else {
+ qDebug() << "Unknown texture source " << static_cast<int>(surface_source);
+ break;
+ }
+
+ const auto texture = Pica::g_state.regs.GetTextures()[texture_index];
+ auto info = Pica::DebugUtils::TextureInfo::FromPicaRegister(texture.config, texture.format);
+
+ surface_address = info.physical_address;
+ surface_width = info.width;
+ surface_height = info.height;
+ surface_format = static_cast<Format>(info.format);
+
+ if (surface_format > Format::MaxTextureFormat) {
+ qDebug() << "Unknown texture format " << static_cast<int>(info.format);
+ }
+ break;
+ }
+
+ case Source::Custom: {
+ // Keep user-specified values
+ break;
+ }
+
+ default:
+ qDebug() << "Unknown surface source " << static_cast<int>(surface_source);
+ break;
+ }
+
+ surface_address_control->SetValue(surface_address);
+ surface_width_control->setValue(surface_width);
+ surface_height_control->setValue(surface_height);
+ surface_format_control->setCurrentIndex(static_cast<int>(surface_format));
+
+ // TODO: Implement a good way to visualize alpha components!
+
+ QImage decoded_image(surface_width, surface_height, QImage::Format_ARGB32);
+ u8* buffer = Memory::GetPhysicalPointer(surface_address);
+
+ if (buffer == nullptr) {
+ surface_picture_label->hide();
+ surface_info_label->setText(tr("(invalid surface address)"));
+ surface_info_label->setAlignment(Qt::AlignCenter);
+ surface_picker_x_control->setEnabled(false);
+ surface_picker_y_control->setEnabled(false);
+ save_surface->setEnabled(false);
+ return;
+ }
+
+ if (surface_format == Format::Unknown) {
+ surface_picture_label->hide();
+ surface_info_label->setText(tr("(unknown surface format)"));
+ surface_info_label->setAlignment(Qt::AlignCenter);
+ surface_picker_x_control->setEnabled(false);
+ surface_picker_y_control->setEnabled(false);
+ save_surface->setEnabled(false);
+ return;
+ }
+
+ surface_picture_label->show();
+
+ unsigned nibbles_per_pixel = GraphicsSurfaceWidget::NibblesPerPixel(surface_format);
+ unsigned stride = nibbles_per_pixel * surface_width / 2;
+
+ // We handle depth formats here because DebugUtils only supports TextureFormats
+ if (surface_format <= Format::MaxTextureFormat) {
+
+ // Generate a virtual texture
+ Pica::DebugUtils::TextureInfo info;
+ info.physical_address = surface_address;
+ info.width = surface_width;
+ info.height = surface_height;
+ info.format = static_cast<Pica::Regs::TextureFormat>(surface_format);
+ info.stride = stride;
+
+ for (unsigned int y = 0; y < surface_height; ++y) {
+ for (unsigned int x = 0; x < surface_width; ++x) {
+ Math::Vec4<u8> color = Pica::DebugUtils::LookupTexture(buffer, x, y, info, true);
+ decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), color.a()));
+ }
+ }
+
+ } else {
+
+ ASSERT_MSG(nibbles_per_pixel >= 2,
+ "Depth decoder only supports formats with at least one byte per pixel");
+ unsigned bytes_per_pixel = nibbles_per_pixel / 2;
+
+ for (unsigned int y = 0; y < surface_height; ++y) {
+ for (unsigned int x = 0; x < surface_width; ++x) {
+ const u32 coarse_y = y & ~7;
+ u32 offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * stride;
+ const u8* pixel = buffer + offset;
+ Math::Vec4<u8> color = {0, 0, 0, 0};
+
+ switch (surface_format) {
+ case Format::D16: {
+ u32 data = Color::DecodeD16(pixel);
+ color.r() = data & 0xFF;
+ color.g() = (data >> 8) & 0xFF;
+ break;
+ }
+ case Format::D24: {
+ u32 data = Color::DecodeD24(pixel);
+ color.r() = data & 0xFF;
+ color.g() = (data >> 8) & 0xFF;
+ color.b() = (data >> 16) & 0xFF;
+ break;
+ }
+ case Format::D24X8: {
+ Math::Vec2<u32> data = Color::DecodeD24S8(pixel);
+ color.r() = data.x & 0xFF;
+ color.g() = (data.x >> 8) & 0xFF;
+ color.b() = (data.x >> 16) & 0xFF;
+ break;
+ }
+ case Format::X24S8: {
+ Math::Vec2<u32> data = Color::DecodeD24S8(pixel);
+ color.r() = color.g() = color.b() = data.y;
+ break;
+ }
+ default:
+ qDebug() << "Unknown surface format " << static_cast<int>(surface_format);
+ break;
+ }
+
+ decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), 255));
+ }
+ }
+ }
+
+ pixmap = QPixmap::fromImage(decoded_image);
+ surface_picture_label->setPixmap(pixmap);
+ surface_picture_label->resize(pixmap.size());
+
+ // Update the info with pixel data
+ surface_picker_x_control->setEnabled(true);
+ surface_picker_y_control->setEnabled(true);
+ Pick(surface_picker_x, surface_picker_y);
+
+ // Enable saving the converted pixmap to file
+ save_surface->setEnabled(true);
+}
+
+void GraphicsSurfaceWidget::SaveSurface() {
+ QString png_filter = tr("Portable Network Graphic (*.png)");
+ QString bin_filter = tr("Binary data (*.bin)");
+
+ QString selectedFilter;
+ QString filename = QFileDialog::getSaveFileName(
+ this, tr("Save Surface"),
+ QString("texture-0x%1.png").arg(QString::number(surface_address, 16)),
+ QString("%1;;%2").arg(png_filter, bin_filter), &selectedFilter);
+
+ if (filename.isEmpty()) {
+ // If the user canceled the dialog, don't save anything.
+ return;
+ }
+
+ if (selectedFilter == png_filter) {
+ const QPixmap* pixmap = surface_picture_label->pixmap();
+ ASSERT_MSG(pixmap != nullptr, "No pixmap set");
+
+ QFile file(filename);
+ file.open(QIODevice::WriteOnly);
+ if (pixmap)
+ pixmap->save(&file, "PNG");
+ } else if (selectedFilter == bin_filter) {
+ const u8* buffer = Memory::GetPhysicalPointer(surface_address);
+ ASSERT_MSG(buffer != nullptr, "Memory not accessible");
+
+ QFile file(filename);
+ file.open(QIODevice::WriteOnly);
+ int size = surface_width * surface_height * NibblesPerPixel(surface_format) / 2;
+ QByteArray data(reinterpret_cast<const char*>(buffer), size);
+ file.write(data);
+ } else {
+ UNREACHABLE_MSG("Unhandled filter selected");
+ }
+}
+
+unsigned int GraphicsSurfaceWidget::NibblesPerPixel(GraphicsSurfaceWidget::Format format) {
+ if (format <= Format::MaxTextureFormat) {
+ return Pica::Regs::NibblesPerPixel(static_cast<Pica::Regs::TextureFormat>(format));
+ }
+
+ switch (format) {
+ case Format::D24X8:
+ case Format::X24S8:
+ return 4 * 2;
+ case Format::D24:
+ return 3 * 2;
+ case Format::D16:
+ return 2 * 2;
+ default:
+ UNREACHABLE_MSG("GraphicsSurfaceWidget::BytesPerPixel: this should not be reached as this "
+ "function should be given a format which is in "
+ "GraphicsSurfaceWidget::Format. Instead got %i",
+ static_cast<int>(format));
+ return 0;
+ }
+}
diff --git a/src/citra_qt/debugger/graphics/graphics_surface.h b/src/citra_qt/debugger/graphics/graphics_surface.h
new file mode 100644
index 000000000..28f5650a7
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_surface.h
@@ -0,0 +1,118 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QLabel>
+#include <QPushButton>
+#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h"
+
+class QComboBox;
+class QSpinBox;
+class CSpinBox;
+
+class GraphicsSurfaceWidget;
+
+class SurfacePicture : public QLabel {
+ Q_OBJECT
+
+public:
+ explicit SurfacePicture(QWidget* parent = nullptr,
+ GraphicsSurfaceWidget* surface_widget = nullptr);
+ ~SurfacePicture();
+
+protected slots:
+ virtual void mouseMoveEvent(QMouseEvent* event);
+ virtual void mousePressEvent(QMouseEvent* event);
+
+private:
+ GraphicsSurfaceWidget* surface_widget;
+};
+
+class GraphicsSurfaceWidget : public BreakPointObserverDock {
+ Q_OBJECT
+
+ using Event = Pica::DebugContext::Event;
+
+ enum class Source {
+ ColorBuffer = 0,
+ DepthBuffer = 1,
+ StencilBuffer = 2,
+ Texture0 = 3,
+ Texture1 = 4,
+ Texture2 = 5,
+ Custom = 6,
+ };
+
+ enum class Format {
+ // These must match the TextureFormat type!
+ RGBA8 = 0,
+ RGB8 = 1,
+ RGB5A1 = 2,
+ RGB565 = 3,
+ RGBA4 = 4,
+ IA8 = 5,
+ RG8 = 6, ///< @note Also called HILO8 in 3DBrew.
+ I8 = 7,
+ A8 = 8,
+ IA4 = 9,
+ I4 = 10,
+ A4 = 11,
+ ETC1 = 12, // compressed
+ ETC1A4 = 13,
+ MaxTextureFormat = 13,
+ D16 = 14,
+ D24 = 15,
+ D24X8 = 16,
+ X24S8 = 17,
+ Unknown = 18,
+ };
+
+ static unsigned int NibblesPerPixel(Format format);
+
+public:
+ explicit GraphicsSurfaceWidget(std::shared_ptr<Pica::DebugContext> debug_context,
+ QWidget* parent = nullptr);
+ void Pick(int x, int y);
+
+public slots:
+ void OnSurfaceSourceChanged(int new_value);
+ void OnSurfaceAddressChanged(qint64 new_value);
+ void OnSurfaceWidthChanged(int new_value);
+ void OnSurfaceHeightChanged(int new_value);
+ void OnSurfaceFormatChanged(int new_value);
+ void OnSurfacePickerXChanged(int new_value);
+ void OnSurfacePickerYChanged(int new_value);
+ void OnUpdate();
+
+private slots:
+ void OnBreakPointHit(Pica::DebugContext::Event event, void* data) override;
+ void OnResumed() override;
+
+ void SaveSurface();
+
+signals:
+ void Update();
+
+private:
+ QComboBox* surface_source_list;
+ CSpinBox* surface_address_control;
+ QSpinBox* surface_width_control;
+ QSpinBox* surface_height_control;
+ QComboBox* surface_format_control;
+
+ SurfacePicture* surface_picture_label;
+ QSpinBox* surface_picker_x_control;
+ QSpinBox* surface_picker_y_control;
+ QLabel* surface_info_label;
+ QPushButton* save_surface;
+
+ Source surface_source;
+ unsigned surface_address;
+ unsigned surface_width;
+ unsigned surface_height;
+ Format surface_format;
+ int surface_picker_x = 0;
+ int surface_picker_y = 0;
+};
diff --git a/src/citra_qt/debugger/graphics/graphics_tracing.cpp b/src/citra_qt/debugger/graphics/graphics_tracing.cpp
new file mode 100644
index 000000000..716ed50b8
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_tracing.cpp
@@ -0,0 +1,178 @@
+// Copyright 2015 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <array>
+#include <iterator>
+#include <memory>
+#include <QBoxLayout>
+#include <QComboBox>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QPushButton>
+#include <boost/range/algorithm/copy.hpp>
+#include "citra_qt/debugger/graphics/graphics_tracing.h"
+#include "common/common_types.h"
+#include "core/hw/gpu.h"
+#include "core/hw/lcd.h"
+#include "core/tracer/recorder.h"
+#include "nihstro/float24.h"
+#include "video_core/pica.h"
+#include "video_core/pica_state.h"
+
+GraphicsTracingWidget::GraphicsTracingWidget(std::shared_ptr<Pica::DebugContext> debug_context,
+ QWidget* parent)
+ : BreakPointObserverDock(debug_context, tr("CiTrace Recorder"), parent) {
+
+ setObjectName("CiTracing");
+
+ QPushButton* start_recording = new QPushButton(tr("Start Recording"));
+ QPushButton* stop_recording =
+ new QPushButton(QIcon::fromTheme("document-save"), tr("Stop and Save"));
+ QPushButton* abort_recording = new QPushButton(tr("Abort Recording"));
+
+ connect(this, SIGNAL(SetStartTracingButtonEnabled(bool)), start_recording,
+ SLOT(setVisible(bool)));
+ connect(this, SIGNAL(SetStopTracingButtonEnabled(bool)), stop_recording,
+ SLOT(setVisible(bool)));
+ connect(this, SIGNAL(SetAbortTracingButtonEnabled(bool)), abort_recording,
+ SLOT(setVisible(bool)));
+ connect(start_recording, SIGNAL(clicked()), this, SLOT(StartRecording()));
+ connect(stop_recording, SIGNAL(clicked()), this, SLOT(StopRecording()));
+ connect(abort_recording, SIGNAL(clicked()), this, SLOT(AbortRecording()));
+
+ stop_recording->setVisible(false);
+ abort_recording->setVisible(false);
+
+ auto main_widget = new QWidget;
+ auto main_layout = new QVBoxLayout;
+ {
+ auto sub_layout = new QHBoxLayout;
+ sub_layout->addWidget(start_recording);
+ sub_layout->addWidget(stop_recording);
+ sub_layout->addWidget(abort_recording);
+ main_layout->addLayout(sub_layout);
+ }
+ main_widget->setLayout(main_layout);
+ setWidget(main_widget);
+}
+
+void GraphicsTracingWidget::StartRecording() {
+ auto context = context_weak.lock();
+ if (!context)
+ return;
+
+ auto shader_binary = Pica::g_state.vs.program_code;
+ auto swizzle_data = Pica::g_state.vs.swizzle_data;
+
+ // Encode floating point numbers to 24-bit values
+ // TODO: Drop this explicit conversion once we store float24 values bit-correctly internally.
+ std::array<u32, 4 * 16> default_attributes;
+ for (unsigned i = 0; i < 16; ++i) {
+ for (unsigned comp = 0; comp < 3; ++comp) {
+ default_attributes[4 * i + comp] =
+ nihstro::to_float24(Pica::g_state.vs_default_attributes[i][comp].ToFloat32());
+ }
+ }
+
+ std::array<u32, 4 * 96> vs_float_uniforms;
+ for (unsigned i = 0; i < 96; ++i)
+ for (unsigned comp = 0; comp < 3; ++comp)
+ vs_float_uniforms[4 * i + comp] =
+ nihstro::to_float24(Pica::g_state.vs.uniforms.f[i][comp].ToFloat32());
+
+ CiTrace::Recorder::InitialState state;
+ std::copy_n((u32*)&GPU::g_regs, sizeof(GPU::g_regs) / sizeof(u32),
+ std::back_inserter(state.gpu_registers));
+ std::copy_n((u32*)&LCD::g_regs, sizeof(LCD::g_regs) / sizeof(u32),
+ std::back_inserter(state.lcd_registers));
+ std::copy_n((u32*)&Pica::g_state.regs, sizeof(Pica::g_state.regs) / sizeof(u32),
+ std::back_inserter(state.pica_registers));
+ boost::copy(default_attributes, std::back_inserter(state.default_attributes));
+ boost::copy(shader_binary, std::back_inserter(state.vs_program_binary));
+ boost::copy(swizzle_data, std::back_inserter(state.vs_swizzle_data));
+ boost::copy(vs_float_uniforms, std::back_inserter(state.vs_float_uniforms));
+ // boost::copy(TODO: Not implemented, std::back_inserter(state.gs_program_binary));
+ // boost::copy(TODO: Not implemented, std::back_inserter(state.gs_swizzle_data));
+ // boost::copy(TODO: Not implemented, std::back_inserter(state.gs_float_uniforms));
+
+ auto recorder = new CiTrace::Recorder(state);
+ context->recorder = std::shared_ptr<CiTrace::Recorder>(recorder);
+
+ emit SetStartTracingButtonEnabled(false);
+ emit SetStopTracingButtonEnabled(true);
+ emit SetAbortTracingButtonEnabled(true);
+}
+
+void GraphicsTracingWidget::StopRecording() {
+ auto context = context_weak.lock();
+ if (!context)
+ return;
+
+ QString filename = QFileDialog::getSaveFileName(this, tr("Save CiTrace"), "citrace.ctf",
+ tr("CiTrace File (*.ctf)"));
+
+ if (filename.isEmpty()) {
+ // If the user canceled the dialog, keep recording
+ return;
+ }
+
+ context->recorder->Finish(filename.toStdString());
+ context->recorder = nullptr;
+
+ emit SetStopTracingButtonEnabled(false);
+ emit SetAbortTracingButtonEnabled(false);
+ emit SetStartTracingButtonEnabled(true);
+}
+
+void GraphicsTracingWidget::AbortRecording() {
+ auto context = context_weak.lock();
+ if (!context)
+ return;
+
+ context->recorder = nullptr;
+
+ emit SetStopTracingButtonEnabled(false);
+ emit SetAbortTracingButtonEnabled(false);
+ emit SetStartTracingButtonEnabled(true);
+}
+
+void GraphicsTracingWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) {
+ widget()->setEnabled(true);
+}
+
+void GraphicsTracingWidget::OnResumed() {
+ widget()->setEnabled(false);
+}
+
+void GraphicsTracingWidget::OnEmulationStarting(EmuThread* emu_thread) {
+ // Disable tracing starting/stopping until a GPU breakpoint is reached
+ widget()->setEnabled(false);
+}
+
+void GraphicsTracingWidget::OnEmulationStopping() {
+ // TODO: Is it safe to access the context here?
+
+ auto context = context_weak.lock();
+ if (!context)
+ return;
+
+ if (context->recorder) {
+ auto reply =
+ QMessageBox::question(this, tr("CiTracing still active"),
+ tr("A CiTrace is still being recorded. Do you want to save it? "
+ "If not, all recorded data will be discarded."),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+
+ if (reply == QMessageBox::Yes) {
+ StopRecording();
+ } else {
+ AbortRecording();
+ }
+ }
+
+ // If the widget was disabled before, enable it now to allow starting
+ // tracing before starting the next emulation session
+ widget()->setEnabled(true);
+}
diff --git a/src/citra_qt/debugger/graphics/graphics_tracing.h b/src/citra_qt/debugger/graphics/graphics_tracing.h
new file mode 100644
index 000000000..3f73bcd2e
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_tracing.h
@@ -0,0 +1,33 @@
+// Copyright 2015 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h"
+
+class EmuThread;
+
+class GraphicsTracingWidget : public BreakPointObserverDock {
+ Q_OBJECT
+
+public:
+ explicit GraphicsTracingWidget(std::shared_ptr<Pica::DebugContext> debug_context,
+ QWidget* parent = nullptr);
+
+private slots:
+ void StartRecording();
+ void StopRecording();
+ void AbortRecording();
+
+ void OnBreakPointHit(Pica::DebugContext::Event event, void* data) override;
+ void OnResumed() override;
+
+ void OnEmulationStarting(EmuThread* emu_thread);
+ void OnEmulationStopping();
+
+signals:
+ void SetStartTracingButtonEnabled(bool enable);
+ void SetStopTracingButtonEnabled(bool enable);
+ void SetAbortTracingButtonEnabled(bool enable);
+};
diff --git a/src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp b/src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp
new file mode 100644
index 000000000..b75b94ef8
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp
@@ -0,0 +1,629 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <iomanip>
+#include <sstream>
+#include <QBoxLayout>
+#include <QFileDialog>
+#include <QFormLayout>
+#include <QGroupBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QSignalMapper>
+#include <QSpinBox>
+#include <QTreeView>
+#include "citra_qt/debugger/graphics/graphics_vertex_shader.h"
+#include "citra_qt/util/util.h"
+#include "video_core/pica.h"
+#include "video_core/pica_state.h"
+#include "video_core/shader/shader.h"
+
+using nihstro::OpCode;
+using nihstro::Instruction;
+using nihstro::SourceRegister;
+using nihstro::SwizzlePattern;
+
+GraphicsVertexShaderModel::GraphicsVertexShaderModel(GraphicsVertexShaderWidget* parent)
+ : QAbstractTableModel(parent), par(parent) {}
+
+int GraphicsVertexShaderModel::columnCount(const QModelIndex& parent) const {
+ return 3;
+}
+
+int GraphicsVertexShaderModel::rowCount(const QModelIndex& parent) const {
+ return static_cast<int>(par->info.code.size());
+}
+
+QVariant GraphicsVertexShaderModel::headerData(int section, Qt::Orientation orientation,
+ int role) const {
+ switch (role) {
+ case Qt::DisplayRole: {
+ if (section == 0) {
+ return tr("Offset");
+ } else if (section == 1) {
+ return tr("Raw");
+ } else if (section == 2) {
+ return tr("Disassembly");
+ }
+
+ break;
+ }
+ }
+
+ return QVariant();
+}
+
+static std::string SelectorToString(u32 selector) {
+ std::string ret;
+ for (int i = 0; i < 4; ++i) {
+ int component = (selector >> ((3 - i) * 2)) & 3;
+ ret += "xyzw"[component];
+ }
+ return ret;
+}
+
+// e.g. "-c92[a0.x].xyzw"
+static void print_input(std::ostringstream& output, const SourceRegister& input, bool negate,
+ const std::string& swizzle_mask, bool align = true,
+ const std::string& address_register_name = std::string()) {
+ if (align)
+ output << std::setw(4) << std::right;
+ output << ((negate ? "-" : "") + input.GetName());
+
+ if (!address_register_name.empty())
+ output << '[' << address_register_name << ']';
+ output << '.' << swizzle_mask;
+};
+
+QVariant GraphicsVertexShaderModel::data(const QModelIndex& index, int role) const {
+ switch (role) {
+ case Qt::DisplayRole: {
+ switch (index.column()) {
+ case 0:
+ if (par->info.HasLabel(index.row()))
+ return QString::fromStdString(par->info.GetLabel(index.row()));
+
+ return QString("%1").arg(4 * index.row(), 4, 16, QLatin1Char('0'));
+
+ case 1:
+ return QString("%1").arg(par->info.code[index.row()].hex, 8, 16, QLatin1Char('0'));
+
+ case 2: {
+ std::ostringstream output;
+ output.flags(std::ios::uppercase);
+
+ // To make the code aligning columns of assembly easier to keep track of, this function
+ // keeps track of the start of the start of the previous column, allowing alignment
+ // based on desired field widths.
+ int current_column = 0;
+ auto AlignToColumn = [&](int col_width) {
+ // Prints spaces to the output to pad previous column to size and advances the
+ // column marker.
+ current_column += col_width;
+ int to_add = std::max(1, current_column - (int)output.tellp());
+ for (int i = 0; i < to_add; ++i) {
+ output << ' ';
+ }
+ };
+
+ const Instruction instr = par->info.code[index.row()];
+ const OpCode opcode = instr.opcode;
+ const OpCode::Info opcode_info = opcode.GetInfo();
+ const u32 operand_desc_id = opcode_info.type == OpCode::Type::MultiplyAdd
+ ? instr.mad.operand_desc_id.Value()
+ : instr.common.operand_desc_id.Value();
+ const SwizzlePattern swizzle = par->info.swizzle_info[operand_desc_id].pattern;
+
+ // longest known instruction name: "setemit "
+ int kOpcodeColumnWidth = 8;
+ // "rXX.xyzw "
+ int kOutputColumnWidth = 10;
+ // "-rXX.xyzw ", no attempt is made to align indexed inputs
+ int kInputOperandColumnWidth = 11;
+
+ output << opcode_info.name;
+
+ switch (opcode_info.type) {
+ case OpCode::Type::Trivial:
+ // Nothing to do here
+ break;
+
+ case OpCode::Type::Arithmetic:
+ case OpCode::Type::MultiplyAdd: {
+ // Use custom code for special instructions
+ switch (opcode.EffectiveOpCode()) {
+ case OpCode::Id::CMP: {
+ AlignToColumn(kOpcodeColumnWidth);
+
+ // NOTE: CMP always writes both cc components, so we do not consider the dest
+ // mask here.
+ output << " cc.xy";
+ AlignToColumn(kOutputColumnWidth);
+
+ SourceRegister src1 = instr.common.GetSrc1(false);
+ SourceRegister src2 = instr.common.GetSrc2(false);
+
+ output << ' ';
+ print_input(output, src1, swizzle.negate_src1,
+ swizzle.SelectorToString(false).substr(0, 1), false,
+ instr.common.AddressRegisterName());
+ output << ' ' << instr.common.compare_op.ToString(instr.common.compare_op.x)
+ << ' ';
+ print_input(output, src2, swizzle.negate_src2,
+ swizzle.SelectorToString(true).substr(0, 1), false);
+
+ output << ", ";
+
+ print_input(output, src1, swizzle.negate_src1,
+ swizzle.SelectorToString(false).substr(1, 1), false,
+ instr.common.AddressRegisterName());
+ output << ' ' << instr.common.compare_op.ToString(instr.common.compare_op.y)
+ << ' ';
+ print_input(output, src2, swizzle.negate_src2,
+ swizzle.SelectorToString(true).substr(1, 1), false);
+
+ break;
+ }
+
+ case OpCode::Id::MAD:
+ case OpCode::Id::MADI: {
+ AlignToColumn(kOpcodeColumnWidth);
+
+ bool src_is_inverted = 0 != (opcode_info.subtype & OpCode::Info::SrcInversed);
+ SourceRegister src1 = instr.mad.GetSrc1(src_is_inverted);
+ SourceRegister src2 = instr.mad.GetSrc2(src_is_inverted);
+ SourceRegister src3 = instr.mad.GetSrc3(src_is_inverted);
+
+ output << std::setw(3) << std::right << instr.mad.dest.Value().GetName() << '.'
+ << swizzle.DestMaskToString();
+ AlignToColumn(kOutputColumnWidth);
+ print_input(output, src1, swizzle.negate_src1,
+ SelectorToString(swizzle.src1_selector));
+ AlignToColumn(kInputOperandColumnWidth);
+ if (src_is_inverted) {
+ print_input(output, src2, swizzle.negate_src2,
+ SelectorToString(swizzle.src2_selector));
+ } else {
+ print_input(output, src2, swizzle.negate_src2,
+ SelectorToString(swizzle.src2_selector), true,
+ instr.mad.AddressRegisterName());
+ }
+ AlignToColumn(kInputOperandColumnWidth);
+ if (src_is_inverted) {
+ print_input(output, src3, swizzle.negate_src3,
+ SelectorToString(swizzle.src3_selector), true,
+ instr.mad.AddressRegisterName());
+ } else {
+ print_input(output, src3, swizzle.negate_src3,
+ SelectorToString(swizzle.src3_selector));
+ }
+ AlignToColumn(kInputOperandColumnWidth);
+ break;
+ }
+
+ default: {
+ AlignToColumn(kOpcodeColumnWidth);
+
+ bool src_is_inverted = 0 != (opcode_info.subtype & OpCode::Info::SrcInversed);
+
+ if (opcode_info.subtype & OpCode::Info::Dest) {
+ // e.g. "r12.xy__"
+ output << std::setw(3) << std::right << instr.common.dest.Value().GetName()
+ << '.' << swizzle.DestMaskToString();
+ } else if (opcode_info.subtype == OpCode::Info::MOVA) {
+ output << " a0." << swizzle.DestMaskToString();
+ }
+ AlignToColumn(kOutputColumnWidth);
+
+ if (opcode_info.subtype & OpCode::Info::Src1) {
+ SourceRegister src1 = instr.common.GetSrc1(src_is_inverted);
+ print_input(output, src1, swizzle.negate_src1,
+ swizzle.SelectorToString(false), true,
+ instr.common.AddressRegisterName());
+ AlignToColumn(kInputOperandColumnWidth);
+ }
+
+ // TODO: In some cases, the Address Register is used as an index for SRC2
+ // instead of SRC1
+ if (opcode_info.subtype & OpCode::Info::Src2) {
+ SourceRegister src2 = instr.common.GetSrc2(src_is_inverted);
+ print_input(output, src2, swizzle.negate_src2,
+ swizzle.SelectorToString(true));
+ AlignToColumn(kInputOperandColumnWidth);
+ }
+ break;
+ }
+ }
+
+ break;
+ }
+
+ case OpCode::Type::Conditional:
+ case OpCode::Type::UniformFlowControl: {
+ output << ' ';
+
+ switch (opcode.EffectiveOpCode()) {
+ case OpCode::Id::LOOP:
+ output << "(unknown instruction format)";
+ break;
+
+ default:
+ if (opcode_info.subtype & OpCode::Info::HasCondition) {
+ output << '(';
+
+ if (instr.flow_control.op != instr.flow_control.JustY) {
+ if (instr.flow_control.refx)
+ output << '!';
+ output << "cc.x";
+ }
+
+ if (instr.flow_control.op == instr.flow_control.Or) {
+ output << " || ";
+ } else if (instr.flow_control.op == instr.flow_control.And) {
+ output << " && ";
+ }
+
+ if (instr.flow_control.op != instr.flow_control.JustX) {
+ if (instr.flow_control.refy)
+ output << '!';
+ output << "cc.y";
+ }
+
+ output << ") ";
+ } else if (opcode_info.subtype & OpCode::Info::HasUniformIndex) {
+ output << 'b' << instr.flow_control.bool_uniform_id << ' ';
+ }
+
+ u32 target_addr = instr.flow_control.dest_offset;
+ u32 target_addr_else = instr.flow_control.dest_offset;
+
+ if (opcode_info.subtype & OpCode::Info::HasAlternative) {
+ output << "else jump to 0x" << std::setw(4) << std::right
+ << std::setfill('0') << std::hex
+ << (4 * instr.flow_control.dest_offset);
+ } else if (opcode_info.subtype & OpCode::Info::HasExplicitDest) {
+ output << "jump to 0x" << std::setw(4) << std::right << std::setfill('0')
+ << std::hex << (4 * instr.flow_control.dest_offset);
+ } else {
+ // TODO: Handle other cases
+ output << "(unknown destination)";
+ }
+
+ if (opcode_info.subtype & OpCode::Info::HasFinishPoint) {
+ output << " (return on 0x" << std::setw(4) << std::right
+ << std::setfill('0') << std::hex
+ << (4 * instr.flow_control.dest_offset +
+ 4 * instr.flow_control.num_instructions)
+ << ')';
+ }
+
+ break;
+ }
+ break;
+ }
+
+ default:
+ output << " (unknown instruction format)";
+ break;
+ }
+
+ return QString::fromLatin1(output.str().c_str());
+ }
+
+ default:
+ break;
+ }
+ }
+
+ case Qt::FontRole:
+ return GetMonospaceFont();
+
+ case Qt::BackgroundRole: {
+ // Highlight current instruction
+ int current_record_index = par->cycle_index->value();
+ if (current_record_index < static_cast<int>(par->debug_data.records.size())) {
+ const auto& current_record = par->debug_data.records[current_record_index];
+ if (index.row() == static_cast<int>(current_record.instruction_offset)) {
+ return QColor(255, 255, 63);
+ }
+ }
+
+ // Use a grey background for instructions which have no debug data associated to them
+ for (const auto& record : par->debug_data.records)
+ if (index.row() == static_cast<int>(record.instruction_offset))
+ return QVariant();
+
+ return QBrush(QColor(192, 192, 192));
+ }
+
+ // TODO: Draw arrows for each "reachable" instruction to visualize control flow
+
+ default:
+ break;
+ }
+
+ return QVariant();
+}
+
+void GraphicsVertexShaderWidget::DumpShader() {
+ QString filename = QFileDialog::getSaveFileName(
+ this, tr("Save Shader Dump"), "shader_dump.shbin", tr("Shader Binary (*.shbin)"));
+
+ if (filename.isEmpty()) {
+ // If the user canceled the dialog, don't dump anything.
+ return;
+ }
+
+ auto& setup = Pica::g_state.vs;
+ auto& config = Pica::g_state.regs.vs;
+
+ Pica::DebugUtils::DumpShader(filename.toStdString(), config, setup,
+ Pica::g_state.regs.vs_output_attributes);
+}
+
+GraphicsVertexShaderWidget::GraphicsVertexShaderWidget(
+ std::shared_ptr<Pica::DebugContext> debug_context, QWidget* parent)
+ : BreakPointObserverDock(debug_context, "Pica Vertex Shader", parent) {
+ setObjectName("PicaVertexShader");
+
+ // Clear input vertex data so that it contains valid float values in case a debug shader
+ // execution happens before the first Vertex Loaded breakpoint.
+ // TODO: This makes a crash in the interpreter much less likely, but not impossible. The
+ // interpreter should guard against out-of-bounds accesses to ensure crashes in it aren't
+ // possible.
+ std::memset(&input_vertex, 0, sizeof(input_vertex));
+
+ auto input_data_mapper = new QSignalMapper(this);
+
+ // TODO: Support inputting data in hexadecimal raw format
+ for (unsigned i = 0; i < ARRAY_SIZE(input_data); ++i) {
+ input_data[i] = new QLineEdit;
+ input_data[i]->setValidator(new QDoubleValidator(input_data[i]));
+ }
+
+ breakpoint_warning =
+ new QLabel(tr("(data only available at vertex shader invocation breakpoints)"));
+
+ // TODO: Add some button for jumping to the shader entry point
+
+ model = new GraphicsVertexShaderModel(this);
+ binary_list = new QTreeView;
+ binary_list->setModel(model);
+ binary_list->setRootIsDecorated(false);
+ binary_list->setAlternatingRowColors(true);
+
+ auto dump_shader = new QPushButton(QIcon::fromTheme("document-save"), tr("Dump"));
+
+ instruction_description = new QLabel;
+
+ cycle_index = new QSpinBox;
+
+ connect(dump_shader, SIGNAL(clicked()), this, SLOT(DumpShader()));
+
+ connect(cycle_index, SIGNAL(valueChanged(int)), this, SLOT(OnCycleIndexChanged(int)));
+
+ for (unsigned i = 0; i < ARRAY_SIZE(input_data); ++i) {
+ connect(input_data[i], SIGNAL(textEdited(const QString&)), input_data_mapper, SLOT(map()));
+ input_data_mapper->setMapping(input_data[i], i);
+ }
+ connect(input_data_mapper, SIGNAL(mapped(int)), this, SLOT(OnInputAttributeChanged(int)));
+
+ auto main_widget = new QWidget;
+ auto main_layout = new QVBoxLayout;
+ {
+ auto input_data_group = new QGroupBox(tr("Input Data"));
+
+ // For each vertex attribute, add a QHBoxLayout consisting of:
+ // - A QLabel denoting the source attribute index
+ // - Four QLineEdits for showing and manipulating attribute data
+ // - A QLabel denoting the shader input attribute index
+ auto sub_layout = new QVBoxLayout;
+ for (unsigned i = 0; i < 16; ++i) {
+ // Create an HBoxLayout to store the widgets used to specify a particular attribute
+ // and store it in a QWidget to allow for easy hiding and unhiding.
+ auto row_layout = new QHBoxLayout;
+ // Remove unnecessary padding between rows
+ row_layout->setContentsMargins(0, 0, 0, 0);
+
+ row_layout->addWidget(new QLabel(tr("Attribute %1").arg(i, 2)));
+ for (unsigned comp = 0; comp < 4; ++comp)
+ row_layout->addWidget(input_data[4 * i + comp]);
+
+ row_layout->addWidget(input_data_mapping[i] = new QLabel);
+
+ input_data_container[i] = new QWidget;
+ input_data_container[i]->setLayout(row_layout);
+ input_data_container[i]->hide();
+
+ sub_layout->addWidget(input_data_container[i]);
+ }
+
+ sub_layout->addWidget(breakpoint_warning);
+ breakpoint_warning->hide();
+
+ input_data_group->setLayout(sub_layout);
+ main_layout->addWidget(input_data_group);
+ }
+
+ // Make program listing expand to fill available space in the dialog
+ binary_list->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
+ main_layout->addWidget(binary_list);
+
+ main_layout->addWidget(dump_shader);
+ {
+ auto sub_layout = new QFormLayout;
+ sub_layout->addRow(tr("Cycle Index:"), cycle_index);
+
+ main_layout->addLayout(sub_layout);
+ }
+
+ // Set a minimum height so that the size of this label doesn't cause the rest of the bottom
+ // part of the UI to keep jumping up and down when cycling through instructions.
+ instruction_description->setMinimumHeight(instruction_description->fontMetrics().lineSpacing() *
+ 6);
+ instruction_description->setAlignment(Qt::AlignLeft | Qt::AlignTop);
+ main_layout->addWidget(instruction_description);
+
+ main_widget->setLayout(main_layout);
+ setWidget(main_widget);
+
+ widget()->setEnabled(false);
+}
+
+void GraphicsVertexShaderWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) {
+ auto input = static_cast<Pica::Shader::InputVertex*>(data);
+ if (event == Pica::DebugContext::Event::VertexShaderInvocation) {
+ Reload(true, data);
+ } else {
+ // No vertex data is retrievable => invalidate currently stored vertex data
+ Reload(true, nullptr);
+ }
+ widget()->setEnabled(true);
+}
+
+void GraphicsVertexShaderWidget::Reload(bool replace_vertex_data, void* vertex_data) {
+ model->beginResetModel();
+
+ if (replace_vertex_data) {
+ if (vertex_data) {
+ memcpy(&input_vertex, vertex_data, sizeof(input_vertex));
+ for (unsigned attr = 0; attr < 16; ++attr) {
+ for (unsigned comp = 0; comp < 4; ++comp) {
+ input_data[4 * attr + comp]->setText(
+ QString("%1").arg(input_vertex.attr[attr][comp].ToFloat32()));
+ }
+ }
+ breakpoint_warning->hide();
+ } else {
+ for (unsigned attr = 0; attr < 16; ++attr) {
+ for (unsigned comp = 0; comp < 4; ++comp) {
+ input_data[4 * attr + comp]->setText(QString("???"));
+ }
+ }
+ breakpoint_warning->show();
+ }
+ }
+
+ // Reload shader code
+ info.Clear();
+
+ auto& shader_setup = Pica::g_state.vs;
+ auto& shader_config = Pica::g_state.regs.vs;
+ for (auto instr : shader_setup.program_code)
+ info.code.push_back({instr});
+ int num_attributes = Pica::g_state.regs.vertex_attributes.GetNumTotalAttributes();
+
+ for (auto pattern : shader_setup.swizzle_data)
+ info.swizzle_info.push_back({pattern});
+
+ u32 entry_point = Pica::g_state.regs.vs.main_offset;
+ info.labels.insert({entry_point, "main"});
+
+ // Generate debug information
+ debug_data = Pica::g_state.vs.ProduceDebugInfo(input_vertex, num_attributes, shader_config,
+ shader_setup);
+
+ // Reload widget state
+ for (int attr = 0; attr < num_attributes; ++attr) {
+ unsigned source_attr = shader_config.input_register_map.GetRegisterForAttribute(attr);
+ input_data_mapping[attr]->setText(QString("-> v%1").arg(source_attr));
+ input_data_container[attr]->setVisible(true);
+ }
+ // Only show input attributes which are used as input to the shader
+ for (unsigned int attr = num_attributes; attr < 16; ++attr) {
+ input_data_container[attr]->setVisible(false);
+ }
+
+ // Initialize debug info text for current cycle count
+ cycle_index->setMaximum(static_cast<int>(debug_data.records.size() - 1));
+ OnCycleIndexChanged(cycle_index->value());
+
+ model->endResetModel();
+}
+
+void GraphicsVertexShaderWidget::OnResumed() {
+ widget()->setEnabled(false);
+}
+
+void GraphicsVertexShaderWidget::OnInputAttributeChanged(int index) {
+ float value = input_data[index]->text().toFloat();
+ input_vertex.attr[index / 4][index % 4] = Pica::float24::FromFloat32(value);
+ // Re-execute shader with updated value
+ Reload();
+}
+
+void GraphicsVertexShaderWidget::OnCycleIndexChanged(int index) {
+ QString text;
+
+ auto& record = debug_data.records[index];
+ if (record.mask & Pica::Shader::DebugDataRecord::SRC1)
+ text += tr("SRC1: %1, %2, %3, %4\n")
+ .arg(record.src1.x.ToFloat32())
+ .arg(record.src1.y.ToFloat32())
+ .arg(record.src1.z.ToFloat32())
+ .arg(record.src1.w.ToFloat32());
+ if (record.mask & Pica::Shader::DebugDataRecord::SRC2)
+ text += tr("SRC2: %1, %2, %3, %4\n")
+ .arg(record.src2.x.ToFloat32())
+ .arg(record.src2.y.ToFloat32())
+ .arg(record.src2.z.ToFloat32())
+ .arg(record.src2.w.ToFloat32());
+ if (record.mask & Pica::Shader::DebugDataRecord::SRC3)
+ text += tr("SRC3: %1, %2, %3, %4\n")
+ .arg(record.src3.x.ToFloat32())
+ .arg(record.src3.y.ToFloat32())
+ .arg(record.src3.z.ToFloat32())
+ .arg(record.src3.w.ToFloat32());
+ if (record.mask & Pica::Shader::DebugDataRecord::DEST_IN)
+ text += tr("DEST_IN: %1, %2, %3, %4\n")
+ .arg(record.dest_in.x.ToFloat32())
+ .arg(record.dest_in.y.ToFloat32())
+ .arg(record.dest_in.z.ToFloat32())
+ .arg(record.dest_in.w.ToFloat32());
+ if (record.mask & Pica::Shader::DebugDataRecord::DEST_OUT)
+ text += tr("DEST_OUT: %1, %2, %3, %4\n")
+ .arg(record.dest_out.x.ToFloat32())
+ .arg(record.dest_out.y.ToFloat32())
+ .arg(record.dest_out.z.ToFloat32())
+ .arg(record.dest_out.w.ToFloat32());
+
+ if (record.mask & Pica::Shader::DebugDataRecord::ADDR_REG_OUT)
+ text += tr("Address Registers: %1, %2\n")
+ .arg(record.address_registers[0])
+ .arg(record.address_registers[1]);
+ if (record.mask & Pica::Shader::DebugDataRecord::CMP_RESULT)
+ text += tr("Compare Result: %1, %2\n")
+ .arg(record.conditional_code[0] ? "true" : "false")
+ .arg(record.conditional_code[1] ? "true" : "false");
+
+ if (record.mask & Pica::Shader::DebugDataRecord::COND_BOOL_IN)
+ text += tr("Static Condition: %1\n").arg(record.cond_bool ? "true" : "false");
+ if (record.mask & Pica::Shader::DebugDataRecord::COND_CMP_IN)
+ text += tr("Dynamic Conditions: %1, %2\n")
+ .arg(record.cond_cmp[0] ? "true" : "false")
+ .arg(record.cond_cmp[1] ? "true" : "false");
+ if (record.mask & Pica::Shader::DebugDataRecord::LOOP_INT_IN)
+ text += tr("Loop Parameters: %1 (repeats), %2 (initializer), %3 (increment), %4\n")
+ .arg(record.loop_int.x)
+ .arg(record.loop_int.y)
+ .arg(record.loop_int.z)
+ .arg(record.loop_int.w);
+
+ text +=
+ tr("Instruction offset: 0x%1").arg(4 * record.instruction_offset, 4, 16, QLatin1Char('0'));
+ if (record.mask & Pica::Shader::DebugDataRecord::NEXT_INSTR) {
+ text += tr(" -> 0x%2").arg(4 * record.next_instruction, 4, 16, QLatin1Char('0'));
+ } else {
+ text += tr(" (last instruction)");
+ }
+
+ instruction_description->setText(text);
+
+ // Emit model update notification and scroll to current instruction
+ QModelIndex instr_index = model->index(record.instruction_offset, 0);
+ emit model->dataChanged(instr_index,
+ model->index(record.instruction_offset, model->columnCount()));
+ binary_list->scrollTo(instr_index, QAbstractItemView::EnsureVisible);
+}
diff --git a/src/citra_qt/debugger/graphics/graphics_vertex_shader.h b/src/citra_qt/debugger/graphics/graphics_vertex_shader.h
new file mode 100644
index 000000000..bedea0bed
--- /dev/null
+++ b/src/citra_qt/debugger/graphics/graphics_vertex_shader.h
@@ -0,0 +1,87 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QAbstractTableModel>
+#include <QTreeView>
+#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h"
+#include "nihstro/parser_shbin.h"
+#include "video_core/shader/shader.h"
+
+class QLabel;
+class QSpinBox;
+
+class GraphicsVertexShaderWidget;
+
+class GraphicsVertexShaderModel : public QAbstractTableModel {
+ Q_OBJECT
+
+public:
+ explicit GraphicsVertexShaderModel(GraphicsVertexShaderWidget* parent);
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+
+private:
+ GraphicsVertexShaderWidget* par;
+
+ friend class GraphicsVertexShaderWidget;
+};
+
+class GraphicsVertexShaderWidget : public BreakPointObserverDock {
+ Q_OBJECT
+
+ using Event = Pica::DebugContext::Event;
+
+public:
+ GraphicsVertexShaderWidget(std::shared_ptr<Pica::DebugContext> debug_context,
+ QWidget* parent = nullptr);
+
+private slots:
+ void OnBreakPointHit(Pica::DebugContext::Event event, void* data) override;
+ void OnResumed() override;
+
+ void OnInputAttributeChanged(int index);
+
+ void OnCycleIndexChanged(int index);
+
+ void DumpShader();
+
+ /**
+ * Reload widget based on the current PICA200 state
+ * @param replace_vertex_data If true, invalidate all current vertex data
+ * @param vertex_data New vertex data to use, as passed to OnBreakPointHit. May be nullptr to
+ * specify that no valid vertex data can be retrieved currently. Only used if
+ * replace_vertex_data is true.
+ */
+ void Reload(bool replace_vertex_data = false, void* vertex_data = nullptr);
+
+private:
+ QLabel* instruction_description;
+ QTreeView* binary_list;
+ GraphicsVertexShaderModel* model;
+
+ /// TODO: Move these into a single struct
+ std::array<QLineEdit*, 4 * 16>
+ input_data; // A text box for each of the 4 components of up to 16 vertex attributes
+ std::array<QWidget*, 16>
+ input_data_container; // QWidget containing the QLayout containing each vertex attribute
+ std::array<QLabel*, 16> input_data_mapping; // A QLabel denoting the shader input attribute
+ // which the vertex attribute maps to
+
+ // Text to be shown when input vertex data is not retrievable
+ QLabel* breakpoint_warning;
+
+ QSpinBox* cycle_index;
+
+ nihstro::ShaderInfo info;
+ Pica::Shader::DebugData<true> debug_data;
+ Pica::Shader::InputVertex input_vertex;
+
+ friend class GraphicsVertexShaderModel;
+};