PartialUnmaps.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. using ARMeilleure.Signal;
  2. using ARMeilleure.Translation;
  3. using NUnit.Framework;
  4. using Ryujinx.Common.Memory.PartialUnmaps;
  5. using Ryujinx.Cpu;
  6. using Ryujinx.Cpu.Jit;
  7. using Ryujinx.Memory;
  8. using Ryujinx.Memory.Tracking;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.Diagnostics.CodeAnalysis;
  12. using System.Runtime.CompilerServices;
  13. using System.Runtime.InteropServices;
  14. using System.Threading;
  15. namespace Ryujinx.Tests.Memory
  16. {
  17. [TestFixture]
  18. internal class PartialUnmaps
  19. {
  20. private static Translator _translator;
  21. private (MemoryBlock virt, MemoryBlock mirror, MemoryEhMeilleure exceptionHandler) GetVirtual(ulong asSize)
  22. {
  23. MemoryAllocationFlags asFlags = MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible;
  24. var addressSpace = new MemoryBlock(asSize, asFlags);
  25. var addressSpaceMirror = new MemoryBlock(asSize, asFlags);
  26. var tracking = new MemoryTracking(new MockVirtualMemoryManager(asSize, 0x1000), 0x1000);
  27. var exceptionHandler = new MemoryEhMeilleure(addressSpace, addressSpaceMirror, tracking);
  28. return (addressSpace, addressSpaceMirror, exceptionHandler);
  29. }
  30. private int CountThreads(ref PartialUnmapState state)
  31. {
  32. int count = 0;
  33. ref var ids = ref state.LocalCounts.ThreadIds;
  34. for (int i = 0; i < ids.Length; i++)
  35. {
  36. if (ids[i] != 0)
  37. {
  38. count++;
  39. }
  40. }
  41. return count;
  42. }
  43. private void EnsureTranslator()
  44. {
  45. // Create a translator, as one is needed to register the signal handler or emit methods.
  46. _translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true);
  47. }
  48. [Test]
  49. // Memory aliasing tests fail on CI at the moment.
  50. [Platform(Exclude = "MacOsX")]
  51. public void PartialUnmap([Values] bool readOnly)
  52. {
  53. // Set up an address space to test partial unmapping.
  54. // Should register the signal handler to deal with this on Windows.
  55. ulong vaSize = 0x100000;
  56. // The first 0x100000 is mapped to start. It is replaced from the center with the 0x200000 mapping.
  57. var backing = new MemoryBlock(vaSize * 2, MemoryAllocationFlags.Mirrorable);
  58. (MemoryBlock unusedMainMemory, MemoryBlock memory, MemoryEhMeilleure exceptionHandler) = GetVirtual(vaSize * 2);
  59. EnsureTranslator();
  60. ref var state = ref PartialUnmapState.GetRef();
  61. Thread testThread = null;
  62. bool shouldAccess = true;
  63. try
  64. {
  65. // Globally reset the struct for handling partial unmap races.
  66. PartialUnmapState.Reset();
  67. bool error = false;
  68. // Create a large mapping.
  69. memory.MapView(backing, 0, 0, vaSize);
  70. if (readOnly)
  71. {
  72. memory.Reprotect(0, vaSize, MemoryPermission.Read);
  73. }
  74. if (readOnly)
  75. {
  76. // Write a value to the physical memory, then try to read it repeately from virtual.
  77. // It should not change.
  78. testThread = new Thread(() =>
  79. {
  80. int i = 12345;
  81. backing.Write(vaSize - 0x1000, i);
  82. while (shouldAccess)
  83. {
  84. if (memory.Read<int>(vaSize - 0x1000) != i)
  85. {
  86. error = true;
  87. shouldAccess = false;
  88. }
  89. }
  90. });
  91. }
  92. else
  93. {
  94. // Repeatedly write and check the value on the last page of the mapping on another thread.
  95. testThread = new Thread(() =>
  96. {
  97. int i = 0;
  98. while (shouldAccess)
  99. {
  100. memory.Write(vaSize - 0x1000, i);
  101. if (memory.Read<int>(vaSize - 0x1000) != i)
  102. {
  103. error = true;
  104. shouldAccess = false;
  105. }
  106. i++;
  107. }
  108. });
  109. }
  110. testThread.Start();
  111. // Create a smaller mapping, covering the larger mapping.
  112. // Immediately try to write to the part of the larger mapping that did not change.
  113. // Do this a lot, with the smaller mapping gradually increasing in size. Should not crash, data should not be lost.
  114. ulong pageSize = 0x1000;
  115. int mappingExpandCount = (int)(vaSize / (pageSize * 2)) - 1;
  116. ulong vaCenter = vaSize / 2;
  117. for (int i = 1; i <= mappingExpandCount; i++)
  118. {
  119. ulong start = vaCenter - (pageSize * (ulong)i);
  120. ulong size = pageSize * (ulong)i * 2;
  121. ulong startPa = start + vaSize;
  122. memory.MapView(backing, startPa, start, size);
  123. }
  124. // On Windows, this should put unmap counts on the thread local map.
  125. if (OperatingSystem.IsWindows())
  126. {
  127. // One thread should be present on the thread local map. Trimming should remove it.
  128. Assert.AreEqual(1, CountThreads(ref state));
  129. }
  130. shouldAccess = false;
  131. testThread.Join();
  132. Assert.False(error);
  133. string test = null;
  134. try
  135. {
  136. test.IndexOf('1');
  137. }
  138. catch (NullReferenceException)
  139. {
  140. // This shouldn't freeze.
  141. }
  142. if (OperatingSystem.IsWindows())
  143. {
  144. state.TrimThreads();
  145. Assert.AreEqual(0, CountThreads(ref state));
  146. }
  147. /*
  148. * Use this to test invalid access. Can't put this in the test suite unfortunately as invalid access crashes the test process.
  149. * memory.Reprotect(vaSize - 0x1000, 0x1000, MemoryPermission.None);
  150. * //memory.UnmapView(backing, vaSize - 0x1000, 0x1000);
  151. * memory.Read<int>(vaSize - 0x1000);
  152. */
  153. }
  154. finally
  155. {
  156. // In case something failed, we want to ensure the test thread is dead before disposing of the memory.
  157. shouldAccess = false;
  158. testThread?.Join();
  159. exceptionHandler.Dispose();
  160. unusedMainMemory.Dispose();
  161. memory.Dispose();
  162. backing.Dispose();
  163. }
  164. }
  165. [Test]
  166. // Memory aliasing tests fail on CI at the moment.
  167. [Platform(Exclude = "MacOsX")]
  168. public unsafe void PartialUnmapNative()
  169. {
  170. // Set up an address space to test partial unmapping.
  171. // Should register the signal handler to deal with this on Windows.
  172. ulong vaSize = 0x100000;
  173. // The first 0x100000 is mapped to start. It is replaced from the center with the 0x200000 mapping.
  174. var backing = new MemoryBlock(vaSize * 2, MemoryAllocationFlags.Mirrorable);
  175. (MemoryBlock mainMemory, MemoryBlock unusedMirror, MemoryEhMeilleure exceptionHandler) = GetVirtual(vaSize * 2);
  176. EnsureTranslator();
  177. ref var state = ref PartialUnmapState.GetRef();
  178. // Create some state to be used for managing the native writing loop.
  179. int stateSize = Unsafe.SizeOf<NativeWriteLoopState>();
  180. var statePtr = Marshal.AllocHGlobal(stateSize);
  181. Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize);
  182. ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr);
  183. writeLoopState.Running = 1;
  184. writeLoopState.Error = 0;
  185. try
  186. {
  187. // Globally reset the struct for handling partial unmap races.
  188. PartialUnmapState.Reset();
  189. // Create a large mapping.
  190. mainMemory.MapView(backing, 0, 0, vaSize);
  191. var writeFunc = TestMethods.GenerateDebugNativeWriteLoop();
  192. IntPtr writePtr = mainMemory.GetPointer(vaSize - 0x1000, 4);
  193. Thread testThread = new Thread(() =>
  194. {
  195. writeFunc(statePtr, writePtr);
  196. });
  197. testThread.Start();
  198. // Create a smaller mapping, covering the larger mapping.
  199. // Immediately try to write to the part of the larger mapping that did not change.
  200. // Do this a lot, with the smaller mapping gradually increasing in size. Should not crash, data should not be lost.
  201. ulong pageSize = 0x1000;
  202. int mappingExpandCount = (int)(vaSize / (pageSize * 2)) - 1;
  203. ulong vaCenter = vaSize / 2;
  204. for (int i = 1; i <= mappingExpandCount; i++)
  205. {
  206. ulong start = vaCenter - (pageSize * (ulong)i);
  207. ulong size = pageSize * (ulong)i * 2;
  208. ulong startPa = start + vaSize;
  209. mainMemory.MapView(backing, startPa, start, size);
  210. }
  211. writeLoopState.Running = 0;
  212. testThread.Join();
  213. Assert.False(writeLoopState.Error != 0);
  214. }
  215. finally
  216. {
  217. Marshal.FreeHGlobal(statePtr);
  218. exceptionHandler.Dispose();
  219. mainMemory.Dispose();
  220. unusedMirror.Dispose();
  221. backing.Dispose();
  222. }
  223. }
  224. [Test]
  225. // Only test in Windows, as this is only used on Windows and uses Windows APIs for trimming.
  226. [Platform("Win")]
  227. [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
  228. public void ThreadLocalMap()
  229. {
  230. PartialUnmapState.Reset();
  231. ref var state = ref PartialUnmapState.GetRef();
  232. bool running = true;
  233. var testThread = new Thread(() =>
  234. {
  235. PartialUnmapState.GetRef().RetryFromAccessViolation();
  236. while (running)
  237. {
  238. Thread.Sleep(1);
  239. }
  240. });
  241. testThread.Start();
  242. Thread.Sleep(200);
  243. Assert.AreEqual(1, CountThreads(ref state));
  244. // Trimming should not remove the thread as it's still active.
  245. state.TrimThreads();
  246. Assert.AreEqual(1, CountThreads(ref state));
  247. running = false;
  248. testThread.Join();
  249. // Should trim now that it's inactive.
  250. state.TrimThreads();
  251. Assert.AreEqual(0, CountThreads(ref state));
  252. }
  253. [Test]
  254. // Only test in Windows, as this is only used on Windows and uses Windows APIs for trimming.
  255. [Platform("Win")]
  256. public unsafe void ThreadLocalMapNative()
  257. {
  258. EnsureTranslator();
  259. PartialUnmapState.Reset();
  260. ref var state = ref PartialUnmapState.GetRef();
  261. fixed (void* localMap = &state.LocalCounts)
  262. {
  263. var getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve((IntPtr)localMap);
  264. for (int i = 0; i < ThreadLocalMap<int>.MapSize; i++)
  265. {
  266. // Should obtain the index matching the call #.
  267. Assert.AreEqual(i, getOrReserve(i + 1, i));
  268. // Check that this and all previously reserved thread IDs and struct contents are intact.
  269. for (int j = 0; j <= i; j++)
  270. {
  271. Assert.AreEqual(j + 1, state.LocalCounts.ThreadIds[j]);
  272. Assert.AreEqual(j, state.LocalCounts.Structs[j]);
  273. }
  274. }
  275. // Trying to reserve again when the map is full should return -1.
  276. Assert.AreEqual(-1, getOrReserve(200, 0));
  277. for (int i = 0; i < ThreadLocalMap<int>.MapSize; i++)
  278. {
  279. // Should obtain the index matching the call #, as it already exists.
  280. Assert.AreEqual(i, getOrReserve(i + 1, -1));
  281. // The struct should not be reset to -1.
  282. Assert.AreEqual(i, state.LocalCounts.Structs[i]);
  283. }
  284. // Clear one of the ids as if it were freed.
  285. state.LocalCounts.ThreadIds[13] = 0;
  286. // GetOrReserve should now obtain and return 13.
  287. Assert.AreEqual(13, getOrReserve(300, 301));
  288. Assert.AreEqual(300, state.LocalCounts.ThreadIds[13]);
  289. Assert.AreEqual(301, state.LocalCounts.Structs[13]);
  290. }
  291. }
  292. [Test]
  293. public void NativeReaderWriterLock()
  294. {
  295. var rwLock = new NativeReaderWriterLock();
  296. var threads = new List<Thread>();
  297. int value = 0;
  298. bool running = true;
  299. bool error = false;
  300. int readersAllowed = 1;
  301. for (int i = 0; i < 5; i++)
  302. {
  303. var readThread = new Thread(() =>
  304. {
  305. int count = 0;
  306. while (running)
  307. {
  308. rwLock.AcquireReaderLock();
  309. int originalValue = Thread.VolatileRead(ref value);
  310. count++;
  311. // Spin a bit.
  312. for (int i = 0; i < 100; i++)
  313. {
  314. if (Thread.VolatileRead(ref readersAllowed) == 0)
  315. {
  316. error = true;
  317. running = false;
  318. }
  319. }
  320. // Should not change while the lock is held.
  321. if (Thread.VolatileRead(ref value) != originalValue)
  322. {
  323. error = true;
  324. running = false;
  325. }
  326. rwLock.ReleaseReaderLock();
  327. }
  328. });
  329. threads.Add(readThread);
  330. }
  331. for (int i = 0; i < 2; i++)
  332. {
  333. var writeThread = new Thread(() =>
  334. {
  335. int count = 0;
  336. while (running)
  337. {
  338. rwLock.AcquireReaderLock();
  339. rwLock.UpgradeToWriterLock();
  340. Thread.Sleep(2);
  341. count++;
  342. Interlocked.Exchange(ref readersAllowed, 0);
  343. for (int i = 0; i < 10; i++)
  344. {
  345. Interlocked.Increment(ref value);
  346. }
  347. Interlocked.Exchange(ref readersAllowed, 1);
  348. rwLock.DowngradeFromWriterLock();
  349. rwLock.ReleaseReaderLock();
  350. Thread.Sleep(1);
  351. }
  352. });
  353. threads.Add(writeThread);
  354. }
  355. foreach (var thread in threads)
  356. {
  357. thread.Start();
  358. }
  359. Thread.Sleep(1000);
  360. running = false;
  361. foreach (var thread in threads)
  362. {
  363. thread.Join();
  364. }
  365. Assert.False(error);
  366. }
  367. }
  368. }