diff --git a/src/video_core/renderer_opengl/gl_device.cpp b/src/video_core/renderer_opengl/gl_device.cpp
index 1a2e2a9f7d..e824e216d6 100644
--- a/src/video_core/renderer_opengl/gl_device.cpp
+++ b/src/video_core/renderer_opengl/gl_device.cpp
@@ -157,6 +157,7 @@ Device::Device() : base_bindings{BuildBaseBindings()} {
     has_precise_bug = TestPreciseBug();
     has_broken_compute = is_intel_proprietary;
     has_fast_buffer_sub_data = is_nvidia;
+    has_debug_tool = HasExtension(extensions, "GL_EXT_debug_tool");
 
     LOG_INFO(Render_OpenGL, "Renderer_VariableAOFFI: {}", has_variable_aoffi);
     LOG_INFO(Render_OpenGL, "Renderer_ComponentIndexingBug: {}", has_component_indexing_bug);
diff --git a/src/video_core/renderer_opengl/gl_device.h b/src/video_core/renderer_opengl/gl_device.h
index d73b099d01..7aaa3a077f 100644
--- a/src/video_core/renderer_opengl/gl_device.h
+++ b/src/video_core/renderer_opengl/gl_device.h
@@ -84,6 +84,10 @@ public:
         return has_fast_buffer_sub_data;
     }
 
+    bool HasDebugTool() const {
+        return has_debug_tool;
+    }
+
 private:
     static bool TestVariableAoffi();
     static bool TestPreciseBug();
@@ -102,6 +106,7 @@ private:
     bool has_precise_bug{};
     bool has_broken_compute{};
     bool has_fast_buffer_sub_data{};
+    bool has_debug_tool{};
 };
 
 } // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index 12333e8c9f..c91658cd1b 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -56,7 +56,7 @@ public:
     std::deque<Frame*> present_queue;
     Frame* previous_frame{};
 
-    FrameMailbox() {
+    FrameMailbox() : has_debug_tool{Device().HasDebugTool()} {
         for (auto& frame : swap_chain) {
             free_queue.push(&frame);
         }
@@ -127,9 +127,13 @@ public:
         std::unique_lock lock{swap_chain_lock};
         present_queue.push_front(frame);
         present_cv.notify_one();
+
+        DebugNotifyNextFrame();
     }
 
     Frame* TryGetPresentFrame(int timeout_ms) {
+        DebugWaitForNextFrame();
+
         std::unique_lock lock{swap_chain_lock};
         // wait for new entries in the present_queue
         present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
@@ -155,6 +159,33 @@ public:
         previous_frame = frame;
         return frame;
     }
+
+private:
+    std::mutex debug_synch_mutex;
+    std::condition_variable debug_synch_condition;
+    std::atomic_int frame_for_debug{};
+    const bool has_debug_tool; // When true, using a GPU debugger, so keep frames in lock-step
+
+    /// Signal that a new frame is available (called from GPU thread)
+    void DebugNotifyNextFrame() {
+        if (!has_debug_tool) {
+            return;
+        }
+        frame_for_debug++;
+        std::lock_guard lock{debug_synch_mutex};
+        debug_synch_condition.notify_one();
+    }
+
+    /// Wait for a new frame to be available (called from presentation thread)
+    void DebugWaitForNextFrame() {
+        if (!has_debug_tool) {
+            return;
+        }
+        const int last_frame = frame_for_debug;
+        std::unique_lock lock{debug_synch_mutex};
+        debug_synch_condition.wait(lock,
+                                   [this, last_frame] { return frame_for_debug > last_frame; });
+    }
 };
 
 namespace {