| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469 |
- using ARMeilleure.Signal;
- using ARMeilleure.Translation;
- using NUnit.Framework;
- using Ryujinx.Common.Memory.PartialUnmaps;
- using Ryujinx.Cpu;
- using Ryujinx.Cpu.Jit;
- using Ryujinx.Memory;
- using Ryujinx.Memory.Tests;
- using Ryujinx.Memory.Tracking;
- using System;
- using System.Collections.Generic;
- using System.Diagnostics.CodeAnalysis;
- using System.Runtime.CompilerServices;
- using System.Runtime.InteropServices;
- using System.Threading;
- namespace Ryujinx.Tests.Memory
- {
- [TestFixture]
- internal class PartialUnmaps
- {
- private static Translator _translator;
- private (MemoryBlock virt, MemoryBlock mirror, MemoryEhMeilleure exceptionHandler) GetVirtual(ulong asSize)
- {
- MemoryAllocationFlags asFlags = MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible;
- var addressSpace = new MemoryBlock(asSize, asFlags);
- var addressSpaceMirror = new MemoryBlock(asSize, asFlags);
- var tracking = new MemoryTracking(new MockVirtualMemoryManager(asSize, 0x1000), 0x1000);
- var exceptionHandler = new MemoryEhMeilleure(addressSpace, addressSpaceMirror, tracking);
- return (addressSpace, addressSpaceMirror, exceptionHandler);
- }
- private int CountThreads(ref PartialUnmapState state)
- {
- int count = 0;
- ref var ids = ref state.LocalCounts.ThreadIds;
- for (int i = 0; i < ids.Length; i++)
- {
- if (ids[i] != 0)
- {
- count++;
- }
- }
- return count;
- }
- private void EnsureTranslator()
- {
- // Create a translator, as one is needed to register the signal handler or emit methods.
- _translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true);
- }
- [Test]
- // Memory aliasing tests fail on CI at the moment.
- [Platform(Exclude = "MacOsX")]
- public void PartialUnmap([Values] bool readOnly)
- {
- // Set up an address space to test partial unmapping.
- // Should register the signal handler to deal with this on Windows.
- ulong vaSize = 0x100000;
- // The first 0x100000 is mapped to start. It is replaced from the center with the 0x200000 mapping.
- var backing = new MemoryBlock(vaSize * 2, MemoryAllocationFlags.Mirrorable);
- (MemoryBlock unusedMainMemory, MemoryBlock memory, MemoryEhMeilleure exceptionHandler) = GetVirtual(vaSize * 2);
- EnsureTranslator();
- ref var state = ref PartialUnmapState.GetRef();
- Thread testThread = null;
- bool shouldAccess = true;
- try
- {
- // Globally reset the struct for handling partial unmap races.
- PartialUnmapState.Reset();
- bool error = false;
- // Create a large mapping.
- memory.MapView(backing, 0, 0, vaSize);
- if (readOnly)
- {
- memory.Reprotect(0, vaSize, MemoryPermission.Read);
- }
- if (readOnly)
- {
- // Write a value to the physical memory, then try to read it repeately from virtual.
- // It should not change.
- testThread = new Thread(() =>
- {
- int i = 12345;
- backing.Write(vaSize - 0x1000, i);
- while (shouldAccess)
- {
- if (memory.Read<int>(vaSize - 0x1000) != i)
- {
- error = true;
- shouldAccess = false;
- }
- }
- });
- }
- else
- {
- // Repeatedly write and check the value on the last page of the mapping on another thread.
- testThread = new Thread(() =>
- {
- int i = 0;
- while (shouldAccess)
- {
- memory.Write(vaSize - 0x1000, i);
- if (memory.Read<int>(vaSize - 0x1000) != i)
- {
- error = true;
- shouldAccess = false;
- }
- i++;
- }
- });
- }
- testThread.Start();
- // Create a smaller mapping, covering the larger mapping.
- // Immediately try to write to the part of the larger mapping that did not change.
- // Do this a lot, with the smaller mapping gradually increasing in size. Should not crash, data should not be lost.
- ulong pageSize = 0x1000;
- int mappingExpandCount = (int)(vaSize / (pageSize * 2)) - 1;
- ulong vaCenter = vaSize / 2;
- for (int i = 1; i <= mappingExpandCount; i++)
- {
- ulong start = vaCenter - (pageSize * (ulong)i);
- ulong size = pageSize * (ulong)i * 2;
- ulong startPa = start + vaSize;
- memory.MapView(backing, startPa, start, size);
- }
- // On Windows, this should put unmap counts on the thread local map.
- if (OperatingSystem.IsWindows())
- {
- // One thread should be present on the thread local map. Trimming should remove it.
- Assert.AreEqual(1, CountThreads(ref state));
- }
- shouldAccess = false;
- testThread.Join();
- Assert.False(error);
- string test = null;
- try
- {
- test.IndexOf('1');
- }
- catch (NullReferenceException)
- {
- // This shouldn't freeze.
- }
- if (OperatingSystem.IsWindows())
- {
- state.TrimThreads();
- Assert.AreEqual(0, CountThreads(ref state));
- }
- /*
- * Use this to test invalid access. Can't put this in the test suite unfortunately as invalid access crashes the test process.
- * memory.Reprotect(vaSize - 0x1000, 0x1000, MemoryPermission.None);
- * //memory.UnmapView(backing, vaSize - 0x1000, 0x1000);
- * memory.Read<int>(vaSize - 0x1000);
- */
- }
- finally
- {
- // In case something failed, we want to ensure the test thread is dead before disposing of the memory.
- shouldAccess = false;
- testThread?.Join();
- exceptionHandler.Dispose();
- unusedMainMemory.Dispose();
- memory.Dispose();
- backing.Dispose();
- }
- }
- [Test]
- // Memory aliasing tests fail on CI at the moment.
- [Platform(Exclude = "MacOsX")]
- public unsafe void PartialUnmapNative()
- {
- // Set up an address space to test partial unmapping.
- // Should register the signal handler to deal with this on Windows.
- ulong vaSize = 0x100000;
- // The first 0x100000 is mapped to start. It is replaced from the center with the 0x200000 mapping.
- var backing = new MemoryBlock(vaSize * 2, MemoryAllocationFlags.Mirrorable);
- (MemoryBlock mainMemory, MemoryBlock unusedMirror, MemoryEhMeilleure exceptionHandler) = GetVirtual(vaSize * 2);
- EnsureTranslator();
- ref var state = ref PartialUnmapState.GetRef();
- // Create some state to be used for managing the native writing loop.
- int stateSize = Unsafe.SizeOf<NativeWriteLoopState>();
- var statePtr = Marshal.AllocHGlobal(stateSize);
- Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize);
- ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr);
- writeLoopState.Running = 1;
- writeLoopState.Error = 0;
- try
- {
- // Globally reset the struct for handling partial unmap races.
- PartialUnmapState.Reset();
- // Create a large mapping.
- mainMemory.MapView(backing, 0, 0, vaSize);
- var writeFunc = TestMethods.GenerateDebugNativeWriteLoop();
- IntPtr writePtr = mainMemory.GetPointer(vaSize - 0x1000, 4);
- Thread testThread = new Thread(() =>
- {
- writeFunc(statePtr, writePtr);
- });
- testThread.Start();
- // Create a smaller mapping, covering the larger mapping.
- // Immediately try to write to the part of the larger mapping that did not change.
- // Do this a lot, with the smaller mapping gradually increasing in size. Should not crash, data should not be lost.
- ulong pageSize = 0x1000;
- int mappingExpandCount = (int)(vaSize / (pageSize * 2)) - 1;
- ulong vaCenter = vaSize / 2;
- for (int i = 1; i <= mappingExpandCount; i++)
- {
- ulong start = vaCenter - (pageSize * (ulong)i);
- ulong size = pageSize * (ulong)i * 2;
- ulong startPa = start + vaSize;
- mainMemory.MapView(backing, startPa, start, size);
- }
- writeLoopState.Running = 0;
- testThread.Join();
- Assert.False(writeLoopState.Error != 0);
- }
- finally
- {
- Marshal.FreeHGlobal(statePtr);
- exceptionHandler.Dispose();
- mainMemory.Dispose();
- unusedMirror.Dispose();
- backing.Dispose();
- }
- }
- [Test]
- // Only test in Windows, as this is only used on Windows and uses Windows APIs for trimming.
- [Platform("Win")]
- [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
- public void ThreadLocalMap()
- {
- PartialUnmapState.Reset();
- ref var state = ref PartialUnmapState.GetRef();
- bool running = true;
- var testThread = new Thread(() =>
- {
- PartialUnmapState.GetRef().RetryFromAccessViolation();
- while (running)
- {
- Thread.Sleep(1);
- }
- });
- testThread.Start();
- Thread.Sleep(200);
- Assert.AreEqual(1, CountThreads(ref state));
- // Trimming should not remove the thread as it's still active.
- state.TrimThreads();
- Assert.AreEqual(1, CountThreads(ref state));
- running = false;
- testThread.Join();
- // Should trim now that it's inactive.
- state.TrimThreads();
- Assert.AreEqual(0, CountThreads(ref state));
- }
- [Test]
- // Only test in Windows, as this is only used on Windows and uses Windows APIs for trimming.
- [Platform("Win")]
- public unsafe void ThreadLocalMapNative()
- {
- EnsureTranslator();
- PartialUnmapState.Reset();
- ref var state = ref PartialUnmapState.GetRef();
- fixed (void* localMap = &state.LocalCounts)
- {
- var getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve((IntPtr)localMap);
- for (int i = 0; i < ThreadLocalMap<int>.MapSize; i++)
- {
- // Should obtain the index matching the call #.
- Assert.AreEqual(i, getOrReserve(i + 1, i));
- // Check that this and all previously reserved thread IDs and struct contents are intact.
- for (int j = 0; j <= i; j++)
- {
- Assert.AreEqual(j + 1, state.LocalCounts.ThreadIds[j]);
- Assert.AreEqual(j, state.LocalCounts.Structs[j]);
- }
- }
- // Trying to reserve again when the map is full should return -1.
- Assert.AreEqual(-1, getOrReserve(200, 0));
- for (int i = 0; i < ThreadLocalMap<int>.MapSize; i++)
- {
- // Should obtain the index matching the call #, as it already exists.
- Assert.AreEqual(i, getOrReserve(i + 1, -1));
- // The struct should not be reset to -1.
- Assert.AreEqual(i, state.LocalCounts.Structs[i]);
- }
- // Clear one of the ids as if it were freed.
- state.LocalCounts.ThreadIds[13] = 0;
- // GetOrReserve should now obtain and return 13.
- Assert.AreEqual(13, getOrReserve(300, 301));
- Assert.AreEqual(300, state.LocalCounts.ThreadIds[13]);
- Assert.AreEqual(301, state.LocalCounts.Structs[13]);
- }
- }
- [Test]
- public void NativeReaderWriterLock()
- {
- var rwLock = new NativeReaderWriterLock();
- var threads = new List<Thread>();
- int value = 0;
- bool running = true;
- bool error = false;
- int readersAllowed = 1;
- for (int i = 0; i < 5; i++)
- {
- var readThread = new Thread(() =>
- {
- int count = 0;
- while (running)
- {
- rwLock.AcquireReaderLock();
- int originalValue = Thread.VolatileRead(ref value);
- count++;
- // Spin a bit.
- for (int i = 0; i < 100; i++)
- {
- if (Thread.VolatileRead(ref readersAllowed) == 0)
- {
- error = true;
- running = false;
- }
- }
- // Should not change while the lock is held.
- if (Thread.VolatileRead(ref value) != originalValue)
- {
- error = true;
- running = false;
- }
- rwLock.ReleaseReaderLock();
- }
- });
- threads.Add(readThread);
- }
- for (int i = 0; i < 2; i++)
- {
- var writeThread = new Thread(() =>
- {
- int count = 0;
- while (running)
- {
- rwLock.AcquireReaderLock();
- rwLock.UpgradeToWriterLock();
- Thread.Sleep(2);
- count++;
- Interlocked.Exchange(ref readersAllowed, 0);
- for (int i = 0; i < 10; i++)
- {
- Interlocked.Increment(ref value);
- }
- Interlocked.Exchange(ref readersAllowed, 1);
- rwLock.DowngradeFromWriterLock();
- rwLock.ReleaseReaderLock();
- Thread.Sleep(1);
- }
- });
- threads.Add(writeThread);
- }
- foreach (var thread in threads)
- {
- thread.Start();
- }
- Thread.Sleep(1000);
- running = false;
- foreach (var thread in threads)
- {
- thread.Join();
- }
- Assert.False(error);
- }
- }
- }
|