diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp
index 9a29f974b1..b53206be65 100644
--- a/src/citra_qt/bootmanager.cpp
+++ b/src/citra_qt/bootmanager.cpp
@@ -14,6 +14,8 @@
 #include "core/core.h"
 #include "core/settings.h"
 
+#include "video_core/debug_utils/debug_utils.h"
+
 #include "video_core/video_core.h"
 
 #include "citra_qt/version.h"
@@ -65,14 +67,21 @@ void EmuThread::Stop()
     }
     stop_run = true;
 
+    // Release emu threads from any breakpoints, so that this doesn't hang forever.
+    Pica::g_debug_context->ClearBreakpoints();
+
     //core::g_state = core::SYS_DIE;
 
-    wait(500);
+    // TODO: Waiting here is just a bad workaround for retarded shutdown logic.
+    wait(1000);
     if (isRunning())
     {
         WARN_LOG(MASTER_LOG, "EmuThread still running, terminating...");
         quit();
-        wait(1000);
+
+        // TODO: Waiting 50 seconds can be necessary if the logging subsystem has a lot of spam
+        // queued... This should be fixed.
+        wait(50000);
         if (isRunning())
         {
             WARN_LOG(MASTER_LOG, "EmuThread STILL running, something is wrong here...");
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 0701decefa..869826e615 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -36,6 +36,8 @@ GMainWindow::GMainWindow()
 {
     LogManager::Init();
 
+    Pica::g_debug_context = Pica::DebugContext::Construct();
+
     Config config;
 
     if (!Settings::values.enable_log)
@@ -133,6 +135,8 @@ GMainWindow::~GMainWindow()
     // will get automatically deleted otherwise
     if (render_window->parent() == nullptr)
         delete render_window;
+
+    Pica::g_debug_context.reset();
 }
 
 void GMainWindow::BootGame(std::string filename)
diff --git a/src/video_core/command_processor.cpp b/src/video_core/command_processor.cpp
index 8a6ba25606..298b04c51e 100644
--- a/src/video_core/command_processor.cpp
+++ b/src/video_core/command_processor.cpp
@@ -34,6 +34,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
     u32 old_value = registers[id];
     registers[id] = (old_value & ~mask) | (value & mask);
 
+    if (g_debug_context)
+        g_debug_context->OnEvent(DebugContext::Event::CommandLoaded, reinterpret_cast<void*>(&id));
+
     DebugUtils::OnPicaRegWrite(id, registers[id]);
 
     switch(id) {
@@ -43,6 +46,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
         {
             DebugUtils::DumpTevStageConfig(registers.GetTevStages());
 
+            if (g_debug_context)
+                g_debug_context->OnEvent(DebugContext::Event::IncomingPrimitiveBatch, nullptr);
+
             const auto& attribute_config = registers.vertex_attributes;
             const u8* const base_address = Memory::GetPointer(attribute_config.GetBaseAddress());
 
@@ -132,6 +138,10 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
                 clipper_primitive_assembler.SubmitVertex(output, Clipper::ProcessTriangle);
             }
             geometry_dumper.Dump();
+
+            if (g_debug_context)
+                g_debug_context->OnEvent(DebugContext::Event::FinishedPrimitiveBatch, nullptr);
+
             break;
         }
 
@@ -229,6 +239,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
         default:
             break;
     }
+
+    if (g_debug_context)
+        g_debug_context->OnEvent(DebugContext::Event::CommandProcessed, reinterpret_cast<void*>(&id));
 }
 
 static std::ptrdiff_t ExecuteCommandBlock(const u32* first_command_word) {
diff --git a/src/video_core/debug_utils/debug_utils.cpp b/src/video_core/debug_utils/debug_utils.cpp
index 8a5f114247..11f87d9882 100644
--- a/src/video_core/debug_utils/debug_utils.cpp
+++ b/src/video_core/debug_utils/debug_utils.cpp
@@ -3,6 +3,8 @@
 // Refer to the license.txt file included.
 
 #include <algorithm>
+#include <condition_variable>
+#include <list>
 #include <map>
 #include <fstream>
 #include <mutex>
@@ -12,6 +14,7 @@
 #include <png.h>
 #endif
 
+#include "common/log.h"
 #include "common/file_util.h"
 
 #include "video_core/pica.h"
@@ -20,6 +23,46 @@
 
 namespace Pica {
 
+void DebugContext::OnEvent(Event event, void* data) {
+    if (!breakpoints[event].enabled)
+        return;
+
+    {
+        std::unique_lock<std::mutex> lock(breakpoint_mutex);
+
+        // 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->OnPicaBreakPointHit(event, data);
+        }
+
+        // Wait until another thread tells us to Resume()
+        resume_from_breakpoint.wait(lock, [&]{ return !at_breakpoint; });
+    }
+}
+
+void DebugContext::Resume() {
+    {
+        std::unique_lock<std::mutex> lock(breakpoint_mutex);
+
+        // Tell all observers that we are about to resume
+        for (auto& breakpoint_observer : breakpoint_observers) {
+            breakpoint_observer->OnPicaResume();
+        }
+
+        // Resume the waiting thread (i.e. OnEvent())
+        at_breakpoint = false;
+    }
+
+    resume_from_breakpoint.notify_one();
+}
+
+std::shared_ptr<DebugContext> g_debug_context; // TODO: Get rid of this global
+
 namespace DebugUtils {
 
 void GeometryDumper::AddTriangle(Vertex& v0, Vertex& v1, Vertex& v2) {
diff --git a/src/video_core/debug_utils/debug_utils.h b/src/video_core/debug_utils/debug_utils.h
index b1558cfae6..26b26e22f1 100644
--- a/src/video_core/debug_utils/debug_utils.h
+++ b/src/video_core/debug_utils/debug_utils.h
@@ -5,13 +5,146 @@
 #pragma once
 
 #include <array>
+#include <condition_variable>
+#include <list>
+#include <map>
 #include <memory>
+#include <mutex>
 #include <vector>
 
 #include "video_core/pica.h"
 
 namespace Pica {
 
+class DebugContext {
+public:
+    enum class Event {
+        FirstEvent = 0,
+
+        CommandLoaded = FirstEvent,
+        CommandProcessed,
+        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 OnPicaBreakPointHit and OnPicaResume 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 Citra
+                // 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 OnPicaBreakPointHit(Event, void*) {
+        }
+
+        /**
+         * 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 OnPicaResume() {
+        }
+
+    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);
+
+    /**
+     * 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() {
+        breakpoints.clear();
+        Resume();
+    }
+
+    // TODO: Evaluate if access to these members should be hidden behind a public interface.
+    std::map<Event, BreakPoint> 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; // TODO: Get rid of this global
+
 namespace DebugUtils {
 
 // Simple utility class for dumping geometry data to an OBJ file