From 0a24aa6af26cc55c079e265a071a42569d28d2c0 Mon Sep 17 00:00:00 2001
From: gdkchan <gab.dark.100@gmail.com>
Date: Tue, 22 Feb 2022 13:34:16 -0300
Subject: [PATCH] Allow textures to have their data partially mapped (#2629)

* Allow textures to have their data partially mapped

* Explicitly check for invalid memory ranges on the MultiRangeList

* Update GetWritableRegion to also support unmapped ranges
---
 Ryujinx.Graphics.Gpu/Image/TextureCache.cs    | 57 +++++++++++----
 Ryujinx.Graphics.Gpu/Image/TextureGroup.cs    | 21 ++++--
 Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs  | 53 ++++++++------
 Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs | 71 ++++++++++++-------
 Ryujinx.Memory/Range/MemoryRange.cs           |  7 ++
 Ryujinx.Memory/Range/MultiRangeList.cs        | 29 ++++++++
 6 files changed, 170 insertions(+), 68 deletions(-)

diff --git a/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
index 203a3a125a..89ad8aa075 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
@@ -40,6 +40,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         private readonly PhysicalMemory _physicalMemory;
 
         private readonly MultiRangeList<Texture> _textures;
+        private readonly HashSet<Texture> _partiallyMappedTextures;
 
         private Texture[] _textureOverlaps;
         private OverlapInfo[] _overlapInfo;
@@ -57,6 +58,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             _physicalMemory = physicalMemory;
 
             _textures = new MultiRangeList<Texture>();
+            _partiallyMappedTextures = new HashSet<Texture>();
 
             _textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
             _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
@@ -74,17 +76,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             Texture[] overlaps = new Texture[10];
             int overlapCount;
 
-            MultiRange unmapped;
-
-            try
-            {
-                unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
-            }
-            catch (InvalidMemoryRegionException)
-            {
-                // This event fires on Map in case any mappings are overwritten. In that case, there may not be an existing mapping.
-                return;
-            }
+            MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
 
             lock (_textures)
             {
@@ -95,6 +87,24 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 overlaps[i].Unmapped(unmapped);
             }
+
+            // If any range was previously unmapped, we also need to purge
+            // all partially mapped texture, as they might be fully mapped now.
+            for (int i = 0; i < unmapped.Count; i++)
+            {
+                if (unmapped.GetSubRange(i).Address == MemoryManager.PteUnmapped)
+                {
+                    lock (_partiallyMappedTextures)
+                    {
+                        foreach (var texture in _partiallyMappedTextures)
+                        {
+                            texture.Unmapped(unmapped);
+                        }
+                    }
+
+                    break;
+                }
+            }
         }
 
         /// <summary>
@@ -495,10 +505,20 @@ namespace Ryujinx.Graphics.Gpu.Image
             SizeInfo sizeInfo = info.CalculateSizeInfo(layerSize);
 
             ulong size = (ulong)sizeInfo.TotalSize;
+            bool partiallyMapped = false;
 
             if (range == null)
             {
                 range = memoryManager.GetPhysicalRegions(info.GpuAddress, size);
+
+                for (int i = 0; i < range.Value.Count; i++)
+                {
+                    if (range.Value.GetSubRange(i).Address == MemoryManager.PteUnmapped)
+                    {
+                        partiallyMapped = true;
+                        break;
+                    }
+                }
             }
 
             // Find view compatible matches.
@@ -668,7 +688,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                     else
                     {
                         bool dataOverlaps = texture.DataOverlaps(overlap, compatibility);
-                        
+
                         if (!overlap.IsView && dataOverlaps && !incompatibleOverlaps.Exists(incompatible => incompatible.Group == overlap.Group))
                         {
                             incompatibleOverlaps.Add(new TextureIncompatibleOverlap(overlap.Group, compatibility));
@@ -784,6 +804,14 @@ namespace Ryujinx.Graphics.Gpu.Image
                 _textures.Add(texture);
             }
 
+            if (partiallyMapped)
+            {
+                lock (_partiallyMappedTextures)
+                {
+                    _partiallyMappedTextures.Add(texture);
+                }
+            }
+
             ShrinkOverlapsBufferIfNeeded();
 
             for (int i = 0; i < overlapsCount; i++)
@@ -1079,6 +1107,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 _textures.Remove(texture);
             }
+
+            lock (_partiallyMappedTextures)
+            {
+                _partiallyMappedTextures.Remove(texture);
+            }
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
index bfc1105ca8..be0abea213 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -236,7 +236,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         }
 
         /// <summary>
-        /// Synchronize memory for a given texture. 
+        /// 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>
@@ -280,7 +280,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                     // 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 we need to be synchronized and there is a copy present, prefer the copy.
 
                     if (group.NeedsCopy && group.Copy(_context))
                     {
@@ -618,7 +618,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         }
 
         /// <summary>
-        /// Evaluate the range of tracking handles which a view texture overlaps with, 
+        /// 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>
@@ -879,7 +879,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                 int sliceStart = Math.Clamp(offset, 0, subRangeSize);
                 int sliceEnd = Math.Clamp(endOffset, 0, subRangeSize);
 
-                if (sliceStart != sliceEnd)
+                if (sliceStart != sliceEnd && item.Address != MemoryManager.PteUnmapped)
                 {
                     result.Add(GenerateHandle(item.Address + (ulong)sliceStart, (ulong)(sliceEnd - sliceStart)));
                 }
@@ -1097,11 +1097,20 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 // Single dirty region.
                 var cpuRegionHandles = new CpuRegionHandle[TextureRange.Count];
+                int count = 0;
 
                 for (int i = 0; i < TextureRange.Count; i++)
                 {
                     var currentRange = TextureRange.GetSubRange(i);
-                    cpuRegionHandles[i] = GenerateHandle(currentRange.Address, currentRange.Size);
+                    if (currentRange.Address != MemoryManager.PteUnmapped)
+                    {
+                        cpuRegionHandles[count++] = GenerateHandle(currentRange.Address, currentRange.Size);
+                    }
+                }
+
+                if (count != TextureRange.Count)
+                {
+                    Array.Resize(ref cpuRegionHandles, count);
                 }
 
                 var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, 0, _allOffsets.Length, cpuRegionHandles);
@@ -1277,7 +1286,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                         TextureInfo info = Storage.Info;
                         TextureInfo otherInfo = other.Storage.Info;
 
-                        if (TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) && 
+                        if (TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) &&
                             TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel))
                         {
                             // These textures are copy compatible. Create the dependency.
diff --git a/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs b/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs
index b6395e73f6..977fbdf988 100644
--- a/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs
@@ -28,7 +28,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
         private const int PtLvl1Bit = PtPageBits;
         private const int AddressSpaceBits = PtPageBits + PtLvl1Bits + PtLvl0Bits;
 
-        public const ulong PteUnmapped = 0xffffffff_ffffffff;
+        public const ulong PteUnmapped = ulong.MaxValue;
 
         private readonly ulong[][] _pageTable;
 
@@ -340,7 +340,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
         /// <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))
@@ -348,11 +347,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
                 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));
 
@@ -367,14 +361,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
 
             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 currPa = Translate(va);
                 ulong newPa = Translate(va + PageSize);
 
-                if (Translate(va) + PageSize != newPa)
+                if ((currPa != PteUnmapped || newPa != PteUnmapped) && currPa + PageSize != newPa)
                 {
                     regions.Add(new MemoryRange(regionStart, regionSize));
                     regionStart = newPa;
@@ -405,18 +395,35 @@ namespace Ryujinx.Graphics.Gpu.Memory
             {
                 MemoryRange currentRange = range.GetSubRange(i);
 
-                ulong address = currentRange.Address & ~PageMask;
-                ulong endAddress = (currentRange.EndAddress + PageMask) & ~PageMask;
-
-                while (address < endAddress)
+                if (currentRange.Address != PteUnmapped)
                 {
-                    if (Translate(va) != address)
-                    {
-                        return false;
-                    }
+                    ulong address = currentRange.Address & ~PageMask;
+                    ulong endAddress = (currentRange.EndAddress + PageMask) & ~PageMask;
 
-                    va += PageSize;
-                    address += PageSize;
+                    while (address < endAddress)
+                    {
+                        if (Translate(va) != address)
+                        {
+                            return false;
+                        }
+
+                        va += PageSize;
+                        address += PageSize;
+                    }
+                }
+                else
+                {
+                    ulong endVa = va + (((currentRange.Size) + PageMask) & ~PageMask);
+
+                    while (va < endVa)
+                    {
+                        if (Translate(va) != PteUnmapped)
+                        {
+                            return false;
+                        }
+
+                        va += PageSize;
+                    }
                 }
             }
 
diff --git a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
index 1ff086eea8..bfd3c04fa8 100644
--- a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs
@@ -7,8 +7,6 @@ using Ryujinx.Memory.Range;
 using Ryujinx.Memory.Tracking;
 using System;
 using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
 using System.Threading;
 
 namespace Ryujinx.Graphics.Gpu.Memory
@@ -19,8 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
     /// </summary>
     class PhysicalMemory : IDisposable
     {
-        public const int PageSize = 0x1000;
-
         private readonly GpuContext _context;
         private IVirtualMemoryManagerTracked _cpuMemory;
         private int _referenceCount;
@@ -103,24 +99,28 @@ namespace Ryujinx.Graphics.Gpu.Memory
             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++)
+                if (singleRange.Address != MemoryManager.PteUnmapped)
                 {
-                    var currentRange = range.GetSubRange(i);
-                    int size = (int)currentRange.Size;
-                    _cpuMemory.GetSpan(currentRange.Address, size, tracked).CopyTo(data.Slice(offset, size));
-                    offset += size;
+                    return _cpuMemory.GetSpan(singleRange.Address, (int)singleRange.Size, tracked);
                 }
-
-                return data;
             }
+
+            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;
+                if (currentRange.Address != MemoryManager.PteUnmapped)
+                {
+                    _cpuMemory.GetSpan(currentRange.Address, size, tracked).CopyTo(data.Slice(offset, size));
+                }
+                offset += size;
+            }
+
+            return data;
         }
 
         /// <summary>
@@ -156,11 +156,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
                 int offset = 0;
                 for (int i = 0; i < range.Count; i++)
                 {
-                    MemoryRange subrange = range.GetSubRange(i);
-
-                    GetSpan(subrange.Address, (int)subrange.Size).CopyTo(memory.Span.Slice(offset, (int)subrange.Size));
-
-                    offset += (int)subrange.Size;
+                    var currentRange = range.GetSubRange(i);
+                    int size = (int)currentRange.Size;
+                    if (currentRange.Address != MemoryManager.PteUnmapped)
+                    {
+                        GetSpan(currentRange.Address, size).CopyTo(memory.Span.Slice(offset, size));
+                    }
+                    offset += size;
                 }
 
                 return new WritableRegion(new MultiRangeWritableBlock(range, this), 0, memory, tracked);
@@ -253,7 +255,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
             if (range.Count == 1)
             {
                 var singleRange = range.GetSubRange(0);
-                writeCallback(singleRange.Address, data);
+                if (singleRange.Address != MemoryManager.PteUnmapped)
+                {
+                    writeCallback(singleRange.Address, data);
+                }
             }
             else
             {
@@ -263,7 +268,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
                 {
                     var currentRange = range.GetSubRange(i);
                     int size = (int)currentRange.Size;
-                    writeCallback(currentRange.Address, data.Slice(offset, size));
+                    if (currentRange.Address != MemoryManager.PteUnmapped)
+                    {
+                        writeCallback(currentRange.Address, data.Slice(offset, size));
+                    }
                     offset += size;
                 }
             }
@@ -288,11 +296,20 @@ namespace Ryujinx.Graphics.Gpu.Memory
         public GpuRegionHandle BeginTracking(MultiRange range)
         {
             var cpuRegionHandles = new CpuRegionHandle[range.Count];
+            int count = 0;
 
             for (int i = 0; i < range.Count; i++)
             {
                 var currentRange = range.GetSubRange(i);
-                cpuRegionHandles[i] = _cpuMemory.BeginTracking(currentRange.Address, currentRange.Size);
+                if (currentRange.Address != MemoryManager.PteUnmapped)
+                {
+                    cpuRegionHandles[count++] = _cpuMemory.BeginTracking(currentRange.Address, currentRange.Size);
+                }
+            }
+
+            if (count != range.Count)
+            {
+                Array.Resize(ref cpuRegionHandles, count);
             }
 
             return new GpuRegionHandle(cpuRegionHandles);
diff --git a/Ryujinx.Memory/Range/MemoryRange.cs b/Ryujinx.Memory/Range/MemoryRange.cs
index ba12bae5e3..33d2439f60 100644
--- a/Ryujinx.Memory/Range/MemoryRange.cs
+++ b/Ryujinx.Memory/Range/MemoryRange.cs
@@ -50,6 +50,13 @@ namespace Ryujinx.Memory.Range
             ulong otherAddress = other.Address;
             ulong otherEndAddress = other.EndAddress;
 
+            // If any of the ranges if invalid (address + size overflows),
+            // then they are never considered to overlap.
+            if (thisEndAddress < thisAddress || otherEndAddress < otherAddress)
+            {
+                return false;
+            }
+
             return thisAddress < otherEndAddress && otherAddress < thisEndAddress;
         }
 
diff --git a/Ryujinx.Memory/Range/MultiRangeList.cs b/Ryujinx.Memory/Range/MultiRangeList.cs
index 38ca63b483..5131889fcd 100644
--- a/Ryujinx.Memory/Range/MultiRangeList.cs
+++ b/Ryujinx.Memory/Range/MultiRangeList.cs
@@ -29,6 +29,12 @@ namespace Ryujinx.Memory.Range
             for (int i = 0; i < range.Count; i++)
             {
                 var subrange = range.GetSubRange(i);
+
+                if (IsInvalid(ref subrange))
+                {
+                    continue;
+                }
+
                 _items.Add(subrange.Address, subrange.EndAddress, item);
             }
 
@@ -49,6 +55,12 @@ namespace Ryujinx.Memory.Range
             for (int i = 0; i < range.Count; i++)
             {
                 var subrange = range.GetSubRange(i);
+
+                if (IsInvalid(ref subrange))
+                {
+                    continue;
+                }
+
                 removed += _items.Remove(subrange.Address, item);
             }
 
@@ -86,6 +98,12 @@ namespace Ryujinx.Memory.Range
             for (int i = 0; i < range.Count; i++)
             {
                 var subrange = range.GetSubRange(i);
+
+                if (IsInvalid(ref subrange))
+                {
+                    continue;
+                }
+
                 overlapCount = _items.Get(subrange.Address, subrange.EndAddress, ref output, overlapCount);
             }
 
@@ -124,6 +142,17 @@ namespace Ryujinx.Memory.Range
             return overlapCount;
         }
 
+        /// <summary>
+        /// Checks if a given sub-range of memory is invalid.
+        /// Those are used to represent unmapped memory regions (holes in the region mapping).
+        /// </summary>
+        /// <param name="subRange">Memory range to checl</param>
+        /// <returns>True if the memory range is considered invalid, false otherwise</returns>
+        private static bool IsInvalid(ref MemoryRange subRange)
+        {
+            return subRange.Address == ulong.MaxValue;
+        }
+
         /// <summary>
         /// Gets all items on the list starting at the specified memory address.
         /// </summary>