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