From 77fd0d47e70968bcbc87a3b5607cd29e6211f656 Mon Sep 17 00:00:00 2001
From: Subv <subv2112@gmail.com>
Date: Thu, 22 Mar 2018 15:19:35 -0500
Subject: [PATCH] Frontend: Ported the GPU breakpoints and surface viewer
 widgets from citra.

---
 src/video_core/CMakeLists.txt                 |   2 +
 src/video_core/debug_utils/debug_utils.cpp    |  66 +++
 src/video_core/debug_utils/debug_utils.h      | 165 +++++++
 src/video_core/gpu.cpp                        |   4 +
 src/video_core/gpu.h                          |   5 +
 src/yuzu/CMakeLists.txt                       |   7 +
 .../graphics/graphics_breakpoint_observer.cpp |  27 ++
 .../graphics/graphics_breakpoint_observer.h   |  33 ++
 .../graphics/graphics_breakpoints.cpp         | 212 +++++++++
 .../debugger/graphics/graphics_breakpoints.h  |  46 ++
 .../graphics/graphics_breakpoints_p.h         |  36 ++
 .../debugger/graphics/graphics_surface.cpp    | 445 ++++++++++++++++++
 src/yuzu/debugger/graphics/graphics_surface.h |  97 ++++
 src/yuzu/main.cpp                             |   9 +
 src/yuzu/main.h                               |   5 +-
 15 files changed, 1155 insertions(+), 4 deletions(-)
 create mode 100644 src/video_core/debug_utils/debug_utils.cpp
 create mode 100644 src/video_core/debug_utils/debug_utils.h
 create mode 100644 src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp
 create mode 100644 src/yuzu/debugger/graphics/graphics_breakpoint_observer.h
 create mode 100644 src/yuzu/debugger/graphics/graphics_breakpoints.cpp
 create mode 100644 src/yuzu/debugger/graphics/graphics_breakpoints.h
 create mode 100644 src/yuzu/debugger/graphics/graphics_breakpoints_p.h
 create mode 100644 src/yuzu/debugger/graphics/graphics_surface.cpp
 create mode 100644 src/yuzu/debugger/graphics/graphics_surface.h

diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 8c0e6663b..3dab81769 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -1,6 +1,8 @@
 add_library(video_core STATIC
     command_processor.cpp
     command_processor.h
+    debug_utils/debug_utils.cpp
+    debug_utils/debug_utils.h
     engines/fermi_2d.cpp
     engines/fermi_2d.h
     engines/maxwell_3d.cpp
diff --git a/src/video_core/debug_utils/debug_utils.cpp b/src/video_core/debug_utils/debug_utils.cpp
new file mode 100644
index 000000000..73fd4d7a3
--- /dev/null
+++ b/src/video_core/debug_utils/debug_utils.cpp
@@ -0,0 +1,66 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <condition_variable>
+#include <cstdint>
+#include <cstring>
+#include <fstream>
+#include <map>
+#include <mutex>
+#include <string>
+
+#include "common/assert.h"
+#include "common/bit_field.h"
+#include "common/color.h"
+#include "common/common_types.h"
+#include "common/file_util.h"
+#include "common/logging/log.h"
+#include "common/math_util.h"
+#include "common/vector_math.h"
+#include "video_core/debug_utils/debug_utils.h"
+
+namespace Tegra {
+
+std::shared_ptr<DebugContext> g_debug_context;
+
+void DebugContext::DoOnEvent(Event event, void* data) {
+    {
+        std::unique_lock<std::mutex> lock(breakpoint_mutex);
+
+        // TODO(Subv): Commit the rasterizer's caches so framebuffers, render targets, etc. will
+        // show on debug widgets
+
+        // TODO: Should stop the CPU thread here once we multithread emulation.
+
+        active_breakpoint = event;
+        at_breakpoint = true;
+
+        // Tell all observers that we hit a breakpoint
+        for (auto& breakpoint_observer : breakpoint_observers) {
+            breakpoint_observer->OnMaxwellBreakPointHit(event, data);
+        }
+
+        // Wait until another thread tells us to Resume()
+        resume_from_breakpoint.wait(lock, [&] { return !at_breakpoint; });
+    }
+}
+
+void DebugContext::Resume() {
+    {
+        std::lock_guard<std::mutex> lock(breakpoint_mutex);
+
+        // Tell all observers that we are about to resume
+        for (auto& breakpoint_observer : breakpoint_observers) {
+            breakpoint_observer->OnMaxwellResume();
+        }
+
+        // Resume the waiting thread (i.e. OnEvent())
+        at_breakpoint = false;
+    }
+
+    resume_from_breakpoint.notify_one();
+}
+
+} // namespace Tegra
diff --git a/src/video_core/debug_utils/debug_utils.h b/src/video_core/debug_utils/debug_utils.h
new file mode 100644
index 000000000..98461d6d9
--- /dev/null
+++ b/src/video_core/debug_utils/debug_utils.h
@@ -0,0 +1,165 @@
+// Copyright 2014 Citra Emulator Project
+// Licensed under GPLv2
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <algorithm>
+#include <array>
+#include <condition_variable>
+#include <iterator>
+#include <list>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+#include "common/common_types.h"
+#include "common/vector_math.h"
+
+namespace Tegra {
+
+class DebugContext {
+public:
+    enum class Event {
+        FirstEvent = 0,
+
+        MaxwellCommandLoaded = FirstEvent,
+        MaxwellCommandProcessed,
+        IncomingPrimitiveBatch,
+        FinishedPrimitiveBatch,
+
+        NumEvents
+    };
+
+    /**
+     * Inherit from this class to be notified of events registered to some debug context.
+     * Most importantly this is used for our debugger GUI.
+     *
+     * To implement event handling, override the OnMaxwellBreakPointHit and OnMaxwellResume methods.
+     * @warning All BreakPointObservers need to be on the same thread to guarantee thread-safe state
+     * access
+     * @todo Evaluate an alternative interface, in which there is only one managing observer and
+     * multiple child observers running (by design) on the same thread.
+     */
+    class BreakPointObserver {
+    public:
+        /// Constructs the object such that it observes events of the given DebugContext.
+        BreakPointObserver(std::shared_ptr<DebugContext> debug_context)
+            : context_weak(debug_context) {
+            std::unique_lock<std::mutex> lock(debug_context->breakpoint_mutex);
+            debug_context->breakpoint_observers.push_back(this);
+        }
+
+        virtual ~BreakPointObserver() {
+            auto context = context_weak.lock();
+            if (context) {
+                std::unique_lock<std::mutex> lock(context->breakpoint_mutex);
+                context->breakpoint_observers.remove(this);
+
+                // If we are the last observer to be destroyed, tell the debugger context that
+                // it is free to continue. In particular, this is required for a proper yuzu
+                // shutdown, when the emulation thread is waiting at a breakpoint.
+                if (context->breakpoint_observers.empty())
+                    context->Resume();
+            }
+        }
+
+        /**
+         * Action to perform when a breakpoint was reached.
+         * @param event Type of event which triggered the breakpoint
+         * @param data Optional data pointer (if unused, this is a nullptr)
+         * @note This function will perform nothing unless it is overridden in the child class.
+         */
+        virtual void OnMaxwellBreakPointHit(Event event, void* data) {}
+
+        /**
+         * Action to perform when emulation is resumed from a breakpoint.
+         * @note This function will perform nothing unless it is overridden in the child class.
+         */
+        virtual void OnMaxwellResume() {}
+
+    protected:
+        /**
+         * Weak context pointer. This need not be valid, so when requesting a shared_ptr via
+         * context_weak.lock(), always compare the result against nullptr.
+         */
+        std::weak_ptr<DebugContext> context_weak;
+    };
+
+    /**
+     * Simple structure defining a breakpoint state
+     */
+    struct BreakPoint {
+        bool enabled = false;
+    };
+
+    /**
+     * Static constructor used to create a shared_ptr of a DebugContext.
+     */
+    static std::shared_ptr<DebugContext> Construct() {
+        return std::shared_ptr<DebugContext>(new DebugContext);
+    }
+
+    /**
+     * Used by the emulation core when a given event has happened. If a breakpoint has been set
+     * for this event, OnEvent calls the event handlers of the registered breakpoint observers.
+     * The current thread then is halted until Resume() is called from another thread (or until
+     * emulation is stopped).
+     * @param event Event which has happened
+     * @param data Optional data pointer (pass nullptr if unused). Needs to remain valid until
+     * Resume() is called.
+     */
+    void OnEvent(Event event, void* data) {
+        // This check is left in the header to allow the compiler to inline it.
+        if (!breakpoints[(int)event].enabled)
+            return;
+        // For the rest of event handling, call a separate function.
+        DoOnEvent(event, data);
+    }
+
+    void DoOnEvent(Event event, void* data);
+
+    /**
+     * Resume from the current breakpoint.
+     * @warning Calling this from the same thread that OnEvent was called in will cause a deadlock.
+     * Calling from any other thread is safe.
+     */
+    void Resume();
+
+    /**
+     * Delete all set breakpoints and resume emulation.
+     */
+    void ClearBreakpoints() {
+        for (auto& bp : breakpoints) {
+            bp.enabled = false;
+        }
+        Resume();
+    }
+
+    // TODO: Evaluate if access to these members should be hidden behind a public interface.
+    std::array<BreakPoint, (int)Event::NumEvents> breakpoints;
+    Event active_breakpoint;
+    bool at_breakpoint = false;
+
+private:
+    /**
+     * Private default constructor to make sure people always construct this through Construct()
+     * instead.
+     */
+    DebugContext() = default;
+
+    /// Mutex protecting current breakpoint state and the observer list.
+    std::mutex breakpoint_mutex;
+
+    /// Used by OnEvent to wait for resumption.
+    std::condition_variable resume_from_breakpoint;
+
+    /// List of registered observers
+    std::list<BreakPointObserver*> breakpoint_observers;
+};
+
+extern std::shared_ptr<DebugContext> g_debug_context;
+
+} // namespace Tegra
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index c384d236e..9463cd5d6 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -18,4 +18,8 @@ GPU::GPU() {
 
 GPU::~GPU() = default;
 
+const Tegra::Engines::Maxwell3D& GPU::Get3DEngine() const {
+    return *maxwell_3d;
+}
+
 } // namespace Tegra
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index 206b3e05e..778b63218 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.h
@@ -13,6 +13,8 @@
 
 namespace Tegra {
 
+class DebugContext;
+
 /**
  * Struct describing framebuffer configuration
  */
@@ -66,6 +68,9 @@ public:
     /// Processes a command list stored at the specified address in GPU memory.
     void ProcessCommandList(GPUVAddr address, u32 size);
 
+    /// Returns a reference to the Maxwell3D GPU engine.
+    const Engines::Maxwell3D& Get3DEngine() const;
+
     std::unique_ptr<MemoryManager> memory_manager;
 
     Engines::Maxwell3D& Maxwell3D() {
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 0c4056c49..5af3154d7 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -23,6 +23,13 @@ add_executable(yuzu
     configuration/configure_input.h
     configuration/configure_system.cpp
     configuration/configure_system.h
+    debugger/graphics/graphics_breakpoint_observer.cpp
+    debugger/graphics/graphics_breakpoint_observer.h
+    debugger/graphics/graphics_breakpoints.cpp
+    debugger/graphics/graphics_breakpoints.h
+    debugger/graphics/graphics_breakpoints_p.h
+    debugger/graphics/graphics_surface.cpp
+    debugger/graphics/graphics_surface.h
     debugger/profiler.cpp
     debugger/profiler.h
     debugger/registers.cpp
diff --git a/src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp b/src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp
new file mode 100644
index 000000000..d6d61a739
--- /dev/null
+++ b/src/yuzu/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 "yuzu/debugger/graphics/graphics_breakpoint_observer.h"
+
+BreakPointObserverDock::BreakPointObserverDock(std::shared_ptr<Tegra::DebugContext> debug_context,
+                                               const QString& title, QWidget* parent)
+    : QDockWidget(title, parent), BreakPointObserver(debug_context) {
+    qRegisterMetaType<Tegra::DebugContext::Event>("Tegra::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(Tegra::DebugContext::Event, void*)), this,
+            SLOT(OnBreakPointHit(Tegra::DebugContext::Event, void*)), Qt::BlockingQueuedConnection);
+}
+
+void BreakPointObserverDock::OnMaxwellBreakPointHit(Tegra::DebugContext::Event event, void* data) {
+    emit BreakPointHit(event, data);
+}
+
+void BreakPointObserverDock::OnMaxwellResume() {
+    emit Resumed();
+}
diff --git a/src/yuzu/debugger/graphics/graphics_breakpoint_observer.h b/src/yuzu/debugger/graphics/graphics_breakpoint_observer.h
new file mode 100644
index 000000000..9d05493cf
--- /dev/null
+++ b/src/yuzu/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 OnMaxwellBreakPointHit and OnMaxwellResume to public slots.
+ * This is because the Maxwell 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 Tegra::DebugContext::BreakPointObserver {
+    Q_OBJECT
+
+public:
+    BreakPointObserverDock(std::shared_ptr<Tegra::DebugContext> debug_context, const QString& title,
+                           QWidget* parent = nullptr);
+
+    void OnMaxwellBreakPointHit(Tegra::DebugContext::Event event, void* data) override;
+    void OnMaxwellResume() override;
+
+private slots:
+    virtual void OnBreakPointHit(Tegra::DebugContext::Event event, void* data) = 0;
+    virtual void OnResumed() = 0;
+
+signals:
+    void Resumed();
+    void BreakPointHit(Tegra::DebugContext::Event event, void* data);
+};
diff --git a/src/yuzu/debugger/graphics/graphics_breakpoints.cpp b/src/yuzu/debugger/graphics/graphics_breakpoints.cpp
new file mode 100644
index 000000000..f98cc8152
--- /dev/null
+++ b/src/yuzu/debugger/graphics/graphics_breakpoints.cpp
@@ -0,0 +1,212 @@
+// 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 "common/assert.h"
+#include "yuzu/debugger/graphics/graphics_breakpoints.h"
+#include "yuzu/debugger/graphics/graphics_breakpoints_p.h"
+
+BreakPointModel::BreakPointModel(std::shared_ptr<Tegra::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>(Tegra::DebugContext::Event::NumEvents);
+}
+
+QVariant BreakPointModel::data(const QModelIndex& index, int role) const {
+    const auto event = static_cast<Tegra::DebugContext::Event>(index.row());
+
+    switch (role) {
+    case Qt::DisplayRole: {
+        if (index.column() == 0) {
+            static const std::map<Tegra::DebugContext::Event, QString> map = {
+                {Tegra::DebugContext::Event::MaxwellCommandLoaded, tr("Maxwell command loaded")},
+                {Tegra::DebugContext::Event::MaxwellCommandProcessed,
+                 tr("Maxwell command processed")},
+                {Tegra::DebugContext::Event::IncomingPrimitiveBatch,
+                 tr("Incoming primitive batch")},
+                {Tegra::DebugContext::Event::FinishedPrimitiveBatch,
+                 tr("Finished primitive batch")},
+            };
+
+            DEBUG_ASSERT(map.size() == static_cast<size_t>(Tegra::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<Tegra::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(Tegra::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<Tegra::DebugContext> debug_context, QWidget* parent)
+    : QDockWidget(tr("Maxwell Breakpoints"), parent), Tegra::DebugContext::BreakPointObserver(
+                                                          debug_context) {
+    setObjectName("TegraBreakPointsWidget");
+
+    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<Tegra::DebugContext::Event>("Tegra::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(Tegra::DebugContext::Event, void*)), this,
+            SLOT(OnBreakPointHit(Tegra::DebugContext::Event, void*)), Qt::BlockingQueuedConnection);
+    connect(this, SIGNAL(Resumed()), this, SLOT(OnResumed()));
+
+    connect(this, SIGNAL(BreakPointHit(Tegra::DebugContext::Event, void*)), breakpoint_model,
+            SLOT(OnBreakPointHit(Tegra::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::OnMaxwellBreakPointHit(Event event, void* data) {
+    // Process in GUI thread
+    emit BreakPointHit(event, data);
+}
+
+void GraphicsBreakPointsWidget::OnBreakPointHit(Tegra::DebugContext::Event event, void* data) {
+    status_text->setText(tr("Emulation halted at breakpoint"));
+    resume_button->setEnabled(true);
+}
+
+void GraphicsBreakPointsWidget::OnMaxwellResume() {
+    // 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/yuzu/debugger/graphics/graphics_breakpoints.h b/src/yuzu/debugger/graphics/graphics_breakpoints.h
new file mode 100644
index 000000000..ae0ede2e8
--- /dev/null
+++ b/src/yuzu/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, Tegra::DebugContext::BreakPointObserver {
+    Q_OBJECT
+
+    using Event = Tegra::DebugContext::Event;
+
+public:
+    explicit GraphicsBreakPointsWidget(std::shared_ptr<Tegra::DebugContext> debug_context,
+                                       QWidget* parent = nullptr);
+
+    void OnMaxwellBreakPointHit(Tegra::DebugContext::Event event, void* data) override;
+    void OnMaxwellResume() override;
+
+public slots:
+    void OnBreakPointHit(Tegra::DebugContext::Event event, void* data);
+    void OnItemDoubleClicked(const QModelIndex&);
+    void OnResumeRequested();
+    void OnResumed();
+
+signals:
+    void Resumed();
+    void BreakPointHit(Tegra::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/yuzu/debugger/graphics/graphics_breakpoints_p.h b/src/yuzu/debugger/graphics/graphics_breakpoints_p.h
new file mode 100644
index 000000000..35a6876ae
--- /dev/null
+++ b/src/yuzu/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<Tegra::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(Tegra::DebugContext::Event event);
+    void OnResumed();
+
+private:
+    std::weak_ptr<Tegra::DebugContext> context_weak;
+    bool at_breakpoint;
+    Tegra::DebugContext::Event active_breakpoint;
+};
diff --git a/src/yuzu/debugger/graphics/graphics_surface.cpp b/src/yuzu/debugger/graphics/graphics_surface.cpp
new file mode 100644
index 000000000..54b816054
--- /dev/null
+++ b/src/yuzu/debugger/graphics/graphics_surface.cpp
@@ -0,0 +1,445 @@
+// 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 "core/core.h"
+#include "video_core/engines/maxwell_3d.h"
+#include "video_core/textures/decoders.h"
+#include "video_core/textures/texture.h"
+#include "video_core/utils.h"
+#include "yuzu/debugger/graphics/graphics_surface.h"
+#include "yuzu/util/spinbox.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<Tegra::DebugContext> debug_context,
+                                             QWidget* parent)
+    : BreakPointObserverDock(debug_context, tr("Maxwell Surface Viewer"), parent),
+      surface_source(Source::RenderTarget0) {
+    setObjectName("MaxwellSurface");
+
+    surface_source_list = new QComboBox;
+    surface_source_list->addItem(tr("Render Target 0"));
+    surface_source_list->addItem(tr("Render Target 1"));
+    surface_source_list->addItem(tr("Render Target 2"));
+    surface_source_list->addItem(tr("Render Target 3"));
+    surface_source_list->addItem(tr("Render Target 4"));
+    surface_source_list->addItem(tr("Render Target 5"));
+    surface_source_list->addItem(tr("Render Target 6"));
+    surface_source_list->addItem(tr("Render Target 7"));
+    surface_source_list->addItem(tr("Z Buffer"));
+    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 Maxwell texture format index
+    surface_format_control->addItem(tr("None"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("A8R8G8B8"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("Unknown"));
+    surface_format_control->addItem(tr("DXT1"));
+    surface_format_control->addItem(tr("DXT23"));
+    surface_format_control->addItem(tr("DXT45"));
+    surface_format_control->addItem(tr("DXN1"));
+    surface_format_control->addItem(tr("DXN2"));
+
+    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("GPU 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(Tegra::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<Tegra::Texture::TextureFormat>(new_value)) {
+        surface_format = static_cast<Tegra::Texture::TextureFormat>(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 >= static_cast<int>(surface_width) || y < 0 ||
+        y >= static_cast<int>(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;
+    }
+
+    surface_info_label->setText(QString("Raw: <Unimplemented>\n(%1)").arg("<Unimplemented>"));
+    surface_info_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+}
+
+void GraphicsSurfaceWidget::OnUpdate() {
+    auto& gpu = Core::System::GetInstance().GPU();
+
+    QPixmap pixmap;
+
+    Tegra::GPUVAddr surface_address = 0;
+
+    switch (surface_source) {
+    case Source::RenderTarget0:
+    case Source::RenderTarget1:
+    case Source::RenderTarget2:
+    case Source::RenderTarget3:
+    case Source::RenderTarget4:
+    case Source::RenderTarget5:
+    case Source::RenderTarget6:
+    case Source::RenderTarget7: {
+        // TODO: Store a reference to the registers in the debug context instead of accessing them
+        // directly...
+
+        auto& registers = gpu.Get3DEngine().regs;
+
+        surface_address = 0;
+        surface_width = 0;
+        surface_height = 0;
+        surface_format = Tegra::Texture::TextureFormat::DXT1;
+
+        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));
+
+    if (surface_address == 0) {
+        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;
+    }
+
+    // TODO: Implement a good way to visualize alpha components!
+
+    QImage decoded_image(surface_width, surface_height, QImage::Format_ARGB32);
+    VAddr address = gpu.memory_manager->PhysicalToVirtualAddress(surface_address);
+
+    auto unswizzled_data =
+        Tegra::Texture::UnswizzleTexture(address, surface_format, surface_width, surface_height);
+
+    auto texture_data = Tegra::Texture::DecodeTexture(unswizzled_data, surface_format,
+                                                      surface_width, surface_height);
+
+    ASSERT(texture_data.size() ==
+           surface_width * surface_height *
+               Tegra::Texture::BytesPerPixel(Tegra::Texture::TextureFormat::A8R8G8B8));
+    surface_picture_label->show();
+
+    for (unsigned int y = 0; y < surface_height; ++y) {
+        for (unsigned int x = 0; x < surface_width; ++x) {
+            Math::Vec4<u8> color;
+            color[0] = texture_data[x + y * surface_width + 0];
+            color[1] = texture_data[x + y * surface_width + 1];
+            color[2] = texture_data[x + y * surface_width + 2];
+            color[3] = texture_data[x + y * surface_width + 3];
+            decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), color.a()));
+        }
+    }
+
+    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 * Tegra::Texture::BytesPerPixel(surface_format);
+        QByteArray data(reinterpret_cast<const char*>(buffer), size);
+        file.write(data);
+    } else {
+        UNREACHABLE_MSG("Unhandled filter selected");
+    }
+}
diff --git a/src/yuzu/debugger/graphics/graphics_surface.h b/src/yuzu/debugger/graphics/graphics_surface.h
new file mode 100644
index 000000000..6a344bdfc
--- /dev/null
+++ b/src/yuzu/debugger/graphics/graphics_surface.h
@@ -0,0 +1,97 @@
+// 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 "video_core/memory_manager.h"
+#include "video_core/textures/texture.h"
+#include "yuzu/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 = Tegra::DebugContext::Event;
+
+    enum class Source {
+        RenderTarget0 = 0,
+        RenderTarget1 = 1,
+        RenderTarget2 = 2,
+        RenderTarget3 = 3,
+        RenderTarget4 = 4,
+        RenderTarget5 = 5,
+        RenderTarget6 = 6,
+        RenderTarget7 = 7,
+        ZBuffer = 8,
+        Custom = 9,
+    };
+
+public:
+    explicit GraphicsSurfaceWidget(std::shared_ptr<Tegra::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(Tegra::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;
+    Tegra::GPUVAddr surface_address;
+    unsigned surface_width;
+    unsigned surface_height;
+    Tegra::Texture::TextureFormat surface_format;
+    int surface_picker_x = 0;
+    int surface_picker_y = 0;
+};
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index eb22a8ccf..7b065ee7b 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -29,6 +29,7 @@
 #include "yuzu/bootmanager.h"
 #include "yuzu/configuration/config.h"
 #include "yuzu/configuration/configure_dialog.h"
+#include "yuzu/debugger/graphics/graphics_breakpoints.h"
 #include "yuzu/debugger/profiler.h"
 #include "yuzu/debugger/registers.h"
 #include "yuzu/debugger/wait_tree.h"
@@ -68,6 +69,9 @@ static void ShowCalloutMessage(const QString& message, CalloutFlag flag) {
 void GMainWindow::ShowCallouts() {}
 
 GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
+
+    Tegra::g_debug_context = Tegra::DebugContext::Construct();
+
     setAcceptDrops(true);
     ui.setupUi(this);
     statusBar()->hide();
@@ -160,6 +164,11 @@ void GMainWindow::InitializeDebugWidgets() {
     connect(this, &GMainWindow::EmulationStopping, registersWidget,
             &RegistersWidget::OnEmulationStopping);
 
+    graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Tegra::g_debug_context, this);
+    addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget);
+    graphicsBreakpointsWidget->hide();
+    debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction());
+
     waitTreeWidget = new WaitTreeWidget(this);
     addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget);
     waitTreeWidget->hide();
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 4a0d912bb..86528f5b0 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -15,11 +15,7 @@ class Config;
 class EmuThread;
 class GameList;
 class GImageInfo;
-class GPUCommandStreamWidget;
-class GPUCommandListWidget;
 class GraphicsBreakPointsWidget;
-class GraphicsTracingWidget;
-class GraphicsVertexShaderWidget;
 class GRenderWindow;
 class MicroProfileDialog;
 class ProfilerWidget;
@@ -158,6 +154,7 @@ private:
     ProfilerWidget* profilerWidget;
     MicroProfileDialog* microProfileDialog;
     RegistersWidget* registersWidget;
+    GraphicsBreakPointsWidget* graphicsBreakpointsWidget;
     WaitTreeWidget* waitTreeWidget;
 
     QAction* actions_recent_files[max_recent_files_item];