Add TextureView caching.

Built off a suggestion by lokokung@
(https://dawn-review.googlesource.com/c/dawn/+/244496),
adds TextureView caching to the Dawn front end.
Caching only applies to Textures created by API
calls, not those created internally (ie:
temporary textures created for workarounds.)

Bug: 416088623
Change-Id: I9cb94e8b24ac25485febb1b3c442a4c84fcfb20e
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/245014
Auto-Submit: Brandon Jones <bajones@chromium.org>
Commit-Queue: Brandon Jones <bajones@chromium.org>
Reviewed-by: Loko Kung <lokokung@google.com>
diff --git a/src/dawn/common/BUILD.gn b/src/dawn/common/BUILD.gn
index 8c0d76e..a2fa395 100644
--- a/src/dawn/common/BUILD.gn
+++ b/src/dawn/common/BUILD.gn
@@ -284,6 +284,7 @@
       "GPUInfo.h",
       "HashUtils.h",
       "IOKitRef.h",
+      "LRUCache.h",
       "LinkedList.h",
       "Log.cpp",
       "Log.h",
diff --git a/src/dawn/common/CMakeLists.txt b/src/dawn/common/CMakeLists.txt
index 7f723d5..bbf2ccd 100644
--- a/src/dawn/common/CMakeLists.txt
+++ b/src/dawn/common/CMakeLists.txt
@@ -69,6 +69,7 @@
     "ityp_vector.h"
     "LinkedList.h"
     "Log.h"
+    "LRUCache.h"
     "MatchVariant.h"
     "Math.h"
     "Mutex.h"
diff --git a/src/dawn/common/LRUCache.h b/src/dawn/common/LRUCache.h
new file mode 100644
index 0000000..a57c6ac
--- /dev/null
+++ b/src/dawn/common/LRUCache.h
@@ -0,0 +1,161 @@
+// 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_COMMON_LRUCACHE_H_
+#define SRC_DAWN_COMMON_LRUCACHE_H_
+
+#include <array>
+#include <list>
+#include <utility>
+
+#include "absl/container/flat_hash_map.h"
+#include "dawn/common/MutexProtected.h"
+#include "dawn/common/Result.h"
+
+namespace dawn {
+
+// LRUCache is a Least-Recently-Used cache that retains the most recently queried values (up to
+// mCapacity), and evicts older items from the cache as new values are created.
+// Example usage:
+//  class CachedValue {
+//    public:
+//      CachedValue(uint32_t a, bool b);
+//  };
+//  struct CachedValueKey {
+//    uint32_t a;
+//    bool b;
+//  };
+//
+//  struct CachedValueCacheFuncs {
+//    size_t operator()(const CachedValueCacheFuncs& key) const {
+//      size_t hash = Hash(key.a);
+//      HashCombine(&hash, key.b);
+//      return hash;
+//    }
+//    bool operator()(const CachedValueCacheFuncs& a, const CachedValueCacheFuncs& b) const {
+//      return a.a == b.a && a.b == b.b;
+//    }
+//  };
+//
+//  class CacheUser {
+//    public:
+//      static const kCacheCapacity = 32;
+//      CacheUser() : mCache(kCacheCapacity) {}
+//
+//      Ref<CachedValue> GetValue(const CachedValueKey& valueKey) {
+//        return mCache.GetOrCreate(valueKey, [](const CachedValueKey& key) -> {
+//          return Ref<CachedValue>(new CachedValue(key.a, key.b));
+//        });
+//      }
+//
+//    private:
+//      using ValueCache = LRUCache<CachedValueKey, CachedValue, ErrorData, CachedValueCacheFuncs>;
+//      ValueCache mCache;
+//  };
+
+template <typename Key, typename Value, typename Error, typename CacheFuncs>
+class LRUCache {
+  public:
+    explicit LRUCache(size_t capacity) : mCapacity(capacity) {}
+    virtual ~LRUCache() = default;
+
+    template <typename CreateFn>
+    Result<Value, Error> GetOrCreate(Key& key, CreateFn createFn) {
+        // A capacity of 0 means caching is disabled, so just return the result
+        // of the createFn directly.
+        if (mCapacity == 0) {
+            auto result = createFn(key);
+            if (result.IsError()) [[unlikely]] {
+                return Result<Value, Error>(result.AcquireError());
+            }
+            auto value = result.AcquireSuccess();
+            // If caching is disabled treat every value as if it is immediately evicted from the
+            // cache on creation.
+            EvictedFromCache(value);
+            return value;
+        }
+
+        return mCache.Use([&](auto cache) -> Result<Value, Error> {
+            // Try to lookup |Key| to see if we have a cache hit.
+            auto it = cache->map.find(key);
+            if (it != cache->map.end()) {
+                // Using iterators 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)
+                cache->list.splice(cache->list.begin(), cache->list, it->second);
+                return it->second->second;
+            }
+
+            // Otherwise, we need to try to create the entry.
+            Result<Value, Error> result = createFn(key);
+            if (result.IsError()) [[unlikely]] {
+                return Result<Value, Error>(result.AcquireError());
+            }
+            auto value = result.AcquireSuccess();
+            cache->list.emplace_front(key, value);
+            cache->map.emplace(key, cache->list.begin());
+
+            // If the LRU has exceeded it's capacity, remove the oldest entry.
+            if (cache->list.size() > mCapacity) {
+                auto back = cache->list.back();
+                EvictedFromCache(back.second);
+                cache->map.erase(back.first);
+                cache->list.pop_back();
+            }
+
+            return value;
+        });
+    }
+
+    void Clear() {
+        mCache.Use([](auto cache) {
+            cache->map.clear();
+            cache->list.clear();
+        });
+    }
+
+    // Override if values need to be deleted or otherwise handled as they are evicted from the
+    // cache. Useful for values that are not Ref<> values.
+    virtual void EvictedFromCache(const Value& value) {}
+
+  protected:
+    const size_t mCapacity;
+
+    using RecentList = std::list<std::pair<Key, Value>>;
+    using Map = absl::flat_hash_map<Key, typename RecentList::iterator, CacheFuncs, CacheFuncs>;
+    struct Cache {
+        RecentList list;
+        Map map;
+    };
+    MutexProtected<Cache> mCache;
+};
+
+}  // namespace dawn
+
+#endif  // SRC_DAWN_COMMON_LRUCACHE_H_
diff --git a/src/dawn/common/RefCountedWithExternalCount.h b/src/dawn/common/RefCountedWithExternalCount.h
index 149fccc..86bd70b 100644
--- a/src/dawn/common/RefCountedWithExternalCount.h
+++ b/src/dawn/common/RefCountedWithExternalCount.h
@@ -36,9 +36,12 @@
 // refcount for calls to APIAddRef/APIRelease (refs added/removed by the application).
 // The external refcount starts at 0, and the total refcount starts at 1 - i.e. the first
 // ref isn't an external ref.
-// When the external refcount drops to zero, WillDropLastExternalRef is called. and it can be called
+// When the external refcount is incremented from 0 to 1, WillAddFirstExternalRef is called, and it
+// can be called more than once.
+// When the external refcount drops to zero, WillDropLastExternalRef is called, and it can be called
 // more than once.
-// The derived class must override the behavior of WillDropLastExternalRef.
+// The derived class must override the behavior of WillDropLastExternalRef, overriding
+// WillAddFirstExternalRef is optional.
 template <typename T>
 class RefCountedWithExternalCount : public T {
   public:
@@ -58,13 +61,18 @@
         T::APIRelease();
     }
 
-    void IncrementExternalRefCount() { mExternalRefCount.Increment(); }
+    void IncrementExternalRefCount() {
+        if (mExternalRefCount.Increment()) {
+            WillAddFirstExternalRef();
+        }
+    }
 
     uint64_t GetExternalRefCountForTesting() const {
         return mExternalRefCount.GetValueForTesting();
     }
 
   private:
+    virtual void WillAddFirstExternalRef() {}
     virtual void WillDropLastExternalRef() = 0;
 
     RefCount mExternalRefCount{/*initCount=*/0, /*payload=*/0};
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 6ab35e6..2cd37d1 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -2256,7 +2256,11 @@
     } else {
         descriptor = Unpack(&desc);
     }
-    return CreateTextureViewImpl(texture, descriptor);
+
+    return texture->GetOrCreateViewFromCache(
+        descriptor, [&](TextureViewQuery&) -> ResultOrError<Ref<TextureViewBase>> {
+            return CreateTextureViewImpl(texture, descriptor);
+        });
 }
 
 // Other implementation details
diff --git a/src/dawn/native/Surface.cpp b/src/dawn/native/Surface.cpp
index d0b30ae..cf39927 100644
--- a/src/dawn/native/Surface.cpp
+++ b/src/dawn/native/Surface.cpp
@@ -608,6 +608,7 @@
 
 void Surface::APIGetCurrentTexture(SurfaceTexture* surfaceTexture) const {
     MaybeError maybeError = GetCurrentTexture(surfaceTexture);
+
     if (!GetCurrentDevice()) {
         [[maybe_unused]] bool error = mInstance->ConsumedError(std::move(maybeError));
     } else {
diff --git a/src/dawn/native/SwapChain.cpp b/src/dawn/native/SwapChain.cpp
index 9af635f..7660eba 100644
--- a/src/dawn/native/SwapChain.cpp
+++ b/src/dawn/native/SwapChain.cpp
@@ -129,7 +129,8 @@
     DAWN_ASSERT(mCurrentTextureInfo.texture->GetViewFormats() == ComputeViewFormatSet());
 
     // Calling GetCurrentTexture always returns a new reference.
-    surfaceTexture.texture = Ref<TextureBase>(mCurrentTextureInfo.texture).Detach();
+    auto texture = mCurrentTextureInfo.texture;
+    surfaceTexture.texture = ReturnToAPI(std::move(texture));
     return surfaceTexture;
 }
 
diff --git a/src/dawn/native/Texture.cpp b/src/dawn/native/Texture.cpp
index ca093b3..29485bd 100644
--- a/src/dawn/native/Texture.cpp
+++ b/src/dawn/native/Texture.cpp
@@ -33,6 +33,7 @@
 #include "absl/strings/str_format.h"
 #include "dawn/common/Assert.h"
 #include "dawn/common/Constants.h"
+#include "dawn/common/HashUtils.h"
 #include "dawn/common/Math.h"
 #include "dawn/native/Adapter.h"
 #include "dawn/native/BlitBufferToTexture.h"
@@ -925,7 +926,7 @@
 TextureBase::TextureState::TextureState() : hasAccess(true), destroyed(false) {}
 
 TextureBase::TextureBase(DeviceBase* device, const UnpackedPtr<TextureDescriptor>& descriptor)
-    : SharedResource(device, descriptor->label),
+    : RefCountedWithExternalCount<SharedResource>(device, descriptor->label),
       mDimension(descriptor->dimension),
       mCompatibilityTextureBindingViewDimension(
           ResolveDefaultCompatiblityTextureBindingViewDimension(device, descriptor)),
@@ -965,7 +966,7 @@
 TextureBase::TextureBase(DeviceBase* device,
                          const TextureDescriptor* descriptor,
                          ObjectBase::ErrorTag tag)
-    : SharedResource(device, tag, descriptor->label),
+    : RefCountedWithExternalCount<SharedResource>(device, tag, descriptor->label),
       mDimension(descriptor->dimension),
       mFormat(kUnusedFormat),
       mBaseSize(descriptor->size),
@@ -987,6 +988,9 @@
     //   other threads using the texture since there are no other live refs.
     mState.destroyed = true;
 
+    // Drop all the cache references to TextureViews.
+    mTextureViewCache = nullptr;
+
     // Destroy all of the views associated with the texture as well.
     mTextureViews.Destroy();
 }
@@ -1012,6 +1016,21 @@
     }
 }
 
+void TextureBase::WillAddFirstExternalRef() {
+    // Only enable the view cache once an external reference has been added. This prevents textures
+    // created for internal uses, such as workarounds, from being kept alive by the views in the
+    // cache.
+    if (!IsError()) {
+        mTextureViewCache = std::make_unique<TextureViewCache>(kDefaultTextureViewCacheCapacity);
+    }
+}
+
+void TextureBase::WillDropLastExternalRef() {
+    // Drop all the additional references to TextureViews that we were holding as a part of the
+    // cache.
+    mTextureViewCache = nullptr;
+}
+
 std::string TextureBase::GetSizeLabel() const {
     if (mDimension == wgpu::TextureDimension::e1D) {
         return absl::StrFormat("%d px", mBaseSize.width);
@@ -1433,6 +1452,36 @@
     return mUsage;
 }
 
+// TextureViewQuery
+
+TextureViewQuery::TextureViewQuery(const UnpackedPtr<TextureViewDescriptor>& desc) {
+    // TextureViewDescriptor fields
+    format = desc->format;
+    dimension = desc->dimension;
+    baseMipLevel = desc->baseMipLevel;
+    mipLevelCount = desc->mipLevelCount;
+    baseArrayLayer = desc->baseArrayLayer;
+    arrayLayerCount = desc->arrayLayerCount;
+    aspect = desc->aspect;
+    usage = desc->usage;
+}
+
+// TextureViewCacheFuncs
+
+size_t TextureViewCacheFuncs::operator()(const TextureViewQuery& desc) const {
+    size_t hash = Hash(desc.format);
+
+    HashCombine(&hash, desc.dimension, desc.aspect, desc.usage);
+    HashCombine(&hash, desc.baseMipLevel, desc.mipLevelCount, desc.baseArrayLayer,
+                desc.arrayLayerCount);
+
+    return hash;
+}
+
+bool TextureViewCacheFuncs::operator()(const TextureViewQuery& a, const TextureViewQuery& b) const {
+    return a == b;
+}
+
 // TextureViewBase
 
 TextureViewBase::TextureViewBase(TextureBase* texture,
diff --git a/src/dawn/native/Texture.h b/src/dawn/native/Texture.h
index 19b1d7e..3743655 100644
--- a/src/dawn/native/Texture.h
+++ b/src/dawn/native/Texture.h
@@ -28,9 +28,12 @@
 #ifndef SRC_DAWN_NATIVE_TEXTURE_H_
 #define SRC_DAWN_NATIVE_TEXTURE_H_
 
+#include <memory>
 #include <string>
 #include <vector>
 
+#include "dawn/common/LRUCache.h"
+#include "dawn/common/RefCountedWithExternalCount.h"
 #include "dawn/common/WeakRef.h"
 #include "dawn/common/ityp_array.h"
 #include "dawn/common/ityp_bitset.h"
@@ -84,7 +87,31 @@
     kShaderTextureUsages | kResolveTextureLoadAndStoreUsages |
     wgpu::TextureUsage::TransientAttachment | wgpu::TextureUsage::StorageAttachment;
 
-class TextureBase : public SharedResource {
+// A flattened version of TextureViewDescriptor used to query the texture view cache.
+struct TextureViewQuery {
+    explicit TextureViewQuery(const UnpackedPtr<TextureViewDescriptor>& desc);
+    bool operator==(const TextureViewQuery& b) const = default;
+
+    // TextureViewDescriptor fields (label ignored)
+    wgpu::TextureFormat format;
+    wgpu::TextureViewDimension dimension;
+    uint32_t baseMipLevel;
+    uint32_t mipLevelCount;
+    uint32_t baseArrayLayer;
+    uint32_t arrayLayerCount;
+    wgpu::TextureAspect aspect;
+    wgpu::TextureUsage usage;
+
+    // Update with fields from relevant chained structs as they are added.
+};
+
+static const size_t kDefaultTextureViewCacheCapacity = 4;
+struct TextureViewCacheFuncs {
+    size_t operator()(const TextureViewQuery& desc) const;
+    bool operator()(const TextureViewQuery& a, const TextureViewQuery& b) const;
+};
+
+class TextureBase : public RefCountedWithExternalCount<SharedResource> {
   public:
     enum class ClearValue { Zero, NonZero };
 
@@ -164,6 +191,11 @@
 
     uint64_t ComputeEstimatedByteSize() const;
 
+    template <typename CreateFn>
+    ResultOrError<Ref<TextureViewBase>> GetOrCreateViewFromCache(
+        const UnpackedPtr<TextureViewDescriptor>& desc,
+        CreateFn createFn);
+
     // Dawn API
     TextureViewBase* APICreateView(const TextureViewDescriptor* descriptor = nullptr);
     TextureViewBase* APICreateErrorView(const TextureViewDescriptor* descriptor = nullptr);
@@ -201,6 +233,9 @@
 
     std::string GetSizeLabel() const;
 
+    void WillAddFirstExternalRef() override;
+    void WillDropLastExternalRef() override;
+
     wgpu::TextureDimension mDimension;
     // Only used for compatibility mode
     wgpu::TextureViewDimension mCompatibilityTextureBindingViewDimension =
@@ -219,10 +254,27 @@
     // is destroyed.
     ApiObjectList mTextureViews;
 
+    using TextureViewCache =
+        LRUCache<TextureViewQuery, Ref<TextureViewBase>, ErrorData, TextureViewCacheFuncs>;
+    std::unique_ptr<TextureViewCache> mTextureViewCache;
+
     // TODO(crbug.com/dawn/845): Use a more optimized data structure to save space
     std::vector<bool> mIsSubresourceContentInitializedAtIndex;
 };
 
+template <typename CreateFn>
+ResultOrError<Ref<TextureViewBase>> TextureBase::GetOrCreateViewFromCache(
+    const UnpackedPtr<TextureViewDescriptor>& desc,
+    CreateFn createFn) {
+    TextureViewQuery query(desc);
+
+    if (!mTextureViewCache) {
+        return createFn(query);
+    }
+
+    return mTextureViewCache->GetOrCreate(query, createFn);
+}
+
 class TextureViewBase : public ApiObjectBase {
   public:
     TextureViewBase(TextureBase* texture, const UnpackedPtr<TextureViewDescriptor>& descriptor);