diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index e615b238e..fcedad3fa 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -169,6 +169,8 @@ if (ENABLE_VULKAN)
         renderer_vulkan/vk_scheduler.h
         renderer_vulkan/vk_shader_decompiler.cpp
         renderer_vulkan/vk_shader_decompiler.h
+        renderer_vulkan/vk_staging_buffer_pool.cpp
+        renderer_vulkan/vk_staging_buffer_pool.h
         renderer_vulkan/vk_stream_buffer.cpp
         renderer_vulkan/vk_stream_buffer.h
         renderer_vulkan/vk_swapchain.cpp
diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
new file mode 100644
index 000000000..171d78afc
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
@@ -0,0 +1,127 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "common/bit_util.h"
+#include "common/common_types.h"
+#include "video_core/renderer_vulkan/vk_device.h"
+#include "video_core/renderer_vulkan/vk_resource_manager.h"
+#include "video_core/renderer_vulkan/vk_scheduler.h"
+#include "video_core/renderer_vulkan/vk_staging_buffer_pool.h"
+
+namespace Vulkan {
+
+VKStagingBufferPool::StagingBuffer::StagingBuffer(std::unique_ptr<VKBuffer> buffer, VKFence& fence,
+                                                  u64 last_epoch)
+    : buffer{std::move(buffer)}, watch{fence}, last_epoch{last_epoch} {}
+
+VKStagingBufferPool::StagingBuffer::StagingBuffer(StagingBuffer&& rhs) noexcept {
+    buffer = std::move(rhs.buffer);
+    watch = std::move(rhs.watch);
+    last_epoch = rhs.last_epoch;
+}
+
+VKStagingBufferPool::StagingBuffer::~StagingBuffer() = default;
+
+VKStagingBufferPool::StagingBuffer& VKStagingBufferPool::StagingBuffer::operator=(
+    StagingBuffer&& rhs) noexcept {
+    buffer = std::move(rhs.buffer);
+    watch = std::move(rhs.watch);
+    last_epoch = rhs.last_epoch;
+    return *this;
+}
+
+VKStagingBufferPool::VKStagingBufferPool(const VKDevice& device, VKMemoryManager& memory_manager,
+                                         VKScheduler& scheduler)
+    : device{device}, memory_manager{memory_manager}, scheduler{scheduler},
+      is_device_integrated{device.IsIntegrated()} {}
+
+VKStagingBufferPool::~VKStagingBufferPool() = default;
+
+VKBuffer& VKStagingBufferPool::GetUnusedBuffer(std::size_t size, bool host_visible) {
+    if (const auto buffer = TryGetReservedBuffer(size, host_visible)) {
+        return *buffer;
+    }
+    return CreateStagingBuffer(size, host_visible);
+}
+
+void VKStagingBufferPool::TickFrame() {
+    ++epoch;
+    current_delete_level = (current_delete_level + 1) % NumLevels;
+
+    ReleaseCache(true);
+    if (!is_device_integrated) {
+        ReleaseCache(false);
+    }
+}
+
+VKBuffer* VKStagingBufferPool::TryGetReservedBuffer(std::size_t size, bool host_visible) {
+    for (auto& entry : GetCache(host_visible)[Common::Log2Ceil64(size)].entries) {
+        if (entry.watch.TryWatch(scheduler.GetFence())) {
+            entry.last_epoch = epoch;
+            return &*entry.buffer;
+        }
+    }
+    return nullptr;
+}
+
+VKBuffer& VKStagingBufferPool::CreateStagingBuffer(std::size_t size, bool host_visible) {
+    const auto usage =
+        vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst |
+        vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eIndexBuffer;
+    const u32 log2 = Common::Log2Ceil64(size);
+    const vk::BufferCreateInfo buffer_ci({}, 1ULL << log2, usage, vk::SharingMode::eExclusive, 0,
+                                         nullptr);
+    const auto dev = device.GetLogical();
+    auto buffer = std::make_unique<VKBuffer>();
+    buffer->handle = dev.createBufferUnique(buffer_ci, nullptr, device.GetDispatchLoader());
+    buffer->commit = memory_manager.Commit(*buffer->handle, host_visible);
+
+    auto& entries = GetCache(host_visible)[log2].entries;
+    return *entries.emplace_back(std::move(buffer), scheduler.GetFence(), epoch).buffer;
+}
+
+VKStagingBufferPool::StagingBuffersCache& VKStagingBufferPool::GetCache(bool host_visible) {
+    return is_device_integrated || host_visible ? host_staging_buffers : device_staging_buffers;
+}
+
+void VKStagingBufferPool::ReleaseCache(bool host_visible) {
+    auto& cache = GetCache(host_visible);
+    const u64 size = ReleaseLevel(cache, current_delete_level);
+    if (size == 0) {
+        return;
+    }
+}
+
+u64 VKStagingBufferPool::ReleaseLevel(StagingBuffersCache& cache, std::size_t log2) {
+    static constexpr u64 epochs_to_destroy = 180;
+    static constexpr std::size_t deletions_per_tick = 16;
+
+    auto& staging = cache[log2];
+    auto& entries = staging.entries;
+    const std::size_t old_size = entries.size();
+
+    const auto is_deleteable = [this](const auto& entry) {
+        return entry.last_epoch + epochs_to_destroy < epoch && !entry.watch.IsUsed();
+    };
+    const std::size_t begin_offset = staging.delete_index;
+    const std::size_t end_offset = std::min(begin_offset + deletions_per_tick, old_size);
+    const auto begin = std::begin(entries) + begin_offset;
+    const auto end = std::begin(entries) + end_offset;
+    entries.erase(std::remove_if(begin, end, is_deleteable), end);
+
+    const std::size_t new_size = entries.size();
+    staging.delete_index += deletions_per_tick;
+    if (staging.delete_index >= new_size) {
+        staging.delete_index = 0;
+    }
+
+    return (1ULL << log2) * (old_size - new_size);
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
new file mode 100644
index 000000000..02310375f
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
@@ -0,0 +1,83 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <climits>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "common/common_types.h"
+
+#include "video_core/renderer_vulkan/declarations.h"
+#include "video_core/renderer_vulkan/vk_memory_manager.h"
+
+namespace Vulkan {
+
+class VKDevice;
+class VKFenceWatch;
+class VKScheduler;
+
+struct VKBuffer final {
+    UniqueBuffer handle;
+    VKMemoryCommit commit;
+};
+
+class VKStagingBufferPool final {
+public:
+    explicit VKStagingBufferPool(const VKDevice& device, VKMemoryManager& memory_manager,
+                                 VKScheduler& scheduler);
+    ~VKStagingBufferPool();
+
+    VKBuffer& GetUnusedBuffer(std::size_t size, bool host_visible);
+
+    void TickFrame();
+
+private:
+    struct StagingBuffer final {
+        explicit StagingBuffer(std::unique_ptr<VKBuffer> buffer, VKFence& fence, u64 last_epoch);
+        StagingBuffer(StagingBuffer&& rhs) noexcept;
+        StagingBuffer(const StagingBuffer&) = delete;
+        ~StagingBuffer();
+
+        StagingBuffer& operator=(StagingBuffer&& rhs) noexcept;
+
+        std::unique_ptr<VKBuffer> buffer;
+        VKFenceWatch watch;
+        u64 last_epoch = 0;
+    };
+
+    struct StagingBuffers final {
+        std::vector<StagingBuffer> entries;
+        std::size_t delete_index = 0;
+    };
+
+    static constexpr std::size_t NumLevels = sizeof(std::size_t) * CHAR_BIT;
+    using StagingBuffersCache = std::array<StagingBuffers, NumLevels>;
+
+    VKBuffer* TryGetReservedBuffer(std::size_t size, bool host_visible);
+
+    VKBuffer& CreateStagingBuffer(std::size_t size, bool host_visible);
+
+    StagingBuffersCache& GetCache(bool host_visible);
+
+    void ReleaseCache(bool host_visible);
+
+    u64 ReleaseLevel(StagingBuffersCache& cache, std::size_t log2);
+
+    const VKDevice& device;
+    VKMemoryManager& memory_manager;
+    VKScheduler& scheduler;
+    const bool is_device_integrated;
+
+    StagingBuffersCache host_staging_buffers;
+    StagingBuffersCache device_staging_buffers;
+
+    u64 epoch = 0;
+
+    std::size_t current_delete_level = 0;
+};
+
+} // namespace Vulkan