From b530f0e1104723b894695b2860cf0f568f24cc9a Mon Sep 17 00:00:00 2001
From: riperiperi <rhy3756547@hotmail.com>
Date: Tue, 2 Mar 2021 22:30:54 +0000
Subject: [PATCH] Texture Cache: "Texture Groups" and "Texture Dependencies"
 (#2001)

* Initial implementation (3d tex mips broken)

This works rather well for most games, just need to fix 3d texture mips.

* Cleanup

* Address feedback

* Copy Dependencies and various other fixes

* Fix layer/level offset for copy from view<->view.

* Remove dirty flag from dependency

The dirty flag behaviour is not needed - DeferredCopy is all we need.

* Fix tracking mip slices.

* Propagate granularity (fix astral chain)

* Address Feedback pt 1

* Save slice sizes as part of SizeInfo

* Fix nits

* Fix disposing multiple dependencies causing a crash

This list is obviously modified when removing dependencies, so create a copy of it.
---
 Ryujinx.Cpu/Tracking/CpuRegionHandle.cs       |   6 +-
 Ryujinx.Graphics.GAL/ITexture.cs              |   2 +
 Ryujinx.Graphics.Gpu/Engine/Methods.cs        |  10 -
 Ryujinx.Graphics.Gpu/Image/Texture.cs         | 293 ++++--
 .../Image/TextureCompatibility.cs             |  41 +-
 .../Image/TextureDependency.cs                |  37 +
 Ryujinx.Graphics.Gpu/Image/TextureGroup.cs    | 971 ++++++++++++++++++
 .../Image/TextureGroupHandle.cs               | 327 ++++++
 Ryujinx.Graphics.Gpu/Image/TextureInfo.cs     |  25 +
 Ryujinx.Graphics.Gpu/Image/TextureManager.cs  | 199 +++-
 .../Memory/GpuRegionHandle.cs                 |   4 +-
 .../Image/TextureBuffer.cs                    |  10 +
 Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs  |  41 +-
 Ryujinx.Graphics.OpenGL/Image/TextureView.cs  |  65 +-
 Ryujinx.Graphics.Texture/SizeCalculator.cs    |  29 +-
 Ryujinx.Graphics.Texture/SizeInfo.cs          |  43 +-
 Ryujinx.Memory/Tracking/IRegionHandle.cs      |   2 +-
 Ryujinx.Memory/Tracking/RegionHandle.cs       |  30 +-
 18 files changed, 1915 insertions(+), 220 deletions(-)
 create mode 100644 Ryujinx.Graphics.Gpu/Image/TextureDependency.cs
 create mode 100644 Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
 create mode 100644 Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs

diff --git a/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs b/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs
index 9dbdbfcbc..f4391aad4 100644
--- a/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs
+++ b/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs
@@ -1,4 +1,5 @@
 using Ryujinx.Memory.Tracking;
+using System;
 
 namespace Ryujinx.Cpu.Tracking
 {
@@ -18,6 +19,9 @@ namespace Ryujinx.Cpu.Tracking
 
         public void Dispose() => _impl.Dispose();
         public void RegisterAction(RegionSignal action) => _impl.RegisterAction(action);
-        public void Reprotect() => _impl.Reprotect();
+        public void RegisterDirtyEvent(Action action) => _impl.RegisterDirtyEvent(action);
+        public void Reprotect(bool asDirty = false) => _impl.Reprotect(asDirty);
+
+        public bool OverlapsWith(ulong address, ulong size) => _impl.OverlapsWith(address, size);
     }
 }
diff --git a/Ryujinx.Graphics.GAL/ITexture.cs b/Ryujinx.Graphics.GAL/ITexture.cs
index 543f9de08..ad8fd297c 100644
--- a/Ryujinx.Graphics.GAL/ITexture.cs
+++ b/Ryujinx.Graphics.GAL/ITexture.cs
@@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.GAL
         float ScaleFactor { get; }
 
         void CopyTo(ITexture destination, int firstLayer, int firstLevel);
+        void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel);
         void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter);
 
         ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel);
@@ -16,6 +17,7 @@ namespace Ryujinx.Graphics.GAL
         byte[] GetData();
 
         void SetData(ReadOnlySpan<byte> data);
+        void SetData(ReadOnlySpan<byte> data, int layer, int level);
         void SetStorage(BufferRange buffer);
         void Release();
     }
diff --git a/Ryujinx.Graphics.Gpu/Engine/Methods.cs b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
index a41fd5414..0731f1c2b 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Methods.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
@@ -377,11 +377,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
                 Texture color = TextureManager.FindOrCreateTexture(colorState, samplesInX, samplesInY, sizeHint);
 
                 changedScale |= TextureManager.SetRenderTargetColor(index, color);
-
-                if (color != null)
-                {
-                    color.SignalModified();
-                }
             }
 
             bool dsEnable = state.Get<Boolean32>(MethodOffset.RtDepthStencilEnable);
@@ -406,11 +401,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
                 UpdateViewportTransform(state);
                 UpdateScissorState(state);
             }
-
-            if (depthStencil != null)
-            {
-                depthStencil.SignalModified();
-            }
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/Texture.cs b/Ryujinx.Graphics.Gpu/Image/Texture.cs
index 6dfe46288..4d4091cb1 100644
--- a/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -50,6 +50,11 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public TextureScaleMode ScaleMode { get; private set; }
 
+        /// <summary>
+        /// Group that this texture belongs to. Manages read/write memory tracking.
+        /// </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>
@@ -63,10 +68,11 @@ namespace Ryujinx.Graphics.Gpu.Image
 
         private int _depth;
         private int _layers;
-        private int _firstLayer;
-        private int _firstLevel;
+        public int FirstLayer { get; private set; }
+        public int FirstLevel { get; private set; }
 
         private bool _hasData;
+        private bool _dirty = true;
         private int _updateCount;
         private byte[] _currentData;
 
@@ -99,12 +105,20 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public MultiRange Range { get; private set; }
 
+        /// <summary>
+        /// Layer size in bytes.
+        /// </summary>
+        public int LayerSize => _sizeInfo.LayerSize;
+
         /// <summary>
         /// Texture size in bytes.
         /// </summary>
         public ulong Size => (ulong)_sizeInfo.TotalSize;
 
-        private GpuRegionHandle _memoryTracking;
+        /// <summary>
+        /// Whether or not the texture belongs is a view.
+        /// </summary>
+        public bool IsView => _viewStorage != this;
 
         private int _referenceCount;
 
@@ -131,8 +145,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             InitializeTexture(context, info, sizeInfo, range);
 
-            _firstLayer = firstLayer;
-            _firstLevel = firstLevel;
+            FirstLayer = firstLayer;
+            FirstLevel = firstLevel;
 
             ScaleFactor = scaleFactor;
             ScaleMode = scaleMode;
@@ -186,8 +200,6 @@ 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)
         {
-            _memoryTracking = _context.PhysicalMemory.BeginTracking(Range);
-
             if (withData)
             {
                 Debug.Assert(!isView);
@@ -203,12 +215,13 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
             else
             {
-                // Don't update this texture the next time we synchronize.
-                ConsumeModified();
                 _hasData = true;
 
                 if (!isView)
                 {
+                    // Don't update this texture the next time we synchronize.
+                    ConsumeModified();
+
                     if (ScaleMode == TextureScaleMode.Scaled)
                     {
                         // Don't need to start at 1x as there is no data to scale, just go straight to the target scale.
@@ -221,6 +234,18 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Initialize a new texture group with this texture as storage.
+        /// </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)
+        {
+            Group = new TextureGroup(_context, this);
+
+            Group.Initialize(ref _sizeInfo, hasLayerViews, hasMipViews);
+        }
+
         /// <summary>
         /// Create a texture view from this texture.
         /// A texture view is defined as a child texture, from a sub-range of their parent texture.
@@ -240,8 +265,8 @@ namespace Ryujinx.Graphics.Gpu.Image
                 info,
                 sizeInfo,
                 range,
-                _firstLayer + firstLayer,
-                _firstLevel + firstLevel,
+                FirstLayer + firstLayer,
+                FirstLevel + firstLevel,
                 ScaleFactor,
                 ScaleMode);
 
@@ -259,11 +284,26 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="texture">The child texture</param>
         private void AddView(Texture texture)
         {
-            DisableMemoryTracking();
+            IncrementReferenceCount();
 
             _views.Add(texture);
 
             texture._viewStorage = this;
+
+            Group.UpdateViews(_views);
+
+            if (texture.Group != null && texture.Group != Group)
+            {
+                if (texture.Group.Storage == texture)
+                {
+                    // This texture's group is no longer used.
+                    Group.Inherit(texture.Group);
+
+                    texture.Group.Dispose();
+                }
+            }
+
+            texture.Group = Group;
         }
 
         /// <summary>
@@ -276,7 +316,27 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             texture._viewStorage = texture;
 
-            DeleteIfNotUsed();
+            DecrementReferenceCount();
+        }
+
+        /// <summary>
+        /// Create a copy dependency to a texture that is view compatible with this one.
+        /// When either texture is modified, the texture data will be copied to the other to keep them in sync.
+        /// This is essentially an emulated view, useful for handling multiple view parents or format incompatibility.
+        /// This also forces a copy on creation, to or from the given texture to get them in sync immediately.
+        /// </summary>
+        /// <param name="contained">The view compatible texture to create a dependency to</param>
+        /// <param name="layer">The base layer of the given texture relative to this one</param>
+        /// <param name="level">The base level of the given texture relative to 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(Texture contained, int layer, int level, bool copyTo)
+        {
+            if (contained.Group == Group)
+            {
+                return;
+            }
+
+            Group.CreateCopyDependency(contained, FirstLayer + layer, FirstLevel + level, copyTo);
         }
 
         /// <summary>
@@ -294,12 +354,12 @@ namespace Ryujinx.Graphics.Gpu.Image
             int blockWidth = Info.FormatInfo.BlockWidth;
             int blockHeight = Info.FormatInfo.BlockHeight;
 
-            width  <<= _firstLevel;
-            height <<= _firstLevel;
+            width  <<= FirstLevel;
+            height <<= FirstLevel;
 
             if (Target == Target.Texture3D)
             {
-                depthOrLayers <<= _firstLevel;
+                depthOrLayers <<= FirstLevel;
             }
             else
             {
@@ -310,14 +370,14 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             foreach (Texture view in _viewStorage._views)
             {
-                int viewWidth  = Math.Max(1, width  >> view._firstLevel);
-                int viewHeight = Math.Max(1, height >> view._firstLevel);
+                int viewWidth  = Math.Max(1, width  >> view.FirstLevel);
+                int viewHeight = Math.Max(1, height >> view.FirstLevel);
 
                 int viewDepthOrLayers;
 
                 if (view.Info.Target == Target.Texture3D)
                 {
-                    viewDepthOrLayers = Math.Max(1, depthOrLayers >> view._firstLevel);
+                    viewDepthOrLayers = Math.Max(1, depthOrLayers >> view.FirstLevel);
                 }
                 else
                 {
@@ -328,16 +388,6 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
-        /// <summary>
-        /// Disables memory tracking on this texture. Currently used for view containers, as we assume their views are covering all memory regions.
-        /// Textures with disabled memory tracking also cannot flush in most circumstances.
-        /// </summary>
-        public void DisableMemoryTracking()
-        {
-            _memoryTracking?.Dispose();
-            _memoryTracking = null;
-        }
-
         /// <summary>
         /// Recreates the texture storage (or view, in the case of child textures) of this texture.
         /// This allows recreating the texture with a new size.
@@ -393,7 +443,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             if (_viewStorage != this)
             {
-                ReplaceStorage(_viewStorage.HostTexture.CreateView(createInfo, _firstLayer, _firstLevel));
+                ReplaceStorage(_viewStorage.HostTexture.CreateView(createInfo, FirstLayer, FirstLevel));
             }
             else
             {
@@ -495,7 +545,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     view.ScaleFactor = scale;
 
                     TextureCreateInfo viewCreateInfo = TextureManager.GetCreateInfo(view.Info, _context.Capabilities, scale);
-                    ITexture newView = HostTexture.CreateView(viewCreateInfo, view._firstLayer - _firstLayer, view._firstLevel - _firstLevel);
+                    ITexture newView = HostTexture.CreateView(viewCreateInfo, view.FirstLayer - FirstLayer, view.FirstLevel - FirstLevel);
 
                     view.ReplaceStorage(newView);
                     view.ScaleMode = newScaleMode;
@@ -517,17 +567,10 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// Checks if the memory for this texture was modified, and returns true if it was.
         /// The modified flags are consumed as a result.
         /// </summary>
-        /// <remarks>
-        /// If there is no memory tracking for this texture, it will always report as modified.
-        /// </remarks>
         /// <returns>True if the texture was modified, false otherwise.</returns>
         public bool ConsumeModified()
         {
-            bool wasDirty = _memoryTracking?.Dirty ?? true;
-
-            _memoryTracking?.Reprotect();
-
-            return wasDirty;
+            return Group.ConsumeDirty(this);
         }
 
         /// <summary>
@@ -544,17 +587,42 @@ namespace Ryujinx.Graphics.Gpu.Image
                 return;
             }
 
-            if (_hasData)
+            if (!_dirty)
             {
-                if (_memoryTracking?.Dirty != true)
-                {
-                    return;
-                }
-
-                BlacklistScale();
+                return;
             }
 
-            _memoryTracking?.Reprotect();
+            _dirty = false;
+
+            if (_hasData)
+            {
+                Group.SynchronizeMemory(this);
+            }
+            else
+            {
+                Group.ConsumeDirty(this);
+                SynchronizeFull();
+            }
+        }
+
+        /// <summary>
+        /// Signal that this texture is dirty, indicating that the texture group must be checked.
+        /// </summary>
+        public void SignalGroupDirty()
+        {
+            _dirty = true;
+        }
+
+        /// <summary>
+        /// Fully synchronizes guest and host memory. 
+        /// This will replace the entire texture with the data present in guest memory.
+        /// </summary>
+        public void SynchronizeFull()
+        {
+            if (_hasData)
+            {
+                BlacklistScale();
+            }
 
             ReadOnlySpan<byte> data = _context.PhysicalMemory.GetSpan(Range);
 
@@ -596,7 +664,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             BlacklistScale();
 
-            _memoryTracking?.Reprotect();
+            Group.ConsumeDirty(this);
 
             IsModified = false;
 
@@ -605,18 +673,46 @@ namespace Ryujinx.Graphics.Gpu.Image
             _hasData = true;
         }
 
+        /// <summary>
+        /// Uploads new texture data to the host GPU for a specific layer/level.
+        /// </summary>
+        /// <param name="data">New data</param>
+        /// <param name="layer">Target layer</param>
+        /// <param name="level">Target level</param>
+        public void SetData(ReadOnlySpan<byte> data, int layer, int level)
+        {
+            BlacklistScale();
+
+            HostTexture.SetData(data, layer, level);
+
+            _currentData = null;
+
+            _hasData = true;
+        }
+
         /// <summary>
         /// Converts texture data to a format and layout that is supported by the host GPU.
         /// </summary>
         /// <param name="data">Data to be converted</param>
         /// <returns>Converted data</returns>
-        private ReadOnlySpan<byte> ConvertToHostCompatibleFormat(ReadOnlySpan<byte> data)
+        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 layers = single ? 1 : _layers;
+            int levels = single ? 1 : Info.Levels;
+
+            width = Math.Max(width >> level, 1);
+            height = Math.Max(height >> level, 1);
+            depth = Math.Max(depth >> level, 1);
+
             if (Info.IsLinear)
             {
                 data = LayoutConverter.ConvertLinearStridedToLinear(
-                    Info.Width,
-                    Info.Height,
+                    width,
+                    height,
                     Info.FormatInfo.BlockWidth,
                     Info.FormatInfo.BlockHeight,
                     Info.Stride,
@@ -626,11 +722,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             else
             {
                 data = LayoutConverter.ConvertBlockLinearToLinear(
-                    Info.Width,
-                    Info.Height,
-                    _depth,
-                    Info.Levels,
-                    _layers,
+                    width,
+                    height,
+                    depth,
+                    levels,
+                    layers,
                     Info.FormatInfo.BlockWidth,
                     Info.FormatInfo.BlockHeight,
                     Info.FormatInfo.BytesPerPixel,
@@ -650,11 +746,11 @@ namespace Ryujinx.Graphics.Gpu.Image
                     data.ToArray(),
                     Info.FormatInfo.BlockWidth,
                     Info.FormatInfo.BlockHeight,
-                    Info.Width,
-                    Info.Height,
-                    _depth,
-                    Info.Levels,
-                    _layers,
+                    width,
+                    height,
+                    depth,
+                    levels,
+                    layers,
                     out Span<byte> decoded))
                 {
                     string texInfo = $"{Info.Target} {Info.FormatInfo.Format} {Info.Width}x{Info.Height}x{Info.DepthOrLayers} levels {Info.Levels}";
@@ -666,11 +762,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
             else if (Target == Target.Texture3D && Info.FormatInfo.Format.IsBc4())
             {
-                data = BCnDecoder.DecodeBC4(data, Info.Width, Info.Height, _depth, Info.Levels, _layers, Info.FormatInfo.Format == Format.Bc4Snorm);
+                data = BCnDecoder.DecodeBC4(data, width, height, depth, levels, layers, Info.FormatInfo.Format == Format.Bc4Snorm);
             }
             else if (Target == Target.Texture3D && Info.FormatInfo.Format.IsBc5())
             {
-                data = BCnDecoder.DecodeBC5(data, Info.Width, Info.Height, _depth, Info.Levels, _layers, Info.FormatInfo.Format == Format.Bc5Snorm);
+                data = BCnDecoder.DecodeBC5(data, width, height, depth, levels, layers, Info.FormatInfo.Format == Format.Bc5Snorm);
             }
 
             return data;
@@ -710,7 +806,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public void ExternalFlush(ulong address, ulong size)
         {
-            if (!IsModified || _memoryTracking == null)
+            if (!IsModified)
             {
                 return;
             }
@@ -869,7 +965,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <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, out int firstLayer, out int firstLevel)
+        public TextureViewCompatibility IsViewCompatible(TextureInfo info, MultiRange range, int layerSize, out int firstLayer, out int firstLevel)
         {
             int offset = Range.FindOffset(range);
 
@@ -892,15 +988,17 @@ namespace Ryujinx.Graphics.Gpu.Image
                 return TextureViewCompatibility.Incompatible;
             }
 
-            if (!TextureCompatibility.ViewFormatCompatible(Info, info))
+            if (info.GetSlices() > 1 && LayerSize != layerSize)
             {
                 return TextureViewCompatibility.Incompatible;
             }
 
             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));
 
             return (Info.SamplesInX == info.SamplesInX &&
                     Info.SamplesInY == info.SamplesInY) ? result : TextureViewCompatibility.Incompatible;
@@ -1003,14 +1101,37 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="firstLevel">The first level of the view</param>
         public void ReplaceView(Texture parent, TextureInfo info, ITexture hostTexture, int firstLayer, int firstLevel)
         {
+            IncrementReferenceCount();
             parent._viewStorage.SynchronizeMemory();
+
+            // If this texture has views, they must be given to the new parent.
+            if (_views.Count > 0)
+            {
+                Texture[] viewCopy = _views.ToArray();
+
+                foreach (Texture view in viewCopy)
+                {
+                    TextureCreateInfo createInfo = TextureManager.GetCreateInfo(view.Info, _context.Capabilities, ScaleFactor);
+
+                    ITexture newView = parent.HostTexture.CreateView(createInfo, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel); 
+
+                    view.ReplaceView(parent, view.Info, newView, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel);
+                }
+            }
+
             ReplaceStorage(hostTexture);
 
-            _firstLayer = parent._firstLayer + firstLayer;
-            _firstLevel = parent._firstLevel + firstLevel;
+            if (_viewStorage != this)
+            {
+                _viewStorage.RemoveView(this);
+            }
+
+            FirstLayer = parent.FirstLayer + firstLayer;
+            FirstLevel = parent.FirstLevel + firstLevel;
             parent._viewStorage.AddView(this);
 
             SetInfo(info);
+            DecrementReferenceCount();
         }
 
         /// <summary>
@@ -1031,14 +1152,28 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         public void SignalModified()
         {
-            IsModified = true;
-
-            if (_viewStorage != this)
+            bool wasModified = IsModified;
+            if (!wasModified || Group.HasCopyDependencies)
             {
-                _viewStorage.SignalModified();
+                IsModified = true;
+                Group.SignalModified(this, !wasModified);
             }
+        }
 
-            _memoryTracking?.RegisterAction(ExternalFlush);
+        /// <summary>
+        /// Signals that a texture has been bound, or has been unbound.
+        /// During this time, lazy copies will not clear the dirty flag.
+        /// </summary>
+        /// <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)
+            {
+                IsModified = true;
+                Group.SignalModifying(this, bound, !wasModified);
+            }
         }
 
         /// <summary>
@@ -1066,7 +1201,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             foreach (Texture view in _views)
             {
-                if (texture.IsViewCompatible(view.Info, view.Range, out _, out _) != TextureViewCompatibility.Incompatible)
+                if (texture.IsViewCompatible(view.Info, view.Range, view.LayerSize, out _, out _) != TextureViewCompatibility.Incompatible)
                 {
                     return true;
                 }
@@ -1148,10 +1283,6 @@ namespace Ryujinx.Graphics.Gpu.Image
         public void Unmapped()
         {
             IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped.
-
-            var tracking = _memoryTracking;
-            tracking?.Reprotect();
-            tracking?.RegisterAction(null);
         }
 
         /// <summary>
@@ -1162,7 +1293,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             DisposeTextures();
 
             Disposed?.Invoke(this);
-            _memoryTracking?.Dispose();
+
+            if (Group.Storage == this)
+            {
+                Group.Dispose();
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
index e3574be5d..d613612ff 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
@@ -215,7 +215,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="lhs">Texture information of the texture view</param>
         /// <param name="rhs">Texture information of the texture view to match against</param>
         /// <param name="level">Mipmap level of the texture view in relation to this texture</param>
-        /// <returns>True if the sizes are compatible, false otherwise</returns>
+        /// <returns>The view compatibility level of the view sizes</returns>
         public static TextureViewCompatibility ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level)
         {
             Size size = GetAlignedSize(lhs, level);
@@ -235,6 +235,27 @@ namespace Ryujinx.Graphics.Gpu.Image
                     size.Height == otherSize.Height) ? result : TextureViewCompatibility.Incompatible;
         }
 
+        /// <summary>
+        /// Checks if the potential child texture fits within the level and layer bounds of the parent.
+        /// </summary>
+        /// <param name="parent">Texture information for the parent</param>
+        /// <param name="child">Texture information for the child</param>
+        /// <param name="layer">Base layer of the child texture</param>
+        /// <param name="level">Base level of the child texture</param>
+        /// <returns>Full compatiblity if the child's layer and level count fit within the parent, incompatible otherwise</returns>
+        public static TextureViewCompatibility ViewSubImagesInBounds(TextureInfo parent, TextureInfo child, int layer, int level)
+        {
+            if (level + child.Levels <= parent.Levels &&
+                layer + child.GetSlices() <= parent.GetSlices())
+            {
+                return TextureViewCompatibility.Full;
+            }
+            else
+            {
+                return TextureViewCompatibility.Incompatible;
+            }
+        }
+
         /// <summary>
         /// Checks if the texture sizes of the supplied texture informations match.
         /// </summary>
@@ -382,10 +403,22 @@ 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>
-        /// <returns>True if the formats are compatible, false otherwise</returns>
-        public static bool ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs)
+        /// <returns>The view compatibility level of the texture formats</returns>
+        public static TextureViewCompatibility ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs)
         {
-            return FormatCompatible(lhs.FormatInfo, rhs.FormatInfo);
+            if (FormatCompatible(lhs.FormatInfo, rhs.FormatInfo))
+            {
+                if (lhs.FormatInfo.IsCompressed != rhs.FormatInfo.IsCompressed)
+                {
+                    return TextureViewCompatibility.CopyOnly;
+                }
+                else
+                {
+                    return TextureViewCompatibility.Full;
+                }
+            }
+
+            return TextureViewCompatibility.Incompatible;
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs b/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs
new file mode 100644
index 000000000..269ddbd97
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs
@@ -0,0 +1,37 @@
+namespace Ryujinx.Graphics.Gpu.Image
+{
+    /// <summary>
+    /// One side of a two-way dependency between one texture view and another.
+    /// Contains a reference to the handle owning the dependency, and the other dependency.
+    /// </summary>
+    class TextureDependency
+    {
+        /// <summary>
+        /// The handle that owns this dependency.
+        /// </summary>
+        public TextureGroupHandle Handle;
+
+        /// <summary>
+        /// The other dependency linked to this one, which belongs to another handle.
+        /// </summary>
+        public TextureDependency Other;
+
+        /// <summary>
+        /// Create a new texture dependency.
+        /// </summary>
+        /// <param name="handle">The handle that owns the dependency</param>
+        public TextureDependency(TextureGroupHandle handle)
+        {
+            Handle = handle;
+        }
+
+        /// <summary>
+        /// Signal that the owner of this dependency has been modified,
+        /// meaning that the other dependency's handle must defer a copy from it.
+        /// </summary>
+        public void SignalModified()
+        {
+            Other.Handle.DeferCopy(Handle);
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
new file mode 100644
index 000000000..5d150559f
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -0,0 +1,971 @@
+using Ryujinx.Common;
+using Ryujinx.Cpu.Tracking;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Texture;
+using Ryujinx.Memory.Range;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.Gpu.Image
+{
+    /// <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.
+    /// The group iteratively adds more granular tracking as views of different kinds are added.
+    /// Note that a texture group can be absorbed into another when it becomes a view parent.
+    /// </summary>
+    class TextureGroup : IDisposable
+    {
+        private const int StrideAlignment = 32;
+        private const int GobAlignment = 64;
+
+        private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false);
+
+        /// <summary>
+        /// The storage texture associated with this group.
+        /// </summary>
+        public Texture Storage { get; }
+
+        /// <summary>
+        /// Indicates if the texture has copy dependencies. If true, then all modifications
+        /// must be signalled to the group, rather than skipping ones still to be flushed.
+        /// </summary>
+        public bool HasCopyDependencies { get; set; }
+
+        private GpuContext _context;
+
+        private int[] _allOffsets;
+        private int[] _sliceSizes;
+        private bool _is3D;
+        private bool _hasMipViews;
+        private bool _hasLayerViews;
+        private int _layers;
+        private int _levels;
+
+        private MultiRange TextureRange => Storage.Range;
+
+        /// <summary>
+        /// The views list from the storage texture.
+        /// </summary>
+        private List<Texture> _views;
+        private TextureGroupHandle[] _handles;
+        private bool[] _loadNeeded;
+
+        /// <summary>
+        /// Create a new texture group.
+        /// </summary>
+        /// <param name="context">GPU context that the texture group belongs to</param>
+        /// <param name="storage">The storage texture for this group</param>
+        public TextureGroup(GpuContext context, Texture storage)
+        {
+            Storage = storage;
+            _context = context;
+
+            _is3D = storage.Info.Target == Target.Texture3D;
+            _layers = storage.Info.GetSlices();
+            _levels = storage.Info.Levels;
+        }
+
+        /// <summary>
+        /// Initialize a new texture group's dirty regions and offsets.
+        /// </summary>
+        /// <param name="size">Size info for the storage texture</param>
+        /// <param name="hasLayerViews">True if the storage will have layer views</param>
+        /// <param name="hasMipViews">True if the storage will have mip views</param>
+        public void Initialize(ref SizeInfo size, bool hasLayerViews, bool hasMipViews)
+        {
+            _allOffsets = size.AllOffsets;
+            _sliceSizes = size.SliceSizes;
+
+            (_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
+
+            RecalculateHandleRegions();
+        }
+
+        /// <summary>
+        /// Consume the dirty flags for a given texture. The state is shared between views of the same layers and levels.
+        /// </summary>
+        /// <param name="texture">The texture being used</param>
+        /// <returns>True if a flag was dirty, false otherwise</returns>
+        public bool ConsumeDirty(Texture texture)
+        {
+            bool dirty = false;
+
+            EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
+            {
+                for (int i = 0; i < regionCount; i++)
+                {
+                    TextureGroupHandle group = _handles[baseHandle + i];
+
+                    foreach (CpuRegionHandle handle in group.Handles)
+                    {
+                        if (handle.Dirty)
+                        {
+                            handle.Reprotect();
+                            dirty = true;
+                        }
+                    }
+                }
+            });
+
+            return dirty;
+        }
+
+        /// <summary>
+        /// Synchronize memory for a given texture. 
+        /// If overlapping tracking handles are dirty, fully or partially synchronize the texture data.
+        /// </summary>
+        /// <param name="texture">The texture being used</param>
+        public void SynchronizeMemory(Texture texture)
+        {
+            EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
+            {
+                bool dirty = false;
+                bool anyModified = false;
+
+                for (int i = 0; i < regionCount; i++)
+                {
+                    TextureGroupHandle group = _handles[baseHandle + i];
+
+                    bool modified = group.Modified;
+                    bool handleDirty = false;
+                    bool handleModified = false;
+
+                    foreach (CpuRegionHandle handle in group.Handles)
+                    {
+                        if (handle.Dirty)
+                        {
+                            handle.Reprotect();
+                            handleDirty = true;
+                        }
+                        else
+                        {
+                            handleModified |= modified;
+                        }
+                    }
+
+                    // 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())
+                    {
+                        anyModified |= true; // The copy target has been modified.
+                        handleDirty = false;
+                    }
+                    else
+                    {
+                        anyModified |= handleModified;
+                        dirty |= handleDirty;
+                    }
+
+                    if (group.NeedsCopy)
+                    {
+                        // The texture we copied from is still being written to. Copy from it again the next time this texture is used.
+                        texture.SignalGroupDirty();
+                    }
+
+                    _loadNeeded[baseHandle + i] = handleDirty;
+                }
+
+                if (dirty)
+                {
+                    if (_handles.Length > 1 && (anyModified || split))
+                    {
+                        // Partial texture invalidation. Only update the layers/levels with dirty flags of the storage.
+
+                        SynchronizePartial(baseHandle, regionCount);
+                    }
+                    else
+                    {
+                        // Full texture invalidation.
+
+                        texture.SynchronizeFull();
+                    }
+                }
+            });
+        }
+
+        /// <summary>
+        /// Synchronize part of the storage texture, represented by a given range of handles.
+        /// Only handles marked by the _loadNeeded array will be synchronized.
+        /// </summary>
+        /// <param name="baseHandle">The base index of the range of handles</param>
+        /// <param name="regionCount">The number of handles to synchronize</param>
+        private void SynchronizePartial(int baseHandle, int regionCount)
+        {
+            ReadOnlySpan<byte> fullData = _context.PhysicalMemory.GetSpan(Storage.Range);
+
+            for (int i = 0; i < regionCount; i++)
+            {
+                if (_loadNeeded[baseHandle + i])
+                {
+                    var info = GetHandleInformation(baseHandle + i);
+                    int offsetIndex = info.Index;
+
+                    // Only one of these will be greater than 1, as partial sync is only called when there are sub-image views.
+                    for (int layer = 0; layer < info.Layers; layer++)
+                    {
+                        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 size = endOffset - offset;
+
+                            ReadOnlySpan<byte> data = fullData.Slice(offset, size);
+
+                            data = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel, true);
+
+                            Storage.SetData(data, info.BaseLayer, info.BaseLevel);
+
+                            offsetIndex++;
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <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)
+        {
+            EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
+            {
+                for (int i = 0; i < regionCount; i++)
+                {
+                    TextureGroupHandle group = _handles[baseHandle + i];
+
+                    group.SignalModified();
+
+                    if (registerAction)
+                    {
+                        RegisterAction(group);
+                    }
+                }
+            });
+        }
+
+        /// <summary>
+        /// Signal that a texture in the group is actively bound, or has been unbound by the GPU.
+        /// </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)
+        {
+            EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
+            {
+                for (int i = 0; i < regionCount; i++)
+                {
+                    TextureGroupHandle group = _handles[baseHandle + i];
+
+                    group.SignalModifying(bound);
+
+                    if (registerAction)
+                    {
+                        RegisterAction(group);
+                    }
+                }
+            });
+        }
+
+        /// <summary>
+        /// Register a read/write action to flush for a texture group.
+        /// </summary>
+        /// <param name="group">The group to register an action for</param>
+        public void RegisterAction(TextureGroupHandle group)
+        {
+            foreach (CpuRegionHandle handle in group.Handles)
+            {
+                handle.RegisterAction((address, size) => FlushAction(group, address, size));
+            }
+        }
+
+        /// <summary>
+        /// Propagates the mip/layer view flags depending on the texture type.
+        /// When the most granular type of subresource has views, the other type of subresource must be segmented granularly too.
+        /// </summary>
+        /// <param name="hasLayerViews">True if the storage has layer views</param>
+        /// <param name="hasMipViews">True if the storage has mip views</param>
+        /// <returns>The input values after propagation</returns>
+        private (bool HasLayerViews, bool HasMipViews) PropagateGranularity(bool hasLayerViews, bool hasMipViews)
+        {
+            if (_is3D)
+            {
+                hasMipViews |= hasLayerViews;
+            }
+            else
+            {
+                hasLayerViews |= hasMipViews;
+            }
+
+            return (hasLayerViews, hasMipViews);
+        }
+
+        /// <summary>
+        /// Evaluate the range of tracking handles which a view texture overlaps with.
+        /// </summary>
+        /// <param name="texture">The texture to get handles for</param>
+        /// <param name="callback">
+        /// A function to be called with the base index of the range of handles for the given texture, and the number of handles it covers.
+        /// This can be called for multiple disjoint ranges, if required.
+        /// </param>
+        private void EvaluateRelevantHandles(Texture texture, HandlesCallbackDelegate callback)
+        {
+            if (texture == Storage || !(_hasMipViews || _hasLayerViews))
+            {
+                callback(0, _handles.Length);
+
+                return;
+            }
+
+            EvaluateRelevantHandles(texture.FirstLayer, texture.FirstLevel, texture.Info.GetSlices(), texture.Info.Levels, callback);
+        }
+
+        /// <summary>
+        /// Evaluate the range of tracking handles which a view texture overlaps with, 
+        /// using the view's position and slice/level counts.
+        /// </summary>
+        /// <param name="firstLayer">The first layer of the texture</param>
+        /// <param name="firstLevel">The first level of the texture</param>
+        /// <param name="slices">The slice count of the texture</param>
+        /// <param name="levels">The level count of the texture</param>
+        /// <param name="callback">
+        /// A function to be called with the base index of the range of handles for the given texture, and the number of handles it covers.
+        /// This can be called for multiple disjoint ranges, if required.
+        /// </param>
+        private void EvaluateRelevantHandles(int firstLayer, int firstLevel, int slices, int levels, HandlesCallbackDelegate callback)
+        {
+            int targetLayerHandles = _hasLayerViews ? slices : 1;
+            int targetLevelHandles = _hasMipViews ? levels : 1;
+
+            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.
+
+                    callback(firstLevel, targetLevelHandles);
+                }
+                else
+                {
+                    (int levelIndex, int layerCount) = Get3DLevelRange(firstLevel);
+
+                    if (levels > 1 && slices < _layers)
+                    {
+                        // The given texture only covers some of the depth of multiple mips. (a "depth slice")
+                        // Callback with each mip's range separately.
+                        // Can assume that the group is fully subdivided (both slices and levels > 1 for storage)
+
+                        while (levels-- > 1)
+                        {
+                            callback(firstLayer + levelIndex, slices);
+
+                            levelIndex += layerCount;
+                            layerCount = Math.Max(layerCount >> 1, 1);
+                            slices = Math.Max(layerCount >> 1, 1);
+                        }
+                    }
+                    else
+                    {
+                        int totalSize = Math.Min(layerCount, slices);
+
+                        while (levels-- > 1)
+                        {
+                            layerCount = Math.Max(layerCount >> 1, 1);
+                            totalSize += layerCount;
+                        }
+
+                        callback(firstLayer + levelIndex, totalSize);
+                    }
+                }
+            }
+            else
+            {
+                // Future layers come after all mipmaps of the last.
+                int levelHandles = _hasMipViews ? _levels : 1;
+
+                if (slices > 1 && levels < _levels)
+                {
+                    // The given texture only covers some of the mipmaps of multiple slices. (a "mip slice")
+                    // Callback with each layer's range separately.
+                    // Can assume that the group is fully subdivided (both slices and levels > 1 for storage)
+
+                    for (int i = 0; i < slices; i++)
+                    {
+                        callback(firstLevel + (firstLayer + i) * levelHandles, targetLevelHandles, true);
+                    }
+                }
+                else
+                {
+                    callback(firstLevel + firstLayer * levelHandles, targetLevelHandles + (targetLayerHandles - 1) * levelHandles);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Get the range of offsets for a given mip level of a 3D texture.
+        /// </summary>
+        /// <param name="level">The level to return</param>
+        /// <returns>Start index and count of offsets for the given level</returns>
+        private (int Index, int Count) Get3DLevelRange(int level)
+        {
+            int index = 0;
+            int count = _layers; // Depth. Halves with each mip level.
+
+            while (level-- > 0)
+            {
+                index += count;
+                count = Math.Max(count >> 1, 1);
+            }
+
+            return (index, count);
+        }
+
+        /// <summary>
+        /// Get view information for a single tracking handle.
+        /// </summary>
+        /// <param name="handleIndex">The index of the handle</param>
+        /// <returns>The layers and levels that the handle covers, and its index in the offsets array</returns>
+        private (int BaseLayer, int BaseLevel, int Levels, int Layers, int Index) GetHandleInformation(int handleIndex)
+        {
+            int baseLayer;
+            int baseLevel;
+            int levels = _hasMipViews ? 1 : _levels;
+            int layers = _hasLayerViews ? 1 : _layers;
+            int index;
+
+            if (_is3D)
+            {
+                if (_hasLayerViews)
+                {
+                    // NOTE: Will also have mip views, or only one level in storage.
+
+                    index = handleIndex;
+                    baseLevel = 0;
+
+                    int layerLevels = _levels;
+
+                    while (handleIndex >= layerLevels)
+                    {
+                        handleIndex -= layerLevels;
+                        baseLevel++;
+                        layerLevels = Math.Max(layerLevels >> 1, 1);
+                    }
+
+                    baseLayer = handleIndex;
+                } 
+                else
+                {
+                    baseLayer = 0;
+                    baseLevel = handleIndex;
+
+                    (index, _) = Get3DLevelRange(baseLevel);
+                }
+            }
+            else
+            {
+                baseLevel = _hasMipViews ? handleIndex % _levels : 0;
+                baseLayer = _hasMipViews ? handleIndex / _levels : handleIndex;
+                index = baseLevel + baseLayer * _levels;
+            }
+
+            return (baseLayer, baseLevel, levels, layers, index);
+        }
+
+        /// <summary>
+        /// Gets the layer and level for a given view.
+        /// </summary>
+        /// <param name="index">The index of the view</param>
+        /// <returns>The layer and level of the specified view</returns>
+        private (int BaseLayer, int BaseLevel) GetLayerLevelForView(int index)
+        {
+            if (_is3D)
+            {
+                int baseLevel = 0;
+
+                int layerLevels = _layers;
+
+                while (index >= layerLevels)
+                {
+                    index -= layerLevels;
+                    baseLevel++;
+                    layerLevels = Math.Max(layerLevels >> 1, 1);
+                }
+
+                return (index, baseLevel);
+            }
+            else
+            {
+                return (index / _levels, index % _levels);
+            }
+        }
+
+        /// <summary>
+        /// Find the byte offset of a given texture relative to the storage.
+        /// </summary>
+        /// <param name="texture">The texture to locate</param>
+        /// <returns>The offset of the texture in bytes</returns>
+        public int FindOffset(Texture texture)
+        {
+            return _allOffsets[GetOffsetIndex(texture.FirstLayer, texture.FirstLevel)];
+        }
+
+        /// <summary>
+        /// Find the offset index of a given layer and level.
+        /// </summary>
+        /// <param name="layer">The view layer</param>
+        /// <param name="level">The view level</param>
+        /// <returns>The offset index of the given layer and level</returns>
+        public int GetOffsetIndex(int layer, int level)
+        {
+            if (_is3D)
+            {
+                return layer + Get3DLevelRange(level).Index;
+            }
+            else
+            {
+                return level + layer * _levels;
+            }
+        }
+
+        /// <summary>
+        /// The action to perform when a memory tracking handle is flipped to dirty.
+        /// This notifies overlapping textures that the memory needs to be synchronized.
+        /// </summary>
+        /// <param name="groupHandle">The handle that a dirty flag was set on</param>
+        private void DirtyAction(TextureGroupHandle groupHandle)
+        {
+            // Notify all textures that belong to this handle.
+
+            Storage.SignalGroupDirty();
+
+            lock (groupHandle.Overlaps)
+            {
+                foreach (Texture overlap in groupHandle.Overlaps)
+                {
+                    overlap.SignalGroupDirty();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Generate a CpuRegionHandle for a given address and size range in CPU VA.
+        /// </summary>
+        /// <param name="address">The start address of the tracked region</param>
+        /// <param name="size">The size of the tracked region</param>
+        /// <returns>A CpuRegionHandle covering the given range</returns>
+        private CpuRegionHandle GenerateHandle(ulong address, ulong size)
+        {
+            return _context.PhysicalMemory.BeginTracking(address, size);
+        }
+
+        /// <summary>
+        /// Generate a TextureGroupHandle covering a specified range of views.
+        /// </summary>
+        /// <param name="viewStart">The start view of the handle</param>
+        /// <param name="views">The number of views to cover</param>
+        /// <returns>A TextureGroupHandle covering the given views</returns>
+        private TextureGroupHandle GenerateHandles(int viewStart, int views)
+        {
+            int offset = _allOffsets[viewStart];
+            int endOffset = (viewStart + views == _allOffsets.Length) ? (int)Storage.Size : _allOffsets[viewStart + views];
+            int size = endOffset - offset;
+
+            var result = new List<CpuRegionHandle>();
+
+            for (int i = 0; i < TextureRange.Count; i++)
+            {
+                MemoryRange item = TextureRange.GetSubRange(i);
+                int subRangeSize = (int)item.Size;
+
+                int sliceStart = Math.Clamp(offset, 0, subRangeSize);
+                int sliceEnd = Math.Clamp(endOffset, 0, subRangeSize);
+
+                if (sliceStart != sliceEnd)
+                {
+                    result.Add(GenerateHandle(item.Address + (ulong)sliceStart, (ulong)(sliceEnd - sliceStart)));
+                }
+
+                offset -= subRangeSize;
+                endOffset -= subRangeSize;
+
+                if (endOffset <= 0)
+                {
+                    break;
+                }
+            }
+
+            (int firstLayer, int firstLevel) = GetLayerLevelForView(viewStart);
+
+            if (_hasLayerViews && _hasMipViews)
+            {
+                size = _sliceSizes[firstLevel];
+            }
+
+            var groupHandle = new TextureGroupHandle(this, _allOffsets[viewStart], (ulong)size, _views, firstLayer, firstLevel, result.ToArray());
+
+            foreach (CpuRegionHandle handle in result)
+            {
+                handle.RegisterDirtyEvent(() => DirtyAction(groupHandle));
+            }
+
+            return groupHandle;
+        }
+
+        /// <summary>
+        /// Update the views in this texture group, rebuilding the memory tracking if required.
+        /// </summary>
+        /// <param name="views">The views list of the storage texture</param>
+        public void UpdateViews(List<Texture> views)
+        {
+            // This is saved to calculate overlapping views for each handle.
+            _views = views;
+
+            bool layerViews = _hasLayerViews;
+            bool mipViews = _hasMipViews;
+            bool regionsRebuilt = false;
+
+            if (!(layerViews && mipViews))
+            {
+                foreach (Texture view in views)
+                {
+                    if (view.Info.GetSlices() < _layers)
+                    {
+                        layerViews = true;
+                    }
+
+                    if (view.Info.Levels < _levels)
+                    {
+                        mipViews = true;
+                    }
+                }
+
+                (layerViews, mipViews) = PropagateGranularity(layerViews, mipViews);
+
+                if (layerViews != _hasLayerViews || mipViews != _hasMipViews)
+                {
+                    _hasLayerViews = layerViews;
+                    _hasMipViews = mipViews;
+
+                    RecalculateHandleRegions();
+                    regionsRebuilt = true;
+                }
+            }
+
+            if (!regionsRebuilt)
+            {
+                // Must update the overlapping views on all handles, but only if they were not just recreated.
+
+                foreach (TextureGroupHandle handle in _handles)
+                {
+                    handle.RecalculateOverlaps(this, views);
+                }
+            }
+
+            Storage.SignalGroupDirty();
+            foreach (Texture texture in views)
+            {
+                texture.SignalGroupDirty();
+            }
+        }
+
+        /// <summary>
+        /// Inherit handle state from an old set of handles, such as modified and dirty flags.
+        /// </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)
+        {
+            foreach (var group in handles)
+            {
+                foreach (var handle in group.Handles)
+                {
+                    bool dirty = false;
+
+                    foreach (var oldGroup in oldHandles)
+                    {
+                        if (group.OverlapsWith(oldGroup.Offset, oldGroup.Size))
+                        {
+                            foreach (var oldHandle in oldGroup.Handles)
+                            {
+                                if (handle.OverlapsWith(oldHandle.Address, oldHandle.Size))
+                                {
+                                    dirty |= oldHandle.Dirty;
+                                }
+                            }
+                            
+                            group.Inherit(oldGroup);
+                        }
+                    }
+
+                    if (dirty && !handle.Dirty)
+                    { 
+                        handle.Reprotect(true);
+                    }
+
+                    if (group.Modified)
+                    {
+                        handle.RegisterAction((address, size) => FlushAction(group, address, size));
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Inherit state from another texture group.
+        /// </summary>
+        /// <param name="other">The texture group to inherit from</param>
+        public void Inherit(TextureGroup other)
+        {
+            bool layerViews = _hasLayerViews || other._hasLayerViews;
+            bool mipViews = _hasMipViews || other._hasMipViews;
+
+            if (layerViews != _hasLayerViews || mipViews != _hasMipViews)
+            {
+                _hasLayerViews = layerViews;
+                _hasMipViews = mipViews;
+
+                RecalculateHandleRegions();
+            }
+
+            InheritHandles(other._handles, _handles);
+        }
+
+        /// <summary>
+        /// Replace the current handles with the new handles. It is assumed that the new handles start dirty.
+        /// The dirty flags from the previous handles will be kept.
+        /// </summary>
+        /// <param name="handles">The handles to replace the current handles with</param>
+        private void ReplaceHandles(TextureGroupHandle[] handles)
+        {
+            if (_handles != null)
+            {
+                // When replacing handles, they should start as non-dirty.
+
+                foreach (TextureGroupHandle groupHandle in handles)
+                {
+                    foreach (CpuRegionHandle handle in groupHandle.Handles)
+                    {
+                        handle.Reprotect();
+                    }
+                }
+
+                InheritHandles(_handles, handles);
+
+                foreach (var oldGroup in _handles)
+                {
+                    foreach (var oldHandle in oldGroup.Handles)
+                    {
+                        oldHandle.Dispose();
+                    }
+                }
+            }
+
+            _handles = handles;
+            _loadNeeded = new bool[_handles.Length];
+        }
+
+        /// <summary>
+        /// Recalculate handle regions for this texture group, and inherit existing state into the new handles.
+        /// </summary>
+        private void RecalculateHandleRegions()
+        {
+            TextureGroupHandle[] handles;
+
+            if (!(_hasMipViews || _hasLayerViews))
+            {
+                // Single dirty region.
+                var cpuRegionHandles = new CpuRegionHandle[TextureRange.Count];
+
+                for (int i = 0; i < TextureRange.Count; i++)
+                {
+                    var currentRange = TextureRange.GetSubRange(i);
+                    cpuRegionHandles[i] = GenerateHandle(currentRange.Address, currentRange.Size);
+                }
+
+                var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, cpuRegionHandles);
+
+                foreach (CpuRegionHandle handle in cpuRegionHandles)
+                {
+                    handle.RegisterDirtyEvent(() => DirtyAction(groupHandle));
+                }
+
+                handles = new TextureGroupHandle[] { groupHandle };
+            }
+            else
+            {
+                // Get views for the host texture.
+                // It's worth noting that either the texture has layer views or mip views when getting to this point, which simplifies the logic a little.
+                // Depending on if the texture is 3d, either the mip views imply that layer views are present (2d) or the other way around (3d).
+                // This is enforced by the way the texture matched as a view, so we don't need to check.
+
+                int layerHandles = _hasLayerViews ? _layers : 1;
+                int levelHandles = _hasMipViews ? _levels : 1;
+
+                int handleIndex = 0;
+
+                if (_is3D)
+                {
+                    var handlesList = new List<TextureGroupHandle>();
+
+                    for (int i = 0; i < levelHandles; i++)
+                    {
+                        for (int j = 0; j < layerHandles; j++)
+                        {
+                            (int viewStart, int views) = Get3DLevelRange(i);
+                            viewStart += j;
+                            views = _hasLayerViews ? 1 : views; // A layer view is also a mip view.
+
+                            handlesList.Add(GenerateHandles(viewStart, views));
+                        }
+
+                        layerHandles = Math.Max(1, layerHandles >> 1);
+                    }
+
+                    handles = handlesList.ToArray();
+                } 
+                else
+                {
+                    handles = new TextureGroupHandle[layerHandles * levelHandles];
+
+                    for (int i = 0; i < layerHandles; i++)
+                    {
+                        for (int j = 0; j < levelHandles; j++)
+                        {
+                            int viewStart = j + i * _levels;
+                            int views = _hasMipViews ? 1 : _levels; // A mip view is also a layer view.
+
+                            handles[handleIndex++] = GenerateHandles(viewStart, views);
+                        }
+                    }
+                }
+            }
+
+            ReplaceHandles(handles);
+        }
+
+        /// <summary>
+        /// Ensure that there is a handle for each potential texture view. Required for copy dependencies to work.
+        /// </summary>
+        private void EnsureFullSubdivision()
+        {
+            if (!(_hasLayerViews && _hasMipViews))
+            {
+                _hasLayerViews = true;
+                _hasMipViews = true;
+
+                RecalculateHandleRegions();
+            }
+        }
+
+        /// <summary>
+        /// Create a copy dependency between this texture group, and a texture at a given layer/level offset.
+        /// </summary>
+        /// <param name="other">The view compatible texture to create a dependency to</param>
+        /// <param name="firstLayer">The base layer of the given texture relative to the storage</param>
+        /// <param name="firstLevel">The base level of the given texture relative to the storage</param>
+        /// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param>
+        public void CreateCopyDependency(Texture other, int firstLayer, int firstLevel, bool copyTo)
+        {
+            TextureGroup otherGroup = other.Group;
+
+            EnsureFullSubdivision();
+            otherGroup.EnsureFullSubdivision();
+
+            // Get the location of each texture within its storage, so we can find the handles to apply the dependency to.
+            // This can consist of multiple disjoint regions, for example if this is a mip slice of an array texture.
+
+            var targetRange = new List<(int BaseHandle, int RegionCount)>();
+            var otherRange = new List<(int BaseHandle, int RegionCount)>();
+
+            EvaluateRelevantHandles(firstLayer, firstLevel, other.Info.GetSlices(), other.Info.Levels, (baseHandle, regionCount, split) => targetRange.Add((baseHandle, regionCount)));
+            otherGroup.EvaluateRelevantHandles(other, (baseHandle, regionCount, split) => otherRange.Add((baseHandle, regionCount)));
+
+            int targetIndex = 0;
+            int otherIndex = 0;
+            (int Handle, int RegionCount) targetRegion = (0, 0);
+            (int Handle, int RegionCount) otherRegion = (0, 0);
+
+            while (true)
+            {
+                if (targetRegion.RegionCount == 0)
+                {
+                    if (targetIndex >= targetRange.Count)
+                    {
+                        break;
+                    }
+
+                    targetRegion = targetRange[targetIndex++];
+                }
+
+                if (otherRegion.RegionCount == 0)
+                {
+                    if (otherIndex >= otherRange.Count)
+                    {
+                        break;
+                    }
+
+                    otherRegion = otherRange[otherIndex++];
+                }
+
+                TextureGroupHandle handle = _handles[targetRegion.Handle++];
+                TextureGroupHandle otherHandle = other.Group._handles[otherRegion.Handle++];
+
+                targetRegion.RegionCount--;
+                otherRegion.RegionCount--;
+
+                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(handle);
+                }
+                else
+                {
+                    handle.Copy(otherHandle);
+                }
+            }
+        }
+
+        /// <summary>
+        /// A flush has been requested on a tracked region. Find an appropriate view to flush.
+        /// </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)
+            {
+                foreach (Texture overlap in handle.Overlaps)
+                {
+                    overlap.ExternalFlush(address, size);
+                }
+            }
+
+            handle.Modified = false;
+        }
+
+        /// <summary>
+        /// Dispose this texture group, disposing all related memory tracking handles.
+        /// </summary>
+        public void Dispose()
+        {
+            foreach (TextureGroupHandle group in _handles)
+            {
+                group.Dispose();
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
new file mode 100644
index 000000000..27ee1e499
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
@@ -0,0 +1,327 @@
+using Ryujinx.Cpu.Tracking;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Graphics.Gpu.Image
+{
+    /// <summary>
+    /// A tracking handle for a texture group, which represents a range of views in a storage texture.
+    /// Retains a list of overlapping texture views, a modified flag, and tracking for each
+    /// CPU VA range that the views cover.
+    /// Also tracks copy dependencies for the handle - references to other handles that must be kept 
+    /// in sync with this one before use.
+    /// </summary>
+    class TextureGroupHandle : IDisposable
+    {
+        private TextureGroup _group;
+        private int _bindCount;
+        private int _firstLevel;
+        private int _firstLayer;
+
+        /// <summary>
+        /// The byte offset from the start of the storage of this handle.
+        /// </summary>
+        public int Offset { get; }
+
+        /// <summary>
+        /// The size in bytes covered by this handle.
+        /// </summary>
+        public int Size { get; }
+
+        /// <summary>
+        /// The textures which this handle overlaps with.
+        /// </summary>
+        public List<Texture> Overlaps { get; }
+
+        /// <summary>
+        /// The CPU memory tracking handles that cover this handle.
+        /// </summary>
+        public CpuRegionHandle[] Handles { get; }
+
+        /// <summary>
+        /// True if a texture overlapping this handle has been modified. Is set false when the flush action is called.
+        /// </summary>
+        public bool Modified { get; set; }
+
+        /// <summary>
+        /// Dependencies to handles from other texture groups.
+        /// </summary>
+        public List<TextureDependency> Dependencies { get; }
+
+        /// <summary>
+        /// A flag indicating that a copy is required from one of the dependencies.
+        /// </summary>
+        public bool NeedsCopy => DeferredCopy != null;
+
+        /// <summary>
+        /// A data copy that must be acknowledged the next time this handle is used.
+        /// </summary>
+        public TextureGroupHandle DeferredCopy { get; set; }
+
+        /// <summary>
+        /// Create a new texture group handle, representing a range of views in a storage texture.
+        /// </summary>
+        /// <param name="group">The TextureGroup that the handle belongs to</param>
+        /// <param name="offset">The byte offset from the start of the storage of the handle</param>
+        /// <param name="size">The size in bytes covered by the handle</param>
+        /// <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="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)
+        {
+            _group = group;
+            _firstLayer = firstLayer;
+            _firstLevel = firstLevel;
+
+            Offset = offset;
+            Size = (int)size;
+            Overlaps = new List<Texture>();
+            Dependencies = new List<TextureDependency>();
+
+            if (views != null)
+            {
+                RecalculateOverlaps(group, views);
+            }
+
+            Handles = handles;
+        }
+
+        /// <summary>
+        /// Calculate a list of which views overlap this handle.
+        /// </summary>
+        /// <param name="group">The parent texture group, used to find a view's base CPU VA offset</param>
+        /// <param name="views">The list of views to search for overlaps</param>
+        public void RecalculateOverlaps(TextureGroup group, List<Texture> views)
+        {
+            // Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
+            lock (Overlaps)
+            {
+                int endOffset = Offset + Size;
+
+                Overlaps.Clear();
+
+                foreach (Texture view in views)
+                {
+                    int viewOffset = group.FindOffset(view);
+                    if (viewOffset < endOffset && Offset < viewOffset + (int)view.Size)
+                    {
+                        Overlaps.Add(view);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Signal that this handle has been modified to any existing dependencies, and set the modified flag.
+        /// </summary>
+        public void SignalModified()
+        {
+            Modified = true;
+
+            // If this handle has any copy dependencies, notify the other handle that a copy needs to be performed.
+
+            foreach (TextureDependency dependency in Dependencies)
+            {
+                dependency.SignalModified();
+            }
+        }
+
+        /// <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)
+        {
+            SignalModified();
+
+            // 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));
+        }
+
+        /// <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)
+        {
+            DeferredCopy = copyFrom;
+
+            _group.Storage.SignalGroupDirty();
+
+            foreach (Texture overlap in Overlaps)
+            {
+                overlap.SignalGroupDirty();
+            }
+        }
+
+        /// <summary>
+        /// Create a copy dependency between this handle, and another.
+        /// </summary>
+        /// <param name="other">The handle to create a copy dependency to</param>
+        /// <param name="copyToOther">True if a copy should be deferred to all of the other handle's dependencies</param>
+        public void CreateCopyDependency(TextureGroupHandle other, bool copyToOther = false)
+        {
+            // Does this dependency already exist?
+            foreach (TextureDependency existing in Dependencies)
+            {
+                if (existing.Other.Handle == other)
+                {
+                    // Do not need to create it again. May need to set the dirty flag.
+                    return;
+                }
+            }
+
+            _group.HasCopyDependencies = true;
+            other._group.HasCopyDependencies = true;
+
+            TextureDependency dependency = new TextureDependency(this);
+            TextureDependency otherDependency = new TextureDependency(other);
+
+            dependency.Other = otherDependency;
+            otherDependency.Other = dependency;
+
+            Dependencies.Add(dependency);
+            other.Dependencies.Add(otherDependency);
+
+            // Recursively create dependency:
+            // All of this handle's dependencies must depend on the other.
+            foreach (TextureDependency existing in Dependencies.ToArray())
+            {
+                if (existing != dependency && existing.Other.Handle != other)
+                {
+                    existing.Other.Handle.CreateCopyDependency(other);
+                }
+            }
+
+            // All of the other handle's dependencies must depend on this.
+            foreach (TextureDependency existing in other.Dependencies.ToArray())
+            {
+                if (existing != otherDependency && existing.Other.Handle != this)
+                {
+                    existing.Other.Handle.CreateCopyDependency(this);
+
+                    if (copyToOther)
+                    {
+                        existing.Other.Handle.DeferCopy(this);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Remove a dependency from this handle's dependency list.
+        /// </summary>
+        /// <param name="dependency">The dependency to remove</param>
+        public void RemoveDependency(TextureDependency dependency)
+        {
+            Dependencies.Remove(dependency);
+        }
+
+        /// <summary>
+        /// Check if any of this handle's memory tracking handles are dirty.
+        /// </summary>
+        /// <returns>True if at least one of the handles is dirty</returns>
+        private bool CheckDirty()
+        {
+            return Handles.Any(handle => handle.Dirty);
+        }
+
+        /// <summary>
+        /// Perform a copy from the provided handle to this one, or perform a deferred copy if none is provided.
+        /// </summary>
+        /// <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)
+        {
+            bool result = false;
+
+            if (fromHandle == null)
+            {
+                fromHandle = DeferredCopy;
+
+                if (fromHandle != null && fromHandle._bindCount == 0)
+                {
+                    // Repeat the copy in future if the bind count is greater than 0.
+                    DeferredCopy = null;
+                }
+            }
+
+            if (fromHandle != null)
+            {
+                // If the copy texture is dirty, do not copy. Its data no longer matters, and this handle should also be dirty.
+                if (!fromHandle.CheckDirty())
+                {
+                    Texture from = fromHandle._group.Storage;
+                    Texture to = _group.Storage;
+
+                    if (from.ScaleFactor != to.ScaleFactor)
+                    {
+                        to.PropagateScale(from);
+                    }
+
+                    from.HostTexture.CopyTo(
+                        to.HostTexture,
+                        fromHandle._firstLayer,
+                        _firstLayer,
+                        fromHandle._firstLevel,
+                        _firstLevel);
+
+                    Modified = true;
+
+                    _group.RegisterAction(this);
+
+                    result = true;
+                }
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Inherit modified flags and dependencies from another texture handle.
+        /// </summary>
+        /// <param name="old">The texture handle to inherit from</param>
+        public void Inherit(TextureGroupHandle old)
+        {
+            Modified |= old.Modified;
+
+            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>
+        /// Check if this region overlaps with another.
+        /// </summary>
+        /// <param name="address">Base address</param>
+        /// <param name="size">Size of the region</param>
+        /// <returns>True if overlapping, false otherwise</returns>
+        public bool OverlapsWith(int offset, int size)
+        {
+            return Offset < offset + size && offset < Offset + Size;
+        }
+
+        public void Dispose()
+        {
+            foreach (CpuRegionHandle handle in Handles)
+            {
+                handle.Dispose();
+            }
+
+            foreach (TextureDependency dependency in Dependencies.ToArray())
+            {
+                dependency.Other.Handle.RemoveDependency(dependency.Other);
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs b/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs
index 3137f8b8c..571f440e5 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs
@@ -232,6 +232,31 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Gets the number of 2D slices of the texture.
+        /// Returns 6 for cubemap textures, layer faces for cubemap array textures, and DepthOrLayers for everything else.
+        /// </summary>
+        /// <returns>The number of texture slices</returns>
+        public int GetSlices()
+        {
+            if (Target == Target.Texture3D || Target == Target.Texture2DArray || Target == Target.Texture2DMultisampleArray)
+            {
+                return DepthOrLayers;
+            }
+            else if (Target == Target.CubemapArray)
+            {
+                return DepthOrLayers * 6;
+            }
+            else if (Target == Target.Cubemap)
+            {
+                return 6;
+            }
+            else
+            {
+                return 1;
+            }
+        }
+
         /// <summary>
         /// Calculates the size information from the texture information.
         /// </summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
index 2646a75b7..f13c3443d 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
@@ -185,7 +185,14 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             bool hasValue = color != null;
             bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && RenderTargetScale != color.ScaleFactor);
-            _rtColors[index] = color;
+
+            if (_rtColors[index] != color)
+            {
+                _rtColors[index]?.SignalModifying(false);
+                color?.SignalModifying(true);
+
+                _rtColors[index] = color;
+            }
 
             return changesScale || (hasValue && color.ScaleMode != TextureScaleMode.Blacklisted && color.ScaleFactor != GraphicsConfig.ResScale);
         }
@@ -292,7 +299,14 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             bool hasValue = depthStencil != null;
             bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && RenderTargetScale != depthStencil.ScaleFactor);
-            _rtDepthStencil = depthStencil;
+
+            if (_rtDepthStencil != depthStencil)
+            {
+                _rtDepthStencil?.SignalModifying(false);
+                depthStencil?.SignalModifying(true);
+
+                _rtDepthStencil = depthStencil;
+            }
 
             return changesScale || (hasValue && depthStencil.ScaleMode != TextureScaleMode.Blacklisted && depthStencil.ScaleFactor != GraphicsConfig.ResScale);
         }
@@ -754,38 +768,97 @@ namespace Ryujinx.Graphics.Gpu.Image
                 overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
             }
 
+            if (_overlapInfo.Length != _textureOverlaps.Length)
+            {
+                Array.Resize(ref _overlapInfo, _textureOverlaps.Length);
+            }
+
+            // =============== Find Texture View of Existing Texture =============== 
+
+            int fullyCompatible = 0;
+
+            // Evaluate compatibility of overlaps
+
             for (int index = 0; index < overlapsCount; index++)
             {
                 Texture overlap = _textureOverlaps[index];
-                TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, range.Value, out int firstLayer, out int firstLevel);
+                TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, range.Value, sizeInfo.LayerSize, out int firstLayer, out int firstLevel);
 
                 if (overlapCompatibility == TextureViewCompatibility.Full)
                 {
-                    TextureInfo oInfo = AdjustSizes(overlap, info, firstLevel);
+                    if (overlap.IsView)
+                    {
+                        overlapCompatibility = TextureViewCompatibility.CopyOnly;
+                    }
+                    else
+                    {
+                        fullyCompatible++;
+                    }
+                }
+
+                _overlapInfo[index] = new OverlapInfo(overlapCompatibility, firstLayer, firstLevel);
+            }
+
+            // Search through the overlaps to find a compatible view and establish any copy dependencies.
+
+            for (int index = 0; index < overlapsCount; index++)
+            {
+                Texture overlap = _textureOverlaps[index];
+                OverlapInfo oInfo = _overlapInfo[index];
+
+                if (oInfo.Compatibility == TextureViewCompatibility.Full)
+                {
+                    TextureInfo adjInfo = AdjustSizes(overlap, info, oInfo.FirstLevel);
 
                     if (!isSamplerTexture)
                     {
-                        info = oInfo;
+                        info = adjInfo;
                     }
 
-                    texture = overlap.CreateView(oInfo, sizeInfo, range.Value, firstLayer, firstLevel);
-
-                    if (overlap.IsModified)
-                    {
-                        texture.SignalModified();
-                    }
+                    texture = overlap.CreateView(adjInfo, sizeInfo, range.Value, oInfo.FirstLayer, oInfo.FirstLevel);
 
                     ChangeSizeIfNeeded(info, texture, isSamplerTexture, sizeHint);
 
+                    texture.SynchronizeMemory();
                     break;
                 }
-                else if (overlapCompatibility == TextureViewCompatibility.CopyOnly)
+                else if (oInfo.Compatibility == TextureViewCompatibility.CopyOnly && fullyCompatible == 0)
                 {
-                    // TODO: Copy rules for targets created after the container texture. See below.
-                    overlap.DisableMemoryTracking();
+                    // Only copy compatible. If there's another choice for a FULLY compatible texture, choose that instead.
+
+                    texture = new Texture(_context, info, sizeInfo, range.Value, scaleMode);
+                    texture.InitializeGroup(true, true);
+                    texture.InitializeData(false, false);
+
+                    overlap.SynchronizeMemory();
+                    overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true);
+                    break;
                 }
             }
 
+            if (texture != null)
+            {
+                // This texture could be a view of multiple parent textures with different storages, even if it is a view.
+                // When a texture is created, make sure all possible dependencies to other textures are created as copies. 
+                // (even if it could be fulfilled without a copy)
+
+                for (int index = 0; index < overlapsCount; index++)
+                {
+                    Texture overlap = _textureOverlaps[index];
+                    OverlapInfo oInfo = _overlapInfo[index];
+
+                    if (oInfo.Compatibility != TextureViewCompatibility.Incompatible && overlap.Group != texture.Group)
+                    {
+                        overlap.SynchronizeMemory();
+                        overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true);
+                    }
+                }
+
+                texture.SynchronizeMemory();
+            }
+
+            // =============== Create a New Texture =============== 
+
             // No match, create a new texture.
             if (texture == null)
             {
@@ -795,24 +868,53 @@ namespace Ryujinx.Graphics.Gpu.Image
                 // Any textures that are incompatible will contain garbage data, so they should be removed where possible.
 
                 int viewCompatible = 0;
+                fullyCompatible = 0;
                 bool setData = isSamplerTexture || overlapsCount == 0 || flags.HasFlag(TextureSearchFlags.ForCopy);
 
+                bool hasLayerViews = false;
+                bool hasMipViews = false;
+
                 for (int index = 0; index < overlapsCount; index++)
                 {
                     Texture overlap = _textureOverlaps[index];
                     bool overlapInCache = overlap.CacheNode != null;
 
-                    TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Range, out int firstLayer, out int firstLevel);
+                    TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Range, overlap.LayerSize, out int firstLayer, out int firstLevel);
+
+                    if (overlap.IsView && compatibility == TextureViewCompatibility.Full)
+                    {
+                        compatibility = TextureViewCompatibility.CopyOnly;
+                    }
 
                     if (compatibility != TextureViewCompatibility.Incompatible)
                     {
-                        if (_overlapInfo.Length != _textureOverlaps.Length)
+                        if (compatibility == TextureViewCompatibility.Full)
                         {
-                            Array.Resize(ref _overlapInfo, _textureOverlaps.Length);
+                            if (viewCompatible == fullyCompatible)
+                            {
+                                _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
+                                _textureOverlaps[viewCompatible++] = overlap;
+                            }
+                            else
+                            {
+                                // Swap overlaps so that the fully compatible views have priority.
+
+                                _overlapInfo[viewCompatible] = _overlapInfo[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;
                         }
 
-                        _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
-                        _textureOverlaps[viewCompatible++] = overlap;
+                        hasLayerViews |= overlap.Info.GetSlices() < texture.Info.GetSlices();
+                        hasMipViews |= overlap.Info.Levels < texture.Info.Levels;
                     }
                     else if (overlapInCache || !setData)
                     {
@@ -841,6 +943,8 @@ namespace Ryujinx.Graphics.Gpu.Image
                     }
                 }
 
+                texture.InitializeGroup(hasLayerViews, hasMipViews);
+
                 // 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);
@@ -848,17 +952,17 @@ namespace Ryujinx.Graphics.Gpu.Image
                 for (int index = 0; index < viewCompatible; index++)
                 {
                     Texture overlap = _textureOverlaps[index];
+
                     OverlapInfo oInfo = _overlapInfo[index];
 
-                    if (oInfo.Compatibility != TextureViewCompatibility.Full)
+                    if (overlap.Group == texture.Group)
                     {
-                        continue; // Copy only compatibilty.
+                        // If the texture group is equal, then this texture (or its parent) is already a view.
+                        continue;
                     }
 
                     TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, oInfo.FirstLevel);
 
-                    TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities, overlap.ScaleFactor);
-
                     if (texture.ScaleFactor != overlap.ScaleFactor)
                     {
                         // A bit tricky, our new texture may need to contain an existing texture that is upscaled, but isn't itself.
@@ -867,47 +971,30 @@ namespace Ryujinx.Graphics.Gpu.Image
                         texture.PropagateScale(overlap);
                     }
 
-                    ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel);
-
-                    overlap.HostTexture.CopyTo(newView, 0, 0);
-
-                    // Inherit modification from overlapping texture, do that before replacing
-                    // the view since the replacement operation removes it from the list.
-                    if (overlap.IsModified)
+                    if (oInfo.Compatibility != TextureViewCompatibility.Full)
                     {
-                        texture.SignalModified();
+                        // Copy only compatibility, or target texture is already a view.
+
+                        ChangeSizeIfNeeded(overlapInfo, overlap, false, sizeHint); // Force a size match for copy
+
+                        overlap.SynchronizeMemory();
+                        texture.CreateCopyDependency(overlap, oInfo.FirstLayer, oInfo.FirstLevel, false);
                     }
-
-                    overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel);
-                }
-
-                // If the texture is a 3D texture, we need to additionally copy any slice
-                // of the 3D texture to the newly created 3D texture.
-                if (info.Target == Target.Texture3D && viewCompatible > 0)
-                {
-                    // TODO: This copy can currently only happen when the 3D texture is created.
-                    // If a game clears and redraws the slices, we won't be able to copy the new data to the 3D texture.
-                    // Disable tracking to try keep at least the original data in there for as long as possible.
-                    texture.DisableMemoryTracking();
-
-                    for (int index = 0; index < viewCompatible; index++)
+                    else
                     {
-                        Texture overlap = _textureOverlaps[index];
-                        OverlapInfo oInfo = _overlapInfo[index];
+                        TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities, overlap.ScaleFactor);
 
-                        if (oInfo.Compatibility != TextureViewCompatibility.Incompatible)
-                        {
-                            overlap.BlacklistScale();
+                        ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel);
 
-                            overlap.HostTexture.CopyTo(texture.HostTexture, oInfo.FirstLayer, oInfo.FirstLevel);
+                        overlap.SynchronizeMemory();
 
-                            if (overlap.IsModified)
-                            {
-                                texture.SignalModified();
-                            }
-                        }
+                        overlap.HostTexture.CopyTo(newView, 0, 0);
+
+                        overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel);
                     }
                 }
+                
+                texture.SynchronizeMemory();
             }
 
             // Sampler textures are managed by the texture pool, all other textures
diff --git a/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs b/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs
index d2a054953..92099b6a5 100644
--- a/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs
@@ -49,11 +49,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
             }
         }
 
-        public void Reprotect()
+        public void Reprotect(bool asDirty = false)
         {
             foreach (var regionHandle in _cpuRegionHandles)
             {
-                regionHandle.Reprotect();
+                regionHandle.Reprotect(asDirty);
             }
         }
     }
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
index 6df2b630c..5607fb401 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
@@ -18,6 +18,11 @@ namespace Ryujinx.Graphics.OpenGL.Image
             throw new NotSupportedException();
         }
 
+        public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel)
+        {
+            throw new NotSupportedException();
+        }
+
         public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter)
         {
             throw new NotSupportedException();
@@ -38,6 +43,11 @@ namespace Ryujinx.Graphics.OpenGL.Image
             Buffer.SetData(_buffer, _bufferOffset, data.Slice(0, Math.Min(data.Length, _bufferSize)));
         }
 
+        public void SetData(ReadOnlySpan<byte> data, int layer, int level)
+        {
+            throw new NotSupportedException();
+        }
+
         public void SetStorage(BufferRange buffer)
         {
             if (buffer.Handle == _buffer &&
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs b/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
index 20a3b9149..b27403b22 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
@@ -115,18 +115,44 @@ namespace Ryujinx.Graphics.OpenGL.Image
             TextureCreateInfo srcInfo = src.Info;
             TextureCreateInfo dstInfo = dst.Info;
 
+            int srcDepth = srcInfo.GetDepthOrLayers();
+            int srcLevels = srcInfo.Levels;
+
+            int dstDepth = dstInfo.GetDepthOrLayers();
+            int dstLevels = dstInfo.Levels;
+
+            if (dstInfo.Target == Target.Texture3D)
+            {
+                dstDepth = Math.Max(1, dstDepth >> dstLevel);
+            }
+
+            int depth = Math.Min(srcDepth, dstDepth);
+            int levels = Math.Min(srcLevels, dstLevels);
+
+            CopyUnscaled(src, dst, srcLayer, dstLayer, srcLevel, dstLevel, depth, levels);
+        }
+
+        public void CopyUnscaled(
+            ITextureInfo src,
+            ITextureInfo dst,
+            int srcLayer,
+            int dstLayer,
+            int srcLevel,
+            int dstLevel,
+            int depth,
+            int levels)
+        {
+            TextureCreateInfo srcInfo = src.Info;
+            TextureCreateInfo dstInfo = dst.Info;
+
             int srcHandle = src.Handle;
             int dstHandle = dst.Handle;
 
             int srcWidth = srcInfo.Width;
             int srcHeight = srcInfo.Height;
-            int srcDepth = srcInfo.GetDepthOrLayers();
-            int srcLevels = srcInfo.Levels;
 
             int dstWidth = dstInfo.Width;
             int dstHeight = dstInfo.Height;
-            int dstDepth = dstInfo.GetDepthOrLayers();
-            int dstLevels = dstInfo.Levels;
 
             srcWidth = Math.Max(1, srcWidth >> srcLevel);
             srcHeight = Math.Max(1, srcHeight >> srcLevel);
@@ -134,11 +160,6 @@ namespace Ryujinx.Graphics.OpenGL.Image
             dstWidth = Math.Max(1, dstWidth >> dstLevel);
             dstHeight = Math.Max(1, dstHeight >> dstLevel);
 
-            if (dstInfo.Target == Target.Texture3D)
-            {
-                dstDepth = Math.Max(1, dstDepth >> dstLevel);
-            }
-
             int blockWidth = 1;
             int blockHeight = 1;
             bool sizeInBlocks = false;
@@ -166,8 +187,6 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
             int width = Math.Min(srcWidth, dstWidth);
             int height = Math.Min(srcHeight, dstHeight);
-            int depth = Math.Min(srcDepth, dstDepth);
-            int levels = Math.Min(srcLevels, dstLevels);
 
             for (int level = 0; level < levels; level++)
             {
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
index 89f74ceec..053fb3c24 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
@@ -10,8 +10,6 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
         private readonly TextureStorage _parent;
 
-        private TextureView _emulatedViewParent;
-
         private TextureView _incompatibleFormatView;
 
         public int FirstLayer { get; private set; }
@@ -96,37 +94,10 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
         public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel)
         {
-            if (Info.IsCompressed == info.IsCompressed)
-            {
-                firstLayer += FirstLayer;
-                firstLevel += FirstLevel;
+            firstLayer += FirstLayer;
+            firstLevel += FirstLevel;
 
-                return _parent.CreateView(info, firstLayer, firstLevel);
-            }
-            else
-            {
-                // TODO: Most graphics APIs doesn't support creating a texture view from a compressed format
-                // with a non-compressed format (or vice-versa), however NVN seems to support it.
-                // So we emulate that here with a texture copy (see the first CopyTo overload).
-                // However right now it only does a single copy right after the view is created,
-                // so it doesn't work for all cases.
-                TextureView emulatedView = (TextureView)_renderer.CreateTexture(info, ScaleFactor);
-
-                _renderer.TextureCopy.CopyUnscaled(
-                    this,
-                    emulatedView,
-                    0,
-                    firstLayer,
-                    0,
-                    firstLevel);
-
-                emulatedView._emulatedViewParent = this;
-
-                emulatedView.FirstLayer = firstLayer;
-                emulatedView.FirstLevel = firstLevel;
-
-                return emulatedView;
-            }
+            return _parent.CreateView(info, firstLayer, firstLevel);
         }
 
         public int GetIncompatibleFormatViewHandle()
@@ -163,17 +134,13 @@ namespace Ryujinx.Graphics.OpenGL.Image
             TextureView destinationView = (TextureView)destination;
 
             _renderer.TextureCopy.CopyUnscaled(this, destinationView, 0, firstLayer, 0, firstLevel);
+        }
 
-            if (destinationView._emulatedViewParent != null)
-            {
-                _renderer.TextureCopy.CopyUnscaled(
-                    this,
-                    destinationView._emulatedViewParent,
-                    0,
-                    destinationView.FirstLayer,
-                    0,
-                    destinationView.FirstLevel);
-            }
+        public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel)
+        {
+             TextureView destinationView = (TextureView)destination;
+
+            _renderer.TextureCopy.CopyUnscaled(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
         }
 
         public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter)
@@ -308,6 +275,20 @@ namespace Ryujinx.Graphics.OpenGL.Image
             }
         }
 
+        public void SetData(ReadOnlySpan<byte> data, int layer, int level)
+        {
+            unsafe
+            {
+                fixed (byte* ptr = data)
+                {
+                    int width = Math.Max(Info.Width >> level, 1);
+                    int height = Math.Max(Info.Height >> level, 1);
+
+                    ReadFrom2D((IntPtr)ptr, layer, level, width, height);
+                }
+            }
+        }
+
         public void ReadFromPbo(int offset, int size)
         {
             ReadFrom(IntPtr.Zero + offset, size);
diff --git a/Ryujinx.Graphics.Texture/SizeCalculator.cs b/Ryujinx.Graphics.Texture/SizeCalculator.cs
index 2dc608697..7eee13c0c 100644
--- a/Ryujinx.Graphics.Texture/SizeCalculator.cs
+++ b/Ryujinx.Graphics.Texture/SizeCalculator.cs
@@ -9,6 +9,19 @@ namespace Ryujinx.Graphics.Texture
     {
         private const int StrideAlignment = 32;
 
+        private static int Calculate3DOffsetCount(int levels, int depth)
+        {
+            int offsetCount = depth;
+
+            while (--levels > 0)
+            {
+                depth = Math.Max(1, depth >> 1);
+                offsetCount += depth;
+            }
+
+            return offsetCount;
+        }
+
         public static SizeInfo GetBlockLinearTextureSize(
             int width,
             int height,
@@ -27,8 +40,9 @@ namespace Ryujinx.Graphics.Texture
 
             int layerSize = 0;
 
-            int[] allOffsets = new int[levels * layers * depth];
+            int[] allOffsets = new int[is3D ? Calculate3DOffsetCount(levels, depth) : levels * layers * depth];
             int[] mipOffsets = new int[levels];
+            int[] sliceSizes = new int[levels];
 
             int mipGobBlocksInY = gobBlocksInY;
             int mipGobBlocksInZ = gobBlocksInZ;
@@ -36,6 +50,8 @@ namespace Ryujinx.Graphics.Texture
             int gobWidth  = (GobStride / bytesPerPixel) * gobBlocksInTileX;
             int gobHeight = gobBlocksInY * GobHeight;
 
+            int depthLevelOffset = 0;
+
             for (int level = 0; level < levels; level++)
             {
                 int w = Math.Max(1, width  >> level);
@@ -86,13 +102,16 @@ namespace Ryujinx.Graphics.Texture
                         int zLow  = z &  mask;
                         int zHigh = z & ~mask;
 
-                        allOffsets[z * levels + level] = baseOffset + zLow * gobSize + zHigh * sliceSize;
+                        allOffsets[z + depthLevelOffset] = baseOffset + zLow * gobSize + zHigh * sliceSize;
                     }
                 }
 
                 mipOffsets[level] = layerSize;
+                sliceSizes[level] = totalBlocksOfGobsInY * robSize;
 
-                layerSize += totalBlocksOfGobsInZ * totalBlocksOfGobsInY * robSize;
+                layerSize += totalBlocksOfGobsInZ * sliceSizes[level];
+
+                depthLevelOffset += d;
             }
 
             if (layers > 1)
@@ -133,7 +152,7 @@ namespace Ryujinx.Graphics.Texture
                 }
             }
 
-            return new SizeInfo(mipOffsets, allOffsets, levels, layerSize, totalSize);
+            return new SizeInfo(mipOffsets, allOffsets, sliceSizes, depth, levels, layerSize, totalSize, is3D);
         }
 
         public static SizeInfo GetLinearTextureSize(int stride, int height, int blockHeight)
@@ -142,7 +161,7 @@ namespace Ryujinx.Graphics.Texture
             // so we only need to handle a single case (2D textures without mipmaps).
             int totalSize = stride * BitUtils.DivRoundUp(height, blockHeight);
 
-            return new SizeInfo(new int[] { 0 }, new int[] { 0 }, 1, totalSize, totalSize);
+            return new SizeInfo(totalSize);
         }
 
         private static int AlignLayerSize(
diff --git a/Ryujinx.Graphics.Texture/SizeInfo.cs b/Ryujinx.Graphics.Texture/SizeInfo.cs
index 55b22e3af..f518ee4b1 100644
--- a/Ryujinx.Graphics.Texture/SizeInfo.cs
+++ b/Ryujinx.Graphics.Texture/SizeInfo.cs
@@ -5,34 +5,46 @@ namespace Ryujinx.Graphics.Texture
     public struct SizeInfo
     {
         private readonly int[] _mipOffsets;
-        private readonly int[] _allOffsets;
 
         private readonly int _levels;
+        private readonly int _depth;
+        private readonly bool _is3D;
 
+        public readonly int[] AllOffsets;
+        public readonly int[] SliceSizes;
         public int LayerSize { get; }
         public int TotalSize { get; }
 
         public SizeInfo(int size)
         {
             _mipOffsets = new int[] { 0 };
-            _allOffsets = new int[] { 0 };
+            AllOffsets  = new int[] { 0 };
+            SliceSizes  = new int[] { size };
+            _depth      = 1;
             _levels     = 1;
             LayerSize   = size;
             TotalSize   = size;
+            _is3D       = false;
         }
 
         internal SizeInfo(
             int[] mipOffsets,
             int[] allOffsets,
+            int[] sliceSizes,
+            int   depth,
             int   levels,
             int   layerSize,
-            int   totalSize)
+            int   totalSize,
+            bool  is3D)
         {
             _mipOffsets = mipOffsets;
-            _allOffsets = allOffsets;
+            AllOffsets  = allOffsets;
+            SliceSizes  = sliceSizes;
+            _depth      = depth;
             _levels     = levels;
             LayerSize   = layerSize;
             TotalSize   = totalSize;
+            _is3D       = is3D;
         }
 
         public int GetMipOffset(int level)
@@ -47,7 +59,7 @@ namespace Ryujinx.Graphics.Texture
 
         public bool FindView(int offset, out int firstLayer, out int firstLevel)
         {
-            int index = Array.BinarySearch(_allOffsets, offset);
+            int index = Array.BinarySearch(AllOffsets, offset);
 
             if (index < 0)
             {
@@ -57,8 +69,25 @@ namespace Ryujinx.Graphics.Texture
                 return false;
             }
 
-            firstLayer = index / _levels;
-            firstLevel = index - (firstLayer * _levels);
+            if (_is3D)
+            {
+                firstLayer = index;
+                firstLevel = 0;
+
+                int levelDepth = _depth;
+
+                while (firstLayer >= levelDepth)
+                {
+                    firstLayer -= levelDepth;
+                    firstLevel++;
+                    levelDepth = Math.Max(levelDepth >> 1, 1);
+                }
+            }
+            else
+            {
+                firstLayer = index / _levels;
+                firstLevel = index - (firstLayer * _levels);
+            }
 
             return true;
         }
diff --git a/Ryujinx.Memory/Tracking/IRegionHandle.cs b/Ryujinx.Memory/Tracking/IRegionHandle.cs
index 33628da64..cd33e5c8f 100644
--- a/Ryujinx.Memory/Tracking/IRegionHandle.cs
+++ b/Ryujinx.Memory/Tracking/IRegionHandle.cs
@@ -10,7 +10,7 @@ namespace Ryujinx.Memory.Tracking
         ulong Size { get; }
         ulong EndAddress { get; }
 
-        void Reprotect();
+        void Reprotect(bool asDirty = false);
         void RegisterAction(RegionSignal action);
     }
 }
diff --git a/Ryujinx.Memory/Tracking/RegionHandle.cs b/Ryujinx.Memory/Tracking/RegionHandle.cs
index 3ddcb6db4..4da184dd7 100644
--- a/Ryujinx.Memory/Tracking/RegionHandle.cs
+++ b/Ryujinx.Memory/Tracking/RegionHandle.cs
@@ -1,4 +1,5 @@
 using Ryujinx.Memory.Range;
+using System;
 using System.Collections.Generic;
 using System.Threading;
 
@@ -19,9 +20,12 @@ namespace Ryujinx.Memory.Tracking
         internal IMultiRegionHandle Parent { get; set; }
         internal int SequenceNumber { get; set; }
 
+        private event Action _onDirty;
+
         private RegionSignal _preAction; // Action to perform before a read or write. This will block the memory access.
         private readonly List<VirtualRegion> _regions;
         private readonly MemoryTracking _tracking;
+        private bool _disposed;
 
         internal MemoryPermission RequiredPermission => _preAction != null ? MemoryPermission.None : (Dirty ? MemoryPermission.ReadAndWrite : MemoryPermission.Read);
         internal RegionSignal PreAction => _preAction;
@@ -60,7 +64,12 @@ namespace Ryujinx.Memory.Tracking
 
             if (write)
             {
+                bool oldDirty = Dirty;
                 Dirty = true;
+                if (!oldDirty)
+                {
+                    _onDirty?.Invoke();
+                }
                 Parent?.SignalWrite();
             }
         }
@@ -68,9 +77,9 @@ namespace Ryujinx.Memory.Tracking
         /// <summary>
         /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write.
         /// </summary>
-        public void Reprotect()
+        public void Reprotect(bool asDirty = false)
         {
-            Dirty = false;
+            Dirty = asDirty;
             lock (_tracking.TrackingLock)
             {
                 foreach (VirtualRegion region in _regions)
@@ -100,6 +109,16 @@ namespace Ryujinx.Memory.Tracking
             }
         }
 
+        /// <summary>
+        /// Register an action to perform when the region is written to.
+        /// This action will not be removed when it is called - it is called each time the dirty flag is set.
+        /// </summary>
+        /// <param name="action">Action to call on dirty</param>
+        public void RegisterDirtyEvent(Action action)
+        {
+            _onDirty += action;
+        }
+
         /// <summary>
         /// Add a child virtual region to this handle.
         /// </summary>
@@ -125,6 +144,13 @@ namespace Ryujinx.Memory.Tracking
         /// </summary>
         public void Dispose()
         {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().FullName);
+            }
+
+            _disposed = true;
+
             lock (_tracking.TrackingLock)
             {
                 foreach (VirtualRegion region in _regions)