using System;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Ryujinx.Memory
{
///
/// Represents a block of contiguous physical guest memory.
///
public sealed class MemoryBlock : IWritableBlock, IDisposable
{
private readonly bool _usesSharedMemory;
private readonly bool _isMirror;
private readonly bool _viewCompatible;
private IntPtr _sharedMemory;
private IntPtr _pointer;
private ConcurrentDictionary _viewStorages;
private int _viewCount;
///
/// Pointer to the memory block data.
///
public IntPtr Pointer => _pointer;
///
/// Size of the memory block.
///
public ulong Size { get; }
///
/// Creates a new instance of the memory block class.
///
/// Size of the memory block in bytes
/// Flags that controls memory block memory allocation
/// Throw when there's no enough memory to allocate the requested size
/// Throw when the current platform is not supported
public MemoryBlock(ulong size, MemoryAllocationFlags flags = MemoryAllocationFlags.None)
{
if (flags.HasFlag(MemoryAllocationFlags.Mirrorable))
{
_sharedMemory = MemoryManagement.CreateSharedMemory(size, flags.HasFlag(MemoryAllocationFlags.Reserve));
_pointer = MemoryManagement.MapSharedMemory(_sharedMemory, size);
_usesSharedMemory = true;
}
else if (flags.HasFlag(MemoryAllocationFlags.Reserve))
{
_viewCompatible = flags.HasFlag(MemoryAllocationFlags.ViewCompatible);
_pointer = MemoryManagement.Reserve(size, _viewCompatible);
}
else
{
_pointer = MemoryManagement.Allocate(size);
}
Size = size;
_viewStorages = new ConcurrentDictionary();
_viewStorages.TryAdd(this, 0);
_viewCount = 1;
}
///
/// Creates a new instance of the memory block class, with a existing backing storage.
///
/// Size of the memory block in bytes
/// Shared memory to use as backing storage for this block
/// Throw when there's no enough address space left to map the shared memory
/// Throw when the current platform is not supported
private MemoryBlock(ulong size, IntPtr sharedMemory)
{
_pointer = MemoryManagement.MapSharedMemory(sharedMemory, size);
Size = size;
_usesSharedMemory = true;
_isMirror = true;
}
///
/// 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.
///
/// A new memory block that shares storage with this one
/// Throw when the current memory block does not support mirroring
/// Throw when there's no enough address space left to map the shared memory
/// Throw when the current platform is not supported
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);
}
///
/// Commits a region of memory that has previously been reserved.
/// This can be used to allocate memory on demand.
///
/// Starting offset of the range to be committed
/// Size of the range to be committed
/// True if the operation was successful, false otherwise
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
public bool Commit(ulong offset, ulong size)
{
return MemoryManagement.Commit(GetPointerInternal(offset, size), size);
}
///
/// Decommits a region of memory that has previously been reserved and optionally comitted.
/// This can be used to free previously allocated memory on demand.
///
/// Starting offset of the range to be decommitted
/// Size of the range to be decommitted
/// True if the operation was successful, false otherwise
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
public bool Decommit(ulong offset, ulong size)
{
return MemoryManagement.Decommit(GetPointerInternal(offset, size), size);
}
///
/// Maps a view of memory from another memory block.
///
/// Memory block from where the backing memory will be taken
/// Offset on of the region that should be mapped
/// Offset to map the view into on this block
/// Size of the range to be mapped
/// Throw when the source memory block does not support mirroring
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
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.");
}
if (_viewStorages.TryAdd(srcBlock, 0))
{
srcBlock.IncrementViewCount();
}
MemoryManagement.MapView(srcBlock._sharedMemory, srcOffset, GetPointerInternal(dstOffset, size), size, this);
}
///
/// Unmaps a view of memory from another memory block.
///
/// Memory block from where the backing memory was taken during map
/// Offset of the view previously mapped with
/// Size of the range to be unmapped
public void UnmapView(MemoryBlock srcBlock, ulong offset, ulong size)
{
MemoryManagement.UnmapView(srcBlock._sharedMemory, GetPointerInternal(offset, size), size, this);
}
///
/// Reprotects a region of memory.
///
/// Starting offset of the range to be reprotected
/// Size of the range to be reprotected
/// New memory permissions
/// True if a failed reprotect should throw
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
/// Throw when is invalid
public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail = true)
{
MemoryManagement.Reprotect(GetPointerInternal(offset, size), size, permission, _viewCompatible, throwOnFail);
}
///
/// Reads bytes from the memory block.
///
/// Starting offset of the range being read
/// Span where the bytes being read will be copied to
/// Throw when the memory block has already been disposed
/// Throw when the memory region specified for the the data is out of range
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Read(ulong offset, Span data)
{
GetSpan(offset, data.Length).CopyTo(data);
}
///
/// Reads data from the memory block.
///
/// Type of the data
/// Offset where the data is located
/// Data at the specified address
/// Throw when the memory block has already been disposed
/// Throw when the memory region specified for the the data is out of range
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Read(ulong offset) where T : unmanaged
{
return GetRef(offset);
}
///
/// Writes bytes to the memory block.
///
/// Starting offset of the range being written
/// Span where the bytes being written will be copied from
/// Throw when the memory block has already been disposed
/// Throw when the memory region specified for the the data is out of range
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(ulong offset, ReadOnlySpan data)
{
data.CopyTo(GetSpan(offset, data.Length));
}
///
/// Writes data to the memory block.
///
/// Type of the data being written
/// Offset to write the data into
/// Data to be written
/// Throw when the memory block has already been disposed
/// Throw when the memory region specified for the the data is out of range
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(ulong offset, T data) where T : unmanaged
{
GetRef(offset) = data;
}
///
/// Copies data from one memory location to another.
///
/// Destination offset to write the data into
/// Source offset to read the data from
/// Size of the copy in bytes
/// Throw when the memory block has already been disposed
/// Throw when , or is out of range
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));
}
}
///
/// Fills a region of memory with .
///
/// Offset of the region to fill with
/// Size in bytes of the region to fill
/// Value to use for the fill
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
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);
}
}
///
/// Gets a reference of the data at a given memory block region.
///
/// Data type
/// Offset of the memory region
/// A reference to the given memory region data
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ref T GetRef(ulong offset) where T : unmanaged
{
IntPtr ptr = _pointer;
ObjectDisposedException.ThrowIf(ptr == IntPtr.Zero, this);
int size = Unsafe.SizeOf();
ulong endOffset = offset + (ulong)size;
if (endOffset > Size || endOffset < offset)
{
ThrowInvalidMemoryRegionException();
}
return ref Unsafe.AsRef((void*)PtrAddr(ptr, offset));
}
///
/// Gets the pointer of a given memory block region.
///
/// Start offset of the memory region
/// Size in bytes of the region
/// The pointer to the memory region
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
[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);
}
///
/// Gets the of a given memory block region.
///
/// Start offset of the memory region
/// Size in bytes of the region
/// Span of the memory region
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe Span GetSpan(ulong offset, int size)
{
return new Span((void*)GetPointerInternal(offset, (ulong)size), size);
}
///
/// Gets the of a given memory block region.
///
/// Start offset of the memory region
/// Size in bytes of the region
/// Memory of the memory region
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe Memory GetMemory(ulong offset, int size)
{
return new NativeMemoryManager((byte*)GetPointerInternal(offset, (ulong)size), size).Memory;
}
///
/// Gets a writable region of a given memory block region.
///
/// Start offset of the memory region
/// Size in bytes of the region
/// Writable region of the memory region
/// Throw when the memory block has already been disposed
/// Throw when either or are out of range
public WritableRegion GetWritableRegion(ulong offset, int size)
{
return new WritableRegion(null, offset, GetMemory(offset, size));
}
///
/// Adds a 64-bits offset to a native pointer.
///
/// Native pointer
/// Offset to add
/// Native pointer with the added offset
private IntPtr PtrAddr(IntPtr pointer, ulong offset)
{
return (IntPtr)(pointer.ToInt64() + (long)offset);
}
///
/// Frees the memory allocated for this memory block.
///
///
/// It's an error to use the memory block after disposal.
///
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);
}
foreach (MemoryBlock viewStorage in _viewStorages.Keys)
{
viewStorage.DecrementViewCount();
}
_viewStorages.Clear();
}
}
///
/// Increments the number of views that uses this memory block as storage.
///
private void IncrementViewCount()
{
Interlocked.Increment(ref _viewCount);
}
///
/// Decrements the number of views that uses this memory block as storage.
///
private void DecrementViewCount()
{
if (Interlocked.Decrement(ref _viewCount) == 0 && _sharedMemory != IntPtr.Zero && !_isMirror)
{
MemoryManagement.DestroySharedMemory(_sharedMemory);
_sharedMemory = IntPtr.Zero;
}
}
///
/// Checks if the specified memory allocation flags are supported on the current platform.
///
/// Flags to be checked
/// True if the platform supports all the flags, false otherwise
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;
}
private static void ThrowInvalidMemoryRegionException() => throw new InvalidMemoryRegionException();
}
}