From cda659955ced1b16839cdd1e7fea1ef6f8d99041 Mon Sep 17 00:00:00 2001
From: riperiperi <rhy3756547@hotmail.com>
Date: Sun, 9 Jan 2022 16:28:48 +0000
Subject: [PATCH] Texture Sync, incompatible overlap handling, data flush
 improvements. (#2971)

* Initial test for texture sync

* WIP new texture flushing setup

* Improve rules for incompatible overlaps

Fixes a lot of issues with Unreal Engine games. Still a few minor issues (some caused by dma fast path?) Needs docs and cleanup.

* Cleanup, improvements

Improve rules for fast DMA

* Small tweak to group together flushes of overlapping handles.

* Fixes, flush overlapping texture data for ASTC and BC4/5 compressed textures.

Fixes the new Life is Strange game.

* Flush overlaps before init data, fix 3d texture size/overlap stuff

* Fix 3D Textures, faster single layer flush

Note: nosy people can no longer merge this with Vulkan. (unless they are nosy enough to implement the new backend methods)

* Remove unused method

* Minor cleanup

* More cleanup

* Use the More Fun and Hopefully No Driver Bugs method for getting compressed tex too

This one's for metro

* Address feedback, ASTC+ETC to FormatClass

* Change offset to use Span slice rather than IntPtr Add

* Fix this too
---
 Ryujinx.Graphics.GAL/ITexture.cs              |   1 +
 .../Multithreading/CommandHelper.cs           |   2 +
 .../Multithreading/CommandType.cs             |   1 +
 .../Texture/TextureGetDataSliceCommand.cs     |  30 +
 .../Resources/ThreadedTexture.cs              |  18 +
 Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs   |   4 +-
 .../Engine/GPFifo/GPFifoClass.cs              |   6 +-
 .../Engine/Threed/ThreedClass.cs              |   2 +-
 Ryujinx.Graphics.Gpu/GpuContext.cs            |  51 +-
 Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs |   8 +-
 Ryujinx.Graphics.Gpu/Image/Texture.cs         | 360 +++++++-----
 Ryujinx.Graphics.Gpu/Image/TextureCache.cs    | 117 ++--
 .../Image/TextureCompatibility.cs             | 219 ++++++-
 Ryujinx.Graphics.Gpu/Image/TextureGroup.cs    | 532 ++++++++++++++++--
 .../Image/TextureGroupHandle.cs               | 247 ++++++--
 .../Image/TextureViewCompatibility.cs         |   1 +
 .../Memory/MultiRangeWritableBlock.cs         |  58 ++
 Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs |  32 ++
 .../Image/TextureBuffer.cs                    |   5 +
 Ryujinx.Graphics.OpenGL/Image/TextureView.cs  |  44 +-
 Ryujinx.Graphics.OpenGL/PersistentBuffers.cs  |  15 +
 Ryujinx.Graphics.Texture/LayoutConverter.cs   |  10 +-
 Ryujinx.Graphics.Texture/SizeCalculator.cs    |   6 +-
 Ryujinx.Graphics.Texture/SizeInfo.cs          |   7 +-
 Ryujinx.Memory/Range/MultiRange.cs            |   5 +
 Ryujinx/Ui/RendererWidgetBase.cs              |   1 +
 26 files changed, 1453 insertions(+), 329 deletions(-)
 create mode 100644 Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureGetDataSliceCommand.cs
 create mode 100644 Ryujinx.Graphics.Gpu/Memory/MultiRangeWritableBlock.cs

diff --git a/Ryujinx.Graphics.GAL/ITexture.cs b/Ryujinx.Graphics.GAL/ITexture.cs
index c011b05ce7..96b592937c 100644
--- a/Ryujinx.Graphics.GAL/ITexture.cs
+++ b/Ryujinx.Graphics.GAL/ITexture.cs
@@ -15,6 +15,7 @@ namespace Ryujinx.Graphics.GAL
         ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel);
 
         ReadOnlySpan<byte> GetData();
+        ReadOnlySpan<byte> GetData(int layer, int level);
 
         void SetData(ReadOnlySpan<byte> data);
         void SetData(ReadOnlySpan<byte> data, int layer, int level);
diff --git a/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index 6111e32ceb..67e8315b4e 100644
--- a/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -111,6 +111,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading
                 TextureCreateViewCommand.Run(ref GetCommand<TextureCreateViewCommand>(memory), threaded, renderer);
             _lookup[(int)CommandType.TextureGetData] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
                 TextureGetDataCommand.Run(ref GetCommand<TextureGetDataCommand>(memory), threaded, renderer);
+            _lookup[(int)CommandType.TextureGetDataSlice] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
+                TextureGetDataSliceCommand.Run(ref GetCommand<TextureGetDataSliceCommand>(memory), threaded, renderer);
             _lookup[(int)CommandType.TextureRelease] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
                 TextureReleaseCommand.Run(ref GetCommand<TextureReleaseCommand>(memory), threaded, renderer);
             _lookup[(int)CommandType.TextureSetData] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
diff --git a/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
index 4bceaa1edc..e0a03ce7f8 100644
--- a/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
+++ b/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
@@ -36,6 +36,7 @@
         TextureCopyToSlice,
         TextureCreateView,
         TextureGetData,
+        TextureGetDataSlice,
         TextureRelease,
         TextureSetData,
         TextureSetDataSlice,
diff --git a/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureGetDataSliceCommand.cs b/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureGetDataSliceCommand.cs
new file mode 100644
index 0000000000..207e5784cf
--- /dev/null
+++ b/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureGetDataSliceCommand.cs
@@ -0,0 +1,30 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+using System;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture
+{
+    struct TextureGetDataSliceCommand : IGALCommand
+    {
+        public CommandType CommandType => CommandType.TextureGetDataSlice;
+        private TableRef<ThreadedTexture> _texture;
+        private TableRef<ResultBox<PinnedSpan<byte>>> _result;
+        private int _layer;
+        private int _level;
+
+        public void Set(TableRef<ThreadedTexture> texture, TableRef<ResultBox<PinnedSpan<byte>>> result, int layer, int level)
+        {
+            _texture = texture;
+            _result = result;
+            _layer = layer;
+            _level = level;
+        }
+
+        public static void Run(ref TextureGetDataSliceCommand command, ThreadedRenderer threaded, IRenderer renderer)
+        {
+            ReadOnlySpan<byte> result = command._texture.Get(threaded).Base.GetData(command._layer, command._level);
+
+            command._result.Get(threaded).Result = new PinnedSpan<byte>(result);
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs b/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs
index 634d32fa2a..64d8aa3bd6 100644
--- a/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs
+++ b/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs
@@ -89,6 +89,24 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
             }
         }
 
+        public ReadOnlySpan<byte> GetData(int layer, int level)
+        {
+            if (_renderer.IsGpuThread())
+            {
+                ResultBox<PinnedSpan<byte>> box = new ResultBox<PinnedSpan<byte>>();
+                _renderer.New<TextureGetDataSliceCommand>().Set(Ref(this), Ref(box), layer, level);
+                _renderer.InvokeCommand();
+
+                return box.Result.Get();
+            }
+            else
+            {
+                ThreadedHelpers.SpinUntilNonNull(ref Base);
+
+                return Base.GetData(layer, level);
+            }
+        }
+
         public void SetData(ReadOnlySpan<byte> data)
         {
             _renderer.New<TextureSetDataCommand>().Set(Ref(this), Ref(data.ToArray()));
diff --git a/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs
index c00e2bef67..44964b8fda 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs
@@ -232,8 +232,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
                             data = LayoutConverter.ConvertBlockLinearToLinear(
                                 src.Width,
                                 src.Height,
+                                src.Depth,
+                                1,
                                 1,
-                                target.Info.Levels,
                                 1,
                                 1,
                                 1,
@@ -245,6 +246,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
                                 srcSpan);
                         }
 
+                        target.SynchronizeMemory();
                         target.SetData(data);
                         target.SignalModified();
 
diff --git a/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs b/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs
index fe49b0f27d..dab4e9db63 100644
--- a/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs
@@ -136,7 +136,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
             }
             else if (operation == SyncpointbOperation.Incr)
             {
-                _context.CreateHostSyncIfNeeded();
+                _context.CreateHostSyncIfNeeded(true);
                 _context.Synchronization.IncrementSyncpoint(syncpointId);
             }
 
@@ -152,7 +152,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
             _parent.PerformDeferredDraws();
             _context.Renderer.Pipeline.Barrier();
 
-            _context.CreateHostSyncIfNeeded();
+            _context.CreateHostSyncIfNeeded(false);
         }
 
         /// <summary>
@@ -163,7 +163,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
         {
             _context.Renderer.Pipeline.CommandBufferBarrier();
 
-            _context.CreateHostSyncIfNeeded();
+            _context.CreateHostSyncIfNeeded(false);
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs
index dc45802633..f6de2730fb 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs
@@ -224,7 +224,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             uint syncpointId = (uint)argument & 0xFFFF;
 
             _context.AdvanceSequence();
-            _context.CreateHostSyncIfNeeded();
+            _context.CreateHostSyncIfNeeded(true);
             _context.Renderer.UpdateCounters(); // Poll the query counters, the game may want an updated result.
             _context.Synchronization.IncrementSyncpoint(syncpointId);
         }
diff --git a/Ryujinx.Graphics.Gpu/GpuContext.cs b/Ryujinx.Graphics.Gpu/GpuContext.cs
index 034c8fcb95..5c9af3839e 100644
--- a/Ryujinx.Graphics.Gpu/GpuContext.cs
+++ b/Ryujinx.Graphics.Gpu/GpuContext.cs
@@ -52,12 +52,19 @@ namespace Ryujinx.Graphics.Gpu
         internal ulong SyncNumber { get; private set; }
 
         /// <summary>
-        /// Actions to be performed when a CPU waiting sync point is triggered.
+        /// Actions to be performed when a CPU waiting syncpoint or barrier is triggered.
         /// If there are more than 0 items when this happens, a host sync object will be generated for the given <see cref="SyncNumber"/>,
         /// and the SyncNumber will be incremented.
         /// </summary>
         internal List<Action> SyncActions { get; }
 
+        /// <summary>
+        /// Actions to be performed when a CPU waiting syncpoint is triggered.
+        /// If there are more than 0 items when this happens, a host sync object will be generated for the given <see cref="SyncNumber"/>,
+        /// and the SyncNumber will be incremented.
+        /// </summary>
+        internal List<Action> SyncpointActions { get; }
+
         /// <summary>
         /// Queue with deferred actions that must run on the render thread.
         /// </summary>
@@ -79,6 +86,7 @@ namespace Ryujinx.Graphics.Gpu
         public event Action<ShaderCacheState, int, int> ShaderCacheStateChanged;
 
         private readonly Lazy<Capabilities> _caps;
+        private Thread _gpuThread;
 
         /// <summary>
         /// Creates a new instance of the GPU emulation context.
@@ -97,6 +105,7 @@ namespace Ryujinx.Graphics.Gpu
             HostInitalized = new ManualResetEvent(false);
 
             SyncActions = new List<Action>();
+            SyncpointActions = new List<Action>();
 
             DeferredActions = new Queue<Action>();
 
@@ -184,6 +193,23 @@ namespace Ryujinx.Graphics.Gpu
             }
         }
 
+        /// <summary>
+        /// Sets the current thread as the main GPU thread.
+        /// </summary>
+        public void SetGpuThread()
+        {
+            _gpuThread = Thread.CurrentThread;
+        }
+
+        /// <summary>
+        /// Checks if the current thread is the GPU thread.
+        /// </summary>
+        /// <returns>True if the thread is the GPU thread, false otherwise</returns>
+        public bool IsGpuThread()
+        {
+            return _gpuThread == Thread.CurrentThread;
+        }
+
         /// <summary>
         /// Processes the queue of shaders that must save their binaries to the disk cache.
         /// </summary>
@@ -209,18 +235,27 @@ namespace Ryujinx.Graphics.Gpu
         /// This will also ensure a host sync object is created, and <see cref="SyncNumber"/> is incremented.
         /// </summary>
         /// <param name="action">The action to be performed on sync object creation</param>
-        public void RegisterSyncAction(Action action)
+        /// <param name="syncpointOnly">True if the sync action should only run when syncpoints are incremented</param>
+        public void RegisterSyncAction(Action action, bool syncpointOnly = false)
         {
-            SyncActions.Add(action);
+            if (syncpointOnly)
+            {
+                SyncpointActions.Add(action);
+            }
+            else
+            {
+                SyncActions.Add(action);
+            }
         }
 
         /// <summary>
         /// Creates a host sync object if there are any pending sync actions. The actions will then be called.
         /// If no actions are present, a host sync object is not created.
         /// </summary>
-        public void CreateHostSyncIfNeeded()
+        /// <param name="syncpoint">True if host sync is being created by a syncpoint</param>
+        public void CreateHostSyncIfNeeded(bool syncpoint)
         {
-            if (SyncActions.Count > 0)
+            if (SyncActions.Count > 0 || (syncpoint && SyncpointActions.Count > 0))
             {
                 Renderer.CreateSync(SyncNumber);
 
@@ -231,7 +266,13 @@ namespace Ryujinx.Graphics.Gpu
                     action();
                 }
 
+                foreach (Action action in SyncpointActions)
+                {
+                    action();
+                }
+
                 SyncActions.Clear();
+                SyncpointActions.Clear();
             }
         }
 
diff --git a/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs b/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs
index 8bc6d5448c..7bee844eca 100644
--- a/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs
+++ b/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs
@@ -41,14 +41,14 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 Texture oldestTexture = _textures.First.Value;
 
-                if (oldestTexture.IsModified && !oldestTexture.CheckModified(false))
+                if (!oldestTexture.CheckModified(false))
                 {
                     // 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.Group.SynchronizeDependents(oldestTexture);
-                    oldestTexture.Flush(false);
+                    oldestTexture.FlushModified(false);
                 }
 
                 _textures.RemoveFirst();
@@ -93,9 +93,9 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
 
             // Remove our reference to this texture.
-            if (flush && texture.IsModified)
+            if (flush)
             {
-                texture.Flush(false);
+                texture.FlushModified(false);
             }
 
             _textures.Remove(texture.CacheNode);
diff --git a/Ryujinx.Graphics.Gpu/Image/Texture.cs b/Ryujinx.Graphics.Gpu/Image/Texture.cs
index 6d981479ab..ca43c4300f 100644
--- a/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -72,11 +72,6 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public TextureGroup Group { get; private set; }
 
-        /// <summary>
-        /// Set when a texture has been modified by the Host GPU since it was last flushed.
-        /// </summary>
-        public bool IsModified { get; internal set; }
-
         /// <summary>
         /// Set when a texture has been changed size. This indicates that it may need to be
         /// changed again when obtained as a sampler.
@@ -89,6 +84,12 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public bool ChangedMapping { get; private set; }
 
+        /// <summary>
+        /// True if the data for this texture must always be flushed when an overlap appears.
+        /// This is useful if SetData is called directly on this texture, but the data is meant for a future texture.
+        /// </summary>
+        public bool AlwaysFlushOnOverlap { get; private set; }
+
         private int _depth;
         private int _layers;
         public int FirstLayer { get; private set; }
@@ -99,6 +100,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         private int _updateCount;
         private byte[] _currentData;
 
+        private bool _modifiedStale = true;
+
         private ITexture _arrayViewTexture;
         private Target   _arrayViewTarget;
 
@@ -241,6 +244,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="withData">True if the texture is to be initialized with data</param>
         public void InitializeData(bool isView, bool withData = false)
         {
+            withData |= Group != null && Group.FlushIncompatibleOverlapsIfNeeded();
+
             if (withData)
             {
                 Debug.Assert(!isView);
@@ -280,9 +285,10 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         /// <param name="hasLayerViews">True if the texture will have layer views</param>
         /// <param name="hasMipViews">True if the texture will have mip views</param>
-        public void InitializeGroup(bool hasLayerViews, bool hasMipViews)
+        /// <param name="incompatibleOverlaps">Groups that overlap with this one but are incompatible</param>
+        public void InitializeGroup(bool hasLayerViews, bool hasMipViews, List<TextureIncompatibleOverlap> incompatibleOverlaps)
         {
-            Group = new TextureGroup(_context, _physicalMemory, this);
+            Group = new TextureGroup(_context, _physicalMemory, this, incompatibleOverlaps);
 
             Group.Initialize(ref _sizeInfo, hasLayerViews, hasMipViews);
         }
@@ -657,6 +663,14 @@ namespace Ryujinx.Graphics.Gpu.Image
             _dirty = true;
         }
 
+        /// <summary>
+        /// Signal that the modified state is dirty, indicating that the texture group should be notified when it changes.
+        /// </summary>
+        public void SignalModifiedDirty()
+        {
+            _modifiedStale = true;
+        }
+
         /// <summary>
         /// Fully synchronizes guest and host memory.
         /// This will replace the entire texture with the data present in guest memory.
@@ -670,8 +684,6 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Range);
 
-            IsModified = false;
-
             // If the host does not support ASTC compression, we need to do the decompression.
             // The decompression is slow, so we want to avoid it as much as possible.
             // This does a byte-by-byte check and skips the update if the data is equal in this case.
@@ -710,7 +722,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             Group.CheckDirty(this, true);
 
-            IsModified = false;
+            AlwaysFlushOnOverlap = true;
 
             HostTexture.SetData(data);
 
@@ -738,15 +750,17 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// Converts texture data to a format and layout that is supported by the host GPU.
         /// </summary>
         /// <param name="data">Data to be converted</param>
+        /// <param name="level">Mip level to convert</param>
+        /// <param name="single">True to convert a single slice</param>
         /// <returns>Converted data</returns>
         public ReadOnlySpan<byte> ConvertToHostCompatibleFormat(ReadOnlySpan<byte> data, int level = 0, bool single = false)
         {
             int width = Info.Width;
             int height = Info.Height;
 
-            int depth = single ? 1 : _depth;
+            int depth = _depth;
             int layers = single ? 1 : _layers;
-            int levels = single ? 1 : Info.Levels;
+            int levels = single ? 1 : (Info.Levels - level);
 
             width = Math.Max(width >> level, 1);
             height = Math.Max(height >> level, 1);
@@ -770,6 +784,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     width,
                     height,
                     depth,
+                    single ? 1 : depth,
                     levels,
                     layers,
                     Info.FormatInfo.BlockWidth,
@@ -821,6 +836,65 @@ namespace Ryujinx.Graphics.Gpu.Image
             return data;
         }
 
+        /// <summary>
+        /// Converts texture data from a format and layout that is supported by the host GPU, back into the intended format on the guest GPU.
+        /// </summary>
+        /// <param name="output">Optional output span to convert into</param>
+        /// <param name="data">Data to be converted</param>
+        /// <param name="level">Mip level to convert</param>
+        /// <param name="single">True to convert a single slice</param>
+        /// <returns>Converted data</returns>
+        public ReadOnlySpan<byte> ConvertFromHostCompatibleFormat(Span<byte> output, ReadOnlySpan<byte> data, int level = 0, bool single = false)
+        {
+            if (Target != Target.TextureBuffer)
+            {
+                int width = Info.Width;
+                int height = Info.Height;
+
+                int depth = _depth;
+                int layers = single ? 1 : _layers;
+                int levels = single ? 1 : (Info.Levels - level);
+
+                width = Math.Max(width >> level, 1);
+                height = Math.Max(height >> level, 1);
+                depth = Math.Max(depth >> level, 1);
+
+                if (Info.IsLinear)
+                {
+                    data = LayoutConverter.ConvertLinearToLinearStrided(
+                        output,
+                        Info.Width,
+                        Info.Height,
+                        Info.FormatInfo.BlockWidth,
+                        Info.FormatInfo.BlockHeight,
+                        Info.Stride,
+                        Info.FormatInfo.BytesPerPixel,
+                        data);
+                }
+                else
+                {
+                    data = LayoutConverter.ConvertLinearToBlockLinear(
+                        output,
+                        width,
+                        height,
+                        depth,
+                        single ? 1 : depth,
+                        levels,
+                        layers,
+                        Info.FormatInfo.BlockWidth,
+                        Info.FormatInfo.BlockHeight,
+                        Info.FormatInfo.BytesPerPixel,
+                        Info.GobBlocksInY,
+                        Info.GobBlocksInZ,
+                        Info.GobBlocksInTileX,
+                        _sizeInfo,
+                        data);
+                }
+            }
+
+            return data;
+        }
+
         /// <summary>
         /// Flushes the texture data.
         /// This causes the texture data to be written back to guest memory.
@@ -830,56 +904,48 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// This may cause data corruption if the memory is already being used for something else on the CPU side.
         /// </summary>
         /// <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)
+        /// <returns>True if data was flushed, false otherwise</returns>
+        public bool FlushModified(bool tracked = true)
         {
-            IsModified = false;
-            if (TextureCompatibility.IsFormatHostIncompatible(Info, _context.Capabilities))
-            {
-                return; // Flushing this format is not supported, as it may have been converted to another host format.
-            }
-
-            FlushTextureDataToGuest(tracked);
+            return TextureCompatibility.CanTextureFlush(Info, _context.Capabilities) && Group.FlushModified(this, tracked);
         }
 
         /// <summary>
-        /// Flushes the texture data, to be called from an external thread.
-        /// The host backend must ensure that we have shared access to the resource from this thread.
-        /// This is used when flushing from memory access handlers.
+        /// Flushes the texture data.
+        /// This causes the texture data to be written back to guest memory.
+        /// If the texture was written by the GPU, this includes all modification made by the GPU
+        /// up to this point.
+        /// 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 ExternalFlush(ulong address, ulong size)
+        /// <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)
         {
-            if (!IsModified)
+            if (TextureCompatibility.CanTextureFlush(Info, _context.Capabilities))
             {
-                return;
+                FlushTextureDataToGuest(tracked);
+            }
+        }
+        
+        /// <summary>
+        /// Gets a host texture to use for flushing the texture, at 1x resolution.
+        /// If the HostTexture is already at 1x resolution, it is returned directly.
+        /// </summary>
+        /// <returns>The host texture to flush</returns>
+        public ITexture GetFlushTexture()
+        {
+            ITexture texture = HostTexture;
+            if (ScaleFactor != 1f)
+            {
+                // If needed, create a texture to flush back to host at 1x scale.
+                texture = _flushHostTexture = GetScaledHostTexture(1f, _flushHostTexture);
             }
 
-            _context.Renderer.BackgroundContextAction(() =>
-            {
-                IsModified = false;
-                if (TextureCompatibility.IsFormatHostIncompatible(Info, _context.Capabilities))
-                {
-                    return; // Flushing this format is not supported, as it may have been converted to another host format.
-                }
-
-                if (Info.Target == Target.Texture2DMultisample ||
-                    Info.Target == Target.Texture2DMultisampleArray)
-                {
-                    return; // Flushing multisample textures is not supported, the host does not allow getting their data.
-                }
-
-                ITexture texture = HostTexture;
-                if (ScaleFactor != 1f)
-                {
-                    // If needed, create a texture to flush back to host at 1x scale.
-                    texture = _flushHostTexture = GetScaledHostTexture(1f, _flushHostTexture);
-                }
-
-                FlushTextureDataToGuest(false, texture);
-            });
+            return texture;
         }
 
         /// <summary>
-        /// Gets data from the host GPU, and flushes it to guest memory.
+        /// Gets data from the host GPU, and flushes it all to guest memory.
         /// </summary>
         /// <remarks>
         /// This method should be used to retrieve data that was modified by the host GPU.
@@ -888,28 +954,11 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </remarks>
         /// <param name="tracked">True if writing the texture data is tracked, false otherwise</param>
         /// <param name="texture">The specific host texture to flush. Defaults to this texture</param>
-        private void FlushTextureDataToGuest(bool tracked, ITexture texture = null)
+        public void FlushTextureDataToGuest(bool tracked, ITexture texture = null)
         {
-            if (Range.Count == 1)
-            {
-                MemoryRange subrange = Range.GetSubRange(0);
+            using WritableRegion region = _physicalMemory.GetWritableRegion(Range, tracked);
 
-                using (WritableRegion region = _physicalMemory.GetWritableRegion(subrange.Address, (int)subrange.Size, tracked))
-                {
-                    GetTextureDataFromGpu(region.Memory.Span, tracked, texture);
-                }
-            }
-            else
-            {
-                if (tracked)
-                {
-                    _physicalMemory.Write(Range, GetTextureDataFromGpu(Span<byte>.Empty, true, texture));
-                }
-                else
-                {
-                    _physicalMemory.WriteUntracked(Range, GetTextureDataFromGpu(Span<byte>.Empty, false, texture));
-                }
-            }
+            GetTextureDataFromGpu(region.Memory.Span, tracked, texture);
         }
 
         /// <summary>
@@ -951,40 +1000,54 @@ namespace Ryujinx.Graphics.Gpu.Image
                 }
             }
 
-            if (Target != Target.TextureBuffer)
+            data = ConvertFromHostCompatibleFormat(output, data);
+
+            return data;
+        }
+
+        /// <summary>
+        /// Gets data from the host GPU for a single slice.
+        /// </summary>
+        /// <remarks>
+        /// This method should be used to retrieve data that was modified by the host GPU.
+        /// This is not cheap, avoid doing that unless strictly needed.
+        /// </remarks>
+        /// <param name="output">An output span to place the texture data into. If empty, one is generated</param>
+        /// <param name="layer">The layer of the texture to flush</param>
+        /// <param name="level">The level of the texture to flush</param>
+        /// <param name="blacklist">True if the texture should be blacklisted, false otherwise</param>
+        /// <param name="texture">The specific host texture to flush. Defaults to this texture</param>
+        /// <returns>The span containing the texture data</returns>
+        public ReadOnlySpan<byte> GetTextureDataSliceFromGpu(Span<byte> output, int layer, int level, bool blacklist, ITexture texture = null)
+        {
+            ReadOnlySpan<byte> data;
+
+            if (texture != null)
             {
-                if (Info.IsLinear)
+                data = texture.GetData(layer, level);
+            }
+            else
+            {
+                if (blacklist)
                 {
-                    data = LayoutConverter.ConvertLinearToLinearStrided(
-                        output,
-                        Info.Width,
-                        Info.Height,
-                        Info.FormatInfo.BlockWidth,
-                        Info.FormatInfo.BlockHeight,
-                        Info.Stride,
-                        Info.FormatInfo.BytesPerPixel,
-                        data);
+                    BlacklistScale();
+                    data = HostTexture.GetData(layer, level);
+                }
+                else if (ScaleFactor != 1f)
+                {
+                    float scale = ScaleFactor;
+                    SetScale(1f);
+                    data = HostTexture.GetData(layer, level);
+                    SetScale(scale);
                 }
                 else
                 {
-                    data = LayoutConverter.ConvertLinearToBlockLinear(
-                        output,
-                        Info.Width,
-                        Info.Height,
-                        _depth,
-                        Info.Levels,
-                        _layers,
-                        Info.FormatInfo.BlockWidth,
-                        Info.FormatInfo.BlockHeight,
-                        Info.FormatInfo.BytesPerPixel,
-                        Info.GobBlocksInY,
-                        Info.GobBlocksInZ,
-                        Info.GobBlocksInTileX,
-                        _sizeInfo,
-                        data);
+                    data = HostTexture.GetData(layer, level);
                 }
             }
 
+            data = ConvertFromHostCompatibleFormat(output, data, level, true);
+
             return data;
         }
 
@@ -1043,55 +1106,64 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         /// <param name="info">Texture view information</param>
         /// <param name="range">Texture view physical memory ranges</param>
+        /// <param name="layerSize">Layer size on the given texture</param>
+        /// <param name="caps">Host GPU capabilities</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>The level of compatiblilty a view with the given parameters created from this texture has</returns>
-        public TextureViewCompatibility IsViewCompatible(TextureInfo info, MultiRange range, int layerSize, out int firstLayer, out int firstLevel)
+        public TextureViewCompatibility IsViewCompatible(TextureInfo info, MultiRange range, int layerSize, Capabilities caps, out int firstLayer, out int firstLevel)
         {
-            int offset = Range.FindOffset(range);
+            TextureViewCompatibility result = TextureViewCompatibility.Full;
 
-            // Out of range.
-            if (offset < 0)
+            result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewFormatCompatible(Info, info, caps));
+            if (result != TextureViewCompatibility.Incompatible)
             {
-                firstLayer = 0;
-                firstLevel = 0;
+                result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info));
 
+                if (result == TextureViewCompatibility.Full && Info.FormatInfo.Format != info.FormatInfo.Format && !_context.Capabilities.SupportsMismatchingViewFormat)
+                {
+                    // AMD and Intel have a bug where the view format is always ignored;
+                    // they use the parent format instead.
+                    // Create a copy dependency to avoid this issue.
+
+                    result = TextureViewCompatibility.CopyOnly;
+                }
+
+                if (Info.SamplesInX != info.SamplesInX || Info.SamplesInY != info.SamplesInY)
+                {
+                    result = TextureViewCompatibility.Incompatible;
+                }
+            }
+
+            firstLayer = 0;
+            firstLevel = 0;
+
+            if (result == TextureViewCompatibility.Incompatible)
+            {
                 return TextureViewCompatibility.Incompatible;
             }
 
-            if (!_sizeInfo.FindView(offset, out firstLayer, out firstLevel))
+            int offset = Range.FindOffset(range);
+
+            if (offset < 0 || !_sizeInfo.FindView(offset, out firstLayer, out firstLevel))
             {
-                return TextureViewCompatibility.Incompatible;
+                return TextureViewCompatibility.LayoutIncompatible;
             }
 
             if (!TextureCompatibility.ViewLayoutCompatible(Info, info, firstLevel))
             {
-                return TextureViewCompatibility.Incompatible;
+                return TextureViewCompatibility.LayoutIncompatible;
             }
 
             if (info.GetSlices() > 1 && LayerSize != layerSize)
             {
-                return TextureViewCompatibility.Incompatible;
+                return TextureViewCompatibility.LayoutIncompatible;
             }
 
-            TextureViewCompatibility result = TextureViewCompatibility.Full;
-
-            result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewFormatCompatible(Info, info));
             result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSizeMatches(Info, info, firstLevel));
-            result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info));
             result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSubImagesInBounds(Info, info, firstLayer, firstLevel));
 
-            if (result == TextureViewCompatibility.Full && Info.FormatInfo.Format != info.FormatInfo.Format && !_context.Capabilities.SupportsMismatchingViewFormat)
-            {
-                // AMD and Intel have a bug where the view format is always ignored;
-                // they use the parent format instead.
-                // Create a copy dependency to avoid this issue.
-
-                result = TextureViewCompatibility.CopyOnly;
-            }
-
-            return (Info.SamplesInX == info.SamplesInX &&
-                    Info.SamplesInY == info.SamplesInY) ? result : TextureViewCompatibility.Incompatible;
+            return result;
         }
 
         /// <summary>
@@ -1261,11 +1333,10 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public void SignalModified()
         {
-            bool wasModified = IsModified;
-            if (!wasModified || Group.HasCopyDependencies)
+            if (_modifiedStale || Group.HasCopyDependencies)
             {
-                IsModified = true;
-                Group.SignalModified(this, !wasModified);
+                _modifiedStale = false;
+                Group.SignalModified(this);
             }
 
             _physicalMemory.TextureCache.Lift(this);
@@ -1278,12 +1349,10 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="bound">True if the texture has been bound, false if it has been unbound</param>
         public void SignalModifying(bool bound)
         {
-            bool wasModified = IsModified;
-
-            if (!wasModified || Group.HasCopyDependencies)
+            if (_modifiedStale || Group.HasCopyDependencies)
             {
-                IsModified = true;
-                Group.SignalModifying(this, bound, !wasModified);
+                _modifiedStale = false;
+                Group.SignalModifying(this, bound);
             }
 
             _physicalMemory.TextureCache.Lift(this);
@@ -1309,29 +1378,6 @@ namespace Ryujinx.Graphics.Gpu.Image
             HostTexture = hostTexture;
         }
 
-        /// <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.Range, view.LayerSize, out _, out _) != TextureViewCompatibility.Incompatible)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
         /// <summary>
         /// Determine if any of this texture's data overlaps with another.
         /// </summary>
@@ -1489,11 +1535,15 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// Called when the memory for this texture has been unmapped.
         /// Calls are from non-gpu threads.
         /// </summary>
-        public void Unmapped()
+        /// <param name="unmapRange">The range of memory being unmapped</param>
+        public void Unmapped(MultiRange unmapRange)
         {
             ChangedMapping = true;
 
-            IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped.
+            if (Group.Storage == this)
+            {
+                Group.ClearModified(unmapRange);
+            }
 
             RemoveFromPools(true);
         }
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
index 1aa09b90c6..fab38e1468 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
@@ -7,8 +7,10 @@ using Ryujinx.Graphics.Gpu.Engine.Types;
 using Ryujinx.Graphics.Gpu.Image;
 using Ryujinx.Graphics.Gpu.Memory;
 using Ryujinx.Graphics.Texture;
+using Ryujinx.Memory;
 using Ryujinx.Memory.Range;
 using System;
+using System.Collections.Generic;
 
 namespace Ryujinx.Graphics.Gpu.Image
 {
@@ -72,14 +74,26 @@ namespace Ryujinx.Graphics.Gpu.Image
             Texture[] overlaps = new Texture[10];
             int overlapCount;
 
+            MultiRange unmapped;
+
+            try
+            {
+                unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
+            }
+            catch (InvalidMemoryRegionException)
+            {
+                // This event fires on Map in case any mappings are overwritten. In that case, there may not be an existing mapping.
+                return;
+            }
+
             lock (_textures)
             {
-                overlapCount = _textures.FindOverlaps(((MemoryManager)sender).Translate(e.Address), e.Size, ref overlaps);
+                overlapCount = _textures.FindOverlaps(unmapped, ref overlaps);
             }
 
             for (int i = 0; i < overlapCount; i++)
             {
-                overlaps[i].Unmapped();
+                overlaps[i].Unmapped(unmapped);
             }
         }
 
@@ -494,12 +508,12 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             int fullyCompatible = 0;
 
-            // Evaluate compatibility of overlaps
+            // Evaluate compatibility of overlaps, add temporary references
 
             for (int index = 0; index < overlapsCount; index++)
             {
                 Texture overlap = _textureOverlaps[index];
-                TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, range.Value, sizeInfo.LayerSize, out int firstLayer, out int firstLevel);
+                TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, range.Value, sizeInfo.LayerSize, _context.Capabilities, out int firstLayer, out int firstLevel);
 
                 if (overlapCompatibility == TextureViewCompatibility.Full)
                 {
@@ -514,6 +528,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                 }
 
                 _overlapInfo[index] = new OverlapInfo(overlapCompatibility, firstLayer, firstLevel);
+                overlap.IncrementReferenceCount();
             }
 
             // Search through the overlaps to find a compatible view and establish any copy dependencies.
@@ -544,7 +559,8 @@ namespace Ryujinx.Graphics.Gpu.Image
                     // Only copy compatible. If there's another choice for a FULLY compatible texture, choose that instead.
 
                     texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode);
-                    texture.InitializeGroup(true, true);
+
+                    texture.InitializeGroup(true, true, new List<TextureIncompatibleOverlap>());
                     texture.InitializeData(false, false);
 
                     overlap.SynchronizeMemory();
@@ -564,7 +580,14 @@ namespace Ryujinx.Graphics.Gpu.Image
                     Texture overlap = _textureOverlaps[index];
                     OverlapInfo oInfo = _overlapInfo[index];
 
-                    if (oInfo.Compatibility != TextureViewCompatibility.Incompatible && overlap.Group != texture.Group)
+                    if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
+                    {
+                        if (!overlap.IsView && texture.DataOverlaps(overlap))
+                        {
+                            texture.Group.RegisterIncompatibleOverlap(new TextureIncompatibleOverlap(overlap.Group, oInfo.Compatibility), true);
+                        }
+                    }
+                    else if (overlap.Group != texture.Group)
                     {
                         overlap.SynchronizeMemory();
                         overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true);
@@ -591,78 +614,82 @@ namespace Ryujinx.Graphics.Gpu.Image
                 bool hasLayerViews = false;
                 bool hasMipViews = false;
 
+                var incompatibleOverlaps = new List<TextureIncompatibleOverlap>();
+
                 for (int index = 0; index < overlapsCount; index++)
                 {
                     Texture overlap = _textureOverlaps[index];
                     bool overlapInCache = overlap.CacheNode != null;
 
-                    TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Range, overlap.LayerSize, out int firstLayer, out int firstLevel);
+                    TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Range, overlap.LayerSize, _context.Capabilities, out int firstLayer, out int firstLevel);
 
                     if (overlap.IsView && compatibility == TextureViewCompatibility.Full)
                     {
                         compatibility = TextureViewCompatibility.CopyOnly;
                     }
 
-                    if (compatibility != TextureViewCompatibility.Incompatible)
+                    if (compatibility > TextureViewCompatibility.LayoutIncompatible)
                     {
+                        _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
+                        _textureOverlaps[index] = _textureOverlaps[viewCompatible];
+                        _textureOverlaps[viewCompatible] = overlap;
+
                         if (compatibility == TextureViewCompatibility.Full)
                         {
-                            if (viewCompatible == fullyCompatible)
-                            {
-                                _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
-                                _textureOverlaps[viewCompatible++] = overlap;
-                            }
-                            else
+                            if (viewCompatible != fullyCompatible)
                             {
                                 // Swap overlaps so that the fully compatible views have priority.
 
                                 _overlapInfo[viewCompatible] = _overlapInfo[fullyCompatible];
-                                _textureOverlaps[viewCompatible++] = _textureOverlaps[fullyCompatible];
+                                _textureOverlaps[viewCompatible] = _textureOverlaps[fullyCompatible];
 
                                 _overlapInfo[fullyCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
                                 _textureOverlaps[fullyCompatible] = overlap;
                             }
+
                             fullyCompatible++;
                         }
-                        else
-                        {
-                            _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
-                            _textureOverlaps[viewCompatible++] = overlap;
-                        }
+
+                        viewCompatible++;
 
                         hasLayerViews |= overlap.Info.GetSlices() < texture.Info.GetSlices();
                         hasMipViews |= overlap.Info.Levels < texture.Info.Levels;
                     }
                     else
                     {
+                        bool dataOverlaps = texture.DataOverlaps(overlap);
+                        
+                        if (!overlap.IsView && dataOverlaps && !incompatibleOverlaps.Exists(incompatible => incompatible.Group == overlap.Group))
+                        {
+                            incompatibleOverlaps.Add(new TextureIncompatibleOverlap(overlap.Group, compatibility));
+                        }
+
                         bool removeOverlap;
                         bool modified = overlap.CheckModified(false);
 
                         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;
-                            }
-
-                            if (!texture.DataOverlaps(overlap))
+                            if (!dataOverlaps)
                             {
                                 // Allow textures to overlap if their data does not actually overlap.
                                 // This typically happens when mip level subranges of a layered texture are used. (each texture fills the gaps of the others)
                                 continue;
                             }
 
+                            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.
+                            // The texture group will obtain copy dependencies for any subresources that are compatible between the two textures,
+                            // but sometimes its data must be flushed regardless.
 
                             // 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 viewCompatibleChild = overlap.HasViewCompatibleChild(texture);
-
-                            bool flush = overlapInCache && !modified && !texture.Range.Contains(overlap.Range) && viewCompatibleChild;
+                            bool flush = overlapInCache && !modified && overlap.AlwaysFlushOnOverlap;
 
                             setData |= modified || flush;
 
@@ -671,7 +698,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                                 _cache.Remove(overlap, flush);
                             }
 
-                            removeOverlap = modified && !viewCompatibleChild;
+                            removeOverlap = modified;
                         }
                         else
                         {
@@ -687,12 +714,14 @@ namespace Ryujinx.Graphics.Gpu.Image
                     }
                 }
 
-                texture.InitializeGroup(hasLayerViews, hasMipViews);
+                texture.InitializeGroup(hasLayerViews, hasMipViews, incompatibleOverlaps);
 
                 // 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);
 
+                texture.Group.InitializeOverlaps();
+
                 for (int index = 0; index < viewCompatible; index++)
                 {
                     Texture overlap = _textureOverlaps[index];
@@ -753,6 +782,11 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             ShrinkOverlapsBufferIfNeeded();
 
+            for (int i = 0; i < overlapsCount; i++)
+            {
+                _textureOverlaps[i].DecrementReferenceCount();
+            }
+
             return texture;
         }
 
@@ -824,14 +858,16 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
 
             int addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps);
+            Texture textureMatch = null;
 
             for (int i = 0; i < addressMatches; i++)
             {
                 Texture texture = _textureOverlaps[i];
                 FormatInfo format = texture.Info.FormatInfo;
 
-                if (texture.Info.DepthOrLayers > 1)
+                if (texture.Info.DepthOrLayers > 1 || texture.Info.Levels > 1 || texture.Info.FormatInfo.IsCompressed)
                 {
+                    // Don't support direct buffer copies to anything that isn't a single 2D image, uncompressed.
                     continue;
                 }
 
@@ -859,11 +895,18 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                 if (match)
                 {
-                    return texture;
+                    if (textureMatch == null)
+                    {
+                        textureMatch = texture;
+                    }
+                    else if (texture.Group != textureMatch.Group)
+                    {
+                        return null; // It's ambiguous which texture should match between multiple choices, so leave it up to the slow path.
+                    }
                 }
             }
 
-            return null;
+            return textureMatch;
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
index ce9fd75c77..0461a81f78 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
@@ -2,6 +2,7 @@ using Ryujinx.Common;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Texture;
 using System;
+using System.Numerics;
 
 namespace Ryujinx.Graphics.Gpu.Image
 {
@@ -22,7 +23,23 @@ namespace Ryujinx.Graphics.Gpu.Image
             Bc4,
             Bc5,
             Bc6,
-            Bc7
+            Bc7,
+            Etc2Rgb,
+            Etc2Rgba,
+            Astc4x4,
+            Astc5x4,
+            Astc5x5,
+            Astc6x5,
+            Astc6x6,
+            Astc8x5,
+            Astc8x6,
+            Astc8x8,
+            Astc10x5,
+            Astc10x6,
+            Astc10x8,
+            Astc10x10,
+            Astc12x10,
+            Astc12x12
         }
 
         /// <summary>
@@ -92,34 +109,60 @@ namespace Ryujinx.Graphics.Gpu.Image
             return info.FormatInfo;
         }
 
+        /// <summary>
+        /// Determines whether a texture can flush its data back to guest memory.
+        /// </summary>
+        /// <param name="info">Texture information</param>
+        /// <param name="caps">Host GPU Capabilities</param>
+        /// <returns>True if the texture can flush, false otherwise</returns>
+        public static bool CanTextureFlush(TextureInfo info, Capabilities caps)
+        {
+            if (IsFormatHostIncompatible(info, caps))
+            {
+                return false; // Flushing this format is not supported, as it may have been converted to another host format.
+            }
+
+            if (info.Target == Target.Texture2DMultisample ||
+                info.Target == Target.Texture2DMultisampleArray)
+            {
+                return false; // Flushing multisample textures is not supported, the host does not allow getting their data.
+            }
+
+            return true;
+        }
+
         /// <summary>
         /// Checks if two formats are compatible, according to the host API copy format compatibility rules.
         /// </summary>
-        /// <param name="lhs">First comparand</param>
-        /// <param name="rhs">Second comparand</param>
+        /// <param name="lhsFormat">First comparand</param>
+        /// <param name="rhsFormat">Second comparand</param>
+        /// <param name="caps">Host GPU capabilities</param>
         /// <returns>True if the formats are compatible, false otherwise</returns>
-        public static bool FormatCompatible(FormatInfo lhs, FormatInfo rhs)
+        public static bool FormatCompatible(TextureInfo lhs, TextureInfo rhs, Capabilities caps)
         {
-            if (lhs.Format.IsDepthOrStencil() || rhs.Format.IsDepthOrStencil())
+            FormatInfo lhsFormat = lhs.FormatInfo;
+            FormatInfo rhsFormat = rhs.FormatInfo;
+
+            if (lhsFormat.Format.IsDepthOrStencil() || rhsFormat.Format.IsDepthOrStencil())
             {
-                return lhs.Format == rhs.Format;
+                return lhsFormat.Format == rhsFormat.Format;
             }
 
-            if (lhs.Format.IsAstc() || rhs.Format.IsAstc())
+            if (IsFormatHostIncompatible(lhs, caps) || IsFormatHostIncompatible(rhs, caps))
             {
-                return lhs.Format == rhs.Format;
+                return lhsFormat.Format == rhsFormat.Format;
             }
 
-            if (lhs.IsCompressed && rhs.IsCompressed)
+            if (lhsFormat.IsCompressed && rhsFormat.IsCompressed)
             {
-                FormatClass lhsClass = GetFormatClass(lhs.Format);
-                FormatClass rhsClass = GetFormatClass(rhs.Format);
+                FormatClass lhsClass = GetFormatClass(lhsFormat.Format);
+                FormatClass rhsClass = GetFormatClass(rhsFormat.Format);
 
                 return lhsClass == rhsClass;
             }
             else
             {
-                return lhs.BytesPerPixel == rhs.BytesPerPixel;
+                return lhsFormat.BytesPerPixel == rhsFormat.BytesPerPixel;
             }
         }
 
@@ -204,6 +247,10 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 return TextureViewCompatibility.Incompatible;
             }
+            else if (first == TextureViewCompatibility.LayoutIncompatible || second == TextureViewCompatibility.LayoutIncompatible)
+            {
+                return TextureViewCompatibility.LayoutIncompatible;
+            }
             else if (first == TextureViewCompatibility.CopyOnly || second == TextureViewCompatibility.CopyOnly)
             {
                 return TextureViewCompatibility.CopyOnly;
@@ -214,6 +261,37 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Checks if the sizes of two texture levels are copy 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="lhsLevel">Mipmap level of the texture view in relation to this texture</param>
+        /// <param name="rhsLevel">Mipmap level of the texture view in relation to the second texture</param>
+        /// <returns>True if both levels are view compatible</returns>
+        public static bool CopySizeMatches(TextureInfo lhs, TextureInfo rhs, int lhsLevel, int rhsLevel)
+        {
+            Size size = GetAlignedSize(lhs, lhsLevel);
+
+            Size otherSize = GetAlignedSize(rhs, rhsLevel);
+
+            if (size.Width == otherSize.Width && size.Height == otherSize.Height)
+            {
+                return true;
+            }
+            else if (lhs.IsLinear && rhs.IsLinear)
+            {
+                // Copy between linear textures with matching stride.
+                int stride = BitUtils.AlignUp(Math.Max(1, lhs.Stride >> lhsLevel), Constants.StrideAlignment);
+
+                return stride == rhs.Stride;
+            }
+            else
+            {
+                return false;
+            }
+        }
+
         /// <summary>
         /// Checks if the sizes of two given textures are view compatible.
         /// </summary>
@@ -259,11 +337,11 @@ namespace Ryujinx.Graphics.Gpu.Image
                 // Copy between linear textures with matching stride.
                 int stride = BitUtils.AlignUp(Math.Max(1, lhs.Stride >> level), Constants.StrideAlignment);
 
-                return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.Incompatible;
+                return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.LayoutIncompatible;
             }
             else
             {
-                return TextureViewCompatibility.Incompatible;
+                return TextureViewCompatibility.LayoutIncompatible;
             }
         }
 
@@ -284,7 +362,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
             else
             {
-                return TextureViewCompatibility.Incompatible;
+                return TextureViewCompatibility.LayoutIncompatible;
             }
         }
 
@@ -435,7 +513,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             if (rhs.IsLinear)
             {
                 int stride = Math.Max(1, lhs.Stride >> level);
-                stride = BitUtils.AlignUp(stride, 32);
+                stride = BitUtils.AlignUp(stride, Constants.StrideAlignment);
 
                 return stride == rhs.Stride;
             }
@@ -456,6 +534,62 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Check if it's possible to create a view with the layout of the second texture information from the first.
+        /// The layout information is composed of the Stride for linear textures, or GOB block size
+        /// for block linear textures.
+        /// </summary>
+        /// <param name="lhs">Texture information of the texture view</param>
+        /// <param name="rhs">Texture information of the texture view to compare against</param>
+        /// <param name="lhsLevel">Start level of the texture view, in relation with the first texture</param>
+        /// <param name="rhsLevel">Start level of the texture view, in relation with the second texture</param>
+        /// <returns>True if the layout is compatible, false otherwise</returns>
+        public static bool ViewLayoutCompatible(TextureInfo lhs, TextureInfo rhs, int lhsLevel, int rhsLevel)
+        {
+            if (lhs.IsLinear != rhs.IsLinear)
+            {
+                return false;
+            }
+
+            // For linear textures, gob block sizes are ignored.
+            // For block linear textures, the stride is ignored.
+            if (rhs.IsLinear)
+            {
+                int lhsStride = Math.Max(1, lhs.Stride >> lhsLevel);
+                lhsStride = BitUtils.AlignUp(lhsStride, Constants.StrideAlignment);
+
+                int rhsStride = Math.Max(1, rhs.Stride >> rhsLevel);
+                rhsStride = BitUtils.AlignUp(rhsStride, Constants.StrideAlignment);
+
+                return lhsStride == rhsStride;
+            }
+            else
+            {
+                int lhsHeight = Math.Max(1, lhs.Height >> lhsLevel);
+                int lhsDepth = Math.Max(1, lhs.GetDepth() >> lhsLevel);
+
+                (int lhsGobBlocksInY, int lhsGobBlocksInZ) = SizeCalculator.GetMipGobBlockSizes(
+                    lhsHeight,
+                    lhsDepth,
+                    lhs.FormatInfo.BlockHeight,
+                    lhs.GobBlocksInY,
+                    lhs.GobBlocksInZ);
+
+                int rhsHeight = Math.Max(1, rhs.Height >> rhsLevel);
+                int rhsDepth = Math.Max(1, rhs.GetDepth() >> rhsLevel);
+
+                (int rhsGobBlocksInY, int rhsGobBlocksInZ) = SizeCalculator.GetMipGobBlockSizes(
+                    rhsHeight,
+                    rhsDepth,
+                    rhs.FormatInfo.BlockHeight,
+                    rhs.GobBlocksInY,
+                    rhs.GobBlocksInZ);
+
+                return lhsGobBlocksInY == rhsGobBlocksInY &&
+                       lhsGobBlocksInZ == rhsGobBlocksInZ;
+            }
+        }
+
         /// <summary>
         /// Checks if the view format of the first texture format is compatible with the format of the second.
         /// In general, the formats are considered compatible if the bytes per pixel values are equal,
@@ -464,10 +598,11 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         /// <param name="lhs">Texture information of the texture view</param>
         /// <param name="rhs">Texture information of the texture view</param>
+        /// <param name="caps">Host GPU capabilities</param>
         /// <returns>The view compatibility level of the texture formats</returns>
-        public static TextureViewCompatibility ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs)
+        public static TextureViewCompatibility ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs, Capabilities caps)
         {
-            if (FormatCompatible(lhs.FormatInfo, rhs.FormatInfo))
+            if (FormatCompatible(lhs, rhs, caps))
             {
                 if (lhs.FormatInfo.IsCompressed != rhs.FormatInfo.IsCompressed)
                 {
@@ -638,6 +773,54 @@ namespace Ryujinx.Graphics.Gpu.Image
                 case Format.Bc7Srgb:
                 case Format.Bc7Unorm:
                     return FormatClass.Bc7;
+                case Format.Etc2RgbSrgb:
+                case Format.Etc2RgbUnorm:
+                    return FormatClass.Etc2Rgb;
+                case Format.Etc2RgbaSrgb:
+                case Format.Etc2RgbaUnorm:
+                    return FormatClass.Etc2Rgba;
+                case Format.Astc4x4Srgb:
+                case Format.Astc4x4Unorm:
+                    return FormatClass.Astc4x4;
+                case Format.Astc5x4Srgb:
+                case Format.Astc5x4Unorm:
+                    return FormatClass.Astc5x4;
+                case Format.Astc5x5Srgb:
+                case Format.Astc5x5Unorm:
+                    return FormatClass.Astc5x5;
+                case Format.Astc6x5Srgb:
+                case Format.Astc6x5Unorm:
+                    return FormatClass.Astc6x5;
+                case Format.Astc6x6Srgb:
+                case Format.Astc6x6Unorm:
+                    return FormatClass.Astc6x6;
+                case Format.Astc8x5Srgb:
+                case Format.Astc8x5Unorm:
+                    return FormatClass.Astc8x5;
+                case Format.Astc8x6Srgb:
+                case Format.Astc8x6Unorm:
+                    return FormatClass.Astc8x6;
+                case Format.Astc8x8Srgb:
+                case Format.Astc8x8Unorm:
+                    return FormatClass.Astc8x8;
+                case Format.Astc10x5Srgb:
+                case Format.Astc10x5Unorm:
+                    return FormatClass.Astc10x5;
+                case Format.Astc10x6Srgb:
+                case Format.Astc10x6Unorm:
+                    return FormatClass.Astc10x6;
+                case Format.Astc10x8Srgb:
+                case Format.Astc10x8Unorm:
+                    return FormatClass.Astc10x8;
+                case Format.Astc10x10Srgb:
+                case Format.Astc10x10Unorm:
+                    return FormatClass.Astc10x10;
+                case Format.Astc12x10Srgb:
+                case Format.Astc12x10Unorm:
+                    return FormatClass.Astc12x10;
+                case Format.Astc12x12Srgb:
+                case Format.Astc12x12Unorm:
+                    return FormatClass.Astc12x12;
             }
 
             return FormatClass.Unclassified;
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
index 2bd97432c7..bfc1105ca8 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -2,12 +2,34 @@
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu.Memory;
 using Ryujinx.Graphics.Texture;
+using Ryujinx.Memory;
 using Ryujinx.Memory.Range;
 using System;
 using System.Collections.Generic;
+using System.Runtime.CompilerServices;
 
 namespace Ryujinx.Graphics.Gpu.Image
 {
+    /// <summary>
+    /// An overlapping texture group with a given view compatibility.
+    /// </summary>
+    struct TextureIncompatibleOverlap
+    {
+        public readonly TextureGroup Group;
+        public readonly TextureViewCompatibility Compatibility;
+
+        /// <summary>
+        /// Create a new texture incompatible overlap.
+        /// </summary>
+        /// <param name="group">The group that is incompatible</param>
+        /// <param name="compatibility">The view compatibility for the group</param>
+        public TextureIncompatibleOverlap(TextureGroup group, TextureViewCompatibility compatibility)
+        {
+            Group = group;
+            Compatibility = compatibility;
+        }
+    }
+
     /// <summary>
     /// A texture group represents a group of textures that belong to the same storage.
     /// When views are created, this class will track memory accesses for them separately.
@@ -29,6 +51,11 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public bool HasCopyDependencies { get; set; }
 
+        /// <summary>
+        /// Indicates if this texture has any incompatible overlaps alive.
+        /// </summary>
+        public bool HasIncompatibleOverlaps => _incompatibleOverlaps.Count > 0;
+
         private readonly GpuContext _context;
         private readonly PhysicalMemory _physicalMemory;
 
@@ -49,13 +76,21 @@ namespace Ryujinx.Graphics.Gpu.Image
         private TextureGroupHandle[] _handles;
         private bool[] _loadNeeded;
 
+        /// <summary>
+        /// Other texture groups that have incompatible overlaps with this one.
+        /// </summary>
+        private List<TextureIncompatibleOverlap> _incompatibleOverlaps;
+        private bool _incompatibleOverlapsDirty = true;
+        private bool _flushIncompatibleOverlaps;
+
         /// <summary>
         /// Create a new texture group.
         /// </summary>
         /// <param name="context">GPU context that the texture group belongs to</param>
         /// <param name="physicalMemory">Physical memory where the <paramref name="storage"/> texture is mapped</param>
         /// <param name="storage">The storage texture for this group</param>
-        public TextureGroup(GpuContext context, PhysicalMemory physicalMemory, Texture storage)
+        /// <param name="incompatibleOverlaps">Groups that overlap with this one but are incompatible</param>
+        public TextureGroup(GpuContext context, PhysicalMemory physicalMemory, Texture storage, List<TextureIncompatibleOverlap> incompatibleOverlaps)
         {
             Storage = storage;
             _context = context;
@@ -64,6 +99,9 @@ namespace Ryujinx.Graphics.Gpu.Image
             _is3D = storage.Info.Target == Target.Texture3D;
             _layers = storage.Info.GetSlices();
             _levels = storage.Info.Levels;
+
+            _incompatibleOverlaps = incompatibleOverlaps;
+            _flushIncompatibleOverlaps = TextureCompatibility.IsFormatHostIncompatible(storage.Info, context.Capabilities);
         }
 
         /// <summary>
@@ -82,6 +120,86 @@ namespace Ryujinx.Graphics.Gpu.Image
             RecalculateHandleRegions();
         }
 
+        /// <summary>
+        /// Initialize all incompatible overlaps in the list, registering them with the other texture groups
+        /// and creating copy dependencies when partially compatible.
+        /// </summary>
+        public void InitializeOverlaps()
+        {
+            foreach (TextureIncompatibleOverlap overlap in _incompatibleOverlaps)
+            {
+                if (overlap.Compatibility == TextureViewCompatibility.LayoutIncompatible)
+                {
+                    CreateCopyDependency(overlap.Group, false);
+                }
+
+                overlap.Group._incompatibleOverlaps.Add(new TextureIncompatibleOverlap(this, overlap.Compatibility));
+                overlap.Group._incompatibleOverlapsDirty = true;
+            }
+
+            if (_incompatibleOverlaps.Count > 0)
+            {
+                SignalIncompatibleOverlapModified();
+            }
+        }
+
+        /// <summary>
+        /// Signal that the group is dirty to all views and the storage.
+        /// </summary>
+        private void SignalAllDirty()
+        {
+            Storage.SignalGroupDirty();
+            if (_views != null)
+            {
+                foreach (Texture texture in _views)
+                {
+                    texture.SignalGroupDirty();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Signal that an incompatible overlap has been modified.
+        /// If this group must flush incompatible overlaps, the group is signalled as dirty too.
+        /// </summary>
+        private void SignalIncompatibleOverlapModified()
+        {
+            _incompatibleOverlapsDirty = true;
+
+            if (_flushIncompatibleOverlaps)
+            {
+                SignalAllDirty();
+            }
+        }
+
+
+        /// <summary>
+        /// Flushes incompatible overlaps if the storage format requires it, and they have been modified.
+        /// This allows unsupported host formats to accept data written to format aliased textures.
+        /// </summary>
+        /// <returns>True if data was flushed, false otherwise</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public bool FlushIncompatibleOverlapsIfNeeded()
+        {
+            if (_flushIncompatibleOverlaps && _incompatibleOverlapsDirty)
+            {
+                bool flushed = false;
+
+                foreach (var overlap in _incompatibleOverlaps)
+                {
+                    flushed |= overlap.Group.Storage.FlushModified(true);
+                }
+
+                _incompatibleOverlapsDirty = false;
+
+                return flushed;
+            }
+            else
+            {
+                return false;
+            }
+        }
+
         /// <summary>
         /// Check and optionally consume the dirty flags for a given texture.
         /// The state is shared between views of the same layers and levels.
@@ -124,6 +242,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="texture">The texture being used</param>
         public void SynchronizeMemory(Texture texture)
         {
+            FlushIncompatibleOverlapsIfNeeded();
+
             EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
             {
                 bool dirty = false;
@@ -136,7 +256,6 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                     bool modified = group.Modified;
                     bool handleDirty = false;
-                    bool handleModified = false;
                     bool handleUnmapped = false;
 
                     foreach (CpuRegionHandle handle in group.Handles)
@@ -149,22 +268,28 @@ namespace Ryujinx.Graphics.Gpu.Image
                         else
                         {
                             handleUnmapped |= handle.Unmapped;
-                            handleModified |= modified;
                         }
                     }
 
+                    // If the modified flag is still present, prefer the data written from gpu.
+                    // A write from CPU will do a flush before writing its data, which should unset this.
+                    if (modified)
+                    {
+                        handleDirty = false;
+                    }
+
                     // Evaluate if any copy dependencies need to be fulfilled. A few rules:
                     // If the copy handle needs to be synchronized, prefer our own state.
                     // If we need to be synchronized and there is a copy present, prefer the copy. 
 
-                    if (group.NeedsCopy && group.Copy())
+                    if (group.NeedsCopy && group.Copy(_context))
                     {
                         anyModified |= true; // The copy target has been modified.
                         handleDirty = false;
                     }
                     else
                     {
-                        anyModified |= handleModified;
+                        anyModified |= modified;
                         dirty |= handleDirty;
                     }
 
@@ -218,7 +343,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                         for (int level = 0; level < info.Levels; level++)
                         {
                             int offset = _allOffsets[offsetIndex];
-                            int endOffset = (offsetIndex + 1 == _allOffsets.Length) ? (int)Storage.Size : _allOffsets[offsetIndex + 1];
+                            int endOffset = Math.Min(offset + _sliceSizes[info.BaseLevel + level], (int)Storage.Size);
                             int size = endOffset - offset;
 
                             ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Storage.Range.GetSlice((ulong)offset, (ulong)size));
@@ -251,25 +376,170 @@ namespace Ryujinx.Graphics.Gpu.Image
             });
         }
 
+        /// <summary>
+        /// Determines whether flushes in this texture group should be tracked.
+        /// Incompatible overlaps may need data from this texture to flush tracked for it to be visible to them.
+        /// </summary>
+        /// <returns>True if flushes should be tracked, false otherwise</returns>
+        private bool ShouldFlushTriggerTracking()
+        {
+            foreach (var overlap in _incompatibleOverlaps)
+            {
+                if (overlap.Group._flushIncompatibleOverlaps)
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Gets data from the host GPU, and flushes a slice to guest memory.
+        /// </summary>
+        /// <remarks>
+        /// This method should be used to retrieve data that was modified by the host GPU.
+        /// This is not cheap, avoid doing that unless strictly needed.
+        /// When possible, the data is written directly into guest memory, rather than copied.
+        /// </remarks>
+        /// <param name="tracked">True if writing the texture data is tracked, false otherwise</param>
+        /// <param name="sliceIndex">The index of the slice to flush</param>
+        /// <param name="texture">The specific host texture to flush. Defaults to the storage texture</param>
+        private void FlushTextureDataSliceToGuest(bool tracked, int sliceIndex, ITexture texture = null)
+        {
+            (int layer, int level) = GetLayerLevelForView(sliceIndex);
+
+            int offset = _allOffsets[sliceIndex];
+            int endOffset = Math.Min(offset + _sliceSizes[level], (int)Storage.Size);
+            int size = endOffset - offset;
+
+            using WritableRegion region = _physicalMemory.GetWritableRegion(Storage.Range.GetSlice((ulong)offset, (ulong)size), tracked);
+
+            Storage.GetTextureDataSliceFromGpu(region.Memory.Span, layer, level, tracked, texture);
+        }
+
+        /// <summary>
+        /// Gets and flushes a number of slices of the storage texture to guest memory.
+        /// </summary>
+        /// <param name="tracked">True if writing the texture data is tracked, false otherwise</param>
+        /// <param name="sliceStart">The first slice to flush</param>
+        /// <param name="sliceEnd">The slice to finish flushing on (exclusive)</param>
+        /// <param name="texture">The specific host texture to flush. Defaults to the storage texture</param>
+        private void FlushSliceRange(bool tracked, int sliceStart, int sliceEnd, ITexture texture = null)
+        {
+            for (int i = sliceStart; i < sliceEnd; i++)
+            {
+                FlushTextureDataSliceToGuest(tracked, i, texture);
+            }
+        }
+
+        /// <summary>
+        /// Flush modified ranges for a given texture.
+        /// </summary>
+        /// <param name="texture">The texture being used</param>
+        /// <param name="tracked">True if the flush writes should be tracked, false otherwise</param>
+        /// <returns>True if data was flushed, false otherwise</returns>
+        public bool FlushModified(Texture texture, bool tracked)
+        {
+            tracked = tracked || ShouldFlushTriggerTracking();
+            bool flushed = false;
+
+            EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
+            {
+                int startSlice = 0;
+                int endSlice = 0;
+                bool allModified = true;
+
+                for (int i = 0; i < regionCount; i++)
+                {
+                    TextureGroupHandle group = _handles[baseHandle + i];
+
+                    if (group.Modified)
+                    {
+                        if (endSlice < group.BaseSlice)
+                        {
+                            if (endSlice > startSlice)
+                            {
+                                FlushSliceRange(tracked, startSlice, endSlice);
+                                flushed = true;
+                            }
+
+                            startSlice = group.BaseSlice;
+                        }
+
+                        endSlice = group.BaseSlice + group.SliceCount;
+
+                        if (tracked)
+                        {
+                            group.Modified = false;
+
+                            foreach (Texture texture in group.Overlaps)
+                            {
+                                texture.SignalModifiedDirty();
+                            }
+                        }
+                    }
+                    else
+                    {
+                        allModified = false;
+                    }
+                }
+
+                if (endSlice > startSlice)
+                {
+                    if (allModified && !split)
+                    {
+                        texture.Flush(tracked);
+                    }
+                    else
+                    {
+                        FlushSliceRange(tracked, startSlice, endSlice);
+                    }
+
+                    flushed = true;
+                }
+            });
+
+            Storage.SignalModifiedDirty();
+
+            return flushed;
+        }
+
+        /// <summary>
+        /// Clears competing modified flags for all incompatible ranges, if they have possibly been modified.
+        /// </summary>
+        /// <param name="texture">The texture that has been modified</param>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private void ClearIncompatibleOverlaps(Texture texture)
+        {
+            if (_incompatibleOverlapsDirty)
+            {
+                foreach (TextureIncompatibleOverlap incompatible in _incompatibleOverlaps)
+                {
+                    incompatible.Group.ClearModified(texture.Range, this);
+
+                    incompatible.Group.SignalIncompatibleOverlapModified();
+                }
+
+                _incompatibleOverlapsDirty = false;
+            }
+        }
+
         /// <summary>
         /// Signal that a texture in the group has been modified by the GPU.
         /// </summary>
         /// <param name="texture">The texture that has been modified</param>
-        /// <param name="registerAction">True if the flushing read action should be registered, false otherwise</param>
-        public void SignalModified(Texture texture, bool registerAction)
+        public void SignalModified(Texture texture)
         {
+            ClearIncompatibleOverlaps(texture);
+
             EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
             {
                 for (int i = 0; i < regionCount; i++)
                 {
                     TextureGroupHandle group = _handles[baseHandle + i];
 
-                    group.SignalModified();
-
-                    if (registerAction)
-                    {
-                        RegisterAction(group);
-                    }
+                    group.SignalModified(_context);
                 }
             });
         }
@@ -279,21 +549,17 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         /// <param name="texture">The texture that has been modified</param>
         /// <param name="bound">True if this texture is being bound, false if unbound</param>
-        /// <param name="registerAction">True if the flushing read action should be registered, false otherwise</param>
-        public void SignalModifying(Texture texture, bool bound, bool registerAction)
+        public void SignalModifying(Texture texture, bool bound)
         {
+            ClearIncompatibleOverlaps(texture);
+
             EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
             {
                 for (int i = 0; i < regionCount; i++)
                 {
                     TextureGroupHandle group = _handles[baseHandle + i];
 
-                    group.SignalModifying(bound);
-
-                    if (registerAction)
-                    {
-                        RegisterAction(group);
-                    }
+                    group.SignalModifying(bound, _context);
                 }
             });
         }
@@ -371,7 +637,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             if (_is3D)
             {
                 // Future mip levels come after all layers of the last mip level. Each mipmap has less layers (depth) than the last.
-                
+
                 if (!_hasLayerViews)
                 {
                     // When there are no layer views, the mips are at a consistent offset.
@@ -485,7 +751,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     }
 
                     baseLayer = handleIndex;
-                } 
+                }
                 else
                 {
                     baseLayer = 0;
@@ -634,7 +900,19 @@ namespace Ryujinx.Graphics.Gpu.Image
                 size = _sliceSizes[firstLevel];
             }
 
-            var groupHandle = new TextureGroupHandle(this, _allOffsets[viewStart], (ulong)size, _views, firstLayer, firstLevel, result.ToArray());
+            offset = _allOffsets[viewStart];
+            ulong maxSize = Storage.Size - (ulong)offset;
+
+            var groupHandle = new TextureGroupHandle(
+                this,
+                offset,
+                Math.Min(maxSize, (ulong)size),
+                _views,
+                firstLayer,
+                firstLevel,
+                viewStart,
+                views,
+                result.ToArray());
 
             foreach (CpuRegionHandle handle in result)
             {
@@ -694,11 +972,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                 }
             }
 
-            Storage.SignalGroupDirty();
-            foreach (Texture texture in views)
-            {
-                texture.SignalGroupDirty();
-            }
+            SignalAllDirty();
         }
 
         /// <summary>
@@ -706,7 +980,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         /// <param name="oldHandles">The set of handles to inherit state from</param>
         /// <param name="handles">The set of handles inheriting the state</param>
-        private void InheritHandles(TextureGroupHandle[] oldHandles, TextureGroupHandle[] handles)
+        /// <param name="relativeOffset">The offset of the old handles in relation to the new ones</param>
+        private void InheritHandles(TextureGroupHandle[] oldHandles, TextureGroupHandle[] handles, int relativeOffset)
         {
             foreach (var group in handles)
             {
@@ -716,7 +991,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                     foreach (var oldGroup in oldHandles)
                     {
-                        if (group.OverlapsWith(oldGroup.Offset, oldGroup.Size))
+                        if (group.OverlapsWith(oldGroup.Offset + relativeOffset, oldGroup.Size))
                         {
                             foreach (var oldHandle in oldGroup.Handles)
                             {
@@ -725,13 +1000,13 @@ namespace Ryujinx.Graphics.Gpu.Image
                                     dirty |= oldHandle.Dirty;
                                 }
                             }
-                            
-                            group.Inherit(oldGroup);
+
+                            group.Inherit(oldGroup, group.Offset == oldGroup.Offset + relativeOffset);
                         }
                     }
 
                     if (dirty && !handle.Dirty)
-                    { 
+                    {
                         handle.Reprotect(true);
                     }
 
@@ -741,6 +1016,11 @@ namespace Ryujinx.Graphics.Gpu.Image
                     }
                 }
             }
+
+            foreach (var oldGroup in oldHandles)
+            {
+                oldGroup.Modified = false;
+            }
         }
 
         /// <summary>
@@ -760,7 +1040,16 @@ namespace Ryujinx.Graphics.Gpu.Image
                 RecalculateHandleRegions();
             }
 
-            InheritHandles(other._handles, _handles);
+            foreach (TextureIncompatibleOverlap incompatible in other._incompatibleOverlaps)
+            {
+                RegisterIncompatibleOverlap(incompatible, false);
+
+                incompatible.Group._incompatibleOverlaps.RemoveAll(overlap => overlap.Group == other);
+            }
+
+            int relativeOffset = Storage.Range.FindOffset(other.Storage.Range);
+
+            InheritHandles(other._handles, _handles, relativeOffset);
         }
 
         /// <summary>
@@ -782,7 +1071,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     }
                 }
 
-                InheritHandles(_handles, handles);
+                InheritHandles(_handles, handles, 0);
 
                 foreach (var oldGroup in _handles)
                 {
@@ -815,7 +1104,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     cpuRegionHandles[i] = GenerateHandle(currentRange.Address, currentRange.Size);
                 }
 
-                var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, cpuRegionHandles);
+                var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, 0, _allOffsets.Length, cpuRegionHandles);
 
                 foreach (CpuRegionHandle handle in cpuRegionHandles)
                 {
@@ -855,7 +1144,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     }
 
                     handles = handlesList.ToArray();
-                } 
+                }
                 else
                 {
                     handles = new TextureGroupHandle[layerHandles * levelHandles];
@@ -953,34 +1242,172 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                 if (copyTo)
                 {
-                    otherHandle.Copy(handle);
+                    otherHandle.Copy(_context, handle);
                 }
                 else
                 {
-                    handle.Copy(otherHandle);
+                    handle.Copy(_context, otherHandle);
                 }
             }
         }
 
         /// <summary>
-        /// A flush has been requested on a tracked region. Find an appropriate view to flush.
+        /// Creates a copy dependency to another texture group, where handles overlap.
+        /// Scans through all handles to find compatible patches in the other group.
+        /// </summary>
+        /// <param name="other">The texture group that overlaps this one</param>
+        /// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param>
+        public void CreateCopyDependency(TextureGroup other, bool copyTo)
+        {
+            for (int i = 0; i < _allOffsets.Length; i++)
+            {
+                (int layer, int level) = GetLayerLevelForView(i);
+                MultiRange handleRange = Storage.Range.GetSlice((ulong)_allOffsets[i], 1);
+                ulong handleBase = handleRange.GetSubRange(0).Address;
+
+                for (int j = 0; j < other._handles.Length; j++)
+                {
+                    (int otherLayer, int otherLevel) = other.GetLayerLevelForView(j);
+                    MultiRange otherHandleRange = other.Storage.Range.GetSlice((ulong)other._allOffsets[j], 1);
+                    ulong otherHandleBase = otherHandleRange.GetSubRange(0).Address;
+
+                    if (handleBase == otherHandleBase)
+                    {
+                        // Check if the two sizes are compatible.
+                        TextureInfo info = Storage.Info;
+                        TextureInfo otherInfo = other.Storage.Info;
+
+                        if (TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) && 
+                            TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel))
+                        {
+                            // These textures are copy compatible. Create the dependency.
+
+                            EnsureFullSubdivision();
+                            other.EnsureFullSubdivision();
+
+                            TextureGroupHandle handle = _handles[i];
+                            TextureGroupHandle otherHandle = other._handles[j];
+
+                            handle.CreateCopyDependency(otherHandle, copyTo);
+
+                            // If "copyTo" is true, this texture must copy to the other.
+                            // Otherwise, it must copy to this texture.
+
+                            if (copyTo)
+                            {
+                                otherHandle.Copy(_context, handle);
+                            }
+                            else
+                            {
+                                handle.Copy(_context, otherHandle);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Registers another texture group as an incompatible overlap, if not already registered.
+        /// </summary>
+        /// <param name="other">The texture group to add to the incompatible overlaps list</param>
+        /// <param name="copy">True if the overlap should register copy dependencies</param>
+        public void RegisterIncompatibleOverlap(TextureIncompatibleOverlap other, bool copy)
+        {
+            if (!_incompatibleOverlaps.Exists(overlap => overlap.Group == other.Group))
+            {
+                if (copy && other.Compatibility == TextureViewCompatibility.LayoutIncompatible)
+                {
+                    // Any of the group's views may share compatibility, even if the parents do not fully.
+                    CreateCopyDependency(other.Group, false);
+                }
+
+                _incompatibleOverlaps.Add(other);
+                other.Group._incompatibleOverlaps.Add(new TextureIncompatibleOverlap(this, other.Compatibility));
+            }
+
+            other.Group.SignalIncompatibleOverlapModified();
+            SignalIncompatibleOverlapModified();
+        }
+
+        /// <summary>
+        /// Clear modified flags in the given range.
+        /// This will stop any GPU written data from flushing or copying to dependent textures.
+        /// </summary>
+        /// <param name="range">The range to clear modified flags in</param>
+        /// <param name="ignore">Ignore handles that have a copy dependency to the specified group</param>
+        public void ClearModified(MultiRange range, TextureGroup ignore = null)
+        {
+            TextureGroupHandle[] handles = _handles;
+
+            foreach (TextureGroupHandle handle in handles)
+            {
+                // Handles list is not modified by another thread, only replaced, so this is thread safe.
+                // Remove modified flags from all overlapping handles, so that the textures don't flush to unmapped/remapped GPU memory.
+
+                MultiRange subRange = Storage.Range.GetSlice((ulong)handle.Offset, (ulong)handle.Size);
+
+                if (range.OverlapsWith(subRange))
+                {
+                    if ((ignore == null || !handle.HasDependencyTo(ignore)) && handle.Modified)
+                    {
+                        handle.Modified = false;
+                        Storage.SignalModifiedDirty();
+
+                        lock (handle.Overlaps)
+                        {
+                            foreach (Texture texture in handle.Overlaps)
+                            {
+                                texture.SignalModifiedDirty();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Storage.SignalModifiedDirty();
+
+            if (_views != null)
+            {
+                foreach (Texture texture in _views)
+                {
+                    texture.SignalModifiedDirty();
+                }
+            }
+        }
+
+        /// <summary>
+        /// A flush has been requested on a tracked region. Flush texture data for the given handle.
         /// </summary>
         /// <param name="handle">The handle this flush action is for</param>
         /// <param name="address">The address of the flushing memory access</param>
         /// <param name="size">The size of the flushing memory access</param>
         public void FlushAction(TextureGroupHandle handle, ulong address, ulong size)
         {
-            Storage.ExternalFlush(address, size);
-
-            lock (handle.Overlaps)
+            if (!handle.Modified)
             {
-                foreach (Texture overlap in handle.Overlaps)
-                {
-                    overlap.ExternalFlush(address, size);
-                }
+                return;
             }
 
-            handle.Modified = false;
+            _context.Renderer.BackgroundContextAction(() =>
+            {
+                handle.Sync(_context);
+
+                Storage.SignalModifiedDirty();
+
+                lock (handle.Overlaps)
+                {
+                    foreach (Texture texture in handle.Overlaps)
+                    {
+                        texture.SignalModifiedDirty();
+                    }
+                }
+
+                if (TextureCompatibility.CanTextureFlush(Storage.Info, _context.Capabilities))
+                {
+                    FlushSliceRange(false, handle.BaseSlice, handle.BaseSlice + handle.SliceCount, Storage.GetFlushTexture());
+                }
+            });
         }
 
         /// <summary>
@@ -992,6 +1419,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 group.Dispose();
             }
+
+            foreach (TextureIncompatibleOverlap incompatible in _incompatibleOverlaps)
+            {
+                incompatible.Group._incompatibleOverlaps.RemoveAll(overlap => overlap.Group == this);
+            }
         }
     }
 }
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
index 0b69b6c730..7d4aec4116 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
@@ -19,6 +19,28 @@ namespace Ryujinx.Graphics.Gpu.Image
         private int _firstLevel;
         private int _firstLayer;
 
+        // Sync state for texture flush.
+
+        /// <summary>
+        /// The sync number last registered.
+        /// </summary>
+        private ulong _registeredSync;
+
+        /// <summary>
+        /// The sync number when the texture was last modified by GPU.
+        /// </summary>
+        private ulong _modifiedSync;
+
+        /// <summary>
+        /// Whether a tracking action is currently registered or not.
+        /// </summary>
+        private bool _actionRegistered;
+
+        /// <summary>
+        /// Whether a sync action is currently registered or not.
+        /// </summary>
+        private bool _syncActionRegistered;
+
         /// <summary>
         /// The byte offset from the start of the storage of this handle.
         /// </summary>
@@ -29,6 +51,16 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public int Size { get; }
 
+        /// <summary>
+        /// The base slice index for this handle.
+        /// </summary>
+        public int BaseSlice { get; }
+
+        /// <summary>
+        /// The number of slices covered by this handle.
+        /// </summary>
+        public int SliceCount { get; }
+
         /// <summary>
         /// The textures which this handle overlaps with.
         /// </summary>
@@ -68,8 +100,18 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="views">All views of the storage texture, used to calculate overlaps</param>
         /// <param name="firstLayer">The first layer of this handle in the storage texture</param>
         /// <param name="firstLevel">The first level of this handle in the storage texture</param>
+        /// <param name="baseSlice">The base slice index of this handle</param>
+        /// <param name="sliceCount">The number of slices this handle covers</param>
         /// <param name="handles">The memory tracking handles that cover this handle</param>
-        public TextureGroupHandle(TextureGroup group, int offset, ulong size, List<Texture> views, int firstLayer, int firstLevel, CpuRegionHandle[] handles)
+        public TextureGroupHandle(TextureGroup group,
+                                  int offset,
+                                  ulong size,
+                                  List<Texture> views,
+                                  int firstLayer,
+                                  int firstLevel,
+                                  int baseSlice,
+                                  int sliceCount,
+                                  CpuRegionHandle[] handles)
         {
             _group = group;
             _firstLayer = firstLayer;
@@ -80,6 +122,9 @@ namespace Ryujinx.Graphics.Gpu.Image
             Overlaps = new List<Texture>();
             Dependencies = new List<TextureDependency>();
 
+            BaseSlice = baseSlice;
+            SliceCount = sliceCount;
+
             if (views != null)
             {
                 RecalculateOverlaps(group, views);
@@ -113,10 +158,32 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Registers a sync action to happen for this handle, and an interim flush action on the tracking handle.
+        /// </summary>
+        /// <param name="context">The GPU context to register a sync action on</param>
+        private void RegisterSync(GpuContext context)
+        {
+            if (!_syncActionRegistered)
+            {
+                _modifiedSync = context.SyncNumber;
+                context.RegisterSyncAction(SyncAction, true);
+                _syncActionRegistered = true;
+            }
+
+            if (!_actionRegistered)
+            {
+                _group.RegisterAction(this);
+
+                _actionRegistered = true;
+            }
+        }
+
         /// <summary>
         /// Signal that this handle has been modified to any existing dependencies, and set the modified flag.
         /// </summary>
-        public void SignalModified()
+        /// <param name="context">The GPU context to register a sync action on</param>
+        public void SignalModified(GpuContext context)
         {
             Modified = true;
 
@@ -126,15 +193,18 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 dependency.SignalModified();
             }
+
+            RegisterSync(context);
         }
 
         /// <summary>
         /// Signal that this handle has either started or ended being modified.
         /// </summary>
         /// <param name="bound">True if this handle is being bound, false if unbound</param>
-        public void SignalModifying(bool bound)
+        /// <param name="context">The GPU context to register a sync action on</param>
+        public void SignalModifying(bool bound, GpuContext context)
         {
-            SignalModified();
+            SignalModified(context);
 
             // Note: Bind count currently resets to 0 on inherit for safety, as the handle <-> view relationship can change.
             _bindCount = Math.Max(0, _bindCount + (bound ? 1 : -1));
@@ -156,12 +226,69 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Wait for the latest sync number that the texture handle was written to,
+        /// removing the modified flag if it was reached, or leaving it set if it has not yet been created.
+        /// </summary>
+        /// <param name="context">The GPU context used to wait for sync</param>
+        public void Sync(GpuContext context)
+        {
+            _actionRegistered = false;
+
+            bool needsSync = !context.IsGpuThread();
+
+            if (needsSync)
+            {
+                ulong registeredSync = _registeredSync;
+                long diff = (long)(context.SyncNumber - registeredSync);
+
+                if (diff > 0)
+                {
+                    context.Renderer.WaitSync(registeredSync);
+
+                    if ((long)(_modifiedSync - registeredSync) > 0)
+                    {
+                        // Flush the data in a previous state. Do not remove the modified flag - it will be removed to ignore following writes.
+                        return;
+                    }
+
+                    Modified = false;
+                }
+                
+                // If the difference is <= 0, no data is not ready yet. Flush any data we can without waiting or removing modified flag.
+            }
+            else
+            {
+                Modified = false;
+            }
+        }
+
+        /// <summary>
+        /// Action to perform when a sync number is registered after modification.
+        /// This action will register a read tracking action on the memory tracking handle so that a flush from CPU can happen.
+        /// </summary>
+        private void SyncAction()
+        {
+            // Register region tracking for CPU? (again)
+            _registeredSync = _modifiedSync;
+            _syncActionRegistered = false;
+
+            if (!_actionRegistered)
+            {
+                _group.RegisterAction(this);
+
+                _actionRegistered = true;
+            }
+        }
+
         /// <summary>
         /// Signal that a copy dependent texture has been modified, and must have its data copied to this one.
         /// </summary>
         /// <param name="copyFrom">The texture handle that must defer a copy to this one</param>
         public void DeferCopy(TextureGroupHandle copyFrom)
         {
+            Modified = false;
+
             DeferredCopy = copyFrom;
 
             _group.Storage.SignalGroupDirty();
@@ -247,73 +374,112 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <summary>
         /// Perform a copy from the provided handle to this one, or perform a deferred copy if none is provided.
         /// </summary>
+        /// <param name="context">GPU context to register sync for modified handles</param>
         /// <param name="fromHandle">The handle to copy from. If not provided, this method will copy from and clear the deferred copy instead</param>
         /// <returns>True if the copy was performed, false otherwise</returns>
-        public bool Copy(TextureGroupHandle fromHandle = null)
+        public bool Copy(GpuContext context, TextureGroupHandle fromHandle = null)
         {
             bool result = false;
+            bool shouldCopy = false;
 
             if (fromHandle == null)
             {
                 fromHandle = DeferredCopy;
 
-                if (fromHandle != null && fromHandle._bindCount == 0)
+                if (fromHandle != null)
                 {
-                    // Repeat the copy in future if the bind count is greater than 0.
-                    DeferredCopy = null;
+                    // Only copy if the copy texture is still modified.
+                    // It will be set as unmodified if new data is written from CPU, as the data previously in the texture will flush.
+                    // It will also set as unmodified if a copy is deferred to it.
+
+                    shouldCopy = fromHandle.Modified;
+
+                    if (fromHandle._bindCount == 0)
+                    {
+                        // Repeat the copy in future if the bind count is greater than 0.
+                        DeferredCopy = null;
+                    }
                 }
             }
-
-            if (fromHandle != null)
+            else
             {
-                // If the copy texture is dirty, do not copy. Its data no longer matters, and this handle should also be dirty.
-                if (!fromHandle.CheckDirty())
+                // Copies happen directly when initializing a copy dependency.
+                // If dirty, do not copy. Its data no longer matters, and this handle should also be dirty.
+                // Also, only direct copy if the data in this handle is not already modified (can be set by copies from modified handles).
+                shouldCopy = !fromHandle.CheckDirty() && (fromHandle.Modified || !Modified);
+            }
+
+            if (shouldCopy)
+            {
+                Texture from = fromHandle._group.Storage;
+                Texture to = _group.Storage;
+
+                if (from.ScaleFactor != to.ScaleFactor)
                 {
-                    Texture from = fromHandle._group.Storage;
-                    Texture to = _group.Storage;
+                    to.PropagateScale(from);
+                }
 
-                    if (from.ScaleFactor != to.ScaleFactor)
-                    {
-                        to.PropagateScale(from);
-                    }
-
-                    from.HostTexture.CopyTo(
-                        to.HostTexture,
-                        fromHandle._firstLayer,
-                        _firstLayer,
-                        fromHandle._firstLevel,
-                        _firstLevel);
+                from.HostTexture.CopyTo(
+                    to.HostTexture,
+                    fromHandle._firstLayer,
+                    _firstLayer,
+                    fromHandle._firstLevel,
+                    _firstLevel);
 
+                if (fromHandle.Modified)
+                {
                     Modified = true;
 
-                    _group.RegisterAction(this);
-
-                    result = true;
+                    RegisterSync(context);
                 }
+
+                result = true;
             }
 
             return result;
         }
 
         /// <summary>
-        /// Inherit modified flags and dependencies from another texture handle.
+        /// Check if this handle has a dependency to a given texture group.
         /// </summary>
-        /// <param name="old">The texture handle to inherit from</param>
-        public void Inherit(TextureGroupHandle old)
+        /// <param name="group">The texture group to check for</param>
+        /// <returns>True if there is a dependency, false otherwise</returns>
+        public bool HasDependencyTo(TextureGroup group)
         {
-            Modified |= old.Modified;
-
-            foreach (TextureDependency dependency in old.Dependencies.ToArray())
+            foreach (TextureDependency dep in Dependencies)
             {
-                CreateCopyDependency(dependency.Other.Handle);
-
-                if (dependency.Other.Handle.DeferredCopy == old)
+                if (dep.Other.Handle._group == group)
                 {
-                    dependency.Other.Handle.DeferredCopy = this;
+                    return true;
                 }
             }
 
-            DeferredCopy = old.DeferredCopy;
+            return false;
+        }
+
+        /// <summary>
+        /// Inherit modified flags and dependencies from another texture handle.
+        /// </summary>
+        /// <param name="old">The texture handle to inherit from</param>
+        /// <param name="withCopies">Whether the handle should inherit copy dependencies or not</param>
+        public void Inherit(TextureGroupHandle old, bool withCopies)
+        {
+            Modified |= old.Modified;
+
+            if (withCopies)
+            {
+                foreach (TextureDependency dependency in old.Dependencies.ToArray())
+                {
+                    CreateCopyDependency(dependency.Other.Handle);
+
+                    if (dependency.Other.Handle.DeferredCopy == old)
+                    {
+                        dependency.Other.Handle.DeferredCopy = this;
+                    }
+                }
+
+                DeferredCopy = old.DeferredCopy;
+            }
         }
 
         /// <summary>
@@ -327,6 +493,9 @@ namespace Ryujinx.Graphics.Gpu.Image
             return Offset < offset + size && offset < Offset + Size;
         }
 
+        /// <summary>
+        /// Dispose this texture group handle, removing all its dependencies and disposing its memory tracking handles.
+        /// </summary>
         public void Dispose()
         {
             foreach (CpuRegionHandle handle in Handles)
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs
index 4671af4677..b89936ebda 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs
@@ -7,6 +7,7 @@
     enum TextureViewCompatibility
     {
         Incompatible = 0,
+        LayoutIncompatible,
         CopyOnly,
         Full
     }
diff --git a/Ryujinx.Graphics.Gpu/Memory/MultiRangeWritableBlock.cs b/Ryujinx.Graphics.Gpu/Memory/MultiRangeWritableBlock.cs
new file mode 100644
index 0000000000..155dda7bd5
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/Memory/MultiRangeWritableBlock.cs
@@ -0,0 +1,58 @@
+using Ryujinx.Memory;
+using Ryujinx.Memory.Range;
+using System;
+
+namespace Ryujinx.Graphics.Gpu.Memory
+{
+    /// <summary>
+    /// A writable block that targets a given MultiRange within a PhysicalMemory instance.
+    /// </summary>
+    internal class MultiRangeWritableBlock : IWritableBlock
+    {
+        private readonly MultiRange _range;
+        private readonly PhysicalMemory _physicalMemory;
+
+        /// <summary>
+        /// Creates a new MultiRangeWritableBlock.
+        /// </summary>
+        /// <param name="range">The MultiRange to write to</param>
+        /// <param name="physicalMemory">The PhysicalMemory the given MultiRange addresses</param>
+        public MultiRangeWritableBlock(MultiRange range, PhysicalMemory physicalMemory)
+        {
+            _range = range;
+            _physicalMemory = physicalMemory;
+        }
+
+        /// <summary>
+        /// Write data to the MultiRange.
+        /// </summary>
+        /// <param name="va">Offset address</param>
+        /// <param name="data">Data to write</param>
+        /// <exception cref="ArgumentException">Throw when a non-zero offset is given</exception>
+        public void Write(ulong va, ReadOnlySpan<byte> data)
+        {
+            if (va != 0)
+            {
+                throw new ArgumentException($"{nameof(va)} cannot be non-zero for {nameof(MultiRangeWritableBlock)}.");
+            }
+
+            _physicalMemory.Write(_range, data);
+        }
+
+        /// <summary>
+        /// Write data to the MultiRange, without tracking.
+        /// </summary>
+        /// <param name="va">Offset address</param>
+        /// <param name="data">Data to write</param>
+        /// <exception cref="ArgumentException">Throw when a non-zero offset is given</exception>
+        public void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
+        {
+            if (va != 0)
+            {
+                throw new ArgumentException($"{nameof(va)} cannot be non-zero for {nameof(MultiRangeWritableBlock)}.");
+            }
+
+            _physicalMemory.WriteUntracked(_range, data);
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
index d292fab072..1ff086eea8 100644
--- a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
@@ -135,6 +135,38 @@ namespace Ryujinx.Graphics.Gpu.Memory
             return _cpuMemory.GetWritableRegion(address, size, tracked);
         }
 
+        /// <summary>
+        /// Gets a writable region from GPU mapped memory.
+        /// </summary>
+        /// <param name="range">Range</param>
+        /// <param name="tracked">True if write tracking is triggered on the span</param>
+        /// <returns>A writable region with the data at the specified memory location</returns>
+        public WritableRegion GetWritableRegion(MultiRange range, bool tracked = false)
+        {
+            if (range.Count == 1)
+            {
+                MemoryRange subrange = range.GetSubRange(0);
+
+                return GetWritableRegion(subrange.Address, (int)subrange.Size, tracked);
+            }
+            else
+            {
+                Memory<byte> memory = new byte[range.GetSize()];
+
+                int offset = 0;
+                for (int i = 0; i < range.Count; i++)
+                {
+                    MemoryRange subrange = range.GetSubRange(i);
+
+                    GetSpan(subrange.Address, (int)subrange.Size).CopyTo(memory.Span.Slice(offset, (int)subrange.Size));
+
+                    offset += (int)subrange.Size;
+                }
+
+                return new WritableRegion(new MultiRangeWritableBlock(range, this), 0, memory, tracked);
+            }
+        }
+
         /// <summary>
         /// Reads data from the application process.
         /// </summary>
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
index 899deedf99..dc9bf6af40 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
@@ -43,6 +43,11 @@ namespace Ryujinx.Graphics.OpenGL.Image
             return Buffer.GetData(_renderer, _buffer, _bufferOffset, _bufferSize);
         }
 
+        public ReadOnlySpan<byte> GetData(int layer, int level)
+        {
+            return GetData();
+        }
+
         public void SetData(ReadOnlySpan<byte> data)
         {
             Buffer.SetData(_buffer, _bufferOffset, data.Slice(0, Math.Min(data.Length, _bufferSize)));
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
index 40ab18ef28..f03653c43a 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
@@ -154,6 +154,24 @@ namespace Ryujinx.Graphics.OpenGL.Image
             }
         }
 
+        public unsafe ReadOnlySpan<byte> GetData(int layer, int level)
+        {
+            int size = Info.GetMipSize(level);
+
+            if (HwCapabilities.UsePersistentBufferForFlush)
+            {
+                return _renderer.PersistentBuffers.Default.GetTextureData(this, size, layer, level);
+            }
+            else
+            {
+                IntPtr target = _renderer.PersistentBuffers.Default.GetHostArray(size);
+
+                int offset = WriteTo2D(target, layer, level);
+
+                return new ReadOnlySpan<byte>(target.ToPointer(), size).Slice(offset);
+            }
+        }
+
         public void WriteToPbo(int offset, bool forceBgra)
         {
             WriteTo(IntPtr.Zero + offset, forceBgra);
@@ -182,25 +200,29 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
             int mipSize = Info.GetMipSize2D(level);
 
-            // The GL function returns all layers. Must return the offset of the layer we're interested in.
-            int resultOffset = target switch
-            {
-                TextureTarget.TextureCubeMapArray => (layer / 6) * mipSize,
-                TextureTarget.Texture1DArray => layer * mipSize,
-                TextureTarget.Texture2DArray => layer * mipSize,
-                _ => 0
-            };
-
             if (format.IsCompressed)
             {
-                GL.GetCompressedTexImage(target, level, data);
+                GL.GetCompressedTextureSubImage(Handle, level, 0, 0, layer, Math.Max(1, Info.Width >> level), Math.Max(1, Info.Height >> level), 1, mipSize, data);
+            }
+            else if (format.PixelFormat != PixelFormat.DepthStencil)
+            {
+                GL.GetTextureSubImage(Handle, level, 0, 0, layer, Math.Max(1, Info.Width >> level), Math.Max(1, Info.Height >> level), 1, pixelFormat, pixelType, mipSize, data);
             }
             else
             {
                 GL.GetTexImage(target, level, pixelFormat, pixelType, data);
+
+                // The GL function returns all layers. Must return the offset of the layer we're interested in.
+                return target switch
+                {
+                    TextureTarget.TextureCubeMapArray => (layer / 6) * mipSize,
+                    TextureTarget.Texture1DArray => layer * mipSize,
+                    TextureTarget.Texture2DArray => layer * mipSize,
+                    _ => 0
+                };
             }
 
-            return resultOffset;
+            return 0;
         }
 
         private void WriteTo(IntPtr data, bool forceBgra = false)
diff --git a/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs b/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs
index fe224433c0..872efcc371 100644
--- a/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs
+++ b/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs
@@ -94,6 +94,21 @@ namespace Ryujinx.Graphics.OpenGL
             return new ReadOnlySpan<byte>(_bufferMap.ToPointer(), size);
         }
 
+        public unsafe ReadOnlySpan<byte> GetTextureData(TextureView view, int size, int layer, int level)
+        {
+            EnsureBuffer(size);
+
+            GL.BindBuffer(BufferTarget.PixelPackBuffer, _copyBufferHandle);
+
+            int offset = view.WriteToPbo2D(0, layer, level);
+
+            GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
+
+            Sync();
+
+            return new ReadOnlySpan<byte>(_bufferMap.ToPointer(), size).Slice(offset);
+        }
+
         public unsafe ReadOnlySpan<byte> GetBufferData(BufferHandle buffer, int offset, int size)
         {
             EnsureBuffer(size);
diff --git a/Ryujinx.Graphics.Texture/LayoutConverter.cs b/Ryujinx.Graphics.Texture/LayoutConverter.cs
index 1039ea26a3..2b327375af 100644
--- a/Ryujinx.Graphics.Texture/LayoutConverter.cs
+++ b/Ryujinx.Graphics.Texture/LayoutConverter.cs
@@ -97,6 +97,7 @@ namespace Ryujinx.Graphics.Texture
             int width,
             int height,
             int depth,
+            int sliceDepth,
             int levels,
             int layers,
             int blockWidth,
@@ -172,6 +173,8 @@ namespace Ryujinx.Graphics.Texture
                     mipGobBlocksInZ,
                     bytesPerPixel);
 
+                int sd = Math.Max(1, sliceDepth >> level);
+
                 unsafe bool Convert<T>(Span<byte> output, ReadOnlySpan<byte> data) where T : unmanaged
                 {
                     fixed (byte* outputPtr = output, dataPtr = data)
@@ -181,7 +184,7 @@ namespace Ryujinx.Graphics.Texture
                         {
                             byte* inBaseOffset = dataPtr + (layer * sizeInfo.LayerSize + sizeInfo.GetMipOffset(level));
 
-                            for (int z = 0; z < d; z++)
+                            for (int z = 0; z < sd; z++)
                             {
                                 layoutConverter.SetZ(z);
                                 for (int y = 0; y < h; y++)
@@ -364,6 +367,7 @@ namespace Ryujinx.Graphics.Texture
             int width,
             int height,
             int depth,
+            int sliceDepth,
             int levels,
             int layers,
             int blockWidth,
@@ -432,6 +436,8 @@ namespace Ryujinx.Graphics.Texture
                     mipGobBlocksInZ,
                     bytesPerPixel);
 
+                int sd = Math.Max(1, sliceDepth >> level);
+
                 unsafe bool Convert<T>(Span<byte> output, ReadOnlySpan<byte> data) where T : unmanaged
                 {
                     fixed (byte* outputPtr = output, dataPtr = data)
@@ -441,7 +447,7 @@ namespace Ryujinx.Graphics.Texture
                         {
                             byte* outBaseOffset = outputPtr + (layer * sizeInfo.LayerSize + sizeInfo.GetMipOffset(level));
 
-                            for (int z = 0; z < d; z++)
+                            for (int z = 0; z < sd; z++)
                             {
                                 layoutConverter.SetZ(z);
                                 for (int y = 0; y < h; y++)
diff --git a/Ryujinx.Graphics.Texture/SizeCalculator.cs b/Ryujinx.Graphics.Texture/SizeCalculator.cs
index 7eee13c0cf..5568784f8f 100644
--- a/Ryujinx.Graphics.Texture/SizeCalculator.cs
+++ b/Ryujinx.Graphics.Texture/SizeCalculator.cs
@@ -43,6 +43,7 @@ namespace Ryujinx.Graphics.Texture
             int[] allOffsets = new int[is3D ? Calculate3DOffsetCount(levels, depth) : levels * layers * depth];
             int[] mipOffsets = new int[levels];
             int[] sliceSizes = new int[levels];
+            int[] levelSizes = new int[levels];
 
             int mipGobBlocksInY = gobBlocksInY;
             int mipGobBlocksInZ = gobBlocksInZ;
@@ -108,8 +109,9 @@ namespace Ryujinx.Graphics.Texture
 
                 mipOffsets[level] = layerSize;
                 sliceSizes[level] = totalBlocksOfGobsInY * robSize;
+                levelSizes[level] = totalBlocksOfGobsInZ * sliceSizes[level];
 
-                layerSize += totalBlocksOfGobsInZ * sliceSizes[level];
+                layerSize += levelSizes[level];
 
                 depthLevelOffset += d;
             }
@@ -152,7 +154,7 @@ namespace Ryujinx.Graphics.Texture
                 }
             }
 
-            return new SizeInfo(mipOffsets, allOffsets, sliceSizes, depth, levels, layerSize, totalSize, is3D);
+            return new SizeInfo(mipOffsets, allOffsets, sliceSizes, levelSizes, depth, levels, layerSize, totalSize, is3D);
         }
 
         public static SizeInfo GetLinearTextureSize(int stride, int height, int blockHeight)
diff --git a/Ryujinx.Graphics.Texture/SizeInfo.cs b/Ryujinx.Graphics.Texture/SizeInfo.cs
index 880d677b95..ed5379b344 100644
--- a/Ryujinx.Graphics.Texture/SizeInfo.cs
+++ b/Ryujinx.Graphics.Texture/SizeInfo.cs
@@ -13,6 +13,7 @@ namespace Ryujinx.Graphics.Texture
 
         public readonly int[] AllOffsets;
         public readonly int[] SliceSizes;
+        public readonly int[] LevelSizes;
         public int LayerSize { get; }
         public int TotalSize { get; }
 
@@ -21,6 +22,7 @@ namespace Ryujinx.Graphics.Texture
             _mipOffsets = new int[] { 0 };
             AllOffsets  = new int[] { 0 };
             SliceSizes  = new int[] { size };
+            LevelSizes  = new int[] { size };
             _depth      = 1;
             _levels     = 1;
             LayerSize   = size;
@@ -32,6 +34,7 @@ namespace Ryujinx.Graphics.Texture
             int[] mipOffsets,
             int[] allOffsets,
             int[] sliceSizes,
+            int[] levelSizes,
             int   depth,
             int   levels,
             int   layerSize,
@@ -41,6 +44,7 @@ namespace Ryujinx.Graphics.Texture
             _mipOffsets = mipOffsets;
             AllOffsets  = allOffsets;
             SliceSizes  = sliceSizes;
+            LevelSizes  = levelSizes;
             _depth      = depth;
             _levels     = levels;
             LayerSize   = layerSize;
@@ -99,7 +103,8 @@ namespace Ryujinx.Graphics.Texture
             {
                 for (int i = 0; i < _mipOffsets.Length; i++)
                 {
-                    yield return new Region(_mipOffsets[i], SliceSizes[i]);
+                    int maxSize = TotalSize - _mipOffsets[i];
+                    yield return new Region(_mipOffsets[i], Math.Min(maxSize, LevelSizes[i]));
                 }
             }
             else
diff --git a/Ryujinx.Memory/Range/MultiRange.cs b/Ryujinx.Memory/Range/MultiRange.cs
index 3d505c7caf..21ca5f093b 100644
--- a/Ryujinx.Memory/Range/MultiRange.cs
+++ b/Ryujinx.Memory/Range/MultiRange.cs
@@ -284,6 +284,11 @@ namespace Ryujinx.Memory.Range
         /// <returns>Total size in bytes</returns>
         public ulong GetSize()
         {
+            if (HasSingleRange)
+            {
+                return _singleRange.Size;
+            }
+
             ulong sum = 0;
 
             foreach (MemoryRange range in _ranges)
diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs
index 8830a4f555..cdbf5d6c5d 100644
--- a/Ryujinx/Ui/RendererWidgetBase.cs
+++ b/Ryujinx/Ui/RendererWidgetBase.cs
@@ -386,6 +386,7 @@ namespace Ryujinx.Ui
 
             Device.Gpu.Renderer.RunLoop(() =>
             {
+                Device.Gpu.SetGpuThread();
                 Device.Gpu.InitializeShaderCache();
                 Translator.IsReadyForTranslation.Set();