diff options
Diffstat (limited to 'src/Ryujinx.Memory')
47 files changed, 6716 insertions, 0 deletions
diff --git a/src/Ryujinx.Memory/AddressSpaceManager.cs b/src/Ryujinx.Memory/AddressSpaceManager.cs new file mode 100644 index 00000000..ac89fca6 --- /dev/null +++ b/src/Ryujinx.Memory/AddressSpaceManager.cs @@ -0,0 +1,470 @@ +using Ryujinx.Memory.Range; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Memory +{ + /// <summary> + /// 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, IWritableBlock + { + public const int PageBits = PageTable<nuint>.PageBits; + public const int PageSize = PageTable<nuint>.PageSize; + public const int PageMask = PageTable<nuint>.PageMask; + + /// <inheritdoc/> + public bool Supports4KBPages => true; + + /// <summary> + /// Address space width in bits. + /// </summary> + public int AddressSpaceBits { get; } + + private readonly ulong _addressSpaceSize; + + private readonly MemoryBlock _backingMemory; + private readonly PageTable<nuint> _pageTable; + + /// <summary> + /// Creates a new instance of the memory manager. + /// </summary> + /// <param name="backingMemory">Physical backing memory where virtual memory will be mapped to</param> + /// <param name="addressSpaceSize">Size of the address space</param> + public AddressSpaceManager(MemoryBlock backingMemory, ulong addressSpaceSize) + { + ulong asSize = PageSize; + int asBits = PageBits; + + while (asSize < addressSpaceSize) + { + asSize <<= 1; + asBits++; + } + + AddressSpaceBits = asBits; + _addressSpaceSize = asSize; + _backingMemory = backingMemory; + _pageTable = new PageTable<nuint>(); + } + + /// <inheritdoc/> + public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) + { + AssertValidAddressAndSize(va, size); + + while (size != 0) + { + _pageTable.Map(va, (nuint)(ulong)_backingMemory.GetPointer(pa, PageSize)); + + va += PageSize; + pa += PageSize; + size -= PageSize; + } + } + + /// <inheritdoc/> + public void MapForeign(ulong va, nuint hostPointer, ulong size) + { + AssertValidAddressAndSize(va, size); + + while (size != 0) + { + _pageTable.Map(va, hostPointer); + + va += PageSize; + hostPointer += PageSize; + size -= PageSize; + } + } + + /// <inheritdoc/> + public void Unmap(ulong va, ulong size) + { + AssertValidAddressAndSize(va, size); + + while (size != 0) + { + _pageTable.Unmap(va); + + va += PageSize; + size -= PageSize; + } + } + + /// <inheritdoc/> + public T Read<T>(ulong va) where T : unmanaged + { + return MemoryMarshal.Cast<byte, T>(GetSpan(va, Unsafe.SizeOf<T>()))[0]; + } + + /// <inheritdoc/> + public void Read(ulong va, Span<byte> data) + { + ReadImpl(va, data); + } + + /// <inheritdoc/> + public void Write<T>(ulong va, T value) where T : unmanaged + { + Write(va, MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1))); + } + + /// <inheritdoc/> + public void Write(ulong va, ReadOnlySpan<byte> data) + { + if (data.Length == 0) + { + return; + } + + AssertValidAddressAndSize(va, (ulong)data.Length); + + if (IsContiguousAndMapped(va, data.Length)) + { + data.CopyTo(GetHostSpanContiguous(va, data.Length)); + } + else + { + int offset = 0, size; + + if ((va & PageMask) != 0) + { + size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); + + data.Slice(0, size).CopyTo(GetHostSpanContiguous(va, size)); + + offset += size; + } + + for (; offset < data.Length; offset += size) + { + size = Math.Min(data.Length - offset, PageSize); + + data.Slice(offset, size).CopyTo(GetHostSpanContiguous(va + (ulong)offset, size)); + } + } + } + + /// <inheritdoc/> + public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data) + { + Write(va, data); + + return true; + } + + /// <inheritdoc/> + public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false) + { + if (size == 0) + { + return ReadOnlySpan<byte>.Empty; + } + + if (IsContiguousAndMapped(va, size)) + { + return GetHostSpanContiguous(va, size); + } + else + { + Span<byte> data = new byte[size]; + + ReadImpl(va, data); + + return data; + } + } + + /// <inheritdoc/> + public unsafe WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) + { + if (size == 0) + { + return new WritableRegion(null, va, Memory<byte>.Empty); + } + + if (IsContiguousAndMapped(va, size)) + { + return new WritableRegion(null, va, new NativeMemoryManager<byte>((byte*)GetHostAddress(va), size).Memory); + } + else + { + Memory<byte> memory = new byte[size]; + + GetSpan(va, size).CopyTo(memory.Span); + + return new WritableRegion(this, va, memory); + } + } + + /// <inheritdoc/> + public unsafe ref T GetRef<T>(ulong va) where T : unmanaged + { + if (!IsContiguous(va, Unsafe.SizeOf<T>())) + { + ThrowMemoryNotContiguous(); + } + + return ref *(T*)GetHostAddress(va); + } + + /// <inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetPagesCount(ulong va, uint size, out ulong startVa) + { + // WARNING: Always check if ulong does not overflow during the operations. + startVa = va & ~(ulong)PageMask; + ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; + + return (int)(vaSpan / PageSize); + } + + private void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsContiguous(ulong va, int size) + { + if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size)) + { + return false; + } + + int pages = GetPagesCount(va, (uint)size, out va); + + for (int page = 0; page < pages - 1; page++) + { + if (!ValidateAddress(va + PageSize)) + { + return false; + } + + if (GetHostAddress(va) + PageSize != GetHostAddress(va + PageSize)) + { + return false; + } + + va += PageSize; + } + + return true; + } + + /// <inheritdoc/> + public IEnumerable<HostMemoryRange> GetHostRegions(ulong va, ulong size) + { + if (size == 0) + { + return Enumerable.Empty<HostMemoryRange>(); + } + + return GetHostRegionsImpl(va, size); + } + + /// <inheritdoc/> + public IEnumerable<MemoryRange> GetPhysicalRegions(ulong va, ulong size) + { + if (size == 0) + { + return Enumerable.Empty<MemoryRange>(); + } + + var hostRegions = GetHostRegionsImpl(va, size); + if (hostRegions == null) + { + return null; + } + + var regions = new MemoryRange[hostRegions.Count]; + + ulong backingStart = (ulong)_backingMemory.Pointer; + ulong backingEnd = backingStart + _backingMemory.Size; + + int count = 0; + + for (int i = 0; i < regions.Length; i++) + { + var hostRegion = hostRegions[i]; + + if ((ulong)hostRegion.Address >= backingStart && (ulong)hostRegion.Address < backingEnd) + { + regions[count++] = new MemoryRange((ulong)hostRegion.Address - backingStart, hostRegion.Size); + } + } + + if (count != regions.Length) + { + return new ArraySegment<MemoryRange>(regions, 0, count); + } + + return regions; + } + + private List<HostMemoryRange> GetHostRegionsImpl(ulong va, ulong size) + { + if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size)) + { + return null; + } + + int pages = GetPagesCount(va, (uint)size, out va); + + var regions = new List<HostMemoryRange>(); + + nuint regionStart = GetHostAddress(va); + ulong regionSize = PageSize; + + for (int page = 0; page < pages - 1; page++) + { + if (!ValidateAddress(va + PageSize)) + { + return null; + } + + nuint newHostAddress = GetHostAddress(va + PageSize); + + if (GetHostAddress(va) + PageSize != newHostAddress) + { + regions.Add(new HostMemoryRange(regionStart, regionSize)); + regionStart = newHostAddress; + regionSize = 0; + } + + va += PageSize; + regionSize += PageSize; + } + + regions.Add(new HostMemoryRange(regionStart, regionSize)); + + return regions; + } + + private void ReadImpl(ulong va, Span<byte> data) + { + if (data.Length == 0) + { + return; + } + + AssertValidAddressAndSize(va, (ulong)data.Length); + + int offset = 0, size; + + if ((va & PageMask) != 0) + { + size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); + + GetHostSpanContiguous(va, size).CopyTo(data.Slice(0, size)); + + offset += size; + } + + for (; offset < data.Length; offset += size) + { + size = Math.Min(data.Length - offset, PageSize); + + GetHostSpanContiguous(va + (ulong)offset, size).CopyTo(data.Slice(offset, size)); + } + } + + /// <inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsMapped(ulong va) + { + if (!ValidateAddress(va)) + { + return false; + } + + return _pageTable.Read(va) != 0; + } + + /// <inheritdoc/> + public bool IsRangeMapped(ulong va, ulong size) + { + if (size == 0UL) + { + return true; + } + + if (!ValidateAddressAndSize(va, size)) + { + return false; + } + + int pages = GetPagesCount(va, (uint)size, out va); + + for (int page = 0; page < pages; page++) + { + if (!IsMapped(va)) + { + return false; + } + + va += PageSize; + } + + return true; + } + + private bool ValidateAddress(ulong va) + { + return va < _addressSpaceSize; + } + + /// <summary> + /// Checks if the combination of virtual address and size is part of the addressable space. + /// </summary> + /// <param name="va">Virtual address of the range</param> + /// <param name="size">Size of the range in bytes</param> + /// <returns>True if the combination of virtual address and size is part of the addressable space</returns> + private bool ValidateAddressAndSize(ulong va, ulong size) + { + ulong endVa = va + size; + return endVa >= va && endVa >= size && endVa <= _addressSpaceSize; + } + + /// <summary> + /// Ensures the combination of virtual address and size is part of the addressable space. + /// </summary> + /// <param name="va">Virtual address of the range</param> + /// <param name="size">Size of the range in bytes</param> + /// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified outside the addressable space</exception> + private void AssertValidAddressAndSize(ulong va, ulong size) + { + if (!ValidateAddressAndSize(va, size)) + { + throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}"); + } + } + + private unsafe Span<byte> GetHostSpanContiguous(ulong va, int size) + { + return new Span<byte>((void*)GetHostAddress(va), size); + } + + private nuint GetHostAddress(ulong va) + { + return _pageTable.Read(va) + (nuint)(va & PageMask); + } + + /// <inheritdoc/> + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + { + throw new NotImplementedException(); + } + + /// <inheritdoc/> + public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + { + // Only the ARM Memory Manager has tracking for now. + } + } +} diff --git a/src/Ryujinx.Memory/IRefCounted.cs b/src/Ryujinx.Memory/IRefCounted.cs new file mode 100644 index 00000000..e0a311d6 --- /dev/null +++ b/src/Ryujinx.Memory/IRefCounted.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.Memory +{ + public interface IRefCounted + { + void IncrementReferenceCount(); + void DecrementReferenceCount(); + } +} diff --git a/src/Ryujinx.Memory/IVirtualMemoryManager.cs b/src/Ryujinx.Memory/IVirtualMemoryManager.cs new file mode 100644 index 00000000..edbfc885 --- /dev/null +++ b/src/Ryujinx.Memory/IVirtualMemoryManager.cs @@ -0,0 +1,205 @@ +using Ryujinx.Memory.Range; +using System; +using System.Buffers; +using System.Collections.Generic; + +namespace Ryujinx.Memory +{ + public interface IVirtualMemoryManager + { + /// <summary> + /// Indicates whenever the memory manager supports aliasing pages at 4KB granularity. + /// </summary> + /// <returns>True if 4KB pages are supported by the memory manager, false otherwise</returns> + bool Supports4KBPages { get; } + + /// <summary> + /// Maps a virtual memory range into a physical memory range. + /// </summary> + /// <remarks> + /// Addresses and size must be page aligned. + /// </remarks> + /// <param name="va">Virtual memory address</param> + /// <param name="pa">Physical memory address where the region should be mapped to</param> + /// <param name="size">Size to be mapped</param> + /// <param name="flags">Flags controlling memory mapping</param> + void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags); + + /// <summary> + /// Maps a virtual memory range into an arbitrary host memory range. + /// </summary> + /// <remarks> + /// Addresses and size must be page aligned. + /// Not all memory managers supports this feature. + /// </remarks> + /// <param name="va">Virtual memory address</param> + /// <param name="hostPointer">Host pointer where the virtual region should be mapped</param> + /// <param name="size">Size to be mapped</param> + void MapForeign(ulong va, nuint hostPointer, ulong size); + + /// <summary> + /// Unmaps a previously mapped range of virtual memory. + /// </summary> + /// <param name="va">Virtual address of the range to be unmapped</param> + /// <param name="size">Size of the range to be unmapped</param> + void Unmap(ulong va, ulong size); + + /// <summary> + /// Reads data from CPU mapped memory. + /// </summary> + /// <typeparam name="T">Type of the data being read</typeparam> + /// <param name="va">Virtual address of the data in memory</param> + /// <returns>The data</returns> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + T Read<T>(ulong va) where T : unmanaged; + + /// <summary> + /// Reads data from CPU mapped memory. + /// </summary> + /// <param name="va">Virtual address of the data in memory</param> + /// <param name="data">Span to store the data being read into</param> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + void Read(ulong va, Span<byte> data); + + /// <summary> + /// Writes data to CPU mapped memory. + /// </summary> + /// <typeparam name="T">Type of the data being written</typeparam> + /// <param name="va">Virtual address to write the data into</param> + /// <param name="value">Data to be written</param> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + void Write<T>(ulong va, T value) where T : unmanaged; + + /// <summary> + /// Writes data to CPU mapped memory, with write tracking. + /// </summary> + /// <param name="va">Virtual address to write the data into</param> + /// <param name="data">Data to be written</param> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + void Write(ulong va, ReadOnlySpan<byte> data); + + /// <summary> + /// Writes data to CPU mapped memory, with write tracking. + /// </summary> + /// <param name="va">Virtual address to write the data into</param> + /// <param name="data">Data to be written</param> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + public void Write(ulong va, ReadOnlySequence<byte> data) + { + foreach (ReadOnlyMemory<byte> segment in data) + { + Write(va, segment.Span); + va += (ulong)segment.Length; + } + } + + /// <summary> + /// Writes data to the application process, returning false if the data was not changed. + /// This triggers read memory tracking, as a redundancy check would be useless if the data is not up to date. + /// </summary> + /// <remarks>The memory manager can return that memory has changed when it hasn't to avoid expensive data copies.</remarks> + /// <param name="va">Virtual address to write the data into</param> + /// <param name="data">Data to be written</param> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + /// <returns>True if the data was changed, false otherwise</returns> + bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data); + + void Fill(ulong va, ulong size, byte value) + { + const int MaxChunkSize = 1 << 24; + + for (ulong subOffset = 0; subOffset < size; subOffset += MaxChunkSize) + { + int copySize = (int)Math.Min(MaxChunkSize, size - subOffset); + + using var writableRegion = GetWritableRegion(va + subOffset, copySize); + + writableRegion.Memory.Span.Fill(value); + } + } + + /// <summary> + /// Gets a read-only span of data from CPU mapped memory. + /// </summary> + /// <param name="va">Virtual address of the data</param> + /// <param name="size">Size of the data</param> + /// <param name="tracked">True if read tracking is triggered on the span</param> + /// <returns>A read-only span of the data</returns> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false); + + /// <summary> + /// Gets a region of memory that can be written to. + /// </summary> + /// <param name="va">Virtual address of the data</param> + /// <param name="size">Size of the data</param> + /// <param name="tracked">True if write tracking is triggered on the span</param> + /// <returns>A writable region of memory containing the data</returns> + /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> + WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false); + + /// <summary> + /// Gets a reference for the given type at the specified virtual memory address. + /// </summary> + /// <remarks> + /// The data must be located at a contiguous memory region. + /// </remarks> + /// <typeparam name="T">Type of the data to get the reference</typeparam> + /// <param name="va">Virtual address of the data</param> + /// <returns>A reference to the data in memory</returns> + /// <exception cref="MemoryNotContiguousException">Throw if the specified memory region is not contiguous in physical memory</exception> + ref T GetRef<T>(ulong va) where T : unmanaged; + + /// <summary> + /// Gets the host regions that make up the given virtual address region. + /// If any part of the virtual region is unmapped, null is returned. + /// </summary> + /// <param name="va">Virtual address of the range</param> + /// <param name="size">Size of the range</param> + /// <returns>Array of host regions</returns> + IEnumerable<HostMemoryRange> GetHostRegions(ulong va, ulong size); + + /// <summary> + /// Gets the physical regions that make up the given virtual address region. + /// If any part of the virtual region is unmapped, null is returned. + /// </summary> + /// <param name="va">Virtual address of the range</param> + /// <param name="size">Size of the range</param> + /// <returns>Array of physical regions</returns> + IEnumerable<MemoryRange> GetPhysicalRegions(ulong va, ulong size); + + /// <summary> + /// Checks if the page at a given CPU virtual address is mapped. + /// </summary> + /// <param name="va">Virtual address to check</param> + /// <returns>True if the address is mapped, false otherwise</returns> + bool IsMapped(ulong va); + + /// <summary> + /// Checks if a memory range is mapped. + /// </summary> + /// <param name="va">Virtual address of the range</param> + /// <param name="size">Size of the range in bytes</param> + /// <returns>True if the entire range is mapped, false otherwise</returns> + bool IsRangeMapped(ulong va, ulong size); + + /// <summary> + /// Alerts the memory tracking that a given region has been read from or written to. + /// This should be called before read/write is performed. + /// </summary> + /// <param name="va">Virtual address of the region</param> + /// <param name="size">Size of the region</param> + /// <param name="write">True if the region was written, false if read</param> + /// <param name="precise">True if the access is precise, false otherwise</param> + /// <param name="exemptId">Optional ID of the handles that should not be signalled</param> + void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null); + + /// <summary> + /// Reprotect a region of virtual memory for tracking. + /// </summary> + /// <param name="va">Virtual address base</param> + /// <param name="size">Size of the region to protect</param> + /// <param name="protection">Memory protection to set</param> + void TrackingReprotect(ulong va, ulong size, MemoryPermission protection); + } +} diff --git a/src/Ryujinx.Memory/IWritableBlock.cs b/src/Ryujinx.Memory/IWritableBlock.cs new file mode 100644 index 00000000..36b9f5a6 --- /dev/null +++ b/src/Ryujinx.Memory/IWritableBlock.cs @@ -0,0 +1,11 @@ +using System; + +namespace Ryujinx.Memory +{ + public interface IWritableBlock + { + void Write(ulong va, ReadOnlySpan<byte> data); + + void WriteUntracked(ulong va, ReadOnlySpan<byte> data) => Write(va, data); + } +} diff --git a/src/Ryujinx.Memory/InvalidAccessHandler.cs b/src/Ryujinx.Memory/InvalidAccessHandler.cs new file mode 100644 index 00000000..3dadb766 --- /dev/null +++ b/src/Ryujinx.Memory/InvalidAccessHandler.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Memory +{ + /// <summary> + /// Function that handles a invalid memory access from the emulated CPU. + /// </summary> + /// <param name="va">Virtual address of the invalid region that is being accessed</param> + /// <returns>True if the invalid access should be ignored, false otherwise</returns> + public delegate bool InvalidAccessHandler(ulong va); +} diff --git a/src/Ryujinx.Memory/InvalidMemoryRegionException.cs b/src/Ryujinx.Memory/InvalidMemoryRegionException.cs new file mode 100644 index 00000000..01482400 --- /dev/null +++ b/src/Ryujinx.Memory/InvalidMemoryRegionException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Ryujinx.Memory +{ + public class InvalidMemoryRegionException : Exception + { + public InvalidMemoryRegionException() : base("Attempted to access an invalid memory region.") + { + } + + public InvalidMemoryRegionException(string message) : base(message) + { + } + + public InvalidMemoryRegionException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/Ryujinx.Memory/MemoryAllocationFlags.cs b/src/Ryujinx.Memory/MemoryAllocationFlags.cs new file mode 100644 index 00000000..6f0ef1aa --- /dev/null +++ b/src/Ryujinx.Memory/MemoryAllocationFlags.cs @@ -0,0 +1,52 @@ +using System; + +namespace Ryujinx.Memory +{ + /// <summary> + /// Flags that controls allocation and other properties of the memory block memory. + /// </summary> + [Flags] + public enum MemoryAllocationFlags + { + /// <summary> + /// No special allocation settings. + /// </summary> + None = 0, + + /// <summary> + /// Reserve a region of memory on the process address space, + /// without actually allocation any backing memory. + /// </summary> + Reserve = 1 << 0, + + /// <summary> + /// Enables read and write tracking of the memory block. + /// This currently does nothing and is reserved for future use. + /// </summary> + Tracked = 1 << 1, + + /// <summary> + /// Enables mirroring of the memory block through aliasing of memory pages. + /// When enabled, this allows creating more memory blocks sharing the same backing storage. + /// </summary> + Mirrorable = 1 << 2, + + /// <summary> + /// Indicates that the memory block should support mapping views of a mirrorable memory block. + /// The block that is to have their views mapped should be created with the <see cref="Mirrorable"/> flag. + /// </summary> + ViewCompatible = 1 << 3, + + /// <summary> + /// If used with the <see cref="Mirrorable"/> flag, indicates that the memory block will only be used as + /// backing storage and will never be accessed directly, so the memory for the block will not be mapped. + /// </summary> + NoMap = 1 << 4, + + /// <summary> + /// Indicates that the memory will be used to store JIT generated code. + /// On some platforms, this requires special flags to be passed that will allow the memory to be executable. + /// </summary> + Jit = 1 << 5 + } +} diff --git a/src/Ryujinx.Memory/MemoryBlock.cs b/src/Ryujinx.Memory/MemoryBlock.cs new file mode 100644 index 00000000..885ef456 --- /dev/null +++ b/src/Ryujinx.Memory/MemoryBlock.cs @@ -0,0 +1,443 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Ryujinx.Memory +{ + /// <summary> + /// Represents a block of contiguous physical guest memory. + /// </summary> + public sealed class MemoryBlock : IWritableBlock, IDisposable + { + private readonly bool _usesSharedMemory; + private readonly bool _isMirror; + private readonly bool _viewCompatible; + private readonly bool _forJit; + private IntPtr _sharedMemory; + private IntPtr _pointer; + + /// <summary> + /// Pointer to the memory block data. + /// </summary> + public IntPtr Pointer => _pointer; + + /// <summary> + /// Size of the memory block. + /// </summary> + public ulong Size { get; } + + /// <summary> + /// Creates a new instance of the memory block class. + /// </summary> + /// <param name="size">Size of the memory block in bytes</param> + /// <param name="flags">Flags that controls memory block memory allocation</param> + /// <exception cref="OutOfMemoryException">Throw when there's no enough memory to allocate the requested size</exception> + /// <exception cref="PlatformNotSupportedException">Throw when the current platform is not supported</exception> + public MemoryBlock(ulong size, MemoryAllocationFlags flags = MemoryAllocationFlags.None) + { + if (flags.HasFlag(MemoryAllocationFlags.Mirrorable)) + { + _sharedMemory = MemoryManagement.CreateSharedMemory(size, flags.HasFlag(MemoryAllocationFlags.Reserve)); + + if (!flags.HasFlag(MemoryAllocationFlags.NoMap)) + { + _pointer = MemoryManagement.MapSharedMemory(_sharedMemory, size); + } + + _usesSharedMemory = true; + } + else if (flags.HasFlag(MemoryAllocationFlags.Reserve)) + { + _viewCompatible = flags.HasFlag(MemoryAllocationFlags.ViewCompatible); + _forJit = flags.HasFlag(MemoryAllocationFlags.Jit); + _pointer = MemoryManagement.Reserve(size, _forJit, _viewCompatible); + } + else + { + _forJit = flags.HasFlag(MemoryAllocationFlags.Jit); + _pointer = MemoryManagement.Allocate(size, _forJit); + } + + Size = size; + } + + /// <summary> + /// Creates a new instance of the memory block class, with a existing backing storage. + /// </summary> + /// <param name="size">Size of the memory block in bytes</param> + /// <param name="sharedMemory">Shared memory to use as backing storage for this block</param> + /// <exception cref="OutOfMemoryException">Throw when there's no enough address space left to map the shared memory</exception> + /// <exception cref="PlatformNotSupportedException">Throw when the current platform is not supported</exception> + private MemoryBlock(ulong size, IntPtr sharedMemory) + { + _pointer = MemoryManagement.MapSharedMemory(sharedMemory, size); + Size = size; + _usesSharedMemory = true; + _isMirror = true; + } + + /// <summary> + /// Creates a memory block that shares the backing storage with this block. + /// The memory and page commitments will be shared, however memory protections are separate. + /// </summary> + /// <returns>A new memory block that shares storage with this one</returns> + /// <exception cref="NotSupportedException">Throw when the current memory block does not support mirroring</exception> + /// <exception cref="OutOfMemoryException">Throw when there's no enough address space left to map the shared memory</exception> + /// <exception cref="PlatformNotSupportedException">Throw when the current platform is not supported</exception> + public MemoryBlock CreateMirror() + { + if (_sharedMemory == IntPtr.Zero) + { + throw new NotSupportedException("Mirroring is not supported on the memory block because the Mirrorable flag was not set."); + } + + return new MemoryBlock(Size, _sharedMemory); + } + + /// <summary> + /// Commits a region of memory that has previously been reserved. + /// This can be used to allocate memory on demand. + /// </summary> + /// <param name="offset">Starting offset of the range to be committed</param> + /// <param name="size">Size of the range to be committed</param> + /// <returns>True if the operation was successful, false otherwise</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + public bool Commit(ulong offset, ulong size) + { + return MemoryManagement.Commit(GetPointerInternal(offset, size), size, _forJit); + } + + /// <summary> + /// Decommits a region of memory that has previously been reserved and optionally comitted. + /// This can be used to free previously allocated memory on demand. + /// </summary> + /// <param name="offset">Starting offset of the range to be decommitted</param> + /// <param name="size">Size of the range to be decommitted</param> + /// <returns>True if the operation was successful, false otherwise</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + public bool Decommit(ulong offset, ulong size) + { + return MemoryManagement.Decommit(GetPointerInternal(offset, size), size); + } + + /// <summary> + /// Maps a view of memory from another memory block. + /// </summary> + /// <param name="srcBlock">Memory block from where the backing memory will be taken</param> + /// <param name="srcOffset">Offset on <paramref name="srcBlock"/> of the region that should be mapped</param> + /// <param name="dstOffset">Offset to map the view into on this block</param> + /// <param name="size">Size of the range to be mapped</param> + /// <exception cref="NotSupportedException">Throw when the source memory block does not support mirroring</exception> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size) + { + if (srcBlock._sharedMemory == IntPtr.Zero) + { + throw new ArgumentException("The source memory block is not mirrorable, and thus cannot be mapped on the current block."); + } + + MemoryManagement.MapView(srcBlock._sharedMemory, srcOffset, GetPointerInternal(dstOffset, size), size, this); + } + + /// <summary> + /// Unmaps a view of memory from another memory block. + /// </summary> + /// <param name="srcBlock">Memory block from where the backing memory was taken during map</param> + /// <param name="offset">Offset of the view previously mapped with <see cref="MapView"/></param> + /// <param name="size">Size of the range to be unmapped</param> + public void UnmapView(MemoryBlock srcBlock, ulong offset, ulong size) + { + MemoryManagement.UnmapView(srcBlock._sharedMemory, GetPointerInternal(offset, size), size, this); + } + + /// <summary> + /// Reprotects a region of memory. + /// </summary> + /// <param name="offset">Starting offset of the range to be reprotected</param> + /// <param name="size">Size of the range to be reprotected</param> + /// <param name="permission">New memory permissions</param> + /// <param name="throwOnFail">True if a failed reprotect should throw</param> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + /// <exception cref="MemoryProtectionException">Throw when <paramref name="permission"/> is invalid</exception> + public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail = true) + { + MemoryManagement.Reprotect(GetPointerInternal(offset, size), size, permission, _viewCompatible, throwOnFail); + } + + /// <summary> + /// Reads bytes from the memory block. + /// </summary> + /// <param name="offset">Starting offset of the range being read</param> + /// <param name="data">Span where the bytes being read will be copied to</param> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified for the the data is out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Read(ulong offset, Span<byte> data) + { + GetSpan(offset, data.Length).CopyTo(data); + } + + /// <summary> + /// Reads data from the memory block. + /// </summary> + /// <typeparam name="T">Type of the data</typeparam> + /// <param name="offset">Offset where the data is located</param> + /// <returns>Data at the specified address</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified for the the data is out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Read<T>(ulong offset) where T : unmanaged + { + return GetRef<T>(offset); + } + + /// <summary> + /// Writes bytes to the memory block. + /// </summary> + /// <param name="offset">Starting offset of the range being written</param> + /// <param name="data">Span where the bytes being written will be copied from</param> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified for the the data is out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ulong offset, ReadOnlySpan<byte> data) + { + data.CopyTo(GetSpan(offset, data.Length)); + } + + /// <summary> + /// Writes data to the memory block. + /// </summary> + /// <typeparam name="T">Type of the data being written</typeparam> + /// <param name="offset">Offset to write the data into</param> + /// <param name="data">Data to be written</param> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified for the the data is out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write<T>(ulong offset, T data) where T : unmanaged + { + GetRef<T>(offset) = data; + } + + /// <summary> + /// Copies data from one memory location to another. + /// </summary> + /// <param name="dstOffset">Destination offset to write the data into</param> + /// <param name="srcOffset">Source offset to read the data from</param> + /// <param name="size">Size of the copy in bytes</param> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when <paramref name="srcOffset"/>, <paramref name="dstOffset"/> or <paramref name="size"/> is out of range</exception> + public void Copy(ulong dstOffset, ulong srcOffset, ulong size) + { + const int MaxChunkSize = 1 << 24; + + for (ulong offset = 0; offset < size; offset += MaxChunkSize) + { + int copySize = (int)Math.Min(MaxChunkSize, size - offset); + + Write(dstOffset + offset, GetSpan(srcOffset + offset, copySize)); + } + } + + /// <summary> + /// Fills a region of memory with <paramref name="value"/>. + /// </summary> + /// <param name="offset">Offset of the region to fill with <paramref name="value"/></param> + /// <param name="size">Size in bytes of the region to fill</param> + /// <param name="value">Value to use for the fill</param> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + public void Fill(ulong offset, ulong size, byte value) + { + const int MaxChunkSize = 1 << 24; + + for (ulong subOffset = 0; subOffset < size; subOffset += MaxChunkSize) + { + int copySize = (int)Math.Min(MaxChunkSize, size - subOffset); + + GetSpan(offset + subOffset, copySize).Fill(value); + } + } + + /// <summary> + /// Gets a reference of the data at a given memory block region. + /// </summary> + /// <typeparam name="T">Data type</typeparam> + /// <param name="offset">Offset of the memory region</param> + /// <returns>A reference to the given memory region data</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe ref T GetRef<T>(ulong offset) where T : unmanaged + { + IntPtr ptr = _pointer; + + ObjectDisposedException.ThrowIf(ptr == IntPtr.Zero, this); + + int size = Unsafe.SizeOf<T>(); + + ulong endOffset = offset + (ulong)size; + + if (endOffset > Size || endOffset < offset) + { + ThrowInvalidMemoryRegionException(); + } + + return ref Unsafe.AsRef<T>((void*)PtrAddr(ptr, offset)); + } + + /// <summary> + /// Gets the pointer of a given memory block region. + /// </summary> + /// <param name="offset">Start offset of the memory region</param> + /// <param name="size">Size in bytes of the region</param> + /// <returns>The pointer to the memory region</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IntPtr GetPointer(ulong offset, ulong size) => GetPointerInternal(offset, size); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IntPtr GetPointerInternal(ulong offset, ulong size) + { + IntPtr ptr = _pointer; + + ObjectDisposedException.ThrowIf(ptr == IntPtr.Zero, this); + + ulong endOffset = offset + size; + + if (endOffset > Size || endOffset < offset) + { + ThrowInvalidMemoryRegionException(); + } + + return PtrAddr(ptr, offset); + } + + /// <summary> + /// Gets the <see cref="Span{T}"/> of a given memory block region. + /// </summary> + /// <param name="offset">Start offset of the memory region</param> + /// <param name="size">Size in bytes of the region</param> + /// <returns>Span of the memory region</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe Span<byte> GetSpan(ulong offset, int size) + { + return new Span<byte>((void*)GetPointerInternal(offset, (ulong)size), size); + } + + /// <summary> + /// Gets the <see cref="Memory{T}"/> of a given memory block region. + /// </summary> + /// <param name="offset">Start offset of the memory region</param> + /// <param name="size">Size in bytes of the region</param> + /// <returns>Memory of the memory region</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe Memory<byte> GetMemory(ulong offset, int size) + { + return new NativeMemoryManager<byte>((byte*)GetPointerInternal(offset, (ulong)size), size).Memory; + } + + /// <summary> + /// Gets a writable region of a given memory block region. + /// </summary> + /// <param name="offset">Start offset of the memory region</param> + /// <param name="size">Size in bytes of the region</param> + /// <returns>Writable region of the memory region</returns> + /// <exception cref="ObjectDisposedException">Throw when the memory block has already been disposed</exception> + /// <exception cref="InvalidMemoryRegionException">Throw when either <paramref name="offset"/> or <paramref name="size"/> are out of range</exception> + public WritableRegion GetWritableRegion(ulong offset, int size) + { + return new WritableRegion(null, offset, GetMemory(offset, size)); + } + + /// <summary> + /// Adds a 64-bits offset to a native pointer. + /// </summary> + /// <param name="pointer">Native pointer</param> + /// <param name="offset">Offset to add</param> + /// <returns>Native pointer with the added offset</returns> + private IntPtr PtrAddr(IntPtr pointer, ulong offset) + { + return (IntPtr)(pointer.ToInt64() + (long)offset); + } + + /// <summary> + /// Frees the memory allocated for this memory block. + /// </summary> + /// <remarks> + /// It's an error to use the memory block after disposal. + /// </remarks> + public void Dispose() + { + FreeMemory(); + + GC.SuppressFinalize(this); + } + + ~MemoryBlock() => FreeMemory(); + + private void FreeMemory() + { + IntPtr ptr = Interlocked.Exchange(ref _pointer, IntPtr.Zero); + + // If pointer is null, the memory was already freed or never allocated. + if (ptr != IntPtr.Zero) + { + if (_usesSharedMemory) + { + MemoryManagement.UnmapSharedMemory(ptr, Size); + } + else + { + MemoryManagement.Free(ptr, Size); + } + } + + if (!_isMirror) + { + IntPtr sharedMemory = Interlocked.Exchange(ref _sharedMemory, IntPtr.Zero); + + if (sharedMemory != IntPtr.Zero) + { + MemoryManagement.DestroySharedMemory(sharedMemory); + } + } + } + + /// <summary> + /// Checks if the specified memory allocation flags are supported on the current platform. + /// </summary> + /// <param name="flags">Flags to be checked</param> + /// <returns>True if the platform supports all the flags, false otherwise</returns> + public static bool SupportsFlags(MemoryAllocationFlags flags) + { + if (flags.HasFlag(MemoryAllocationFlags.ViewCompatible)) + { + if (OperatingSystem.IsWindows()) + { + return OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134); + } + + return OperatingSystem.IsLinux() || OperatingSystem.IsMacOS(); + } + + return true; + } + + public static ulong GetPageSize() + { + return (ulong)Environment.SystemPageSize; + } + + private static void ThrowInvalidMemoryRegionException() => throw new InvalidMemoryRegionException(); + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/MemoryConstants.cs b/src/Ryujinx.Memory/MemoryConstants.cs new file mode 100644 index 00000000..fc6f87e0 --- /dev/null +++ b/src/Ryujinx.Memory/MemoryConstants.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Memory +{ + static class MemoryConstants + { + public const int PageBits = 12; + public const int PageSize = 1 << PageBits; + public const int PageMask = PageSize - 1; + } +} diff --git a/src/Ryujinx.Memory/MemoryManagement.cs b/src/Ryujinx.Memory/MemoryManagement.cs new file mode 100644 index 00000000..c4b5ac4c --- /dev/null +++ b/src/Ryujinx.Memory/MemoryManagement.cs @@ -0,0 +1,206 @@ +using System; + +namespace Ryujinx.Memory +{ + public static class MemoryManagement + { + public static IntPtr Allocate(ulong size, bool forJit) + { + if (OperatingSystem.IsWindows()) + { + return MemoryManagementWindows.Allocate((IntPtr)size); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return MemoryManagementUnix.Allocate(size, forJit); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static IntPtr Reserve(ulong size, bool forJit, bool viewCompatible) + { + if (OperatingSystem.IsWindows()) + { + return MemoryManagementWindows.Reserve((IntPtr)size, viewCompatible); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return MemoryManagementUnix.Reserve(size, forJit); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static bool Commit(IntPtr address, ulong size, bool forJit) + { + if (OperatingSystem.IsWindows()) + { + return MemoryManagementWindows.Commit(address, (IntPtr)size); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return MemoryManagementUnix.Commit(address, size, forJit); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static bool Decommit(IntPtr address, ulong size) + { + if (OperatingSystem.IsWindows()) + { + return MemoryManagementWindows.Decommit(address, (IntPtr)size); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return MemoryManagementUnix.Decommit(address, size); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr address, ulong size, MemoryBlock owner) + { + if (OperatingSystem.IsWindows()) + { + MemoryManagementWindows.MapView(sharedMemory, srcOffset, address, (IntPtr)size, owner); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + MemoryManagementUnix.MapView(sharedMemory, srcOffset, address, size); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static void UnmapView(IntPtr sharedMemory, IntPtr address, ulong size, MemoryBlock owner) + { + if (OperatingSystem.IsWindows()) + { + MemoryManagementWindows.UnmapView(sharedMemory, address, (IntPtr)size, owner); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + MemoryManagementUnix.UnmapView(address, size); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static void Reprotect(IntPtr address, ulong size, MemoryPermission permission, bool forView, bool throwOnFail) + { + bool result; + + if (OperatingSystem.IsWindows()) + { + result = MemoryManagementWindows.Reprotect(address, (IntPtr)size, permission, forView); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + result = MemoryManagementUnix.Reprotect(address, size, permission); + } + else + { + throw new PlatformNotSupportedException(); + } + + if (!result && throwOnFail) + { + throw new MemoryProtectionException(permission); + } + } + + public static bool Free(IntPtr address, ulong size) + { + if (OperatingSystem.IsWindows()) + { + return MemoryManagementWindows.Free(address, (IntPtr)size); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return MemoryManagementUnix.Free(address); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static IntPtr CreateSharedMemory(ulong size, bool reserve) + { + if (OperatingSystem.IsWindows()) + { + return MemoryManagementWindows.CreateSharedMemory((IntPtr)size, reserve); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return MemoryManagementUnix.CreateSharedMemory(size, reserve); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static void DestroySharedMemory(IntPtr handle) + { + if (OperatingSystem.IsWindows()) + { + MemoryManagementWindows.DestroySharedMemory(handle); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + MemoryManagementUnix.DestroySharedMemory(handle); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static IntPtr MapSharedMemory(IntPtr handle, ulong size) + { + if (OperatingSystem.IsWindows()) + { + return MemoryManagementWindows.MapSharedMemory(handle); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return MemoryManagementUnix.MapSharedMemory(handle, size); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + public static void UnmapSharedMemory(IntPtr address, ulong size) + { + if (OperatingSystem.IsWindows()) + { + MemoryManagementWindows.UnmapSharedMemory(address); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + MemoryManagementUnix.UnmapSharedMemory(address, size); + } + else + { + throw new PlatformNotSupportedException(); + } + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/MemoryManagementUnix.cs b/src/Ryujinx.Memory/MemoryManagementUnix.cs new file mode 100644 index 00000000..affcff92 --- /dev/null +++ b/src/Ryujinx.Memory/MemoryManagementUnix.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.Versioning; +using System.Text; + +using static Ryujinx.Memory.MemoryManagerUnixHelper; + +namespace Ryujinx.Memory +{ + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + static class MemoryManagementUnix + { + private static readonly ConcurrentDictionary<IntPtr, ulong> _allocations = new ConcurrentDictionary<IntPtr, ulong>(); + + public static IntPtr Allocate(ulong size, bool forJit) + { + return AllocateInternal(size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, forJit); + } + + public static IntPtr Reserve(ulong size, bool forJit) + { + return AllocateInternal(size, MmapProts.PROT_NONE, forJit); + } + + private static IntPtr AllocateInternal(ulong size, MmapProts prot, bool forJit, bool shared = false) + { + MmapFlags flags = MmapFlags.MAP_ANONYMOUS; + + if (shared) + { + flags |= MmapFlags.MAP_SHARED | MmapFlags.MAP_UNLOCKED; + } + else + { + flags |= MmapFlags.MAP_PRIVATE; + } + + if (prot == MmapProts.PROT_NONE) + { + flags |= MmapFlags.MAP_NORESERVE; + } + + if (OperatingSystem.IsMacOSVersionAtLeast(10, 14) && forJit) + { + flags |= MmapFlags.MAP_JIT_DARWIN; + + if (prot == (MmapProts.PROT_READ | MmapProts.PROT_WRITE)) + { + prot |= MmapProts.PROT_EXEC; + } + } + + IntPtr ptr = mmap(IntPtr.Zero, size, prot, flags, -1, 0); + + if (ptr == new IntPtr(-1L)) + { + throw new OutOfMemoryException(); + } + + if (!_allocations.TryAdd(ptr, size)) + { + // This should be impossible, kernel shouldn't return an already mapped address. + throw new InvalidOperationException(); + } + + return ptr; + } + + public static bool Commit(IntPtr address, ulong size, bool forJit) + { + MmapProts prot = MmapProts.PROT_READ | MmapProts.PROT_WRITE; + + if (OperatingSystem.IsMacOSVersionAtLeast(10, 14) && forJit) + { + prot |= MmapProts.PROT_EXEC; + } + + return mprotect(address, size, prot) == 0; + } + + public static bool Decommit(IntPtr address, ulong size) + { + // Must be writable for madvise to work properly. + mprotect(address, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE); + + madvise(address, size, MADV_REMOVE); + + return mprotect(address, size, MmapProts.PROT_NONE) == 0; + } + + public static bool Reprotect(IntPtr address, ulong size, MemoryPermission permission) + { + return mprotect(address, size, GetProtection(permission)) == 0; + } + + private static MmapProts GetProtection(MemoryPermission permission) + { + return permission switch + { + MemoryPermission.None => MmapProts.PROT_NONE, + MemoryPermission.Read => MmapProts.PROT_READ, + MemoryPermission.ReadAndWrite => MmapProts.PROT_READ | MmapProts.PROT_WRITE, + MemoryPermission.ReadAndExecute => MmapProts.PROT_READ | MmapProts.PROT_EXEC, + MemoryPermission.ReadWriteExecute => MmapProts.PROT_READ | MmapProts.PROT_WRITE | MmapProts.PROT_EXEC, + MemoryPermission.Execute => MmapProts.PROT_EXEC, + _ => throw new MemoryProtectionException(permission) + }; + } + + public static bool Free(IntPtr address) + { + if (_allocations.TryRemove(address, out ulong size)) + { + return munmap(address, size) == 0; + } + + return false; + } + + public static bool Unmap(IntPtr address, ulong size) + { + return munmap(address, size) == 0; + } + + public unsafe static IntPtr CreateSharedMemory(ulong size, bool reserve) + { + int fd; + + if (OperatingSystem.IsMacOS()) + { + byte[] memName = Encoding.ASCII.GetBytes("Ryujinx-XXXXXX"); + + fixed (byte* pMemName = memName) + { + fd = shm_open((IntPtr)pMemName, 0x2 | 0x200 | 0x800 | 0x400, 384); // O_RDWR | O_CREAT | O_EXCL | O_TRUNC, 0600 + if (fd == -1) + { + throw new OutOfMemoryException(); + } + + if (shm_unlink((IntPtr)pMemName) != 0) + { + throw new OutOfMemoryException(); + } + } + } + else + { + byte[] fileName = Encoding.ASCII.GetBytes("/dev/shm/Ryujinx-XXXXXX"); + + fixed (byte* pFileName = fileName) + { + fd = mkstemp((IntPtr)pFileName); + if (fd == -1) + { + throw new OutOfMemoryException(); + } + + if (unlink((IntPtr)pFileName) != 0) + { + throw new OutOfMemoryException(); + } + } + } + + if (ftruncate(fd, (IntPtr)size) != 0) + { + throw new OutOfMemoryException(); + } + + return (IntPtr)fd; + } + + public static void DestroySharedMemory(IntPtr handle) + { + close((int)handle); + } + + public static IntPtr MapSharedMemory(IntPtr handle, ulong size) + { + return mmap(IntPtr.Zero, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, MmapFlags.MAP_SHARED, (int)handle, 0); + } + + public static void UnmapSharedMemory(IntPtr address, ulong size) + { + munmap(address, size); + } + + public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, ulong size) + { + mmap(location, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, MmapFlags.MAP_FIXED | MmapFlags.MAP_SHARED, (int)sharedMemory, (long)srcOffset); + } + + public static void UnmapView(IntPtr location, ulong size) + { + mmap(location, size, MmapProts.PROT_NONE, MmapFlags.MAP_FIXED | MmapFlags.MAP_PRIVATE | MmapFlags.MAP_ANONYMOUS | MmapFlags.MAP_NORESERVE, -1, 0); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/MemoryManagementWindows.cs b/src/Ryujinx.Memory/MemoryManagementWindows.cs new file mode 100644 index 00000000..2f89a921 --- /dev/null +++ b/src/Ryujinx.Memory/MemoryManagementWindows.cs @@ -0,0 +1,144 @@ +using Ryujinx.Memory.WindowsShared; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Memory +{ + [SupportedOSPlatform("windows")] + static class MemoryManagementWindows + { + public const int PageSize = 0x1000; + + private static readonly PlaceholderManager _placeholders = new PlaceholderManager(); + + public static IntPtr Allocate(IntPtr size) + { + return AllocateInternal(size, AllocationType.Reserve | AllocationType.Commit); + } + + public static IntPtr Reserve(IntPtr size, bool viewCompatible) + { + if (viewCompatible) + { + IntPtr baseAddress = AllocateInternal2(size, AllocationType.Reserve | AllocationType.ReservePlaceholder); + + _placeholders.ReserveRange((ulong)baseAddress, (ulong)size); + + return baseAddress; + } + + return AllocateInternal(size, AllocationType.Reserve); + } + + private static IntPtr AllocateInternal(IntPtr size, AllocationType flags = 0) + { + IntPtr ptr = WindowsApi.VirtualAlloc(IntPtr.Zero, size, flags, MemoryProtection.ReadWrite); + + if (ptr == IntPtr.Zero) + { + throw new OutOfMemoryException(); + } + + return ptr; + } + + private static IntPtr AllocateInternal2(IntPtr size, AllocationType flags = 0) + { + IntPtr ptr = WindowsApi.VirtualAlloc2(WindowsApi.CurrentProcessHandle, IntPtr.Zero, size, flags, MemoryProtection.NoAccess, IntPtr.Zero, 0); + + if (ptr == IntPtr.Zero) + { + throw new OutOfMemoryException(); + } + + return ptr; + } + + public static bool Commit(IntPtr location, IntPtr size) + { + return WindowsApi.VirtualAlloc(location, size, AllocationType.Commit, MemoryProtection.ReadWrite) != IntPtr.Zero; + } + + public static bool Decommit(IntPtr location, IntPtr size) + { + return WindowsApi.VirtualFree(location, size, AllocationType.Decommit); + } + + public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, MemoryBlock owner) + { + _placeholders.MapView(sharedMemory, srcOffset, location, size, owner); + } + + public static void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner) + { + _placeholders.UnmapView(sharedMemory, location, size, owner); + } + + public static bool Reprotect(IntPtr address, IntPtr size, MemoryPermission permission, bool forView) + { + if (forView) + { + return _placeholders.ReprotectView(address, size, permission); + } + else + { + return WindowsApi.VirtualProtect(address, size, WindowsApi.GetProtection(permission), out _); + } + } + + public static bool Free(IntPtr address, IntPtr size) + { + _placeholders.UnreserveRange((ulong)address, (ulong)size); + + return WindowsApi.VirtualFree(address, IntPtr.Zero, AllocationType.Release); + } + + public static IntPtr CreateSharedMemory(IntPtr size, bool reserve) + { + var prot = reserve ? FileMapProtection.SectionReserve : FileMapProtection.SectionCommit; + + IntPtr handle = WindowsApi.CreateFileMapping( + WindowsApi.InvalidHandleValue, + IntPtr.Zero, + FileMapProtection.PageReadWrite | prot, + (uint)(size.ToInt64() >> 32), + (uint)size.ToInt64(), + null); + + if (handle == IntPtr.Zero) + { + throw new OutOfMemoryException(); + } + + return handle; + } + + public static void DestroySharedMemory(IntPtr handle) + { + if (!WindowsApi.CloseHandle(handle)) + { + throw new ArgumentException("Invalid handle.", nameof(handle)); + } + } + + public static IntPtr MapSharedMemory(IntPtr handle) + { + IntPtr ptr = WindowsApi.MapViewOfFile(handle, 4 | 2, 0, 0, IntPtr.Zero); + + if (ptr == IntPtr.Zero) + { + throw new OutOfMemoryException(); + } + + return ptr; + } + + public static void UnmapSharedMemory(IntPtr address) + { + if (!WindowsApi.UnmapViewOfFile(address)) + { + throw new ArgumentException("Invalid address.", nameof(address)); + } + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs b/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs new file mode 100644 index 00000000..204f1ca4 --- /dev/null +++ b/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs @@ -0,0 +1,167 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Memory +{ + public static partial class MemoryManagerUnixHelper + { + [Flags] + public enum MmapProts : uint + { + PROT_NONE = 0, + PROT_READ = 1, + PROT_WRITE = 2, + PROT_EXEC = 4 + } + + [Flags] + public enum MmapFlags : uint + { + MAP_SHARED = 1, + MAP_PRIVATE = 2, + MAP_ANONYMOUS = 4, + MAP_NORESERVE = 8, + MAP_FIXED = 16, + MAP_UNLOCKED = 32, + MAP_JIT_DARWIN = 0x800 + } + + [Flags] + public enum OpenFlags : uint + { + O_RDONLY = 0, + O_WRONLY = 1, + O_RDWR = 2, + O_CREAT = 4, + O_EXCL = 8, + O_NOCTTY = 16, + O_TRUNC = 32, + O_APPEND = 64, + O_NONBLOCK = 128, + O_SYNC = 256, + } + + private const int MAP_ANONYMOUS_LINUX_GENERIC = 0x20; + private const int MAP_NORESERVE_LINUX_GENERIC = 0x4000; + private const int MAP_UNLOCKED_LINUX_GENERIC = 0x80000; + + private const int MAP_NORESERVE_DARWIN = 0x40; + private const int MAP_ANONYMOUS_DARWIN = 0x1000; + + public const int MADV_DONTNEED = 4; + public const int MADV_REMOVE = 9; + + [LibraryImport("libc", EntryPoint = "mmap", SetLastError = true)] + private static partial IntPtr Internal_mmap(IntPtr address, ulong length, MmapProts prot, int flags, int fd, long offset); + + [LibraryImport("libc", SetLastError = true)] + public static partial int mprotect(IntPtr address, ulong length, MmapProts prot); + + [LibraryImport("libc", SetLastError = true)] + public static partial int munmap(IntPtr address, ulong length); + + [LibraryImport("libc", SetLastError = true)] + public static partial IntPtr mremap(IntPtr old_address, ulong old_size, ulong new_size, int flags, IntPtr new_address); + + [LibraryImport("libc", SetLastError = true)] + public static partial int madvise(IntPtr address, ulong size, int advice); + + [LibraryImport("libc", SetLastError = true)] + public static partial int mkstemp(IntPtr template); + + [LibraryImport("libc", SetLastError = true)] + public static partial int unlink(IntPtr pathname); + + [LibraryImport("libc", SetLastError = true)] + public static partial int ftruncate(int fildes, IntPtr length); + + [LibraryImport("libc", SetLastError = true)] + public static partial int close(int fd); + + [LibraryImport("libc", SetLastError = true)] + public static partial int shm_open(IntPtr name, int oflag, uint mode); + + [LibraryImport("libc", SetLastError = true)] + public static partial int shm_unlink(IntPtr name); + + private static int MmapFlagsToSystemFlags(MmapFlags flags) + { + int result = 0; + + if (flags.HasFlag(MmapFlags.MAP_SHARED)) + { + result |= (int)MmapFlags.MAP_SHARED; + } + + if (flags.HasFlag(MmapFlags.MAP_PRIVATE)) + { + result |= (int)MmapFlags.MAP_PRIVATE; + } + + if (flags.HasFlag(MmapFlags.MAP_FIXED)) + { + result |= (int)MmapFlags.MAP_FIXED; + } + + if (flags.HasFlag(MmapFlags.MAP_ANONYMOUS)) + { + if (OperatingSystem.IsLinux()) + { + result |= MAP_ANONYMOUS_LINUX_GENERIC; + } + else if (OperatingSystem.IsMacOS()) + { + result |= MAP_ANONYMOUS_DARWIN; + } + else + { + throw new NotImplementedException(); + } + } + + if (flags.HasFlag(MmapFlags.MAP_NORESERVE)) + { + if (OperatingSystem.IsLinux()) + { + result |= MAP_NORESERVE_LINUX_GENERIC; + } + else if (OperatingSystem.IsMacOS()) + { + result |= MAP_NORESERVE_DARWIN; + } + else + { + throw new NotImplementedException(); + } + } + + if (flags.HasFlag(MmapFlags.MAP_UNLOCKED)) + { + if (OperatingSystem.IsLinux()) + { + result |= MAP_UNLOCKED_LINUX_GENERIC; + } + else if (OperatingSystem.IsMacOS()) + { + // FIXME: Doesn't exist on Darwin + } + else + { + throw new NotImplementedException(); + } + } + + if (flags.HasFlag(MmapFlags.MAP_JIT_DARWIN) && OperatingSystem.IsMacOSVersionAtLeast(10, 14)) + { + result |= (int)MmapFlags.MAP_JIT_DARWIN; + } + + return result; + } + + public static IntPtr mmap(IntPtr address, ulong length, MmapProts prot, MmapFlags flags, int fd, long offset) + { + return Internal_mmap(address, length, prot, MmapFlagsToSystemFlags(flags), fd, offset); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/MemoryMapFlags.cs b/src/Ryujinx.Memory/MemoryMapFlags.cs new file mode 100644 index 00000000..b4c74c8c --- /dev/null +++ b/src/Ryujinx.Memory/MemoryMapFlags.cs @@ -0,0 +1,23 @@ +using System; + +namespace Ryujinx.Memory +{ + /// <summary> + /// Flags that indicate how the host memory should be mapped. + /// </summary> + [Flags] + public enum MemoryMapFlags + { + /// <summary> + /// No mapping flags. + /// </summary> + None = 0, + + /// <summary> + /// Indicates that the implementation is free to ignore the specified backing memory offset + /// and allocate its own private storage for the mapping. + /// This allows some mappings that would otherwise fail due to host platform restrictions to succeed. + /// </summary> + Private = 1 << 0 + } +} diff --git a/src/Ryujinx.Memory/MemoryNotContiguousException.cs b/src/Ryujinx.Memory/MemoryNotContiguousException.cs new file mode 100644 index 00000000..3106955b --- /dev/null +++ b/src/Ryujinx.Memory/MemoryNotContiguousException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Ryujinx.Memory +{ + public class MemoryNotContiguousException : Exception + { + public MemoryNotContiguousException() : base("The specified memory region is not contiguous.") + { + } + + public MemoryNotContiguousException(string message) : base(message) + { + } + + public MemoryNotContiguousException(string message, Exception innerException) : base(message, innerException) + { + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/MemoryPermission.cs b/src/Ryujinx.Memory/MemoryPermission.cs new file mode 100644 index 00000000..8c3e33cf --- /dev/null +++ b/src/Ryujinx.Memory/MemoryPermission.cs @@ -0,0 +1,51 @@ +using System; + +namespace Ryujinx.Memory +{ + /// <summary> + /// Memory access permission control. + /// </summary> + [Flags] + public enum MemoryPermission + { + /// <summary> + /// No access is allowed on the memory region. + /// </summary> + None = 0, + + /// <summary> + /// Allow reads on the memory region. + /// </summary> + Read = 1 << 0, + + /// <summary> + /// Allow writes on the memory region. + /// </summary> + Write = 1 << 1, + + /// <summary> + /// Allow code execution on the memory region. + /// </summary> + Execute = 1 << 2, + + /// <summary> + /// Allow reads and writes on the memory region. + /// </summary> + ReadAndWrite = Read | Write, + + /// <summary> + /// Allow reads and code execution on the memory region. + /// </summary> + ReadAndExecute = Read | Execute, + + /// <summary> + /// Allow reads, writes, and code execution on the memory region. + /// </summary> + ReadWriteExecute = Read | Write | Execute, + + /// <summary> + /// Indicates an invalid protection. + /// </summary> + Invalid = 255 + } +} diff --git a/src/Ryujinx.Memory/MemoryProtectionException.cs b/src/Ryujinx.Memory/MemoryProtectionException.cs new file mode 100644 index 00000000..27e950a1 --- /dev/null +++ b/src/Ryujinx.Memory/MemoryProtectionException.cs @@ -0,0 +1,23 @@ +using System; + +namespace Ryujinx.Memory +{ + class MemoryProtectionException : Exception + { + public MemoryProtectionException() + { + } + + public MemoryProtectionException(MemoryPermission permission) : base($"Failed to set memory protection to \"{permission}\".") + { + } + + public MemoryProtectionException(string message) : base(message) + { + } + + public MemoryProtectionException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/Ryujinx.Memory/NativeMemoryManager.cs b/src/Ryujinx.Memory/NativeMemoryManager.cs new file mode 100644 index 00000000..d1757935 --- /dev/null +++ b/src/Ryujinx.Memory/NativeMemoryManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Buffers; + +namespace Ryujinx.Memory +{ + public unsafe class NativeMemoryManager<T> : MemoryManager<T> where T : unmanaged + { + private readonly T* _pointer; + private readonly int _length; + + public NativeMemoryManager(T* pointer, int length) + { + _pointer = pointer; + _length = length; + } + + public override Span<T> GetSpan() + { + return new Span<T>((void*)_pointer, _length); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + if ((uint)elementIndex >= _length) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + return new MemoryHandle((void*)(_pointer + elementIndex)); + } + + public override void Unpin() + { + // No need to do anything as pointer already points no native memory, not GC tracked. + } + + protected override void Dispose(bool disposing) + { + // Nothing to dispose, MemoryBlock still owns the memory. + } + } +} diff --git a/src/Ryujinx.Memory/PageTable.cs b/src/Ryujinx.Memory/PageTable.cs new file mode 100644 index 00000000..8fdedd4f --- /dev/null +++ b/src/Ryujinx.Memory/PageTable.cs @@ -0,0 +1,141 @@ +namespace Ryujinx.Memory +{ + public class PageTable<T> where T : unmanaged + { + public const int PageBits = 12; + public const int PageSize = 1 << PageBits; + public const int PageMask = PageSize - 1; + + private const int PtLevelBits = 9; // 9 * 4 + 12 = 48 (max address space size) + private const int PtLevelSize = 1 << PtLevelBits; + private const int PtLevelMask = PtLevelSize - 1; + + private readonly T[][][][] _pageTable; + + public PageTable() + { + _pageTable = new T[PtLevelSize][][][]; + } + + public T Read(ulong va) + { + int l3 = (int)(va >> PageBits) & PtLevelMask; + int l2 = (int)(va >> (PageBits + PtLevelBits)) & PtLevelMask; + int l1 = (int)(va >> (PageBits + PtLevelBits * 2)) & PtLevelMask; + int l0 = (int)(va >> (PageBits + PtLevelBits * 3)) & PtLevelMask; + + if (_pageTable[l0] == null) + { + return default; + } + + if (_pageTable[l0][l1] == null) + { + return default; + } + + if (_pageTable[l0][l1][l2] == null) + { + return default; + } + + return _pageTable[l0][l1][l2][l3]; + } + + public void Map(ulong va, T value) + { + int l3 = (int)(va >> PageBits) & PtLevelMask; + int l2 = (int)(va >> (PageBits + PtLevelBits)) & PtLevelMask; + int l1 = (int)(va >> (PageBits + PtLevelBits * 2)) & PtLevelMask; + int l0 = (int)(va >> (PageBits + PtLevelBits * 3)) & PtLevelMask; + + if (_pageTable[l0] == null) + { + _pageTable[l0] = new T[PtLevelSize][][]; + } + + if (_pageTable[l0][l1] == null) + { + _pageTable[l0][l1] = new T[PtLevelSize][]; + } + + if (_pageTable[l0][l1][l2] == null) + { + _pageTable[l0][l1][l2] = new T[PtLevelSize]; + } + + _pageTable[l0][l1][l2][l3] = value; + } + + public void Unmap(ulong va) + { + int l3 = (int)(va >> PageBits) & PtLevelMask; + int l2 = (int)(va >> (PageBits + PtLevelBits)) & PtLevelMask; + int l1 = (int)(va >> (PageBits + PtLevelBits * 2)) & PtLevelMask; + int l0 = (int)(va >> (PageBits + PtLevelBits * 3)) & PtLevelMask; + + if (_pageTable[l0] == null) + { + return; + } + + if (_pageTable[l0][l1] == null) + { + return; + } + + if (_pageTable[l0][l1][l2] == null) + { + return; + } + + _pageTable[l0][l1][l2][l3] = default; + + bool empty = true; + + for (int i = 0; i < _pageTable[l0][l1][l2].Length; i++) + { + empty &= _pageTable[l0][l1][l2][i].Equals(default); + } + + if (empty) + { + _pageTable[l0][l1][l2] = null; + + RemoveIfAllNull(l0, l1); + } + } + + private void RemoveIfAllNull(int l0, int l1) + { + bool empty = true; + + for (int i = 0; i < _pageTable[l0][l1].Length; i++) + { + empty &= (_pageTable[l0][l1][i] == null); + } + + if (empty) + { + _pageTable[l0][l1] = null; + + RemoveIfAllNull(l0); + } + } + + private void RemoveIfAllNull(int l0) + { + bool empty = true; + + for (int i = 0; i < _pageTable[l0].Length; i++) + { + empty &= (_pageTable[l0][i] == null); + } + + if (empty) + { + _pageTable[l0] = null; + } + } + } +} diff --git a/src/Ryujinx.Memory/Range/HostMemoryRange.cs b/src/Ryujinx.Memory/Range/HostMemoryRange.cs new file mode 100644 index 00000000..79c649d8 --- /dev/null +++ b/src/Ryujinx.Memory/Range/HostMemoryRange.cs @@ -0,0 +1,71 @@ +using System; + +namespace Ryujinx.Memory.Range +{ + /// <summary> + /// Range of memory composed of an address and size. + /// </summary> + public struct HostMemoryRange : IEquatable<HostMemoryRange> + { + /// <summary> + /// An empty memory range, with a null address and zero size. + /// </summary> + public static HostMemoryRange Empty => new HostMemoryRange(0, 0); + + /// <summary> + /// Start address of the range. + /// </summary> + public nuint Address { get; } + + /// <summary> + /// Size of the range in bytes. + /// </summary> + public ulong Size { get; } + + /// <summary> + /// Address where the range ends (exclusive). + /// </summary> + public nuint EndAddress => Address + (nuint)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 HostMemoryRange(nuint 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(HostMemoryRange other) + { + nuint thisAddress = Address; + nuint thisEndAddress = EndAddress; + nuint otherAddress = other.Address; + nuint otherEndAddress = other.EndAddress; + + return thisAddress < otherEndAddress && otherAddress < thisEndAddress; + } + + public override bool Equals(object obj) + { + return obj is HostMemoryRange other && Equals(other); + } + + public bool Equals(HostMemoryRange other) + { + return Address == other.Address && Size == other.Size; + } + + public override int GetHashCode() + { + return HashCode.Combine(Address, Size); + } + } +} diff --git a/src/Ryujinx.Memory/Range/IMultiRangeItem.cs b/src/Ryujinx.Memory/Range/IMultiRangeItem.cs new file mode 100644 index 00000000..e95a69fc --- /dev/null +++ b/src/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; + } +} diff --git a/src/Ryujinx.Memory/Range/INonOverlappingRange.cs b/src/Ryujinx.Memory/Range/INonOverlappingRange.cs new file mode 100644 index 00000000..1886eb1d --- /dev/null +++ b/src/Ryujinx.Memory/Range/INonOverlappingRange.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Memory.Range +{ + /// <summary> + /// Range of memory that can be split in two. + /// </summary> + interface INonOverlappingRange : IRange + { + /// <summary> + /// Split this region into two, around the specified address. + /// This region is updated to end at the split address, and a new region is created to represent past that point. + /// </summary> + /// <param name="splitAddress">Address to split the region around</param> + /// <returns>The second part of the split region, with start address at the given split.</returns> + public INonOverlappingRange Split(ulong splitAddress); + } +} diff --git a/src/Ryujinx.Memory/Range/IRange.cs b/src/Ryujinx.Memory/Range/IRange.cs new file mode 100644 index 00000000..1685396d --- /dev/null +++ b/src/Ryujinx.Memory/Range/IRange.cs @@ -0,0 +1,31 @@ +namespace Ryujinx.Memory.Range +{ + /// <summary> + /// Range of memory. + /// </summary> + public interface IRange + { + /// <summary> + /// Base address. + /// </summary> + ulong Address { get; } + + /// <summary> + /// Size of the range. + /// </summary> + ulong Size { get; } + + /// <summary> + /// End address. + /// </summary> + ulong EndAddress { get; } + + /// <summary> + /// Check if this range overlaps with another. + /// </summary> + /// <param name="address">Base address</param> + /// <param name="size">Size of the range</param> + /// <returns>True if overlapping, false otherwise</returns> + bool OverlapsWith(ulong address, ulong size); + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/Range/MemoryRange.cs b/src/Ryujinx.Memory/Range/MemoryRange.cs new file mode 100644 index 00000000..7465fbcb --- /dev/null +++ b/src/Ryujinx.Memory/Range/MemoryRange.cs @@ -0,0 +1,61 @@ +namespace Ryujinx.Memory.Range +{ + /// <summary> + /// Range of memory composed of an address and size. + /// </summary> + public readonly record struct 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; + + // 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/src/Ryujinx.Memory/Range/MultiRange.cs b/src/Ryujinx.Memory/Range/MultiRange.cs new file mode 100644 index 00000000..9dbd76ec --- /dev/null +++ b/src/Ryujinx.Memory/Range/MultiRange.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Memory.Range +{ + /// <summary> + /// Sequence of physical memory regions that a single non-contiguous virtual memory region maps to. + /// </summary> + public readonly struct MultiRange : IEquatable<MultiRange> + { + private const ulong InvalidAddress = ulong.MaxValue; + + 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> + /// 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; + } + + /// <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)); + } + + /// <summary> + /// Gets a slice of the multi-range. + /// </summary> + /// <param name="offset">Offset of the slice into the multi-range in bytes</param> + /// <param name="size">Size of the slice in bytes</param> + /// <returns>A new multi-range representing the given slice of this one</returns> + public MultiRange Slice(ulong offset, ulong size) + { + if (HasSingleRange) + { + if (_singleRange.Size - offset < size) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + + return new MultiRange(_singleRange.Address + offset, size); + } + else + { + var ranges = new List<MemoryRange>(); + + foreach (MemoryRange range in _ranges) + { + if ((long)offset <= 0) + { + ranges.Add(new MemoryRange(range.Address, Math.Min(size, range.Size))); + size -= range.Size; + } + else if (offset < range.Size) + { + ulong sliceSize = Math.Min(size, range.Size - offset); + + if (range.Address == InvalidAddress) + { + ranges.Add(new MemoryRange(range.Address, sliceSize)); + } + else + { + ranges.Add(new MemoryRange(range.Address + offset, sliceSize)); + } + + size -= sliceSize; + } + + if ((long)size <= 0) + { + break; + } + + offset -= range.Size; + } + + return new MultiRange(ranges.ToArray()); + } + } + + /// <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() + { + if (HasSingleRange) + { + return _singleRange.Size; + } + + 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(); + } + } +} diff --git a/src/Ryujinx.Memory/Range/MultiRangeList.cs b/src/Ryujinx.Memory/Range/MultiRangeList.cs new file mode 100644 index 00000000..5131889f --- /dev/null +++ b/src/Ryujinx.Memory/Range/MultiRangeList.cs @@ -0,0 +1,210 @@ +using Ryujinx.Common.Collections; +using System.Collections; +using System.Collections.Generic; + +namespace Ryujinx.Memory.Range +{ + public class MultiRangeList<T> : IEnumerable<T> where T : IMultiRangeItem + { + private readonly IntervalTree<ulong, T> _items; + + public int Count { get; private set; } + + /// <summary> + /// Creates a new range list. + /// </summary> + public MultiRangeList() + { + _items = new IntervalTree<ulong, T>(); + } + + /// <summary> + /// Adds a new item to the list. + /// </summary> + /// <param name="item">The item to be added</param> + public void Add(T item) + { + MultiRange range = item.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); + } + + Count++; + } + + /// <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) + { + MultiRange range = item.Range; + + int removed = 0; + + for (int i = 0; i < range.Count; i++) + { + var subrange = range.GetSubRange(i); + + if (IsInvalid(ref subrange)) + { + continue; + } + + removed += _items.Remove(subrange.Address, item); + } + + if (removed > 0) + { + // All deleted intervals are for the same item - the one we removed. + Count--; + } + + return removed > 0; + } + + /// <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 overlapCount = 0; + + 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); + } + + // Remove any duplicates, caused by items having multiple sub range nodes in the tree. + if (overlapCount > 1) + { + int insertPtr = 0; + for (int i = 0; i < overlapCount; i++) + { + T item = output[i]; + bool duplicate = false; + + for (int j = insertPtr - 1; j >= 0; j--) + { + if (item.Equals(output[j])) + { + duplicate = true; + break; + } + } + + if (!duplicate) + { + if (insertPtr != i) + { + output[insertPtr] = item; + } + + insertPtr++; + } + } + + overlapCount = insertPtr; + } + + 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> + /// <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 count = _items.Get(baseAddress, ref output); + + // Only output items with matching base address + int insertPtr = 0; + for (int i = 0; i < count; i++) + { + if (output[i].BaseAddress == baseAddress) + { + if (i != insertPtr) + { + output[insertPtr] = output[i]; + } + + insertPtr++; + } + } + + return insertPtr; + } + + private List<T> GetList() + { + var items = _items.AsList(); + var result = new List<T>(); + + foreach (RangeNode<ulong, T> item in items) + { + if (item.Start == item.Value.BaseAddress) + { + result.Add(item.Value); + } + } + + return result; + } + + public IEnumerator<T> GetEnumerator() + { + return GetList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetList().GetEnumerator(); + } + } +} diff --git a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs new file mode 100644 index 00000000..60b2b378 --- /dev/null +++ b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Memory.Range +{ + /// <summary> + /// A range list that assumes ranges are non-overlapping, with list items that can be split in two to avoid overlaps. + /// </summary> + /// <typeparam name="T">Type of the range.</typeparam> + class NonOverlappingRangeList<T> : RangeList<T> where T : INonOverlappingRange + { + /// <summary> + /// Finds a list of regions that cover the desired (address, size) range. + /// If this range starts or ends in the middle of an existing region, it is split and only the relevant part is added. + /// If there is no matching region, or there is a gap, then new regions are created with the factory. + /// Regions are added to the list in address ascending order. + /// </summary> + /// <param name="list">List to add found regions to</param> + /// <param name="address">Start address of the search region</param> + /// <param name="size">Size of the search region</param> + /// <param name="factory">Factory for creating new ranges</param> + public void GetOrAddRegions(List<T> list, ulong address, ulong size, Func<ulong, ulong, T> factory) + { + // (regarding the specific case this generalized function is used for) + // A new region may be split into multiple parts if multiple virtual regions have mapped to it. + // For instance, while a virtual mapping could cover 0-2 in physical space, the space 0-1 may have already been reserved... + // So we need to return both the split 0-1 and 1-2 ranges. + + var results = new T[1]; + int count = FindOverlapsNonOverlapping(address, size, ref results); + + if (count == 0) + { + // The region is fully unmapped. Create and add it to the range list. + T region = factory(address, size); + list.Add(region); + Add(region); + } + else + { + ulong lastAddress = address; + ulong endAddress = address + size; + + for (int i = 0; i < count; i++) + { + T region = results[i]; + if (count == 1 && region.Address == address && region.Size == size) + { + // Exact match, no splitting required. + list.Add(region); + return; + } + + if (lastAddress < region.Address) + { + // There is a gap between this region and the last. We need to fill it. + T fillRegion = factory(lastAddress, region.Address - lastAddress); + list.Add(fillRegion); + Add(fillRegion); + } + + if (region.Address < address) + { + // Split the region around our base address and take the high half. + + region = Split(region, address); + } + + if (region.EndAddress > address + size) + { + // Split the region around our end address and take the low half. + + Split(region, address + size); + } + + list.Add(region); + lastAddress = region.EndAddress; + } + + if (lastAddress < endAddress) + { + // There is a gap between this region and the end. We need to fill it. + T fillRegion = factory(lastAddress, endAddress - lastAddress); + list.Add(fillRegion); + Add(fillRegion); + } + } + } + + /// <summary> + /// Splits a region around a target point and updates the region list. + /// The original region's size is modified, but its address stays the same. + /// A new region starting from the split address is added to the region list and returned. + /// </summary> + /// <param name="region">The region to split</param> + /// <param name="splitAddress">The address to split with</param> + /// <returns>The new region (high part)</returns> + private T Split(T region, ulong splitAddress) + { + T newRegion = (T)region.Split(splitAddress); + Update(region); + Add(newRegion); + return newRegion; + } + } +} diff --git a/src/Ryujinx.Memory/Range/RangeList.cs b/src/Ryujinx.Memory/Range/RangeList.cs new file mode 100644 index 00000000..46919597 --- /dev/null +++ b/src/Ryujinx.Memory/Range/RangeList.cs @@ -0,0 +1,483 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Memory.Range +{ + /// <summary> + /// Sorted list of ranges that supports binary search. + /// </summary> + /// <typeparam name="T">Type of the range.</typeparam> + public class RangeList<T> : IEnumerable<T> where T : IRange + { + private readonly struct RangeItem<TValue> where TValue : IRange + { + public readonly ulong Address; + public readonly ulong EndAddress; + + public readonly TValue Value; + + public RangeItem(TValue value) + { + Value = value; + + Address = value.Address; + EndAddress = value.Address + value.Size; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool OverlapsWith(ulong address, ulong endAddress) + { + return Address < endAddress && address < EndAddress; + } + } + + private const int BackingInitialSize = 1024; + private const int ArrayGrowthSize = 32; + + private RangeItem<T>[] _items; + private readonly int _backingGrowthSize; + + public int Count { get; protected set; } + + /// <summary> + /// Creates a new range list. + /// </summary> + /// <param name="backingInitialSize">The initial size of the backing array</param> + public RangeList(int backingInitialSize = BackingInitialSize) + { + _backingGrowthSize = backingInitialSize; + _items = new RangeItem<T>[backingInitialSize]; + } + + /// <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.Address); + + if (index < 0) + { + index = ~index; + } + + Insert(index, new RangeItem<T>(item)); + } + + /// <summary> + /// Updates an item's end address on the list. Address must be the same. + /// </summary> + /// <param name="item">The item to be updated</param> + /// <returns>True if the item was located and updated, false otherwise</returns> + public bool Update(T item) + { + int index = BinarySearch(item.Address); + + if (index >= 0) + { + while (index > 0 && _items[index - 1].Address == item.Address) + { + index--; + } + + while (index < Count) + { + if (_items[index].Value.Equals(item)) + { + _items[index] = new RangeItem<T>(item); + + return true; + } + + if (_items[index].Address > item.Address) + { + break; + } + + index++; + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Insert(int index, RangeItem<T> item) + { + if (Count + 1 > _items.Length) + { + Array.Resize(ref _items, _items.Length + _backingGrowthSize); + } + + if (index >= Count) + { + if (index == Count) + { + _items[Count++] = item; + } + } + else + { + Array.Copy(_items, index, _items, index + 1, Count - index); + + _items[index] = item; + Count++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveAt(int index) + { + if (index < --Count) + { + Array.Copy(_items, index + 1, _items, index, Count - index); + } + } + + /// <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.Address); + + if (index >= 0) + { + while (index > 0 && _items[index - 1].Address == item.Address) + { + index--; + } + + while (index < Count) + { + if (_items[index].Value.Equals(item)) + { + RemoveAt(index); + + return true; + } + + if (_items[index].Address > item.Address) + { + break; + } + + index++; + } + } + + return false; + } + + /// <summary> + /// Updates an item's end address. + /// </summary> + /// <param name="item">The item to be updated</param> + public void UpdateEndAddress(T item) + { + int index = BinarySearch(item.Address); + + if (index >= 0) + { + while (index > 0 && _items[index - 1].Address == item.Address) + { + index--; + } + + while (index < Count) + { + if (_items[index].Value.Equals(item)) + { + _items[index] = new RangeItem<T>(item); + + return; + } + + if (_items[index].Address > item.Address) + { + break; + } + + index++; + } + } + } + + /// <summary> + /// Gets the first item on the list overlapping in memory with the specified item. + /// </summary> + /// <remarks> + /// Despite the name, this has no ordering guarantees of the returned item. + /// It only ensures that the item returned overlaps the specified item. + /// </remarks> + /// <param name="item">Item to check for overlaps</param> + /// <returns>The overlapping item, or the default value for the type if none found</returns> + public T FindFirstOverlap(T item) + { + return FindFirstOverlap(item.Address, item.Size); + } + + /// <summary> + /// Gets the first item on the list overlapping the specified memory range. + /// </summary> + /// <remarks> + /// Despite the name, this has no ordering guarantees of the returned item. + /// It only ensures that the item returned overlaps the specified memory range. + /// </remarks> + /// <param name="address">Start address of the range</param> + /// <param name="size">Size in bytes of the range</param> + /// <returns>The overlapping item, or the default value for the type if none found</returns> + public T FindFirstOverlap(ulong address, ulong size) + { + int index = BinarySearch(address, address + size); + + if (index < 0) + { + return default(T); + } + + return _items[index].Value; + } + + /// <summary> + /// Gets all items overlapping with the specified item in memory. + /// </summary> + /// <param name="item">Item to check for overlaps</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(T item, ref T[] output) + { + return FindOverlaps(item.Address, item.Size, ref output); + } + + /// <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) + { + int outputIndex = 0; + + ulong endAddress = address + size; + + for (int i = 0; i < Count; i++) + { + ref RangeItem<T> item = ref _items[i]; + + if (item.Address >= endAddress) + { + break; + } + + if (item.OverlapsWith(address, endAddress)) + { + if (outputIndex == output.Length) + { + Array.Resize(ref output, outputIndex + ArrayGrowthSize); + } + + output[outputIndex++] = item.Value; + } + } + + return outputIndex; + } + + /// <summary> + /// Gets all items overlapping with the specified item in memory. + /// </summary> + /// <remarks> + /// This method only returns correct results if none of the items on the list overlaps with + /// each other. If that is not the case, this method should not be used. + /// This method is faster than the regular method to find all overlaps. + /// </remarks> + /// <param name="item">Item to check for overlaps</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 FindOverlapsNonOverlapping(T item, ref T[] output) + { + return FindOverlapsNonOverlapping(item.Address, item.Size, ref output); + } + + /// <summary> + /// Gets all items on the list overlapping the specified memory range. + /// </summary> + /// <remarks> + /// This method only returns correct results if none of the items on the list overlaps with + /// each other. If that is not the case, this method should not be used. + /// This method is faster than the regular method to find all overlaps. + /// </remarks> + /// <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 FindOverlapsNonOverlapping(ulong address, ulong size, ref T[] output) + { + // This is a bit faster than FindOverlaps, but only works + // when none of the items on the list overlaps with each other. + int outputIndex = 0; + + ulong endAddress = address + size; + + int index = BinarySearch(address, endAddress); + + if (index >= 0) + { + while (index > 0 && _items[index - 1].OverlapsWith(address, endAddress)) + { + index--; + } + + do + { + if (outputIndex == output.Length) + { + Array.Resize(ref output, outputIndex + ArrayGrowthSize); + } + + output[outputIndex++] = _items[index++].Value; + } + while (index < Count && _items[index].OverlapsWith(address, endAddress)); + } + + return outputIndex; + } + + /// <summary> + /// Gets all items on the list with the specified memory address. + /// </summary> + /// <param name="address">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 address, ref T[] output) + { + int index = BinarySearch(address); + + int outputIndex = 0; + + if (index >= 0) + { + while (index > 0 && _items[index - 1].Address == address) + { + index--; + } + + while (index < Count) + { + ref RangeItem<T> overlap = ref _items[index++]; + + if (overlap.Address != address) + { + break; + } + + if (outputIndex == output.Length) + { + Array.Resize(ref output, outputIndex + ArrayGrowthSize); + } + + output[outputIndex++] = overlap.Value; + } + } + + 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 = Count - 1; + + while (left <= right) + { + int range = right - left; + + int middle = left + (range >> 1); + + ref RangeItem<T> item = ref _items[middle]; + + if (item.Address == address) + { + return middle; + } + + if (address < item.Address) + { + right = middle - 1; + } + else + { + left = middle + 1; + } + } + + return ~left; + } + + /// <summary> + /// Performs binary search for items overlapping a given memory range. + /// </summary> + /// <param name="address">Start address of the range</param> + /// <param name="endAddress">End address of the range</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, ulong endAddress) + { + int left = 0; + int right = Count - 1; + + while (left <= right) + { + int range = right - left; + + int middle = left + (range >> 1); + + ref RangeItem<T> item = ref _items[middle]; + + if (item.OverlapsWith(address, endAddress)) + { + return middle; + } + + if (address < item.Address) + { + right = middle - 1; + } + else + { + left = middle + 1; + } + } + + return ~left; + } + + public IEnumerator<T> GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return _items[i].Value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return _items[i].Value; + } + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/Ryujinx.Memory.csproj b/src/Ryujinx.Memory/Ryujinx.Memory.csproj new file mode 100644 index 00000000..91e46e48 --- /dev/null +++ b/src/Ryujinx.Memory/Ryujinx.Memory.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/Ryujinx.Memory/Tracking/AbstractRegion.cs b/src/Ryujinx.Memory/Tracking/AbstractRegion.cs new file mode 100644 index 00000000..bd4c8ab5 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/AbstractRegion.cs @@ -0,0 +1,73 @@ +using Ryujinx.Memory.Range; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// A region of memory. + /// </summary> + abstract class AbstractRegion : INonOverlappingRange + { + /// <summary> + /// Base address. + /// </summary> + public ulong Address { get; } + + /// <summary> + /// Size of the range in bytes. + /// </summary> + public ulong Size { get; protected set; } + + /// <summary> + /// End address. + /// </summary> + public ulong EndAddress => Address + Size; + + /// <summary> + /// Create a new region. + /// </summary> + /// <param name="address">Base address</param> + /// <param name="size">Size of the range</param> + protected AbstractRegion(ulong address, ulong size) + { + Address = address; + Size = size; + } + + /// <summary> + /// Check if this range overlaps with another. + /// </summary> + /// <param name="address">Base address</param> + /// <param name="size">Size of the range</param> + /// <returns>True if overlapping, false otherwise</returns> + public bool OverlapsWith(ulong address, ulong size) + { + return Address < address + size && address < EndAddress; + } + + /// <summary> + /// Signals to the handles that a memory event has occurred, and unprotects the region. Assumes that the tracking lock has been obtained. + /// </summary> + /// <param name="address">Address accessed</param> + /// <param name="size">Size of the region affected in bytes</param> + /// <param name="write">Whether the region was written to or read</param> + /// <param name="exemptId">Optional ID of the handles that should not be signalled</param> + public abstract void Signal(ulong address, ulong size, bool write, int? exemptId); + + /// <summary> + /// Signals to the handles that a precise memory event has occurred. Assumes that the tracking lock has been obtained. + /// </summary> + /// <param name="address">Address accessed</param> + /// <param name="size">Size of the region affected in bytes</param> + /// <param name="write">Whether the region was written to or read</param> + /// <param name="exemptId">Optional ID of the handles that should not be signalled</param> + public abstract void SignalPrecise(ulong address, ulong size, bool write, int? exemptId); + + /// <summary> + /// Split this region into two, around the specified address. + /// This region is updated to end at the split address, and a new region is created to represent past that point. + /// </summary> + /// <param name="splitAddress">Address to split the region around</param> + /// <returns>The second part of the split region, with start address at the given split.</returns> + public abstract INonOverlappingRange Split(ulong splitAddress); + } +} diff --git a/src/Ryujinx.Memory/Tracking/BitMap.cs b/src/Ryujinx.Memory/Tracking/BitMap.cs new file mode 100644 index 00000000..173952f3 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/BitMap.cs @@ -0,0 +1,199 @@ +using System.Runtime.CompilerServices; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// A bitmap that can check or set large ranges of true/false values at once. + /// </summary> + readonly struct BitMap + { + public const int IntSize = 64; + + private const int IntShift = 6; + private const int IntMask = IntSize - 1; + + /// <summary> + /// Masks representing the bitmap. Least significant bit first, 64-bits per mask. + /// </summary> + public readonly long[] Masks; + + /// <summary> + /// Create a new bitmap. + /// </summary> + /// <param name="count">The number of bits to reserve</param> + public BitMap(int count) + { + Masks = new long[(count + IntMask) / IntSize]; + } + + /// <summary> + /// Check if any bit in the bitmap is set. + /// </summary> + /// <returns>True if any bits are set, false otherwise</returns> + public bool AnySet() + { + for (int i = 0; i < Masks.Length; i++) + { + if (Masks[i] != 0) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Check if a bit in the bitmap is set. + /// </summary> + /// <param name="bit">The bit index to check</param> + /// <returns>True if the bit is set, false otherwise</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsSet(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + return (Masks[wordIndex] & wordMask) != 0; + } + + /// <summary> + /// Check if any bit in a range of bits in the bitmap are set. (inclusive) + /// </summary> + /// <param name="start">The first bit index to check</param> + /// <param name="end">The last bit index to check</param> + /// <returns>True if a bit is set, false otherwise</returns> + public bool IsSet(int start, int end) + { + if (start == end) + { + return IsSet(start); + } + + int startIndex = start >> IntShift; + int startBit = start & IntMask; + long startMask = -1L << startBit; + + int endIndex = end >> IntShift; + int endBit = end & IntMask; + long endMask = (long)(ulong.MaxValue >> (IntMask - endBit)); + + if (startIndex == endIndex) + { + return (Masks[startIndex] & startMask & endMask) != 0; + } + + if ((Masks[startIndex] & startMask) != 0) + { + return true; + } + + for (int i = startIndex + 1; i < endIndex; i++) + { + if (Masks[i] != 0) + { + return true; + } + } + + if ((Masks[endIndex] & endMask) != 0) + { + return true; + } + + return false; + } + + /// <summary> + /// Set a bit at a specific index to 1. + /// </summary> + /// <param name="bit">The bit index to set</param> + /// <returns>True if the bit is set, false if it was already set</returns> + public bool Set(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + if ((Masks[wordIndex] & wordMask) != 0) + { + return false; + } + + Masks[wordIndex] |= wordMask; + + return true; + } + + /// <summary> + /// Set a range of bits in the bitmap to 1. + /// </summary> + /// <param name="start">The first bit index to set</param> + /// <param name="end">The last bit index to set</param> + public void SetRange(int start, int end) + { + if (start == end) + { + Set(start); + return; + } + + int startIndex = start >> IntShift; + int startBit = start & IntMask; + long startMask = -1L << startBit; + + int endIndex = end >> IntShift; + int endBit = end & IntMask; + long endMask = (long)(ulong.MaxValue >> (IntMask - endBit)); + + if (startIndex == endIndex) + { + Masks[startIndex] |= startMask & endMask; + } + else + { + Masks[startIndex] |= startMask; + + for (int i = startIndex + 1; i < endIndex; i++) + { + Masks[i] |= -1; + } + + Masks[endIndex] |= endMask; + } + } + + /// <summary> + /// Clear a bit at a specific index to 0. + /// </summary> + /// <param name="bit">The bit index to clear</param> + /// <returns>True if the bit was set, false if it was not</returns> + public bool Clear(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + bool wasSet = (Masks[wordIndex] & wordMask) != 0; + + Masks[wordIndex] &= ~wordMask; + + return wasSet; + } + + /// <summary> + /// Clear the bitmap entirely, setting all bits to 0. + /// </summary> + public void Clear() + { + for (int i = 0; i < Masks.Length; i++) + { + Masks[i] = 0; + } + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/ConcurrentBitmap.cs b/src/Ryujinx.Memory/Tracking/ConcurrentBitmap.cs new file mode 100644 index 00000000..994fda92 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/ConcurrentBitmap.cs @@ -0,0 +1,152 @@ +using System; +using System.Threading; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// A bitmap that can be safely modified from multiple threads. + /// </summary> + internal class ConcurrentBitmap + { + public const int IntSize = 64; + + public const int IntShift = 6; + public const int IntMask = IntSize - 1; + + /// <summary> + /// Masks representing the bitmap. Least significant bit first, 64-bits per mask. + /// </summary> + public readonly long[] Masks; + + /// <summary> + /// Create a new multithreaded bitmap. + /// </summary> + /// <param name="count">The number of bits to reserve</param> + /// <param name="set">Whether the bits should be initially set or not</param> + public ConcurrentBitmap(int count, bool set) + { + Masks = new long[(count + IntMask) / IntSize]; + + if (set) + { + Array.Fill(Masks, -1L); + } + } + + /// <summary> + /// Check if any bit in the bitmap is set. + /// </summary> + /// <returns>True if any bits are set, false otherwise</returns> + public bool AnySet() + { + for (int i = 0; i < Masks.Length; i++) + { + if (Interlocked.Read(ref Masks[i]) != 0) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Check if a bit in the bitmap is set. + /// </summary> + /// <param name="bit">The bit index to check</param> + /// <returns>True if the bit is set, false otherwise</returns> + public bool IsSet(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + return (Interlocked.Read(ref Masks[wordIndex]) & wordMask) != 0; + } + + /// <summary> + /// Check if any bit in a range of bits in the bitmap are set. (inclusive) + /// </summary> + /// <param name="start">The first bit index to check</param> + /// <param name="end">The last bit index to check</param> + /// <returns>True if a bit is set, false otherwise</returns> + public bool IsSet(int start, int end) + { + if (start == end) + { + return IsSet(start); + } + + int startIndex = start >> IntShift; + int startBit = start & IntMask; + long startMask = -1L << startBit; + + int endIndex = end >> IntShift; + int endBit = end & IntMask; + long endMask = (long)(ulong.MaxValue >> (IntMask - endBit)); + + long startValue = Interlocked.Read(ref Masks[startIndex]); + + if (startIndex == endIndex) + { + return (startValue & startMask & endMask) != 0; + } + + if ((startValue & startMask) != 0) + { + return true; + } + + for (int i = startIndex + 1; i < endIndex; i++) + { + if (Interlocked.Read(ref Masks[i]) != 0) + { + return true; + } + } + + long endValue = Interlocked.Read(ref Masks[endIndex]); + + if ((endValue & endMask) != 0) + { + return true; + } + + return false; + } + + /// <summary> + /// Set a bit at a specific index to either true or false. + /// </summary> + /// <param name="bit">The bit index to set</param> + /// <param name="value">Whether the bit should be set or not</param> + public void Set(int bit, bool value) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + if (value) + { + Interlocked.Or(ref Masks[wordIndex], wordMask); + } + else + { + Interlocked.And(ref Masks[wordIndex], ~wordMask); + } + } + + /// <summary> + /// Clear the bitmap entirely, setting all bits to 0. + /// </summary> + public void Clear() + { + for (int i = 0; i < Masks.Length; i++) + { + Interlocked.Exchange(ref Masks[i], 0); + } + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/IMultiRegionHandle.cs b/src/Ryujinx.Memory/Tracking/IMultiRegionHandle.cs new file mode 100644 index 00000000..71bd602f --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/IMultiRegionHandle.cs @@ -0,0 +1,55 @@ +using System; + +namespace Ryujinx.Memory.Tracking +{ + public interface IMultiRegionHandle : IDisposable + { + /// <summary> + /// True if any write has occurred to the whole region since the last use of QueryModified (with no subregion specified). + /// </summary> + bool Dirty { get; } + + /// <summary> + /// Force the range of handles to be dirty, without reprotecting. + /// </summary> + /// <param name="address">Start address of the range</param> + /// <param name="size">Size of the range</param> + public void ForceDirty(ulong address, ulong size); + + /// <summary> + /// Check if any part of the region has been modified, and perform an action for each. + /// Contiguous modified regions are combined. + /// </summary> + /// <param name="modifiedAction">Action to perform for modified regions</param> + void QueryModified(Action<ulong, ulong> modifiedAction); + + + /// <summary> + /// Check if part of the region has been modified within a given range, and perform an action for each. + /// The range is aligned to the level of granularity of the contained handles. + /// Contiguous modified regions are combined. + /// </summary> + /// <param name="address">Start address of the range</param> + /// <param name="size">Size of the range</param> + /// <param name="modifiedAction">Action to perform for modified regions</param> + void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction); + + /// <summary> + /// Check if part of the region has been modified within a given range, and perform an action for each. + /// The sequence number provided is compared with each handle's saved sequence number. + /// If it is equal, then the handle's dirty flag is ignored. Otherwise, the sequence number is saved. + /// The range is aligned to the level of granularity of the contained handles. + /// Contiguous modified regions are combined. + /// </summary> + /// <param name="address">Start address of the range</param> + /// <param name="size">Size of the range</param> + /// <param name="modifiedAction">Action to perform for modified regions</param> + /// <param name="sequenceNumber">Current sequence number</param> + void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction, int sequenceNumber); + + /// <summary> + /// Signal that one of the subregions of this multi-region has been modified. This sets the overall dirty flag. + /// </summary> + void SignalWrite(); + } +} diff --git a/src/Ryujinx.Memory/Tracking/IRegionHandle.cs b/src/Ryujinx.Memory/Tracking/IRegionHandle.cs new file mode 100644 index 00000000..9d99d90e --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/IRegionHandle.cs @@ -0,0 +1,18 @@ +using System; + +namespace Ryujinx.Memory.Tracking +{ + public interface IRegionHandle : IDisposable + { + bool Dirty { get; } + + ulong Address { get; } + ulong Size { get; } + ulong EndAddress { get; } + + void ForceDirty(); + void Reprotect(bool asDirty = false); + void RegisterAction(RegionSignal action); + void RegisterPreciseAction(PreciseRegionSignal action); + } +} diff --git a/src/Ryujinx.Memory/Tracking/MemoryTracking.cs b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs new file mode 100644 index 00000000..bf1e0ad3 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs @@ -0,0 +1,306 @@ +using Ryujinx.Common.Pools; +using Ryujinx.Memory.Range; +using System.Collections.Generic; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// Manages memory tracking for a given virutal/physical memory block. + /// </summary> + public class MemoryTracking + { + private readonly IVirtualMemoryManager _memoryManager; + private readonly InvalidAccessHandler _invalidAccessHandler; + + // Only use these from within the lock. + private readonly NonOverlappingRangeList<VirtualRegion> _virtualRegions; + + private readonly int _pageSize; + + /// <summary> + /// This lock must be obtained when traversing or updating the region-handle hierarchy. + /// It is not required when reading dirty flags. + /// </summary> + internal object TrackingLock = new object(); + + /// <summary> + /// Create a new tracking structure for the given "physical" memory block, + /// with a given "virtual" memory manager that will provide mappings and virtual memory protection. + /// </summary> + /// <param name="memoryManager">Virtual memory manager</param> + /// <param name="block">Physical memory block</param> + /// <param name="pageSize">Page size of the virtual memory space</param> + public MemoryTracking(IVirtualMemoryManager memoryManager, int pageSize, InvalidAccessHandler invalidAccessHandler = null) + { + _memoryManager = memoryManager; + _pageSize = pageSize; + _invalidAccessHandler = invalidAccessHandler; + + _virtualRegions = new NonOverlappingRangeList<VirtualRegion>(); + } + + private (ulong address, ulong size) PageAlign(ulong address, ulong size) + { + ulong pageMask = (ulong)_pageSize - 1; + ulong rA = address & ~pageMask; + ulong rS = ((address + size + pageMask) & ~pageMask) - rA; + return (rA, rS); + } + + /// <summary> + /// Indicate that a virtual region has been mapped, and which physical region it has been mapped to. + /// Should be called after the mapping is complete. + /// </summary> + /// <param name="va">Virtual memory address</param> + /// <param name="size">Size to be mapped</param> + public void Map(ulong va, ulong size) + { + // A mapping may mean we need to re-evaluate each VirtualRegion's affected area. + // Find all handles that overlap with the range, we need to recalculate their physical regions + + lock (TrackingLock) + { + ref var overlaps = ref ThreadStaticArray<VirtualRegion>.Get(); + + int count = _virtualRegions.FindOverlapsNonOverlapping(va, size, ref overlaps); + + for (int i = 0; i < count; i++) + { + VirtualRegion region = overlaps[i]; + + // If the region has been fully remapped, signal that it has been mapped again. + bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size); + if (remapped) + { + region.SignalMappingChanged(true); + } + + region.UpdateProtection(); + } + } + } + + /// <summary> + /// Indicate that a virtual region has been unmapped. + /// Should be called before the unmapping is complete. + /// </summary> + /// <param name="va">Virtual memory address</param> + /// <param name="size">Size to be unmapped</param> + public void Unmap(ulong va, ulong size) + { + // An unmapping may mean we need to re-evaluate each VirtualRegion's affected area. + // Find all handles that overlap with the range, we need to notify them that the region was unmapped. + + lock (TrackingLock) + { + ref var overlaps = ref ThreadStaticArray<VirtualRegion>.Get(); + + int count = _virtualRegions.FindOverlapsNonOverlapping(va, size, ref overlaps); + + for (int i = 0; i < count; i++) + { + VirtualRegion region = overlaps[i]; + + region.SignalMappingChanged(false); + } + } + } + + /// <summary> + /// Get a list of virtual regions that a handle covers. + /// </summary> + /// <param name="va">Starting virtual memory address of the handle</param> + /// <param name="size">Size of the handle's memory region</param> + /// <returns>A list of virtual regions within the given range</returns> + internal List<VirtualRegion> GetVirtualRegionsForHandle(ulong va, ulong size) + { + List<VirtualRegion> result = new List<VirtualRegion>(); + _virtualRegions.GetOrAddRegions(result, va, size, (va, size) => new VirtualRegion(this, va, size)); + + return result; + } + + /// <summary> + /// Remove a virtual region from the range list. This assumes that the lock has been acquired. + /// </summary> + /// <param name="region">Region to remove</param> + internal void RemoveVirtual(VirtualRegion region) + { + _virtualRegions.Remove(region); + } + + /// <summary> + /// Obtains a memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with. + /// </summary> + /// <param name="address">CPU virtual address of the region</param> + /// <param name="size">Size of the region</param> + /// <param name="handles">Handles to inherit state from or reuse. When none are present, provide null</param> + /// <param name="granularity">Desired granularity of write tracking</param> + /// <param name="id">Handle ID</param> + /// <returns>The memory tracking handle</returns> + public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id) + { + return new MultiRegionHandle(this, address, size, handles, granularity, id); + } + + /// <summary> + /// Obtains a smart memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with. + /// </summary> + /// <param name="address">CPU virtual address of the region</param> + /// <param name="size">Size of the region</param> + /// <param name="granularity">Desired granularity of write tracking</param> + /// <param name="id">Handle ID</param> + /// <returns>The memory tracking handle</returns> + public SmartMultiRegionHandle BeginSmartGranularTracking(ulong address, ulong size, ulong granularity, int id) + { + (address, size) = PageAlign(address, size); + + return new SmartMultiRegionHandle(this, address, size, granularity, id); + } + + /// <summary> + /// Obtains a memory tracking handle for the given virtual region. This should be disposed when finished with. + /// </summary> + /// <param name="address">CPU virtual address of the region</param> + /// <param name="size">Size of the region</param> + /// <param name="id">Handle ID</param> + /// <returns>The memory tracking handle</returns> + public RegionHandle BeginTracking(ulong address, ulong size, int id) + { + var (paAddress, paSize) = PageAlign(address, size); + + lock (TrackingLock) + { + bool mapped = _memoryManager.IsRangeMapped(address, size); + RegionHandle handle = new RegionHandle(this, paAddress, paSize, address, size, id, mapped); + + return handle; + } + } + + /// <summary> + /// Obtains a memory tracking handle for the given virtual region. This should be disposed when finished with. + /// </summary> + /// <param name="address">CPU virtual address of the region</param> + /// <param name="size">Size of the region</param> + /// <param name="bitmap">The bitmap owning the dirty flag for this handle</param> + /// <param name="bit">The bit of this handle within the dirty flag</param> + /// <param name="id">Handle ID</param> + /// <returns>The memory tracking handle</returns> + internal RegionHandle BeginTrackingBitmap(ulong address, ulong size, ConcurrentBitmap bitmap, int bit, int id) + { + var (paAddress, paSize) = PageAlign(address, size); + + lock (TrackingLock) + { + bool mapped = _memoryManager.IsRangeMapped(address, size); + RegionHandle handle = new RegionHandle(this, paAddress, paSize, address, size, bitmap, bit, id, mapped); + + return handle; + } + } + + /// <summary> + /// Signal that a virtual memory event happened at the given location. + /// </summary> + /// <param name="address">Virtual address accessed</param> + /// <param name="size">Size of the region affected in bytes</param> + /// <param name="write">Whether the region was written to or read</param> + /// <returns>True if the event triggered any tracking regions, false otherwise</returns> + public bool VirtualMemoryEvent(ulong address, ulong size, bool write) + { + return VirtualMemoryEvent(address, size, write, precise: false, null); + } + + /// <summary> + /// Signal that a virtual memory event happened at the given location. + /// This can be flagged as a precise event, which will avoid reprotection and call special handlers if possible. + /// A precise event has an exact address and size, rather than triggering on page granularity. + /// </summary> + /// <param name="address">Virtual address accessed</param> + /// <param name="size">Size of the region affected in bytes</param> + /// <param name="write">Whether the region was written to or read</param> + /// <param name="precise">True if the access is precise, false otherwise</param> + /// <param name="exemptId">Optional ID that of the handles that should not be signalled</param> + /// <returns>True if the event triggered any tracking regions, false otherwise</returns> + public bool VirtualMemoryEvent(ulong address, ulong size, bool write, bool precise, int? exemptId = null) + { + // Look up the virtual region using the region list. + // Signal up the chain to relevant handles. + + bool shouldThrow = false; + + lock (TrackingLock) + { + ref var overlaps = ref ThreadStaticArray<VirtualRegion>.Get(); + + int count = _virtualRegions.FindOverlapsNonOverlapping(address, size, ref overlaps); + + if (count == 0 && !precise) + { + if (_memoryManager.IsRangeMapped(address, size)) + { + // TODO: There is currently the possibility that a page can be protected after its virtual region is removed. + // This code handles that case when it happens, but it would be better to find out how this happens. + _memoryManager.TrackingReprotect(address & ~(ulong)(_pageSize - 1), (ulong)_pageSize, MemoryPermission.ReadAndWrite); + return true; // This memory _should_ be mapped, so we need to try again. + } + else + { + shouldThrow = true; + } + } + else + { + for (int i = 0; i < count; i++) + { + VirtualRegion region = overlaps[i]; + + if (precise) + { + region.SignalPrecise(address, size, write, exemptId); + } + else + { + region.Signal(address, size, write, exemptId); + } + } + } + } + + if (shouldThrow) + { + _invalidAccessHandler?.Invoke(address); + + // We can't continue - it's impossible to remove protection from the page. + // Even if the access handler wants us to continue, we wouldn't be able to. + throw new InvalidMemoryRegionException(); + } + + return true; + } + + /// <summary> + /// Reprotect a given virtual region. The virtual memory manager will handle this. + /// </summary> + /// <param name="region">Region to reprotect</param> + /// <param name="permission">Memory permission to protect with</param> + internal void ProtectVirtualRegion(VirtualRegion region, MemoryPermission permission) + { + _memoryManager.TrackingReprotect(region.Address, region.Size, permission); + } + + /// <summary> + /// Returns the number of virtual regions currently being tracked. + /// Useful for tests and metrics. + /// </summary> + /// <returns>The number of virtual regions</returns> + public int GetRegionCount() + { + lock (TrackingLock) + { + return _virtualRegions.Count; + } + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/MultiRegionHandle.cs b/src/Ryujinx.Memory/Tracking/MultiRegionHandle.cs new file mode 100644 index 00000000..68fc5e75 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/MultiRegionHandle.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// A region handle that tracks a large region using many smaller handles, to provide + /// granular tracking that can be used to track partial updates. Backed by a bitmap + /// to improve performance when scanning large regions. + /// </summary> + public class MultiRegionHandle : IMultiRegionHandle + { + /// <summary> + /// A list of region handles for each granularity sized chunk of the whole region. + /// </summary> + private readonly RegionHandle[] _handles; + private readonly ulong Address; + private readonly ulong Granularity; + private readonly ulong Size; + + private ConcurrentBitmap _dirtyBitmap; + + private int _sequenceNumber; + private BitMap _sequenceNumberBitmap; + private BitMap _dirtyCheckedBitmap; + private int _uncheckedHandles; + + public bool Dirty { get; private set; } = true; + + internal MultiRegionHandle( + MemoryTracking tracking, + ulong address, + ulong size, + IEnumerable<IRegionHandle> handles, + ulong granularity, + int id) + { + _handles = new RegionHandle[(size + granularity - 1) / granularity]; + Granularity = granularity; + + _dirtyBitmap = new ConcurrentBitmap(_handles.Length, true); + _sequenceNumberBitmap = new BitMap(_handles.Length); + _dirtyCheckedBitmap = new BitMap(_handles.Length); + + int i = 0; + + if (handles != null) + { + // Inherit from the handles we were given. Any gaps must be filled with new handles, + // and old handles larger than our granularity must copy their state onto new granular handles and dispose. + // It is assumed that the provided handles do not overlap, in order, are on page boundaries, + // and don't extend past the requested range. + + foreach (RegionHandle handle in handles) + { + int startIndex = (int)((handle.RealAddress - address) / granularity); + + // Fill any gap left before this handle. + while (i < startIndex) + { + RegionHandle fillHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id); + fillHandle.Parent = this; + _handles[i++] = fillHandle; + } + + lock (tracking.TrackingLock) + { + if (handle is RegionHandle bitHandle && handle.Size == granularity) + { + handle.Parent = this; + + bitHandle.ReplaceBitmap(_dirtyBitmap, i); + + _handles[i++] = bitHandle; + } + else + { + int endIndex = (int)((handle.RealEndAddress - address) / granularity); + + while (i < endIndex) + { + RegionHandle splitHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id); + splitHandle.Parent = this; + + splitHandle.Reprotect(handle.Dirty); + + RegionSignal signal = handle.PreAction; + if (signal != null) + { + splitHandle.RegisterAction(signal); + } + + _handles[i++] = splitHandle; + } + + handle.Dispose(); + } + } + } + } + + // Fill any remaining space with new handles. + while (i < _handles.Length) + { + RegionHandle handle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id); + handle.Parent = this; + _handles[i++] = handle; + } + + _uncheckedHandles = _handles.Length; + + Address = address; + Size = size; + } + + public void SignalWrite() + { + Dirty = true; + } + + public IEnumerable<RegionHandle> GetHandles() + { + return _handles; + } + + public void ForceDirty(ulong address, ulong size) + { + Dirty = true; + + int startHandle = (int)((address - Address) / Granularity); + int lastHandle = (int)((address + (size - 1) - Address) / Granularity); + + for (int i = startHandle; i <= lastHandle; i++) + { + if (_sequenceNumberBitmap.Clear(i)) + { + _uncheckedHandles++; + } + + _handles[i].ForceDirty(); + } + } + + public void QueryModified(Action<ulong, ulong> modifiedAction) + { + if (!Dirty) + { + return; + } + + Dirty = false; + + QueryModified(Address, Size, modifiedAction); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ParseDirtyBits(long dirtyBits, ref int baseBit, ref int prevHandle, ref ulong rgStart, ref ulong rgSize, Action<ulong, ulong> modifiedAction) + { + while (dirtyBits != 0) + { + int bit = BitOperations.TrailingZeroCount(dirtyBits); + + dirtyBits &= ~(1L << bit); + + int handleIndex = baseBit + bit; + + RegionHandle handle = _handles[handleIndex]; + + if (handleIndex != prevHandle + 1) + { + // Submit handles scanned until the gap as dirty + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + rgSize = 0; + } + + rgStart = handle.RealAddress; + } + + if (handle.Dirty) + { + rgSize += handle.RealSize; + handle.Reprotect(); + } + + prevHandle = handleIndex; + } + + baseBit += ConcurrentBitmap.IntSize; + } + + public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction) + { + int startHandle = (int)((address - Address) / Granularity); + int lastHandle = (int)((address + (size - 1) - Address) / Granularity); + + ulong rgStart = Address + (ulong)startHandle * Granularity; + + if (startHandle == lastHandle) + { + RegionHandle handle = _handles[startHandle]; + + if (handle.Dirty) + { + handle.Reprotect(); + modifiedAction(rgStart, handle.RealSize); + } + + return; + } + + ulong rgSize = 0; + + long[] masks = _dirtyBitmap.Masks; + + int startIndex = startHandle >> ConcurrentBitmap.IntShift; + int startBit = startHandle & ConcurrentBitmap.IntMask; + long startMask = -1L << startBit; + + int endIndex = lastHandle >> ConcurrentBitmap.IntShift; + int endBit = lastHandle & ConcurrentBitmap.IntMask; + long endMask = (long)(ulong.MaxValue >> (ConcurrentBitmap.IntMask - endBit)); + + long startValue = Volatile.Read(ref masks[startIndex]); + + int baseBit = startIndex << ConcurrentBitmap.IntShift; + int prevHandle = startHandle - 1; + + if (startIndex == endIndex) + { + ParseDirtyBits(startValue & startMask & endMask, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + } + else + { + ParseDirtyBits(startValue & startMask, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + + for (int i = startIndex + 1; i < endIndex; i++) + { + ParseDirtyBits(Volatile.Read(ref masks[i]), ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + } + + long endValue = Volatile.Read(ref masks[endIndex]); + + ParseDirtyBits(endValue & endMask, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + } + + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ParseDirtyBits(long dirtyBits, long mask, int index, long[] seqMasks, long[] checkMasks, ref int baseBit, ref int prevHandle, ref ulong rgStart, ref ulong rgSize, Action<ulong, ulong> modifiedAction) + { + long seqMask = mask & ~seqMasks[index]; + long checkMask = (~dirtyBits) & seqMask; + dirtyBits &= seqMask; + + while (dirtyBits != 0) + { + int bit = BitOperations.TrailingZeroCount(dirtyBits); + long bitValue = 1L << bit; + + dirtyBits &= ~bitValue; + + int handleIndex = baseBit + bit; + + RegionHandle handle = _handles[handleIndex]; + + if (handleIndex != prevHandle + 1) + { + // Submit handles scanned until the gap as dirty + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + rgSize = 0; + } + rgStart = handle.RealAddress; + } + + rgSize += handle.RealSize; + handle.Reprotect(false, (checkMasks[index] & bitValue) == 0); + + checkMasks[index] &= ~bitValue; + + prevHandle = handleIndex; + } + + checkMasks[index] |= checkMask; + seqMasks[index] |= mask; + _uncheckedHandles -= BitOperations.PopCount((ulong)seqMask); + + baseBit += ConcurrentBitmap.IntSize; + } + + public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction, int sequenceNumber) + { + int startHandle = (int)((address - Address) / Granularity); + int lastHandle = (int)((address + (size - 1) - Address) / Granularity); + + ulong rgStart = Address + (ulong)startHandle * Granularity; + + if (sequenceNumber != _sequenceNumber) + { + if (_uncheckedHandles != _handles.Length) + { + _sequenceNumberBitmap.Clear(); + _uncheckedHandles = _handles.Length; + } + + _sequenceNumber = sequenceNumber; + } + + if (startHandle == lastHandle) + { + var handle = _handles[startHandle]; + if (_sequenceNumberBitmap.Set(startHandle)) + { + _uncheckedHandles--; + + if (handle.DirtyOrVolatile()) + { + handle.Reprotect(); + + modifiedAction(rgStart, handle.RealSize); + } + } + + return; + } + + if (_uncheckedHandles == 0) + { + return; + } + + ulong rgSize = 0; + + long[] seqMasks = _sequenceNumberBitmap.Masks; + long[] checkedMasks = _dirtyCheckedBitmap.Masks; + long[] masks = _dirtyBitmap.Masks; + + int startIndex = startHandle >> ConcurrentBitmap.IntShift; + int startBit = startHandle & ConcurrentBitmap.IntMask; + long startMask = -1L << startBit; + + int endIndex = lastHandle >> ConcurrentBitmap.IntShift; + int endBit = lastHandle & ConcurrentBitmap.IntMask; + long endMask = (long)(ulong.MaxValue >> (ConcurrentBitmap.IntMask - endBit)); + + long startValue = Volatile.Read(ref masks[startIndex]); + + int baseBit = startIndex << ConcurrentBitmap.IntShift; + int prevHandle = startHandle - 1; + + if (startIndex == endIndex) + { + ParseDirtyBits(startValue, startMask & endMask, startIndex, seqMasks, checkedMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + } + else + { + ParseDirtyBits(startValue, startMask, startIndex, seqMasks, checkedMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + + for (int i = startIndex + 1; i < endIndex; i++) + { + ParseDirtyBits(Volatile.Read(ref masks[i]), -1L, i, seqMasks, checkedMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + } + + long endValue = Volatile.Read(ref masks[endIndex]); + + ParseDirtyBits(endValue, endMask, endIndex, seqMasks, checkedMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction); + } + + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + } + } + + public void RegisterAction(ulong address, ulong size, RegionSignal action) + { + int startHandle = (int)((address - Address) / Granularity); + int lastHandle = (int)((address + (size - 1) - Address) / Granularity); + + for (int i = startHandle; i <= lastHandle; i++) + { + _handles[i].RegisterAction(action); + } + } + + public void RegisterPreciseAction(ulong address, ulong size, PreciseRegionSignal action) + { + int startHandle = (int)((address - Address) / Granularity); + int lastHandle = (int)((address + (size - 1) - Address) / Granularity); + + for (int i = startHandle; i <= lastHandle; i++) + { + _handles[i].RegisterPreciseAction(action); + } + } + + public void Dispose() + { + foreach (var handle in _handles) + { + handle.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/PreciseRegionSignal.cs b/src/Ryujinx.Memory/Tracking/PreciseRegionSignal.cs new file mode 100644 index 00000000..038f9595 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/PreciseRegionSignal.cs @@ -0,0 +1,4 @@ +namespace Ryujinx.Memory.Tracking +{ + public delegate bool PreciseRegionSignal(ulong address, ulong size, bool write); +} diff --git a/src/Ryujinx.Memory/Tracking/RegionHandle.cs b/src/Ryujinx.Memory/Tracking/RegionHandle.cs new file mode 100644 index 00000000..7a59f9f2 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/RegionHandle.cs @@ -0,0 +1,464 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// A tracking handle for a given region of virtual memory. The Dirty flag is updated whenever any changes are made, + /// and an action can be performed when the region is read to or written from. + /// </summary> + public class RegionHandle : IRegionHandle + { + /// <summary> + /// If more than this number of checks have been performed on a dirty flag since its last reprotect, + /// then it is dirtied infrequently. + /// </summary> + private const int CheckCountForInfrequent = 3; + + /// <summary> + /// Number of frequent dirty/consume in a row to make this handle volatile. + /// </summary> + private const int VolatileThreshold = 5; + + public bool Dirty + { + get + { + return Bitmap.IsSet(DirtyBit); + } + protected set + { + Bitmap.Set(DirtyBit, value); + } + } + + internal int SequenceNumber { get; set; } + internal int Id { get; } + + public bool Unmapped { get; private set; } + + public ulong Address { get; } + public ulong Size { get; } + public ulong EndAddress { get; } + + public ulong RealAddress { get; } + public ulong RealSize { get; } + public ulong RealEndAddress { get; } + + internal IMultiRegionHandle Parent { get; set; } + + private event Action _onDirty; + + private object _preActionLock = new object(); + private RegionSignal _preAction; // Action to perform before a read or write. This will block the memory access. + private PreciseRegionSignal _preciseAction; // Action to perform on a precise read or write. + private readonly List<VirtualRegion> _regions; + private readonly MemoryTracking _tracking; + private bool _disposed; + + private int _checkCount = 0; + private int _volatileCount = 0; + private bool _volatile; + + internal MemoryPermission RequiredPermission + { + get + { + // If this is unmapped, allow reprotecting as RW as it can't be dirtied. + // This is required for the partial unmap cases where part of the data are still being accessed. + if (Unmapped) + { + return MemoryPermission.ReadAndWrite; + } + + if (_preAction != null) + { + return MemoryPermission.None; + } + + return Dirty ? MemoryPermission.ReadAndWrite : MemoryPermission.Read; + } + } + + internal RegionSignal PreAction => _preAction; + + internal ConcurrentBitmap Bitmap; + internal int DirtyBit; + + /// <summary> + /// Create a new bitmap backed region handle. The handle is registered with the given tracking object, + /// and will be notified of any changes to the specified region. + /// </summary> + /// <param name="tracking">Tracking object for the target memory block</param> + /// <param name="address">Virtual address of the region to track</param> + /// <param name="size">Size of the region to track</param> + /// <param name="realAddress">The real, unaligned address of the handle</param> + /// <param name="realSize">The real, unaligned size of the handle</param> + /// <param name="bitmap">The bitmap the dirty flag for this handle is stored in</param> + /// <param name="bit">The bit index representing the dirty flag for this handle</param> + /// <param name="id">Handle ID</param> + /// <param name="mapped">True if the region handle starts mapped</param> + internal RegionHandle( + MemoryTracking tracking, + ulong address, + ulong size, + ulong realAddress, + ulong realSize, + ConcurrentBitmap bitmap, + int bit, + int id, + bool mapped = true) + { + Bitmap = bitmap; + DirtyBit = bit; + + Dirty = mapped; + + Id = id; + + Unmapped = !mapped; + Address = address; + Size = size; + EndAddress = address + size; + + RealAddress = realAddress; + RealSize = realSize; + RealEndAddress = realAddress + realSize; + + _tracking = tracking; + _regions = tracking.GetVirtualRegionsForHandle(address, size); + foreach (var region in _regions) + { + region.Handles.Add(this); + } + } + + /// <summary> + /// Create a new region handle. The handle is registered with the given tracking object, + /// and will be notified of any changes to the specified region. + /// </summary> + /// <param name="tracking">Tracking object for the target memory block</param> + /// <param name="address">Virtual address of the region to track</param> + /// <param name="size">Size of the region to track</param> + /// <param name="realAddress">The real, unaligned address of the handle</param> + /// <param name="realSize">The real, unaligned size of the handle</param> + /// <param name="id">Handle ID</param> + /// <param name="mapped">True if the region handle starts mapped</param> + internal RegionHandle(MemoryTracking tracking, ulong address, ulong size, ulong realAddress, ulong realSize, int id, bool mapped = true) + { + Bitmap = new ConcurrentBitmap(1, mapped); + + Id = id; + + Unmapped = !mapped; + + Address = address; + Size = size; + EndAddress = address + size; + + RealAddress = realAddress; + RealSize = realSize; + RealEndAddress = realAddress + realSize; + + _tracking = tracking; + _regions = tracking.GetVirtualRegionsForHandle(address, size); + foreach (var region in _regions) + { + region.Handles.Add(this); + } + } + + /// <summary> + /// Replace the bitmap and bit index used to track dirty state. + /// </summary> + /// <remarks> + /// The tracking lock should be held when this is called, to ensure neither bitmap is modified. + /// </remarks> + /// <param name="bitmap">The bitmap the dirty flag for this handle is stored in</param> + /// <param name="bit">The bit index representing the dirty flag for this handle</param> + internal void ReplaceBitmap(ConcurrentBitmap bitmap, int bit) + { + // Assumes the tracking lock is held, so nothing else can signal right now. + + var oldBitmap = Bitmap; + var oldBit = DirtyBit; + + bitmap.Set(bit, Dirty); + + Bitmap = bitmap; + DirtyBit = bit; + + Dirty |= oldBitmap.IsSet(oldBit); + } + + /// <summary> + /// Clear the volatile state of this handle. + /// </summary> + private void ClearVolatile() + { + _volatileCount = 0; + _volatile = false; + } + + /// <summary> + /// Check if this handle is dirty, or if it is volatile. (changes very often) + /// </summary> + /// <returns>True if the handle is dirty or volatile, false otherwise</returns> + public bool DirtyOrVolatile() + { + _checkCount++; + return _volatile || Dirty; + } + + /// <summary> + /// Signal that a memory action occurred within this handle's virtual regions. + /// </summary> + /// <param name="address">Address accessed</param> + /// <param name="size">Size of the region affected in bytes</param> + /// <param name="write">Whether the region was written to or read</param> + /// <param name="handleIterable">Reference to the handles being iterated, in case the list needs to be copied</param> + internal void Signal(ulong address, ulong size, bool write, ref IList<RegionHandle> handleIterable) + { + // If this handle was already unmapped (even if just partially), + // then we have nothing to do until it is mapped again. + // The pre-action should be still consumed to avoid flushing on remap. + if (Unmapped) + { + Interlocked.Exchange(ref _preAction, null); + return; + } + + if (_preAction != null) + { + // Limit the range to within this handle. + ulong maxAddress = Math.Max(address, RealAddress); + ulong minEndAddress = Math.Min(address + size, RealAddress + RealSize); + + // Copy the handles list in case it changes when we're out of the lock. + if (handleIterable is List<RegionHandle>) + { + handleIterable = handleIterable.ToArray(); + } + + // Temporarily release the tracking lock while we're running the action. + Monitor.Exit(_tracking.TrackingLock); + + try + { + lock (_preActionLock) + { + _preAction?.Invoke(maxAddress, minEndAddress - maxAddress); + + // The action is removed after it returns, to ensure that the null check above succeeds when + // it's still in progress rather than continuing and possibly missing a required data flush. + Interlocked.Exchange(ref _preAction, null); + } + } + finally + { + Monitor.Enter(_tracking.TrackingLock); + } + } + + if (write) + { + bool oldDirty = Dirty; + Dirty = true; + if (!oldDirty) + { + _onDirty?.Invoke(); + } + Parent?.SignalWrite(); + } + } + + /// <summary> + /// Signal that a precise memory action occurred within this handle's virtual regions. + /// If there is no precise action, or the action returns false, the normal signal handler will be called. + /// </summary> + /// <param name="address">Address accessed</param> + /// <param name="size">Size of the region affected in bytes</param> + /// <param name="write">Whether the region was written to or read</param> + /// <param name="handleIterable">Reference to the handles being iterated, in case the list needs to be copied</param> + /// <returns>True if a precise action was performed and returned true, false otherwise</returns> + internal bool SignalPrecise(ulong address, ulong size, bool write, ref IList<RegionHandle> handleIterable) + { + if (!Unmapped && _preciseAction != null && _preciseAction(address, size, write)) + { + return true; + } + + Signal(address, size, write, ref handleIterable); + + return false; + } + + /// <summary> + /// Force this handle to be dirty, without reprotecting. + /// </summary> + public void ForceDirty() + { + Dirty = true; + } + + /// <summary> + /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write. + /// </summary> + /// <param name="asDirty">True if the handle should be reprotected as dirty, rather than have it cleared</param> + /// <param name="consecutiveCheck">True if this reprotect is the result of consecutive dirty checks</param> + public void Reprotect(bool asDirty, bool consecutiveCheck = false) + { + if (_volatile) return; + + Dirty = asDirty; + + bool protectionChanged = false; + + lock (_tracking.TrackingLock) + { + foreach (VirtualRegion region in _regions) + { + protectionChanged |= region.UpdateProtection(); + } + } + + if (!protectionChanged) + { + // Counteract the check count being incremented when this handle was forced dirty. + // It doesn't count for protected write tracking. + + _checkCount--; + } + else if (!asDirty) + { + if (consecutiveCheck || (_checkCount > 0 && _checkCount < CheckCountForInfrequent)) + { + if (++_volatileCount >= VolatileThreshold && _preAction == null) + { + _volatile = true; + return; + } + } + else + { + _volatileCount = 0; + } + + _checkCount = 0; + } + } + + /// <summary> + /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write. + /// </summary> + /// <param name="asDirty">True if the handle should be reprotected as dirty, rather than have it cleared</param> + public void Reprotect(bool asDirty = false) + { + Reprotect(asDirty, false); + } + + /// <summary> + /// Register an action to perform when the tracked region is read or written. + /// The action is automatically removed after it runs. + /// </summary> + /// <param name="action">Action to call on read or write</param> + public void RegisterAction(RegionSignal action) + { + ClearVolatile(); + + lock (_preActionLock) + { + RegionSignal lastAction = Interlocked.Exchange(ref _preAction, action); + + if (lastAction == null && action != lastAction) + { + lock (_tracking.TrackingLock) + { + foreach (VirtualRegion region in _regions) + { + region.UpdateProtection(); + } + } + } + } + } + + /// <summary> + /// Register an action to perform when a precise access occurs (one with exact address and size). + /// If the action returns true, read/write tracking are skipped. + /// </summary> + /// <param name="action">Action to call on read or write</param> + public void RegisterPreciseAction(PreciseRegionSignal action) + { + _preciseAction = action; + } + + /// <summary> + /// Register an action to perform when the region is written to. + /// This action will not be removed when it is called - it is called each time the dirty flag is set. + /// </summary> + /// <param name="action">Action to call on dirty</param> + public void RegisterDirtyEvent(Action action) + { + _onDirty += action; + } + + /// <summary> + /// Add a child virtual region to this handle. + /// </summary> + /// <param name="region">Virtual region to add as a child</param> + internal void AddChild(VirtualRegion region) + { + _regions.Add(region); + } + + /// <summary> + /// Signal that this handle has been mapped or unmapped. + /// </summary> + /// <param name="mapped">True if the handle has been mapped, false if unmapped</param> + internal void SignalMappingChanged(bool mapped) + { + if (Unmapped == mapped) + { + Unmapped = !mapped; + + if (Unmapped) + { + ClearVolatile(); + Dirty = false; + } + } + } + + /// <summary> + /// Check if this region overlaps with another. + /// </summary> + /// <param name="address">Base address</param> + /// <param name="size">Size of the region</param> + /// <returns>True if overlapping, false otherwise</returns> + public bool OverlapsWith(ulong address, ulong size) + { + return Address < address + size && address < EndAddress; + } + + /// <summary> + /// Dispose the handle. Within the tracking lock, this removes references from virtual regions. + /// </summary> + public void Dispose() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + _disposed = true; + + lock (_tracking.TrackingLock) + { + foreach (VirtualRegion region in _regions) + { + region.RemoveHandle(this); + } + } + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/RegionSignal.cs b/src/Ryujinx.Memory/Tracking/RegionSignal.cs new file mode 100644 index 00000000..c8a28d7d --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/RegionSignal.cs @@ -0,0 +1,4 @@ +namespace Ryujinx.Memory.Tracking +{ + public delegate void RegionSignal(ulong address, ulong size); +} diff --git a/src/Ryujinx.Memory/Tracking/SmartMultiRegionHandle.cs b/src/Ryujinx.Memory/Tracking/SmartMultiRegionHandle.cs new file mode 100644 index 00000000..4acddefa --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/SmartMultiRegionHandle.cs @@ -0,0 +1,280 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// A MultiRegionHandle that attempts to segment a region's handles into the regions requested + /// to avoid iterating over granular chunks for canonically large regions. + /// If minimum granularity is to be expected, use MultiRegionHandle. + /// </summary> + public class SmartMultiRegionHandle : IMultiRegionHandle + { + /// <summary> + /// A list of region handles starting at each granularity size increment. + /// </summary> + private readonly RegionHandle[] _handles; + private readonly ulong _address; + private readonly ulong _granularity; + private readonly ulong _size; + private MemoryTracking _tracking; + private readonly int _id; + + public bool Dirty { get; private set; } = true; + + internal SmartMultiRegionHandle(MemoryTracking tracking, ulong address, ulong size, ulong granularity, int id) + { + // For this multi-region handle, the handle list starts empty. + // As regions are queried, they are added to the _handles array at their start index. + // When a region being added overlaps another, the existing region is split. + // A query can therefore scan multiple regions, though with no overlaps they can cover a large area. + + _tracking = tracking; + _handles = new RegionHandle[size / granularity]; + _granularity = granularity; + + _address = address; + _size = size; + _id = id; + } + + public void SignalWrite() + { + Dirty = true; + } + + public void ForceDirty(ulong address, ulong size) + { + foreach (var handle in _handles) + { + if (handle != null && handle.OverlapsWith(address, size)) + { + handle.ForceDirty(); + } + } + } + + public void RegisterAction(RegionSignal action) + { + foreach (var handle in _handles) + { + if (handle != null) + { + handle?.RegisterAction((address, size) => action(handle.Address, handle.Size)); + } + } + } + + public void RegisterPreciseAction(PreciseRegionSignal action) + { + foreach (var handle in _handles) + { + if (handle != null) + { + handle?.RegisterPreciseAction((address, size, write) => action(handle.Address, handle.Size, write)); + } + } + } + + public void QueryModified(Action<ulong, ulong> modifiedAction) + { + if (!Dirty) + { + return; + } + + Dirty = false; + + QueryModified(_address, _size, modifiedAction); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ulong HandlesToBytes(int handles) + { + return (ulong)handles * _granularity; + } + + private void SplitHandle(int handleIndex, int splitIndex) + { + RegionHandle handle = _handles[handleIndex]; + ulong address = _address + HandlesToBytes(handleIndex); + ulong size = HandlesToBytes(splitIndex - handleIndex); + + // First, the target handle must be removed. Its data can still be used to determine the new handles. + RegionSignal signal = handle.PreAction; + handle.Dispose(); + + RegionHandle splitLow = _tracking.BeginTracking(address, size, _id); + splitLow.Parent = this; + if (signal != null) + { + splitLow.RegisterAction(signal); + } + _handles[handleIndex] = splitLow; + + RegionHandle splitHigh = _tracking.BeginTracking(address + size, handle.Size - size, _id); + splitHigh.Parent = this; + if (signal != null) + { + splitHigh.RegisterAction(signal); + } + _handles[splitIndex] = splitHigh; + } + + private void CreateHandle(int startHandle, int lastHandle) + { + ulong startAddress = _address + HandlesToBytes(startHandle); + + // Scan for the first handle before us. If it's overlapping us, it must be split. + for (int i = startHandle - 1; i >= 0; i--) + { + RegionHandle handle = _handles[i]; + if (handle != null) + { + if (handle.EndAddress > startAddress) + { + SplitHandle(i, startHandle); + return; // The remainer of this handle should be filled in later on. + } + break; + } + } + + // Scan for handles after us. We should create a handle that goes up to this handle's start point, if present. + for (int i = startHandle + 1; i <= lastHandle; i++) + { + RegionHandle handle = _handles[i]; + if (handle != null) + { + // Fill up to the found handle. + handle = _tracking.BeginTracking(startAddress, HandlesToBytes(i - startHandle), _id); + handle.Parent = this; + _handles[startHandle] = handle; + return; + } + } + + // Can fill the whole range. + _handles[startHandle] = _tracking.BeginTracking(startAddress, HandlesToBytes(1 + lastHandle - startHandle), _id); + _handles[startHandle].Parent = this; + } + + public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction) + { + int startHandle = (int)((address - _address) / _granularity); + int lastHandle = (int)((address + (size - 1) - _address) / _granularity); + + ulong rgStart = _address + (ulong)startHandle * _granularity; + ulong rgSize = 0; + + ulong endAddress = _address + ((ulong)lastHandle + 1) * _granularity; + + int i = startHandle; + + while (i <= lastHandle) + { + RegionHandle handle = _handles[i]; + if (handle == null) + { + // Missing handle. A new handle must be created. + CreateHandle(i, lastHandle); + handle = _handles[i]; + } + + if (handle.EndAddress > endAddress) + { + // End address of handle is beyond the end of the search. Force a split. + SplitHandle(i, lastHandle + 1); + handle = _handles[i]; + } + + if (handle.Dirty) + { + rgSize += handle.Size; + handle.Reprotect(); + } + else + { + // Submit the region scanned so far as dirty + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + rgSize = 0; + } + rgStart = handle.EndAddress; + } + + i += (int)(handle.Size / _granularity); + } + + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + } + } + + public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction, int sequenceNumber) + { + int startHandle = (int)((address - _address) / _granularity); + int lastHandle = (int)((address + (size - 1) - _address) / _granularity); + + ulong rgStart = _address + (ulong)startHandle * _granularity; + ulong rgSize = 0; + + ulong endAddress = _address + ((ulong)lastHandle + 1) * _granularity; + + int i = startHandle; + + while (i <= lastHandle) + { + RegionHandle handle = _handles[i]; + if (handle == null) + { + // Missing handle. A new handle must be created. + CreateHandle(i, lastHandle); + handle = _handles[i]; + } + + if (handle.EndAddress > endAddress) + { + // End address of handle is beyond the end of the search. Force a split. + SplitHandle(i, lastHandle + 1); + handle = _handles[i]; + } + + if (handle.Dirty && sequenceNumber != handle.SequenceNumber) + { + rgSize += handle.Size; + handle.Reprotect(); + } + else + { + // Submit the region scanned so far as dirty + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + rgSize = 0; + } + rgStart = handle.EndAddress; + } + + handle.SequenceNumber = sequenceNumber; + + i += (int)(handle.Size / _granularity); + } + + if (rgSize != 0) + { + modifiedAction(rgStart, rgSize); + } + } + + public void Dispose() + { + foreach (var handle in _handles) + { + handle?.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/VirtualRegion.cs b/src/Ryujinx.Memory/Tracking/VirtualRegion.cs new file mode 100644 index 00000000..9651426b --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/VirtualRegion.cs @@ -0,0 +1,144 @@ +using Ryujinx.Memory.Range; +using System.Collections.Generic; + +namespace Ryujinx.Memory.Tracking +{ + /// <summary> + /// A region of virtual memory. + /// </summary> + class VirtualRegion : AbstractRegion + { + public List<RegionHandle> Handles = new List<RegionHandle>(); + + private readonly MemoryTracking _tracking; + private MemoryPermission _lastPermission; + + public VirtualRegion(MemoryTracking tracking, ulong address, ulong size, MemoryPermission lastPermission = MemoryPermission.Invalid) : base(address, size) + { + _lastPermission = lastPermission; + _tracking = tracking; + } + + /// <inheritdoc/> + public override void Signal(ulong address, ulong size, bool write, int? exemptId) + { + IList<RegionHandle> handles = Handles; + + for (int i = 0; i < handles.Count; i++) + { + if (exemptId == null || handles[i].Id != exemptId.Value) + { + handles[i].Signal(address, size, write, ref handles); + } + } + + UpdateProtection(); + } + + /// <inheritdoc/> + public override void SignalPrecise(ulong address, ulong size, bool write, int? exemptId) + { + IList<RegionHandle> handles = Handles; + + bool allPrecise = true; + + for (int i = 0; i < handles.Count; i++) + { + if (exemptId == null || handles[i].Id != exemptId.Value) + { + allPrecise &= handles[i].SignalPrecise(address, size, write, ref handles); + } + } + + // Only update protection if a regular signal handler was called. + // This allows precise actions to skip reprotection costs if they want (they can still do it manually). + if (!allPrecise) + { + UpdateProtection(); + } + } + + /// <summary> + /// Signal that this region has been mapped or unmapped. + /// </summary> + /// <param name="mapped">True if the region has been mapped, false if unmapped</param> + public void SignalMappingChanged(bool mapped) + { + _lastPermission = MemoryPermission.Invalid; + + foreach (RegionHandle handle in Handles) + { + handle.SignalMappingChanged(mapped); + } + } + + /// <summary> + /// Gets the strictest permission that the child handles demand. Assumes that the tracking lock has been obtained. + /// </summary> + /// <returns>Protection level that this region demands</returns> + public MemoryPermission GetRequiredPermission() + { + // Start with Read/Write, each handle can strip off permissions as necessary. + // Assumes the tracking lock has already been obtained. + + MemoryPermission result = MemoryPermission.ReadAndWrite; + + foreach (var handle in Handles) + { + result &= handle.RequiredPermission; + if (result == 0) return result; + } + return result; + } + + /// <summary> + /// Updates the protection for this virtual region. + /// </summary> + public bool UpdateProtection() + { + MemoryPermission permission = GetRequiredPermission(); + + if (_lastPermission != permission) + { + _tracking.ProtectVirtualRegion(this, permission); + _lastPermission = permission; + + return true; + } + + return false; + } + + /// <summary> + /// Removes a handle from this virtual region. If there are no handles left, this virtual region is removed. + /// </summary> + /// <param name="handle">Handle to remove</param> + public void RemoveHandle(RegionHandle handle) + { + lock (_tracking.TrackingLock) + { + Handles.Remove(handle); + UpdateProtection(); + if (Handles.Count == 0) + { + _tracking.RemoveVirtual(this); + } + } + } + + public override INonOverlappingRange Split(ulong splitAddress) + { + VirtualRegion newRegion = new VirtualRegion(_tracking, splitAddress, EndAddress - splitAddress, _lastPermission); + Size = splitAddress - Address; + + // The new region inherits all of our parents. + newRegion.Handles = new List<RegionHandle>(Handles); + foreach (var parent in Handles) + { + parent.AddChild(newRegion); + } + + return newRegion; + } + } +} diff --git a/src/Ryujinx.Memory/WindowsShared/MappingTree.cs b/src/Ryujinx.Memory/WindowsShared/MappingTree.cs new file mode 100644 index 00000000..97758c2b --- /dev/null +++ b/src/Ryujinx.Memory/WindowsShared/MappingTree.cs @@ -0,0 +1,87 @@ +using Ryujinx.Common.Collections; +using System; + +namespace Ryujinx.Memory.WindowsShared +{ + /// <summary> + /// A intrusive Red-Black Tree that also supports getting nodes overlapping a given range. + /// </summary> + /// <typeparam name="T">Type of the value stored on the node</typeparam> + class MappingTree<T> : IntrusiveRedBlackTree<RangeNode<T>> + { + private const int ArrayGrowthSize = 16; + + public int GetNodes(ulong start, ulong end, ref RangeNode<T>[] overlaps, int overlapCount = 0) + { + RangeNode<T> node = this.GetNodeByKey(start); + + for (; node != null; node = node.Successor) + { + if (overlaps.Length <= overlapCount) + { + Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize); + } + + overlaps[overlapCount++] = node; + + if (node.End >= end) + { + break; + } + } + + return overlapCount; + } + } + + class RangeNode<T> : IntrusiveRedBlackTreeNode<RangeNode<T>>, IComparable<RangeNode<T>>, IComparable<ulong> + { + public ulong Start { get; } + public ulong End { get; private set; } + public T Value { get; } + + public RangeNode(ulong start, ulong end, T value) + { + Start = start; + End = end; + Value = value; + } + + public void Extend(ulong sizeDelta) + { + End += sizeDelta; + } + + public int CompareTo(RangeNode<T> other) + { + if (Start < other.Start) + { + return -1; + } + else if (Start <= other.End - 1UL) + { + return 0; + } + else + { + return 1; + } + } + + public int CompareTo(ulong address) + { + if (address < Start) + { + return 1; + } + else if (address <= End - 1UL) + { + return 0; + } + else + { + return -1; + } + } + } +}
\ No newline at end of file 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 diff --git a/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs b/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs new file mode 100644 index 00000000..67e704ea --- /dev/null +++ b/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs @@ -0,0 +1,101 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Memory.WindowsShared +{ + static partial class WindowsApi + { + public static readonly IntPtr InvalidHandleValue = new IntPtr(-1); + public static readonly IntPtr CurrentProcessHandle = new IntPtr(-1); + + [LibraryImport("kernel32.dll", SetLastError = true)] + public static partial IntPtr VirtualAlloc( + IntPtr lpAddress, + IntPtr dwSize, + AllocationType flAllocationType, + MemoryProtection flProtect); + + [LibraryImport("KernelBase.dll", SetLastError = true)] + public static partial IntPtr VirtualAlloc2( + IntPtr process, + IntPtr lpAddress, + IntPtr dwSize, + AllocationType flAllocationType, + MemoryProtection flProtect, + IntPtr extendedParameters, + ulong parameterCount); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool VirtualProtect( + IntPtr lpAddress, + IntPtr dwSize, + MemoryProtection flNewProtect, + out MemoryProtection lpflOldProtect); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool VirtualFree(IntPtr lpAddress, IntPtr dwSize, AllocationType dwFreeType); + + [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateFileMappingW")] + public static partial IntPtr CreateFileMapping( + IntPtr hFile, + IntPtr lpFileMappingAttributes, + FileMapProtection flProtect, + uint dwMaximumSizeHigh, + uint dwMaximumSizeLow, + [MarshalAs(UnmanagedType.LPWStr)] string lpName); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool CloseHandle(IntPtr hObject); + + [LibraryImport("kernel32.dll", SetLastError = true)] + public static partial IntPtr MapViewOfFile( + IntPtr hFileMappingObject, + uint dwDesiredAccess, + uint dwFileOffsetHigh, + uint dwFileOffsetLow, + IntPtr dwNumberOfBytesToMap); + + [LibraryImport("KernelBase.dll", SetLastError = true)] + public static partial IntPtr MapViewOfFile3( + IntPtr hFileMappingObject, + IntPtr process, + IntPtr baseAddress, + ulong offset, + IntPtr dwNumberOfBytesToMap, + ulong allocationType, + MemoryProtection dwDesiredAccess, + IntPtr extendedParameters, + ulong parameterCount); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool UnmapViewOfFile(IntPtr lpBaseAddress); + + [LibraryImport("KernelBase.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool UnmapViewOfFile2(IntPtr process, IntPtr lpBaseAddress, ulong unmapFlags); + + [LibraryImport("kernel32.dll")] + public static partial uint GetLastError(); + + [LibraryImport("kernel32.dll")] + public static partial int GetCurrentThreadId(); + + public static MemoryProtection GetProtection(MemoryPermission permission) + { + return permission switch + { + MemoryPermission.None => MemoryProtection.NoAccess, + MemoryPermission.Read => MemoryProtection.ReadOnly, + MemoryPermission.ReadAndWrite => MemoryProtection.ReadWrite, + MemoryPermission.ReadAndExecute => MemoryProtection.ExecuteRead, + MemoryPermission.ReadWriteExecute => MemoryProtection.ExecuteReadWrite, + MemoryPermission.Execute => MemoryProtection.Execute, + _ => throw new MemoryProtectionException(permission) + }; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs b/src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs new file mode 100644 index 00000000..3140d705 --- /dev/null +++ b/src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs @@ -0,0 +1,24 @@ +using System; + +namespace Ryujinx.Memory.WindowsShared +{ + class WindowsApiException : Exception + { + public WindowsApiException() + { + } + + public WindowsApiException(string functionName) : base(CreateMessage(functionName)) + { + } + + public WindowsApiException(string functionName, Exception inner) : base(CreateMessage(functionName), inner) + { + } + + private static string CreateMessage(string functionName) + { + return $"{functionName} returned error code 0x{WindowsApi.GetLastError():X}."; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Memory/WindowsShared/WindowsFlags.cs b/src/Ryujinx.Memory/WindowsShared/WindowsFlags.cs new file mode 100644 index 00000000..ca69cfe9 --- /dev/null +++ b/src/Ryujinx.Memory/WindowsShared/WindowsFlags.cs @@ -0,0 +1,52 @@ +using System; + +namespace Ryujinx.Memory.WindowsShared +{ + [Flags] + enum AllocationType : uint + { + CoalescePlaceholders = 0x1, + PreservePlaceholder = 0x2, + Commit = 0x1000, + Reserve = 0x2000, + Decommit = 0x4000, + ReplacePlaceholder = 0x4000, + Release = 0x8000, + ReservePlaceholder = 0x40000, + Reset = 0x80000, + Physical = 0x400000, + TopDown = 0x100000, + WriteWatch = 0x200000, + LargePages = 0x20000000 + } + + [Flags] + enum MemoryProtection : uint + { + NoAccess = 0x01, + ReadOnly = 0x02, + ReadWrite = 0x04, + WriteCopy = 0x08, + Execute = 0x10, + ExecuteRead = 0x20, + ExecuteReadWrite = 0x40, + ExecuteWriteCopy = 0x80, + GuardModifierflag = 0x100, + NoCacheModifierflag = 0x200, + WriteCombineModifierflag = 0x400 + } + + [Flags] + enum FileMapProtection : uint + { + PageReadonly = 0x02, + PageReadWrite = 0x04, + PageWriteCopy = 0x08, + PageExecuteRead = 0x20, + PageExecuteReadWrite = 0x40, + SectionCommit = 0x8000000, + SectionImage = 0x1000000, + SectionNoCache = 0x10000000, + SectionReserve = 0x4000000 + } +} diff --git a/src/Ryujinx.Memory/WritableRegion.cs b/src/Ryujinx.Memory/WritableRegion.cs new file mode 100644 index 00000000..21565ea5 --- /dev/null +++ b/src/Ryujinx.Memory/WritableRegion.cs @@ -0,0 +1,38 @@ +using System; + +namespace Ryujinx.Memory +{ + public sealed class WritableRegion : IDisposable + { + private readonly IWritableBlock _block; + private readonly ulong _va; + private readonly bool _tracked; + + private bool NeedsWriteback => _block != null; + + public Memory<byte> Memory { get; } + + public WritableRegion(IWritableBlock block, ulong va, Memory<byte> memory, bool tracked = false) + { + _block = block; + _va = va; + _tracked = tracked; + Memory = memory; + } + + public void Dispose() + { + if (NeedsWriteback) + { + if (_tracked) + { + _block.Write(_va, Memory.Span); + } + else + { + _block.WriteUntracked(_va, Memory.Span); + } + } + } + } +} |
