diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
index 02a9f5ecb..d2e9f4031 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
@@ -109,8 +109,8 @@ void VKBufferCache::Reserve(std::size_t max_size) {
     }
 }
 
-VKExecutionContext VKBufferCache::Send(VKExecutionContext exctx) {
-    return stream_buffer->Send(exctx, buffer_offset - buffer_offset_base);
+void VKBufferCache::Send() {
+    stream_buffer->Send(buffer_offset - buffer_offset_base);
 }
 
 void VKBufferCache::AlignBuffer(std::size_t alignment) {
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index 3edf460df..49f13bcdc 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -77,7 +77,7 @@ public:
     void Reserve(std::size_t max_size);
 
     /// Ensures that the set data is sent to the device.
-    [[nodiscard]] VKExecutionContext Send(VKExecutionContext exctx);
+    void Send();
 
     /// Returns the buffer cache handle.
     vk::Buffer GetBuffer() const {
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp
index f1fea1871..0f8116458 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.cpp
+++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp
@@ -19,23 +19,19 @@ VKScheduler::VKScheduler(const VKDevice& device, VKResourceManager& resource_man
 
 VKScheduler::~VKScheduler() = default;
 
-VKExecutionContext VKScheduler::GetExecutionContext() const {
-    return VKExecutionContext(current_fence, current_cmdbuf);
-}
-
-VKExecutionContext VKScheduler::Flush(vk::Semaphore semaphore) {
+void VKScheduler::Flush(bool release_fence, vk::Semaphore semaphore) {
     SubmitExecution(semaphore);
-    current_fence->Release();
+    if (release_fence)
+        current_fence->Release();
     AllocateNewContext();
-    return GetExecutionContext();
 }
 
-VKExecutionContext VKScheduler::Finish(vk::Semaphore semaphore) {
+void VKScheduler::Finish(bool release_fence, vk::Semaphore semaphore) {
     SubmitExecution(semaphore);
     current_fence->Wait();
-    current_fence->Release();
+    if (release_fence)
+        current_fence->Release();
     AllocateNewContext();
-    return GetExecutionContext();
 }
 
 void VKScheduler::SubmitExecution(vk::Semaphore semaphore) {
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h
index cfaf5376f..0e5b49c7f 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.h
+++ b/src/video_core/renderer_vulkan/vk_scheduler.h
@@ -10,10 +10,43 @@
 namespace Vulkan {
 
 class VKDevice;
-class VKExecutionContext;
 class VKFence;
 class VKResourceManager;
 
+class VKFenceView {
+public:
+    VKFenceView() = default;
+    VKFenceView(VKFence* const& fence) : fence{fence} {}
+
+    VKFence* operator->() const noexcept {
+        return fence;
+    }
+
+    operator VKFence&() const noexcept {
+        return *fence;
+    }
+
+private:
+    VKFence* const& fence;
+};
+
+class VKCommandBufferView {
+public:
+    VKCommandBufferView() = default;
+    VKCommandBufferView(const vk::CommandBuffer& cmdbuf) : cmdbuf{cmdbuf} {}
+
+    const vk::CommandBuffer* operator->() const noexcept {
+        return &cmdbuf;
+    }
+
+    operator vk::CommandBuffer() const noexcept {
+        return cmdbuf;
+    }
+
+private:
+    const vk::CommandBuffer& cmdbuf;
+};
+
 /// The scheduler abstracts command buffer and fence management with an interface that's able to do
 /// OpenGL-like operations on Vulkan command buffers.
 class VKScheduler {
@@ -21,16 +54,21 @@ public:
     explicit VKScheduler(const VKDevice& device, VKResourceManager& resource_manager);
     ~VKScheduler();
 
-    /// Gets the current execution context.
-    [[nodiscard]] VKExecutionContext GetExecutionContext() const;
+    /// Gets a reference to the current fence.
+    VKFenceView GetFence() const {
+        return current_fence;
+    }
 
-    /// Sends the current execution context to the GPU. It invalidates the current execution context
-    /// and returns a new one.
-    VKExecutionContext Flush(vk::Semaphore semaphore = nullptr);
+    /// Gets a reference to the current command buffer.
+    VKCommandBufferView GetCommandBuffer() const {
+        return current_cmdbuf;
+    }
 
-    /// Sends the current execution context to the GPU and waits for it to complete. It invalidates
-    /// the current execution context and returns a new one.
-    VKExecutionContext Finish(vk::Semaphore semaphore = nullptr);
+    /// Sends the current execution context to the GPU.
+    void Flush(bool release_fence = true, vk::Semaphore semaphore = nullptr);
+
+    /// Sends the current execution context to the GPU and waits for it to complete.
+    void Finish(bool release_fence = true, vk::Semaphore semaphore = nullptr);
 
 private:
     void SubmitExecution(vk::Semaphore semaphore);
@@ -44,26 +82,4 @@ private:
     VKFence* next_fence = nullptr;
 };
 
-class VKExecutionContext {
-    friend class VKScheduler;
-
-public:
-    VKExecutionContext() = default;
-
-    VKFence& GetFence() const {
-        return *fence;
-    }
-
-    vk::CommandBuffer GetCommandBuffer() const {
-        return cmdbuf;
-    }
-
-private:
-    explicit VKExecutionContext(VKFence* fence, vk::CommandBuffer cmdbuf)
-        : fence{fence}, cmdbuf{cmdbuf} {}
-
-    VKFence* fence{};
-    vk::CommandBuffer cmdbuf;
-};
-
 } // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_stream_buffer.cpp b/src/video_core/renderer_vulkan/vk_stream_buffer.cpp
index 58ffa42f2..62f1427f5 100644
--- a/src/video_core/renderer_vulkan/vk_stream_buffer.cpp
+++ b/src/video_core/renderer_vulkan/vk_stream_buffer.cpp
@@ -46,12 +46,12 @@ std::tuple<u8*, u64, bool> VKStreamBuffer::Reserve(u64 size) {
     return {mapped_pointer + offset, offset, invalidation_mark.has_value()};
 }
 
-VKExecutionContext VKStreamBuffer::Send(VKExecutionContext exctx, u64 size) {
+void VKStreamBuffer::Send(u64 size) {
     ASSERT_MSG(size <= mapped_size, "Reserved size is too small");
 
     if (invalidation_mark) {
         // TODO(Rodrigo): Find a better way to invalidate than waiting for all watches to finish.
-        exctx = scheduler.Flush();
+        scheduler.Flush();
         std::for_each(watches.begin(), watches.begin() + *invalidation_mark,
                       [&](auto& resource) { resource->Wait(); });
         invalidation_mark = std::nullopt;
@@ -62,11 +62,9 @@ VKExecutionContext VKStreamBuffer::Send(VKExecutionContext exctx, u64 size) {
         ReserveWatches(WATCHES_RESERVE_CHUNK);
     }
     // Add a watch for this allocation.
-    watches[used_watches++]->Watch(exctx.GetFence());
+    watches[used_watches++]->Watch(scheduler.GetFence());
 
     offset += size;
-
-    return exctx;
 }
 
 void VKStreamBuffer::CreateBuffers(VKMemoryManager& memory_manager, vk::BufferUsageFlags usage) {
diff --git a/src/video_core/renderer_vulkan/vk_stream_buffer.h b/src/video_core/renderer_vulkan/vk_stream_buffer.h
index 69d036ccd..842e54162 100644
--- a/src/video_core/renderer_vulkan/vk_stream_buffer.h
+++ b/src/video_core/renderer_vulkan/vk_stream_buffer.h
@@ -37,7 +37,7 @@ public:
     std::tuple<u8*, u64, bool> Reserve(u64 size);
 
     /// Ensures that "size" bytes of memory are available to the GPU, potentially recording a copy.
-    [[nodiscard]] VKExecutionContext Send(VKExecutionContext exctx, u64 size);
+    void Send(u64 size);
 
     vk::Buffer GetBuffer() const {
         return *buffer;