MultiRegionTrackingTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. using NUnit.Framework;
  2. using Ryujinx.Memory;
  3. using Ryujinx.Memory.Tracking;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. namespace Ryujinx.Tests.Memory
  8. {
  9. public class MultiRegionTrackingTests
  10. {
  11. private const int RndCnt = 3;
  12. private const ulong MemorySize = 0x8000;
  13. private const int PageSize = 4096;
  14. private MemoryBlock _memoryBlock;
  15. private MemoryTracking _tracking;
  16. private MockVirtualMemoryManager _memoryManager;
  17. [SetUp]
  18. public void Setup()
  19. {
  20. _memoryBlock = new MemoryBlock(MemorySize);
  21. _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize);
  22. _tracking = new MemoryTracking(_memoryManager, PageSize);
  23. }
  24. [TearDown]
  25. public void Teardown()
  26. {
  27. _memoryBlock.Dispose();
  28. }
  29. private IMultiRegionHandle GetGranular(bool smart, ulong address, ulong size, ulong granularity)
  30. {
  31. return smart ?
  32. _tracking.BeginSmartGranularTracking(address, size, granularity, 0) :
  33. (IMultiRegionHandle)_tracking.BeginGranularTracking(address, size, null, granularity, 0);
  34. }
  35. private void RandomOrder(Random random, List<int> indices, Action<int> action)
  36. {
  37. List<int> choices = indices.ToList();
  38. while (choices.Count > 0)
  39. {
  40. int choice = random.Next(choices.Count);
  41. action(choices[choice]);
  42. choices.RemoveAt(choice);
  43. }
  44. }
  45. private int ExpectQueryInOrder(IMultiRegionHandle handle, ulong startAddress, ulong size, Func<ulong, bool> addressPredicate)
  46. {
  47. int regionCount = 0;
  48. ulong lastAddress = startAddress;
  49. handle.QueryModified(startAddress, size, (address, range) =>
  50. {
  51. Assert.IsTrue(addressPredicate(address)); // Written pages must be even.
  52. Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order.
  53. lastAddress = address;
  54. regionCount++;
  55. });
  56. return regionCount;
  57. }
  58. private int ExpectQueryInOrder(IMultiRegionHandle handle, ulong startAddress, ulong size, Func<ulong, bool> addressPredicate, int sequenceNumber)
  59. {
  60. int regionCount = 0;
  61. ulong lastAddress = startAddress;
  62. handle.QueryModified(startAddress, size, (address, range) =>
  63. {
  64. Assert.IsTrue(addressPredicate(address)); // Written pages must be even.
  65. Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order.
  66. lastAddress = address;
  67. regionCount++;
  68. }, sequenceNumber);
  69. return regionCount;
  70. }
  71. private void PreparePages(IMultiRegionHandle handle, int pageCount, ulong address = 0)
  72. {
  73. Random random = new Random();
  74. // Make sure the list has minimum granularity (smart region changes granularity based on requested ranges)
  75. RandomOrder(random, Enumerable.Range(0, pageCount).ToList(), (i) =>
  76. {
  77. ulong resultAddress = ulong.MaxValue;
  78. handle.QueryModified((ulong)i * PageSize + address, PageSize, (address, range) =>
  79. {
  80. resultAddress = address;
  81. });
  82. Assert.AreEqual(resultAddress, (ulong)i * PageSize + address);
  83. });
  84. }
  85. [Test]
  86. public void DirtyRegionOrdering([Values] bool smart)
  87. {
  88. const int pageCount = 32;
  89. IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * pageCount, PageSize);
  90. Random random = new Random();
  91. PreparePages(handle, pageCount);
  92. IEnumerable<int> halfRange = Enumerable.Range(0, pageCount / 2);
  93. List<int> odd = halfRange.Select(x => x * 2 + 1).ToList();
  94. List<int> even = halfRange.Select(x => x * 2).ToList();
  95. // Write to all the odd pages.
  96. RandomOrder(random, odd, (i) =>
  97. {
  98. _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true);
  99. });
  100. int oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 1);
  101. Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages.
  102. // Write to all the even pages.
  103. RandomOrder(random, even, (i) =>
  104. {
  105. _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true);
  106. });
  107. int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 0);
  108. Assert.AreEqual(evenRegionCount, pageCount / 2);
  109. }
  110. [Test]
  111. public void SequenceNumber([Values] bool smart)
  112. {
  113. // The sequence number can be used to ignore dirty flags, and defer their consumption until later.
  114. // If a user consumes a dirty flag with sequence number 1, then there is a write to the protected region,
  115. // the dirty flag will not be acknowledged until the sequence number is 2.
  116. // This is useful for situations where we know that the data was complete when the sequence number was set.
  117. // ...essentially, when that data can only be updated on a future sequence number.
  118. const int pageCount = 32;
  119. IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * pageCount, PageSize);
  120. PreparePages(handle, pageCount);
  121. Random random = new Random();
  122. IEnumerable<int> halfRange = Enumerable.Range(0, pageCount / 2);
  123. List<int> odd = halfRange.Select(x => x * 2 + 1).ToList();
  124. List<int> even = halfRange.Select(x => x * 2).ToList();
  125. // Write to all the odd pages.
  126. RandomOrder(random, odd, (i) =>
  127. {
  128. _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true);
  129. });
  130. int oddRegionCount = 0;
  131. // Track with sequence number 1. Future dirty flags should only be consumed with sequence number != 1.
  132. // Only track the odd pages, so the even ones don't have their sequence number set.
  133. foreach (int index in odd)
  134. {
  135. handle.QueryModified((ulong)index * PageSize, PageSize, (address, range) =>
  136. {
  137. oddRegionCount++;
  138. }, 1);
  139. }
  140. Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages.
  141. // Write to all pages.
  142. _tracking.VirtualMemoryEvent(0, PageSize * pageCount, true);
  143. // Only the even regions should be reported for sequence number 1.
  144. int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 0, 1);
  145. Assert.AreEqual(evenRegionCount, pageCount / 2); // Must have written to all even pages.
  146. oddRegionCount = 0;
  147. handle.QueryModified(0, PageSize * pageCount, (address, range) => { oddRegionCount++; }, 1);
  148. Assert.AreEqual(oddRegionCount, 0); // Sequence number has not changed, so found no dirty subregions.
  149. // With sequence number 2, all all pages should be reported as modified.
  150. oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 1, 2);
  151. Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages.
  152. }
  153. [Test]
  154. public void SmartRegionTracking()
  155. {
  156. // Smart multi region handles dynamically change their tracking granularity based on QueryMemory calls.
  157. // This can save on reprotects on larger resources.
  158. const int pageCount = 32;
  159. IMultiRegionHandle handle = GetGranular(true, 0, PageSize * pageCount, PageSize);
  160. // Query some large regions to prep the subdivision of the tracking region.
  161. int[] regionSizes = new int[] { 6, 4, 3, 2, 6, 1 };
  162. ulong address = 0;
  163. for (int i = 0; i < regionSizes.Length; i++)
  164. {
  165. int region = regionSizes[i];
  166. handle.QueryModified(address, (ulong)(PageSize * region), (address, size) => { });
  167. // There should be a gap between regions,
  168. // So that they don't combine and we can see the full effects.
  169. address += (ulong)(PageSize * (region + 1));
  170. }
  171. // Clear modified.
  172. handle.QueryModified((address, size) => { });
  173. // Trigger each region with a 1 byte write.
  174. address = 0;
  175. for (int i = 0; i < regionSizes.Length; i++)
  176. {
  177. int region = regionSizes[i];
  178. _tracking.VirtualMemoryEvent(address, 1, true);
  179. address += (ulong)(PageSize * (region + 1));
  180. }
  181. int regionInd = 0;
  182. ulong expectedAddress = 0;
  183. // Expect each region to trigger in its entirety, in address ascending order.
  184. handle.QueryModified((address, size) => {
  185. int region = regionSizes[regionInd++];
  186. Assert.AreEqual(address, expectedAddress);
  187. Assert.AreEqual(size, (ulong)(PageSize * region));
  188. expectedAddress += (ulong)(PageSize * (region + 1));
  189. });
  190. }
  191. [Test]
  192. public void DisposeMultiHandles([Values] bool smart)
  193. {
  194. // Create and initialize two overlapping Multi Region Handles, with PageSize granularity.
  195. const int pageCount = 32;
  196. const int overlapStart = 16;
  197. Assert.AreEqual(0, _tracking.GetRegionCount());
  198. IMultiRegionHandle handleLow = GetGranular(smart, 0, PageSize * pageCount, PageSize);
  199. PreparePages(handleLow, pageCount);
  200. Assert.AreEqual(pageCount, _tracking.GetRegionCount());
  201. IMultiRegionHandle handleHigh = GetGranular(smart, PageSize * overlapStart, PageSize * pageCount, PageSize);
  202. PreparePages(handleHigh, pageCount, PageSize * overlapStart);
  203. // Combined pages (and assuming overlapStart <= pageCount) should be pageCount after overlapStart.
  204. int totalPages = overlapStart + pageCount;
  205. Assert.AreEqual(totalPages, _tracking.GetRegionCount());
  206. handleLow.Dispose(); // After disposing one, the pages for the other remain.
  207. Assert.AreEqual(pageCount, _tracking.GetRegionCount());
  208. handleHigh.Dispose(); // After disposing the other, there are no pages left.
  209. Assert.AreEqual(0, _tracking.GetRegionCount());
  210. }
  211. [Test]
  212. public void InheritHandles()
  213. {
  214. // Test merging the following into a granular region handle:
  215. // - 3x gap (creates new granular handles)
  216. // - 3x from multiregion: not dirty, dirty and with action
  217. // - 2x gap
  218. // - 3x single page: not dirty, dirty and with action
  219. // - 3x two page: not dirty, dirty and with action (handle is not reused, but its state is copied to the granular handles)
  220. // - 1x gap
  221. // For a total of 18 pages.
  222. bool[] actionsTriggered = new bool[3];
  223. MultiRegionHandle granular = _tracking.BeginGranularTracking(PageSize * 3, PageSize * 3, null, PageSize, 0);
  224. PreparePages(granular, 3, PageSize * 3);
  225. // Write to the second handle in the multiregion.
  226. _tracking.VirtualMemoryEvent(PageSize * 4, PageSize, true);
  227. // Add an action to the third handle in the multiregion.
  228. granular.RegisterAction(PageSize * 5, PageSize, (_, _) => { actionsTriggered[0] = true; });
  229. RegionHandle[] singlePages = new RegionHandle[3];
  230. for (int i = 0; i < 3; i++)
  231. {
  232. singlePages[i] = _tracking.BeginTracking(PageSize * (8 + (ulong)i), PageSize, 0);
  233. singlePages[i].Reprotect();
  234. }
  235. // Write to the second handle.
  236. _tracking.VirtualMemoryEvent(PageSize * 9, PageSize, true);
  237. // Add an action to the third handle.
  238. singlePages[2].RegisterAction((_, _) => { actionsTriggered[1] = true; });
  239. RegionHandle[] doublePages = new RegionHandle[3];
  240. for (int i = 0; i < 3; i++)
  241. {
  242. doublePages[i] = _tracking.BeginTracking(PageSize * (11 + (ulong)i * 2), PageSize * 2, 0);
  243. doublePages[i].Reprotect();
  244. }
  245. // Write to the second handle.
  246. _tracking.VirtualMemoryEvent(PageSize * 13, PageSize * 2, true);
  247. // Add an action to the third handle.
  248. doublePages[2].RegisterAction((_, _) => { actionsTriggered[2] = true; });
  249. // Finally, create a granular handle that inherits all these handles.
  250. IEnumerable<IRegionHandle>[] handleGroups = new IEnumerable<IRegionHandle>[]
  251. {
  252. granular.GetHandles(),
  253. singlePages,
  254. doublePages
  255. };
  256. MultiRegionHandle combined = _tracking.BeginGranularTracking(0, PageSize * 18, handleGroups.SelectMany((handles) => handles), PageSize, 0);
  257. bool[] expectedDirty = new bool[]
  258. {
  259. true, true, true, // Gap.
  260. false, true, false, // Multi-region.
  261. true, true, // Gap.
  262. false, true, false, // Individual handles.
  263. false, false, true, true, false, false, // Double size handles.
  264. true // Gap.
  265. };
  266. for (int i = 0; i < 18; i++)
  267. {
  268. bool modified = false;
  269. combined.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { modified = true; });
  270. Assert.AreEqual(expectedDirty[i], modified);
  271. }
  272. Assert.AreEqual(new bool[3], actionsTriggered);
  273. _tracking.VirtualMemoryEvent(PageSize * 5, PageSize, false);
  274. Assert.IsTrue(actionsTriggered[0]);
  275. _tracking.VirtualMemoryEvent(PageSize * 10, PageSize, false);
  276. Assert.IsTrue(actionsTriggered[1]);
  277. _tracking.VirtualMemoryEvent(PageSize * 15, PageSize, false);
  278. Assert.IsTrue(actionsTriggered[2]);
  279. // The double page handles should be disposed, as they were split into granular handles.
  280. foreach (RegionHandle doublePage in doublePages)
  281. {
  282. // These should have been disposed.
  283. bool throws = false;
  284. try
  285. {
  286. doublePage.Dispose();
  287. }
  288. catch (ObjectDisposedException)
  289. {
  290. throws = true;
  291. }
  292. Assert.IsTrue(throws);
  293. }
  294. IEnumerable<IRegionHandle> combinedHandles = combined.GetHandles();
  295. Assert.AreEqual(handleGroups[0].ElementAt(0), combinedHandles.ElementAt(3));
  296. Assert.AreEqual(handleGroups[0].ElementAt(1), combinedHandles.ElementAt(4));
  297. Assert.AreEqual(handleGroups[0].ElementAt(2), combinedHandles.ElementAt(5));
  298. Assert.AreEqual(singlePages[0], combinedHandles.ElementAt(8));
  299. Assert.AreEqual(singlePages[1], combinedHandles.ElementAt(9));
  300. Assert.AreEqual(singlePages[2], combinedHandles.ElementAt(10));
  301. }
  302. [Test]
  303. public void PreciseAction()
  304. {
  305. bool actionTriggered = false;
  306. MultiRegionHandle granular = _tracking.BeginGranularTracking(PageSize * 3, PageSize * 3, null, PageSize, 0);
  307. PreparePages(granular, 3, PageSize * 3);
  308. // Add a precise action to the second and third handle in the multiregion.
  309. granular.RegisterPreciseAction(PageSize * 4, PageSize * 2, (_, _, _) => { actionTriggered = true; return true; });
  310. // Precise write to first handle in the multiregion.
  311. _tracking.VirtualMemoryEvent(PageSize * 3, PageSize, true, precise: true);
  312. Assert.IsFalse(actionTriggered); // Action not triggered.
  313. bool firstPageModified = false;
  314. granular.QueryModified(PageSize * 3, PageSize, (_, _) => { firstPageModified = true; });
  315. Assert.IsTrue(firstPageModified); // First page is modified.
  316. // Precise write to all handles in the multiregion.
  317. _tracking.VirtualMemoryEvent(PageSize * 3, PageSize * 3, true, precise: true);
  318. bool[] pagesModified = new bool[3];
  319. for (int i = 3; i < 6; i++)
  320. {
  321. int index = i - 3;
  322. granular.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { pagesModified[index] = true; });
  323. }
  324. Assert.IsTrue(actionTriggered); // Action triggered.
  325. // Precise writes are ignored on two later handles due to the action returning true.
  326. Assert.AreEqual(pagesModified, new bool[] { true, false, false });
  327. }
  328. }
  329. }