Explorar o código

Support for resources on non-contiguous GPU memory regions (#1905)

* Support for resources on non-contiguous GPU memory regions

* Implement MultiRange physical addresses, only used with a single range for now

* Actually use non-contiguous ranges

* GetPhysicalRegions fixes

* Documentation and remove Address property from TextureInfo

* Finish implementing GetWritableRegion

* Fix typo
gdkchan %!s(int64=5) %!d(string=hai) anos
pai
achega
c4f56c5704

+ 2 - 2
Ryujinx.Cpu/MemoryManager.cs

@@ -13,7 +13,7 @@ namespace Ryujinx.Cpu
     /// <summary>
     /// Represents a CPU memory manager.
     /// </summary>
-    public sealed class MemoryManager : IMemoryManager, IVirtualMemoryManager, IDisposable
+    public sealed class MemoryManager : IMemoryManager, IVirtualMemoryManager, IWritableBlock, IDisposable
     {
         public const int PageBits = 12;
         public const int PageSize = 1 << PageBits;
@@ -202,12 +202,12 @@ namespace Ryujinx.Cpu
             WriteImpl(va, data);
         }
 
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         /// <summary>
         /// Writes data to CPU mapped memory.
         /// </summary>
         /// <param name="va">Virtual address to write the data into</param>
         /// <param name="data">Data to be written</param>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private void WriteImpl(ulong va, ReadOnlySpan<byte> data)
         {
             try

+ 37 - 47
Ryujinx.Graphics.Gpu/Image/Texture.cs

@@ -1,7 +1,7 @@
 using Ryujinx.Common;
 using Ryujinx.Common.Logging;
-using Ryujinx.Cpu.Tracking;
 using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Memory;
 using Ryujinx.Graphics.Texture;
 using Ryujinx.Graphics.Texture.Astc;
 using Ryujinx.Memory.Range;
@@ -14,7 +14,7 @@ namespace Ryujinx.Graphics.Gpu.Image
     /// <summary>
     /// Represents a cached GPU texture.
     /// </summary>
-    class Texture : IRange, IDisposable
+    class Texture : IMultiRangeItem, IDisposable
     {
         // How many updates we need before switching to the byte-by-byte comparison
         // modification check method.
@@ -95,21 +95,16 @@ namespace Ryujinx.Graphics.Gpu.Image
         public event Action<Texture> Disposed;
 
         /// <summary>
-        /// Start address of the texture in guest memory.
+        /// Physical memory ranges where the texture data is located.
         /// </summary>
-        public ulong Address => Info.Address;
-
-        /// <summary>
-        /// End address of the texture in guest memory.
-        /// </summary>
-        public ulong EndAddress => Info.Address + Size;
+        public MultiRange Range { get; private set; }
 
         /// <summary>
         /// Texture size in bytes.
         /// </summary>
         public ulong Size => (ulong)_sizeInfo.TotalSize;
 
-        private CpuRegionHandle _memoryTracking;
+        private GpuRegionHandle _memoryTracking;
 
         private int _referenceCount;
 
@@ -119,6 +114,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="context">GPU context that the texture belongs to</param>
         /// <param name="info">Texture information</param>
         /// <param name="sizeInfo">Size information of the texture</param>
+        /// <param name="range">Physical memory ranges where the texture data is located</param>
         /// <param name="firstLayer">The first layer of the texture, or 0 if the texture has no parent</param>
         /// <param name="firstLevel">The first mipmap level of the texture, or 0 if the texture has no parent</param>
         /// <param name="scaleFactor">The floating point scale factor to initialize with</param>
@@ -127,12 +123,13 @@ namespace Ryujinx.Graphics.Gpu.Image
             GpuContext       context,
             TextureInfo      info,
             SizeInfo         sizeInfo,
+            MultiRange       range,
             int              firstLayer,
             int              firstLevel,
             float            scaleFactor,
             TextureScaleMode scaleMode)
         {
-            InitializeTexture(context, info, sizeInfo);
+            InitializeTexture(context, info, sizeInfo, range);
 
             _firstLayer = firstLayer;
             _firstLevel = firstLevel;
@@ -149,13 +146,14 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="context">GPU context that the texture belongs to</param>
         /// <param name="info">Texture information</param>
         /// <param name="sizeInfo">Size information of the texture</param>
+        /// <param name="range">Physical memory ranges where the texture data is located</param>
         /// <param name="scaleMode">The scale mode to initialize with. If scaled, the texture's data is loaded immediately and scaled up</param>
-        public Texture(GpuContext context, TextureInfo info, SizeInfo sizeInfo, TextureScaleMode scaleMode)
+        public Texture(GpuContext context, TextureInfo info, SizeInfo sizeInfo, MultiRange range, TextureScaleMode scaleMode)
         {
             ScaleFactor = 1f; // Texture is first loaded at scale 1x.
             ScaleMode = scaleMode;
 
-            InitializeTexture(context, info, sizeInfo);
+            InitializeTexture(context, info, sizeInfo, range);
         }
 
         /// <summary>
@@ -166,10 +164,12 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="context">GPU context that the texture belongs to</param>
         /// <param name="info">Texture information</param>
         /// <param name="sizeInfo">Size information of the texture</param>
-        private void InitializeTexture(GpuContext context, TextureInfo info, SizeInfo sizeInfo)
+        /// <param name="range">Physical memory ranges where the texture data is located</param>
+        private void InitializeTexture(GpuContext context, TextureInfo info, SizeInfo sizeInfo, MultiRange range)
         {
             _context  = context;
             _sizeInfo = sizeInfo;
+            Range     = range;
 
             SetInfo(info);
 
@@ -186,7 +186,7 @@ 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(Address, Size);
+            _memoryTracking = _context.PhysicalMemory.BeginTracking(Range);
 
             if (withData)
             {
@@ -229,15 +229,17 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// </summary>
         /// <param name="info">Child texture information</param>
         /// <param name="sizeInfo">Child texture size information</param>
+        /// <param name="range">Physical memory ranges where the texture data is located</param>
         /// <param name="firstLayer">Start layer of the child texture on the parent texture</param>
         /// <param name="firstLevel">Start mipmap level of the child texture on the parent texture</param>
         /// <returns>The child texture</returns>
-        public Texture CreateView(TextureInfo info, SizeInfo sizeInfo, int firstLayer, int firstLevel)
+        public Texture CreateView(TextureInfo info, SizeInfo sizeInfo, MultiRange range, int firstLayer, int firstLevel)
         {
             Texture texture = new Texture(
                 _context,
                 info,
                 sizeInfo,
+                range,
                 _firstLayer + firstLayer,
                 _firstLevel + firstLevel,
                 ScaleFactor,
@@ -367,7 +369,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             ChangedSize = true;
 
             SetInfo(new TextureInfo(
-                Info.Address,
+                Info.GpuAddress,
                 width,
                 height,
                 depthOrLayers,
@@ -554,7 +556,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             _memoryTracking?.Reprotect();
 
-            ReadOnlySpan<byte> data = _context.PhysicalMemory.GetSpan(Address, (int)Size);
+            ReadOnlySpan<byte> data = _context.PhysicalMemory.GetSpan(Range);
 
             IsModified = false;
 
@@ -586,6 +588,10 @@ namespace Ryujinx.Graphics.Gpu.Image
             _hasData = true;
         }
 
+        /// <summary>
+        /// Uploads new texture data to the host GPU.
+        /// </summary>
+        /// <param name="data">New data</param>
         public void SetData(ReadOnlySpan<byte> data)
         {
             BlacklistScale();
@@ -653,7 +659,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                 {
                     string texInfo = $"{Info.Target} {Info.FormatInfo.Format} {Info.Width}x{Info.Height}x{Info.DepthOrLayers} levels {Info.Levels}";
 
-                    Logger.Debug?.Print(LogClass.Gpu, $"Invalid ASTC texture at 0x{Info.Address:X} ({texInfo}).");
+                    Logger.Debug?.Print(LogClass.Gpu, $"Invalid ASTC texture at 0x{Info.GpuAddress:X} ({texInfo}).");
                 }
 
                 data = decoded;
@@ -689,15 +695,14 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             if (tracked)
             {
-                _context.PhysicalMemory.Write(Address, GetTextureDataFromGpu(tracked));
+                _context.PhysicalMemory.Write(Range, GetTextureDataFromGpu(tracked));
             }
             else
             {
-                _context.PhysicalMemory.WriteUntracked(Address, GetTextureDataFromGpu(tracked));
+                _context.PhysicalMemory.WriteUntracked(Range, GetTextureDataFromGpu(tracked));
             }
         }
 
-
         /// <summary>
         /// Flushes the texture data, to be called from an external thread.
         /// The host backend must ensure that we have shared access to the resource from this thread.
@@ -725,7 +730,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     texture = _flushHostTexture = GetScaledHostTexture(1f, _flushHostTexture);
                 }
 
-                _context.PhysicalMemory.WriteUntracked(Address, GetTextureDataFromGpu(false, texture));
+                _context.PhysicalMemory.WriteUntracked(Range, GetTextureDataFromGpu(false, texture));
             });
         }
 
@@ -847,25 +852,23 @@ namespace Ryujinx.Graphics.Gpu.Image
                 return TextureMatchQuality.NoMatch;
             }
 
-            return Info.Address == info.Address && Info.Levels == info.Levels ? matchQuality : TextureMatchQuality.NoMatch;
+            return Info.Levels == info.Levels ? matchQuality : TextureMatchQuality.NoMatch;
         }
 
         /// <summary>
         /// Check if it's possible to create a view, with the given parameters, from this texture.
         /// </summary>
         /// <param name="info">Texture view information</param>
-        /// <param name="size">Texture view size</param>
+        /// <param name="range">Texture view physical memory ranges</param>
         /// <param name="firstLayer">Texture view initial layer on this texture</param>
         /// <param name="firstLevel">Texture view first mipmap level on this texture</param>
         /// <returns>The level of compatiblilty a view with the given parameters created from this texture has</returns>
-        public TextureViewCompatibility IsViewCompatible(
-            TextureInfo info,
-            ulong       size,
-            out int     firstLayer,
-            out int     firstLevel)
+        public TextureViewCompatibility IsViewCompatible(TextureInfo info, MultiRange range, out int firstLayer, out int firstLevel)
         {
+            int offset = Range.FindOffset(range);
+
             // Out of range.
-            if (info.Address < Address || info.Address + size > EndAddress)
+            if (offset < 0)
             {
                 firstLayer = 0;
                 firstLevel = 0;
@@ -873,9 +876,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                 return TextureViewCompatibility.Incompatible;
             }
 
-            int offset = (int)(info.Address - Address);
-
-            if (!_sizeInfo.FindView(offset, (int)size, out firstLayer, out firstLevel))
+            if (!_sizeInfo.FindView(offset, out firstLayer, out firstLevel))
             {
                 return TextureViewCompatibility.Incompatible;
             }
@@ -1045,17 +1046,6 @@ namespace Ryujinx.Graphics.Gpu.Image
             HostTexture = hostTexture;
         }
 
-        /// <summary>
-        /// Checks if the texture overlaps with a memory range.
-        /// </summary>
-        /// <param name="address">Start address of the range</param>
-        /// <param name="size">Size of the range</param>
-        /// <returns>True if the texture overlaps with the range, false otherwise</returns>
-        public bool OverlapsWith(ulong address, ulong size)
-        {
-            return Address < address + size && address < EndAddress;
-        }
-
         /// <summary>
         /// Determine if any of our child textures are compaible as views of the given texture.
         /// </summary>
@@ -1070,7 +1060,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             foreach (Texture view in _views)
             {
-                if (texture.IsViewCompatible(view.Info, view.Size, out int _, out int _) != TextureViewCompatibility.Incompatible)
+                if (texture.IsViewCompatible(view.Info, view.Range, out _, out _) != TextureViewCompatibility.Incompatible)
                 {
                     return true;
                 }
@@ -1153,7 +1143,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped.
 
-            CpuRegionHandle tracking = _memoryTracking;
+            var tracking = _memoryTracking;
             tracking?.Reprotect();
             tracking?.RegisterAction(null);
         }

+ 2 - 2
Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs

@@ -303,7 +303,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     // Ensure that the buffer texture is using the correct buffer as storage.
                     // Buffers are frequently re-created to accomodate larger data, so we need to re-bind
                     // to ensure we're not using a old buffer that was already deleted.
-                    _context.Methods.BufferManager.SetBufferTextureStorage(hostTexture, texture.Address, texture.Size, _isCompute);
+                    _context.Methods.BufferManager.SetBufferTextureStorage(hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, _isCompute);
                 }
 
                 Sampler sampler = _samplerPool.Get(samplerId);
@@ -354,7 +354,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     // Ensure that the buffer texture is using the correct buffer as storage.
                     // Buffers are frequently re-created to accomodate larger data, so we need to re-bind
                     // to ensure we're not using a old buffer that was already deleted.
-                    _context.Methods.BufferManager.SetBufferTextureStorage(hostTexture, texture.Address, texture.Size, _isCompute);
+                    _context.Methods.BufferManager.SetBufferTextureStorage(hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, _isCompute);
                 }
 
                 if (_imageState[stageIndex][index].Texture != hostTexture || _rebind)

+ 5 - 5
Ryujinx.Graphics.Gpu/Image/TextureInfo.cs

@@ -9,9 +9,9 @@ namespace Ryujinx.Graphics.Gpu.Image
     struct TextureInfo
     {
         /// <summary>
-        /// Address of the texture in guest memory.
+        /// Address of the texture in GPU mapped memory.
         /// </summary>
-        public ulong Address { get; }
+        public ulong GpuAddress { get; }
 
         /// <summary>
         /// The width of the texture.
@@ -112,7 +112,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <summary>
         /// Constructs the texture information structure.
         /// </summary>
-        /// <param name="address">The address of the texture</param>
+        /// <param name="gpuAddress">The GPU address of the texture</param>
         /// <param name="width">The width of the texture</param>
         /// <param name="height">The height or the texture</param>
         /// <param name="depthOrLayers">The depth or layers count of the texture</param>
@@ -132,7 +132,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="swizzleB">Swizzle for the blue color channel</param>
         /// <param name="swizzleA">Swizzle for the alpha color channel</param>
         public TextureInfo(
-            ulong            address,
+            ulong            gpuAddress,
             int              width,
             int              height,
             int              depthOrLayers,
@@ -152,7 +152,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             SwizzleComponent swizzleB         = SwizzleComponent.Blue,
             SwizzleComponent swizzleA         = SwizzleComponent.Alpha)
         {
-            Address          = address;
+            GpuAddress       = gpuAddress;
             Width            = width;
             Height           = height;
             DepthOrLayers    = depthOrLayers;

+ 52 - 48
Ryujinx.Graphics.Gpu/Image/TextureManager.cs

@@ -37,14 +37,11 @@ namespace Ryujinx.Graphics.Gpu.Image
         private readonly TextureBindingsManager _gpBindingsManager;
 
         private readonly Texture[] _rtColors;
-
-        private Texture _rtDepthStencil;
-
         private readonly ITexture[] _rtHostColors;
-
+        private Texture _rtDepthStencil;
         private ITexture _rtHostDs;
 
-        private readonly RangeList<Texture> _textures;
+        private readonly MultiRangeList<Texture> _textures;
 
         private Texture[] _textureOverlaps;
         private OverlapInfo[] _overlapInfo;
@@ -70,10 +67,9 @@ namespace Ryujinx.Graphics.Gpu.Image
             _gpBindingsManager = new TextureBindingsManager(context, texturePoolCache, isCompute: false);
 
             _rtColors = new Texture[Constants.TotalRenderTargets];
-
             _rtHostColors = new ITexture[Constants.TotalRenderTargets];
 
-            _textures = new RangeList<Texture>();
+            _textures = new MultiRangeList<Texture>();
 
             _textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
             _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
@@ -470,13 +466,6 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <returns>The texture</returns>
         public Texture FindOrCreateTexture(CopyTexture copyTexture, FormatInfo formatInfo, bool preferScaling = true, Size? sizeHint = null)
         {
-            ulong address = _context.MemoryManager.Translate(copyTexture.Address.Pack());
-
-            if (address == MemoryManager.PteUnmapped)
-            {
-                return null;
-            }
-
             int gobBlocksInY = copyTexture.MemoryLayout.UnpackGobBlocksInY();
             int gobBlocksInZ = copyTexture.MemoryLayout.UnpackGobBlocksInZ();
 
@@ -492,7 +481,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
 
             TextureInfo info = new TextureInfo(
-                address,
+                copyTexture.Address.Pack(),
                 width,
                 copyTexture.Height,
                 copyTexture.Depth,
@@ -514,9 +503,9 @@ namespace Ryujinx.Graphics.Gpu.Image
                 flags |= TextureSearchFlags.WithUpscale;
             }
 
-            Texture texture = FindOrCreateTexture(info, flags, 0, sizeHint);
+            Texture texture = FindOrCreateTexture(flags, info, 0, sizeHint);
 
-            texture.SynchronizeMemory();
+            texture?.SynchronizeMemory();
 
             return texture;
         }
@@ -531,13 +520,6 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <returns>The texture</returns>
         public Texture FindOrCreateTexture(RtColorState colorState, int samplesInX, int samplesInY, Size sizeHint)
         {
-            ulong address = _context.MemoryManager.Translate(colorState.Address.Pack());
-
-            if (address == MemoryManager.PteUnmapped)
-            {
-                return null;
-            }
-
             bool isLinear = colorState.MemoryLayout.UnpackIsLinear();
 
             int gobBlocksInY = colorState.MemoryLayout.UnpackGobBlocksInY();
@@ -583,7 +565,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
 
             TextureInfo info = new TextureInfo(
-                address,
+                colorState.Address.Pack(),
                 width,
                 colorState.Height,
                 colorState.Depth,
@@ -600,9 +582,9 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             int layerSize = !isLinear ? colorState.LayerSize * 4 : 0;
 
-            Texture texture = FindOrCreateTexture(info, TextureSearchFlags.WithUpscale, layerSize, sizeHint);
+            Texture texture = FindOrCreateTexture(TextureSearchFlags.WithUpscale, info, layerSize, sizeHint);
 
-            texture.SynchronizeMemory();
+            texture?.SynchronizeMemory();
 
             return texture;
         }
@@ -618,13 +600,6 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <returns>The texture</returns>
         public Texture FindOrCreateTexture(RtDepthStencilState dsState, Size3D size, int samplesInX, int samplesInY, Size sizeHint)
         {
-            ulong address = _context.MemoryManager.Translate(dsState.Address.Pack());
-
-            if (address == MemoryManager.PteUnmapped)
-            {
-                return null;
-            }
-
             int gobBlocksInY = dsState.MemoryLayout.UnpackGobBlocksInY();
             int gobBlocksInZ = dsState.MemoryLayout.UnpackGobBlocksInZ();
 
@@ -635,7 +610,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             FormatInfo formatInfo = dsState.Format.Convert();
 
             TextureInfo info = new TextureInfo(
-                address,
+                dsState.Address.Pack(),
                 size.Width,
                 size.Height,
                 size.Depth,
@@ -650,9 +625,9 @@ namespace Ryujinx.Graphics.Gpu.Image
                 target,
                 formatInfo);
 
-            Texture texture = FindOrCreateTexture(info, TextureSearchFlags.WithUpscale, dsState.LayerSize * 4, sizeHint);
+            Texture texture = FindOrCreateTexture(TextureSearchFlags.WithUpscale, info, dsState.LayerSize * 4, sizeHint);
 
-            texture.SynchronizeMemory();
+            texture?.SynchronizeMemory();
 
             return texture;
         }
@@ -660,12 +635,13 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <summary>
         /// Tries to find an existing texture, or create a new one if not found.
         /// </summary>
-        /// <param name="info">Texture information of the texture to be found or created</param>
         /// <param name="flags">The texture search flags, defines texture comparison rules</param>
+        /// <param name="info">Texture information of the texture to be found or created</param>
         /// <param name="layerSize">Size in bytes of a single texture layer</param>
         /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param>
+        /// <param name="range">Optional ranges of physical memory where the texture data is located</param>
         /// <returns>The texture</returns>
-        public Texture FindOrCreateTexture(TextureInfo info, TextureSearchFlags flags = TextureSearchFlags.None, int layerSize = 0, Size? sizeHint = null)
+        public Texture FindOrCreateTexture(TextureSearchFlags flags, TextureInfo info, int layerSize = 0, Size? sizeHint = null, MultiRange? range = null)
         {
             bool isSamplerTexture = (flags & TextureSearchFlags.ForSampler) != 0;
 
@@ -677,12 +653,28 @@ namespace Ryujinx.Graphics.Gpu.Image
                 scaleMode = (flags & TextureSearchFlags.WithUpscale) != 0 ? TextureScaleMode.Scaled : TextureScaleMode.Eligible;
             }
 
+            ulong address;
+
+            if (range != null)
+            {
+                address = range.Value.GetSubRange(0).Address;
+            }
+            else
+            {
+                address = _context.MemoryManager.Translate(info.GpuAddress);
+
+                if (address == MemoryManager.PteUnmapped)
+                {
+                    return null;
+                }
+            }
+
             int sameAddressOverlapsCount;
 
             lock (_textures)
             {
                 // Try to find a perfect texture match, with the same address and parameters.
-                sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps);
+                sameAddressOverlapsCount = _textures.FindOverlaps(address, ref _textureOverlaps);
             }
 
             Texture texture = null;
@@ -693,6 +685,12 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 Texture overlap = _textureOverlaps[index];
 
+                bool rangeMatches = range != null ? overlap.Range.Equals(range.Value) : overlap.Info.GpuAddress == info.GpuAddress;
+                if (!rangeMatches)
+                {
+                    continue;
+                }
+
                 TextureMatchQuality matchQuality = overlap.IsExactMatch(info, flags);
 
                 if (matchQuality == TextureMatchQuality.Perfect)
@@ -727,19 +725,25 @@ namespace Ryujinx.Graphics.Gpu.Image
             // Calculate texture sizes, used to find all overlapping textures.
             SizeInfo sizeInfo = info.CalculateSizeInfo(layerSize);
 
-            // Find view compatible matches.
             ulong size = (ulong)sizeInfo.TotalSize;
+
+            if (range == null)
+            {
+                range = _context.MemoryManager.GetPhysicalRegions(info.GpuAddress, size);
+            }
+
+            // Find view compatible matches.
             int overlapsCount;
 
             lock (_textures)
             {
-                overlapsCount = _textures.FindOverlaps(info.Address, size, ref _textureOverlaps);
+                overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
             }
 
             for (int index = 0; index < overlapsCount; index++)
             {
                 Texture overlap = _textureOverlaps[index];
-                TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, size, out int firstLayer, out int firstLevel);
+                TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, range.Value, out int firstLayer, out int firstLevel);
 
                 if (overlapCompatibility == TextureViewCompatibility.Full)
                 {
@@ -750,7 +754,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                         info = oInfo;
                     }
 
-                    texture = overlap.CreateView(oInfo, sizeInfo, firstLayer, firstLevel);
+                    texture = overlap.CreateView(oInfo, sizeInfo, range.Value, firstLayer, firstLevel);
 
                     if (overlap.IsModified)
                     {
@@ -771,7 +775,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             // No match, create a new texture.
             if (texture == null)
             {
-                texture = new Texture(_context, info, sizeInfo, scaleMode);
+                texture = new Texture(_context, info, sizeInfo, range.Value, scaleMode);
 
                 // Step 1: Find textures that are view compatible with the new texture.
                 // Any textures that are incompatible will contain garbage data, so they should be removed where possible.
@@ -784,7 +788,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     Texture overlap = _textureOverlaps[index];
                     bool overlapInCache = overlap.CacheNode != null;
 
-                    TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Size, out int firstLayer, out int firstLevel);
+                    TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Range, out int firstLayer, out int firstLevel);
 
                     if (compatibility != TextureViewCompatibility.Incompatible)
                     {
@@ -812,7 +816,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                         // If the data has been modified by the CPU, then it also shouldn't be flushed.
                         bool modified = overlap.ConsumeModified();
 
-                        bool flush = overlapInCache && !modified && (overlap.Address < texture.Address || overlap.EndAddress > texture.EndAddress) && overlap.HasViewCompatibleChild(texture);
+                        bool flush = overlapInCache && !modified && !texture.Range.Contains(overlap.Range) && overlap.HasViewCompatibleChild(texture);
 
                         setData |= modified || flush;
 
@@ -1070,7 +1074,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
 
             return new TextureInfo(
-                info.Address,
+                info.GpuAddress,
                 width,
                 height,
                 depthOrLayers,

+ 12 - 13
Ryujinx.Graphics.Gpu/Image/TexturePool.cs

@@ -54,15 +54,14 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                 TextureInfo info = GetInfo(descriptor, out int layerSize);
 
-                // Bad address. We can't add a texture with a invalid address
-                // to the cache.
-                if (info.Address == MemoryManager.PteUnmapped)
+                texture = Context.Methods.TextureManager.FindOrCreateTexture(TextureSearchFlags.ForSampler, info, layerSize);
+
+                // If this happens, then the texture address is invalid, we can't add it to the cache.
+                if (texture == null)
                 {
                     return null;
                 }
 
-                texture = Context.Methods.TextureManager.FindOrCreateTexture(info, TextureSearchFlags.ForSampler, layerSize);
-
                 texture.IncrementReferenceCount();
 
                 Items[id] = texture;
@@ -123,7 +122,8 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                     // If the descriptors are the same, the texture is the same,
                     // we don't need to remove as it was not modified. Just continue.
-                    if (texture.IsExactMatch(GetInfo(descriptor, out _), TextureSearchFlags.Strict) != TextureMatchQuality.NoMatch)
+                    if (texture.Info.GpuAddress == descriptor.UnpackAddress() &&
+                        texture.IsExactMatch(GetInfo(descriptor, out _), TextureSearchFlags.Strict) != TextureMatchQuality.NoMatch)
                     {
                         continue;
                     }
@@ -143,9 +143,6 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <returns>The texture information</returns>
         private TextureInfo GetInfo(TextureDescriptor descriptor, out int layerSize)
         {
-            ulong address = Context.MemoryManager.Translate(descriptor.UnpackAddress());
-            bool addressIsValid = address != MemoryManager.PteUnmapped;
-
             int width         = descriptor.UnpackWidth();
             int height        = descriptor.UnpackHeight();
             int depthOrLayers = descriptor.UnpackDepth();
@@ -183,9 +180,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             uint format = descriptor.UnpackFormat();
             bool srgb   = descriptor.UnpackSrgb();
 
+            ulong gpuVa = descriptor.UnpackAddress();
+
             if (!FormatTable.TryGetTextureFormat(format, srgb, out FormatInfo formatInfo))
             {
-                if (addressIsValid && (int)format > 0)
+                if (Context.MemoryManager.IsMapped(gpuVa) && (int)format > 0)
                 {
                     Logger.Error?.Print(LogClass.Gpu, $"Invalid texture format 0x{format:X} (sRGB: {srgb}).");
                 }
@@ -204,7 +203,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             int maxLod = descriptor.UnpackMaxLevelInclusive();
 
             // Linear textures don't support mipmaps, so we don't handle this case here.
-            if ((minLod != 0 || maxLod + 1 != levels) && target != Target.TextureBuffer && !isLinear && addressIsValid)
+            if ((minLod != 0 || maxLod + 1 != levels) && target != Target.TextureBuffer && !isLinear)
             {
                 int depth  = TextureInfo.GetDepth(target, depthOrLayers);
                 int layers = TextureInfo.GetLayers(target, depthOrLayers);
@@ -229,7 +228,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     // If the base level is not zero, we additionally add the mip level offset
                     // to the address, this allows the texture manager to find the base level from the
                     // address if there is a overlapping texture on the cache that can contain the new texture.
-                    address += (ulong)sizeInfo.GetMipOffset(minLod);
+                    gpuVa += (ulong)sizeInfo.GetMipOffset(minLod);
 
                     width  = Math.Max(1, width  >> minLod);
                     height = Math.Max(1, height >> minLod);
@@ -274,7 +273,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
 
             return new TextureInfo(
-                address,
+                gpuVa,
                 width,
                 height,
                 depthOrLayers,

+ 60 - 0
Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs

@@ -0,0 +1,60 @@
+using Ryujinx.Cpu.Tracking;
+using Ryujinx.Memory.Tracking;
+using System;
+
+namespace Ryujinx.Graphics.Gpu.Memory
+{
+    class GpuRegionHandle : IRegionHandle
+    {
+        private readonly CpuRegionHandle[] _cpuRegionHandles;
+
+        public bool Dirty
+        {
+            get
+            {
+                foreach (var regionHandle in _cpuRegionHandles)
+                {
+                    if (regionHandle.Dirty)
+                    {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+        }
+
+        public ulong Address => throw new NotSupportedException();
+        public ulong Size => throw new NotSupportedException();
+        public ulong EndAddress => throw new NotSupportedException();
+
+        public GpuRegionHandle(CpuRegionHandle[] cpuRegionHandles)
+        {
+            _cpuRegionHandles = cpuRegionHandles;
+        }
+
+        public void Dispose()
+        {
+            foreach (var regionHandle in _cpuRegionHandles)
+            {
+                regionHandle.Dispose();
+            }
+        }
+
+        public void RegisterAction(RegionSignal action)
+        {
+            foreach (var regionHandle in _cpuRegionHandles)
+            {
+                regionHandle.RegisterAction(action);
+            }
+        }
+
+        public void Reprotect()
+        {
+            foreach (var regionHandle in _cpuRegionHandles)
+            {
+                regionHandle.Reprotect();
+            }
+        }
+    }
+}

+ 248 - 38
Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs

@@ -1,5 +1,7 @@
 using Ryujinx.Memory;
+using Ryujinx.Memory.Range;
 using System;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 
@@ -8,7 +10,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
     /// <summary>
     /// GPU memory manager.
     /// </summary>
-    public class MemoryManager
+    public class MemoryManager : IWritableBlock
     {
         private const int PtLvl0Bits = 14;
         private const int PtLvl1Bits = 14;
@@ -24,6 +26,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
 
         private const int PtLvl0Bit = PtPageBits + PtLvl1Bits;
         private const int PtLvl1Bit = PtPageBits;
+        private const int AddressSpaceBits = PtPageBits + PtLvl1Bits + PtLvl0Bits;
 
         public const ulong PteUnmapped = 0xffffffff_ffffffff;
 
@@ -46,26 +49,69 @@ namespace Ryujinx.Graphics.Gpu.Memory
         /// Reads data from GPU mapped memory.
         /// </summary>
         /// <typeparam name="T">Type of the data</typeparam>
-        /// <param name="gpuVa">GPU virtual address where the data is located</param>
+        /// <param name="va">GPU virtual address where the data is located</param>
         /// <returns>The data at the specified memory location</returns>
-        public T Read<T>(ulong gpuVa) where T : unmanaged
+        public T Read<T>(ulong va) where T : unmanaged
         {
-            ulong processVa = Translate(gpuVa);
-
-            return MemoryMarshal.Cast<byte, T>(_context.PhysicalMemory.GetSpan(processVa, Unsafe.SizeOf<T>()))[0];
+            return MemoryMarshal.Cast<byte, T>(GetSpan(va, Unsafe.SizeOf<T>()))[0];
         }
 
         /// <summary>
         /// Gets a read-only span of data from GPU mapped memory.
         /// </summary>
-        /// <param name="gpuVa">GPU virtual address where the data is located</param>
+        /// <param name="va">GPU virtual address where the data is located</param>
         /// <param name="size">Size of the data</param>
         /// <returns>The span of the data at the specified memory location</returns>
-        public ReadOnlySpan<byte> GetSpan(ulong gpuVa, int size)
+        public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
         {
-            ulong processVa = Translate(gpuVa);
+            if (IsContiguous(va, size))
+            {
+                return _context.PhysicalMemory.GetSpan(Translate(va), size, tracked);
+            }
+            else
+            {
+                Span<byte> data = new byte[size];
 
-            return _context.PhysicalMemory.GetSpan(processVa, size);
+                ReadImpl(va, data, tracked);
+
+                return data;
+            }
+        }
+
+        /// <summary>
+        /// Reads data from a possibly non-contiguous region of GPU mapped memory.
+        /// </summary>
+        /// <param name="va">GPU virtual address of the data</param>
+        /// <param name="data">Span to write the read data into</param>
+        /// <param name="tracked">True to enable write tracking on read, false otherwise</param>
+        private void ReadImpl(ulong va, Span<byte> data, bool tracked)
+        {
+            if (data.Length == 0)
+            {
+                return;
+            }
+
+            int offset = 0, size;
+
+            if ((va & PageMask) != 0)
+            {
+                ulong pa = Translate(va);
+
+                size = Math.Min(data.Length, (int)PageSize - (int)(va & PageMask));
+
+                _context.PhysicalMemory.GetSpan(pa, size, tracked).CopyTo(data.Slice(0, size));
+
+                offset += size;
+            }
+
+            for (; offset < data.Length; offset += size)
+            {
+                ulong pa = Translate(va + (ulong)offset);
+
+                size = Math.Min(data.Length - offset, (int)PageSize);
+
+                _context.PhysicalMemory.GetSpan(pa, size, tracked).CopyTo(data.Slice(offset, size));
+            }
         }
 
         /// <summary>
@@ -74,36 +120,91 @@ namespace Ryujinx.Graphics.Gpu.Memory
         /// <param name="address">Start address of the range</param>
         /// <param name="size">Size in bytes to be range</param>
         /// <returns>A writable region with the data at the specified memory location</returns>
-        public WritableRegion GetWritableRegion(ulong gpuVa, int size)
+        public WritableRegion GetWritableRegion(ulong va, int size)
         {
-            ulong processVa = Translate(gpuVa);
+            if (IsContiguous(va, size))
+            {
+                return _context.PhysicalMemory.GetWritableRegion(Translate(va), size);
+            }
+            else
+            {
+                Memory<byte> memory = new byte[size];
+
+                GetSpan(va, size).CopyTo(memory.Span);
 
-            return _context.PhysicalMemory.GetWritableRegion(processVa, size);
+                return new WritableRegion(this, va, memory);
+            }
         }
 
         /// <summary>
         /// Writes data to GPU mapped memory.
         /// </summary>
         /// <typeparam name="T">Type of the data</typeparam>
-        /// <param name="gpuVa">GPU virtual address to write the value into</param>
+        /// <param name="va">GPU virtual address to write the value into</param>
         /// <param name="value">The value to be written</param>
-        public void Write<T>(ulong gpuVa, T value) where T : unmanaged
+        public void Write<T>(ulong va, T value) where T : unmanaged
         {
-            ulong processVa = Translate(gpuVa);
-
-            _context.PhysicalMemory.Write(processVa, MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1)));
+            Write(va, MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1)));
         }
 
         /// <summary>
         /// Writes data to GPU mapped memory.
         /// </summary>
-        /// <param name="gpuVa">GPU virtual address to write the data into</param>
+        /// <param name="va">GPU virtual address to write the data into</param>
+        /// <param name="data">The data to be written</param>
+        public void Write(ulong va, ReadOnlySpan<byte> data)
+        {
+            WriteImpl(va, data, _context.PhysicalMemory.Write);
+        }
+
+        /// <summary>
+        /// Writes data to GPU mapped memory without write tracking.
+        /// </summary>
+        /// <param name="va">GPU virtual address to write the data into</param>
         /// <param name="data">The data to be written</param>
-        public void Write(ulong gpuVa, ReadOnlySpan<byte> data)
+        public void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
+        {
+            WriteImpl(va, data, _context.PhysicalMemory.WriteUntracked);
+        }
+
+        private delegate void WriteCallback(ulong address, ReadOnlySpan<byte> data);
+
+        /// <summary>
+        /// Writes data to possibly non-contiguous GPU mapped memory.
+        /// </summary>
+        /// <param name="va">GPU virtual address of the region to write into</param>
+        /// <param name="data">Data to be written</param>
+        /// <param name="writeCallback">Write callback</param>
+        private void WriteImpl(ulong va, ReadOnlySpan<byte> data, WriteCallback writeCallback)
         {
-            ulong processVa = Translate(gpuVa);
+            if (IsContiguous(va, data.Length))
+            {
+                writeCallback(Translate(va), data);
+            }
+            else
+            {
+                int offset = 0, size;
+
+                if ((va & PageMask) != 0)
+                {
+                    ulong pa = Translate(va);
+
+                    size = Math.Min(data.Length, (int)PageSize - (int)(va & PageMask));
+
+                    writeCallback(pa, data.Slice(0, size));
 
-            _context.PhysicalMemory.Write(processVa, data);
+                    offset += size;
+                }
+
+                for (; offset < data.Length; offset += size)
+                {
+                    ulong pa = Translate(va + (ulong)offset);
+
+                    size = Math.Min(data.Length - offset, (int)PageSize);
+
+                    writeCallback(pa, data.Slice(offset, size));
+                }
+            }
         }
 
         /// <summary>
@@ -147,42 +248,151 @@ namespace Ryujinx.Graphics.Gpu.Memory
             }
         }
 
+        /// <summary>
+        /// Checks if a region of GPU mapped memory is contiguous.
+        /// </summary>
+        /// <param name="va">GPU virtual address of the region</param>
+        /// <param name="size">Size of the region</param>
+        /// <returns>True if the region is contiguous, false otherwise</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private bool IsContiguous(ulong va, int size)
+        {
+            if (!ValidateAddress(va) || GetPte(va) == PteUnmapped)
+            {
+                return false;
+            }
+
+            ulong endVa = (va + (ulong)size + PageMask) & ~PageMask;
+
+            va &= ~PageMask;
+
+            int pages = (int)((endVa - va) / PageSize);
+
+            for (int page = 0; page < pages - 1; page++)
+            {
+                if (!ValidateAddress(va + PageSize) || GetPte(va + PageSize) == PteUnmapped)
+                {
+                    return false;
+                }
+
+                if (Translate(va) + PageSize != Translate(va + PageSize))
+                {
+                    return false;
+                }
+
+                va += PageSize;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Gets the physical regions that make up the given virtual address region.
+        /// </summary>
+        /// <param name="va">Virtual address of the range</param>
+        /// <param name="size">Size of the range</param>
+        /// <returns>Multi-range with the physical regions</returns>
+        /// <exception cref="InvalidMemoryRegionException">The memory region specified by <paramref name="va"/> and <paramref name="size"/> is not fully mapped</exception>
+        public MultiRange GetPhysicalRegions(ulong va, ulong size)
+        {
+            if (IsContiguous(va, (int)size))
+            {
+                return new MultiRange(Translate(va), size);
+            }
+
+            if (!IsMapped(va))
+            {
+                throw new InvalidMemoryRegionException($"The specified GPU virtual address 0x{va:X} is not mapped.");
+            }
+
+            ulong regionStart = Translate(va);
+            ulong regionSize = Math.Min(size, PageSize - (va & PageMask));
+
+            ulong endVa = va + size;
+            ulong endVaRounded = (endVa + PageMask) & ~PageMask;
+
+            va &= ~PageMask;
+
+            int pages = (int)((endVaRounded - va) / PageSize);
+
+            var regions = new List<MemoryRange>();
+
+            for (int page = 0; page < pages - 1; page++)
+            {
+                if (!IsMapped(va + PageSize))
+                {
+                    throw new InvalidMemoryRegionException($"The specified GPU virtual memory range 0x{va:X}..0x{(va + size):X} is not fully mapped.");
+                }
+
+                ulong newPa = Translate(va + PageSize);
+
+                if (Translate(va) + PageSize != newPa)
+                {
+                    regions.Add(new MemoryRange(regionStart, regionSize));
+                    regionStart = newPa;
+                    regionSize = 0;
+                }
+
+                va += PageSize;
+                regionSize += Math.Min(endVa - va, PageSize);
+            }
+
+            regions.Add(new MemoryRange(regionStart, regionSize));
+
+            return new MultiRange(regions.ToArray());
+        }
+
+        /// <summary>
+        /// Validates a GPU virtual address.
+        /// </summary>
+        /// <param name="va">Address to validate</param>
+        /// <returns>True if the address is valid, false otherwise</returns>
+        private static bool ValidateAddress(ulong va)
+        {
+            return va < (1UL << AddressSpaceBits);
+        }
+
         /// <summary>
         /// Checks if a given page is mapped.
         /// </summary>
-        /// <param name="gpuVa">GPU virtual address of the page to check</param>
+        /// <param name="va">GPU virtual address of the page to check</param>
         /// <returns>True if the page is mapped, false otherwise</returns>
-        public bool IsMapped(ulong gpuVa)
+        public bool IsMapped(ulong va)
         {
-            return Translate(gpuVa) != PteUnmapped;
+            return Translate(va) != PteUnmapped;
         }
 
         /// <summary>
         /// Translates a GPU virtual address to a CPU virtual address.
         /// </summary>
-        /// <param name="gpuVa">GPU virtual address to be translated</param>
-        /// <returns>CPU virtual address</returns>
-        public ulong Translate(ulong gpuVa)
+        /// <param name="va">GPU virtual address to be translated</param>
+        /// <returns>CPU virtual address, or <see cref="PteUnmapped"/> if unmapped</returns>
+        public ulong Translate(ulong va)
         {
-            ulong baseAddress = GetPte(gpuVa);
+            if (!ValidateAddress(va))
+            {
+                return PteUnmapped;
+            }
+
+            ulong baseAddress = GetPte(va);
 
             if (baseAddress == PteUnmapped)
             {
                 return PteUnmapped;
             }
 
-            return baseAddress + (gpuVa & PageMask);
+            return baseAddress + (va & PageMask);
         }
 
         /// <summary>
         /// Gets the Page Table entry for a given GPU virtual address.
         /// </summary>
-        /// <param name="gpuVa">GPU virtual address</param>
+        /// <param name="va">GPU virtual address</param>
         /// <returns>Page table entry (CPU virtual address)</returns>
-        private ulong GetPte(ulong gpuVa)
+        private ulong GetPte(ulong va)
         {
-            ulong l0 = (gpuVa >> PtLvl0Bit) & PtLvl0Mask;
-            ulong l1 = (gpuVa >> PtLvl1Bit) & PtLvl1Mask;
+            ulong l0 = (va >> PtLvl0Bit) & PtLvl0Mask;
+            ulong l1 = (va >> PtLvl1Bit) & PtLvl1Mask;
 
             if (_pageTable[l0] == null)
             {
@@ -195,12 +405,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
         /// <summary>
         /// Sets a Page Table entry at a given GPU virtual address.
         /// </summary>
-        /// <param name="gpuVa">GPU virtual address</param>
+        /// <param name="va">GPU virtual address</param>
         /// <param name="pte">Page table entry (CPU virtual address)</param>
-        private void SetPte(ulong gpuVa, ulong pte)
+        private void SetPte(ulong va, ulong pte)
         {
-            ulong l0 = (gpuVa >> PtLvl0Bit) & PtLvl0Mask;
-            ulong l1 = (gpuVa >> PtLvl1Bit) & PtLvl1Mask;
+            ulong l0 = (va >> PtLvl0Bit) & PtLvl0Mask;
+            ulong l1 = (va >> PtLvl1Bit) & PtLvl1Mask;
 
             if (_pageTable[l0] == null)
             {

+ 99 - 0
Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs

@@ -1,6 +1,7 @@
 using Ryujinx.Cpu;
 using Ryujinx.Cpu.Tracking;
 using Ryujinx.Memory;
+using Ryujinx.Memory.Range;
 using System;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
@@ -38,6 +39,37 @@ namespace Ryujinx.Graphics.Gpu.Memory
             return _cpuMemory.GetSpan(address, size, tracked);
         }
 
+        /// <summary>
+        /// Gets a span of data from the application process.
+        /// </summary>
+        /// <param name="range">Ranges of physical memory where the data is located</param>
+        /// <param name="tracked">True if read tracking is triggered on the span</param>
+        /// <returns>A read only span of the data at the specified memory location</returns>
+        public ReadOnlySpan<byte> GetSpan(MultiRange range, bool tracked = false)
+        {
+            if (range.Count == 1)
+            {
+                var singleRange = range.GetSubRange(0);
+                return _cpuMemory.GetSpan(singleRange.Address, (int)singleRange.Size, tracked);
+            }
+            else
+            {
+                Span<byte> data = new byte[range.GetSize()];
+
+                int offset = 0;
+
+                for (int i = 0; i < range.Count; i++)
+                {
+                    var currentRange = range.GetSubRange(i);
+                    int size = (int)currentRange.Size;
+                    _cpuMemory.GetSpan(currentRange.Address, size, tracked).CopyTo(data.Slice(offset, size));
+                    offset += size;
+                }
+
+                return data;
+            }
+        }
+
         /// <summary>
         /// Gets a writable region from the application process.
         /// </summary>
@@ -70,6 +102,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
             _cpuMemory.Write(address, data);
         }
 
+        /// <summary>
+        /// Writes data to the application process.
+        /// </summary>
+        /// <param name="range">Ranges of physical memory where the data is located</param>
+        /// <param name="data">Data to be written</param>
+        public void Write(MultiRange range, ReadOnlySpan<byte> data)
+        {
+            WriteImpl(range, data, _cpuMemory.Write);
+        }
+
         /// <summary>
         /// Writes data to the application process, without any tracking.
         /// </summary>
@@ -80,6 +122,45 @@ namespace Ryujinx.Graphics.Gpu.Memory
             _cpuMemory.WriteUntracked(address, data);
         }
 
+        /// <summary>
+        /// Writes data to the application process, without any tracking.
+        /// </summary>
+        /// <param name="range">Ranges of physical memory where the data is located</param>
+        /// <param name="data">Data to be written</param>
+        public void WriteUntracked(MultiRange range, ReadOnlySpan<byte> data)
+        {
+            WriteImpl(range, data, _cpuMemory.WriteUntracked);
+        }
+
+        private delegate void WriteCallback(ulong address, ReadOnlySpan<byte> data);
+
+        /// <summary>
+        /// Writes data to the application process, using the supplied callback method.
+        /// </summary>
+        /// <param name="range">Ranges of physical memory where the data is located</param>
+        /// <param name="data">Data to be written</param>
+        /// <param name="writeCallback">Callback method that will perform the write</param>
+        private void WriteImpl(MultiRange range, ReadOnlySpan<byte> data, WriteCallback writeCallback)
+        {
+            if (range.Count == 1)
+            {
+                var singleRange = range.GetSubRange(0);
+                writeCallback(singleRange.Address, data);
+            }
+            else
+            {
+                int offset = 0;
+
+                for (int i = 0; i < range.Count; i++)
+                {
+                    var currentRange = range.GetSubRange(i);
+                    int size = (int)currentRange.Size;
+                    writeCallback(currentRange.Address, data.Slice(offset, size));
+                    offset += size;
+                }
+            }
+        }
+
         /// <summary>
         /// Obtains a memory tracking handle for the given virtual region. This should be disposed when finished with.
         /// </summary>
@@ -91,6 +172,24 @@ namespace Ryujinx.Graphics.Gpu.Memory
             return _cpuMemory.BeginTracking(address, size);
         }
 
+        /// <summary>
+        /// Obtains a memory tracking handle for the given virtual region. This should be disposed when finished with.
+        /// </summary>
+        /// <param name="range">Ranges of physical memory where the data is located</param>
+        /// <returns>The memory tracking handle</returns>
+        public GpuRegionHandle BeginTracking(MultiRange range)
+        {
+            var cpuRegionHandles = new CpuRegionHandle[range.Count];
+
+            for (int i = 0; i < range.Count; i++)
+            {
+                var currentRange = range.GetSubRange(i);
+                cpuRegionHandles[i] = _cpuMemory.BeginTracking(currentRange.Address, currentRange.Size);
+            }
+
+            return new GpuRegionHandle(cpuRegionHandles);
+        }
+
         /// <summary>
         /// Obtains a memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with.
         /// </summary>

+ 28 - 3
Ryujinx.Graphics.Gpu/Window.cs

@@ -1,5 +1,7 @@
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu.Image;
+using Ryujinx.Graphics.Texture;
+using Ryujinx.Memory.Range;
 using System;
 using System.Collections.Concurrent;
 using System.Threading;
@@ -25,6 +27,11 @@ namespace Ryujinx.Graphics.Gpu
             /// </summary>
             public TextureInfo Info { get; }
 
+            /// <summary>
+            /// Physical memory locations where the texture data is located.
+            /// </summary>
+            public MultiRange Range { get; }
+
             /// <summary>
             /// Texture crop region.
             /// </summary>
@@ -49,18 +56,21 @@ namespace Ryujinx.Graphics.Gpu
             /// Creates a new instance of the presentation texture.
             /// </summary>
             /// <param name="info">Information of the texture to be presented</param>
+            /// <param name="range">Physical memory locations where the texture data is located</param>
             /// <param name="crop">Texture crop region</param>
             /// <param name="acquireCallback">Texture acquire callback</param>
             /// <param name="releaseCallback">Texture release callback</param>
             /// <param name="userObj">User defined object passed to the release callback, can be used to identify the texture</param>
             public PresentationTexture(
                 TextureInfo                info,
+                MultiRange                 range,
                 ImageCrop                  crop,
                 Action<GpuContext, object> acquireCallback,
                 Action<object>             releaseCallback,
                 object                     userObj)
             {
                 Info            = info;
+                Range           = range;
                 Crop            = crop;
                 AcquireCallback = acquireCallback;
                 ReleaseCallback = releaseCallback;
@@ -118,7 +128,7 @@ namespace Ryujinx.Graphics.Gpu
             FormatInfo formatInfo = new FormatInfo(format, 1, 1, bytesPerPixel, 4);
 
             TextureInfo info = new TextureInfo(
-                address,
+                0UL,
                 width,
                 height,
                 1,
@@ -133,7 +143,22 @@ namespace Ryujinx.Graphics.Gpu
                 Target.Texture2D,
                 formatInfo);
 
-            _frameQueue.Enqueue(new PresentationTexture(info, crop, acquireCallback, releaseCallback, userObj));
+            int size = SizeCalculator.GetBlockLinearTextureSize(
+                width,
+                height,
+                1,
+                1,
+                1,
+                1,
+                1,
+                bytesPerPixel,
+                gobBlocksInY,
+                1,
+                1).TotalSize;
+
+            MultiRange range = new MultiRange(address, (ulong)size);
+
+            _frameQueue.Enqueue(new PresentationTexture(info, range, crop, acquireCallback, releaseCallback, userObj));
         }
 
         /// <summary>
@@ -149,7 +174,7 @@ namespace Ryujinx.Graphics.Gpu
             {
                 pt.AcquireCallback(_context, pt.UserObj);
 
-                Texture texture = _context.Methods.TextureManager.FindOrCreateTexture(pt.Info, TextureSearchFlags.WithUpscale);
+                Texture texture = _context.Methods.TextureManager.FindOrCreateTexture(TextureSearchFlags.WithUpscale, pt.Info, 0, null, pt.Range);
 
                 texture.SynchronizeMemory();
 

+ 1 - 1
Ryujinx.Graphics.Texture/SizeInfo.cs

@@ -45,7 +45,7 @@ namespace Ryujinx.Graphics.Texture
             return _mipOffsets[level];
         }
 
-        public bool FindView(int offset, int size, out int firstLayer, out int firstLevel)
+        public bool FindView(int offset, out int firstLayer, out int firstLevel)
         {
             int index = Array.BinarySearch(_allOffsets, offset);
 

+ 2 - 3
Ryujinx.Memory/AddressSpaceManager.cs

@@ -1,5 +1,4 @@
-using Ryujinx.Common;
-using System;
+using System;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 
@@ -9,7 +8,7 @@ namespace Ryujinx.Memory
     /// Represents a address space manager.
     /// Supports virtual memory region mapping, address translation and read/write access to mapped regions.
     /// </summary>
-    public sealed class AddressSpaceManager : IVirtualMemoryManager
+    public sealed class AddressSpaceManager : IVirtualMemoryManager, IWritableBlock
     {
         public const int PageBits = 12;
         public const int PageSize = 1 << PageBits;

+ 9 - 0
Ryujinx.Memory/IWritableBlock.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace Ryujinx.Memory
+{
+    public interface IWritableBlock
+    {
+        void Write(ulong va, ReadOnlySpan<byte> data);
+    }
+}

+ 9 - 0
Ryujinx.Memory/Range/IMultiRangeItem.cs

@@ -0,0 +1,9 @@
+namespace Ryujinx.Memory.Range
+{
+    public interface IMultiRangeItem
+    {
+        MultiRange Range { get; }
+
+        ulong BaseAddress => Range.GetSubRange(0).Address;
+    }
+}

+ 71 - 0
Ryujinx.Memory/Range/MemoryRange.cs

@@ -0,0 +1,71 @@
+using System;
+
+namespace Ryujinx.Memory.Range
+{
+    /// <summary>
+    /// Range of memory composed of an address and size.
+    /// </summary>
+    public struct MemoryRange : IEquatable<MemoryRange>
+    {
+        /// <summary>
+        /// An empty memory range, with a null address and zero size.
+        /// </summary>
+        public static MemoryRange Empty => new MemoryRange(0UL, 0);
+
+        /// <summary>
+        /// Start address of the range.
+        /// </summary>
+        public ulong Address { get; }
+
+        /// <summary>
+        /// Size of the range in bytes.
+        /// </summary>
+        public ulong Size { get; }
+
+        /// <summary>
+        /// Address where the range ends (exclusive).
+        /// </summary>
+        public ulong EndAddress => Address + Size;
+
+        /// <summary>
+        /// Creates a new memory range with the specified address and size.
+        /// </summary>
+        /// <param name="address">Start address</param>
+        /// <param name="size">Size in bytes</param>
+        public MemoryRange(ulong address, ulong size)
+        {
+            Address = address;
+            Size = size;
+        }
+
+        /// <summary>
+        /// Checks if the range overlaps with another.
+        /// </summary>
+        /// <param name="other">The other range to check for overlap</param>
+        /// <returns>True if the ranges overlap, false otherwise</returns>
+        public bool OverlapsWith(MemoryRange other)
+        {
+            ulong thisAddress = Address;
+            ulong thisEndAddress = EndAddress;
+            ulong otherAddress = other.Address;
+            ulong otherEndAddress = other.EndAddress;
+
+            return thisAddress < otherEndAddress && otherAddress < thisEndAddress;
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is MemoryRange other && Equals(other);
+        }
+
+        public bool Equals(MemoryRange other)
+        {
+            return Address == other.Address && Size == other.Size;
+        }
+
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(Address, Size);
+        }
+    }
+}

+ 295 - 0
Ryujinx.Memory/Range/MultiRange.cs

@@ -0,0 +1,295 @@
+using System;
+
+namespace Ryujinx.Memory.Range
+{
+    /// <summary>
+    /// Sequence of physical memory regions that a single non-contiguous virtual memory region maps to.
+    /// </summary>
+    public struct MultiRange : IEquatable<MultiRange>
+    {
+        private readonly MemoryRange _singleRange;
+        private readonly MemoryRange[] _ranges;
+
+        private bool HasSingleRange => _ranges == null;
+
+        /// <summary>
+        /// Total of physical sub-ranges on the virtual memory region.
+        /// </summary>
+        public int Count => HasSingleRange ? 1 : _ranges.Length;
+
+        /// <summary>
+        /// Minimum start address of all sub-ranges.
+        /// </summary>
+        public ulong MinAddress { get; }
+
+        /// <summary>
+        /// Maximum end address of all sub-ranges.
+        /// </summary>
+        public ulong MaxAddress { get; }
+
+        /// <summary>
+        /// Creates a new multi-range with a single physical region.
+        /// </summary>
+        /// <param name="address">Start address of the region</param>
+        /// <param name="size">Size of the region in bytes</param>
+        public MultiRange(ulong address, ulong size)
+        {
+            _singleRange = new MemoryRange(address, size);
+            _ranges = null;
+            MinAddress = address;
+            MaxAddress = address + size;
+        }
+
+        /// <summary>
+        /// Creates a new multi-range with multiple physical regions.
+        /// </summary>
+        /// <param name="ranges">Array of physical regions</param>
+        /// <exception cref="ArgumentNullException"><paramref name="ranges"/> is null</exception>
+        public MultiRange(MemoryRange[] ranges)
+        {
+            _singleRange = MemoryRange.Empty;
+            _ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
+
+            if (ranges.Length != 0)
+            {
+                MinAddress = ulong.MaxValue;
+                MaxAddress = 0UL;
+
+                foreach (MemoryRange range in ranges)
+                {
+                    if (MinAddress > range.Address)
+                    {
+                        MinAddress = range.Address;
+                    }
+
+                    if (MaxAddress < range.EndAddress)
+                    {
+                        MaxAddress = range.EndAddress;
+                    }
+                }
+            }
+            else
+            {
+                MinAddress = 0UL;
+                MaxAddress = 0UL;
+            }
+        }
+
+        /// <summary>
+        /// Gets the physical region at the specified index.
+        /// </summary>
+        /// <param name="index">Index of the physical region</param>
+        /// <returns>Region at the index specified</returns>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is invalid</exception>
+        public MemoryRange GetSubRange(int index)
+        {
+            if (HasSingleRange)
+            {
+                if (index != 0)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(index));
+                }
+
+                return _singleRange;
+            }
+            else
+            {
+                if ((uint)index >= _ranges.Length)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(index));
+                }
+
+                return _ranges[index];
+            }
+        }
+
+        /// <summary>
+        /// Gets the physical region at the specified index, without explicit bounds checking.
+        /// </summary>
+        /// <param name="index">Index of the physical region</param>
+        /// <returns>Region at the index specified</returns>
+        private MemoryRange GetSubRangeUnchecked(int index)
+        {
+            return HasSingleRange ? _singleRange : _ranges[index];
+        }
+
+        /// <summary>
+        /// Check if two multi-ranges overlap with each other.
+        /// </summary>
+        /// <param name="other">Other multi-range to check for overlap</param>
+        /// <returns>True if any sub-range overlaps, false otherwise</returns>
+        public bool OverlapsWith(MultiRange other)
+        {
+            if (HasSingleRange && other.HasSingleRange)
+            {
+                return _singleRange.OverlapsWith(other._singleRange);
+            }
+            else
+            {
+                for (int i = 0; i < Count; i++)
+                {
+                    MemoryRange currentRange = GetSubRangeUnchecked(i);
+
+                    for (int j = 0; j < other.Count; j++)
+                    {
+                        if (currentRange.OverlapsWith(other.GetSubRangeUnchecked(j)))
+                        {
+                            return true;
+                        }
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Checks if a given multi-range is fully contained inside another.
+        /// </summary>
+        /// <param name="other">Multi-range to be checked</param>
+        /// <returns>True if all the sub-ranges on <paramref name="other"/> are contained inside the multi-range, with the same order, false otherwise</returns>
+        public bool Contains(MultiRange other)
+        {
+            return FindOffset(other) >= 0;
+        }
+
+        /// <summary>
+        /// Calculates the offset of a given multi-range inside another, when the multi-range is fully contained
+        /// inside the other multi-range, otherwise returns -1.
+        /// </summary>
+        /// <param name="other">Multi-range that should be fully contained inside this one</param>
+        /// <returns>Offset in bytes if fully contained, otherwise -1</returns>
+        public int FindOffset(MultiRange other)
+        {
+            int thisCount = Count;
+            int otherCount = other.Count;
+
+            if (thisCount == 1 && otherCount == 1)
+            {
+                MemoryRange otherFirstRange = other.GetSubRangeUnchecked(0);
+                MemoryRange currentFirstRange = GetSubRangeUnchecked(0);
+
+                if (otherFirstRange.Address >= currentFirstRange.Address &&
+                    otherFirstRange.EndAddress <= currentFirstRange.EndAddress)
+                {
+                    return (int)(otherFirstRange.Address - currentFirstRange.Address);
+                }
+            }
+            else if (thisCount >= otherCount)
+            {
+                ulong baseOffset = 0;
+
+                MemoryRange otherFirstRange = other.GetSubRangeUnchecked(0);
+                MemoryRange otherLastRange = other.GetSubRangeUnchecked(otherCount - 1);
+
+                for (int i = 0; i < (thisCount - otherCount) + 1; baseOffset += GetSubRangeUnchecked(i).Size, i++)
+                {
+                    MemoryRange currentFirstRange = GetSubRangeUnchecked(i);
+                    MemoryRange currentLastRange = GetSubRangeUnchecked(i + otherCount - 1);
+
+                    if (otherCount > 1)
+                    {
+                        if (otherFirstRange.Address < currentFirstRange.Address ||
+                            otherFirstRange.EndAddress != currentFirstRange.EndAddress)
+                        {
+                            continue;
+                        }
+
+                        if (otherLastRange.Address != currentLastRange.Address ||
+                            otherLastRange.EndAddress > currentLastRange.EndAddress)
+                        {
+                            continue;
+                        }
+
+                        bool fullMatch = true;
+
+                        for (int j = 1; j < otherCount - 1; j++)
+                        {
+                            if (!GetSubRangeUnchecked(i + j).Equals(other.GetSubRangeUnchecked(j)))
+                            {
+                                fullMatch = false;
+                                break;
+                            }
+                        }
+
+                        if (!fullMatch)
+                        {
+                            continue;
+                        }
+                    }
+                    else if (currentFirstRange.Address > otherFirstRange.Address ||
+                             currentFirstRange.EndAddress < otherFirstRange.EndAddress)
+                    {
+                        continue;
+                    }
+
+                    return (int)(baseOffset + (otherFirstRange.Address - currentFirstRange.Address));
+                }
+            }
+
+            return -1;
+        }
+
+        /// <summary>
+        /// Gets the total size of all sub-ranges in bytes.
+        /// </summary>
+        /// <returns>Total size in bytes</returns>
+        public ulong GetSize()
+        {
+            ulong sum = 0;
+
+            foreach (MemoryRange range in _ranges)
+            {
+                sum += range.Size;
+            }
+
+            return sum;
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is MultiRange other && Equals(other);
+        }
+
+        public bool Equals(MultiRange other)
+        {
+            if (HasSingleRange && other.HasSingleRange)
+            {
+                return _singleRange.Equals(other._singleRange);
+            }
+
+            int thisCount = Count;
+            if (thisCount != other.Count)
+            {
+                return false;
+            }
+
+            for (int i = 0; i < thisCount; i++)
+            {
+                if (!GetSubRangeUnchecked(i).Equals(other.GetSubRangeUnchecked(i)))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        public override int GetHashCode()
+        {
+            if (HasSingleRange)
+            {
+                return _singleRange.GetHashCode();
+            }
+
+            HashCode hash = new HashCode();
+
+            foreach (MemoryRange range in _ranges)
+            {
+                hash.Add(range);
+            }
+
+            return hash.ToHashCode();
+        }
+    }
+}

+ 204 - 0
Ryujinx.Memory/Range/MultiRangeList.cs

@@ -0,0 +1,204 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Ryujinx.Memory.Range
+{
+    /// <summary>
+    /// Sorted list of ranges that supports binary search.
+    /// </summary>
+    /// <typeparam name="T">Type of the range.</typeparam>
+    public class MultiRangeList<T> : IEnumerable<T> where T : IMultiRangeItem
+    {
+        private const int ArrayGrowthSize = 32;
+
+        private readonly List<T> _items;
+
+        public int Count => _items.Count;
+
+        /// <summary>
+        /// Creates a new range list.
+        /// </summary>
+        public MultiRangeList()
+        {
+            _items = new List<T>();
+        }
+
+        /// <summary>
+        /// Adds a new item to the list.
+        /// </summary>
+        /// <param name="item">The item to be added</param>
+        public void Add(T item)
+        {
+            int index = BinarySearch(item.BaseAddress);
+
+            if (index < 0)
+            {
+                index = ~index;
+            }
+
+            _items.Insert(index, item);
+        }
+
+        /// <summary>
+        /// Removes an item from the list.
+        /// </summary>
+        /// <param name="item">The item to be removed</param>
+        /// <returns>True if the item was removed, or false if it was not found</returns>
+        public bool Remove(T item)
+        {
+            int index = BinarySearch(item.BaseAddress);
+
+            if (index >= 0)
+            {
+                while (index > 0 && _items[index - 1].BaseAddress == item.BaseAddress)
+                {
+                    index--;
+                }
+
+                while (index < _items.Count)
+                {
+                    if (_items[index].Equals(item))
+                    {
+                        _items.RemoveAt(index);
+
+                        return true;
+                    }
+
+                    if (_items[index].BaseAddress > item.BaseAddress)
+                    {
+                        break;
+                    }
+
+                    index++;
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Gets all items on the list overlapping the specified memory range.
+        /// </summary>
+        /// <param name="address">Start address of the range</param>
+        /// <param name="size">Size in bytes of the range</param>
+        /// <param name="output">Output array where matches will be written. It is automatically resized to fit the results</param>
+        /// <returns>The number of overlapping items found</returns>
+        public int FindOverlaps(ulong address, ulong size, ref T[] output)
+        {
+            return FindOverlaps(new MultiRange(address, size), ref output);
+        }
+
+        /// <summary>
+        /// Gets all items on the list overlapping the specified memory ranges.
+        /// </summary>
+        /// <param name="range">Ranges of memory being searched</param>
+        /// <param name="output">Output array where matches will be written. It is automatically resized to fit the results</param>
+        /// <returns>The number of overlapping items found</returns>
+        public int FindOverlaps(MultiRange range, ref T[] output)
+        {
+            int outputIndex = 0;
+
+            foreach (T item in _items)
+            {
+                if (item.Range.OverlapsWith(range))
+                {
+                    if (outputIndex == output.Length)
+                    {
+                        Array.Resize(ref output, outputIndex + ArrayGrowthSize);
+                    }
+
+                    output[outputIndex++] = item;
+                }
+            }
+
+            return outputIndex;
+        }
+
+        /// <summary>
+        /// Gets all items on the list starting at the specified memory address.
+        /// </summary>
+        /// <param name="baseAddress">Base address to find</param>
+        /// <param name="output">Output array where matches will be written. It is automatically resized to fit the results</param>
+        /// <returns>The number of matches found</returns>
+        public int FindOverlaps(ulong baseAddress, ref T[] output)
+        {
+            int index = BinarySearch(baseAddress);
+
+            int outputIndex = 0;
+
+            if (index >= 0)
+            {
+                while (index > 0 && _items[index - 1].BaseAddress == baseAddress)
+                {
+                    index--;
+                }
+
+                while (index < _items.Count)
+                {
+                    T overlap = _items[index++];
+
+                    if (overlap.BaseAddress != baseAddress)
+                    {
+                        break;
+                    }
+
+                    if (outputIndex == output.Length)
+                    {
+                        Array.Resize(ref output, outputIndex + ArrayGrowthSize);
+                    }
+
+                    output[outputIndex++] = overlap;
+                }
+            }
+
+            return outputIndex;
+        }
+
+        /// <summary>
+        /// Performs binary search on the internal list of items.
+        /// </summary>
+        /// <param name="address">Address to find</param>
+        /// <returns>List index of the item, or complement index of nearest item with lower value on the list</returns>
+        private int BinarySearch(ulong address)
+        {
+            int left = 0;
+            int right = _items.Count - 1;
+
+            while (left <= right)
+            {
+                int range = right - left;
+
+                int middle = left + (range >> 1);
+
+                T item = _items[middle];
+
+                if (item.BaseAddress == address)
+                {
+                    return middle;
+                }
+
+                if (address < item.BaseAddress)
+                {
+                    right = middle - 1;
+                }
+                else
+                {
+                    left = middle + 1;
+                }
+            }
+
+            return ~left;
+        }
+
+        public IEnumerator<T> GetEnumerator()
+        {
+            return _items.GetEnumerator();
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return _items.GetEnumerator();
+        }
+    }
+}

+ 5 - 5
Ryujinx.Memory/WritableRegion.cs

@@ -4,16 +4,16 @@ namespace Ryujinx.Memory
 {
     public sealed class WritableRegion : IDisposable
     {
-        private readonly IVirtualMemoryManager _mm;
+        private readonly IWritableBlock _block;
         private readonly ulong _va;
 
-        private bool NeedsWriteback => _mm != null;
+        private bool NeedsWriteback => _block != null;
 
         public Memory<byte> Memory { get; }
 
-        public WritableRegion(IVirtualMemoryManager mm, ulong va, Memory<byte> memory)
+        public WritableRegion(IWritableBlock block, ulong va, Memory<byte> memory)
         {
-            _mm = mm;
+            _block = block;
             _va = va;
             Memory = memory;
         }
@@ -22,7 +22,7 @@ namespace Ryujinx.Memory
         {
             if (NeedsWriteback)
             {
-                _mm.Write(_va, Memory.Span);
+                _block.Write(_va, Memory.Span);
             }
         }
     }