Cache VkFramebuffers (re-reland)

Creation of VkFramebuffers is more expensive than expected on mobile
devices, so this introduces a framebuffer cache to reduce that
overhead.

Previous attempt at landing was reverted after an MSan error on Linux.
See https://ci.chromium.org/ui/p/chromium/builders/try/linux_chromium_msan_rel_ng/10596/test-results

Prior to that, had trouble landing due to crashes reported in
https://crbug.com/425093010.

Discovered that the likely reason for the crashes was that Vulkan may
reuse handle values for things like image views, making them
inappropriate for use as cache keys, as I was doing previously.
Refactored this CL to use explicit unique IDs instead, which resolved
the problems on the hardware I tested against.

Bug: 416088623
Change-Id: Ic22483f0d5388ab8ea3fc307bcc69910d6699888
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/251254
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Brandon Jones <bajones@chromium.org>
diff --git a/src/dawn/common/LRUCache.h b/src/dawn/common/LRUCache.h
index a57c6ac..448f629 100644
--- a/src/dawn/common/LRUCache.h
+++ b/src/dawn/common/LRUCache.h
@@ -134,7 +134,10 @@
     }
 
     void Clear() {
-        mCache.Use([](auto cache) {
+        mCache.Use([&](auto cache) {
+            for (auto [_, value] : cache->list) {
+                EvictedFromCache(value);
+            }
             cache->map.clear();
             cache->list.clear();
         });
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index fd4da9b..1f9682e 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -842,6 +842,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 852e277..6a144ef 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -714,6 +714,7 @@
         "vulkan/ExternalHandle.h"
         "vulkan/FencedDeleter.h"
         "vulkan/Forward.h"
+        "vulkan/FramebufferCache.h"
         "vulkan/PhysicalDeviceVk.h"
         "vulkan/PipelineVk.h"
         "vulkan/PipelineCacheVk.h"
@@ -755,6 +756,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/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index 81883e6..99a420b 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"
@@ -417,6 +418,7 @@
 
     // Query a VkRenderPass from the cache
     VkRenderPass renderPassVK = VK_NULL_HANDLE;
+    uint32_t renderPassId = 0;
     {
         RenderPassCacheQuery query;
 
@@ -442,17 +444,17 @@
         RenderPassCache::RenderPassInfo renderPassInfo;
         DAWN_TRY_ASSIGN(renderPassInfo, device->GetRenderPassCache()->GetRenderPass(query));
         renderPassVK = renderPassInfo.renderPass;
+        renderPassId = renderPassInfo.uniqueId;
     }
 
-    // Create a framebuffer that will be used once for the render pass and gather the clear
-    // values for the attachments at the same time.
-    std::array<VkClearValue, kMaxColorAttachments + 1> clearValues;
+    // Query a framebuffer from the cache and gather the clear values for the attachments at the
+    // same time.
+    FramebufferCacheQuery framebufferQuery;
     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(renderPassId, 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());
@@ -460,21 +462,13 @@
                 continue;
             }
 
-            if (view->GetDimension() == wgpu::TextureViewDimension::e3D) {
-                VkImageView handleFor2DViewOn3D;
-                DAWN_TRY_ASSIGN(handleFor2DViewOn3D,
-                                view->GetOrCreate2DViewOn3D(attachmentInfo.depthSlice));
-                attachments[attachmentCount] = handleFor2DViewOn3D;
-            } else {
-                attachments[attachmentCount] = view->GetHandle();
-            }
-
+            VkClearValue clearValue;
             switch (view->GetFormat().GetAspectInfo(Aspect::Color).baseType) {
                 case TextureComponentType::Float: {
                     const std::array<float, 4> appliedClearColor =
                         ConvertToFloatColor(attachmentInfo.clearColor);
                     for (uint32_t j = 0; j < 4; ++j) {
-                        clearValues[attachmentCount].color.float32[j] = appliedClearColor[j];
+                        clearValue.color.float32[j] = appliedClearColor[j];
                     }
                     break;
                 }
@@ -482,7 +476,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];
+                        clearValue.color.uint32[j] = appliedClearColor[j];
                     }
                     break;
                 }
@@ -490,55 +484,58 @@
                     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];
+                        clearValue.color.int32[j] = appliedClearColor[j];
                     }
                     break;
                 }
             }
-            attachmentCount++;
+
+            uint32_t depthSlice = view->GetDimension() == wgpu::TextureViewDimension::e3D
+                                      ? attachmentInfo.depthSlice
+                                      : 0;
+            DAWN_TRY(framebufferQuery.AddAttachment(view, clearValue, depthSlice));
         }
 
         if (renderPass->attachmentState->HasDepthStencilAttachment()) {
             auto& attachmentInfo = renderPass->depthStencilAttachment;
             TextureView* view = ToBackend(attachmentInfo.view.Get());
 
-            attachments[attachmentCount] = view->GetHandle();
+            VkClearValue clearValue;
+            clearValue.depthStencil.depth = attachmentInfo.clearDepth;
+            clearValue.depthStencil.stencil = attachmentInfo.clearStencil;
 
-            clearValues[attachmentCount].depthStencil.depth = attachmentInfo.clearDepth;
-            clearValues[attachmentCount].depthStencil.stencil = attachmentInfo.clearStencil;
-
-            attachmentCount++;
+            DAWN_TRY(framebufferQuery.AddAttachment(view, clearValue));
         }
 
         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++;
+                DAWN_TRY(framebufferQuery.AddAttachment(view));
             }
         }
 
-        // 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_ASSIGN(framebuffer,
+                        device->GetFramebufferCache()->GetOrCreate(
+                            framebufferQuery,
+                            [&](FramebufferCacheQuery& query) -> ResultOrError<VkFramebuffer> {
+                                VkFramebufferCreateInfo createInfo;
+                                createInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+                                createInfo.pNext = nullptr;
+                                createInfo.flags = 0;
+                                createInfo.renderPass = renderPassVK;
+                                createInfo.attachmentCount = query.attachmentCount;
+                                createInfo.pAttachments = AsVkArray(query.attachments.data());
+                                createInfo.width = query.width;
+                                createInfo.height = query.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);
+                                VkFramebuffer framebuffer;
+                                DAWN_TRY(CheckVkSuccess(
+                                    device->fn.CreateFramebuffer(device->GetVkDevice(), &createInfo,
+                                                                 nullptr, &*framebuffer),
+                                    "CreateFramebuffer"));
+                                return framebuffer;
+                            }));
     }
 
     VkRenderPassBeginInfo beginInfo;
@@ -550,8 +547,8 @@
     beginInfo.renderArea.offset.y = 0;
     beginInfo.renderArea.extent.width = renderPass->width;
     beginInfo.renderArea.extent.height = renderPass->height;
-    beginInfo.clearValueCount = attachmentCount;
-    beginInfo.pClearValues = clearValues.data();
+    beginInfo.clearValueCount = framebufferQuery.attachmentCount;
+    beginInfo.pClearValues = framebufferQuery.clearValues.data();
 
     if (renderPass->attachmentState->GetExpandResolveInfo().attachmentsToExpandResolve.any()) {
         DAWN_TRY(BeginRenderPassAndExpandResolveTextureWithDraw(device, recordingContext,
diff --git a/src/dawn/native/vulkan/DeviceVk.cpp b/src/dawn/native/vulkan/DeviceVk.cpp
index ce434f0..3d82975 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 =
@@ -388,6 +390,10 @@
     return *mDeleter;
 }
 
+FramebufferCache* Device::GetFramebufferCache() const {
+    return mFramebufferCache.get();
+}
+
 RenderPassCache* Device::GetRenderPassCache() const {
     return mRenderPassCache.get();
 }
@@ -956,8 +962,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.
@@ -1060,6 +1067,8 @@
 bool Device::ReduceMemoryUsageImpl() {
     GetResourceMemoryAllocator()->FreeRecycledMemory();
 
+    GetFramebufferCache()->Clear();
+
     auto GetLastPendingDeletionSerial = [this]() {
         // Only hold the lock for one of these objects at a time to avoid lock-order-inversion.
         auto deleterSerial = GetFencedDeleter()->GetLastPendingDeletionSerial();
diff --git a/src/dawn/native/vulkan/DeviceVk.h b/src/dawn/native/vulkan/DeviceVk.h
index 547e362..690e9e9 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;
@@ -201,6 +203,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..b6497b7
--- /dev/null
+++ b/src/dawn/native/vulkan/FramebufferCache.cpp
@@ -0,0 +1,120 @@
+// 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/TextureVk.h"
+#include "dawn/native/vulkan/VulkanError.h"
+
+namespace dawn::native::vulkan {
+
+// FramebufferCacheQuery
+
+void FramebufferCacheQuery::SetRenderPass(uint64_t passId,
+                                          uint32_t passWidth,
+                                          uint32_t passHeight) {
+    renderPassId = passId;
+    width = passWidth;
+    height = passHeight;
+}
+
+MaybeError FramebufferCacheQuery::AddAttachment(TextureView* view,
+                                                VkClearValue clearValue,
+                                                uint32_t depthSlice) {
+    textureViews[attachmentCount].weakRef = GetWeakRef(view);
+    textureViews[attachmentCount].depthSlice = depthSlice;
+
+    if (view->GetDimension() == wgpu::TextureViewDimension::e3D) {
+        VkImageView handleFor2DViewOn3D;
+        DAWN_TRY_ASSIGN(handleFor2DViewOn3D, view->GetOrCreate2DViewOn3D(depthSlice));
+        attachments[attachmentCount] = handleFor2DViewOn3D;
+    } else {
+        attachments[attachmentCount] = view->GetHandle();
+    }
+
+    clearValues[attachmentCount] = clearValue;
+
+    attachmentCount++;
+
+    return {};
+}
+
+// FramebufferCache
+
+FramebufferCache::FramebufferCache(Device* device, size_t capacity)
+    : FramebufferCache::Base(capacity), mDevice(device) {}
+
+FramebufferCache::~FramebufferCache() {
+    mCache.Use([this](auto cache) {
+        for (auto [_, framebuffer] : cache->list) {
+            mDevice->fn.DestroyFramebuffer(mDevice->GetVkDevice(), framebuffer, nullptr);
+        }
+    });
+}
+
+void FramebufferCache::EvictedFromCache(const VkFramebuffer& framebuffer) {
+    mDevice->GetFencedDeleter()->DeleteWhenUnused(framebuffer);
+}
+
+size_t FramebufferCacheFuncs::operator()(const FramebufferCacheQuery& query) const {
+    size_t hash = Hash(query.renderPassId);
+
+    HashCombine(&hash, query.width, query.height, query.attachmentCount);
+
+    for (uint32_t i = 0; i < query.attachmentCount; ++i) {
+        Ref<TextureView> view = query.textureViews[i].weakRef.Promote();
+        HashCombine(&hash, view.Get(), query.textureViews[i].depthSlice);
+    }
+
+    return hash;
+}
+
+bool FramebufferCacheFuncs::operator()(const FramebufferCacheQuery& a,
+                                       const FramebufferCacheQuery& b) const {
+    if (a.renderPassId != b.renderPassId || a.width != b.width || a.height != b.height ||
+        a.attachmentCount != b.attachmentCount) {
+        return false;
+    }
+
+    for (uint32_t i = 0; i < a.attachmentCount; ++i) {
+        Ref<TextureView> viewA = a.textureViews[i].weakRef.Promote();
+        Ref<TextureView> viewB = b.textureViews[i].weakRef.Promote();
+        if (viewA != viewB || a.textureViews[i].depthSlice != b.textureViews[i].depthSlice) {
+            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..a632d3c
--- /dev/null
+++ b/src/dawn/native/vulkan/FramebufferCache.h
@@ -0,0 +1,112 @@
+// 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/LRUCache.h"
+#include "dawn/common/WeakRef.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;
+class TextureView;
+
+struct FramebufferCacheTextureView {
+    // Uses a weak ref in the cache key as we need to make sure the pointer values are not reused
+    // if the texture view is no longer used and new views created.
+    WeakRef<TextureView> weakRef;
+    uint32_t depthSlice;
+};
+
+// A key to query the FramebufferCache
+struct FramebufferCacheQuery {
+    // Use these helpers to build the query, they make sure all relevant data is initialized and
+    // masks set.
+    void SetRenderPass(uint64_t passId, uint32_t passWidth, uint32_t passHeight);
+    MaybeError AddAttachment(TextureView* attachment,
+                             VkClearValue clearValue = {},
+                             uint32_t depthSlice = 0);
+
+    // A unique ID for the render pass is used for cache lookup rather than the VkRenderPass
+    // because Vulkan handles may be reused, making them unreliable as cache keys.
+    uint64_t renderPassId;
+    uint32_t width;
+    uint32_t height;
+
+    std::array<FramebufferCacheTextureView, kMaxColorAttachments * 2 + 1> textureViews;
+
+    // Attachments and clearValues are not used as part of the query hash, but are stored here
+    // anyway because it's natural to build them up at the same time as the query criteria.
+    std::array<VkImageView, kMaxColorAttachments * 2 + 1> attachments;
+    std::array<VkClearValue, kMaxColorAttachments + 1> clearValues;
+
+    uint32_t attachmentCount = 0;
+};
+
+// Implements the functors necessary for to use RenderPassCacheQueries as absl::flat_hash_map keys.
+struct FramebufferCacheFuncs {
+    size_t operator()(const FramebufferCacheQuery& query) const;
+    bool operator()(const FramebufferCacheQuery& a, const FramebufferCacheQuery& b) const;
+};
+
+// 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 final
+    : public LRUCache<FramebufferCacheQuery, VkFramebuffer, ErrorData, FramebufferCacheFuncs> {
+    using Base = LRUCache<FramebufferCacheQuery, VkFramebuffer, ErrorData, FramebufferCacheFuncs>;
+
+  public:
+    static const size_t kDefaultCapacity = 32;
+    explicit FramebufferCache(Device* device, size_t capacity = kDefaultCapacity);
+    ~FramebufferCache() override;
+
+    void EvictedFromCache(const VkFramebuffer& value) override;
+
+  private:
+    // We use a raw pointer to the device here because the cache is always owned by the device
+    // and hence should always be valid.
+    raw_ptr<Device> mDevice;
+};
+
+}  // namespace dawn::native::vulkan
+
+#endif  // SRC_DAWN_NATIVE_VULKAN_FRAMEBUFFERCACHE_H_
diff --git a/src/dawn/native/vulkan/RenderPassCache.cpp b/src/dawn/native/vulkan/RenderPassCache.cpp
index b605493..1086080 100644
--- a/src/dawn/native/vulkan/RenderPassCache.cpp
+++ b/src/dawn/native/vulkan/RenderPassCache.cpp
@@ -159,7 +159,7 @@
 }
 
 ResultOrError<RenderPassCache::RenderPassInfo> RenderPassCache::CreateRenderPassForQuery(
-    const RenderPassCacheQuery& query) const {
+    const RenderPassCacheQuery& query) {
     // The Vulkan subpasses want to know the layout of the attachments with VkAttachmentRef.
     // Precompute them as they must be pointer-chained in VkSubpassDescription.
     // Note that both colorAttachmentRefs and resolveAttachmentRefs can be sparse with holes
@@ -323,6 +323,7 @@
     // Create the render pass from the zillion parameters
     RenderPassInfo renderPassInfo;
     renderPassInfo.mainSubpass = subpassDescs.size() - 1;
+    renderPassInfo.uniqueId = nextRenderPassId++;
     DAWN_TRY(CheckVkSuccess(mDevice->fn.CreateRenderPass(mDevice->GetVkDevice(), &createInfo,
                                                          nullptr, &*renderPassInfo.renderPass),
                             "CreateRenderPass"));
diff --git a/src/dawn/native/vulkan/RenderPassCache.h b/src/dawn/native/vulkan/RenderPassCache.h
index 29470aa..5753fd0 100644
--- a/src/dawn/native/vulkan/RenderPassCache.h
+++ b/src/dawn/native/vulkan/RenderPassCache.h
@@ -29,6 +29,7 @@
 #define SRC_DAWN_NATIVE_VULKAN_RENDERPASSCACHE_H_
 
 #include <array>
+#include <atomic>
 #include <bitset>
 #include <mutex>
 
@@ -99,13 +100,14 @@
     struct RenderPassInfo {
         VkRenderPass renderPass = VK_NULL_HANDLE;
         uint32_t mainSubpass = 0;
+        uint64_t uniqueId;
     };
 
     ResultOrError<RenderPassInfo> GetRenderPass(const RenderPassCacheQuery& query);
 
   private:
     // Does the actual VkRenderPass creation on a cache miss.
-    ResultOrError<RenderPassInfo> CreateRenderPassForQuery(const RenderPassCacheQuery& query) const;
+    ResultOrError<RenderPassInfo> CreateRenderPassForQuery(const RenderPassCacheQuery& query);
 
     // Implements the functors necessary for to use RenderPassCacheQueries as absl::flat_hash_map
     // keys.
@@ -117,6 +119,8 @@
 
     raw_ptr<Device> mDevice = nullptr;
 
+    std::atomic<uint64_t> nextRenderPassId = 1;
+
     std::mutex mMutex;
     Cache mCache;
 };
diff --git a/src/dawn/native/vulkan/TextureVk.cpp b/src/dawn/native/vulkan/TextureVk.cpp
index f0e90db..98781ba 100644
--- a/src/dawn/native/vulkan/TextureVk.cpp
+++ b/src/dawn/native/vulkan/TextureVk.cpp
@@ -27,6 +27,7 @@
 
 #include "dawn/native/vulkan/TextureVk.h"
 
+#include <iostream>
 #include <utility>
 
 #include "dawn/common/Assert.h"
@@ -1989,6 +1990,8 @@
     return {};
 }
 
+TextureView::TextureView(TextureBase* texture, const UnpackedPtr<TextureViewDescriptor>& descriptor)
+    : TextureViewBase(texture, descriptor) {}
 TextureView::~TextureView() {}
 
 void TextureView::DestroyImpl() {
diff --git a/src/dawn/native/vulkan/TextureVk.h b/src/dawn/native/vulkan/TextureVk.h
index 88398b9..557ea67 100644
--- a/src/dawn/native/vulkan/TextureVk.h
+++ b/src/dawn/native/vulkan/TextureVk.h
@@ -308,7 +308,7 @@
     SharedTextureMemoryObjects mSharedTextureMemoryObjects;
 };
 
-class TextureView final : public TextureViewBase {
+class TextureView final : public TextureViewBase, public WeakRefSupport<TextureView> {
   public:
     static ResultOrError<Ref<TextureView>> Create(
         TextureBase* texture,
@@ -322,6 +322,7 @@
     YCbCrVkDescriptor GetYCbCrVkDescriptor() const override;
 
   private:
+    TextureView(TextureBase* texture, const UnpackedPtr<TextureViewDescriptor>& descriptor);
     ~TextureView() override;
     void DestroyImpl() override;
     using TextureViewBase::TextureViewBase;