diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs')
| -rw-r--r-- | src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs | 736 |
1 files changed, 736 insertions, 0 deletions
diff --git a/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs new file mode 100644 index 00000000..3022b661 --- /dev/null +++ b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs @@ -0,0 +1,736 @@ +using Ryujinx.Common.Collections; +using Ryujinx.Common.Memory.PartialUnmaps; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Memory.WindowsShared +{ + /// <summary> + /// Windows memory placeholder manager. + /// </summary> + [SupportedOSPlatform("windows")] + class PlaceholderManager + { + private const int InitialOverlapsSize = 10; + + private readonly MappingTree<ulong> _mappings; + private readonly MappingTree<MemoryPermission> _protections; + private readonly IntPtr _partialUnmapStatePtr; + private readonly Thread _partialUnmapTrimThread; + + /// <summary> + /// Creates a new instance of the Windows memory placeholder manager. + /// </summary> + public PlaceholderManager() + { + _mappings = new MappingTree<ulong>(); + _protections = new MappingTree<MemoryPermission>(); + + _partialUnmapStatePtr = PartialUnmapState.GlobalState; + + _partialUnmapTrimThread = new Thread(TrimThreadLocalMapLoop); + _partialUnmapTrimThread.Name = "CPU.PartialUnmapTrimThread"; + _partialUnmapTrimThread.IsBackground = true; + _partialUnmapTrimThread.Start(); + } + + /// <summary> + /// Gets a reference to the partial unmap state struct. + /// </summary> + /// <returns>A reference to the partial unmap state struct</returns> + private unsafe ref PartialUnmapState GetPartialUnmapState() + { + return ref Unsafe.AsRef<PartialUnmapState>((void*)_partialUnmapStatePtr); + } + + /// <summary> + /// Trims inactive threads from the partial unmap state's thread mapping every few seconds. + /// Should be run in a Background thread so that it doesn't stop the program from closing. + /// </summary> + private void TrimThreadLocalMapLoop() + { + while (true) + { + Thread.Sleep(2000); + GetPartialUnmapState().TrimThreads(); + } + } + + /// <summary> + /// Reserves a range of the address space to be later mapped as shared memory views. + /// </summary> + /// <param name="address">Start address of the region to reserve</param> + /// <param name="size">Size in bytes of the region to reserve</param> + public void ReserveRange(ulong address, ulong size) + { + lock (_mappings) + { + _mappings.Add(new RangeNode<ulong>(address, address + size, ulong.MaxValue)); + } + + lock (_protections) + { + _protections.Add(new RangeNode<MemoryPermission>(address, address + size, MemoryPermission.None)); + } + } + + /// <summary> + /// Unreserves a range of memory that has been previously reserved with <see cref="ReserveRange"/>. + /// </summary> + /// <param name="address">Start address of the region to unreserve</param> + /// <param name="size">Size in bytes of the region to unreserve</param> + /// <exception cref="WindowsApiException">Thrown when the Windows API returns an error unreserving the memory</exception> + public void UnreserveRange(ulong address, ulong size) + { + ulong endAddress = address + size; + + lock (_mappings) + { + RangeNode<ulong> node = _mappings.GetNodeByKey(address); + RangeNode<ulong> successorNode; + + for (; node != null; node = successorNode) + { + successorNode = node.Successor; + + if (IsMapped(node.Value)) + { + if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)node.Start, 2)) + { + throw new WindowsApiException("UnmapViewOfFile2"); + } + } + + _mappings.Remove(node); + + if (node.End >= endAddress) + { + break; + } + } + } + + RemoveProtection(address, size); + } + + /// <summary> + /// Maps a shared memory view on a previously reserved memory region. + /// </summary> + /// <param name="sharedMemory">Shared memory that will be the backing storage for the view</param> + /// <param name="srcOffset">Offset in the shared memory to map</param> + /// <param name="location">Address to map the view into</param> + /// <param name="size">Size of the view in bytes</param> + /// <param name="owner">Memory block that owns the mapping</param> + public void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, MemoryBlock owner) + { + ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock; + partialUnmapLock.AcquireReaderLock(); + + try + { + UnmapViewInternal(sharedMemory, location, size, owner, updateProtection: false); + MapViewInternal(sharedMemory, srcOffset, location, size, updateProtection: true); + } + finally + { + partialUnmapLock.ReleaseReaderLock(); + } + } + + /// <summary> + /// Maps a shared memory view on a previously reserved memory region. + /// </summary> + /// <param name="sharedMemory">Shared memory that will be the backing storage for the view</param> + /// <param name="srcOffset">Offset in the shared memory to map</param> + /// <param name="location">Address to map the view into</param> + /// <param name="size">Size of the view in bytes</param> + /// <param name="updateProtection">Indicates if the memory protections should be updated after the map</param> + /// <exception cref="WindowsApiException">Thrown when the Windows API returns an error mapping the memory</exception> + private void MapViewInternal(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, bool updateProtection) + { + SplitForMap((ulong)location, (ulong)size, srcOffset); + + var ptr = WindowsApi.MapViewOfFile3( + sharedMemory, + WindowsApi.CurrentProcessHandle, + location, + srcOffset, + size, + 0x4000, + MemoryProtection.ReadWrite, + IntPtr.Zero, + 0); + + if (ptr == IntPtr.Zero) + { + throw new WindowsApiException("MapViewOfFile3"); + } + + if (updateProtection) + { + UpdateProtection((ulong)location, (ulong)size, MemoryPermission.ReadAndWrite); + } + } + + /// <summary> + /// Splits a larger placeholder, slicing at the start and end address, for a new memory mapping. + /// </summary> + /// <param name="address">Address to split</param> + /// <param name="size">Size of the new region</param> + /// <param name="backingOffset">Offset in the shared memory that will be mapped</param> + private void SplitForMap(ulong address, ulong size, ulong backingOffset) + { + ulong endAddress = address + size; + + var overlaps = new RangeNode<ulong>[InitialOverlapsSize]; + + lock (_mappings) + { + int count = _mappings.GetNodes(address, endAddress, ref overlaps); + + Debug.Assert(count == 1); + Debug.Assert(!IsMapped(overlaps[0].Value)); + + var overlap = overlaps[0]; + + ulong overlapStart = overlap.Start; + ulong overlapEnd = overlap.End; + ulong overlapValue = overlap.Value; + + _mappings.Remove(overlap); + + bool overlapStartsBefore = overlapStart < address; + bool overlapEndsAfter = overlapEnd > endAddress; + + if (overlapStartsBefore && overlapEndsAfter) + { + CheckFreeResult(WindowsApi.VirtualFree( + (IntPtr)address, + (IntPtr)size, + AllocationType.Release | AllocationType.PreservePlaceholder)); + + _mappings.Add(new RangeNode<ulong>(overlapStart, address, overlapValue)); + _mappings.Add(new RangeNode<ulong>(endAddress, overlapEnd, AddBackingOffset(overlapValue, endAddress - overlapStart))); + } + else if (overlapStartsBefore) + { + ulong overlappedSize = overlapEnd - address; + + CheckFreeResult(WindowsApi.VirtualFree( + (IntPtr)address, + (IntPtr)overlappedSize, + AllocationType.Release | AllocationType.PreservePlaceholder)); + + _mappings.Add(new RangeNode<ulong>(overlapStart, address, overlapValue)); + } + else if (overlapEndsAfter) + { + ulong overlappedSize = endAddress - overlapStart; + + CheckFreeResult(WindowsApi.VirtualFree( + (IntPtr)overlapStart, + (IntPtr)overlappedSize, + AllocationType.Release | AllocationType.PreservePlaceholder)); + + _mappings.Add(new RangeNode<ulong>(endAddress, overlapEnd, AddBackingOffset(overlapValue, overlappedSize))); + } + + _mappings.Add(new RangeNode<ulong>(address, endAddress, backingOffset)); + } + } + + /// <summary> + /// Unmaps a view that has been previously mapped with <see cref="MapView"/>. + /// </summary> + /// <remarks> + /// For "partial unmaps" (when not the entire mapped range is being unmapped), it might be + /// necessary to unmap the whole range and then remap the sub-ranges that should remain mapped. + /// </remarks> + /// <param name="sharedMemory">Shared memory that the view being unmapped belongs to</param> + /// <param name="location">Address to unmap</param> + /// <param name="size">Size of the region to unmap in bytes</param> + /// <param name="owner">Memory block that owns the mapping</param> + public void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner) + { + ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock; + partialUnmapLock.AcquireReaderLock(); + + try + { + UnmapViewInternal(sharedMemory, location, size, owner, updateProtection: true); + } + finally + { + partialUnmapLock.ReleaseReaderLock(); + } + } + + /// <summary> + /// Unmaps a view that has been previously mapped with <see cref="MapView"/>. + /// </summary> + /// <remarks> + /// For "partial unmaps" (when not the entire mapped range is being unmapped), it might be + /// necessary to unmap the whole range and then remap the sub-ranges that should remain mapped. + /// </remarks> + /// <param name="sharedMemory">Shared memory that the view being unmapped belongs to</param> + /// <param name="location">Address to unmap</param> + /// <param name="size">Size of the region to unmap in bytes</param> + /// <param name="owner">Memory block that owns the mapping</param> + /// <param name="updateProtection">Indicates if the memory protections should be updated after the unmap</param> + /// <exception cref="WindowsApiException">Thrown when the Windows API returns an error unmapping or remapping the memory</exception> + private void UnmapViewInternal(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner, bool updateProtection) + { + ulong startAddress = (ulong)location; + ulong unmapSize = (ulong)size; + ulong endAddress = startAddress + unmapSize; + + var overlaps = new RangeNode<ulong>[InitialOverlapsSize]; + int count; + + lock (_mappings) + { + count = _mappings.GetNodes(startAddress, endAddress, ref overlaps); + } + + for (int index = 0; index < count; index++) + { + var overlap = overlaps[index]; + + if (IsMapped(overlap.Value)) + { + lock (_mappings) + { + _mappings.Remove(overlap); + _mappings.Add(new RangeNode<ulong>(overlap.Start, overlap.End, ulong.MaxValue)); + } + + bool overlapStartsBefore = overlap.Start < startAddress; + bool overlapEndsAfter = overlap.End > endAddress; + + if (overlapStartsBefore || overlapEndsAfter) + { + // If the overlap extends beyond the region we are unmapping, + // then we need to re-map the regions that are supposed to remain mapped. + // This is necessary because Windows does not support partial view unmaps. + // That is, you can only fully unmap a view that was previously mapped, you can't just unmap a chunck of it. + + ref var partialUnmapState = ref GetPartialUnmapState(); + ref var partialUnmapLock = ref partialUnmapState.PartialUnmapLock; + partialUnmapLock.UpgradeToWriterLock(); + + try + { + partialUnmapState.PartialUnmapsCount++; + + if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)overlap.Start, 2)) + { + throw new WindowsApiException("UnmapViewOfFile2"); + } + + if (overlapStartsBefore) + { + ulong remapSize = startAddress - overlap.Start; + + MapViewInternal(sharedMemory, overlap.Value, (IntPtr)overlap.Start, (IntPtr)remapSize, updateProtection: false); + RestoreRangeProtection(overlap.Start, remapSize); + } + + if (overlapEndsAfter) + { + ulong overlappedSize = endAddress - overlap.Start; + ulong remapBackingOffset = overlap.Value + overlappedSize; + ulong remapAddress = overlap.Start + overlappedSize; + ulong remapSize = overlap.End - endAddress; + + MapViewInternal(sharedMemory, remapBackingOffset, (IntPtr)remapAddress, (IntPtr)remapSize, updateProtection: false); + RestoreRangeProtection(remapAddress, remapSize); + } + } + finally + { + partialUnmapLock.DowngradeFromWriterLock(); + } + } + else if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)overlap.Start, 2)) + { + throw new WindowsApiException("UnmapViewOfFile2"); + } + } + } + + CoalesceForUnmap(startAddress, unmapSize, owner); + + if (updateProtection) + { + UpdateProtection(startAddress, unmapSize, MemoryPermission.None); + } + } + + /// <summary> + /// Coalesces adjacent placeholders after unmap. + /// </summary> + /// <param name="address">Address of the region that was unmapped</param> + /// <param name="size">Size of the region that was unmapped in bytes</param> + /// <param name="owner">Memory block that owns the mapping</param> + private void CoalesceForUnmap(ulong address, ulong size, MemoryBlock owner) + { + ulong endAddress = address + size; + ulong blockAddress = (ulong)owner.Pointer; + ulong blockEnd = blockAddress + owner.Size; + int unmappedCount = 0; + + lock (_mappings) + { + RangeNode<ulong> node = _mappings.GetNodeByKey(address); + + if (node == null) + { + // Nothing to coalesce if we have no overlaps. + return; + } + + RangeNode<ulong> predecessor = node.Predecessor; + RangeNode<ulong> successor = null; + + for (; node != null; node = successor) + { + successor = node.Successor; + var overlap = node; + + if (!IsMapped(overlap.Value)) + { + address = Math.Min(address, overlap.Start); + endAddress = Math.Max(endAddress, overlap.End); + + _mappings.Remove(overlap); + unmappedCount++; + } + + if (node.End >= endAddress) + { + break; + } + } + + if (predecessor != null && !IsMapped(predecessor.Value) && predecessor.Start >= blockAddress) + { + address = Math.Min(address, predecessor.Start); + + _mappings.Remove(predecessor); + unmappedCount++; + } + + if (successor != null && !IsMapped(successor.Value) && successor.End <= blockEnd) + { + endAddress = Math.Max(endAddress, successor.End); + + _mappings.Remove(successor); + unmappedCount++; + } + + _mappings.Add(new RangeNode<ulong>(address, endAddress, ulong.MaxValue)); + } + + if (unmappedCount > 1) + { + size = endAddress - address; + + CheckFreeResult(WindowsApi.VirtualFree( + (IntPtr)address, + (IntPtr)size, + AllocationType.Release | AllocationType.CoalescePlaceholders)); + } + } + + /// <summary> + /// Reprotects a region of memory that has been mapped. + /// </summary> + /// <param name="address">Address of the region to reprotect</param> + /// <param name="size">Size of the region to reprotect in bytes</param> + /// <param name="permission">New permissions</param> + /// <returns>True if the reprotection was successful, false otherwise</returns> + public bool ReprotectView(IntPtr address, IntPtr size, MemoryPermission permission) + { + ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock; + partialUnmapLock.AcquireReaderLock(); + + try + { + return ReprotectViewInternal(address, size, permission, false); + } + finally + { + partialUnmapLock.ReleaseReaderLock(); + } + } + + /// <summary> + /// Reprotects a region of memory that has been mapped. + /// </summary> + /// <param name="address">Address of the region to reprotect</param> + /// <param name="size">Size of the region to reprotect in bytes</param> + /// <param name="permission">New permissions</param> + /// <param name="throwOnError">Throw an exception instead of returning an error if the operation fails</param> + /// <returns>True if the reprotection was successful or if <paramref name="throwOnError"/> is true, false otherwise</returns> + /// <exception cref="WindowsApiException">If <paramref name="throwOnError"/> is true, it is thrown when the Windows API returns an error reprotecting the memory</exception> + private bool ReprotectViewInternal(IntPtr address, IntPtr size, MemoryPermission permission, bool throwOnError) + { + ulong reprotectAddress = (ulong)address; + ulong reprotectSize = (ulong)size; + ulong endAddress = reprotectAddress + reprotectSize; + + bool success = true; + + lock (_mappings) + { + RangeNode<ulong> node = _mappings.GetNodeByKey(reprotectAddress); + RangeNode<ulong> successorNode; + + for (; node != null; node = successorNode) + { + successorNode = node.Successor; + var overlap = node; + + ulong mappedAddress = overlap.Start; + ulong mappedSize = overlap.End - overlap.Start; + + if (mappedAddress < reprotectAddress) + { + ulong delta = reprotectAddress - mappedAddress; + mappedAddress = reprotectAddress; + mappedSize -= delta; + } + + ulong mappedEndAddress = mappedAddress + mappedSize; + + if (mappedEndAddress > endAddress) + { + ulong delta = mappedEndAddress - endAddress; + mappedSize -= delta; + } + + if (!WindowsApi.VirtualProtect((IntPtr)mappedAddress, (IntPtr)mappedSize, WindowsApi.GetProtection(permission), out _)) + { + if (throwOnError) + { + throw new WindowsApiException("VirtualProtect"); + } + + success = false; + } + + if (node.End >= endAddress) + { + break; + } + } + } + + UpdateProtection(reprotectAddress, reprotectSize, permission); + + return success; + } + + /// <summary> + /// Checks the result of a VirtualFree operation, throwing if needed. + /// </summary> + /// <param name="success">Operation result</param> + /// <exception cref="WindowsApiException">Thrown if <paramref name="success"/> is false</exception> + private static void CheckFreeResult(bool success) + { + if (!success) + { + throw new WindowsApiException("VirtualFree"); + } + } + + /// <summary> + /// Adds an offset to a backing offset. This will do nothing if the backing offset is the special "unmapped" value. + /// </summary> + /// <param name="backingOffset">Backing offset</param> + /// <param name="offset">Offset to be added</param> + /// <returns>Added offset or just <paramref name="backingOffset"/> if the region is unmapped</returns> + private static ulong AddBackingOffset(ulong backingOffset, ulong offset) + { + if (backingOffset == ulong.MaxValue) + { + return backingOffset; + } + + return backingOffset + offset; + } + + /// <summary> + /// Checks if a region is unmapped. + /// </summary> + /// <param name="backingOffset">Backing offset to check</param> + /// <returns>True if the backing offset is the special "unmapped" value, false otherwise</returns> + private static bool IsMapped(ulong backingOffset) + { + return backingOffset != ulong.MaxValue; + } + + /// <summary> + /// Adds a protection to the list of protections. + /// </summary> + /// <param name="address">Address of the protected region</param> + /// <param name="size">Size of the protected region in bytes</param> + /// <param name="permission">Memory permissions of the region</param> + private void UpdateProtection(ulong address, ulong size, MemoryPermission permission) + { + ulong endAddress = address + size; + + lock (_protections) + { + RangeNode<MemoryPermission> node = _protections.GetNodeByKey(address); + + if (node != null && + node.Start <= address && + node.End >= endAddress && + node.Value == permission) + { + return; + } + + RangeNode<MemoryPermission> successorNode; + + ulong startAddress = address; + + for (; node != null; node = successorNode) + { + successorNode = node.Successor; + var protection = node; + + ulong protAddress = protection.Start; + ulong protEndAddress = protection.End; + MemoryPermission protPermission = protection.Value; + + _protections.Remove(protection); + + if (protPermission == permission) + { + if (startAddress > protAddress) + { + startAddress = protAddress; + } + + if (endAddress < protEndAddress) + { + endAddress = protEndAddress; + } + } + else + { + if (startAddress > protAddress) + { + _protections.Add(new RangeNode<MemoryPermission>(protAddress, startAddress, protPermission)); + } + + if (endAddress < protEndAddress) + { + _protections.Add(new RangeNode<MemoryPermission>(endAddress, protEndAddress, protPermission)); + } + } + + if (node.End >= endAddress) + { + break; + } + } + + _protections.Add(new RangeNode<MemoryPermission>(startAddress, endAddress, permission)); + } + } + + /// <summary> + /// Removes protection from the list of protections. + /// </summary> + /// <param name="address">Address of the protected region</param> + /// <param name="size">Size of the protected region in bytes</param> + private void RemoveProtection(ulong address, ulong size) + { + ulong endAddress = address + size; + + lock (_protections) + { + RangeNode<MemoryPermission> node = _protections.GetNodeByKey(address); + RangeNode<MemoryPermission> successorNode; + + for (; node != null; node = successorNode) + { + successorNode = node.Successor; + var protection = node; + + ulong protAddress = protection.Start; + ulong protEndAddress = protection.End; + MemoryPermission protPermission = protection.Value; + + _protections.Remove(protection); + + if (address > protAddress) + { + _protections.Add(new RangeNode<MemoryPermission>(protAddress, address, protPermission)); + } + + if (endAddress < protEndAddress) + { + _protections.Add(new RangeNode<MemoryPermission>(endAddress, protEndAddress, protPermission)); + } + + if (node.End >= endAddress) + { + break; + } + } + } + } + + /// <summary> + /// Restores the protection of a given memory region that was remapped, using the protections list. + /// </summary> + /// <param name="address">Address of the remapped region</param> + /// <param name="size">Size of the remapped region in bytes</param> + private void RestoreRangeProtection(ulong address, ulong size) + { + ulong endAddress = address + size; + var overlaps = new RangeNode<MemoryPermission>[InitialOverlapsSize]; + int count; + + lock (_protections) + { + count = _protections.GetNodes(address, endAddress, ref overlaps); + } + + ulong startAddress = address; + + for (int index = 0; index < count; index++) + { + var protection = overlaps[index]; + + // If protection is R/W we don't need to reprotect as views are initially mapped as R/W. + if (protection.Value == MemoryPermission.ReadAndWrite) + { + continue; + } + + ulong protAddress = protection.Start; + ulong protEndAddress = protection.End; + + if (protAddress < address) + { + protAddress = address; + } + + if (protEndAddress > endAddress) + { + protEndAddress = endAddress; + } + + ReprotectViewInternal((IntPtr)protAddress, (IntPtr)(protEndAddress - protAddress), protection.Value, true); + } + } + } +}
\ No newline at end of file |
