Ryujinx/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs
riperiperi 14ce9e1567
Move partial unmap handler to the native signal handler (#3437)
* Initial commit with a lot of testing stuff.

* Partial Unmap Cleanup Part 1

* Fix some minor issues, hopefully windows tests.

* Disable partial unmap tests on macos for now

Weird issue.

* Goodbye magic number

* Add COMPlus_EnableAlternateStackCheck for tests

`COMPlus_EnableAlternateStackCheck` is needed for NullReferenceException handling to work on linux after registering the signal handler, due to how dotnet registers its own signal handler.

* Address some feedback

* Force retry when memory is mapped in memory tracking

This case existed before, but returning `false` no longer retries, so it would crash immediately after unprotecting the memory... Now, we return `true` to deliberately retry.

This case existed before (was just broken by this change) and I don't really want to look into fixing the issue right now. Technically, this means that on guest code partial unmaps will retry _due to this_ rather than hitting the handler. I don't expect this to cause any issues.

This should fix random crashes in Xenoblade Chronicles 2.

* Use IsRangeMapped

* Suppress MockMemoryManager.UnmapEvent warning

This event is not signalled by the mock memory manager.

* Remove 4kb mapping
2022-07-29 19:16:29 -03:00

160 lines
5.8 KiB
C#

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using static Ryujinx.Common.Memory.PartialUnmaps.PartialUnmapHelpers;
namespace Ryujinx.Common.Memory.PartialUnmaps
{
/// <summary>
/// State for partial unmaps. Intended to be used on Windows.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PartialUnmapState
{
public NativeReaderWriterLock PartialUnmapLock;
public int PartialUnmapsCount;
public ThreadLocalMap<int> LocalCounts;
public readonly static int PartialUnmapLockOffset;
public readonly static int PartialUnmapsCountOffset;
public readonly static int LocalCountsOffset;
public readonly static IntPtr GlobalState;
[SupportedOSPlatform("windows")]
[DllImport("kernel32.dll")]
public static extern int GetCurrentThreadId();
[SupportedOSPlatform("windows")]
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr OpenThread(int dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
[SupportedOSPlatform("windows")]
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[SupportedOSPlatform("windows")]
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
/// <summary>
/// Creates a global static PartialUnmapState and populates the field offsets.
/// </summary>
static unsafe PartialUnmapState()
{
PartialUnmapState instance = new PartialUnmapState();
PartialUnmapLockOffset = OffsetOf(ref instance, ref instance.PartialUnmapLock);
PartialUnmapsCountOffset = OffsetOf(ref instance, ref instance.PartialUnmapsCount);
LocalCountsOffset = OffsetOf(ref instance, ref instance.LocalCounts);
int size = Unsafe.SizeOf<PartialUnmapState>();
GlobalState = Marshal.AllocHGlobal(size);
Unsafe.InitBlockUnaligned((void*)GlobalState, 0, (uint)size);
}
/// <summary>
/// Resets the global state.
/// </summary>
public static unsafe void Reset()
{
int size = Unsafe.SizeOf<PartialUnmapState>();
Unsafe.InitBlockUnaligned((void*)GlobalState, 0, (uint)size);
}
/// <summary>
/// Gets a reference to the global state.
/// </summary>
/// <returns>A reference to the global state</returns>
public static unsafe ref PartialUnmapState GetRef()
{
return ref Unsafe.AsRef<PartialUnmapState>((void*)GlobalState);
}
/// <summary>
/// Checks if an access violation handler should retry execution due to a fault caused by partial unmap.
/// </summary>
/// <remarks>
/// Due to Windows limitations, <see cref="UnmapView"/> might need to unmap more memory than requested.
/// The additional memory that was unmapped is later remapped, however this leaves a time gap where the
/// memory might be accessed but is unmapped. Users of the API must compensate for that by catching the
/// access violation and retrying if it happened between the unmap and remap operation.
/// This method can be used to decide if retrying in such cases is necessary or not.
///
/// This version of the function is not used, but serves as a reference for the native
/// implementation in ARMeilleure.
/// </remarks>
/// <returns>True if execution should be retried, false otherwise</returns>
[SupportedOSPlatform("windows")]
public bool RetryFromAccessViolation()
{
PartialUnmapLock.AcquireReaderLock();
int threadID = GetCurrentThreadId();
int threadIndex = LocalCounts.GetOrReserve(threadID, 0);
if (threadIndex == -1)
{
// Out of thread local space... try again later.
PartialUnmapLock.ReleaseReaderLock();
return true;
}
ref int threadLocalPartialUnmapsCount = ref LocalCounts.GetValue(threadIndex);
bool retry = threadLocalPartialUnmapsCount != PartialUnmapsCount;
if (retry)
{
threadLocalPartialUnmapsCount = PartialUnmapsCount;
}
PartialUnmapLock.ReleaseReaderLock();
return retry;
}
/// <summary>
/// Iterates and trims threads in the thread -> count map that
/// are no longer active.
/// </summary>
[SupportedOSPlatform("windows")]
public void TrimThreads()
{
const uint ExitCodeStillActive = 259;
const int ThreadQueryInformation = 0x40;
Span<int> ids = LocalCounts.ThreadIds.ToSpan();
for (int i = 0; i < ids.Length; i++)
{
int id = ids[i];
if (id != 0)
{
IntPtr handle = OpenThread(ThreadQueryInformation, false, (uint)id);
if (handle == IntPtr.Zero)
{
Interlocked.CompareExchange(ref ids[i], 0, id);
}
else
{
GetExitCodeThread(handle, out uint exitCode);
if (exitCode != ExitCodeStillActive)
{
Interlocked.CompareExchange(ref ids[i], 0, id);
}
CloseHandle(handle);
}
}
}
}
}
}