diff --git a/src/core/core.cpp b/src/core/core.cpp
index 01ab8481c..cde0daa94 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -308,6 +308,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo
     Service::Init(*this);
     GDBStub::Init();
 
+#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
+    video_dumper = std::make_unique<VideoDumper::FFmpegBackend>();
+#else
+    video_dumper = std::make_unique<VideoDumper::NullBackend>();
+#endif
+
     VideoCore::ResultStatus result = VideoCore::Init(emu_window, *memory);
     if (result != VideoCore::ResultStatus::Success) {
         switch (result) {
@@ -320,12 +326,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo
         }
     }
 
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
-    video_dumper = std::make_unique<VideoDumper::FFmpegBackend>();
-#else
-    video_dumper = std::make_unique<VideoDumper::NullBackend>();
-#endif
-
     LOG_DEBUG(Core, "Initialized OK");
 
     initalized = true;
diff --git a/src/core/dumping/backend.cpp b/src/core/dumping/backend.cpp
index daf43c744..88686b7a2 100644
--- a/src/core/dumping/backend.cpp
+++ b/src/core/dumping/backend.cpp
@@ -8,17 +8,7 @@
 namespace VideoDumper {
 
 VideoFrame::VideoFrame(std::size_t width_, std::size_t height_, u8* data_)
-    : width(width_), height(height_), stride(width * 4), data(width * height * 4) {
-    // While copying, rotate the image to put the pixels in correct order
-    // (As OpenGL returns pixel data starting from the lowest position)
-    for (std::size_t i = 0; i < height; i++) {
-        for (std::size_t j = 0; j < width; j++) {
-            for (std::size_t k = 0; k < 4; k++) {
-                data[i * stride + j * 4 + k] = data_[(height - i - 1) * stride + j * 4 + k];
-            }
-        }
-    }
-}
+    : width(width_), height(height_), stride(width * 4), data(data_, data_ + width * height * 4) {}
 
 Backend::~Backend() = default;
 NullBackend::~NullBackend() = default;
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 4cb976354..245b0c49d 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -23,6 +23,8 @@ add_library(video_core STATIC
     regs_texturing.h
     renderer_base.cpp
     renderer_base.h
+    renderer_opengl/frame_dumper_opengl.cpp
+    renderer_opengl/frame_dumper_opengl.h
     renderer_opengl/gl_rasterizer.cpp
     renderer_opengl/gl_rasterizer.h
     renderer_opengl/gl_rasterizer_cache.cpp
diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp
new file mode 100644
index 000000000..7be4cc8ef
--- /dev/null
+++ b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp
@@ -0,0 +1,98 @@
+// Copyright 2020 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <glad/glad.h>
+#include "core/frontend/emu_window.h"
+#include "core/frontend/scope_acquire_context.h"
+#include "video_core/renderer_opengl/frame_dumper_opengl.h"
+#include "video_core/renderer_opengl/renderer_opengl.h"
+
+namespace OpenGL {
+
+FrameDumperOpenGL::FrameDumperOpenGL(VideoDumper::Backend& video_dumper_,
+                                     Frontend::EmuWindow& emu_window)
+    : video_dumper(video_dumper_), context(emu_window.CreateSharedContext()) {}
+
+FrameDumperOpenGL::~FrameDumperOpenGL() {
+    if (present_thread.joinable())
+        present_thread.join();
+}
+
+bool FrameDumperOpenGL::IsDumping() const {
+    return video_dumper.IsDumping();
+}
+
+Layout::FramebufferLayout FrameDumperOpenGL::GetLayout() const {
+    return video_dumper.GetLayout();
+}
+
+void FrameDumperOpenGL::StartDumping() {
+    if (present_thread.joinable())
+        present_thread.join();
+
+    present_thread = std::thread(&FrameDumperOpenGL::PresentLoop, this);
+}
+
+void FrameDumperOpenGL::StopDumping() {
+    stop_requested.store(true, std::memory_order_relaxed);
+}
+
+void FrameDumperOpenGL::PresentLoop() {
+    Frontend::ScopeAcquireContext scope{*context};
+    InitializeOpenGLObjects();
+
+    const auto& layout = GetLayout();
+    while (!stop_requested.exchange(false)) {
+        auto frame = mailbox->TryGetPresentFrame(200);
+        if (!frame) {
+            continue;
+        }
+
+        if (frame->color_reloaded) {
+            LOG_DEBUG(Render_OpenGL, "Reloading present frame");
+            mailbox->ReloadPresentFrame(frame, layout.width, layout.height);
+        }
+        glWaitSync(frame->render_fence, 0, GL_TIMEOUT_IGNORED);
+
+        glBindFramebuffer(GL_READ_FRAMEBUFFER, frame->present.handle);
+        glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[current_pbo].handle);
+        glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0);
+
+        // Insert fence for the main thread to block on
+        frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+        glFlush();
+
+        // Bind the previous PBO and read the pixels
+        glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle);
+        GLubyte* pixels = static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
+        VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
+        video_dumper.AddVideoFrame(frame_data);
+        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+
+        current_pbo = (current_pbo + 1) % 2;
+        next_pbo = (current_pbo + 1) % 2;
+    }
+
+    CleanupOpenGLObjects();
+}
+
+void FrameDumperOpenGL::InitializeOpenGLObjects() {
+    const auto& layout = GetLayout();
+    for (auto& buffer : pbos) {
+        buffer.Create();
+        glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle);
+        glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr,
+                     GL_STREAM_READ);
+        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+    }
+}
+
+void FrameDumperOpenGL::CleanupOpenGLObjects() {
+    for (auto& buffer : pbos) {
+        buffer.Release();
+    }
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.h b/src/video_core/renderer_opengl/frame_dumper_opengl.h
new file mode 100644
index 000000000..da6d96053
--- /dev/null
+++ b/src/video_core/renderer_opengl/frame_dumper_opengl.h
@@ -0,0 +1,57 @@
+// Copyright 2020 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <atomic>
+#include <memory>
+#include <thread>
+#include "core/dumping/backend.h"
+#include "core/frontend/framebuffer_layout.h"
+#include "video_core/renderer_opengl/gl_resource_manager.h"
+
+namespace Frontend {
+class EmuWindow;
+class GraphicsContext;
+class TextureMailbox;
+} // namespace Frontend
+
+namespace OpenGL {
+
+class RendererOpenGL;
+
+/**
+ * This is the 'presentation' part in frame dumping.
+ * Processes frames/textures sent to its mailbox, downloads the pixels and sends the data
+ * to the video encoding backend.
+ */
+class FrameDumperOpenGL {
+public:
+    explicit FrameDumperOpenGL(VideoDumper::Backend& video_dumper, Frontend::EmuWindow& emu_window);
+    ~FrameDumperOpenGL();
+
+    bool IsDumping() const;
+    Layout::FramebufferLayout GetLayout() const;
+    void StartDumping();
+    void StopDumping();
+
+    std::unique_ptr<Frontend::TextureMailbox> mailbox;
+
+private:
+    void InitializeOpenGLObjects();
+    void CleanupOpenGLObjects();
+    void PresentLoop();
+
+    VideoDumper::Backend& video_dumper;
+    std::unique_ptr<Frontend::GraphicsContext> context;
+    std::thread present_thread;
+    std::atomic_bool stop_requested{false};
+
+    // PBOs used to dump frames faster
+    std::array<OGLBuffer, 2> pbos;
+    GLuint current_pbo = 1;
+    GLuint next_pbo = 0;
+};
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index 5046895e0..b1fcfb592 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -34,20 +34,6 @@
 #include "video_core/renderer_opengl/renderer_opengl.h"
 #include "video_core/video_core.h"
 
-namespace Frontend {
-
-struct Frame {
-    u32 width{};                      /// Width of the frame (to detect resize)
-    u32 height{};                     /// Height of the frame
-    bool color_reloaded = false;      /// Texture attachment was recreated (ie: resized)
-    OpenGL::OGLRenderbuffer color{};  /// Buffer shared between the render/present FBO
-    OpenGL::OGLFramebuffer render{};  /// FBO created on the render thread
-    OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread
-    GLsync render_fence{};            /// Fence created on the render thread
-    GLsync present_fence{};           /// Fence created on the presentation thread
-};
-} // namespace Frontend
-
 namespace OpenGL {
 
 // If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have
@@ -78,6 +64,7 @@ public:
         std::queue<Frontend::Frame*>().swap(free_queue);
         present_queue.clear();
         present_cv.notify_all();
+        free_cv.notify_all();
     }
 
     void ReloadPresentFrame(Frontend::Frame* frame, u32 height, u32 width) override {
@@ -88,7 +75,7 @@ public:
         glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle);
         glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
                                   frame->color.handle);
-        if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
+        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
             LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!");
         }
         glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo);
@@ -114,7 +101,7 @@ public:
         state.Apply();
         glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
                                   frame->color.handle);
-        if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
+        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
             LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!");
         }
         prev_state.Apply();
@@ -144,19 +131,12 @@ public:
         present_cv.notify_one();
     }
 
-    Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
-        std::unique_lock<std::mutex> lock(swap_chain_lock);
-        // wait for new entries in the present_queue
-        present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
-                            [&] { return !present_queue.empty(); });
-        if (present_queue.empty()) {
-            // timed out waiting for a frame to draw so return the previous frame
-            return previous_frame;
-        }
-
+    // This is virtual as it is to be overriden in OGLVideoDumpingMailbox below.
+    virtual void LoadPresentFrame() {
         // free the previous frame and add it back to the free queue
         if (previous_frame) {
             free_queue.push(previous_frame);
+            free_cv.notify_one();
         }
 
         // the newest entries are pushed to the front of the queue
@@ -168,8 +148,72 @@ public:
         }
         present_queue.clear();
         previous_frame = frame;
+    }
+
+    Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
+        std::unique_lock<std::mutex> lock(swap_chain_lock);
+        // wait for new entries in the present_queue
+        present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
+                            [&] { return !present_queue.empty(); });
+        if (present_queue.empty()) {
+            // timed out waiting for a frame to draw so return the previous frame
+            return previous_frame;
+        }
+
+        LoadPresentFrame();
+        return previous_frame;
+    }
+};
+
+/// This mailbox is different in that it will never discard rendered frames
+class OGLVideoDumpingMailbox : public OGLTextureMailbox {
+public:
+    Frontend::Frame* GetRenderFrame() override {
+        std::unique_lock<std::mutex> lock(swap_chain_lock);
+
+        // If theres no free frames, we will wait until one shows up
+        if (free_queue.empty()) {
+            free_cv.wait(lock, [&] { return !free_queue.empty(); });
+        }
+
+        if (free_queue.empty()) {
+            LOG_CRITICAL(Render_OpenGL, "Could not get free frame");
+            return nullptr;
+        }
+
+        Frontend::Frame* frame = free_queue.front();
+        free_queue.pop();
         return frame;
     }
+
+    void LoadPresentFrame() override {
+        // free the previous frame and add it back to the free queue
+        if (previous_frame) {
+            free_queue.push(previous_frame);
+            free_cv.notify_one();
+        }
+
+        Frontend::Frame* frame = present_queue.back();
+        present_queue.pop_back();
+        previous_frame = frame;
+
+        // Do not remove entries from the present_queue, as video dumping would require
+        // that we preserve all frames
+    }
+
+    Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
+        std::unique_lock<std::mutex> lock(swap_chain_lock);
+        // wait for new entries in the present_queue
+        present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
+                            [&] { return !present_queue.empty(); });
+        if (present_queue.empty()) {
+            // timed out waiting for a frame
+            return nullptr;
+        }
+
+        LoadPresentFrame();
+        return previous_frame;
+    }
 };
 
 static const char vertex_shader[] = R"(
@@ -278,21 +322,35 @@ struct ScreenRectVertex {
  *
  * The projection part of the matrix is trivial, hence these operations are represented
  * by a 3x2 matrix.
+ *
+ * @param flipped Whether the frame should be flipped upside down.
  */
-static std::array<GLfloat, 3 * 2> MakeOrthographicMatrix(const float width, const float height) {
+static std::array<GLfloat, 3 * 2> MakeOrthographicMatrix(const float width, const float height,
+                                                         bool flipped) {
+
     std::array<GLfloat, 3 * 2> matrix; // Laid out in column-major order
 
-    // clang-format off
-    matrix[0] = 2.f / width; matrix[2] = 0.f;           matrix[4] = -1.f;
-    matrix[1] = 0.f;         matrix[3] = -2.f / height; matrix[5] = 1.f;
     // Last matrix row is implicitly assumed to be [0, 0, 1].
-    // clang-format on
+    if (flipped) {
+        // clang-format off
+        matrix[0] = 2.f / width; matrix[2] = 0.f;           matrix[4] = -1.f;
+        matrix[1] = 0.f;         matrix[3] = 2.f / height;  matrix[5] = -1.f;
+        // clang-format on
+    } else {
+        // clang-format off
+        matrix[0] = 2.f / width; matrix[2] = 0.f;           matrix[4] = -1.f;
+        matrix[1] = 0.f;         matrix[3] = -2.f / height; matrix[5] = 1.f;
+        // clang-format on
+    }
 
     return matrix;
 }
 
-RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) : RendererBase{window} {
+RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window)
+    : RendererBase{window}, frame_dumper(Core::System::GetInstance().VideoDumper(), window) {
+
     window.mailbox = std::make_unique<OGLTextureMailbox>();
+    frame_dumper.mailbox = std::make_unique<OGLVideoDumpingMailbox>();
 }
 
 RendererOpenGL::~RendererOpenGL() = default;
@@ -310,56 +368,14 @@ void RendererOpenGL::SwapBuffers() {
 
     RenderScreenshot();
 
-    RenderVideoDumping();
-
     const auto& layout = render_window.GetFramebufferLayout();
+    RenderToMailbox(layout, render_window.mailbox, false);
 
-    Frontend::Frame* frame;
-    {
-        MICROPROFILE_SCOPE(OpenGL_WaitPresent);
-
-        frame = render_window.mailbox->GetRenderFrame();
-
-        // Clean up sync objects before drawing
-
-        // INTEL driver workaround. We can't delete the previous render sync object until we are
-        // sure that the presentation is done
-        if (frame->present_fence) {
-            glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
-        }
-
-        // delete the draw fence if the frame wasn't presented
-        if (frame->render_fence) {
-            glDeleteSync(frame->render_fence);
-            frame->render_fence = 0;
-        }
-
-        // wait for the presentation to be done
-        if (frame->present_fence) {
-            glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
-            glDeleteSync(frame->present_fence);
-            frame->present_fence = 0;
-        }
+    if (frame_dumper.IsDumping()) {
+        RenderToMailbox(frame_dumper.GetLayout(), frame_dumper.mailbox, true);
     }
 
-    {
-        MICROPROFILE_SCOPE(OpenGL_RenderFrame);
-        // Recreate the frame if the size of the window has changed
-        if (layout.width != frame->width || layout.height != frame->height) {
-            LOG_DEBUG(Render_OpenGL, "Reloading render frame");
-            render_window.mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
-        }
-
-        GLuint render_texture = frame->color.handle;
-        state.draw.draw_framebuffer = frame->render.handle;
-        state.Apply();
-        DrawScreens(layout);
-        // Create a fence for the frontend to wait on and swap this frame to OffTex
-        frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
-        glFlush();
-        render_window.mailbox->ReleaseRenderFrame(frame);
-        m_current_frame++;
-    }
+    m_current_frame++;
 
     Core::System::GetInstance().perf_stats->EndSystemFrame();
 
@@ -395,7 +411,7 @@ void RendererOpenGL::RenderScreenshot() {
         glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
                                   renderbuffer);
 
-        DrawScreens(layout);
+        DrawScreens(layout, false);
 
         glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
                      VideoCore::g_screenshot_bits);
@@ -448,33 +464,54 @@ void RendererOpenGL::PrepareRendertarget() {
     }
 }
 
-void RendererOpenGL::RenderVideoDumping() {
-    if (cleanup_video_dumping.exchange(false)) {
-        ReleaseVideoDumpingGLObjects();
-    }
+void RendererOpenGL::RenderToMailbox(const Layout::FramebufferLayout& layout,
+                                     std::unique_ptr<Frontend::TextureMailbox>& mailbox,
+                                     bool flipped) {
 
-    if (Core::System::GetInstance().VideoDumper().IsDumping()) {
-        if (prepare_video_dumping.exchange(false)) {
-            InitVideoDumpingGLObjects();
+    Frontend::Frame* frame;
+    {
+        MICROPROFILE_SCOPE(OpenGL_WaitPresent);
+
+        frame = mailbox->GetRenderFrame();
+
+        // Clean up sync objects before drawing
+
+        // INTEL driver workaround. We can't delete the previous render sync object until we are
+        // sure that the presentation is done
+        if (frame->present_fence) {
+            glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
         }
 
-        const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout();
-        glBindFramebuffer(GL_READ_FRAMEBUFFER, frame_dumping_framebuffer.handle);
-        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle);
-        DrawScreens(layout);
+        // delete the draw fence if the frame wasn't presented
+        if (frame->render_fence) {
+            glDeleteSync(frame->render_fence);
+            frame->render_fence = 0;
+        }
 
-        glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[current_pbo].handle);
-        glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0);
-        glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[next_pbo].handle);
+        // wait for the presentation to be done
+        if (frame->present_fence) {
+            glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
+            glDeleteSync(frame->present_fence);
+            frame->present_fence = 0;
+        }
+    }
 
-        GLubyte* pixels = static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
-        VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
-        Core::System::GetInstance().VideoDumper().AddVideoFrame(frame_data);
+    {
+        MICROPROFILE_SCOPE(OpenGL_RenderFrame);
+        // Recreate the frame if the size of the window has changed
+        if (layout.width != frame->width || layout.height != frame->height) {
+            LOG_DEBUG(Render_OpenGL, "Reloading render frame");
+            mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
+        }
 
-        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
-        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
-        current_pbo = (current_pbo + 1) % 2;
-        next_pbo = (current_pbo + 1) % 2;
+        GLuint render_texture = frame->color.handle;
+        state.draw.draw_framebuffer = frame->render.handle;
+        state.Apply();
+        DrawScreens(layout, flipped);
+        // Create a fence for the frontend to wait on and swap this frame to OffTex
+        frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+        glFlush();
+        mailbox->ReleaseRenderFrame(frame);
     }
 }
 
@@ -885,7 +922,7 @@ void RendererOpenGL::DrawSingleScreenStereo(const ScreenInfo& screen_info_l,
 /**
  * Draws the emulated screens to the emulator window.
  */
-void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) {
+void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool flipped) {
     if (VideoCore::g_renderer_bg_color_update_requested.exchange(false)) {
         // Update background color before drawing
         glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue,
@@ -912,7 +949,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) {
 
     // Set projection matrix
     std::array<GLfloat, 3 * 2> ortho_matrix =
-        MakeOrthographicMatrix((float)layout.width, (float)layout.height);
+        MakeOrthographicMatrix((float)layout.width, (float)layout.height, flipped);
     glUniformMatrix3x2fv(uniform_modelview_matrix, 1, GL_FALSE, ortho_matrix.data());
 
     // Bind texture in Texture Unit 0
@@ -1051,41 +1088,11 @@ void RendererOpenGL::TryPresent(int timeout_ms) {
 void RendererOpenGL::UpdateFramerate() {}
 
 void RendererOpenGL::PrepareVideoDumping() {
-    prepare_video_dumping = true;
+    frame_dumper.StartDumping();
 }
 
 void RendererOpenGL::CleanupVideoDumping() {
-    cleanup_video_dumping = true;
-}
-
-void RendererOpenGL::InitVideoDumpingGLObjects() {
-    const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout();
-
-    frame_dumping_framebuffer.Create();
-    glGenRenderbuffers(1, &frame_dumping_renderbuffer);
-    glBindRenderbuffer(GL_RENDERBUFFER, frame_dumping_renderbuffer);
-    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, layout.width, layout.height);
-    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle);
-    glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
-                              frame_dumping_renderbuffer);
-    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
-
-    for (auto& buffer : frame_dumping_pbos) {
-        buffer.Create();
-        glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle);
-        glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr,
-                     GL_STREAM_READ);
-        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
-    }
-}
-
-void RendererOpenGL::ReleaseVideoDumpingGLObjects() {
-    frame_dumping_framebuffer.Release();
-    glDeleteRenderbuffers(1, &frame_dumping_renderbuffer);
-
-    for (auto& buffer : frame_dumping_pbos) {
-        buffer.Release();
-    }
+    frame_dumper.StopDumping();
 }
 
 static const char* GetSource(GLenum source) {
diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h
index 96df7f8ac..634d26ca4 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.h
+++ b/src/video_core/renderer_opengl/renderer_opengl.h
@@ -10,6 +10,7 @@
 #include "common/math_util.h"
 #include "core/hw/gpu.h"
 #include "video_core/renderer_base.h"
+#include "video_core/renderer_opengl/frame_dumper_opengl.h"
 #include "video_core/renderer_opengl/gl_resource_manager.h"
 #include "video_core/renderer_opengl/gl_state.h"
 
@@ -17,6 +18,20 @@ namespace Layout {
 struct FramebufferLayout;
 }
 
+namespace Frontend {
+
+struct Frame {
+    u32 width{};                      /// Width of the frame (to detect resize)
+    u32 height{};                     /// Height of the frame
+    bool color_reloaded = false;      /// Texture attachment was recreated (ie: resized)
+    OpenGL::OGLRenderbuffer color{};  /// Buffer shared between the render/present FBO
+    OpenGL::OGLFramebuffer render{};  /// FBO created on the render thread
+    OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread
+    GLsync render_fence{};            /// Fence created on the render thread
+    GLsync present_fence{};           /// Fence created on the presentation thread
+};
+} // namespace Frontend
+
 namespace OpenGL {
 
 /// Structure used for storing information about the textures for each 3DS screen
@@ -72,10 +87,11 @@ private:
     void ReloadShader();
     void PrepareRendertarget();
     void RenderScreenshot();
-    void RenderVideoDumping();
+    void RenderToMailbox(const Layout::FramebufferLayout& layout,
+                         std::unique_ptr<Frontend::TextureMailbox>& mailbox, bool flipped);
     void ConfigureFramebufferTexture(TextureInfo& texture,
                                      const GPU::Regs::FramebufferConfig& framebuffer);
-    void DrawScreens(const Layout::FramebufferLayout& layout);
+    void DrawScreens(const Layout::FramebufferLayout& layout, bool flipped);
     void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h);
     void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h);
     void DrawSingleScreenStereoRotated(const ScreenInfo& screen_info_l,
@@ -91,9 +107,6 @@ private:
     // Fills active OpenGL texture with the given RGB color.
     void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture);
 
-    void InitVideoDumpingGLObjects();
-    void ReleaseVideoDumpingGLObjects();
-
     OpenGLState state;
 
     // OpenGL object IDs
@@ -120,19 +133,7 @@ private:
     GLuint attrib_position;
     GLuint attrib_tex_coord;
 
-    // Frame dumping
-    OGLFramebuffer frame_dumping_framebuffer;
-    GLuint frame_dumping_renderbuffer;
-
-    // Whether prepare/cleanup video dumping has been requested.
-    // They will be executed on next frame.
-    std::atomic_bool prepare_video_dumping = false;
-    std::atomic_bool cleanup_video_dumping = false;
-
-    // PBOs used to dump frames faster
-    std::array<OGLBuffer, 2> frame_dumping_pbos;
-    GLuint current_pbo = 1;
-    GLuint next_pbo = 0;
+    FrameDumperOpenGL frame_dumper;
 };
 
 } // namespace OpenGL