diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs b/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs
index 802b73b8..be392fe0 100644
--- a/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs
@@ -257,7 +257,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
 
             scissors[0] = new Rectangle<int>(0, 0, texture.Width, texture.Height);
 
-            _pipeline.SetRenderTarget(texture.GetImageViewForAttachment(), (uint)texture.Width, (uint)texture.Height, false, texture.VkFormat);
+            _pipeline.SetRenderTarget(texture, (uint)texture.Width, (uint)texture.Height);
             _pipeline.SetRenderTargetColorMasks(colorMasks);
             _pipeline.SetScissors(scissors);
             _pipeline.ClearRenderTargetColor(0, 0, 1, new ColorF(0f, 0f, 0f, 1f));
diff --git a/src/Ryujinx.Graphics.Vulkan/FormatTable.cs b/src/Ryujinx.Graphics.Vulkan/FormatTable.cs
index 5f767df1..a12e3efd 100644
--- a/src/Ryujinx.Graphics.Vulkan/FormatTable.cs
+++ b/src/Ryujinx.Graphics.Vulkan/FormatTable.cs
@@ -1,5 +1,6 @@
 using Ryujinx.Graphics.GAL;
 using System;
+using System.Collections.Generic;
 using VkFormat = Silk.NET.Vulkan.Format;
 
 namespace Ryujinx.Graphics.Vulkan
@@ -7,10 +8,12 @@ namespace Ryujinx.Graphics.Vulkan
     static class FormatTable
     {
         private static readonly VkFormat[] _table;
+        private static readonly Dictionary<VkFormat, Format> _reverseMap;
 
         static FormatTable()
         {
             _table = new VkFormat[Enum.GetNames(typeof(Format)).Length];
+            _reverseMap = new Dictionary<VkFormat, Format>();
 
 #pragma warning disable IDE0055 // Disable formatting
             Add(Format.R8Unorm,             VkFormat.R8Unorm);
@@ -164,6 +167,7 @@ namespace Ryujinx.Graphics.Vulkan
         private static void Add(Format format, VkFormat vkFormat)
         {
             _table[(int)format] = vkFormat;
+            _reverseMap[vkFormat] = format;
         }
 
         public static VkFormat GetFormat(Format format)
@@ -171,6 +175,16 @@ namespace Ryujinx.Graphics.Vulkan
             return _table[(int)format];
         }
 
+        public static Format GetFormat(VkFormat format)
+        {
+            if (!_reverseMap.TryGetValue(format, out Format result))
+            {
+                return Format.B8G8R8A8Unorm;
+            }
+
+            return result;
+        }
+
         public static Format ConvertRgba8SrgbToUnorm(Format format)
         {
             return format switch
diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
index 458a1646..af22f265 100644
--- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
+++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
@@ -12,6 +12,8 @@ namespace Ryujinx.Graphics.Vulkan
         private readonly Auto<DisposableImageView>[] _attachments;
         private readonly TextureView[] _colors;
         private readonly TextureView _depthStencil;
+        private readonly TextureView[] _colorsCanonical;
+        private readonly TextureView _baseAttachment;
         private readonly uint _validColorAttachments;
 
         public uint Width { get; }
@@ -28,25 +30,31 @@ namespace Ryujinx.Graphics.Vulkan
         public bool HasDepthStencil { get; }
         public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0);
 
-        public FramebufferParams(
-            Device device,
-            Auto<DisposableImageView> view,
-            uint width,
-            uint height,
-            uint samples,
-            bool isDepthStencil,
-            VkFormat format)
+        public FramebufferParams(Device device, TextureView view, uint width, uint height)
         {
+            bool isDepthStencil = view.Info.Format.IsDepthOrStencil();
+
             _device = device;
-            _attachments = new[] { view };
+            _attachments = new[] { view.GetImageViewForAttachment() };
             _validColorAttachments = isDepthStencil ? 0u : 1u;
+            _baseAttachment = view;
+
+            if (isDepthStencil)
+            {
+                _depthStencil = view;
+            }
+            else
+            {
+                _colors = new TextureView[] { view };
+                _colorsCanonical = _colors;
+            }
 
             Width = width;
             Height = height;
             Layers = 1;
 
-            AttachmentSamples = new[] { samples };
-            AttachmentFormats = new[] { format };
+            AttachmentSamples = new[] { (uint)view.Info.Samples };
+            AttachmentFormats = new[] { view.VkFormat };
             AttachmentIndices = isDepthStencil ? Array.Empty<int>() : new[] { 0 };
 
             AttachmentsCount = 1;
@@ -64,6 +72,7 @@ namespace Ryujinx.Graphics.Vulkan
 
             _attachments = new Auto<DisposableImageView>[count];
             _colors = new TextureView[colorsCount];
+            _colorsCanonical = colors.Select(color => color is TextureView view && view.Valid ? view : null).ToArray();
 
             AttachmentSamples = new uint[count];
             AttachmentFormats = new VkFormat[count];
@@ -86,6 +95,7 @@ namespace Ryujinx.Graphics.Vulkan
                     _attachments[index] = texture.GetImageViewForAttachment();
                     _colors[index] = texture;
                     _validColorAttachments |= 1u << bindIndex;
+                    _baseAttachment = texture;
 
                     AttachmentSamples[index] = (uint)texture.Info.Samples;
                     AttachmentFormats[index] = texture.VkFormat;
@@ -115,6 +125,7 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 _attachments[count - 1] = dsTexture.GetImageViewForAttachment();
                 _depthStencil = dsTexture;
+                _baseAttachment ??= dsTexture;
 
                 AttachmentSamples[count - 1] = (uint)dsTexture.Info.Samples;
                 AttachmentFormats[count - 1] = dsTexture.VkFormat;
@@ -251,19 +262,11 @@ namespace Ryujinx.Graphics.Vulkan
 
         public void InsertClearBarrier(CommandBufferScoped cbs, int index)
         {
-            if (_colors != null)
-            {
-                int realIndex = Array.IndexOf(AttachmentIndices, index);
-
-                if (realIndex != -1)
-                {
-                    _colors[realIndex].Storage?.InsertReadToWriteBarrier(
-                        cbs,
-                        AccessFlags.ColorAttachmentWriteBit,
-                        PipelineStageFlags.ColorAttachmentOutputBit,
-                        insideRenderPass: true);
-                }
-            }
+            _colorsCanonical?[index]?.Storage?.InsertReadToWriteBarrier(
+               cbs,
+               AccessFlags.ColorAttachmentWriteBit,
+               PipelineStageFlags.ColorAttachmentOutputBit,
+               insideRenderPass: true);
         }
 
         public void InsertClearBarrierDS(CommandBufferScoped cbs)
@@ -274,5 +277,61 @@ namespace Ryujinx.Graphics.Vulkan
                 PipelineStageFlags.LateFragmentTestsBit,
                 insideRenderPass: true);
         }
+
+        public TextureView[] GetAttachmentViews()
+        {
+            var result = new TextureView[_attachments.Length];
+
+            _colors?.CopyTo(result, 0);
+
+            if (_depthStencil != null)
+            {
+                result[^1] = _depthStencil;
+            }
+
+            return result;
+        }
+
+        public RenderPassCacheKey GetRenderPassCacheKey()
+        {
+            return new RenderPassCacheKey(_depthStencil, _colorsCanonical);
+        }
+
+        public void InsertLoadOpBarriers(CommandBufferScoped cbs)
+        {
+            if (_colors != null)
+            {
+                foreach (var color in _colors)
+                {
+                    // If Clear or DontCare were used, this would need to be write bit.
+                    color.Storage?.InsertWriteToReadBarrier(cbs, AccessFlags.ColorAttachmentReadBit, PipelineStageFlags.ColorAttachmentOutputBit);
+                    color.Storage?.SetModification(AccessFlags.ColorAttachmentWriteBit, PipelineStageFlags.ColorAttachmentOutputBit);
+                }
+            }
+
+            if (_depthStencil != null)
+            {
+                _depthStencil.Storage?.InsertWriteToReadBarrier(cbs, AccessFlags.DepthStencilAttachmentReadBit, PipelineStageFlags.EarlyFragmentTestsBit);
+                _depthStencil.Storage?.SetModification(AccessFlags.DepthStencilAttachmentWriteBit, PipelineStageFlags.LateFragmentTestsBit);
+            }
+        }
+
+        public (Auto<DisposableRenderPass> renderPass, Auto<DisposableFramebuffer> framebuffer) GetPassAndFramebuffer(
+            VulkanRenderer gd,
+            Device device,
+            CommandBufferScoped cbs)
+        {
+            return _baseAttachment.GetPassAndFramebuffer(gd, device, cbs, this);
+        }
+
+        public TextureView GetColorView(int index)
+        {
+            return _colorsCanonical[index];
+        }
+
+        public TextureView GetDepthStencilView()
+        {
+            return _depthStencil;
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs b/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs
index ff4eb789..3796e3c5 100644
--- a/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs
+++ b/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Runtime.CompilerServices;
 
 namespace Ryujinx.Graphics.Vulkan
 {
@@ -20,20 +21,29 @@ namespace Ryujinx.Graphics.Vulkan
             public TValue Value;
         }
 
-        private readonly Entry[][] _hashTable = new Entry[TotalBuckets][];
+        private struct Bucket
+        {
+            public int Length;
+            public Entry[] Entries;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            public readonly Span<Entry> AsSpan()
+            {
+                return Entries == null ? Span<Entry>.Empty : Entries.AsSpan(0, Length);
+            }
+        }
+
+        private readonly Bucket[] _hashTable = new Bucket[TotalBuckets];
 
         public IEnumerable<TKey> Keys
         {
             get
             {
-                foreach (Entry[] bucket in _hashTable)
+                foreach (Bucket bucket in _hashTable)
                 {
-                    if (bucket != null)
+                    for (int i = 0; i < bucket.Length; i++)
                     {
-                        foreach (Entry entry in bucket)
-                        {
-                            yield return entry.Key;
-                        }
+                        yield return bucket.Entries[i].Key;
                     }
                 }
             }
@@ -43,14 +53,11 @@ namespace Ryujinx.Graphics.Vulkan
         {
             get
             {
-                foreach (Entry[] bucket in _hashTable)
+                foreach (Bucket bucket in _hashTable)
                 {
-                    if (bucket != null)
+                    for (int i = 0; i < bucket.Length; i++)
                     {
-                        foreach (Entry entry in bucket)
-                        {
-                            yield return entry.Value;
-                        }
+                        yield return bucket.Entries[i].Value;
                     }
                 }
             }
@@ -68,40 +75,64 @@ namespace Ryujinx.Graphics.Vulkan
             int hashCode = key.GetHashCode();
             int bucketIndex = hashCode & TotalBucketsMask;
 
-            var bucket = _hashTable[bucketIndex];
-            if (bucket != null)
+            ref var bucket = ref _hashTable[bucketIndex];
+            if (bucket.Entries != null)
             {
                 int index = bucket.Length;
 
-                Array.Resize(ref _hashTable[bucketIndex], index + 1);
+                if (index >= bucket.Entries.Length)
+                {
+                    Array.Resize(ref bucket.Entries, index + 1);
+                }
 
-                _hashTable[bucketIndex][index] = entry;
+                bucket.Entries[index] = entry;
             }
             else
             {
-                _hashTable[bucketIndex] = new[]
+                bucket.Entries = new[]
                 {
                     entry,
                 };
             }
+
+            bucket.Length++;
+        }
+
+        public bool Remove(ref TKey key)
+        {
+            int hashCode = key.GetHashCode();
+
+            ref var bucket = ref _hashTable[hashCode & TotalBucketsMask];
+            var entries = bucket.AsSpan();
+            for (int i = 0; i < entries.Length; i++)
+            {
+                ref var entry = ref entries[i];
+
+                if (entry.Hash == hashCode && entry.Key.Equals(ref key))
+                {
+                    entries[(i + 1)..].CopyTo(entries[i..]);
+                    bucket.Length--;
+
+                    return true;
+                }
+            }
+
+            return false;
         }
 
         public bool TryGetValue(ref TKey key, out TValue value)
         {
             int hashCode = key.GetHashCode();
 
-            var bucket = _hashTable[hashCode & TotalBucketsMask];
-            if (bucket != null)
+            var entries = _hashTable[hashCode & TotalBucketsMask].AsSpan();
+            for (int i = 0; i < entries.Length; i++)
             {
-                for (int i = 0; i < bucket.Length; i++)
-                {
-                    ref var entry = ref bucket[i];
+                ref var entry = ref entries[i];
 
-                    if (entry.Hash == hashCode && entry.Key.Equals(ref key))
-                    {
-                        value = entry.Value;
-                        return true;
-                    }
+                if (entry.Hash == hashCode && entry.Key.Equals(ref key))
+                {
+                    value = entry.Value;
+                    return true;
                 }
             }
 
diff --git a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs
index ce84f752..c0ded5b3 100644
--- a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs
+++ b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs
@@ -256,17 +256,8 @@ namespace Ryujinx.Graphics.Vulkan
 
             using var cbs = gd.CommandBufferPool.Rent();
 
-            var dstFormat = dst.VkFormat;
-            var dstSamples = dst.Info.Samples;
-
             for (int l = 0; l < levels; l++)
             {
-                int srcWidth = Math.Max(1, src.Width >> l);
-                int srcHeight = Math.Max(1, src.Height >> l);
-
-                int dstWidth = Math.Max(1, dst.Width >> l);
-                int dstHeight = Math.Max(1, dst.Height >> l);
-
                 var mipSrcRegion = new Extents2D(
                     srcRegion.X1 >> l,
                     srcRegion.Y1 >> l,
@@ -290,11 +281,7 @@ namespace Ryujinx.Graphics.Vulkan
                             gd,
                             cbs,
                             srcView,
-                            dst.GetImageViewForAttachment(),
-                            dstWidth,
-                            dstHeight,
-                            dstSamples,
-                            dstFormat,
+                            dstView,
                             mipSrcRegion,
                             mipDstRegion);
                     }
@@ -304,12 +291,7 @@ namespace Ryujinx.Graphics.Vulkan
                             gd,
                             cbs,
                             srcView,
-                            dst.GetImageViewForAttachment(),
-                            dstWidth,
-                            dstHeight,
-                            dstSamples,
-                            dstFormat,
-                            false,
+                            dstView,
                             mipSrcRegion,
                             mipDstRegion,
                             linearFilter,
@@ -367,12 +349,7 @@ namespace Ryujinx.Graphics.Vulkan
                         gd,
                         cbs,
                         srcView,
-                        dstView.GetImageViewForAttachment(),
-                        dstView.Width,
-                        dstView.Height,
-                        dstView.Info.Samples,
-                        dstView.VkFormat,
-                        dstView.Info.Format.IsDepthOrStencil(),
+                        dstView,
                         extents,
                         extents,
                         false);
@@ -394,12 +371,7 @@ namespace Ryujinx.Graphics.Vulkan
             VulkanRenderer gd,
             CommandBufferScoped cbs,
             TextureView src,
-            Auto<DisposableImageView> dst,
-            int dstWidth,
-            int dstHeight,
-            int dstSamples,
-            VkFormat dstFormat,
-            bool dstIsDepthOrStencil,
+            TextureView dst,
             Extents2D srcRegion,
             Extents2D dstRegion,
             bool linearFilter,
@@ -453,6 +425,8 @@ namespace Ryujinx.Graphics.Vulkan
                 0f,
                 1f);
 
+            bool dstIsDepthOrStencil = dst.Info.Format.IsDepthOrStencil();
+
             if (dstIsDepthOrStencil)
             {
                 _pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programDepthBlitMs : _programDepthBlit);
@@ -471,7 +445,10 @@ namespace Ryujinx.Graphics.Vulkan
                 _pipeline.SetProgram(_programColorBlit);
             }
 
-            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, (uint)dstSamples, dstIsDepthOrStencil, dstFormat);
+            int dstWidth = dst.Width;
+            int dstHeight = dst.Height;
+
+            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight);
             _pipeline.SetRenderTargetColorMasks(new uint[] { 0xf });
             _pipeline.SetScissors(stackalloc Rectangle<int>[] { new Rectangle<int>(0, 0, dstWidth, dstHeight) });
 
@@ -496,11 +473,7 @@ namespace Ryujinx.Graphics.Vulkan
             VulkanRenderer gd,
             CommandBufferScoped cbs,
             TextureView src,
-            Auto<DisposableImageView> dst,
-            int dstWidth,
-            int dstHeight,
-            int dstSamples,
-            VkFormat dstFormat,
+            TextureView dst,
             Extents2D srcRegion,
             Extents2D dstRegion)
         {
@@ -548,7 +521,10 @@ namespace Ryujinx.Graphics.Vulkan
                 0f,
                 1f);
 
-            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, (uint)dstSamples, true, dstFormat);
+            int dstWidth = dst.Width;
+            int dstHeight = dst.Height;
+
+            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight);
             _pipeline.SetScissors(stackalloc Rectangle<int>[] { new Rectangle<int>(0, 0, dstWidth, dstHeight) });
             _pipeline.SetViewports(viewports);
             _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
@@ -660,12 +636,11 @@ namespace Ryujinx.Graphics.Vulkan
 
         public void Clear(
             VulkanRenderer gd,
-            Auto<DisposableImageView> dst,
+            TextureView dst,
             ReadOnlySpan<float> clearColor,
             uint componentMask,
             int dstWidth,
             int dstHeight,
-            VkFormat dstFormat,
             ComponentType type,
             Rectangle<int> scissor)
         {
@@ -710,7 +685,7 @@ namespace Ryujinx.Graphics.Vulkan
             }
 
             _pipeline.SetProgram(program);
-            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, false, dstFormat);
+            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight);
             _pipeline.SetRenderTargetColorMasks(new[] { componentMask });
             _pipeline.SetViewports(viewports);
             _pipeline.SetScissors(stackalloc Rectangle<int>[] { scissor });
@@ -721,7 +696,7 @@ namespace Ryujinx.Graphics.Vulkan
 
         public void Clear(
             VulkanRenderer gd,
-            Auto<DisposableImageView> dst,
+            TextureView dst,
             float depthValue,
             bool depthMask,
             int stencilValue,
@@ -757,7 +732,7 @@ namespace Ryujinx.Graphics.Vulkan
                 1f);
 
             _pipeline.SetProgram(_programDepthStencilClear);
-            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, true, dstFormat);
+            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight);
             _pipeline.SetViewports(viewports);
             _pipeline.SetScissors(stackalloc Rectangle<int>[] { scissor });
             _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
@@ -1163,12 +1138,7 @@ namespace Ryujinx.Graphics.Vulkan
                     var srcView = Create2DLayerView(src, srcLayer + z, 0);
                     var dstView = Create2DLayerView(dst, dstLayer + z, 0);
 
-                    _pipeline.SetRenderTarget(
-                        dstView.GetImageViewForAttachment(),
-                        (uint)dst.Width,
-                        (uint)dst.Height,
-                        true,
-                        dst.VkFormat);
+                    _pipeline.SetRenderTarget(dstView, (uint)dst.Width, (uint)dst.Height);
 
                     CopyMSDraw(srcView, aspectFlags, fromMS: true);
 
@@ -1294,13 +1264,7 @@ namespace Ryujinx.Graphics.Vulkan
                     var srcView = Create2DLayerView(src, srcLayer + z, 0);
                     var dstView = Create2DLayerView(dst, dstLayer + z, 0);
 
-                    _pipeline.SetRenderTarget(
-                        dstView.GetImageViewForAttachment(),
-                        (uint)dst.Width,
-                        (uint)dst.Height,
-                        (uint)samples,
-                        true,
-                        dst.VkFormat);
+                    _pipeline.SetRenderTarget(dstView, (uint)dst.Width, (uint)dst.Height);
 
                     CopyMSDraw(srcView, aspectFlags, fromMS: false);
 
@@ -1328,13 +1292,7 @@ namespace Ryujinx.Graphics.Vulkan
                     var dstView = Create2DLayerView(dst, dstLayer + z, 0);
 
                     _pipeline.SetTextureAndSamplerIdentitySwizzle(ShaderStage.Fragment, 0, srcView, null);
-                    _pipeline.SetRenderTarget(
-                        dstView.GetView(format).GetImageViewForAttachment(),
-                        (uint)dst.Width,
-                        (uint)dst.Height,
-                        (uint)samples,
-                        false,
-                        vkFormat);
+                    _pipeline.SetRenderTarget(dstView.GetView(format), (uint)dst.Width, (uint)dst.Height);
 
                     _pipeline.Draw(4, 1, 0, 0);
 
@@ -1471,9 +1429,9 @@ namespace Ryujinx.Graphics.Vulkan
             };
 
             var info = new TextureCreateInfo(
-                from.Info.Width,
-                from.Info.Height,
-                from.Info.Depth,
+                Math.Max(1, from.Info.Width >> level),
+                Math.Max(1, from.Info.Height >> level),
+                1,
                 1,
                 from.Info.Samples,
                 from.Info.BlockWidth,
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 61215b67..3aef1317 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -55,6 +55,7 @@ namespace Ryujinx.Graphics.Vulkan
         protected FramebufferParams FramebufferParams;
         private Auto<DisposableFramebuffer> _framebuffer;
         private Auto<DisposableRenderPass> _renderPass;
+        private RenderPassHolder _nullRenderPass;
         private int _writtenAttachmentCount;
 
         private bool _framebufferUsingColorWriteMask;
@@ -1488,98 +1489,22 @@ namespace Ryujinx.Graphics.Vulkan
 
         protected unsafe void CreateRenderPass()
         {
-            const int MaxAttachments = Constants.MaxRenderTargets + 1;
-
-            AttachmentDescription[] attachmentDescs = null;
-
-            var subpass = new SubpassDescription
-            {
-                PipelineBindPoint = PipelineBindPoint.Graphics,
-            };
-
-            AttachmentReference* attachmentReferences = stackalloc AttachmentReference[MaxAttachments];
-
             var hasFramebuffer = FramebufferParams != null;
 
-            if (hasFramebuffer && FramebufferParams.AttachmentsCount != 0)
-            {
-                attachmentDescs = new AttachmentDescription[FramebufferParams.AttachmentsCount];
-
-                for (int i = 0; i < FramebufferParams.AttachmentsCount; i++)
-                {
-                    attachmentDescs[i] = new AttachmentDescription(
-                        0,
-                        FramebufferParams.AttachmentFormats[i],
-                        TextureStorage.ConvertToSampleCountFlags(Gd.Capabilities.SupportedSampleCounts, FramebufferParams.AttachmentSamples[i]),
-                        AttachmentLoadOp.Load,
-                        AttachmentStoreOp.Store,
-                        AttachmentLoadOp.Load,
-                        AttachmentStoreOp.Store,
-                        ImageLayout.General,
-                        ImageLayout.General);
-                }
-
-                int colorAttachmentsCount = FramebufferParams.ColorAttachmentsCount;
-
-                if (colorAttachmentsCount > MaxAttachments - 1)
-                {
-                    colorAttachmentsCount = MaxAttachments - 1;
-                }
-
-                if (colorAttachmentsCount != 0)
-                {
-                    int maxAttachmentIndex = FramebufferParams.MaxColorAttachmentIndex;
-                    subpass.ColorAttachmentCount = (uint)maxAttachmentIndex + 1;
-                    subpass.PColorAttachments = &attachmentReferences[0];
-
-                    // Fill with VK_ATTACHMENT_UNUSED to cover any gaps.
-                    for (int i = 0; i <= maxAttachmentIndex; i++)
-                    {
-                        subpass.PColorAttachments[i] = new AttachmentReference(Vk.AttachmentUnused, ImageLayout.Undefined);
-                    }
-
-                    for (int i = 0; i < colorAttachmentsCount; i++)
-                    {
-                        int bindIndex = FramebufferParams.AttachmentIndices[i];
-
-                        subpass.PColorAttachments[bindIndex] = new AttachmentReference((uint)i, ImageLayout.General);
-                    }
-                }
-
-                if (FramebufferParams.HasDepthStencil)
-                {
-                    uint dsIndex = (uint)FramebufferParams.AttachmentsCount - 1;
-
-                    subpass.PDepthStencilAttachment = &attachmentReferences[MaxAttachments - 1];
-                    *subpass.PDepthStencilAttachment = new AttachmentReference(dsIndex, ImageLayout.General);
-                }
-            }
-
-            var subpassDependency = PipelineConverter.CreateSubpassDependency();
-
-            fixed (AttachmentDescription* pAttachmentDescs = attachmentDescs)
-            {
-                var renderPassCreateInfo = new RenderPassCreateInfo
-                {
-                    SType = StructureType.RenderPassCreateInfo,
-                    PAttachments = pAttachmentDescs,
-                    AttachmentCount = attachmentDescs != null ? (uint)attachmentDescs.Length : 0,
-                    PSubpasses = &subpass,
-                    SubpassCount = 1,
-                    PDependencies = &subpassDependency,
-                    DependencyCount = 1,
-                };
-
-                Gd.Api.CreateRenderPass(Device, renderPassCreateInfo, null, out var renderPass).ThrowOnError();
-
-                _renderPass?.Dispose();
-                _renderPass = new Auto<DisposableRenderPass>(new DisposableRenderPass(Gd.Api, Device, renderPass));
-            }
-
             EndRenderPass();
 
-            _framebuffer?.Dispose();
-            _framebuffer = hasFramebuffer ? FramebufferParams.Create(Gd.Api, Cbs, _renderPass) : null;
+            if (!hasFramebuffer || FramebufferParams.AttachmentsCount == 0)
+            {
+                // Use the null framebuffer.
+                _nullRenderPass ??= new RenderPassHolder(Gd, Device, new RenderPassCacheKey(), FramebufferParams);
+
+                _renderPass = _nullRenderPass.GetRenderPass();
+                _framebuffer = _nullRenderPass.GetFramebuffer(Gd, Cbs, FramebufferParams);
+            }
+            else
+            {
+                (_renderPass, _framebuffer) = FramebufferParams.GetPassAndFramebuffer(Gd, Device, Cbs);
+            }
         }
 
         protected void SignalStateChange()
@@ -1770,8 +1695,7 @@ namespace Ryujinx.Graphics.Vulkan
         {
             if (disposing)
             {
-                _renderPass?.Dispose();
-                _framebuffer?.Dispose();
+                _nullRenderPass?.Dispose();
                 _newState.Dispose();
                 _descriptorSetUpdater.Dispose();
                 _vertexBufferUpdater.Dispose();
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
index a3e6818f..6c4419cd 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
@@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 // We can't use CmdClearAttachments if not writing all components,
                 // because on Vulkan, the pipeline state does not affect clears.
-                var dstTexture = FramebufferParams.GetAttachment(index);
+                var dstTexture = FramebufferParams.GetColorView(index);
                 if (dstTexture == null)
                 {
                     return;
@@ -71,7 +71,6 @@ namespace Ryujinx.Graphics.Vulkan
                     componentMask,
                     (int)FramebufferParams.Width,
                     (int)FramebufferParams.Height,
-                    FramebufferParams.AttachmentFormats[index],
                     FramebufferParams.GetAttachmentComponentType(index),
                     ClearScissor);
             }
@@ -92,7 +91,7 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 // We can't use CmdClearAttachments if not clearing all (mask is all ones, 0xFF) or none (mask is 0) of the stencil bits,
                 // because on Vulkan, the pipeline state does not affect clears.
-                var dstTexture = FramebufferParams.GetDepthStencilAttachment();
+                var dstTexture = FramebufferParams.GetDepthStencilView();
                 if (dstTexture == null)
                 {
                     return;
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs b/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs
index 0a871a5c..dfbf1901 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs
@@ -9,21 +9,16 @@ namespace Ryujinx.Graphics.Vulkan
         {
         }
 
-        public void SetRenderTarget(Auto<DisposableImageView> view, uint width, uint height, bool isDepthStencil, VkFormat format)
+        public void SetRenderTarget(TextureView view, uint width, uint height)
         {
-            SetRenderTarget(view, width, height, 1u, isDepthStencil, format);
-        }
-
-        public void SetRenderTarget(Auto<DisposableImageView> view, uint width, uint height, uint samples, bool isDepthStencil, VkFormat format)
-        {
-            CreateFramebuffer(view, width, height, samples, isDepthStencil, format);
+            CreateFramebuffer(view, width, height);
             CreateRenderPass();
             SignalStateChange();
         }
 
-        private void CreateFramebuffer(Auto<DisposableImageView> view, uint width, uint height, uint samples, bool isDepthStencil, VkFormat format)
+        private void CreateFramebuffer(TextureView view, uint width, uint height)
         {
-            FramebufferParams = new FramebufferParams(Device, view, width, height, samples, isDepthStencil, format);
+            FramebufferParams = new FramebufferParams(Device, view, width, height);
             UpdatePipelineAttachmentFormats();
         }
 
diff --git a/src/Ryujinx.Graphics.Vulkan/RenderPassCacheKey.cs b/src/Ryujinx.Graphics.Vulkan/RenderPassCacheKey.cs
new file mode 100644
index 00000000..7c57b8fe
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/RenderPassCacheKey.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Linq;
+
+namespace Ryujinx.Graphics.Vulkan
+{
+    internal readonly struct RenderPassCacheKey : IRefEquatable<RenderPassCacheKey>
+    {
+        private readonly TextureView _depthStencil;
+        private readonly TextureView[] _colors;
+
+        public RenderPassCacheKey(TextureView depthStencil, TextureView[] colors)
+        {
+            _depthStencil = depthStencil;
+            _colors = colors;
+        }
+
+        public override int GetHashCode()
+        {
+            HashCode hc = new();
+
+            hc.Add(_depthStencil);
+
+            if (_colors != null)
+            {
+                foreach (var color in _colors)
+                {
+                    hc.Add(color);
+                }
+            }
+
+            return hc.ToHashCode();
+        }
+
+        public bool Equals(ref RenderPassCacheKey other)
+        {
+            bool colorsNull = _colors == null;
+            bool otherNull = other._colors == null;
+            return other._depthStencil == _depthStencil &&
+                colorsNull == otherNull &&
+                (colorsNull || other._colors.SequenceEqual(_colors));
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/RenderPassHolder.cs b/src/Ryujinx.Graphics.Vulkan/RenderPassHolder.cs
new file mode 100644
index 00000000..3d883b2d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/RenderPassHolder.cs
@@ -0,0 +1,180 @@
+using Silk.NET.Vulkan;
+using System;
+
+namespace Ryujinx.Graphics.Vulkan
+{
+    internal class RenderPassHolder
+    {
+        private readonly struct FramebufferCacheKey : IRefEquatable<FramebufferCacheKey>
+        {
+            private readonly uint _width;
+            private readonly uint _height;
+            private readonly uint _layers;
+
+            public FramebufferCacheKey(uint width, uint height, uint layers)
+            {
+                _width = width;
+                _height = height;
+                _layers = layers;
+            }
+
+            public override int GetHashCode()
+            {
+                return HashCode.Combine(_width, _height, _layers);
+            }
+
+            public bool Equals(ref FramebufferCacheKey other)
+            {
+                return other._width == _width && other._height == _height && other._layers == _layers;
+            }
+        }
+
+        private readonly TextureView[] _textures;
+        private readonly Auto<DisposableRenderPass> _renderPass;
+        private readonly HashTableSlim<FramebufferCacheKey, Auto<DisposableFramebuffer>> _framebuffers;
+        private readonly RenderPassCacheKey _key;
+
+        public unsafe RenderPassHolder(VulkanRenderer gd, Device device, RenderPassCacheKey key, FramebufferParams fb)
+        {
+            // Create render pass using framebuffer params.
+
+            const int MaxAttachments = Constants.MaxRenderTargets + 1;
+
+            AttachmentDescription[] attachmentDescs = null;
+
+            var subpass = new SubpassDescription
+            {
+                PipelineBindPoint = PipelineBindPoint.Graphics,
+            };
+
+            AttachmentReference* attachmentReferences = stackalloc AttachmentReference[MaxAttachments];
+
+            var hasFramebuffer = fb != null;
+
+            if (hasFramebuffer && fb.AttachmentsCount != 0)
+            {
+                attachmentDescs = new AttachmentDescription[fb.AttachmentsCount];
+
+                for (int i = 0; i < fb.AttachmentsCount; i++)
+                {
+                    attachmentDescs[i] = new AttachmentDescription(
+                        0,
+                        fb.AttachmentFormats[i],
+                        TextureStorage.ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, fb.AttachmentSamples[i]),
+                        AttachmentLoadOp.Load,
+                        AttachmentStoreOp.Store,
+                        AttachmentLoadOp.Load,
+                        AttachmentStoreOp.Store,
+                        ImageLayout.General,
+                        ImageLayout.General);
+                }
+
+                int colorAttachmentsCount = fb.ColorAttachmentsCount;
+
+                if (colorAttachmentsCount > MaxAttachments - 1)
+                {
+                    colorAttachmentsCount = MaxAttachments - 1;
+                }
+
+                if (colorAttachmentsCount != 0)
+                {
+                    int maxAttachmentIndex = fb.MaxColorAttachmentIndex;
+                    subpass.ColorAttachmentCount = (uint)maxAttachmentIndex + 1;
+                    subpass.PColorAttachments = &attachmentReferences[0];
+
+                    // Fill with VK_ATTACHMENT_UNUSED to cover any gaps.
+                    for (int i = 0; i <= maxAttachmentIndex; i++)
+                    {
+                        subpass.PColorAttachments[i] = new AttachmentReference(Vk.AttachmentUnused, ImageLayout.Undefined);
+                    }
+
+                    for (int i = 0; i < colorAttachmentsCount; i++)
+                    {
+                        int bindIndex = fb.AttachmentIndices[i];
+
+                        subpass.PColorAttachments[bindIndex] = new AttachmentReference((uint)i, ImageLayout.General);
+                    }
+                }
+
+                if (fb.HasDepthStencil)
+                {
+                    uint dsIndex = (uint)fb.AttachmentsCount - 1;
+
+                    subpass.PDepthStencilAttachment = &attachmentReferences[MaxAttachments - 1];
+                    *subpass.PDepthStencilAttachment = new AttachmentReference(dsIndex, ImageLayout.General);
+                }
+            }
+
+            var subpassDependency = PipelineConverter.CreateSubpassDependency();
+
+            fixed (AttachmentDescription* pAttachmentDescs = attachmentDescs)
+            {
+                var renderPassCreateInfo = new RenderPassCreateInfo
+                {
+                    SType = StructureType.RenderPassCreateInfo,
+                    PAttachments = pAttachmentDescs,
+                    AttachmentCount = attachmentDescs != null ? (uint)attachmentDescs.Length : 0,
+                    PSubpasses = &subpass,
+                    SubpassCount = 1,
+                    PDependencies = &subpassDependency,
+                    DependencyCount = 1,
+                };
+
+                gd.Api.CreateRenderPass(device, renderPassCreateInfo, null, out var renderPass).ThrowOnError();
+
+                _renderPass?.Dispose();
+                _renderPass = new Auto<DisposableRenderPass>(new DisposableRenderPass(gd.Api, device, renderPass));
+            }
+
+            _framebuffers = new HashTableSlim<FramebufferCacheKey, Auto<DisposableFramebuffer>>();
+
+            // Register this render pass with all render target views.
+
+            var textures = fb.GetAttachmentViews();
+
+            foreach (var texture in textures)
+            {
+                texture.AddRenderPass(key, this);
+            }
+
+            _textures = textures;
+            _key = key;
+        }
+
+        public Auto<DisposableFramebuffer> GetFramebuffer(VulkanRenderer gd, CommandBufferScoped cbs, FramebufferParams fb)
+        {
+            var key = new FramebufferCacheKey(fb.Width, fb.Height, fb.Layers);
+
+            if (!_framebuffers.TryGetValue(ref key, out Auto<DisposableFramebuffer> result))
+            {
+                result = fb.Create(gd.Api, cbs, _renderPass);
+
+                _framebuffers.Add(ref key, result);
+            }
+
+            return result;
+        }
+
+        public Auto<DisposableRenderPass> GetRenderPass()
+        {
+            return _renderPass;
+        }
+
+        public void Dispose()
+        {
+            // Dispose all framebuffers
+
+            foreach (var fb in _framebuffers.Values)
+            {
+                fb.Dispose();
+            }
+
+            // Notify all texture views that this render pass has been disposed.
+
+            foreach (var texture in _textures)
+            {
+                texture.RemoveRenderPass(_key);
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
index f5b80f94..393db261 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
@@ -3,6 +3,7 @@ using Ryujinx.Graphics.GAL;
 using Silk.NET.Vulkan;
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Format = Ryujinx.Graphics.GAL.Format;
 using VkBuffer = Silk.NET.Vulkan.Buffer;
 using VkFormat = Silk.NET.Vulkan.Format;
@@ -23,6 +24,8 @@ namespace Ryujinx.Graphics.Vulkan
 
         private readonly TextureCreateInfo _info;
 
+        private HashTableSlim<RenderPassCacheKey, RenderPassHolder> _renderPasses;
+
         public TextureCreateInfo Info => _info;
 
         public TextureStorage Storage { get; }
@@ -158,6 +161,26 @@ namespace Ryujinx.Graphics.Vulkan
             Valid = true;
         }
 
+        /// <summary>
+        /// Create a texture view for an existing swapchain image view.
+        /// Does not set storage, so only appropriate for swapchain use.
+        /// </summary>
+        /// <remarks>Do not use this for normal textures, and make sure uses do not try to read storage.</remarks>
+        public TextureView(VulkanRenderer gd, Device device, DisposableImageView view, TextureCreateInfo info, VkFormat format)
+        {
+            _gd = gd;
+            _device = device;
+
+            _imageView = new Auto<DisposableImageView>(view);
+            _imageViewDraw = _imageView;
+            _imageViewIdentity = _imageView;
+            _info = info;
+
+            VkFormat = format;
+
+            Valid = true;
+        }
+
         public Auto<DisposableImage> GetImage()
         {
             return Storage.GetImage();
@@ -939,6 +962,34 @@ namespace Ryujinx.Graphics.Vulkan
             throw new NotImplementedException();
         }
 
+        public (Auto<DisposableRenderPass> renderPass, Auto<DisposableFramebuffer> framebuffer) GetPassAndFramebuffer(
+            VulkanRenderer gd,
+            Device device,
+            CommandBufferScoped cbs,
+            FramebufferParams fb)
+        {
+            var key = fb.GetRenderPassCacheKey();
+
+            if (_renderPasses == null || !_renderPasses.TryGetValue(ref key, out RenderPassHolder rpHolder))
+            {
+                rpHolder = new RenderPassHolder(gd, device, key, fb);
+            }
+
+            return (rpHolder.GetRenderPass(), rpHolder.GetFramebuffer(gd, cbs, fb));
+        }
+
+        public void AddRenderPass(RenderPassCacheKey key, RenderPassHolder renderPass)
+        {
+            _renderPasses ??= new HashTableSlim<RenderPassCacheKey, RenderPassHolder>();
+
+            _renderPasses.Add(ref key, renderPass);
+        }
+
+        public void RemoveRenderPass(RenderPassCacheKey key)
+        {
+            _renderPasses.Remove(ref key);
+        }
+
         protected virtual void Dispose(bool disposing)
         {
             if (disposing)
@@ -948,15 +999,29 @@ namespace Ryujinx.Graphics.Vulkan
                 if (_gd.Textures.Remove(this))
                 {
                     _imageView.Dispose();
-                    _imageViewIdentity.Dispose();
                     _imageView2dArray?.Dispose();
 
+                    if (_imageViewIdentity != _imageView)
+                    {
+                        _imageViewIdentity.Dispose();
+                    }
+
                     if (_imageViewDraw != _imageViewIdentity)
                     {
                         _imageViewDraw.Dispose();
                     }
 
                     Storage.DecrementViewsCount();
+
+                    if (_renderPasses != null)
+                    {
+                        var renderPasses = _renderPasses.Values.ToArray();
+
+                        foreach (var pass in renderPasses)
+                        {
+                            pass.Dispose();
+                        }
+                    }
                 }
             }
         }
diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs
index 2c5764a9..5ddb6eed 100644
--- a/src/Ryujinx.Graphics.Vulkan/Window.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Window.cs
@@ -20,7 +20,7 @@ namespace Ryujinx.Graphics.Vulkan
         private SwapchainKHR _swapchain;
 
         private Image[] _swapchainImages;
-        private Auto<DisposableImageView>[] _swapchainImageViews;
+        private TextureView[] _swapchainImageViews;
 
         private Semaphore[] _imageAvailableSemaphores;
         private Semaphore[] _renderFinishedSemaphores;
@@ -143,6 +143,23 @@ namespace Ryujinx.Graphics.Vulkan
                 Clipped = true,
             };
 
+            var textureCreateInfo = new TextureCreateInfo(
+                _width,
+                _height,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                FormatTable.GetFormat(surfaceFormat.Format),
+                DepthStencilMode.Depth,
+                Target.Texture2D,
+                SwizzleComponent.Red,
+                SwizzleComponent.Green,
+                SwizzleComponent.Blue,
+                SwizzleComponent.Alpha);
+
             _gd.SwapchainApi.CreateSwapchain(_device, swapchainCreateInfo, null, out _swapchain).ThrowOnError();
 
             _gd.SwapchainApi.GetSwapchainImages(_device, _swapchain, &imageCount, null);
@@ -154,11 +171,11 @@ namespace Ryujinx.Graphics.Vulkan
                 _gd.SwapchainApi.GetSwapchainImages(_device, _swapchain, &imageCount, pSwapchainImages);
             }
 
-            _swapchainImageViews = new Auto<DisposableImageView>[imageCount];
+            _swapchainImageViews = new TextureView[imageCount];
 
             for (int i = 0; i < _swapchainImageViews.Length; i++)
             {
-                _swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format);
+                _swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format, textureCreateInfo);
             }
 
             var semaphoreCreateInfo = new SemaphoreCreateInfo
@@ -181,7 +198,7 @@ namespace Ryujinx.Graphics.Vulkan
             }
         }
 
-        private unsafe Auto<DisposableImageView> CreateSwapchainImageView(Image swapchainImage, VkFormat format)
+        private unsafe TextureView CreateSwapchainImageView(Image swapchainImage, VkFormat format, TextureCreateInfo info)
         {
             var componentMapping = new ComponentMapping(
                 ComponentSwizzle.R,
@@ -204,7 +221,8 @@ namespace Ryujinx.Graphics.Vulkan
             };
 
             _gd.Api.CreateImageView(_device, imageCreateInfo, null, out var imageView).ThrowOnError();
-            return new Auto<DisposableImageView>(new DisposableImageView(_gd.Api, _device, imageView));
+
+            return new TextureView(_gd, _device, new DisposableImageView(_gd.Api, _device, imageView), info, format);
         }
 
         private static SurfaceFormatKHR ChooseSwapSurfaceFormat(SurfaceFormatKHR[] availableFormats, bool colorSpacePassthroughEnabled)
@@ -406,7 +424,7 @@ namespace Ryujinx.Graphics.Vulkan
                 _scalingFilter.Run(
                     view,
                     cbs,
-                    _swapchainImageViews[nextImage],
+                    _swapchainImageViews[nextImage].GetImageViewForAttachment(),
                     _format,
                     _width,
                     _height,
@@ -421,11 +439,6 @@ namespace Ryujinx.Graphics.Vulkan
                     cbs,
                     view,
                     _swapchainImageViews[nextImage],
-                    _width,
-                    _height,
-                    1,
-                    _format,
-                    false,
                     new Extents2D(srcX0, srcY0, srcX1, srcY1),
                     new Extents2D(dstX0, dstY1, dstX1, dstY0),
                     _isLinear,