diff --git a/Ryujinx.Cpu/MemoryManager.cs b/Ryujinx.Cpu/MemoryManager.cs
index 75ecca1ca..abbeee5f8 100644
--- a/Ryujinx.Cpu/MemoryManager.cs
+++ b/Ryujinx.Cpu/MemoryManager.cs
@@ -145,10 +145,36 @@ namespace Ryujinx.Cpu
                 return;
             }
 
+            MarkRegionAsModified(va, (ulong)data.Length);
+
+            WriteImpl(va, data);
+        }
+
+        /// <summary>
+        /// Writes data to CPU mapped memory, without tracking.
+        /// </summary>
+        /// <param name="va">Virtual address to write the data into</param>
+        /// <param name="data">Data to be written</param>
+        public void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
+        {
+            if (data.Length == 0)
+            {
+                return;
+            }
+
+            WriteImpl(va, data);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        /// <summary>
+        /// Writes data to CPU mapped memory.
+        /// </summary>
+        /// <param name="va">Virtual address to write the data into</param>
+        /// <param name="data">Data to be written</param>
+        private void WriteImpl(ulong va, ReadOnlySpan<byte> data)
+        {
             try
             {
-                MarkRegionAsModified(va, (ulong)data.Length);
-
                 if (IsContiguousAndMapped(va, data.Length))
                 {
                     data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length));
diff --git a/Ryujinx.Graphics.GAL/IRenderer.cs b/Ryujinx.Graphics.GAL/IRenderer.cs
index fec8d3bea..73fafe495 100644
--- a/Ryujinx.Graphics.GAL/IRenderer.cs
+++ b/Ryujinx.Graphics.GAL/IRenderer.cs
@@ -29,6 +29,8 @@ namespace Ryujinx.Graphics.GAL
 
         void UpdateCounters();
 
+        void PreFrame();
+
         ICounterEvent ReportCounter(CounterType type, EventHandler<ulong> resultHandler);
 
         void ResetCounter(CounterType type);
diff --git a/Ryujinx.Graphics.GAL/ITexture.cs b/Ryujinx.Graphics.GAL/ITexture.cs
index 1c5b6ba5f..543f9de08 100644
--- a/Ryujinx.Graphics.GAL/ITexture.cs
+++ b/Ryujinx.Graphics.GAL/ITexture.cs
@@ -2,7 +2,7 @@ using System;
 
 namespace Ryujinx.Graphics.GAL
 {
-    public interface ITexture : IDisposable
+    public interface ITexture
     {
         int Width { get; }
         int Height { get; }
@@ -17,5 +17,6 @@ namespace Ryujinx.Graphics.GAL
 
         void SetData(ReadOnlySpan<byte> data);
         void SetStorage(BufferRange buffer);
+        void Release();
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.GAL/TextureCreateInfo.cs b/Ryujinx.Graphics.GAL/TextureCreateInfo.cs
index d74ac62dd..eedf58a0c 100644
--- a/Ryujinx.Graphics.GAL/TextureCreateInfo.cs
+++ b/Ryujinx.Graphics.GAL/TextureCreateInfo.cs
@@ -3,7 +3,7 @@ using System;
 
 namespace Ryujinx.Graphics.GAL
 {
-    public struct TextureCreateInfo
+    public struct TextureCreateInfo : IEquatable<TextureCreateInfo>
     {
         public int Width         { get; }
         public int Height        { get; }
@@ -116,5 +116,29 @@ namespace Ryujinx.Graphics.GAL
         {
             return Math.Max(1, size >> level);
         }
+
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(Width, Height);
+        }
+
+        bool IEquatable<TextureCreateInfo>.Equals(TextureCreateInfo other)
+        {
+            return Width == other.Width &&
+                   Height == other.Height &&
+                   Depth == other.Depth &&
+                   Levels == other.Levels &&
+                   Samples == other.Samples &&
+                   BlockWidth == other.BlockWidth &&
+                   BlockHeight == other.BlockHeight &&
+                   BytesPerPixel == other.BytesPerPixel &&
+                   Format == other.Format &&
+                   DepthStencilMode == other.DepthStencilMode &&
+                   Target == other.Target &&
+                   SwizzleR == other.SwizzleR &&
+                   SwizzleG == other.SwizzleG &&
+                   SwizzleB == other.SwizzleB &&
+                   SwizzleA == other.SwizzleA;
+        }
     }
 }
diff --git a/Ryujinx.Graphics.Gpu/Engine/Methods.cs b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
index 9497b0456..79ed3c907 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Methods.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
@@ -58,6 +58,7 @@ namespace Ryujinx.Graphics.Gpu.Engine
             TextureManager = new TextureManager(context);
 
             context.MemoryManager.MemoryUnmapped += _counterCache.MemoryUnmappedHandler;
+            context.MemoryManager.MemoryUnmapped += TextureManager.MemoryUnmappedHandler;
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs b/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs
index d66eab93f..634f9448a 100644
--- a/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs
+++ b/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Logging;
 using System.Collections;
 using System.Collections.Generic;
 
@@ -40,6 +41,16 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 Texture oldestTexture = _textures.First.Value;
 
+                oldestTexture.SynchronizeMemory();
+
+                if (oldestTexture.IsModified)
+                {
+                    // The texture must be flushed if it falls out of the auto delete cache.
+                    // Flushes out of the auto delete cache do not trigger write tracking,
+                    // as it is expected that other overlapping textures exist that have more up-to-date contents.
+                    oldestTexture.Flush(false); 
+                }
+
                 _textures.RemoveFirst();
 
                 oldestTexture.DecrementReferenceCount();
@@ -74,6 +85,26 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        public bool Remove(Texture texture, bool flush)
+        {
+            if (texture.CacheNode == null)
+            {
+                return false;
+            }
+
+            // Remove our reference to this texture.
+            if (flush && texture.IsModified)
+            {
+                texture.Flush(false);
+            }
+
+            _textures.Remove(texture.CacheNode);
+
+            texture.CacheNode = null;
+
+            return texture.DecrementReferenceCount();
+        }
+
         public IEnumerator<Texture> GetEnumerator()
         {
             return _textures.GetEnumerator();
diff --git a/Ryujinx.Graphics.Gpu/Image/Texture.cs b/Ryujinx.Graphics.Gpu/Image/Texture.cs
index 50b184d51..fe129f526 100644
--- a/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -39,6 +39,13 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public TextureScaleMode ScaleMode { get; private set; }
 
+        /// <summary>
+        /// Set when a texture has been modified since it was last flushed.
+        /// </summary>
+        public bool IsModified { get; internal set; }
+
+        private bool _everModified;
+
         private int _depth;
         private int _layers;
         private int _firstLayer;
@@ -121,7 +128,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             ScaleFactor = scaleFactor;
             ScaleMode = scaleMode;
 
-            _hasData = true;
+            InitializeData(true);
         }
 
         /// <summary>
@@ -137,16 +144,6 @@ namespace Ryujinx.Graphics.Gpu.Image
             ScaleMode = scaleMode;
 
             InitializeTexture(context, info, sizeInfo);
-
-            TextureCreateInfo createInfo = TextureManager.GetCreateInfo(info, context.Capabilities);
-
-            HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
-
-            if (scaleMode == TextureScaleMode.Scaled)
-            {
-                SynchronizeMemory(); // Load the data and then scale it up.
-                SetScale(GraphicsConfig.ResScale);
-            }
         }
 
         /// <summary>
@@ -171,6 +168,47 @@ namespace Ryujinx.Graphics.Gpu.Image
             _views = new List<Texture>();
         }
 
+        /// <summary>
+        /// Initializes the data for a texture. Can optionally initialize the texture with or without data.
+        /// If the texture is a view, it will initialize memory tracking to be non-dirty.
+        /// </summary>
+        /// <param name="isView">True if the texture is a view, false otherwise</param>
+        /// <param name="withData">True if the texture is to be initialized with data</param>
+        public void InitializeData(bool isView, bool withData = false)
+        {
+            if (withData)
+            {
+                Debug.Assert(!isView);
+
+                TextureCreateInfo createInfo = TextureManager.GetCreateInfo(Info, _context.Capabilities);
+                HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
+
+                SynchronizeMemory(); // Load the data.
+                if (ScaleMode == TextureScaleMode.Scaled)
+                {
+                    SetScale(GraphicsConfig.ResScale); // Scale the data up.
+                }
+            }
+            else
+            {
+                // Don't update this texture the next time we synchronize.
+                ConsumeModified();
+                _hasData = true;
+
+                if (!isView)
+                {
+                    if (ScaleMode == TextureScaleMode.Scaled)
+                    {
+                        // Don't need to start at 1x as there is no data to scale, just go straight to the target scale.
+                        ScaleFactor = GraphicsConfig.ResScale;
+                    }
+
+                    TextureCreateInfo createInfo = TextureManager.GetCreateInfo(Info, _context.Capabilities);
+                    HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
+                }
+            }
+        }
+
         /// <summary>
         /// Create a texture view from this texture.
         /// A texture view is defined as a child texture, from a sub-range of their parent texture.
@@ -238,6 +276,9 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="depthOrLayers">The new texture depth (for 3D textures) or layers (for layered textures)</param>
         public void ChangeSize(int width, int height, int depthOrLayers)
         {
+            int blockWidth = Info.FormatInfo.BlockWidth;
+            int blockHeight = Info.FormatInfo.BlockHeight;
+
             width  <<= _firstLevel;
             height <<= _firstLevel;
 
@@ -250,7 +291,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                 depthOrLayers = _viewStorage.Info.DepthOrLayers;
             }
 
-            _viewStorage.RecreateStorageOrView(width, height, depthOrLayers);
+            _viewStorage.RecreateStorageOrView(width, height, blockWidth, blockHeight, depthOrLayers);
 
             foreach (Texture view in _viewStorage._views)
             {
@@ -268,10 +309,28 @@ namespace Ryujinx.Graphics.Gpu.Image
                     viewDepthOrLayers = view.Info.DepthOrLayers;
                 }
 
-                view.RecreateStorageOrView(viewWidth, viewHeight, viewDepthOrLayers);
+                view.RecreateStorageOrView(viewWidth, viewHeight, blockWidth, blockHeight, viewDepthOrLayers);
             }
         }
 
+        /// <summary>
+        /// Recreates the texture storage (or view, in the case of child textures) of this texture.
+        /// This allows recreating the texture with a new size.
+        /// A copy is automatically performed from the old to the new texture.
+        /// </summary>
+        /// <param name="width">The new texture width</param>
+        /// <param name="height">The new texture height</param>
+        /// <param name="width">The block width related to the given width</param>
+        /// <param name="height">The block height related to the given height</param>
+        /// <param name="depthOrLayers">The new texture depth (for 3D textures) or layers (for layered textures)</param>
+        private void RecreateStorageOrView(int width, int height, int blockWidth, int blockHeight, int depthOrLayers)
+        {
+            RecreateStorageOrView(
+                BitUtils.DivRoundUp(width * Info.FormatInfo.BlockWidth, blockWidth),
+                BitUtils.DivRoundUp(height * Info.FormatInfo.BlockHeight, blockHeight), 
+                depthOrLayers);
+        }
+
         /// <summary>
         /// Recreates the texture storage (or view, in the case of child textures) of this texture.
         /// This allows recreating the texture with a new size.
@@ -388,8 +447,8 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                 from.CopyTo(to, new Extents2D(0, 0, from.Width, from.Height), new Extents2D(0, 0, to.Width, to.Height), true);
 
-                from.Dispose();
-                to.Dispose();
+                from.Release();
+                to.Release();
             }
         }
 
@@ -462,6 +521,16 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Checks if the memory for this texture was modified, and returns true if it was. 
+        /// The modified flags are consumed as a result.
+        /// </summary>
+        /// <returns>True if the texture was modified, false otherwise.</returns>
+        public bool ConsumeModified()
+        {
+            return _context.PhysicalMemory.QueryModified(Address, Size, ResourceName.Texture, _modifiedRanges) > 0;
+        }
+
         /// <summary>
         /// Synchronizes guest and host memory.
         /// This will overwrite the texture data with the texture data on the guest memory, if a CPU
@@ -494,15 +563,15 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             ReadOnlySpan<byte> data = _context.PhysicalMemory.GetSpan(Address, (int)Size);
 
-            // If the texture was modified by the host GPU, we do partial invalidation
+            // If the texture was ever modified by the host GPU, we do partial invalidation
             // of the texture by getting GPU data and merging in the pages of memory
             // that were modified.
             // Note that if ASTC is not supported by the GPU we can't read it back since
             // it will use a different format. Since applications shouldn't be writing
             // ASTC textures from the GPU anyway, ignoring it should be safe.
-            if (_context.Methods.TextureManager.IsTextureModified(this) && !Info.FormatInfo.Format.IsAstc())
+            if (_everModified && !Info.FormatInfo.Format.IsAstc())
             {
-                Span<byte> gpuData = GetTextureDataFromGpu();
+                Span<byte> gpuData = GetTextureDataFromGpu(true);
 
                 ulong endAddress = Address + Size;
 
@@ -533,6 +602,8 @@ namespace Ryujinx.Graphics.Gpu.Image
                 data = gpuData;
             }
 
+            IsModified = false;
+
             data = ConvertToHostCompatibleFormat(data);
 
             HostTexture.SetData(data);
@@ -607,10 +678,24 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// Be aware that this is an expensive operation, avoid calling it unless strictly needed.
         /// This may cause data corruption if the memory is already being used for something else on the CPU side.
         /// </summary>
-        public void Flush()
+        /// <param name="tracked">Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either.</param>
+        public void Flush(bool tracked = true)
         {
-            BlacklistScale();
-            _context.PhysicalMemory.Write(Address, GetTextureDataFromGpu());
+            IsModified = false;
+
+            if (Info.FormatInfo.Format.IsAstc())
+            {
+                return; // Flushing this format is not supported, as it may have been converted to another host format.
+            }
+
+            if (tracked)
+            {
+                _context.PhysicalMemory.Write(Address, GetTextureDataFromGpu(tracked));
+            }
+            else
+            {
+                _context.PhysicalMemory.WriteUntracked(Address, GetTextureDataFromGpu(tracked));
+            }
         }
 
         /// <summary>
@@ -621,10 +706,26 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// This is not cheap, avoid doing that unless strictly needed.
         /// </remarks>
         /// <returns>Host texture data</returns>
-        private Span<byte> GetTextureDataFromGpu()
+        private Span<byte> GetTextureDataFromGpu(bool blacklist)
         {
-            BlacklistScale();
-            Span<byte> data = HostTexture.GetData();
+            Span<byte> data;
+
+            if (blacklist)
+            {
+                BlacklistScale();
+                data = HostTexture.GetData();
+            } 
+            else if (ScaleFactor != 1f)
+            {
+                float scale = ScaleFactor;
+                SetScale(1f);
+                data = HostTexture.GetData();
+                SetScale(scale);
+            }
+            else
+            {
+                data = HostTexture.GetData();
+            }
 
             if (Info.IsLinear)
             {
@@ -713,31 +814,12 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="size">Texture view size</param>
         /// <param name="firstLayer">Texture view initial layer on this texture</param>
         /// <param name="firstLevel">Texture view first mipmap level on this texture</param>
-        /// <returns>True if a view with the given parameters can be created from this texture, false otherwise</returns>
-        public bool IsViewCompatible(
+        /// <returns>The level of compatiblilty a view with the given parameters created from this texture has</returns>
+        public TextureViewCompatibility IsViewCompatible(
             TextureInfo info,
             ulong       size,
             out int     firstLayer,
             out int     firstLevel)
-        {
-            return IsViewCompatible(info, size, isCopy: false, out firstLayer, out firstLevel);
-        }
-
-        /// <summary>
-        /// Check if it's possible to create a view, with the given parameters, from this texture.
-        /// </summary>
-        /// <param name="info">Texture view information</param>
-        /// <param name="size">Texture view size</param>
-        /// <param name="isCopy">True to check for copy compability, instead of view compatibility</param>
-        /// <param name="firstLayer">Texture view initial layer on this texture</param>
-        /// <param name="firstLevel">Texture view first mipmap level on this texture</param>
-        /// <returns>True if a view with the given parameters can be created from this texture, false otherwise</returns>
-        public bool IsViewCompatible(
-            TextureInfo info,
-            ulong       size,
-            bool        isCopy,
-            out int     firstLayer,
-            out int     firstLevel)
         {
             // Out of range.
             if (info.Address < Address || info.Address + size > EndAddress)
@@ -745,38 +827,46 @@ namespace Ryujinx.Graphics.Gpu.Image
                 firstLayer = 0;
                 firstLevel = 0;
 
-                return false;
+                return TextureViewCompatibility.Incompatible;
             }
 
             int offset = (int)(info.Address - Address);
 
             if (!_sizeInfo.FindView(offset, (int)size, out firstLayer, out firstLevel))
             {
-                return false;
+                return TextureViewCompatibility.Incompatible;
             }
 
             if (!TextureCompatibility.ViewLayoutCompatible(Info, info, firstLevel))
             {
-                return false;
+                return TextureViewCompatibility.Incompatible;
             }
 
             if (!TextureCompatibility.ViewFormatCompatible(Info, info))
             {
-                return false;
+                return TextureViewCompatibility.Incompatible;
             }
 
-            if (!TextureCompatibility.ViewSizeMatches(Info, info, firstLevel, isCopy))
-            {
-                return false;
-            }
+            TextureViewCompatibility result = TextureViewCompatibility.Full;
 
-            if (!TextureCompatibility.ViewTargetCompatible(Info, info, isCopy))
-            {
-                return false;
-            }
+            result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSizeMatches(Info, info, firstLevel));
+            result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info));
 
-            return Info.SamplesInX == info.SamplesInX &&
-                   Info.SamplesInY == info.SamplesInY;
+            return (Info.SamplesInX == info.SamplesInX &&
+                    Info.SamplesInY == info.SamplesInY) ? result : TextureViewCompatibility.Incompatible;
+        }
+
+        /// <summary>
+        /// Checks if the view format is compatible with this texture format.
+        /// In general, the formats are considered compatible if the bytes per pixel values are equal,
+        /// but there are more complex rules for some formats, like compressed or depth-stencil formats.
+        /// This follows the host API copy compatibility rules.
+        /// </summary>
+        /// <param name="info">Texture information of the texture view</param>
+        /// <returns>True if the formats are compatible, false otherwise</returns>
+        private bool ViewFormatCompatible(TextureInfo info)
+        {
+            return TextureCompatibility.FormatCompatible(Info.FormatInfo, info.FormatInfo);
         }
 
         /// <summary>
@@ -902,7 +992,15 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public void SignalModified()
         {
+            IsModified = true;
+            _everModified = true;
+
             Modified?.Invoke(this);
+
+            if (_viewStorage != this)
+            {
+                _viewStorage.SignalModified();
+            }
         }
 
         /// <summary>
@@ -927,6 +1025,29 @@ namespace Ryujinx.Graphics.Gpu.Image
             return Address < address + size && address < EndAddress;
         }
 
+        /// <summary>
+        /// Determine if any of our child textures are compaible as views of the given texture.
+        /// </summary>
+        /// <param name="texture">The texture to check against</param>
+        /// <returns>True if any child is view compatible, false otherwise</returns>
+        public bool HasViewCompatibleChild(Texture texture)
+        {
+            if (_viewStorage != this || _views.Count == 0)
+            {
+                return false;
+            }
+
+            foreach (Texture view in _views)
+            {
+                if (texture.IsViewCompatible(view.Info, view.Size, out int _, out int _) != TextureViewCompatibility.Incompatible)
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
         /// <summary>
         /// Increments the texture reference count.
         /// </summary>
@@ -939,7 +1060,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// Decrements the texture reference count.
         /// When the reference count hits zero, the texture may be deleted and can't be used anymore.
         /// </summary>
-        public void DecrementReferenceCount()
+        /// <returns>True if the texture is now referenceless, false otherwise</returns>
+        public bool DecrementReferenceCount()
         {
             int newRefCount = --_referenceCount;
 
@@ -956,6 +1078,8 @@ namespace Ryujinx.Graphics.Gpu.Image
             Debug.Assert(newRefCount >= 0);
 
             DeleteIfNotUsed();
+
+            return newRefCount <= 0;
         }
 
         /// <summary>
@@ -980,12 +1104,21 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         private void DisposeTextures()
         {
-            HostTexture.Dispose();
+            HostTexture.Release();
 
-            _arrayViewTexture?.Dispose();
+            _arrayViewTexture?.Release();
             _arrayViewTexture = null;
         }
 
+        /// <summary>
+        /// Called when the memory for this texture has been unmapped.
+        /// Calls are from non-gpu threads.
+        /// </summary>
+        public void Unmapped()
+        {
+            IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped.
+        }
+
         /// <summary>
         /// Performs texture disposal, deleting the texture.
         /// </summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
index cc7b0dc27..e8e3c2c24 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
@@ -147,28 +147,51 @@ namespace Ryujinx.Graphics.Gpu.Image
         }
 
         /// <summary>
-        /// Checks if the view sizes of a two given texture informations match.
+        /// Obtain the minimum compatibility level of two provided view compatibility results.
+        /// </summary>
+        /// <param name="first">The first compatibility level</param>
+        /// <param name="second">The second compatibility level</param>
+        /// <returns>The minimum compatibility level of two provided view compatibility results</returns>
+        public static TextureViewCompatibility PropagateViewCompatibility(TextureViewCompatibility first, TextureViewCompatibility second)
+        {
+            if (first == TextureViewCompatibility.Incompatible || second == TextureViewCompatibility.Incompatible)
+            {
+                return TextureViewCompatibility.Incompatible;
+            }
+            else if (first == TextureViewCompatibility.CopyOnly || second == TextureViewCompatibility.CopyOnly)
+            {
+                return TextureViewCompatibility.CopyOnly;
+            }
+            else
+            {
+                return TextureViewCompatibility.Full;
+            }
+        }
+
+        /// <summary>
+        /// Checks if the sizes of two given textures are view compatible.
         /// </summary>
         /// <param name="lhs">Texture information of the texture view</param>
         /// <param name="rhs">Texture information of the texture view to match against</param>
         /// <param name="level">Mipmap level of the texture view in relation to this texture</param>
-        /// <param name="isCopy">True to check for copy compatibility rather than view compatibility</param>
         /// <returns>True if the sizes are compatible, false otherwise</returns>
-        public static bool ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level, bool isCopy)
+        public static TextureViewCompatibility ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level)
         {
             Size size = GetAlignedSize(lhs, level);
 
             Size otherSize = GetAlignedSize(rhs);
 
+            TextureViewCompatibility result = TextureViewCompatibility.Full;
+
             // For copies, we can copy a subset of the 3D texture slices,
             // so the depth may be different in this case.
-            if (!isCopy && rhs.Target == Target.Texture3D && size.Depth != otherSize.Depth)
+            if (rhs.Target == Target.Texture3D && size.Depth != otherSize.Depth)
             {
-                return false;
+                result = TextureViewCompatibility.CopyOnly;
             }
 
-            return size.Width  == otherSize.Width &&
-                   size.Height == otherSize.Height;
+            return (size.Width  == otherSize.Width &&
+                    size.Height == otherSize.Height) ? result : TextureViewCompatibility.Incompatible;
         }
 
         /// <summary>
@@ -330,38 +353,48 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="rhs">Texture information of the texture view</param>
         /// <param name="isCopy">True to check for copy rather than view compatibility</param>
         /// <returns>True if the targets are compatible, false otherwise</returns>
-        public static bool ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs, bool isCopy)
+        public static TextureViewCompatibility ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs)
         {
+            bool result = false;
             switch (lhs.Target)
             {
                 case Target.Texture1D:
                 case Target.Texture1DArray:
-                    return rhs.Target == Target.Texture1D ||
-                           rhs.Target == Target.Texture1DArray;
+                    result = rhs.Target == Target.Texture1D ||
+                             rhs.Target == Target.Texture1DArray;
+                    break;
 
                 case Target.Texture2D:
-                    return rhs.Target == Target.Texture2D ||
-                           rhs.Target == Target.Texture2DArray;
+                    result = rhs.Target == Target.Texture2D ||
+                             rhs.Target == Target.Texture2DArray;
+                    break;
 
                 case Target.Texture2DArray:
                 case Target.Cubemap:
                 case Target.CubemapArray:
-                    return rhs.Target == Target.Texture2D ||
-                           rhs.Target == Target.Texture2DArray ||
-                           rhs.Target == Target.Cubemap ||
-                           rhs.Target == Target.CubemapArray;
+                    result = rhs.Target == Target.Texture2D ||
+                             rhs.Target == Target.Texture2DArray ||
+                             rhs.Target == Target.Cubemap ||
+                             rhs.Target == Target.CubemapArray;
+                    break;
 
                 case Target.Texture2DMultisample:
                 case Target.Texture2DMultisampleArray:
-                    return rhs.Target == Target.Texture2DMultisample ||
-                           rhs.Target == Target.Texture2DMultisampleArray;
+                    result = rhs.Target == Target.Texture2DMultisample ||
+                             rhs.Target == Target.Texture2DMultisampleArray;
+                    break;
 
                 case Target.Texture3D:
-                    return rhs.Target == Target.Texture3D ||
-                          (rhs.Target == Target.Texture2D && isCopy);
+                    if (rhs.Target == Target.Texture2D)
+                    {
+                        return TextureViewCompatibility.CopyOnly;
+                    }
+
+                    result = rhs.Target == Target.Texture3D;
+                    break;
             }
 
-            return false;
+            return result ? TextureViewCompatibility.Full : TextureViewCompatibility.Incompatible;
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
index cab76da1b..0b1d38d10 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
@@ -14,6 +14,20 @@ namespace Ryujinx.Graphics.Gpu.Image
     /// </summary>
     class TextureManager : IDisposable
     {
+        private struct OverlapInfo
+        {
+            public TextureViewCompatibility Compatibility { get; }
+            public int FirstLayer { get; }
+            public int FirstLevel { get; }
+
+            public OverlapInfo(TextureViewCompatibility compatibility, int firstLayer, int firstLevel)
+            {
+                Compatibility = compatibility;
+                FirstLayer = firstLayer;
+                FirstLevel = firstLevel;
+            }
+        }
+
         private const int OverlapsBufferInitialCapacity = 10;
         private const int OverlapsBufferMaxCapacity     = 10000;
 
@@ -33,6 +47,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         private readonly RangeList<Texture> _textures;
 
         private Texture[] _textureOverlaps;
+        private OverlapInfo[] _overlapInfo;
 
         private readonly AutoDeleteCache _cache;
 
@@ -64,6 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             _textures = new RangeList<Texture>();
 
             _textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
+            _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
 
             _cache = new AutoDeleteCache();
 
@@ -407,6 +423,27 @@ namespace Ryujinx.Graphics.Gpu.Image
             return true;
         }
 
+        /// <summary>
+        /// Handles removal of textures written to a memory region being unmapped.
+        /// </summary>
+        /// <param name="sender">Sender object</param>
+        /// <param name="e">Event arguments</param>
+        public void MemoryUnmappedHandler(object sender, UnmapEventArgs e)
+        {
+            Texture[] overlaps = new Texture[10];
+            int overlapCount;
+
+            lock (_textures)
+            {
+                overlapCount = _textures.FindOverlaps(_context.MemoryManager.Translate(e.Address), e.Size, ref overlaps);
+            }
+
+            for (int i = 0; i < overlapCount; i++)
+            {
+                overlaps[i].Unmapped();
+            }
+        }
+
         /// <summary>
         /// Tries to find an existing texture, or create a new one if not found.
         /// </summary>
@@ -618,8 +655,13 @@ namespace Ryujinx.Graphics.Gpu.Image
                 scaleMode = (flags & TextureSearchFlags.WithUpscale) != 0 ? TextureScaleMode.Scaled : TextureScaleMode.Eligible;
             }
 
-            // Try to find a perfect texture match, with the same address and parameters.
-            int sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps);
+            int sameAddressOverlapsCount;
+
+            lock (_textures)
+            {
+                // Try to find a perfect texture match, with the same address and parameters.
+                sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps);
+            }
 
             for (int index = 0; index < sameAddressOverlapsCount; index++)
             {
@@ -681,8 +723,12 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             // Find view compatible matches.
             ulong size = (ulong)sizeInfo.TotalSize;
+            int overlapsCount;
 
-            int overlapsCount = _textures.FindOverlaps(info.Address, size, ref _textureOverlaps);
+            lock (_textures)
+            {
+                overlapsCount = _textures.FindOverlaps(info.Address, size, ref _textureOverlaps);
+            }
 
             Texture texture = null;
 
@@ -690,7 +736,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 Texture overlap = _textureOverlaps[index];
 
-                if (overlap.IsViewCompatible(info, size, out int firstLayer, out int firstLevel))
+                if (overlap.IsViewCompatible(info, size, out int firstLayer, out int firstLevel) == TextureViewCompatibility.Full)
                 {
                     if (!isSamplerTexture)
                     {
@@ -701,7 +747,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                     if (IsTextureModified(overlap))
                     {
-                        CacheTextureModified(texture);
+                        texture.SignalModified();
                     }
 
                     // The size only matters (and is only really reliable) when the
@@ -721,65 +767,114 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 texture = new Texture(_context, info, sizeInfo, scaleMode);
 
-                // We need to synchronize before copying the old view data to the texture,
-                // otherwise the copied data would be overwritten by a future synchronization.
-                texture.SynchronizeMemory();
+                // Step 1: Find textures that are view compatible with the new texture.
+                // Any textures that are incompatible will contain garbage data, so they should be removed where possible.
+
+                int viewCompatible = 0;
+                bool setData = isSamplerTexture || overlapsCount == 0;
 
                 for (int index = 0; index < overlapsCount; index++)
                 {
                     Texture overlap = _textureOverlaps[index];
+                    bool overlapInCache = overlap.CacheNode != null;
 
-                    if (texture.IsViewCompatible(overlap.Info, overlap.Size, out int firstLayer, out int firstLevel))
+                    TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Size, out int firstLayer, out int firstLevel);
+
+                    if (compatibility != TextureViewCompatibility.Incompatible)
                     {
-                        TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, firstLevel);
-
-                        TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities);
-
-                        if (texture.ScaleFactor != overlap.ScaleFactor)
+                        if (_overlapInfo.Length != _textureOverlaps.Length)
                         {
-                            // A bit tricky, our new texture may need to contain an existing texture that is upscaled, but isn't itself.
-                            // In that case, we prefer the higher scale only if our format is render-target-like, otherwise we scale the view down before copy.
-
-                            texture.PropagateScale(overlap);
+                            Array.Resize(ref _overlapInfo, _textureOverlaps.Length);
                         }
 
-                        ITexture newView = texture.HostTexture.CreateView(createInfo, firstLayer, firstLevel);
-
-                        overlap.HostTexture.CopyTo(newView, 0, 0);
-
-                        // Inherit modification from overlapping texture, do that before replacing
-                        // the view since the replacement operation removes it from the list.
-                        if (IsTextureModified(overlap))
-                        {
-                            CacheTextureModified(texture);
-                        }
-
-                        overlap.ReplaceView(texture, overlapInfo, newView, firstLayer, firstLevel);
+                        _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
+                        _textureOverlaps[viewCompatible++] = overlap;
                     }
+                    else if (overlapInCache || !setData)
+                    {
+                        if (info.GobBlocksInZ > 1 && info.GobBlocksInZ == overlap.Info.GobBlocksInZ)
+                        {
+                            // Allow overlapping slices of 3D textures. Could be improved in future by making sure the textures don't overlap.
+                            continue;
+                        }
+
+                        // The overlap texture is going to contain garbage data after we draw, or is generally incompatible.
+                        // If the texture cannot be entirely contained in the new address space, and one of its view children is compatible with us,
+                        // it must be flushed before removal, so that the data is not lost.
+
+                        // If the texture was modified since its last use, then that data is probably meant to go into this texture.
+                        // If the data has been modified by the CPU, then it also shouldn't be flushed.
+                        bool modified = overlap.ConsumeModified();
+
+                        bool flush = overlapInCache && !modified && (overlap.Address < texture.Address || overlap.EndAddress > texture.EndAddress) && overlap.HasViewCompatibleChild(texture);
+
+                        setData |= modified || flush;
+
+                        if (overlapInCache)
+                        {
+                            _cache.Remove(overlap, flush);
+                        }
+                    }
+                }
+
+                // We need to synchronize before copying the old view data to the texture,
+                // otherwise the copied data would be overwritten by a future synchronization.
+                texture.InitializeData(false, setData);
+
+                for (int index = 0; index < viewCompatible; index++)
+                {
+                    Texture overlap = _textureOverlaps[index];
+                    OverlapInfo oInfo = _overlapInfo[index];
+
+                    if (oInfo.Compatibility != TextureViewCompatibility.Full)
+                    {
+                        continue; // Copy only compatibilty.
+                    }
+
+                    TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, oInfo.FirstLevel);
+
+                    TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities);
+
+                    if (texture.ScaleFactor != overlap.ScaleFactor)
+                    {
+                        // A bit tricky, our new texture may need to contain an existing texture that is upscaled, but isn't itself.
+                        // In that case, we prefer the higher scale only if our format is render-target-like, otherwise we scale the view down before copy.
+
+                        texture.PropagateScale(overlap);
+                    }
+
+                    ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel);
+
+                    overlap.HostTexture.CopyTo(newView, 0, 0);
+
+                    // Inherit modification from overlapping texture, do that before replacing
+                    // the view since the replacement operation removes it from the list.
+                    if (IsTextureModified(overlap))
+                    {
+                        texture.SignalModified();
+                    }
+
+                    overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel);
                 }
 
                 // If the texture is a 3D texture, we need to additionally copy any slice
                 // of the 3D texture to the newly created 3D texture.
                 if (info.Target == Target.Texture3D)
                 {
-                    for (int index = 0; index < overlapsCount; index++)
+                    for (int index = 0; index < viewCompatible; index++)
                     {
                         Texture overlap = _textureOverlaps[index];
+                        OverlapInfo oInfo = _overlapInfo[index];
 
-                        if (texture.IsViewCompatible(
-                            overlap.Info,
-                            overlap.Size,
-                            isCopy: true,
-                            out int firstLayer,
-                            out int firstLevel))
+                        if (oInfo.Compatibility != TextureViewCompatibility.Incompatible)
                         {
                             overlap.BlacklistScale();
 
-                            overlap.HostTexture.CopyTo(texture.HostTexture, firstLayer, firstLevel);
+                            overlap.HostTexture.CopyTo(texture.HostTexture, oInfo.FirstLayer, oInfo.FirstLevel);
 
                             if (IsTextureModified(overlap))
                             {
-                                CacheTextureModified(texture);
+                                texture.SignalModified();
                             }
                         }
                     }
@@ -795,7 +890,10 @@ namespace Ryujinx.Graphics.Gpu.Image
                 texture.Disposed += CacheTextureDisposed;
             }
 
-            _textures.Add(texture);
+            lock (_textures)
+            {
+                _textures.Add(texture);
+            }
 
             ShrinkOverlapsBufferIfNeeded();
 
@@ -818,6 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="texture">The texture that was modified.</param>
         private void CacheTextureModified(Texture texture)
         {
+            texture.IsModified = true;
             _modified.Add(texture);
 
             if (texture.Info.IsLinear)
@@ -992,7 +1091,10 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             foreach (Texture texture in _modifiedLinear)
             {
-                texture.Flush();
+                if (texture.IsModified)
+                {
+                    texture.Flush();
+                }
             }
 
             _modifiedLinear.Clear();
@@ -1007,7 +1109,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             foreach (Texture texture in _modified)
             {
-                if (texture.OverlapsWith(address, size))
+                if (texture.OverlapsWith(address, size) && texture.IsModified)
                 {
                     texture.Flush();
                 }
@@ -1024,7 +1126,10 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="texture">The texture to be removed</param>
         public void RemoveTextureFromCache(Texture texture)
         {
-            _textures.Remove(texture);
+            lock (_textures)
+            {
+                _textures.Remove(texture);
+            }
         }
 
         /// <summary>
@@ -1033,10 +1138,13 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public void Dispose()
         {
-            foreach (Texture texture in _textures)
+            lock (_textures)
             {
-                _modified.Remove(texture);
-                texture.Dispose();
+                foreach (Texture texture in _textures)
+                {
+                    _modified.Remove(texture);
+                    texture.Dispose();
+                }
             }
         }
     }
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs
new file mode 100644
index 000000000..4671af467
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs
@@ -0,0 +1,13 @@
+namespace Ryujinx.Graphics.Gpu.Image
+{
+    /// <summary>
+    /// The level of view compatibility one texture has to another. 
+    /// Values are increasing in compatibility from 0 (incompatible).
+    /// </summary>
+    enum TextureViewCompatibility
+    {
+        Incompatible = 0,
+        CopyOnly,
+        Full
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/Ryujinx.Graphics.Gpu/Memory/Buffer.cs
index 5fe85d2ea..2394f90d5 100644
--- a/Ryujinx.Graphics.Gpu/Memory/Buffer.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/Buffer.cs
@@ -151,7 +151,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
 
             byte[] data = _context.Renderer.GetBufferData(Handle, offset, (int)size);
 
-            _context.PhysicalMemory.Write(address, data);
+            // TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers.
+            _context.PhysicalMemory.WriteUntracked(address, data);
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
index 88beab8f1..ed3253698 100644
--- a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
@@ -67,6 +67,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
             _cpuMemory.Write(address, data);
         }
 
+        /// <summary>
+        /// Writes data to the application process, without any tracking.
+        /// </summary>
+        /// <param name="address">Address to write into</param>
+        /// <param name="data">Data to be written</param>
+        public void WriteUntracked(ulong address, ReadOnlySpan<byte> data)
+        {
+            _cpuMemory.WriteUntracked(address, data);
+        }
+
         /// <summary>
         /// Checks if a specified virtual memory region has been modified by the CPU since the last call.
         /// </summary>
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
index 2c69571c3..6df2b630c 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
@@ -67,5 +67,10 @@ namespace Ryujinx.Graphics.OpenGL.Image
                 Handle = 0;
             }
         }
+
+        public void Release()
+        {
+            Dispose();
+        }
     }
 }
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs b/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs
index ed258aee1..635b6c2ce 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs
@@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
         private int _viewsCount;
 
+        internal ITexture DefaultView { get; private set; }
+
         public TextureStorage(Renderer renderer, TextureCreateInfo info, float scaleFactor)
         {
             _renderer = renderer;
@@ -147,7 +149,9 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
         public ITexture CreateDefaultView()
         {
-            return CreateView(Info, 0, 0);
+            DefaultView = CreateView(Info, 0, 0);
+
+            return DefaultView;
         }
 
         public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel)
@@ -167,12 +171,37 @@ namespace Ryujinx.Graphics.OpenGL.Image
             // If we don't have any views, then the storage is now useless, delete it.
             if (--_viewsCount == 0)
             {
-                Dispose();
+                if (DefaultView == null)
+                {
+                    Dispose();
+                }
+                else
+                {
+                    // If the default view still exists, we can put it into the resource pool.
+                    Release();
+                }
             }
         }
 
+        /// <summary>
+        /// Release the TextureStorage to the resource pool without disposing its handle.
+        /// </summary>
+        public void Release()
+        {
+            _viewsCount = 1; // When we are used again, we will have the default view.
+
+            _renderer.ResourcePool.AddTexture((TextureView)DefaultView);
+        }
+
+        public void DeleteDefault()
+        {
+            DefaultView = null;
+        }
+
         public void Dispose()
         {
+            DefaultView = null;
+
             if (Handle != 0)
             {
                 GL.DeleteTexture(Handle);
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
index 2d50eba5e..04cadae73 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
@@ -434,7 +434,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
             throw new NotSupportedException();
         }
 
-        public void Dispose()
+        private void DisposeHandles()
         {
             if (_incompatibleFormatView != null)
             {
@@ -447,10 +447,38 @@ namespace Ryujinx.Graphics.OpenGL.Image
             {
                 GL.DeleteTexture(Handle);
 
-                _parent.DecrementViewsCount();
-
                 Handle = 0;
             }
         }
+
+        /// <summary>
+        /// Release the view without necessarily disposing the parent if we are the default view.
+        /// This allows it to be added to the resource pool and reused later.
+        /// </summary>
+        public void Release()
+        {
+            bool hadHandle = Handle != 0;
+
+            if (_parent.DefaultView != this)
+            {
+                DisposeHandles();
+            }
+
+            if (hadHandle)
+            {
+                _parent.DecrementViewsCount();
+            }
+        }
+
+        public void Dispose()
+        {
+            if (_parent.DefaultView == this)
+            {
+                // Remove the default view (us), so that the texture cannot be released to the cache.
+                _parent.DeleteDefault();
+            }
+
+            Release();
+        }
     }
 }
diff --git a/Ryujinx.Graphics.OpenGL/Renderer.cs b/Ryujinx.Graphics.OpenGL/Renderer.cs
index ee2fe93d3..061821eb6 100644
--- a/Ryujinx.Graphics.OpenGL/Renderer.cs
+++ b/Ryujinx.Graphics.OpenGL/Renderer.cs
@@ -23,6 +23,8 @@ namespace Ryujinx.Graphics.OpenGL
 
         internal TextureCopy TextureCopy { get; }
 
+        internal ResourcePool ResourcePool { get; }
+
         public string GpuVendor { get; private set; }
         public string GpuRenderer { get; private set; }
         public string GpuVersion { get; private set; }
@@ -33,6 +35,7 @@ namespace Ryujinx.Graphics.OpenGL
             _counters = new Counters();
             _window = new Window(this);
             TextureCopy = new TextureCopy(this);
+            ResourcePool = new ResourcePool();
         }
 
         public IShader CompileShader(ShaderProgram shader)
@@ -57,7 +60,14 @@ namespace Ryujinx.Graphics.OpenGL
 
         public ITexture CreateTexture(TextureCreateInfo info, float scaleFactor)
         {
-            return info.Target == Target.TextureBuffer ? new TextureBuffer(info) : new TextureStorage(this, info, scaleFactor).CreateDefaultView();
+            if (info.Target == Target.TextureBuffer)
+            {
+                return new TextureBuffer(info);
+            }
+            else
+            {
+                return ResourcePool.GetTextureOrNull(info, scaleFactor) ?? new TextureStorage(this, info, scaleFactor).CreateDefaultView();
+            }
         }
 
         public void DeleteBuffer(BufferHandle buffer)
@@ -92,6 +102,11 @@ namespace Ryujinx.Graphics.OpenGL
             _counters.Update();
         }
 
+        public void PreFrame()
+        {
+            ResourcePool.Tick();
+        }
+
         public ICounterEvent ReportCounter(CounterType type, EventHandler<ulong> resultHandler)
         {
             return _counters.QueueReport(type, resultHandler);
@@ -123,6 +138,7 @@ namespace Ryujinx.Graphics.OpenGL
         public void Dispose()
         {
             TextureCopy.Dispose();
+            ResourcePool.Dispose();
             _pipeline.Dispose();
             _window.Dispose();
             _counters.Dispose();
diff --git a/Ryujinx.Graphics.OpenGL/ResourcePool.cs b/Ryujinx.Graphics.OpenGL/ResourcePool.cs
new file mode 100644
index 000000000..57231cd65
--- /dev/null
+++ b/Ryujinx.Graphics.OpenGL/ResourcePool.cs
@@ -0,0 +1,122 @@
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.OpenGL.Image;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.OpenGL
+{
+    class DisposedTexture
+    {
+        public TextureCreateInfo Info;
+        public TextureView View;
+        public float ScaleFactor;
+        public int RemainingFrames;
+    }
+
+    /// <summary>
+    /// A structure for pooling resources that can be reused without recreation, such as textures.
+    /// </summary>
+    class ResourcePool : IDisposable
+    {
+        private const int DisposedLiveFrames = 2;
+
+        private readonly object _lock = new object();
+        private readonly Dictionary<TextureCreateInfo, List<DisposedTexture>> _textures = new Dictionary<TextureCreateInfo, List<DisposedTexture>>();
+
+        /// <summary>
+        /// Add a texture that is not being used anymore to the resource pool to be used later.
+        /// Both the texture's view and storage should be completely unused.
+        /// </summary>
+        /// <param name="view">The texture's view</param>
+        public void AddTexture(TextureView view)
+        {
+            lock (_lock)
+            {
+                List<DisposedTexture> list;
+                if (!_textures.TryGetValue(view.Info, out list))
+                {
+                    list = new List<DisposedTexture>();
+                    _textures.Add(view.Info, list);
+                }
+
+                list.Add(new DisposedTexture()
+                {
+                    Info = view.Info,
+                    View = view,
+                    ScaleFactor = view.ScaleFactor,
+                    RemainingFrames = DisposedLiveFrames
+                });
+            }
+        }
+
+        /// <summary>
+        /// Attempt to obtain a texture from the resource cache with the desired parameters.
+        /// </summary>
+        /// <param name="info">The creation info for the desired texture</param>
+        /// <param name="scaleFactor">The scale factor for the desired texture</param>
+        /// <returns>A TextureView with the description specified, or null if one was not found.</returns>
+        public TextureView GetTextureOrNull(TextureCreateInfo info, float scaleFactor)
+        {
+            lock (_lock)
+            {
+                List<DisposedTexture> list;
+                if (!_textures.TryGetValue(info, out list))
+                {
+                    return null;
+                }
+
+                foreach (DisposedTexture texture in list)
+                {
+                    if (scaleFactor == texture.ScaleFactor)
+                    {
+                        list.Remove(texture);
+                        return texture.View;
+                    }
+                }
+
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Update the pool, removing any resources that have expired.
+        /// </summary>
+        public void Tick()
+        {
+            lock (_lock)
+            {
+                foreach (List<DisposedTexture> list in _textures.Values)
+                {
+                    for (int i = 0; i < list.Count; i++)
+                    {
+                        DisposedTexture tex = list[i];
+
+                        if (--tex.RemainingFrames < 0)
+                        {
+                            tex.View.Dispose();
+                            list.RemoveAt(i--);
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Disposes the resource pool.
+        /// </summary>
+        public void Dispose()
+        {
+            lock (_lock)
+            {
+                foreach (List<DisposedTexture> list in _textures.Values)
+                {
+                    foreach (DisposedTexture texture in list)
+                    {
+                        texture.View.Dispose();
+                    }
+                }
+                _textures.Clear();
+            }
+        }
+    }
+}
diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs
index df02e5e57..5401f1ccd 100644
--- a/Ryujinx.HLE/Switch.cs
+++ b/Ryujinx.HLE/Switch.cs
@@ -159,6 +159,8 @@ namespace Ryujinx.HLE
 
         public void ProcessFrame()
         {
+            Gpu.Renderer.PreFrame();
+
             Gpu.GPFifo.DispatchCalls();
         }