Cache VkFramebuffers

It seems that creation of VkFramebuffers is more expensive than expected
on mobile devices, so this introduces a framebuffer cache that mimics
the existing Vulkan Render Pass cache in hopes of reducing that
overhead.

Bug: 416088623
Change-Id: I8be18a9eeaa734b7bd2c86bbef818078291de4a6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/241137
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Commit-Queue: Brandon Jones <bajones@chromium.org>
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index ac0a7fb..3bb03aa 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -816,6 +816,8 @@
       "vulkan/FencedDeleter.cpp",
       "vulkan/FencedDeleter.h",
       "vulkan/Forward.h",
+      "vulkan/FramebufferCache.cpp",
+      "vulkan/FramebufferCache.h",
       "vulkan/PhysicalDeviceVk.cpp",
       "vulkan/PhysicalDeviceVk.h",
       "vulkan/PipelineCacheVk.cpp",
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 2ef2e07..f11f6c9 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -684,6 +684,7 @@
         "vulkan/ExternalHandle.h"
         "vulkan/FencedDeleter.h"
         "vulkan/Forward.h"
+        "vulkan/FramebufferCache.h"
         "vulkan/PhysicalDeviceVk.h"
         "vulkan/PipelineVk.h"
         "vulkan/PipelineCacheVk.h"
@@ -725,6 +726,7 @@
         "vulkan/DescriptorSetAllocator.cpp"
         "vulkan/DeviceVk.cpp"
         "vulkan/FencedDeleter.cpp"
+        "vulkan/FramebufferCache.cpp"
         "vulkan/PhysicalDeviceVk.cpp"
         "vulkan/PipelineVk.cpp"
         "vulkan/PipelineCacheVk.cpp"
diff --git a/src/dawn/native/Toggles.cpp b/src/dawn/native/Toggles.cpp
index 7d7324f..67dc02b 100644
--- a/src/dawn/native/Toggles.cpp
+++ b/src/dawn/native/Toggles.cpp
@@ -647,6 +647,12 @@
       "Adds a small amount of work to empty render passes which perform a resolve. This toggle is "
       "enabled by default on Qualcomm GPUs, where it is needed to force the resolve to complete.",
       "https://crbug.com/411656647", ToggleStage::Device}},
+    {Toggle::VulkanDisableFramebufferCache,
+     {"vulkan_disable_framebuffer_cache",
+      "Prevents caching of VkFramebuffer objects. When active a new framebuffer will be created "
+      "for every render pass. Enabled by default on Qualcomm GPUs, which have issues with "
+      "framebuffer reuse.",
+      "https://crbug.com/416088623", ToggleStage::Device}},
     {Toggle::NoWorkaroundSampleMaskBecomesZeroForAllButLastColorTarget,
      {"no_workaround_sample_mask_becomes_zero_for_all_but_last_color_target",
       "MacOS 12.0+ Intel has a bug where the sample mask is only applied for the last color "
diff --git a/src/dawn/native/Toggles.h b/src/dawn/native/Toggles.h
index 92ac359..ca3c3c8 100644
--- a/src/dawn/native/Toggles.h
+++ b/src/dawn/native/Toggles.h
@@ -153,6 +153,7 @@
     UseVulkanMemoryModel,
     VulkanScalarizeClampBuiltin,
     VulkanAddWorkToEmptyResolvePass,
+    VulkanDisableFramebufferCache,
 
     // Unresolved issues.
     NoWorkaroundSampleMaskBecomesZeroForAllButLastColorTarget,
diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index 25539fe..bca4324 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn/native/vulkan/CommandBufferVk.cpp
@@ -45,6 +45,7 @@
 #include "dawn/native/vulkan/ComputePipelineVk.h"
 #include "dawn/native/vulkan/DeviceVk.h"
 #include "dawn/native/vulkan/FencedDeleter.h"
+#include "dawn/native/vulkan/FramebufferCache.h"
 #include "dawn/native/vulkan/PhysicalDeviceVk.h"
 #include "dawn/native/vulkan/PipelineLayoutVk.h"
 #include "dawn/native/vulkan/QuerySetVk.h"
@@ -452,15 +453,15 @@
         renderPassVK = renderPassInfo.renderPass;
     }
 
-    // Create a framebuffer that will be used once for the render pass and gather the clear
-    // values for the attachments at the same time.
+    // Query a framebuffer from the cache and gather the clear values for the attachments at the
+    // same time.
+    FramebufferCacheQuery framebufferQuery;
     std::array<VkClearValue, kMaxColorAttachments + 1> clearValues;
     VkFramebuffer framebuffer = VK_NULL_HANDLE;
-    uint32_t attachmentCount = 0;
     {
-        // Fill in the attachment info that will be chained in the framebuffer create info.
-        std::array<VkImageView, kMaxColorAttachments * 2 + 1> attachments;
+        framebufferQuery.SetRenderPass(renderPassVK, renderPass->width, renderPass->height);
 
+        // Fill in the attachment info that will be chained in the framebuffer create info.
         for (auto i : renderPass->attachmentState->GetColorAttachmentsMask()) {
             auto& attachmentInfo = renderPass->colorAttachments[i];
             TextureView* view = ToBackend(attachmentInfo.view.Get());
@@ -468,13 +469,14 @@
                 continue;
             }
 
+            uint32_t attachmentIndex;
             if (view->GetDimension() == wgpu::TextureViewDimension::e3D) {
                 VkImageView handleFor2DViewOn3D;
                 DAWN_TRY_ASSIGN(handleFor2DViewOn3D,
                                 view->GetOrCreate2DViewOn3D(attachmentInfo.depthSlice));
-                attachments[attachmentCount] = handleFor2DViewOn3D;
+                attachmentIndex = framebufferQuery.AddAttachment(handleFor2DViewOn3D);
             } else {
-                attachments[attachmentCount] = view->GetHandle();
+                attachmentIndex = framebufferQuery.AddAttachment(view->GetHandle());
             }
 
             switch (view->GetFormat().GetAspectInfo(Aspect::Color).baseType) {
@@ -482,7 +484,7 @@
                     const std::array<float, 4> appliedClearColor =
                         ConvertToFloatColor(attachmentInfo.clearColor);
                     for (uint32_t j = 0; j < 4; ++j) {
-                        clearValues[attachmentCount].color.float32[j] = appliedClearColor[j];
+                        clearValues[attachmentIndex].color.float32[j] = appliedClearColor[j];
                     }
                     break;
                 }
@@ -490,7 +492,7 @@
                     const std::array<uint32_t, 4> appliedClearColor =
                         ConvertToUnsignedIntegerColor(attachmentInfo.clearColor);
                     for (uint32_t j = 0; j < 4; ++j) {
-                        clearValues[attachmentCount].color.uint32[j] = appliedClearColor[j];
+                        clearValues[attachmentIndex].color.uint32[j] = appliedClearColor[j];
                     }
                     break;
                 }
@@ -498,55 +500,32 @@
                     const std::array<int32_t, 4> appliedClearColor =
                         ConvertToSignedIntegerColor(attachmentInfo.clearColor);
                     for (uint32_t j = 0; j < 4; ++j) {
-                        clearValues[attachmentCount].color.int32[j] = appliedClearColor[j];
+                        clearValues[attachmentIndex].color.int32[j] = appliedClearColor[j];
                     }
                     break;
                 }
             }
-            attachmentCount++;
         }
 
         if (renderPass->attachmentState->HasDepthStencilAttachment()) {
             auto& attachmentInfo = renderPass->depthStencilAttachment;
             TextureView* view = ToBackend(attachmentInfo.view.Get());
 
-            attachments[attachmentCount] = view->GetHandle();
+            uint32_t attachmentIndex = framebufferQuery.AddAttachment(view->GetHandle());
 
-            clearValues[attachmentCount].depthStencil.depth = attachmentInfo.clearDepth;
-            clearValues[attachmentCount].depthStencil.stencil = attachmentInfo.clearStencil;
-
-            attachmentCount++;
+            clearValues[attachmentIndex].depthStencil.depth = attachmentInfo.clearDepth;
+            clearValues[attachmentIndex].depthStencil.stencil = attachmentInfo.clearStencil;
         }
 
         for (auto i : renderPass->attachmentState->GetColorAttachmentsMask()) {
             if (renderPass->colorAttachments[i].resolveTarget != nullptr) {
                 TextureView* view = ToBackend(renderPass->colorAttachments[i].resolveTarget.Get());
-
-                attachments[attachmentCount] = view->GetHandle();
-
-                attachmentCount++;
+                framebufferQuery.AddAttachment(view->GetHandle());
             }
         }
 
-        // Chain attachments and create the framebuffer
-        VkFramebufferCreateInfo createInfo;
-        createInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
-        createInfo.pNext = nullptr;
-        createInfo.flags = 0;
-        createInfo.renderPass = renderPassVK;
-        createInfo.attachmentCount = attachmentCount;
-        createInfo.pAttachments = AsVkArray(attachments.data());
-        createInfo.width = renderPass->width;
-        createInfo.height = renderPass->height;
-        createInfo.layers = 1;
-
-        DAWN_TRY(CheckVkSuccess(device->fn.CreateFramebuffer(device->GetVkDevice(), &createInfo,
-                                                             nullptr, &*framebuffer),
-                                "CreateFramebuffer"));
-
-        // We don't reuse VkFramebuffers so mark the framebuffer for deletion as soon as the
-        // commands currently being recorded are finished.
-        device->GetFencedDeleter()->DeleteWhenUnused(framebuffer);
+        DAWN_TRY_ASSIGN(framebuffer,
+                        device->GetFramebufferCache()->GetFramebuffer(framebufferQuery));
     }
 
     VkRenderPassBeginInfo beginInfo;
@@ -558,7 +537,7 @@
     beginInfo.renderArea.offset.y = 0;
     beginInfo.renderArea.extent.width = renderPass->width;
     beginInfo.renderArea.extent.height = renderPass->height;
-    beginInfo.clearValueCount = attachmentCount;
+    beginInfo.clearValueCount = framebufferQuery.attachmentCount;
     beginInfo.pClearValues = clearValues.data();
 
     if (renderPass->attachmentState->GetExpandResolveInfo().attachmentsToExpandResolve.any()) {
diff --git a/src/dawn/native/vulkan/DeviceVk.cpp b/src/dawn/native/vulkan/DeviceVk.cpp
index 58d41f8..bad2473 100644
--- a/src/dawn/native/vulkan/DeviceVk.cpp
+++ b/src/dawn/native/vulkan/DeviceVk.cpp
@@ -50,6 +50,7 @@
 #include "dawn/native/vulkan/CommandBufferVk.h"
 #include "dawn/native/vulkan/ComputePipelineVk.h"
 #include "dawn/native/vulkan/FencedDeleter.h"
+#include "dawn/native/vulkan/FramebufferCache.h"
 #include "dawn/native/vulkan/PhysicalDeviceVk.h"
 #include "dawn/native/vulkan/PipelineCacheVk.h"
 #include "dawn/native/vulkan/PipelineLayoutVk.h"
@@ -141,6 +142,7 @@
         functions->CmdDrawIndexedIndirect = NoopDrawFunction<PFN_vkCmdDrawIndexedIndirect>::Fun;
     }
 
+    mFramebufferCache = std::make_unique<FramebufferCache>(this);
     mRenderPassCache = std::make_unique<RenderPassCache>(this);
 
     VkDeviceSize heapBlockSize =
@@ -390,6 +392,10 @@
     return *mDeleter;
 }
 
+FramebufferCache* Device::GetFramebufferCache() const {
+    return mFramebufferCache.get();
+}
+
 RenderPassCache* Device::GetRenderPassCache() const {
     return mRenderPassCache.get();
 }
@@ -946,8 +952,9 @@
     // Allow recycled memory to be deleted.
     GetResourceMemoryAllocator()->FreeRecycledMemory();
 
-    // The VkRenderPasses in the cache can be destroyed immediately since all commands referring
-    // to them are guaranteed to be finished executing.
+    // The VkFramebuffers and VkRenderPasses in the cache can be destroyed immediately since all
+    // commands referring to them are guaranteed to be finished executing.
+    mFramebufferCache = nullptr;
     mRenderPassCache = nullptr;
 
     // Destroy the VkPipelineCache before VkDevice.
diff --git a/src/dawn/native/vulkan/DeviceVk.h b/src/dawn/native/vulkan/DeviceVk.h
index 2b5a60b..c6d76c7 100644
--- a/src/dawn/native/vulkan/DeviceVk.h
+++ b/src/dawn/native/vulkan/DeviceVk.h
@@ -53,6 +53,7 @@
 
 class BufferUploader;
 class FencedDeleter;
+class FramebufferCache;
 class RenderPassCache;
 class ResourceMemoryAllocator;
 
@@ -76,6 +77,7 @@
     uint32_t GetGraphicsQueueFamily() const;
 
     MutexProtected<FencedDeleter>& GetFencedDeleter() const;
+    FramebufferCache* GetFramebufferCache() const;
     RenderPassCache* GetRenderPassCache() const;
     MutexProtected<ResourceMemoryAllocator>& GetResourceMemoryAllocator() const;
     external_semaphore::Service* GetExternalSemaphoreService() const;
@@ -202,6 +204,7 @@
         mDescriptorAllocatorsPendingDeallocation;
     std::unique_ptr<MutexProtected<FencedDeleter>> mDeleter;
     std::unique_ptr<MutexProtected<ResourceMemoryAllocator>> mResourceMemoryAllocator;
+    std::unique_ptr<FramebufferCache> mFramebufferCache;
     std::unique_ptr<RenderPassCache> mRenderPassCache;
 
     std::unique_ptr<external_memory::Service> mExternalMemoryService;
diff --git a/src/dawn/native/vulkan/FramebufferCache.cpp b/src/dawn/native/vulkan/FramebufferCache.cpp
new file mode 100644
index 0000000..6caf973
--- /dev/null
+++ b/src/dawn/native/vulkan/FramebufferCache.cpp
@@ -0,0 +1,160 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "dawn/native/vulkan/FramebufferCache.h"
+
+#include "absl/container/inlined_vector.h"
+#include "dawn/common/HashUtils.h"
+#include "dawn/common/Range.h"
+#include "dawn/common/vulkan_platform.h"
+#include "dawn/native/vulkan/DeviceVk.h"
+#include "dawn/native/vulkan/FencedDeleter.h"
+#include "dawn/native/vulkan/VulkanError.h"
+
+namespace dawn::native::vulkan {
+
+// FramebufferCacheQuery
+
+void FramebufferCacheQuery::SetRenderPass(VkRenderPass pass,
+                                          uint32_t passWidth,
+                                          uint32_t passHeight) {
+    renderPass = pass;
+    width = passWidth;
+    height = passHeight;
+}
+
+uint32_t FramebufferCacheQuery::AddAttachment(VkImageView attachment) {
+    attachments[attachmentCount] = attachment;
+    return attachmentCount++;
+}
+
+// FramebufferCache
+
+FramebufferCache::FramebufferCache(Device* device, size_t capacity)
+    : mDevice(device), mCapacity(capacity) {}
+
+FramebufferCache::~FramebufferCache() {
+    std::lock_guard<std::mutex> lock(mMutex);
+    for (auto [_, framebuffer] : mRecentList) {
+        mDevice->fn.DestroyFramebuffer(mDevice->GetVkDevice(), framebuffer, nullptr);
+    }
+
+    mRecentList.clear();
+    mCache.clear();
+}
+
+bool FramebufferCache::IsCacheDisabled() const {
+    return mDevice->IsToggleEnabled(Toggle::VulkanDisableFramebufferCache);
+}
+
+ResultOrError<VkFramebuffer> FramebufferCache::GetFramebuffer(FramebufferCacheQuery& query) {
+    if (IsCacheDisabled()) {
+        // Some devices (such as older Qualcomm GPUs) appear to have problems with reusing
+        // framebuffers, so don't attempt to cache if it's been disabled.
+        VkFramebuffer framebuffer;
+        DAWN_TRY_ASSIGN(framebuffer, CreateFramebufferForQuery(query));
+        mDevice->GetFencedDeleter()->DeleteWhenUnused(framebuffer);
+        return framebuffer;
+    }
+
+    std::lock_guard<std::mutex> lock(mMutex);
+    auto it = mCache.find(query);
+    if (it != mCache.end()) {
+        // Move the queried framebuffer to the front of the LRU list.
+        // Using the iterator as a stable reference like this works because "Adding, removing and
+        // moving the elements within the list or across several lists does not invalidate the
+        // iterators or references. An iterator is invalidated only when the corresponding element
+        // is deleted." (From https://en.cppreference.com/w/cpp/container/list)
+        mRecentList.splice(mRecentList.begin(), mRecentList, it->second);
+        return it->second->second;
+    }
+
+    VkFramebuffer framebuffer;
+    DAWN_TRY_ASSIGN(framebuffer, CreateFramebufferForQuery(query));
+
+    mRecentList.emplace_front(query, framebuffer);
+    mCache.emplace(query, mRecentList.begin());
+
+    if (mRecentList.size() > mCapacity) {
+        auto back = mRecentList.back();
+        mDevice->GetFencedDeleter()->DeleteWhenUnused(back.second);
+        mCache.erase(back.first);
+        mRecentList.pop_back();
+    }
+
+    return framebuffer;
+}
+
+ResultOrError<VkFramebuffer> FramebufferCache::CreateFramebufferForQuery(
+    FramebufferCacheQuery& query) const {
+    VkFramebufferCreateInfo createInfo;
+    createInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+    createInfo.pNext = nullptr;
+    createInfo.flags = 0;
+    createInfo.renderPass = query.renderPass;
+    createInfo.attachmentCount = query.attachmentCount;
+    createInfo.pAttachments = AsVkArray(query.attachments.data());
+    createInfo.width = query.width;
+    createInfo.height = query.height;
+    createInfo.layers = 1;
+
+    VkFramebuffer framebuffer;
+    DAWN_TRY(CheckVkSuccess(
+        mDevice->fn.CreateFramebuffer(mDevice->GetVkDevice(), &createInfo, nullptr, &*framebuffer),
+        "CreateFramebuffer"));
+    return framebuffer;
+}
+
+size_t FramebufferCache::CacheFuncs::operator()(const FramebufferCacheQuery& query) const {
+    size_t hash = Hash(query.renderPass.GetHandle());
+
+    HashCombine(&hash, query.width, query.height, query.attachmentCount);
+
+    for (uint32_t i = 0; i < query.attachmentCount; ++i) {
+        HashCombine(&hash, query.attachments[i].GetHandle());
+    }
+
+    return hash;
+}
+
+bool FramebufferCache::CacheFuncs::operator()(const FramebufferCacheQuery& a,
+                                              const FramebufferCacheQuery& b) const {
+    if (a.renderPass != b.renderPass || a.width != b.width || a.height != b.height ||
+        a.attachmentCount != b.attachmentCount) {
+        return false;
+    }
+
+    for (uint32_t i = 0; i < a.attachmentCount; ++i) {
+        if (a.attachments[i] != b.attachments[i]) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+}  // namespace dawn::native::vulkan
diff --git a/src/dawn/native/vulkan/FramebufferCache.h b/src/dawn/native/vulkan/FramebufferCache.h
new file mode 100644
index 0000000..1171667
--- /dev/null
+++ b/src/dawn/native/vulkan/FramebufferCache.h
@@ -0,0 +1,105 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_DAWN_NATIVE_VULKAN_FRAMEBUFFERCACHE_H_
+#define SRC_DAWN_NATIVE_VULKAN_FRAMEBUFFERCACHE_H_
+
+#include <array>
+#include <list>
+#include <mutex>
+#include <utility>
+
+#include "absl/container/flat_hash_map.h"
+#include "dawn/common/Constants.h"
+#include "dawn/common/vulkan_platform.h"
+#include "dawn/native/Error.h"
+#include "dawn/native/dawn_platform.h"
+#include "partition_alloc/pointers/raw_ptr.h"
+
+namespace dawn::native::vulkan {
+
+class Device;
+
+// This is a key to query the RenderPassCache, it can be sparse meaning that only the
+// information for bits set in colorMask or hasDepthStencil need to be provided and the rest can
+// be uninintialized.
+struct FramebufferCacheQuery {
+    // Use these helpers to build the query, they make sure all relevant data is initialized and
+    // masks set.
+    void SetRenderPass(VkRenderPass pass, uint32_t passWidth, uint32_t passHeight);
+    uint32_t AddAttachment(VkImageView attachment);
+
+    VkRenderPass renderPass;
+    uint32_t width;
+    uint32_t height;
+
+    std::array<VkImageView, kMaxColorAttachments * 2 + 1> attachments;
+    uint32_t attachmentCount = 0;
+};
+
+// A LRU Cache of VkFramebuffers so that we reduce the need to re-create framebuffers for every
+// render pass. We always arrange the order of attachments in "color-depthstencil-resolve" order
+// when creating render pass and framebuffer so that we can always make sure the order of
+// attachments in the rendering pipeline matches the one of the framebuffer.
+// All the operations on FramebufferCache are guaranteed to be thread-safe.
+class FramebufferCache {
+    // How many elements the LRU cache will retain before it begins evicting them.
+    static const size_t kDefaultCacheCapacity = 32;
+
+  public:
+    explicit FramebufferCache(Device* device, size_t capacity = kDefaultCacheCapacity);
+    ~FramebufferCache();
+
+    ResultOrError<VkFramebuffer> GetFramebuffer(FramebufferCacheQuery& query);
+
+  private:
+    bool IsCacheDisabled() const;
+
+    // Does the actual VkFramebuffer creation on a cache miss.
+    ResultOrError<VkFramebuffer> CreateFramebufferForQuery(FramebufferCacheQuery& query) const;
+
+    // Implements the functors necessary for to use RenderPassCacheQueries as absl::flat_hash_map
+    // keys.
+    struct CacheFuncs {
+        size_t operator()(const FramebufferCacheQuery& query) const;
+        bool operator()(const FramebufferCacheQuery& a, const FramebufferCacheQuery& b) const;
+    };
+    using RecentList = std::list<std::pair<FramebufferCacheQuery, VkFramebuffer>>;
+    using Cache =
+        absl::flat_hash_map<FramebufferCacheQuery, RecentList::iterator, CacheFuncs, CacheFuncs>;
+
+    raw_ptr<Device> mDevice = nullptr;
+    size_t mCapacity;
+
+    std::mutex mMutex;
+    RecentList mRecentList;
+    Cache mCache;
+};
+
+}  // namespace dawn::native::vulkan
+
+#endif  // SRC_DAWN_NATIVE_VULKAN_FRAMEBUFFERCACHE_H_
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index 6881162..5f4a5c7 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -816,6 +816,9 @@
         // chromium:407109052: Qualcomm devices have a bug where the spirv extended op NClamp
         // modifies other components of a vector when one of the components is nan.
         deviceToggles->Default(Toggle::VulkanScalarizeClampBuiltin, true);
+
+        // chromium:416088623: Some Qualcomm devices have issues with reusing VkFramebuffers.
+        deviceToggles->Default(Toggle::VulkanDisableFramebufferCache, true);
     }
 
     if (IsAndroidARM()) {