diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index e836bf3965..66497a386d 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -131,8 +131,6 @@ add_library(core STATIC
     frontend/framebuffer_layout.cpp
     frontend/framebuffer_layout.h
     frontend/input.h
-    frontend/scope_acquire_context.cpp
-    frontend/scope_acquire_context.h
     gdbstub/gdbstub.cpp
     gdbstub/gdbstub.h
     hardware_interrupt_manager.cpp
diff --git a/src/core/core.cpp b/src/core/core.cpp
index d1bc9340de..3bd90d79fd 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -24,7 +24,6 @@
 #include "core/file_sys/sdmc_factory.h"
 #include "core/file_sys/vfs_concat.h"
 #include "core/file_sys/vfs_real.h"
-#include "core/frontend/scope_acquire_context.h"
 #include "core/gdbstub/gdbstub.h"
 #include "core/hardware_interrupt_manager.h"
 #include "core/hle/kernel/client_port.h"
@@ -168,13 +167,12 @@ struct System::Impl {
         Service::Init(service_manager, system);
         GDBStub::DeferStart();
 
-        renderer = VideoCore::CreateRenderer(emu_window, system);
-        if (!renderer->Init()) {
+        interrupt_manager = std::make_unique<Core::Hardware::InterruptManager>(system);
+        gpu_core = VideoCore::CreateGPU(emu_window, system);
+        if (!gpu_core) {
             return ResultStatus::ErrorVideoCore;
         }
-        interrupt_manager = std::make_unique<Core::Hardware::InterruptManager>(system);
-        gpu_core = VideoCore::CreateGPU(system);
-        renderer->Rasterizer().SetupDirtyFlags();
+        gpu_core->Renderer().Rasterizer().SetupDirtyFlags();
 
         is_powered_on = true;
         exit_lock = false;
@@ -186,8 +184,6 @@ struct System::Impl {
 
     ResultStatus Load(System& system, Frontend::EmuWindow& emu_window,
                       const std::string& filepath) {
-        Core::Frontend::ScopeAcquireContext acquire_context{emu_window};
-
         app_loader = Loader::GetLoader(GetGameFileFromPath(virtual_filesystem, filepath));
         if (!app_loader) {
             LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);
@@ -216,10 +212,6 @@ struct System::Impl {
         AddGlueRegistrationForProcess(*app_loader, *main_process);
         kernel.MakeCurrentProcess(main_process.get());
 
-        // Main process has been loaded and been made current.
-        // Begin GPU and CPU execution.
-        gpu_core->Start();
-
         // Initialize cheat engine
         if (cheat_engine) {
             cheat_engine->Initialize();
@@ -277,7 +269,6 @@ struct System::Impl {
         }
 
         // Shutdown emulation session
-        renderer.reset();
         GDBStub::Shutdown();
         Service::Shutdown();
         service_manager.reset();
@@ -353,7 +344,6 @@ struct System::Impl {
     Service::FileSystem::FileSystemController fs_controller;
     /// AppLoader used to load the current executing application
     std::unique_ptr<Loader::AppLoader> app_loader;
-    std::unique_ptr<VideoCore::RendererBase> renderer;
     std::unique_ptr<Tegra::GPU> gpu_core;
     std::unique_ptr<Hardware::InterruptManager> interrupt_manager;
     Memory::Memory memory;
@@ -536,11 +526,11 @@ const Core::Hardware::InterruptManager& System::InterruptManager() const {
 }
 
 VideoCore::RendererBase& System::Renderer() {
-    return *impl->renderer;
+    return impl->gpu_core->Renderer();
 }
 
 const VideoCore::RendererBase& System::Renderer() const {
-    return *impl->renderer;
+    return impl->gpu_core->Renderer();
 }
 
 Kernel::KernelCore& System::Kernel() {
diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h
index 5eb87fb63c..72294d4d85 100644
--- a/src/core/frontend/emu_window.h
+++ b/src/core/frontend/emu_window.h
@@ -13,19 +13,39 @@
 namespace Core::Frontend {
 
 /**
- * Represents a graphics context that can be used for background computation or drawing. If the
- * graphics backend doesn't require the context, then the implementation of these methods can be
- * stubs
+ * Represents a drawing context that supports graphics operations.
  */
 class GraphicsContext {
 public:
     virtual ~GraphicsContext();
 
+    /// Inform the driver to swap the front/back buffers and present the current image
+    virtual void SwapBuffers() {}
+
     /// Makes the graphics context current for the caller thread
-    virtual void MakeCurrent() = 0;
+    virtual void MakeCurrent() {}
 
     /// Releases (dunno if this is the "right" word) the context from the caller thread
-    virtual void DoneCurrent() = 0;
+    virtual void DoneCurrent() {}
+
+    class Scoped {
+    public:
+        explicit Scoped(GraphicsContext& context_) : context(context_) {
+            context.MakeCurrent();
+        }
+        ~Scoped() {
+            context.DoneCurrent();
+        }
+
+    private:
+        GraphicsContext& context;
+    };
+
+    /// Calls MakeCurrent on the context and calls DoneCurrent when the scope for the returned value
+    /// ends
+    Scoped Acquire() {
+        return Scoped{*this};
+    }
 };
 
 /**
@@ -46,7 +66,7 @@ public:
  * - DO NOT TREAT THIS CLASS AS A GUI TOOLKIT ABSTRACTION LAYER. That's not what it is. Please
  *   re-read the upper points again and think about it if you don't see this.
  */
-class EmuWindow : public GraphicsContext {
+class EmuWindow {
 public:
     /// Data structure to store emuwindow configuration
     struct WindowConfig {
@@ -60,17 +80,9 @@ public:
     virtual void PollEvents() = 0;
 
     /**
-     * Returns a GraphicsContext that the frontend provides that is shared with the emu window. This
-     * context can be used from other threads for background graphics computation. If the frontend
-     * is using a graphics backend that doesn't need anything specific to run on a different thread,
-     * then it can use a stubbed implemenation for GraphicsContext.
-     *
-     * If the return value is null, then the core should assume that the frontend cannot provide a
-     * Shared Context
+     * Returns a GraphicsContext that the frontend provides to be used for rendering.
      */
-    virtual std::unique_ptr<GraphicsContext> CreateSharedContext() const {
-        return nullptr;
-    }
+    virtual std::unique_ptr<GraphicsContext> CreateSharedContext() const = 0;
 
     /// Returns if window is shown (not minimized)
     virtual bool IsShown() const = 0;
diff --git a/src/core/frontend/scope_acquire_context.cpp b/src/core/frontend/scope_acquire_context.cpp
deleted file mode 100644
index 878c3157c2..0000000000
--- a/src/core/frontend/scope_acquire_context.cpp
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2019 yuzu Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#include "core/frontend/emu_window.h"
-#include "core/frontend/scope_acquire_context.h"
-
-namespace Core::Frontend {
-
-ScopeAcquireContext::ScopeAcquireContext(Core::Frontend::GraphicsContext& context)
-    : context{context} {
-    context.MakeCurrent();
-}
-ScopeAcquireContext::~ScopeAcquireContext() {
-    context.DoneCurrent();
-}
-
-} // namespace Core::Frontend
diff --git a/src/core/frontend/scope_acquire_context.h b/src/core/frontend/scope_acquire_context.h
deleted file mode 100644
index 7a65c06231..0000000000
--- a/src/core/frontend/scope_acquire_context.h
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2019 yuzu Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#pragma once
-
-#include "common/common_types.h"
-
-namespace Core::Frontend {
-
-class GraphicsContext;
-
-/// Helper class to acquire/release window context within a given scope
-class ScopeAcquireContext : NonCopyable {
-public:
-    explicit ScopeAcquireContext(Core::Frontend::GraphicsContext& context);
-    ~ScopeAcquireContext();
-
-private:
-    Core::Frontend::GraphicsContext& context;
-};
-
-} // namespace Core::Frontend
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index e8f763ce92..8acf2eda25 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -7,6 +7,7 @@
 #include "core/core.h"
 #include "core/core_timing.h"
 #include "core/core_timing_util.h"
+#include "core/frontend/emu_window.h"
 #include "core/memory.h"
 #include "video_core/engines/fermi_2d.h"
 #include "video_core/engines/kepler_compute.h"
@@ -16,14 +17,15 @@
 #include "video_core/gpu.h"
 #include "video_core/memory_manager.h"
 #include "video_core/renderer_base.h"
+#include "video_core/video_core.h"
 
 namespace Tegra {
 
 MICROPROFILE_DEFINE(GPU_wait, "GPU", "Wait for the GPU", MP_RGB(128, 128, 192));
 
-GPU::GPU(Core::System& system, VideoCore::RendererBase& renderer, bool is_async)
-    : system{system}, renderer{renderer}, is_async{is_async} {
-    auto& rasterizer{renderer.Rasterizer()};
+GPU::GPU(Core::System& system, std::unique_ptr<VideoCore::RendererBase>&& renderer_, bool is_async)
+    : system{system}, renderer{std::move(renderer_)}, is_async{is_async} {
+    auto& rasterizer{renderer->Rasterizer()};
     memory_manager = std::make_unique<Tegra::MemoryManager>(system, rasterizer);
     dma_pusher = std::make_unique<Tegra::DmaPusher>(*this);
     maxwell_3d = std::make_unique<Engines::Maxwell3D>(system, rasterizer, *memory_manager);
@@ -137,7 +139,7 @@ u64 GPU::GetTicks() const {
 }
 
 void GPU::FlushCommands() {
-    renderer.Rasterizer().FlushCommands();
+    renderer->Rasterizer().FlushCommands();
 }
 
 // Note that, traditionally, methods are treated as 4-byte addressable locations, and hence
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index 64acb17dfa..ced9d7e286 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.h
@@ -25,8 +25,11 @@ inline u8* FromCacheAddr(CacheAddr cache_addr) {
 }
 
 namespace Core {
-class System;
+namespace Frontend {
+class EmuWindow;
 }
+class System;
+} // namespace Core
 
 namespace VideoCore {
 class RendererBase;
@@ -129,7 +132,8 @@ class MemoryManager;
 
 class GPU {
 public:
-    explicit GPU(Core::System& system, VideoCore::RendererBase& renderer, bool is_async);
+    explicit GPU(Core::System& system, std::unique_ptr<VideoCore::RendererBase>&& renderer,
+                 bool is_async);
 
     virtual ~GPU();
 
@@ -174,6 +178,14 @@ public:
     /// Returns a reference to the GPU DMA pusher.
     Tegra::DmaPusher& DmaPusher();
 
+    VideoCore::RendererBase& Renderer() {
+        return *renderer;
+    }
+
+    const VideoCore::RendererBase& Renderer() const {
+        return *renderer;
+    }
+
     // Waits for the GPU to finish working
     virtual void WaitIdle() const = 0;
 
@@ -287,7 +299,7 @@ private:
 protected:
     std::unique_ptr<Tegra::DmaPusher> dma_pusher;
     Core::System& system;
-    VideoCore::RendererBase& renderer;
+    std::unique_ptr<VideoCore::RendererBase> renderer;
 
 private:
     std::unique_ptr<Tegra::MemoryManager> memory_manager;
diff --git a/src/video_core/gpu_asynch.cpp b/src/video_core/gpu_asynch.cpp
index 04222d0607..925be8d7b0 100644
--- a/src/video_core/gpu_asynch.cpp
+++ b/src/video_core/gpu_asynch.cpp
@@ -10,13 +10,16 @@
 
 namespace VideoCommon {
 
-GPUAsynch::GPUAsynch(Core::System& system, VideoCore::RendererBase& renderer)
-    : GPU(system, renderer, true), gpu_thread{system} {}
+GPUAsynch::GPUAsynch(Core::System& system, std::unique_ptr<VideoCore::RendererBase>&& renderer_,
+                     std::unique_ptr<Core::Frontend::GraphicsContext>&& context)
+    : GPU(system, std::move(renderer_), true), gpu_thread{system}, gpu_context(std::move(context)),
+      cpu_context(renderer->GetRenderWindow().CreateSharedContext()) {}
 
 GPUAsynch::~GPUAsynch() = default;
 
 void GPUAsynch::Start() {
-    gpu_thread.StartThread(renderer, *dma_pusher);
+    cpu_context->MakeCurrent();
+    gpu_thread.StartThread(*renderer, *gpu_context, *dma_pusher);
 }
 
 void GPUAsynch::PushGPUEntries(Tegra::CommandList&& entries) {
diff --git a/src/video_core/gpu_asynch.h b/src/video_core/gpu_asynch.h
index 1241ade1d7..265c62758c 100644
--- a/src/video_core/gpu_asynch.h
+++ b/src/video_core/gpu_asynch.h
@@ -7,6 +7,10 @@
 #include "video_core/gpu.h"
 #include "video_core/gpu_thread.h"
 
+namespace Core::Frontend {
+class GraphicsContext;
+}
+
 namespace VideoCore {
 class RendererBase;
 } // namespace VideoCore
@@ -16,7 +20,8 @@ namespace VideoCommon {
 /// Implementation of GPU interface that runs the GPU asynchronously
 class GPUAsynch final : public Tegra::GPU {
 public:
-    explicit GPUAsynch(Core::System& system, VideoCore::RendererBase& renderer);
+    explicit GPUAsynch(Core::System& system, std::unique_ptr<VideoCore::RendererBase>&& renderer,
+                       std::unique_ptr<Core::Frontend::GraphicsContext>&& context);
     ~GPUAsynch() override;
 
     void Start() override;
@@ -32,6 +37,8 @@ protected:
 
 private:
     GPUThread::ThreadManager gpu_thread;
+    std::unique_ptr<Core::Frontend::GraphicsContext> cpu_context;
+    std::unique_ptr<Core::Frontend::GraphicsContext> gpu_context;
 };
 
 } // namespace VideoCommon
diff --git a/src/video_core/gpu_synch.cpp b/src/video_core/gpu_synch.cpp
index d482210772..bd5278a5c3 100644
--- a/src/video_core/gpu_synch.cpp
+++ b/src/video_core/gpu_synch.cpp
@@ -7,12 +7,15 @@
 
 namespace VideoCommon {
 
-GPUSynch::GPUSynch(Core::System& system, VideoCore::RendererBase& renderer)
-    : GPU(system, renderer, false) {}
+GPUSynch::GPUSynch(Core::System& system, std::unique_ptr<VideoCore::RendererBase>&& renderer,
+                   std::unique_ptr<Core::Frontend::GraphicsContext>&& context)
+    : GPU(system, std::move(renderer), false), context{std::move(context)} {}
 
 GPUSynch::~GPUSynch() = default;
 
-void GPUSynch::Start() {}
+void GPUSynch::Start() {
+    context->MakeCurrent();
+}
 
 void GPUSynch::PushGPUEntries(Tegra::CommandList&& entries) {
     dma_pusher->Push(std::move(entries));
@@ -20,19 +23,19 @@ void GPUSynch::PushGPUEntries(Tegra::CommandList&& entries) {
 }
 
 void GPUSynch::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) {
-    renderer.SwapBuffers(framebuffer);
+    renderer->SwapBuffers(framebuffer);
 }
 
 void GPUSynch::FlushRegion(CacheAddr addr, u64 size) {
-    renderer.Rasterizer().FlushRegion(addr, size);
+    renderer->Rasterizer().FlushRegion(addr, size);
 }
 
 void GPUSynch::InvalidateRegion(CacheAddr addr, u64 size) {
-    renderer.Rasterizer().InvalidateRegion(addr, size);
+    renderer->Rasterizer().InvalidateRegion(addr, size);
 }
 
 void GPUSynch::FlushAndInvalidateRegion(CacheAddr addr, u64 size) {
-    renderer.Rasterizer().FlushAndInvalidateRegion(addr, size);
+    renderer->Rasterizer().FlushAndInvalidateRegion(addr, size);
 }
 
 } // namespace VideoCommon
diff --git a/src/video_core/gpu_synch.h b/src/video_core/gpu_synch.h
index c71baee89e..866a94c8c2 100644
--- a/src/video_core/gpu_synch.h
+++ b/src/video_core/gpu_synch.h
@@ -6,6 +6,10 @@
 
 #include "video_core/gpu.h"
 
+namespace Core::Frontend {
+class GraphicsContext;
+}
+
 namespace VideoCore {
 class RendererBase;
 } // namespace VideoCore
@@ -15,7 +19,8 @@ namespace VideoCommon {
 /// Implementation of GPU interface that runs the GPU synchronously
 class GPUSynch final : public Tegra::GPU {
 public:
-    explicit GPUSynch(Core::System& system, VideoCore::RendererBase& renderer);
+    explicit GPUSynch(Core::System& system, std::unique_ptr<VideoCore::RendererBase>&& renderer,
+                      std::unique_ptr<Core::Frontend::GraphicsContext>&& context);
     ~GPUSynch() override;
 
     void Start() override;
@@ -29,6 +34,9 @@ public:
 protected:
     void TriggerCpuInterrupt([[maybe_unused]] u32 syncpoint_id,
                              [[maybe_unused]] u32 value) const override {}
+
+private:
+    std::unique_ptr<Core::Frontend::GraphicsContext> context;
 };
 
 } // namespace VideoCommon
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index b1088af3d2..270c7ae0d6 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -5,7 +5,7 @@
 #include "common/assert.h"
 #include "common/microprofile.h"
 #include "core/core.h"
-#include "core/frontend/scope_acquire_context.h"
+#include "core/frontend/emu_window.h"
 #include "video_core/dma_pusher.h"
 #include "video_core/gpu.h"
 #include "video_core/gpu_thread.h"
@@ -14,8 +14,8 @@
 namespace VideoCommon::GPUThread {
 
 /// Runs the GPU thread
-static void RunThread(VideoCore::RendererBase& renderer, Tegra::DmaPusher& dma_pusher,
-                      SynchState& state) {
+static void RunThread(VideoCore::RendererBase& renderer, Core::Frontend::GraphicsContext& context,
+                      Tegra::DmaPusher& dma_pusher, SynchState& state) {
     MicroProfileOnThreadCreate("GpuThread");
 
     // Wait for first GPU command before acquiring the window context
@@ -27,7 +27,7 @@ static void RunThread(VideoCore::RendererBase& renderer, Tegra::DmaPusher& dma_p
         return;
     }
 
-    Core::Frontend::ScopeAcquireContext acquire_context{renderer.GetRenderWindow()};
+    auto current_context = context.Acquire();
 
     CommandDataContainer next;
     while (state.is_running) {
@@ -62,8 +62,11 @@ ThreadManager::~ThreadManager() {
     thread.join();
 }
 
-void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Tegra::DmaPusher& dma_pusher) {
-    thread = std::thread{RunThread, std::ref(renderer), std::ref(dma_pusher), std::ref(state)};
+void ThreadManager::StartThread(VideoCore::RendererBase& renderer,
+                                Core::Frontend::GraphicsContext& context,
+                                Tegra::DmaPusher& dma_pusher) {
+    thread = std::thread{RunThread, std::ref(renderer), std::ref(context), std::ref(dma_pusher),
+                         std::ref(state)};
 }
 
 void ThreadManager::SubmitList(Tegra::CommandList&& entries) {
diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h
index 882e2d9c78..be36c580eb 100644
--- a/src/video_core/gpu_thread.h
+++ b/src/video_core/gpu_thread.h
@@ -10,7 +10,6 @@
 #include <optional>
 #include <thread>
 #include <variant>
-
 #include "common/threadsafe_queue.h"
 #include "video_core/gpu.h"
 
@@ -20,6 +19,9 @@ class DmaPusher;
 } // namespace Tegra
 
 namespace Core {
+namespace Frontend {
+class GraphicsContext;
+}
 class System;
 } // namespace Core
 
@@ -99,7 +101,8 @@ public:
     ~ThreadManager();
 
     /// Creates and starts the GPU thread.
-    void StartThread(VideoCore::RendererBase& renderer, Tegra::DmaPusher& dma_pusher);
+    void StartThread(VideoCore::RendererBase& renderer, Core::Frontend::GraphicsContext& context,
+                     Tegra::DmaPusher& dma_pusher);
 
     /// Push GPU command entries to be processed
     void SubmitList(Tegra::CommandList&& entries);
diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h
index 5ec99a1260..1d85219b65 100644
--- a/src/video_core/renderer_base.h
+++ b/src/video_core/renderer_base.h
@@ -46,7 +46,8 @@ public:
 
     /// Draws the latest frame to the window waiting timeout_ms for a frame to arrive (Renderer
     /// specific implementation)
-    virtual void TryPresent(int timeout_ms) = 0;
+    /// Returns true if a frame was drawn
+    virtual bool TryPresent(int timeout_ms) = 0;
 
     // Getter/setter functions:
     // ------------------------
diff --git a/src/video_core/renderer_opengl/gl_shader_cache.cpp b/src/video_core/renderer_opengl/gl_shader_cache.cpp
index e3d31c3eb9..046ee55a59 100644
--- a/src/video_core/renderer_opengl/gl_shader_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_cache.cpp
@@ -327,8 +327,7 @@ void ShaderCacheOpenGL::LoadDiskCache(const std::atomic_bool& stop_loading,
 
     const auto worker = [&](Core::Frontend::GraphicsContext* context, std::size_t begin,
                             std::size_t end) {
-        context->MakeCurrent();
-        SCOPE_EXIT({ return context->DoneCurrent(); });
+        const auto scope = context->Acquire();
 
         for (std::size_t i = begin; i < end; ++i) {
             if (stop_loading) {
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index fca5e3ec06..f1a28cc210 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -30,8 +30,6 @@ namespace OpenGL {
 
 namespace {
 
-// If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have
-// to wait on available presentation frames.
 constexpr std::size_t SWAP_CHAIN_SIZE = 3;
 
 struct Frame {
@@ -214,7 +212,7 @@ public:
     std::deque<Frame*> present_queue;
     Frame* previous_frame{};
 
-    FrameMailbox() : has_debug_tool{HasDebugTool()} {
+    FrameMailbox() {
         for (auto& frame : swap_chain) {
             free_queue.push(&frame);
         }
@@ -285,13 +283,9 @@ 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),
@@ -317,38 +311,12 @@ 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; });
-    }
 };
 
-RendererOpenGL::RendererOpenGL(Core::Frontend::EmuWindow& emu_window, Core::System& system)
+RendererOpenGL::RendererOpenGL(Core::Frontend::EmuWindow& emu_window, Core::System& system,
+                               Core::Frontend::GraphicsContext& context)
     : VideoCore::RendererBase{emu_window}, emu_window{emu_window}, system{system},
-      frame_mailbox{std::make_unique<FrameMailbox>()} {}
+      frame_mailbox{}, context{context}, has_debug_tool{HasDebugTool()} {}
 
 RendererOpenGL::~RendererOpenGL() = default;
 
@@ -356,8 +324,6 @@ MICROPROFILE_DEFINE(OpenGL_RenderFrame, "OpenGL", "Render Frame", MP_RGB(128, 12
 MICROPROFILE_DEFINE(OpenGL_WaitPresent, "OpenGL", "Wait For Present", MP_RGB(128, 128, 128));
 
 void RendererOpenGL::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) {
-    render_window.PollEvents();
-
     if (!framebuffer) {
         return;
     }
@@ -413,6 +379,13 @@ void RendererOpenGL::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) {
         m_current_frame++;
         rasterizer->TickFrame();
     }
+
+    render_window.PollEvents();
+    if (has_debug_tool) {
+        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+        Present(0);
+        context.SwapBuffers();
+    }
 }
 
 void RendererOpenGL::PrepareRendertarget(const Tegra::FramebufferConfig* framebuffer) {
@@ -480,6 +453,8 @@ void RendererOpenGL::LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color
 }
 
 void RendererOpenGL::InitOpenGLObjects() {
+    frame_mailbox = std::make_unique<FrameMailbox>();
+
     glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue,
                  0.0f);
 
@@ -692,12 +667,21 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
     glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
 }
 
-void RendererOpenGL::TryPresent(int timeout_ms) {
+bool RendererOpenGL::TryPresent(int timeout_ms) {
+    if (has_debug_tool) {
+        LOG_DEBUG(Render_OpenGL,
+                  "Skipping presentation because we are presenting on the main context");
+        return false;
+    }
+    return Present(timeout_ms);
+}
+
+bool RendererOpenGL::Present(int timeout_ms) {
     const auto& layout = render_window.GetFramebufferLayout();
     auto frame = frame_mailbox->TryGetPresentFrame(timeout_ms);
     if (!frame) {
         LOG_DEBUG(Render_OpenGL, "TryGetPresentFrame returned no frame to present");
-        return;
+        return false;
     }
 
     // Clearing before a full overwrite of a fbo can signal to drivers that they can avoid a
@@ -725,6 +709,7 @@ void RendererOpenGL::TryPresent(int timeout_ms) {
     glFlush();
 
     glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+    return true;
 }
 
 void RendererOpenGL::RenderScreenshot() {
diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h
index 33073ce5b0..50b6476612 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.h
+++ b/src/video_core/renderer_opengl/renderer_opengl.h
@@ -55,13 +55,14 @@ class FrameMailbox;
 
 class RendererOpenGL final : public VideoCore::RendererBase {
 public:
-    explicit RendererOpenGL(Core::Frontend::EmuWindow& emu_window, Core::System& system);
+    explicit RendererOpenGL(Core::Frontend::EmuWindow& emu_window, Core::System& system,
+                            Core::Frontend::GraphicsContext& context);
     ~RendererOpenGL() override;
 
     bool Init() override;
     void ShutDown() override;
     void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) override;
-    void TryPresent(int timeout_ms) override;
+    bool TryPresent(int timeout_ms) override;
 
 private:
     /// Initializes the OpenGL state and creates persistent objects.
@@ -89,8 +90,11 @@ private:
 
     void PrepareRendertarget(const Tegra::FramebufferConfig* framebuffer);
 
+    bool Present(int timeout_ms);
+
     Core::Frontend::EmuWindow& emu_window;
     Core::System& system;
+    Core::Frontend::GraphicsContext& context;
 
     StateTracker state_tracker{system};
 
@@ -115,6 +119,8 @@ private:
 
     /// Frame presentation mailbox
     std::unique_ptr<FrameMailbox> frame_mailbox;
+
+    bool has_debug_tool = false;
 };
 
 } // namespace OpenGL
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
index 42bb014187..6953aaafe0 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
@@ -141,8 +141,9 @@ void RendererVulkan::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) {
     render_window.PollEvents();
 }
 
-void RendererVulkan::TryPresent(int /*timeout_ms*/) {
+bool RendererVulkan::TryPresent(int /*timeout_ms*/) {
     // TODO (bunnei): ImplementMe
+    return true;
 }
 
 bool RendererVulkan::Init() {
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h
index 3da08d2e43..d14384e792 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.h
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.h
@@ -42,7 +42,7 @@ public:
     bool Init() override;
     void ShutDown() override;
     void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) override;
-    void TryPresent(int timeout_ms) override;
+    bool TryPresent(int timeout_ms) override;
 
 private:
     std::optional<vk::DebugUtilsMessengerEXT> CreateDebugCallback(
diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp
index a5f81a8a0c..f60bdc60a5 100644
--- a/src/video_core/video_core.cpp
+++ b/src/video_core/video_core.cpp
@@ -15,13 +15,13 @@
 #endif
 #include "video_core/video_core.h"
 
-namespace VideoCore {
-
-std::unique_ptr<RendererBase> CreateRenderer(Core::Frontend::EmuWindow& emu_window,
-                                             Core::System& system) {
+namespace {
+std::unique_ptr<VideoCore::RendererBase> CreateRenderer(Core::Frontend::EmuWindow& emu_window,
+                                                        Core::System& system,
+                                                        Core::Frontend::GraphicsContext& context) {
     switch (Settings::values.renderer_backend) {
     case Settings::RendererBackend::OpenGL:
-        return std::make_unique<OpenGL::RendererOpenGL>(emu_window, system);
+        return std::make_unique<OpenGL::RendererOpenGL>(emu_window, system, context);
 #ifdef HAS_VULKAN
     case Settings::RendererBackend::Vulkan:
         return std::make_unique<Vulkan::RendererVulkan>(emu_window, system);
@@ -30,13 +30,23 @@ std::unique_ptr<RendererBase> CreateRenderer(Core::Frontend::EmuWindow& emu_wind
         return nullptr;
     }
 }
+} // Anonymous namespace
 
-std::unique_ptr<Tegra::GPU> CreateGPU(Core::System& system) {
-    if (Settings::values.use_asynchronous_gpu_emulation) {
-        return std::make_unique<VideoCommon::GPUAsynch>(system, system.Renderer());
+namespace VideoCore {
+
+std::unique_ptr<Tegra::GPU> CreateGPU(Core::Frontend::EmuWindow& emu_window, Core::System& system) {
+    auto context = emu_window.CreateSharedContext();
+    const auto scope = context->Acquire();
+    auto renderer = CreateRenderer(emu_window, system, *context);
+    if (!renderer->Init()) {
+        return nullptr;
     }
 
-    return std::make_unique<VideoCommon::GPUSynch>(system, system.Renderer());
+    if (Settings::values.use_asynchronous_gpu_emulation) {
+        return std::make_unique<VideoCommon::GPUAsynch>(system, std::move(renderer),
+                                                        std::move(context));
+    }
+    return std::make_unique<VideoCommon::GPUSynch>(system, std::move(renderer), std::move(context));
 }
 
 u16 GetResolutionScaleFactor(const RendererBase& renderer) {
diff --git a/src/video_core/video_core.h b/src/video_core/video_core.h
index b8e0ac3728..f5c27125d7 100644
--- a/src/video_core/video_core.h
+++ b/src/video_core/video_core.h
@@ -22,17 +22,8 @@ namespace VideoCore {
 
 class RendererBase;
 
-/**
- * Creates a renderer instance.
- *
- * @note The returned renderer instance is simply allocated. Its Init()
- *       function still needs to be called to fully complete its setup.
- */
-std::unique_ptr<RendererBase> CreateRenderer(Core::Frontend::EmuWindow& emu_window,
-                                             Core::System& system);
-
 /// Creates an emulated GPU instance using the given system context.
-std::unique_ptr<Tegra::GPU> CreateGPU(Core::System& system);
+std::unique_ptr<Tegra::GPU> CreateGPU(Core::Frontend::EmuWindow& emu_window, Core::System& system);
 
 u16 GetResolutionScaleFactor(const RendererBase& renderer);
 
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp
index c3dbb1a88a..eaded2640c 100644
--- a/src/yuzu/bootmanager.cpp
+++ b/src/yuzu/bootmanager.cpp
@@ -10,9 +10,6 @@
 #include <QMessageBox>
 #include <QOffscreenSurface>
 #include <QOpenGLContext>
-#include <QOpenGLFunctions>
-#include <QOpenGLFunctions_4_3_Core>
-#include <QOpenGLWindow>
 #include <QPainter>
 #include <QScreen>
 #include <QStringList>
@@ -29,7 +26,6 @@
 #include "common/scope_exit.h"
 #include "core/core.h"
 #include "core/frontend/framebuffer_layout.h"
-#include "core/frontend/scope_acquire_context.h"
 #include "core/settings.h"
 #include "input_common/keyboard.h"
 #include "input_common/main.h"
@@ -39,26 +35,16 @@
 #include "yuzu/bootmanager.h"
 #include "yuzu/main.h"
 
-EmuThread::EmuThread(GRenderWindow& window)
-    : shared_context{window.CreateSharedContext()},
-      context{(Settings::values.use_asynchronous_gpu_emulation && shared_context) ? *shared_context
-                                                                                  : window} {}
+EmuThread::EmuThread() = default;
 
 EmuThread::~EmuThread() = default;
 
-static GMainWindow* GetMainWindow() {
-    for (QWidget* w : qApp->topLevelWidgets()) {
-        if (GMainWindow* main = qobject_cast<GMainWindow*>(w)) {
-            return main;
-        }
-    }
-    return nullptr;
-}
-
 void EmuThread::run() {
     MicroProfileOnThreadCreate("EmuThread");
 
-    Core::Frontend::ScopeAcquireContext acquire_context{context};
+    // Main process has been loaded. Make the context current to this thread and begin GPU and CPU
+    // execution.
+    Core::System::GetInstance().GPU().Start();
 
     emit LoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
 
@@ -111,162 +97,156 @@ void EmuThread::run() {
 #endif
 }
 
-class GGLContext : public Core::Frontend::GraphicsContext {
+class OpenGLSharedContext : public Core::Frontend::GraphicsContext {
 public:
-    explicit GGLContext(QOpenGLContext* shared_context)
-        : context(new QOpenGLContext(shared_context->parent())),
-          surface(new QOffscreenSurface(nullptr)) {
-
-        // disable vsync for any shared contexts
-        auto format = shared_context->format();
+    /// Create the original context that should be shared from
+    explicit OpenGLSharedContext(QSurface* surface) : surface(surface) {
+        QSurfaceFormat format;
+        format.setVersion(4, 3);
+        format.setProfile(QSurfaceFormat::CompatibilityProfile);
+        format.setOption(QSurfaceFormat::FormatOption::DeprecatedFunctions);
+        // TODO: expose a setting for buffer value (ie default/single/double/triple)
+        format.setSwapBehavior(QSurfaceFormat::DefaultSwapBehavior);
         format.setSwapInterval(0);
 
-        context->setShareContext(shared_context);
+        context = std::make_unique<QOpenGLContext>();
         context->setFormat(format);
-        context->create();
-        surface->setParent(shared_context->parent());
-        surface->setFormat(format);
-        surface->create();
+        if (!context->create()) {
+            LOG_ERROR(Frontend, "Unable to create main openGL context");
+        }
+    }
+
+    /// Create the shared contexts for rendering and presentation
+    explicit OpenGLSharedContext(QOpenGLContext* share_context, QSurface* main_surface = nullptr) {
+
+        // disable vsync for any shared contexts
+        auto format = share_context->format();
+        format.setSwapInterval(main_surface ? Settings::values.use_vsync : 0);
+
+        context = std::make_unique<QOpenGLContext>();
+        context->setShareContext(share_context);
+        context->setFormat(format);
+        if (!context->create()) {
+            LOG_ERROR(Frontend, "Unable to create shared openGL context");
+        }
+
+        if (!main_surface) {
+            offscreen_surface = std::make_unique<QOffscreenSurface>(nullptr);
+            offscreen_surface->setFormat(format);
+            offscreen_surface->create();
+            surface = offscreen_surface.get();
+        } else {
+            surface = main_surface;
+        }
+    }
+
+    ~OpenGLSharedContext() {
+        DoneCurrent();
+    }
+
+    void SwapBuffers() override {
+        context->swapBuffers(surface);
     }
 
     void MakeCurrent() override {
-        context->makeCurrent(surface);
+        if (is_current) {
+            return;
+        }
+        is_current = context->makeCurrent(surface);
     }
 
     void DoneCurrent() override {
-        context->doneCurrent();
-    }
-
-private:
-    QOpenGLContext* context;
-    QOffscreenSurface* surface;
-};
-
-class ChildRenderWindow : public QWindow {
-public:
-    ChildRenderWindow(QWindow* parent, QWidget* event_handler)
-        : QWindow{parent}, event_handler{event_handler} {}
-
-    virtual ~ChildRenderWindow() = default;
-
-    virtual void Present() = 0;
-
-protected:
-    bool event(QEvent* event) override {
-        switch (event->type()) {
-        case QEvent::UpdateRequest:
-            Present();
-            return true;
-        case QEvent::MouseButtonPress:
-        case QEvent::MouseButtonRelease:
-        case QEvent::MouseButtonDblClick:
-        case QEvent::MouseMove:
-        case QEvent::KeyPress:
-        case QEvent::KeyRelease:
-        case QEvent::FocusIn:
-        case QEvent::FocusOut:
-        case QEvent::FocusAboutToChange:
-        case QEvent::Enter:
-        case QEvent::Leave:
-        case QEvent::Wheel:
-        case QEvent::TabletMove:
-        case QEvent::TabletPress:
-        case QEvent::TabletRelease:
-        case QEvent::TabletEnterProximity:
-        case QEvent::TabletLeaveProximity:
-        case QEvent::TouchBegin:
-        case QEvent::TouchUpdate:
-        case QEvent::TouchEnd:
-        case QEvent::InputMethodQuery:
-        case QEvent::TouchCancel:
-            return QCoreApplication::sendEvent(event_handler, event);
-        case QEvent::Drop:
-            GetMainWindow()->DropAction(static_cast<QDropEvent*>(event));
-            return true;
-        case QEvent::DragResponse:
-        case QEvent::DragEnter:
-        case QEvent::DragLeave:
-        case QEvent::DragMove:
-            GetMainWindow()->AcceptDropEvent(static_cast<QDropEvent*>(event));
-            return true;
-        default:
-            return QWindow::event(event);
+        if (!is_current) {
+            return;
         }
+        context->doneCurrent();
+        is_current = false;
     }
 
-    void exposeEvent(QExposeEvent* event) override {
-        QWindow::requestUpdate();
-        QWindow::exposeEvent(event);
+    QOpenGLContext* GetShareContext() {
+        return context.get();
+    }
+
+    const QOpenGLContext* GetShareContext() const {
+        return context.get();
     }
 
 private:
-    QWidget* event_handler{};
+    // Avoid using Qt parent system here since we might move the QObjects to new threads
+    // As a note, this means we should avoid using slots/signals with the objects too
+    std::unique_ptr<QOpenGLContext> context;
+    std::unique_ptr<QOffscreenSurface> offscreen_surface{};
+    QSurface* surface;
+    bool is_current = false;
 };
 
-class OpenGLWindow final : public ChildRenderWindow {
+class DummyContext : public Core::Frontend::GraphicsContext {};
+
+class RenderWidget : public QWidget {
 public:
-    OpenGLWindow(QWindow* parent, QWidget* event_handler, QOpenGLContext* shared_context)
-        : ChildRenderWindow{parent, event_handler},
-          context(new QOpenGLContext(shared_context->parent())) {
-
-        // disable vsync for any shared contexts
-        auto format = shared_context->format();
-        format.setSwapInterval(Settings::values.use_vsync ? 1 : 0);
-        this->setFormat(format);
-
-        context->setShareContext(shared_context);
-        context->setScreen(this->screen());
-        context->setFormat(format);
-        context->create();
-
-        setSurfaceType(QWindow::OpenGLSurface);
-
-        // TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground,
-        // WA_DontShowOnScreen, WA_DeleteOnClose
+    explicit RenderWidget(GRenderWindow* parent) : QWidget(parent), render_window(parent) {
+        setAttribute(Qt::WA_NativeWindow);
+        setAttribute(Qt::WA_PaintOnScreen);
     }
 
-    ~OpenGLWindow() override {
-        context->doneCurrent();
+    virtual ~RenderWidget() = default;
+
+    /// Called on the UI thread when this Widget is ready to draw
+    /// Dervied classes can override this to draw the latest frame.
+    virtual void Present() {}
+
+    void paintEvent(QPaintEvent* event) override {
+        Present();
+        update();
+    }
+
+    QPaintEngine* paintEngine() const override {
+        return nullptr;
+    }
+
+private:
+    GRenderWindow* render_window;
+};
+
+class OpenGLRenderWidget : public RenderWidget {
+public:
+    explicit OpenGLRenderWidget(GRenderWindow* parent) : RenderWidget(parent) {
+        windowHandle()->setSurfaceType(QWindow::OpenGLSurface);
+    }
+
+    void SetContext(std::unique_ptr<Core::Frontend::GraphicsContext>&& context_) {
+        context = std::move(context_);
     }
 
     void Present() override {
-        if (!isExposed()) {
+        if (!isVisible()) {
             return;
         }
 
-        context->makeCurrent(this);
-        Core::System::GetInstance().Renderer().TryPresent(100);
-        context->swapBuffers(this);
-        auto f = context->versionFunctions<QOpenGLFunctions_4_3_Core>();
-        f->glFinish();
-        QWindow::requestUpdate();
+        context->MakeCurrent();
+        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+        if (Core::System::GetInstance().Renderer().TryPresent(100)) {
+            context->SwapBuffers();
+            glFinish();
+        }
     }
 
 private:
-    QOpenGLContext* context{};
+    std::unique_ptr<Core::Frontend::GraphicsContext> context{};
 };
 
 #ifdef HAS_VULKAN
-class VulkanWindow final : public ChildRenderWindow {
+class VulkanRenderWidget : public RenderWidget {
 public:
-    VulkanWindow(QWindow* parent, QWidget* event_handler, QVulkanInstance* instance)
-        : ChildRenderWindow{parent, event_handler} {
-        setSurfaceType(QSurface::SurfaceType::VulkanSurface);
-        setVulkanInstance(instance);
+    explicit VulkanRenderWidget(GRenderWindow* parent, QVulkanInstance* instance)
+        : RenderWidget(parent) {
+        windowHandle()->setSurfaceType(QWindow::VulkanSurface);
+        windowHandle()->setVulkanInstance(instance);
     }
-
-    ~VulkanWindow() override = default;
-
-    void Present() override {
-        // TODO(bunnei): ImplementMe
-    }
-
-private:
-    QWidget* event_handler{};
 };
 #endif
 
-GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread)
+GRenderWindow::GRenderWindow(GMainWindow* parent_, EmuThread* emu_thread)
     : QWidget(parent_), emu_thread(emu_thread) {
     setWindowTitle(QStringLiteral("yuzu %1 | %2-%3")
                        .arg(QString::fromUtf8(Common::g_build_name),
@@ -278,26 +258,13 @@ GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread)
     setLayout(layout);
     InputCommon::Init();
 
-    GMainWindow* parent = GetMainWindow();
-    connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete);
+    connect(this, &GRenderWindow::FirstFrameDisplayed, parent_, &GMainWindow::OnLoadComplete);
 }
 
 GRenderWindow::~GRenderWindow() {
     InputCommon::Shutdown();
 }
 
-void GRenderWindow::MakeCurrent() {
-    if (core_context) {
-        core_context->MakeCurrent();
-    }
-}
-
-void GRenderWindow::DoneCurrent() {
-    if (core_context) {
-        core_context->DoneCurrent();
-    }
-}
-
 void GRenderWindow::PollEvents() {
     if (!first_frame) {
         first_frame = true;
@@ -309,21 +276,6 @@ bool GRenderWindow::IsShown() const {
     return !isMinimized();
 }
 
-void GRenderWindow::RetrieveVulkanHandlers(void* get_instance_proc_addr, void* instance,
-                                           void* surface) const {
-#ifdef HAS_VULKAN
-    const auto instance_proc_addr = vk_instance->getInstanceProcAddr("vkGetInstanceProcAddr");
-    const VkInstance instance_copy = vk_instance->vkInstance();
-    const VkSurfaceKHR surface_copy = vk_instance->surfaceForWindow(child_window);
-
-    std::memcpy(get_instance_proc_addr, &instance_proc_addr, sizeof(instance_proc_addr));
-    std::memcpy(instance, &instance_copy, sizeof(instance_copy));
-    std::memcpy(surface, &surface_copy, sizeof(surface_copy));
-#else
-    UNREACHABLE_MSG("Executing Vulkan code without compiling Vulkan");
-#endif
-}
-
 // On Qt 5.0+, this correctly gets the size of the framebuffer (pixels).
 //
 // Older versions get the window size (density independent pixels),
@@ -367,7 +319,7 @@ qreal GRenderWindow::windowPixelRatio() const {
     return devicePixelRatio();
 }
 
-std::pair<u32, u32> GRenderWindow::ScaleTouch(const QPointF pos) const {
+std::pair<u32, u32> GRenderWindow::ScaleTouch(const QPointF& pos) const {
     const qreal pixel_ratio = windowPixelRatio();
     return {static_cast<u32>(std::max(std::round(pos.x() * pixel_ratio), qreal{0.0})),
             static_cast<u32>(std::max(std::round(pos.y() * pixel_ratio), qreal{0.0}))};
@@ -387,8 +339,10 @@ void GRenderWindow::keyReleaseEvent(QKeyEvent* event) {
 }
 
 void GRenderWindow::mousePressEvent(QMouseEvent* event) {
-    if (event->source() == Qt::MouseEventSynthesizedBySystem)
-        return; // touch input is handled in TouchBeginEvent
+    // touch input is handled in TouchBeginEvent
+    if (event->source() == Qt::MouseEventSynthesizedBySystem) {
+        return;
+    }
 
     auto pos = event->pos();
     if (event->button() == Qt::LeftButton) {
@@ -400,8 +354,10 @@ void GRenderWindow::mousePressEvent(QMouseEvent* event) {
 }
 
 void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
-    if (event->source() == Qt::MouseEventSynthesizedBySystem)
-        return; // touch input is handled in TouchUpdateEvent
+    // touch input is handled in TouchUpdateEvent
+    if (event->source() == Qt::MouseEventSynthesizedBySystem) {
+        return;
+    }
 
     auto pos = event->pos();
     const auto [x, y] = ScaleTouch(pos);
@@ -410,13 +366,16 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
 }
 
 void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
-    if (event->source() == Qt::MouseEventSynthesizedBySystem)
-        return; // touch input is handled in TouchEndEvent
+    // touch input is handled in TouchEndEvent
+    if (event->source() == Qt::MouseEventSynthesizedBySystem) {
+        return;
+    }
 
-    if (event->button() == Qt::LeftButton)
+    if (event->button() == Qt::LeftButton) {
         this->TouchReleased();
-    else if (event->button() == Qt::RightButton)
+    } else if (event->button() == Qt::RightButton) {
         InputCommon::GetMotionEmu()->EndTilt();
+    }
 }
 
 void GRenderWindow::TouchBeginEvent(const QTouchEvent* event) {
@@ -474,9 +433,13 @@ void GRenderWindow::resizeEvent(QResizeEvent* event) {
 
 std::unique_ptr<Core::Frontend::GraphicsContext> GRenderWindow::CreateSharedContext() const {
     if (Settings::values.renderer_backend == Settings::RendererBackend::OpenGL) {
-        return std::make_unique<GGLContext>(QOpenGLContext::globalShareContext());
+        auto c = static_cast<OpenGLSharedContext*>(main_context.get());
+        // Bind the shared contexts to the main surface in case the backend wants to take over
+        // presentation
+        return std::make_unique<OpenGLSharedContext>(c->GetShareContext(),
+                                                     child_widget->windowHandle());
     }
-    return {};
+    return std::make_unique<DummyContext>();
 }
 
 bool GRenderWindow::InitRenderTarget() {
@@ -497,14 +460,11 @@ bool GRenderWindow::InitRenderTarget() {
         break;
     }
 
+    child_widget->resize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height);
+    layout()->addWidget(child_widget);
     // Reset minimum required size to avoid resizing issues on the main window after restarting.
     setMinimumSize(1, 1);
 
-    // Show causes the window to actually be created and the gl context as well, but we don't want
-    // the widget to be shown yet, so immediately hide it.
-    show();
-    hide();
-
     resize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height);
 
     OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size);
@@ -523,9 +483,10 @@ bool GRenderWindow::InitRenderTarget() {
 void GRenderWindow::ReleaseRenderTarget() {
     if (child_widget) {
         layout()->removeWidget(child_widget);
-        delete child_widget;
+        child_widget->deleteLater();
         child_widget = nullptr;
     }
+    main_context.reset();
 }
 
 void GRenderWindow::CaptureScreenshot(u32 res_scale, const QString& screenshot_path) {
@@ -557,24 +518,13 @@ void GRenderWindow::OnMinimalClientAreaChangeRequest(std::pair<u32, u32> minimal
 bool GRenderWindow::InitializeOpenGL() {
     // TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground,
     // WA_DontShowOnScreen, WA_DeleteOnClose
-    QSurfaceFormat fmt;
-    fmt.setVersion(4, 3);
-    fmt.setProfile(QSurfaceFormat::CompatibilityProfile);
-    fmt.setOption(QSurfaceFormat::FormatOption::DeprecatedFunctions);
-    // TODO: expose a setting for buffer value (ie default/single/double/triple)
-    fmt.setSwapBehavior(QSurfaceFormat::DefaultSwapBehavior);
-    fmt.setSwapInterval(0);
-    QSurfaceFormat::setDefaultFormat(fmt);
-
-    GMainWindow* parent = GetMainWindow();
-    QWindow* parent_win_handle = parent ? parent->windowHandle() : nullptr;
-    child_window = new OpenGLWindow(parent_win_handle, this, QOpenGLContext::globalShareContext());
-    child_window->create();
-    child_widget = createWindowContainer(child_window, this);
-    child_widget->resize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height);
-    layout()->addWidget(child_widget);
-
-    core_context = CreateSharedContext();
+    auto child = new OpenGLRenderWidget(this);
+    child_widget = child;
+    child_widget->windowHandle()->create();
+    auto context = std::make_shared<OpenGLSharedContext>(child->windowHandle());
+    main_context = context;
+    child->SetContext(
+        std::make_unique<OpenGLSharedContext>(context->GetShareContext(), child->windowHandle()));
 
     return true;
 }
@@ -604,13 +554,10 @@ bool GRenderWindow::InitializeVulkan() {
         return false;
     }
 
-    GMainWindow* parent = GetMainWindow();
-    QWindow* parent_win_handle = parent ? parent->windowHandle() : nullptr;
-    child_window = new VulkanWindow(parent_win_handle, this, vk_instance.get());
-    child_window->create();
-    child_widget = createWindowContainer(child_window, this);
-    child_widget->resize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height);
-    layout()->addWidget(child_widget);
+    auto child = new VulkanRenderWidget(this, vk_instance.get());
+    child_widget = child;
+    child_widget->windowHandle()->create();
+    main_context = std::make_unique<DummyContext>();
 
     return true;
 #else
@@ -620,8 +567,24 @@ bool GRenderWindow::InitializeVulkan() {
 #endif
 }
 
+void GRenderWindow::RetrieveVulkanHandlers(void* get_instance_proc_addr, void* instance,
+                                           void* surface) const {
+#ifdef HAS_VULKAN
+    const auto instance_proc_addr = vk_instance->getInstanceProcAddr("vkGetInstanceProcAddr");
+    const VkInstance instance_copy = vk_instance->vkInstance();
+    const VkSurfaceKHR surface_copy = vk_instance->surfaceForWindow(child_widget->windowHandle());
+
+    std::memcpy(get_instance_proc_addr, &instance_proc_addr, sizeof(instance_proc_addr));
+    std::memcpy(instance, &instance_copy, sizeof(instance_copy));
+    std::memcpy(surface, &surface_copy, sizeof(surface_copy));
+#else
+    UNREACHABLE_MSG("Executing Vulkan code without compiling Vulkan");
+#endif
+}
+
 bool GRenderWindow::LoadOpenGL() {
-    Core::Frontend::ScopeAcquireContext acquire_context{*this};
+    auto context = CreateSharedContext();
+    auto scope = context->Acquire();
     if (!gladLoadGL()) {
         QMessageBox::critical(this, tr("Error while initializing OpenGL 4.3!"),
                               tr("Your GPU may not support OpenGL 4.3, or you do not have the "
diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h
index 79b0303044..d69078df12 100644
--- a/src/yuzu/bootmanager.h
+++ b/src/yuzu/bootmanager.h
@@ -18,12 +18,10 @@
 #include "core/frontend/emu_window.h"
 
 class GRenderWindow;
+class GMainWindow;
 class QKeyEvent;
-class QScreen;
 class QTouchEvent;
 class QStringList;
-class QSurface;
-class QOpenGLContext;
 #ifdef HAS_VULKAN
 class QVulkanInstance;
 #endif
@@ -36,7 +34,7 @@ class EmuThread final : public QThread {
     Q_OBJECT
 
 public:
-    explicit EmuThread(GRenderWindow& window);
+    explicit EmuThread();
     ~EmuThread() override;
 
     /**
@@ -90,12 +88,6 @@ private:
     std::mutex running_mutex;
     std::condition_variable running_cv;
 
-    /// Only used in asynchronous GPU mode
-    std::unique_ptr<Core::Frontend::GraphicsContext> shared_context;
-
-    /// This is shared_context in asynchronous GPU mode, core_context in synchronous GPU mode
-    Core::Frontend::GraphicsContext& context;
-
 signals:
     /**
      * Emitted when the CPU has halted execution
@@ -124,12 +116,10 @@ class GRenderWindow : public QWidget, public Core::Frontend::EmuWindow {
     Q_OBJECT
 
 public:
-    GRenderWindow(QWidget* parent, EmuThread* emu_thread);
+    GRenderWindow(GMainWindow* parent, EmuThread* emu_thread);
     ~GRenderWindow() override;
 
     // EmuWindow implementation.
-    void MakeCurrent() override;
-    void DoneCurrent() override;
     void PollEvents() override;
     bool IsShown() const override;
     void RetrieveVulkanHandlers(void* get_instance_proc_addr, void* instance,
@@ -165,6 +155,8 @@ public:
 
     void CaptureScreenshot(u32 res_scale, const QString& screenshot_path);
 
+    std::pair<u32, u32> ScaleTouch(const QPointF& pos) const;
+
 public slots:
     void OnEmulationStarting(EmuThread* emu_thread);
     void OnEmulationStopping();
@@ -176,7 +168,6 @@ signals:
     void FirstFrameDisplayed();
 
 private:
-    std::pair<u32, u32> ScaleTouch(QPointF pos) const;
     void TouchBeginEvent(const QTouchEvent* event);
     void TouchUpdateEvent(const QTouchEvent* event);
     void TouchEndEvent();
@@ -190,7 +181,10 @@ private:
 
     EmuThread* emu_thread;
 
-    std::unique_ptr<GraphicsContext> core_context;
+    // Main context that will be shared with all other contexts that are requested.
+    // If this is used in a shared context setting, then this should not be used directly, but
+    // should instead be shared from
+    std::shared_ptr<Core::Frontend::GraphicsContext> main_context;
 
 #ifdef HAS_VULKAN
     std::unique_ptr<QVulkanInstance> vk_instance;
@@ -201,12 +195,6 @@ private:
 
     QByteArray geometry;
 
-    /// Native window handle that backs this presentation widget
-    QWindow* child_window = nullptr;
-
-    /// In order to embed the window into GRenderWindow, you need to use createWindowContainer to
-    /// put the child_window into a widget then add it to the layout. This child_widget can be
-    /// parented to GRenderWindow and use Qt's lifetime system
     QWidget* child_widget = nullptr;
 
     bool first_frame = false;
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index d7e59d0cd1..940f24dc80 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -984,7 +984,7 @@ void GMainWindow::BootGame(const QString& filename) {
         return;
 
     // Create and start the emulation thread
-    emu_thread = std::make_unique<EmuThread>(*render_window);
+    emu_thread = std::make_unique<EmuThread>();
     emit EmulationStarting(emu_thread.get());
     emu_thread->start();
 
@@ -2378,7 +2378,6 @@ int main(int argc, char* argv[]) {
 
     // Enables the core to make the qt created contexts current on std::threads
     QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
-    QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
     QApplication app(argc, argv);
 
     // Qt changes the locale and causes issues in float conversion using std::to_string() when
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp
index c0d373477c..3522dcf6d9 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp
@@ -37,16 +37,24 @@ public:
     }
 
     void MakeCurrent() override {
-        SDL_GL_MakeCurrent(window, context);
+        if (is_current) {
+            return;
+        }
+        is_current = SDL_GL_MakeCurrent(window, context) == 0;
     }
 
     void DoneCurrent() override {
+        if (!is_current) {
+            return;
+        }
         SDL_GL_MakeCurrent(window, nullptr);
+        is_current = false;
     }
 
 private:
     SDL_Window* window;
     SDL_GLContext context;
+    bool is_current = false;
 };
 
 bool EmuWindow_SDL2_GL::SupportsRequiredGLExtensions() {
@@ -148,14 +156,6 @@ EmuWindow_SDL2_GL::~EmuWindow_SDL2_GL() {
     SDL_GL_DeleteContext(window_context);
 }
 
-void EmuWindow_SDL2_GL::MakeCurrent() {
-    core_context->MakeCurrent();
-}
-
-void EmuWindow_SDL2_GL::DoneCurrent() {
-    core_context->DoneCurrent();
-}
-
 void EmuWindow_SDL2_GL::RetrieveVulkanHandlers(void* get_instance_proc_addr, void* instance,
                                                void* surface) const {
     // Should not have been called from OpenGL
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h
index b80669ff05..e092021d79 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h
@@ -13,8 +13,6 @@ public:
     explicit EmuWindow_SDL2_GL(Core::System& system, bool fullscreen);
     ~EmuWindow_SDL2_GL();
 
-    void MakeCurrent() override;
-    void DoneCurrent() override;
     void Present() override;
 
     /// Ignored in OpenGL
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp
index abcc581651..46d053f047 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp
@@ -111,14 +111,6 @@ EmuWindow_SDL2_VK::~EmuWindow_SDL2_VK() {
     vkDestroyInstance(vk_instance, nullptr);
 }
 
-void EmuWindow_SDL2_VK::MakeCurrent() {
-    // Unused on Vulkan
-}
-
-void EmuWindow_SDL2_VK::DoneCurrent() {
-    // Unused on Vulkan
-}
-
 void EmuWindow_SDL2_VK::RetrieveVulkanHandlers(void* get_instance_proc_addr, void* instance,
                                                void* surface) const {
     const auto instance_proc_addr = vkGetInstanceProcAddr;
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h
index 1eb8c0868e..3dd1f3f619 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h
@@ -13,8 +13,6 @@ public:
     explicit EmuWindow_SDL2_VK(Core::System& system, bool fullscreen);
     ~EmuWindow_SDL2_VK();
 
-    void MakeCurrent() override;
-    void DoneCurrent() override;
     void Present() override;
     void RetrieveVulkanHandlers(void* get_instance_proc_addr, void* instance,
                                 void* surface) const override;
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index babf4c3a49..4d2ea7e9e9 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -230,17 +230,10 @@ int main(int argc, char** argv) {
 
     system.TelemetrySession().AddField(Telemetry::FieldType::App, "Frontend", "SDL");
 
-    system.Renderer().Rasterizer().LoadDiskResources();
+    // Core is loaded, start the GPU (makes the GPU contexts current to this thread)
+    system.GPU().Start();
 
-    // Acquire render context for duration of the thread if this is the rendering thread
-    if (!Settings::values.use_asynchronous_gpu_emulation) {
-        emu_window->MakeCurrent();
-    }
-    SCOPE_EXIT({
-        if (!Settings::values.use_asynchronous_gpu_emulation) {
-            emu_window->DoneCurrent();
-        }
-    });
+    system.Renderer().Rasterizer().LoadDiskResources();
 
     std::thread render_thread([&emu_window] { emu_window->Present(); });
     while (emu_window->IsOpen()) {
diff --git a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp
index a1bdb1a12b..a837430cc3 100644
--- a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp
+++ b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp
@@ -102,8 +102,6 @@ EmuWindow_SDL2_Hide::EmuWindow_SDL2_Hide() {
     LOG_INFO(Frontend, "yuzu-tester Version: {} | {}-{}", Common::g_build_fullname,
              Common::g_scm_branch, Common::g_scm_desc);
     Settings::LogSettings();
-
-    DoneCurrent();
 }
 
 EmuWindow_SDL2_Hide::~EmuWindow_SDL2_Hide() {
@@ -114,14 +112,6 @@ EmuWindow_SDL2_Hide::~EmuWindow_SDL2_Hide() {
 
 void EmuWindow_SDL2_Hide::PollEvents() {}
 
-void EmuWindow_SDL2_Hide::MakeCurrent() {
-    SDL_GL_MakeCurrent(render_window, gl_context);
-}
-
-void EmuWindow_SDL2_Hide::DoneCurrent() {
-    SDL_GL_MakeCurrent(render_window, nullptr);
-}
-
 bool EmuWindow_SDL2_Hide::IsShown() const {
     return false;
 }
@@ -129,3 +119,35 @@ bool EmuWindow_SDL2_Hide::IsShown() const {
 void EmuWindow_SDL2_Hide::RetrieveVulkanHandlers(void*, void*, void*) const {
     UNREACHABLE();
 }
+
+class SDLGLContext : public Core::Frontend::GraphicsContext {
+public:
+    explicit SDLGLContext() {
+        // create a hidden window to make the shared context against
+        window = SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0,
+                                  SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL);
+        context = SDL_GL_CreateContext(window);
+    }
+
+    ~SDLGLContext() {
+        DoneCurrent();
+        SDL_GL_DeleteContext(context);
+        SDL_DestroyWindow(window);
+    }
+
+    void MakeCurrent() override {
+        SDL_GL_MakeCurrent(window, context);
+    }
+
+    void DoneCurrent() override {
+        SDL_GL_MakeCurrent(window, nullptr);
+    }
+
+private:
+    SDL_Window* window;
+    SDL_GLContext context;
+};
+
+std::unique_ptr<Core::Frontend::GraphicsContext> EmuWindow_SDL2_Hide::CreateSharedContext() const {
+    return std::make_unique<SDLGLContext>();
+}
diff --git a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h
index b13e153099..9f5d04fca7 100644
--- a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h
+++ b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h
@@ -16,12 +16,6 @@ public:
     /// Polls window events
     void PollEvents() override;
 
-    /// Makes the graphics context current for the caller thread
-    void MakeCurrent() override;
-
-    /// Releases the GL context from the caller thread
-    void DoneCurrent() override;
-
     /// Whether the screen is being shown or not.
     bool IsShown() const override;
 
@@ -29,8 +23,7 @@ public:
     void RetrieveVulkanHandlers(void* get_instance_proc_addr, void* instance,
                                 void* surface) const override;
 
-    /// Whether the window is still open, and a close request hasn't yet been sent
-    bool IsOpen() const;
+    std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override;
 
 private:
     /// Whether the GPU and driver supports the OpenGL extension required
diff --git a/src/yuzu_tester/yuzu.cpp b/src/yuzu_tester/yuzu.cpp
index 94ad50cb33..676e70ebd7 100644
--- a/src/yuzu_tester/yuzu.cpp
+++ b/src/yuzu_tester/yuzu.cpp
@@ -164,11 +164,6 @@ int main(int argc, char** argv) {
 
     std::unique_ptr<EmuWindow_SDL2_Hide> emu_window{std::make_unique<EmuWindow_SDL2_Hide>()};
 
-    if (!Settings::values.use_multi_core) {
-        // Single core mode must acquire OpenGL context for entire emulation session
-        emu_window->MakeCurrent();
-    }
-
     bool finished = false;
     int return_value = 0;
     const auto callback = [&finished,
@@ -257,6 +252,7 @@ int main(int argc, char** argv) {
 
     system.TelemetrySession().AddField(Telemetry::FieldType::App, "Frontend", "SDLHideTester");
 
+    system.GPU().Start();
     system.Renderer().Rasterizer().LoadDiskResources();
 
     while (!finished) {