From 609abc8b9b3c05a63bef94c2133550b3c07f97b0 Mon Sep 17 00:00:00 2001 From: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Date: Wed, 26 Apr 2023 04:34:16 +0200 Subject: Rename Ryujinx.Memory.Tests to Ryujinx.Tests.Memory --- .../MockVirtualMemoryManager.cs | 109 ----- .../MultiRegionTrackingTests.cs | 439 ------------------ .../Ryujinx.Memory.Tests.csproj | 18 - src/Ryujinx.Memory.Tests/Tests.cs | 110 ----- src/Ryujinx.Memory.Tests/TrackingTests.cs | 509 --------------------- .../MockVirtualMemoryManager.cs | 109 +++++ .../MultiRegionTrackingTests.cs | 439 ++++++++++++++++++ .../Ryujinx.Tests.Memory.csproj | 18 + src/Ryujinx.Tests.Memory/Tests.cs | 110 +++++ src/Ryujinx.Tests.Memory/TrackingTests.cs | 509 +++++++++++++++++++++ src/Ryujinx.Tests/Ryujinx.Tests.csproj | 2 +- 11 files changed, 1186 insertions(+), 1186 deletions(-) delete mode 100644 src/Ryujinx.Memory.Tests/MockVirtualMemoryManager.cs delete mode 100644 src/Ryujinx.Memory.Tests/MultiRegionTrackingTests.cs delete mode 100644 src/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj delete mode 100644 src/Ryujinx.Memory.Tests/Tests.cs delete mode 100644 src/Ryujinx.Memory.Tests/TrackingTests.cs create mode 100644 src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs create mode 100644 src/Ryujinx.Tests.Memory/MultiRegionTrackingTests.cs create mode 100644 src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj create mode 100644 src/Ryujinx.Tests.Memory/Tests.cs create mode 100644 src/Ryujinx.Tests.Memory/TrackingTests.cs (limited to 'src') diff --git a/src/Ryujinx.Memory.Tests/MockVirtualMemoryManager.cs b/src/Ryujinx.Memory.Tests/MockVirtualMemoryManager.cs deleted file mode 100644 index ef81a461..00000000 --- a/src/Ryujinx.Memory.Tests/MockVirtualMemoryManager.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Ryujinx.Memory.Range; -using System; -using System.Collections.Generic; - -namespace Ryujinx.Memory.Tests -{ - public class MockVirtualMemoryManager : IVirtualMemoryManager - { - public bool Supports4KBPages => true; - - public bool NoMappings = false; - - public event Action<ulong, ulong, MemoryPermission> OnProtect; - - public MockVirtualMemoryManager(ulong size, int pageSize) - { - } - - public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) - { - throw new NotImplementedException(); - } - - public void MapForeign(ulong va, nuint hostAddress, ulong size) - { - throw new NotImplementedException(); - } - - public void Unmap(ulong va, ulong size) - { - throw new NotImplementedException(); - } - - public T Read<T>(ulong va) where T : unmanaged - { - throw new NotImplementedException(); - } - - public void Read(ulong va, Span<byte> data) - { - throw new NotImplementedException(); - } - - public void Write<T>(ulong va, T value) where T : unmanaged - { - throw new NotImplementedException(); - } - - public void Write(ulong va, ReadOnlySpan<byte> data) - { - throw new NotImplementedException(); - } - - public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data) - { - throw new NotImplementedException(); - } - - public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false) - { - throw new NotImplementedException(); - } - - public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) - { - throw new NotImplementedException(); - } - - public ref T GetRef<T>(ulong va) where T : unmanaged - { - throw new NotImplementedException(); - } - - IEnumerable<HostMemoryRange> IVirtualMemoryManager.GetHostRegions(ulong va, ulong size) - { - throw new NotImplementedException(); - } - - IEnumerable<MemoryRange> IVirtualMemoryManager.GetPhysicalRegions(ulong va, ulong size) - { - return NoMappings ? Array.Empty<MemoryRange>() : new MemoryRange[] { new MemoryRange(va, size) }; - } - - public bool IsMapped(ulong va) - { - return true; - } - - public bool IsRangeMapped(ulong va, ulong size) - { - return true; - } - - public ulong GetPhysicalAddress(ulong va) - { - throw new NotImplementedException(); - } - - public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) - { - throw new NotImplementedException(); - } - - public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) - { - OnProtect?.Invoke(va, size, protection); - } - } -} diff --git a/src/Ryujinx.Memory.Tests/MultiRegionTrackingTests.cs b/src/Ryujinx.Memory.Tests/MultiRegionTrackingTests.cs deleted file mode 100644 index 38cb4921..00000000 --- a/src/Ryujinx.Memory.Tests/MultiRegionTrackingTests.cs +++ /dev/null @@ -1,439 +0,0 @@ -using NUnit.Framework; -using Ryujinx.Memory.Tracking; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Ryujinx.Memory.Tests -{ - public class MultiRegionTrackingTests - { - private const int RndCnt = 3; - - private const ulong MemorySize = 0x8000; - private const int PageSize = 4096; - - private MemoryBlock _memoryBlock; - private MemoryTracking _tracking; - private MockVirtualMemoryManager _memoryManager; - - [SetUp] - public void Setup() - { - _memoryBlock = new MemoryBlock(MemorySize); - _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize); - _tracking = new MemoryTracking(_memoryManager, PageSize); - } - - [TearDown] - public void Teardown() - { - _memoryBlock.Dispose(); - } - - private IMultiRegionHandle GetGranular(bool smart, ulong address, ulong size, ulong granularity) - { - return smart ? - _tracking.BeginSmartGranularTracking(address, size, granularity, 0) : - (IMultiRegionHandle)_tracking.BeginGranularTracking(address, size, null, granularity, 0); - } - - private void RandomOrder(Random random, List<int> indices, Action<int> action) - { - List<int> choices = indices.ToList(); - - while (choices.Count > 0) - { - int choice = random.Next(choices.Count); - action(choices[choice]); - choices.RemoveAt(choice); - } - } - - private int ExpectQueryInOrder(IMultiRegionHandle handle, ulong startAddress, ulong size, Func<ulong, bool> addressPredicate) - { - int regionCount = 0; - ulong lastAddress = startAddress; - - handle.QueryModified(startAddress, size, (address, range) => - { - Assert.IsTrue(addressPredicate(address)); // Written pages must be even. - Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order. - lastAddress = address; - regionCount++; - }); - - return regionCount; - } - - private int ExpectQueryInOrder(IMultiRegionHandle handle, ulong startAddress, ulong size, Func<ulong, bool> addressPredicate, int sequenceNumber) - { - int regionCount = 0; - ulong lastAddress = startAddress; - - handle.QueryModified(startAddress, size, (address, range) => - { - Assert.IsTrue(addressPredicate(address)); // Written pages must be even. - Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order. - lastAddress = address; - regionCount++; - }, sequenceNumber); - - return regionCount; - } - - private void PreparePages(IMultiRegionHandle handle, int pageCount, ulong address = 0) - { - Random random = new Random(); - - // Make sure the list has minimum granularity (smart region changes granularity based on requested ranges) - RandomOrder(random, Enumerable.Range(0, pageCount).ToList(), (i) => - { - ulong resultAddress = ulong.MaxValue; - handle.QueryModified((ulong)i * PageSize + address, PageSize, (address, range) => - { - resultAddress = address; - }); - Assert.AreEqual(resultAddress, (ulong)i * PageSize + address); - }); - } - - [Test] - public void DirtyRegionOrdering([Values] bool smart) - { - const int pageCount = 32; - IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * pageCount, PageSize); - - Random random = new Random(); - - PreparePages(handle, pageCount); - - IEnumerable<int> halfRange = Enumerable.Range(0, pageCount / 2); - List<int> odd = halfRange.Select(x => x * 2 + 1).ToList(); - List<int> even = halfRange.Select(x => x * 2).ToList(); - - // Write to all the odd pages. - RandomOrder(random, odd, (i) => - { - _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true); - }); - - int oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 1); - - Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages. - - // Write to all the even pages. - RandomOrder(random, even, (i) => - { - _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true); - }); - - int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 0); - - Assert.AreEqual(evenRegionCount, pageCount / 2); - } - - [Test] - public void SequenceNumber([Values] bool smart) - { - // The sequence number can be used to ignore dirty flags, and defer their consumption until later. - // If a user consumes a dirty flag with sequence number 1, then there is a write to the protected region, - // the dirty flag will not be acknowledged until the sequence number is 2. - - // This is useful for situations where we know that the data was complete when the sequence number was set. - // ...essentially, when that data can only be updated on a future sequence number. - - const int pageCount = 32; - IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * pageCount, PageSize); - - PreparePages(handle, pageCount); - - Random random = new Random(); - - IEnumerable<int> halfRange = Enumerable.Range(0, pageCount / 2); - List<int> odd = halfRange.Select(x => x * 2 + 1).ToList(); - List<int> even = halfRange.Select(x => x * 2).ToList(); - - // Write to all the odd pages. - RandomOrder(random, odd, (i) => - { - _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true); - }); - - int oddRegionCount = 0; - - // Track with sequence number 1. Future dirty flags should only be consumed with sequence number != 1. - // Only track the odd pages, so the even ones don't have their sequence number set. - - foreach (int index in odd) - { - handle.QueryModified((ulong)index * PageSize, PageSize, (address, range) => - { - oddRegionCount++; - }, 1); - } - - Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages. - - // Write to all pages. - - _tracking.VirtualMemoryEvent(0, PageSize * pageCount, true); - - // Only the even regions should be reported for sequence number 1. - - int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 0, 1); - - Assert.AreEqual(evenRegionCount, pageCount / 2); // Must have written to all even pages. - - oddRegionCount = 0; - - handle.QueryModified(0, PageSize * pageCount, (address, range) => { oddRegionCount++; }, 1); - - Assert.AreEqual(oddRegionCount, 0); // Sequence number has not changed, so found no dirty subregions. - - // With sequence number 2, all all pages should be reported as modified. - - oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 1, 2); - - Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages. - } - - [Test] - public void SmartRegionTracking() - { - // Smart multi region handles dynamically change their tracking granularity based on QueryMemory calls. - // This can save on reprotects on larger resources. - - const int pageCount = 32; - IMultiRegionHandle handle = GetGranular(true, 0, PageSize * pageCount, PageSize); - - // Query some large regions to prep the subdivision of the tracking region. - - int[] regionSizes = new int[] { 6, 4, 3, 2, 6, 1 }; - ulong address = 0; - - for (int i = 0; i < regionSizes.Length; i++) - { - int region = regionSizes[i]; - handle.QueryModified(address, (ulong)(PageSize * region), (address, size) => { }); - - // There should be a gap between regions, - // So that they don't combine and we can see the full effects. - address += (ulong)(PageSize * (region + 1)); - } - - // Clear modified. - handle.QueryModified((address, size) => { }); - - // Trigger each region with a 1 byte write. - address = 0; - - for (int i = 0; i < regionSizes.Length; i++) - { - int region = regionSizes[i]; - _tracking.VirtualMemoryEvent(address, 1, true); - address += (ulong)(PageSize * (region + 1)); - } - - int regionInd = 0; - ulong expectedAddress = 0; - - // Expect each region to trigger in its entirety, in address ascending order. - handle.QueryModified((address, size) => { - int region = regionSizes[regionInd++]; - - Assert.AreEqual(address, expectedAddress); - Assert.AreEqual(size, (ulong)(PageSize * region)); - - expectedAddress += (ulong)(PageSize * (region + 1)); - }); - } - - [Test] - public void DisposeMultiHandles([Values] bool smart) - { - // Create and initialize two overlapping Multi Region Handles, with PageSize granularity. - const int pageCount = 32; - const int overlapStart = 16; - - Assert.AreEqual(0, _tracking.GetRegionCount()); - - IMultiRegionHandle handleLow = GetGranular(smart, 0, PageSize * pageCount, PageSize); - PreparePages(handleLow, pageCount); - - Assert.AreEqual(pageCount, _tracking.GetRegionCount()); - - IMultiRegionHandle handleHigh = GetGranular(smart, PageSize * overlapStart, PageSize * pageCount, PageSize); - PreparePages(handleHigh, pageCount, PageSize * overlapStart); - - // Combined pages (and assuming overlapStart <= pageCount) should be pageCount after overlapStart. - int totalPages = overlapStart + pageCount; - - Assert.AreEqual(totalPages, _tracking.GetRegionCount()); - - handleLow.Dispose(); // After disposing one, the pages for the other remain. - - Assert.AreEqual(pageCount, _tracking.GetRegionCount()); - - handleHigh.Dispose(); // After disposing the other, there are no pages left. - - Assert.AreEqual(0, _tracking.GetRegionCount()); - } - - [Test] - public void InheritHandles() - { - // Test merging the following into a granular region handle: - // - 3x gap (creates new granular handles) - // - 3x from multiregion: not dirty, dirty and with action - // - 2x gap - // - 3x single page: not dirty, dirty and with action - // - 3x two page: not dirty, dirty and with action (handle is not reused, but its state is copied to the granular handles) - // - 1x gap - // For a total of 18 pages. - - bool[] actionsTriggered = new bool[3]; - - MultiRegionHandle granular = _tracking.BeginGranularTracking(PageSize * 3, PageSize * 3, null, PageSize, 0); - PreparePages(granular, 3, PageSize * 3); - - // Write to the second handle in the multiregion. - _tracking.VirtualMemoryEvent(PageSize * 4, PageSize, true); - - // Add an action to the third handle in the multiregion. - granular.RegisterAction(PageSize * 5, PageSize, (_, _) => { actionsTriggered[0] = true; }); - - RegionHandle[] singlePages = new RegionHandle[3]; - - for (int i = 0; i < 3; i++) - { - singlePages[i] = _tracking.BeginTracking(PageSize * (8 + (ulong)i), PageSize, 0); - singlePages[i].Reprotect(); - } - - // Write to the second handle. - _tracking.VirtualMemoryEvent(PageSize * 9, PageSize, true); - - // Add an action to the third handle. - singlePages[2].RegisterAction((_, _) => { actionsTriggered[1] = true; }); - - RegionHandle[] doublePages = new RegionHandle[3]; - - for (int i = 0; i < 3; i++) - { - doublePages[i] = _tracking.BeginTracking(PageSize * (11 + (ulong)i * 2), PageSize * 2, 0); - doublePages[i].Reprotect(); - } - - // Write to the second handle. - _tracking.VirtualMemoryEvent(PageSize * 13, PageSize * 2, true); - - // Add an action to the third handle. - doublePages[2].RegisterAction((_, _) => { actionsTriggered[2] = true; }); - - // Finally, create a granular handle that inherits all these handles. - - IEnumerable<IRegionHandle>[] handleGroups = new IEnumerable<IRegionHandle>[] - { - granular.GetHandles(), - singlePages, - doublePages - }; - - MultiRegionHandle combined = _tracking.BeginGranularTracking(0, PageSize * 18, handleGroups.SelectMany((handles) => handles), PageSize, 0); - - bool[] expectedDirty = new bool[] - { - true, true, true, // Gap. - false, true, false, // Multi-region. - true, true, // Gap. - false, true, false, // Individual handles. - false, false, true, true, false, false, // Double size handles. - true // Gap. - }; - - for (int i = 0; i < 18; i++) - { - bool modified = false; - combined.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { modified = true; }); - - Assert.AreEqual(expectedDirty[i], modified); - } - - Assert.AreEqual(new bool[3], actionsTriggered); - - _tracking.VirtualMemoryEvent(PageSize * 5, PageSize, false); - Assert.IsTrue(actionsTriggered[0]); - - _tracking.VirtualMemoryEvent(PageSize * 10, PageSize, false); - Assert.IsTrue(actionsTriggered[1]); - - _tracking.VirtualMemoryEvent(PageSize * 15, PageSize, false); - Assert.IsTrue(actionsTriggered[2]); - - // The double page handles should be disposed, as they were split into granular handles. - foreach (RegionHandle doublePage in doublePages) - { - // These should have been disposed. - bool throws = false; - - try - { - doublePage.Dispose(); - } - catch (ObjectDisposedException) - { - throws = true; - } - - Assert.IsTrue(throws); - } - - IEnumerable<IRegionHandle> combinedHandles = combined.GetHandles(); - - Assert.AreEqual(handleGroups[0].ElementAt(0), combinedHandles.ElementAt(3)); - Assert.AreEqual(handleGroups[0].ElementAt(1), combinedHandles.ElementAt(4)); - Assert.AreEqual(handleGroups[0].ElementAt(2), combinedHandles.ElementAt(5)); - - Assert.AreEqual(singlePages[0], combinedHandles.ElementAt(8)); - Assert.AreEqual(singlePages[1], combinedHandles.ElementAt(9)); - Assert.AreEqual(singlePages[2], combinedHandles.ElementAt(10)); - } - - [Test] - public void PreciseAction() - { - bool actionTriggered = false; - - MultiRegionHandle granular = _tracking.BeginGranularTracking(PageSize * 3, PageSize * 3, null, PageSize, 0); - PreparePages(granular, 3, PageSize * 3); - - // Add a precise action to the second and third handle in the multiregion. - granular.RegisterPreciseAction(PageSize * 4, PageSize * 2, (_, _, _) => { actionTriggered = true; return true; }); - - // Precise write to first handle in the multiregion. - _tracking.VirtualMemoryEvent(PageSize * 3, PageSize, true, precise: true); - Assert.IsFalse(actionTriggered); // Action not triggered. - - bool firstPageModified = false; - granular.QueryModified(PageSize * 3, PageSize, (_, _) => { firstPageModified = true; }); - Assert.IsTrue(firstPageModified); // First page is modified. - - // Precise write to all handles in the multiregion. - _tracking.VirtualMemoryEvent(PageSize * 3, PageSize * 3, true, precise: true); - - bool[] pagesModified = new bool[3]; - - for (int i = 3; i < 6; i++) - { - int index = i - 3; - granular.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { pagesModified[index] = true; }); - } - - Assert.IsTrue(actionTriggered); // Action triggered. - - // Precise writes are ignored on two later handles due to the action returning true. - Assert.AreEqual(pagesModified, new bool[] { true, false, false }); - } - } -} diff --git a/src/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj b/src/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj deleted file mode 100644 index 4dcb6962..00000000 --- a/src/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> - <IsPackable>false</IsPackable> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="NUnit" /> - <PackageReference Include="NUnit3TestAdapter" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" /> - </ItemGroup> - -</Project> diff --git a/src/Ryujinx.Memory.Tests/Tests.cs b/src/Ryujinx.Memory.Tests/Tests.cs deleted file mode 100644 index 2717b76a..00000000 --- a/src/Ryujinx.Memory.Tests/Tests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using NUnit.Framework; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.Memory.Tests -{ - public class Tests - { - private const ulong MemorySize = 0x8000; - - private MemoryBlock _memoryBlock; - - [SetUp] - public void Setup() - { - _memoryBlock = new MemoryBlock(MemorySize); - } - - [TearDown] - public void Teardown() - { - _memoryBlock.Dispose(); - } - - [Test] - public void Test_Read() - { - Marshal.WriteInt32(_memoryBlock.Pointer, 0x2020, 0x1234abcd); - - Assert.AreEqual(_memoryBlock.Read<int>(0x2020), 0x1234abcd); - } - - [Test] - public void Test_Write() - { - _memoryBlock.Write(0x2040, 0xbadc0de); - - Assert.AreEqual(Marshal.ReadInt32(_memoryBlock.Pointer, 0x2040), 0xbadc0de); - } - - [Test] - // Memory aliasing tests fail on CI at the moment. - [Platform(Exclude = "MacOsX")] - public void Test_Alias() - { - using MemoryBlock backing = new MemoryBlock(0x10000, MemoryAllocationFlags.Mirrorable); - using MemoryBlock toAlias = new MemoryBlock(0x10000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); - - toAlias.MapView(backing, 0x1000, 0, 0x4000); - toAlias.UnmapView(backing, 0x3000, 0x1000); - - toAlias.Write(0, 0xbadc0de); - Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, 0x1000), 0xbadc0de); - } - - [Test] - // Memory aliasing tests fail on CI at the moment. - [Platform(Exclude = "MacOsX")] - public void Test_AliasRandom() - { - using MemoryBlock backing = new MemoryBlock(0x80000, MemoryAllocationFlags.Mirrorable); - using MemoryBlock toAlias = new MemoryBlock(0x80000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); - - Random rng = new Random(123); - - for (int i = 0; i < 20000; i++) - { - int srcPage = rng.Next(0, 64); - int dstPage = rng.Next(0, 64); - int pages = rng.Next(1, 65); - - if ((rng.Next() & 1) != 0) - { - toAlias.MapView(backing, (ulong)srcPage << 12, (ulong)dstPage << 12, (ulong)pages << 12); - - int offset = rng.Next(0, 0x1000 - sizeof(int)); - - toAlias.Write((ulong)((dstPage << 12) + offset), 0xbadc0de); - Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, (srcPage << 12) + offset), 0xbadc0de); - } - else - { - toAlias.UnmapView(backing, (ulong)dstPage << 12, (ulong)pages << 12); - } - } - } - - [Test] - // Memory aliasing tests fail on CI at the moment. - [Platform(Exclude = "MacOsX")] - public void Test_AliasMapLeak() - { - ulong pageSize = 4096; - ulong size = 100000 * pageSize; // The mappings limit on Linux is usually around 65K, so let's make sure we are above that. - - using MemoryBlock backing = new MemoryBlock(pageSize, MemoryAllocationFlags.Mirrorable); - using MemoryBlock toAlias = new MemoryBlock(size, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); - - for (ulong offset = 0; offset < size; offset += pageSize) - { - toAlias.MapView(backing, 0, offset, pageSize); - - toAlias.Write(offset, 0xbadc0de); - Assert.AreEqual(0xbadc0de, backing.Read<int>(0)); - - toAlias.UnmapView(backing, offset, pageSize); - } - } - } -} \ No newline at end of file diff --git a/src/Ryujinx.Memory.Tests/TrackingTests.cs b/src/Ryujinx.Memory.Tests/TrackingTests.cs deleted file mode 100644 index eb679804..00000000 --- a/src/Ryujinx.Memory.Tests/TrackingTests.cs +++ /dev/null @@ -1,509 +0,0 @@ -using NUnit.Framework; -using Ryujinx.Memory.Tracking; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; - -namespace Ryujinx.Memory.Tests -{ - public class TrackingTests - { - private const int RndCnt = 3; - - private const ulong MemorySize = 0x8000; - private const int PageSize = 4096; - - private MemoryBlock _memoryBlock; - private MemoryTracking _tracking; - private MockVirtualMemoryManager _memoryManager; - - [SetUp] - public void Setup() - { - _memoryBlock = new MemoryBlock(MemorySize); - _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize); - _tracking = new MemoryTracking(_memoryManager, PageSize); - } - - [TearDown] - public void Teardown() - { - _memoryBlock.Dispose(); - } - - private bool TestSingleWrite(RegionHandle handle, ulong address, ulong size) - { - handle.Reprotect(); - - _tracking.VirtualMemoryEvent(address, size, true); - - return handle.Dirty; - } - - [Test] - public void SingleRegion() - { - RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); - (ulong address, ulong size)? readTrackingTriggered = null; - handle.RegisterAction((address, size) => - { - readTrackingTriggered = (address, size); - }); - - bool dirtyInitial = handle.Dirty; - Assert.True(dirtyInitial); // Handle starts dirty. - - handle.Reprotect(); - - bool dirtyAfterReprotect = handle.Dirty; - Assert.False(dirtyAfterReprotect); // Handle is no longer dirty. - - _tracking.VirtualMemoryEvent(PageSize * 2, 4, true); - _tracking.VirtualMemoryEvent(PageSize * 2, 4, false); - - bool dirtyAfterUnrelatedReadWrite = handle.Dirty; - Assert.False(dirtyAfterUnrelatedReadWrite); // Not dirtied, as the write was to an unrelated address. - - Assert.IsNull(readTrackingTriggered); // Hasn't been triggered yet - - _tracking.VirtualMemoryEvent(0, 4, false); - - bool dirtyAfterRelatedRead = handle.Dirty; - Assert.False(dirtyAfterRelatedRead); // Only triggers on write. - Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered. - - readTrackingTriggered = null; - _tracking.VirtualMemoryEvent(0, 4, true); - - bool dirtyAfterRelatedWrite = handle.Dirty; - Assert.True(dirtyAfterRelatedWrite); // Dirty flag should now be set. - - _tracking.VirtualMemoryEvent(4, 4, true); - bool dirtyAfterRelatedWrite2 = handle.Dirty; - Assert.True(dirtyAfterRelatedWrite2); // Dirty flag should still be set. - - handle.Reprotect(); - - bool dirtyAfterReprotect2 = handle.Dirty; - Assert.False(dirtyAfterReprotect2); // Handle is no longer dirty. - - handle.Dispose(); - - bool dirtyAfterDispose = TestSingleWrite(handle, 0, 4); - Assert.False(dirtyAfterDispose); // Handle cannot be triggered when disposed - } - - [Test] - public void OverlappingRegions() - { - RegionHandle allHandle = _tracking.BeginTracking(0, PageSize * 16, 0); - allHandle.Reprotect(); - - (ulong address, ulong size)? readTrackingTriggeredAll = null; - Action registerReadAction = () => - { - readTrackingTriggeredAll = null; - allHandle.RegisterAction((address, size) => - { - readTrackingTriggeredAll = (address, size); - }); - }; - registerReadAction(); - - // Create 16 page sized handles contained within the allHandle. - RegionHandle[] containedHandles = new RegionHandle[16]; - - for (int i = 0; i < 16; i++) - { - containedHandles[i] = _tracking.BeginTracking((ulong)i * PageSize, PageSize, 0); - containedHandles[i].Reprotect(); - } - - for (int i = 0; i < 16; i++) - { - // No handles are dirty. - Assert.False(allHandle.Dirty); - Assert.IsNull(readTrackingTriggeredAll); - for (int j = 0; j < 16; j++) - { - Assert.False(containedHandles[j].Dirty); - } - - _tracking.VirtualMemoryEvent((ulong)i * PageSize, 1, true); - - // Only the handle covering the entire range and the relevant contained handle are dirty. - Assert.True(allHandle.Dirty); - Assert.AreEqual(readTrackingTriggeredAll, ((ulong)i * PageSize, 1UL)); // Triggered read tracking - for (int j = 0; j < 16; j++) - { - if (j == i) - { - Assert.True(containedHandles[j].Dirty); - } - else - { - Assert.False(containedHandles[j].Dirty); - } - } - - // Clear flags and reset read action. - registerReadAction(); - allHandle.Reprotect(); - containedHandles[i].Reprotect(); - } - } - - [Test] - public void PageAlignment( - [Values(1ul, 512ul, 2048ul, 4096ul, 65536ul)] [Random(1ul, 65536ul, RndCnt)] ulong address, - [Values(1ul, 4ul, 1024ul, 4096ul, 65536ul)] [Random(1ul, 65536ul, RndCnt)] ulong size) - { - ulong alignedStart = (address / PageSize) * PageSize; - ulong alignedEnd = ((address + size + PageSize - 1) / PageSize) * PageSize; - ulong alignedSize = alignedEnd - alignedStart; - - RegionHandle handle = _tracking.BeginTracking(address, size, 0); - - // Anywhere inside the pages the region is contained on should trigger. - - bool originalRangeTriggers = TestSingleWrite(handle, address, size); - Assert.True(originalRangeTriggers); - - bool alignedRangeTriggers = TestSingleWrite(handle, alignedStart, alignedSize); - Assert.True(alignedRangeTriggers); - - bool alignedStartTriggers = TestSingleWrite(handle, alignedStart, 1); - Assert.True(alignedStartTriggers); - - bool alignedEndTriggers = TestSingleWrite(handle, alignedEnd - 1, 1); - Assert.True(alignedEndTriggers); - - // Outside the tracked range should not trigger. - - bool alignedBeforeTriggers = TestSingleWrite(handle, alignedStart - 1, 1); - Assert.False(alignedBeforeTriggers); - - bool alignedAfterTriggers = TestSingleWrite(handle, alignedEnd, 1); - Assert.False(alignedAfterTriggers); - } - - [Test, Explicit, Timeout(1000)] - public void Multithreading() - { - // Multithreading sanity test - // Multiple threads can easily read/write memory regions from any existing handle. - // Handles can also be owned by different threads, though they should have one owner thread. - // Handles can be created and disposed at any time, by any thread. - - // This test should not throw or deadlock due to invalid state. - - const int threadCount = 1; - const int handlesPerThread = 16; - long finishedTime = 0; - - RegionHandle[] handles = new RegionHandle[threadCount * handlesPerThread]; - Random globalRand = new Random(); - - for (int i = 0; i < handles.Length; i++) - { - handles[i] = _tracking.BeginTracking((ulong)i * PageSize, PageSize, 0); - handles[i].Reprotect(); - } - - List<Thread> testThreads = new List<Thread>(); - - // Dirty flag consumer threads - int dirtyFlagReprotects = 0; - for (int i = 0; i < threadCount; i++) - { - int randSeed = i; - testThreads.Add(new Thread(() => - { - int handleBase = randSeed * handlesPerThread; - while (Stopwatch.GetTimestamp() < finishedTime) - { - Random random = new Random(randSeed); - RegionHandle handle = handles[handleBase + random.Next(handlesPerThread)]; - - if (handle.Dirty) - { - handle.Reprotect(); - Interlocked.Increment(ref dirtyFlagReprotects); - } - } - })); - } - - // Write trigger threads - int writeTriggers = 0; - for (int i = 0; i < threadCount; i++) - { - int randSeed = i; - testThreads.Add(new Thread(() => - { - Random random = new Random(randSeed); - ulong handleBase = (ulong)(randSeed * handlesPerThread * PageSize); - while (Stopwatch.GetTimestamp() < finishedTime) - { - _tracking.VirtualMemoryEvent(handleBase + (ulong)random.Next(PageSize * handlesPerThread), PageSize / 2, true); - Interlocked.Increment(ref writeTriggers); - } - })); - } - - // Handle create/delete threads - int handleLifecycles = 0; - for (int i = 0; i < threadCount; i++) - { - int randSeed = i; - testThreads.Add(new Thread(() => - { - int maxAddress = threadCount * handlesPerThread * PageSize; - Random random = new Random(randSeed + 512); - while (Stopwatch.GetTimestamp() < finishedTime) - { - RegionHandle handle = _tracking.BeginTracking((ulong)random.Next(maxAddress), (ulong)random.Next(65536), 0); - - handle.Dispose(); - - Interlocked.Increment(ref handleLifecycles); - } - })); - } - - finishedTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency / 2; // Run for 500ms; - - foreach (Thread thread in testThreads) - { - thread.Start(); - } - - foreach (Thread thread in testThreads) - { - thread.Join(); - } - - Assert.Greater(dirtyFlagReprotects, 10); - Assert.Greater(writeTriggers, 10); - Assert.Greater(handleLifecycles, 10); - } - - [Test] - public void ReadActionThreadConsumption() - { - // Read actions should only be triggered once for each registration. - // The implementation should use an interlocked exchange to make sure other threads can't get the action. - - RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); - - int triggeredCount = 0; - int registeredCount = 0; - int signalThreadsDone = 0; - bool isRegistered = false; - - Action registerReadAction = () => - { - registeredCount++; - handle.RegisterAction((address, size) => - { - isRegistered = false; - Interlocked.Increment(ref triggeredCount); - }); - }; - - const int threadCount = 16; - const int iterationCount = 10000; - Thread[] signalThreads = new Thread[threadCount]; - - for (int i = 0; i < threadCount; i++) - { - int randSeed = i; - signalThreads[i] = new Thread(() => - { - Random random = new Random(randSeed); - for (int j = 0; j < iterationCount; j++) - { - _tracking.VirtualMemoryEvent((ulong)random.Next(PageSize), 4, false); - } - Interlocked.Increment(ref signalThreadsDone); - }); - } - - for (int i = 0; i < threadCount; i++) - { - signalThreads[i].Start(); - } - - while (signalThreadsDone != -1) - { - if (signalThreadsDone == threadCount) - { - signalThreadsDone = -1; - } - - if (!isRegistered) - { - isRegistered = true; - registerReadAction(); - } - } - - // The action should trigger exactly once for every registration, - // then we register once after all the threads signalling it cease. - Assert.AreEqual(registeredCount, triggeredCount + 1); - } - - [Test] - public void DisposeHandles() - { - // Ensure that disposed handles correctly remove their virtual and physical regions. - - RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); - handle.Reprotect(); - - Assert.AreEqual(1, _tracking.GetRegionCount()); - - handle.Dispose(); - - Assert.AreEqual(0, _tracking.GetRegionCount()); - - // Two handles, small entirely contains big. - // We expect there to be three regions after creating both, one for the small region and two covering the big one around it. - // Regions are always split to avoid overlapping, which is why there are three instead of two. - - RegionHandle handleSmall = _tracking.BeginTracking(PageSize, PageSize, 0); - RegionHandle handleBig = _tracking.BeginTracking(0, PageSize * 4, 0); - - Assert.AreEqual(3, _tracking.GetRegionCount()); - - // After disposing the big region, only the small one will remain. - handleBig.Dispose(); - - Assert.AreEqual(1, _tracking.GetRegionCount()); - - handleSmall.Dispose(); - - Assert.AreEqual(0, _tracking.GetRegionCount()); - } - - [Test] - public void ReadAndWriteProtection() - { - MemoryPermission protection = MemoryPermission.ReadAndWrite; - - _memoryManager.OnProtect += (va, size, newProtection) => - { - Assert.AreEqual((0, PageSize), (va, size)); // Should protect the exact region all the operations use. - protection = newProtection; - }; - - RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); - - // After creating the handle, there is no protection yet. - Assert.AreEqual(MemoryPermission.ReadAndWrite, protection); - - bool dirtyInitial = handle.Dirty; - Assert.True(dirtyInitial); // Handle starts dirty. - - handle.Reprotect(); - - // After a reprotect, there is write protection, which will set a dirty flag when any write happens. - Assert.AreEqual(MemoryPermission.Read, protection); - - (ulong address, ulong size)? readTrackingTriggered = null; - handle.RegisterAction((address, size) => - { - readTrackingTriggered = (address, size); - }); - - // Registering an action adds read/write protection. - Assert.AreEqual(MemoryPermission.None, protection); - - bool dirtyAfterReprotect = handle.Dirty; - Assert.False(dirtyAfterReprotect); // Handle is no longer dirty. - - // First we should read, which will trigger the action. This _should not_ remove write protection on the memory. - - _tracking.VirtualMemoryEvent(0, 4, false); - - bool dirtyAfterRead = handle.Dirty; - Assert.False(dirtyAfterRead); // Not dirtied, as this was a read. - - Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered. - - Assert.AreEqual(MemoryPermission.Read, protection); // Write protection is still present. - - readTrackingTriggered = null; - - // Now, perform a write. - - _tracking.VirtualMemoryEvent(0, 4, true); - - bool dirtyAfterWriteAfterRead = handle.Dirty; - Assert.True(dirtyAfterWriteAfterRead); // Should be dirty. - - Assert.AreEqual(MemoryPermission.ReadAndWrite, protection); // All protection is now be removed from the memory. - - Assert.IsNull(readTrackingTriggered); // Read tracking was removed when the action fired, as it can only fire once. - - handle.Dispose(); - } - - [Test] - public void PreciseAction() - { - RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); - - (ulong address, ulong size, bool write)? preciseTriggered = null; - handle.RegisterPreciseAction((address, size, write) => - { - preciseTriggered = (address, size, write); - - return true; - }); - - (ulong address, ulong size)? readTrackingTriggered = null; - handle.RegisterAction((address, size) => - { - readTrackingTriggered = (address, size); - }); - - handle.Reprotect(); - - _tracking.VirtualMemoryEvent(0, 4, false, precise: true); - - Assert.IsNull(readTrackingTriggered); // Hasn't been triggered - precise action returned true. - Assert.AreEqual(preciseTriggered, (0UL, 4UL, false)); // Precise action was triggered. - - _tracking.VirtualMemoryEvent(0, 4, true, precise: true); - - Assert.IsNull(readTrackingTriggered); // Still hasn't been triggered. - bool dirtyAfterPreciseActionTrue = handle.Dirty; - Assert.False(dirtyAfterPreciseActionTrue); // Not dirtied - precise action returned true. - Assert.AreEqual(preciseTriggered, (0UL, 4UL, true)); // Precise action was triggered. - - // Handle is now dirty. - handle.Reprotect(true); - preciseTriggered = null; - - _tracking.VirtualMemoryEvent(4, 4, true, precise: true); - Assert.AreEqual(preciseTriggered, (4UL, 4UL, true)); // Precise action was triggered even though handle was dirty. - - handle.Reprotect(); - handle.RegisterPreciseAction((address, size, write) => - { - preciseTriggered = (address, size, write); - - return false; // Now, we return false, which indicates that the regular read/write behaviours should trigger. - }); - - _tracking.VirtualMemoryEvent(8, 4, true, precise: true); - - Assert.AreEqual(readTrackingTriggered, (8UL, 4UL)); // Read action triggered, as precise action returned false. - bool dirtyAfterPreciseActionFalse = handle.Dirty; - Assert.True(dirtyAfterPreciseActionFalse); // Dirtied, as precise action returned false. - Assert.AreEqual(preciseTriggered, (8UL, 4UL, true)); // Precise action was triggered. - } - } -} diff --git a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs new file mode 100644 index 00000000..ef81a461 --- /dev/null +++ b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs @@ -0,0 +1,109 @@ +using Ryujinx.Memory.Range; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Memory.Tests +{ + public class MockVirtualMemoryManager : IVirtualMemoryManager + { + public bool Supports4KBPages => true; + + public bool NoMappings = false; + + public event Action<ulong, ulong, MemoryPermission> OnProtect; + + public MockVirtualMemoryManager(ulong size, int pageSize) + { + } + + public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) + { + throw new NotImplementedException(); + } + + public void MapForeign(ulong va, nuint hostAddress, ulong size) + { + throw new NotImplementedException(); + } + + public void Unmap(ulong va, ulong size) + { + throw new NotImplementedException(); + } + + public T Read<T>(ulong va) where T : unmanaged + { + throw new NotImplementedException(); + } + + public void Read(ulong va, Span<byte> data) + { + throw new NotImplementedException(); + } + + public void Write<T>(ulong va, T value) where T : unmanaged + { + throw new NotImplementedException(); + } + + public void Write(ulong va, ReadOnlySpan<byte> data) + { + throw new NotImplementedException(); + } + + public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data) + { + throw new NotImplementedException(); + } + + public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false) + { + throw new NotImplementedException(); + } + + public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) + { + throw new NotImplementedException(); + } + + public ref T GetRef<T>(ulong va) where T : unmanaged + { + throw new NotImplementedException(); + } + + IEnumerable<HostMemoryRange> IVirtualMemoryManager.GetHostRegions(ulong va, ulong size) + { + throw new NotImplementedException(); + } + + IEnumerable<MemoryRange> IVirtualMemoryManager.GetPhysicalRegions(ulong va, ulong size) + { + return NoMappings ? Array.Empty<MemoryRange>() : new MemoryRange[] { new MemoryRange(va, size) }; + } + + public bool IsMapped(ulong va) + { + return true; + } + + public bool IsRangeMapped(ulong va, ulong size) + { + return true; + } + + public ulong GetPhysicalAddress(ulong va) + { + throw new NotImplementedException(); + } + + public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + { + throw new NotImplementedException(); + } + + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + { + OnProtect?.Invoke(va, size, protection); + } + } +} diff --git a/src/Ryujinx.Tests.Memory/MultiRegionTrackingTests.cs b/src/Ryujinx.Tests.Memory/MultiRegionTrackingTests.cs new file mode 100644 index 00000000..38cb4921 --- /dev/null +++ b/src/Ryujinx.Tests.Memory/MultiRegionTrackingTests.cs @@ -0,0 +1,439 @@ +using NUnit.Framework; +using Ryujinx.Memory.Tracking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Memory.Tests +{ + public class MultiRegionTrackingTests + { + private const int RndCnt = 3; + + private const ulong MemorySize = 0x8000; + private const int PageSize = 4096; + + private MemoryBlock _memoryBlock; + private MemoryTracking _tracking; + private MockVirtualMemoryManager _memoryManager; + + [SetUp] + public void Setup() + { + _memoryBlock = new MemoryBlock(MemorySize); + _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize); + _tracking = new MemoryTracking(_memoryManager, PageSize); + } + + [TearDown] + public void Teardown() + { + _memoryBlock.Dispose(); + } + + private IMultiRegionHandle GetGranular(bool smart, ulong address, ulong size, ulong granularity) + { + return smart ? + _tracking.BeginSmartGranularTracking(address, size, granularity, 0) : + (IMultiRegionHandle)_tracking.BeginGranularTracking(address, size, null, granularity, 0); + } + + private void RandomOrder(Random random, List<int> indices, Action<int> action) + { + List<int> choices = indices.ToList(); + + while (choices.Count > 0) + { + int choice = random.Next(choices.Count); + action(choices[choice]); + choices.RemoveAt(choice); + } + } + + private int ExpectQueryInOrder(IMultiRegionHandle handle, ulong startAddress, ulong size, Func<ulong, bool> addressPredicate) + { + int regionCount = 0; + ulong lastAddress = startAddress; + + handle.QueryModified(startAddress, size, (address, range) => + { + Assert.IsTrue(addressPredicate(address)); // Written pages must be even. + Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order. + lastAddress = address; + regionCount++; + }); + + return regionCount; + } + + private int ExpectQueryInOrder(IMultiRegionHandle handle, ulong startAddress, ulong size, Func<ulong, bool> addressPredicate, int sequenceNumber) + { + int regionCount = 0; + ulong lastAddress = startAddress; + + handle.QueryModified(startAddress, size, (address, range) => + { + Assert.IsTrue(addressPredicate(address)); // Written pages must be even. + Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order. + lastAddress = address; + regionCount++; + }, sequenceNumber); + + return regionCount; + } + + private void PreparePages(IMultiRegionHandle handle, int pageCount, ulong address = 0) + { + Random random = new Random(); + + // Make sure the list has minimum granularity (smart region changes granularity based on requested ranges) + RandomOrder(random, Enumerable.Range(0, pageCount).ToList(), (i) => + { + ulong resultAddress = ulong.MaxValue; + handle.QueryModified((ulong)i * PageSize + address, PageSize, (address, range) => + { + resultAddress = address; + }); + Assert.AreEqual(resultAddress, (ulong)i * PageSize + address); + }); + } + + [Test] + public void DirtyRegionOrdering([Values] bool smart) + { + const int pageCount = 32; + IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * pageCount, PageSize); + + Random random = new Random(); + + PreparePages(handle, pageCount); + + IEnumerable<int> halfRange = Enumerable.Range(0, pageCount / 2); + List<int> odd = halfRange.Select(x => x * 2 + 1).ToList(); + List<int> even = halfRange.Select(x => x * 2).ToList(); + + // Write to all the odd pages. + RandomOrder(random, odd, (i) => + { + _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true); + }); + + int oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 1); + + Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages. + + // Write to all the even pages. + RandomOrder(random, even, (i) => + { + _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true); + }); + + int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 0); + + Assert.AreEqual(evenRegionCount, pageCount / 2); + } + + [Test] + public void SequenceNumber([Values] bool smart) + { + // The sequence number can be used to ignore dirty flags, and defer their consumption until later. + // If a user consumes a dirty flag with sequence number 1, then there is a write to the protected region, + // the dirty flag will not be acknowledged until the sequence number is 2. + + // This is useful for situations where we know that the data was complete when the sequence number was set. + // ...essentially, when that data can only be updated on a future sequence number. + + const int pageCount = 32; + IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * pageCount, PageSize); + + PreparePages(handle, pageCount); + + Random random = new Random(); + + IEnumerable<int> halfRange = Enumerable.Range(0, pageCount / 2); + List<int> odd = halfRange.Select(x => x * 2 + 1).ToList(); + List<int> even = halfRange.Select(x => x * 2).ToList(); + + // Write to all the odd pages. + RandomOrder(random, odd, (i) => + { + _tracking.VirtualMemoryEvent((ulong)i * PageSize, PageSize, true); + }); + + int oddRegionCount = 0; + + // Track with sequence number 1. Future dirty flags should only be consumed with sequence number != 1. + // Only track the odd pages, so the even ones don't have their sequence number set. + + foreach (int index in odd) + { + handle.QueryModified((ulong)index * PageSize, PageSize, (address, range) => + { + oddRegionCount++; + }, 1); + } + + Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages. + + // Write to all pages. + + _tracking.VirtualMemoryEvent(0, PageSize * pageCount, true); + + // Only the even regions should be reported for sequence number 1. + + int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 0, 1); + + Assert.AreEqual(evenRegionCount, pageCount / 2); // Must have written to all even pages. + + oddRegionCount = 0; + + handle.QueryModified(0, PageSize * pageCount, (address, range) => { oddRegionCount++; }, 1); + + Assert.AreEqual(oddRegionCount, 0); // Sequence number has not changed, so found no dirty subregions. + + // With sequence number 2, all all pages should be reported as modified. + + oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * pageCount, (address) => (address / PageSize) % 2 == 1, 2); + + Assert.AreEqual(oddRegionCount, pageCount / 2); // Must have written to all odd pages. + } + + [Test] + public void SmartRegionTracking() + { + // Smart multi region handles dynamically change their tracking granularity based on QueryMemory calls. + // This can save on reprotects on larger resources. + + const int pageCount = 32; + IMultiRegionHandle handle = GetGranular(true, 0, PageSize * pageCount, PageSize); + + // Query some large regions to prep the subdivision of the tracking region. + + int[] regionSizes = new int[] { 6, 4, 3, 2, 6, 1 }; + ulong address = 0; + + for (int i = 0; i < regionSizes.Length; i++) + { + int region = regionSizes[i]; + handle.QueryModified(address, (ulong)(PageSize * region), (address, size) => { }); + + // There should be a gap between regions, + // So that they don't combine and we can see the full effects. + address += (ulong)(PageSize * (region + 1)); + } + + // Clear modified. + handle.QueryModified((address, size) => { }); + + // Trigger each region with a 1 byte write. + address = 0; + + for (int i = 0; i < regionSizes.Length; i++) + { + int region = regionSizes[i]; + _tracking.VirtualMemoryEvent(address, 1, true); + address += (ulong)(PageSize * (region + 1)); + } + + int regionInd = 0; + ulong expectedAddress = 0; + + // Expect each region to trigger in its entirety, in address ascending order. + handle.QueryModified((address, size) => { + int region = regionSizes[regionInd++]; + + Assert.AreEqual(address, expectedAddress); + Assert.AreEqual(size, (ulong)(PageSize * region)); + + expectedAddress += (ulong)(PageSize * (region + 1)); + }); + } + + [Test] + public void DisposeMultiHandles([Values] bool smart) + { + // Create and initialize two overlapping Multi Region Handles, with PageSize granularity. + const int pageCount = 32; + const int overlapStart = 16; + + Assert.AreEqual(0, _tracking.GetRegionCount()); + + IMultiRegionHandle handleLow = GetGranular(smart, 0, PageSize * pageCount, PageSize); + PreparePages(handleLow, pageCount); + + Assert.AreEqual(pageCount, _tracking.GetRegionCount()); + + IMultiRegionHandle handleHigh = GetGranular(smart, PageSize * overlapStart, PageSize * pageCount, PageSize); + PreparePages(handleHigh, pageCount, PageSize * overlapStart); + + // Combined pages (and assuming overlapStart <= pageCount) should be pageCount after overlapStart. + int totalPages = overlapStart + pageCount; + + Assert.AreEqual(totalPages, _tracking.GetRegionCount()); + + handleLow.Dispose(); // After disposing one, the pages for the other remain. + + Assert.AreEqual(pageCount, _tracking.GetRegionCount()); + + handleHigh.Dispose(); // After disposing the other, there are no pages left. + + Assert.AreEqual(0, _tracking.GetRegionCount()); + } + + [Test] + public void InheritHandles() + { + // Test merging the following into a granular region handle: + // - 3x gap (creates new granular handles) + // - 3x from multiregion: not dirty, dirty and with action + // - 2x gap + // - 3x single page: not dirty, dirty and with action + // - 3x two page: not dirty, dirty and with action (handle is not reused, but its state is copied to the granular handles) + // - 1x gap + // For a total of 18 pages. + + bool[] actionsTriggered = new bool[3]; + + MultiRegionHandle granular = _tracking.BeginGranularTracking(PageSize * 3, PageSize * 3, null, PageSize, 0); + PreparePages(granular, 3, PageSize * 3); + + // Write to the second handle in the multiregion. + _tracking.VirtualMemoryEvent(PageSize * 4, PageSize, true); + + // Add an action to the third handle in the multiregion. + granular.RegisterAction(PageSize * 5, PageSize, (_, _) => { actionsTriggered[0] = true; }); + + RegionHandle[] singlePages = new RegionHandle[3]; + + for (int i = 0; i < 3; i++) + { + singlePages[i] = _tracking.BeginTracking(PageSize * (8 + (ulong)i), PageSize, 0); + singlePages[i].Reprotect(); + } + + // Write to the second handle. + _tracking.VirtualMemoryEvent(PageSize * 9, PageSize, true); + + // Add an action to the third handle. + singlePages[2].RegisterAction((_, _) => { actionsTriggered[1] = true; }); + + RegionHandle[] doublePages = new RegionHandle[3]; + + for (int i = 0; i < 3; i++) + { + doublePages[i] = _tracking.BeginTracking(PageSize * (11 + (ulong)i * 2), PageSize * 2, 0); + doublePages[i].Reprotect(); + } + + // Write to the second handle. + _tracking.VirtualMemoryEvent(PageSize * 13, PageSize * 2, true); + + // Add an action to the third handle. + doublePages[2].RegisterAction((_, _) => { actionsTriggered[2] = true; }); + + // Finally, create a granular handle that inherits all these handles. + + IEnumerable<IRegionHandle>[] handleGroups = new IEnumerable<IRegionHandle>[] + { + granular.GetHandles(), + singlePages, + doublePages + }; + + MultiRegionHandle combined = _tracking.BeginGranularTracking(0, PageSize * 18, handleGroups.SelectMany((handles) => handles), PageSize, 0); + + bool[] expectedDirty = new bool[] + { + true, true, true, // Gap. + false, true, false, // Multi-region. + true, true, // Gap. + false, true, false, // Individual handles. + false, false, true, true, false, false, // Double size handles. + true // Gap. + }; + + for (int i = 0; i < 18; i++) + { + bool modified = false; + combined.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { modified = true; }); + + Assert.AreEqual(expectedDirty[i], modified); + } + + Assert.AreEqual(new bool[3], actionsTriggered); + + _tracking.VirtualMemoryEvent(PageSize * 5, PageSize, false); + Assert.IsTrue(actionsTriggered[0]); + + _tracking.VirtualMemoryEvent(PageSize * 10, PageSize, false); + Assert.IsTrue(actionsTriggered[1]); + + _tracking.VirtualMemoryEvent(PageSize * 15, PageSize, false); + Assert.IsTrue(actionsTriggered[2]); + + // The double page handles should be disposed, as they were split into granular handles. + foreach (RegionHandle doublePage in doublePages) + { + // These should have been disposed. + bool throws = false; + + try + { + doublePage.Dispose(); + } + catch (ObjectDisposedException) + { + throws = true; + } + + Assert.IsTrue(throws); + } + + IEnumerable<IRegionHandle> combinedHandles = combined.GetHandles(); + + Assert.AreEqual(handleGroups[0].ElementAt(0), combinedHandles.ElementAt(3)); + Assert.AreEqual(handleGroups[0].ElementAt(1), combinedHandles.ElementAt(4)); + Assert.AreEqual(handleGroups[0].ElementAt(2), combinedHandles.ElementAt(5)); + + Assert.AreEqual(singlePages[0], combinedHandles.ElementAt(8)); + Assert.AreEqual(singlePages[1], combinedHandles.ElementAt(9)); + Assert.AreEqual(singlePages[2], combinedHandles.ElementAt(10)); + } + + [Test] + public void PreciseAction() + { + bool actionTriggered = false; + + MultiRegionHandle granular = _tracking.BeginGranularTracking(PageSize * 3, PageSize * 3, null, PageSize, 0); + PreparePages(granular, 3, PageSize * 3); + + // Add a precise action to the second and third handle in the multiregion. + granular.RegisterPreciseAction(PageSize * 4, PageSize * 2, (_, _, _) => { actionTriggered = true; return true; }); + + // Precise write to first handle in the multiregion. + _tracking.VirtualMemoryEvent(PageSize * 3, PageSize, true, precise: true); + Assert.IsFalse(actionTriggered); // Action not triggered. + + bool firstPageModified = false; + granular.QueryModified(PageSize * 3, PageSize, (_, _) => { firstPageModified = true; }); + Assert.IsTrue(firstPageModified); // First page is modified. + + // Precise write to all handles in the multiregion. + _tracking.VirtualMemoryEvent(PageSize * 3, PageSize * 3, true, precise: true); + + bool[] pagesModified = new bool[3]; + + for (int i = 3; i < 6; i++) + { + int index = i - 3; + granular.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { pagesModified[index] = true; }); + } + + Assert.IsTrue(actionTriggered); // Action triggered. + + // Precise writes are ignored on two later handles due to the action returning true. + Assert.AreEqual(pagesModified, new bool[] { true, false, false }); + } + } +} diff --git a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj new file mode 100644 index 00000000..4dcb6962 --- /dev/null +++ b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="NUnit" /> + <PackageReference Include="NUnit3TestAdapter" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/Ryujinx.Tests.Memory/Tests.cs b/src/Ryujinx.Tests.Memory/Tests.cs new file mode 100644 index 00000000..2717b76a --- /dev/null +++ b/src/Ryujinx.Tests.Memory/Tests.cs @@ -0,0 +1,110 @@ +using NUnit.Framework; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Memory.Tests +{ + public class Tests + { + private const ulong MemorySize = 0x8000; + + private MemoryBlock _memoryBlock; + + [SetUp] + public void Setup() + { + _memoryBlock = new MemoryBlock(MemorySize); + } + + [TearDown] + public void Teardown() + { + _memoryBlock.Dispose(); + } + + [Test] + public void Test_Read() + { + Marshal.WriteInt32(_memoryBlock.Pointer, 0x2020, 0x1234abcd); + + Assert.AreEqual(_memoryBlock.Read<int>(0x2020), 0x1234abcd); + } + + [Test] + public void Test_Write() + { + _memoryBlock.Write(0x2040, 0xbadc0de); + + Assert.AreEqual(Marshal.ReadInt32(_memoryBlock.Pointer, 0x2040), 0xbadc0de); + } + + [Test] + // Memory aliasing tests fail on CI at the moment. + [Platform(Exclude = "MacOsX")] + public void Test_Alias() + { + using MemoryBlock backing = new MemoryBlock(0x10000, MemoryAllocationFlags.Mirrorable); + using MemoryBlock toAlias = new MemoryBlock(0x10000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); + + toAlias.MapView(backing, 0x1000, 0, 0x4000); + toAlias.UnmapView(backing, 0x3000, 0x1000); + + toAlias.Write(0, 0xbadc0de); + Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, 0x1000), 0xbadc0de); + } + + [Test] + // Memory aliasing tests fail on CI at the moment. + [Platform(Exclude = "MacOsX")] + public void Test_AliasRandom() + { + using MemoryBlock backing = new MemoryBlock(0x80000, MemoryAllocationFlags.Mirrorable); + using MemoryBlock toAlias = new MemoryBlock(0x80000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); + + Random rng = new Random(123); + + for (int i = 0; i < 20000; i++) + { + int srcPage = rng.Next(0, 64); + int dstPage = rng.Next(0, 64); + int pages = rng.Next(1, 65); + + if ((rng.Next() & 1) != 0) + { + toAlias.MapView(backing, (ulong)srcPage << 12, (ulong)dstPage << 12, (ulong)pages << 12); + + int offset = rng.Next(0, 0x1000 - sizeof(int)); + + toAlias.Write((ulong)((dstPage << 12) + offset), 0xbadc0de); + Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, (srcPage << 12) + offset), 0xbadc0de); + } + else + { + toAlias.UnmapView(backing, (ulong)dstPage << 12, (ulong)pages << 12); + } + } + } + + [Test] + // Memory aliasing tests fail on CI at the moment. + [Platform(Exclude = "MacOsX")] + public void Test_AliasMapLeak() + { + ulong pageSize = 4096; + ulong size = 100000 * pageSize; // The mappings limit on Linux is usually around 65K, so let's make sure we are above that. + + using MemoryBlock backing = new MemoryBlock(pageSize, MemoryAllocationFlags.Mirrorable); + using MemoryBlock toAlias = new MemoryBlock(size, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); + + for (ulong offset = 0; offset < size; offset += pageSize) + { + toAlias.MapView(backing, 0, offset, pageSize); + + toAlias.Write(offset, 0xbadc0de); + Assert.AreEqual(0xbadc0de, backing.Read<int>(0)); + + toAlias.UnmapView(backing, offset, pageSize); + } + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Tests.Memory/TrackingTests.cs b/src/Ryujinx.Tests.Memory/TrackingTests.cs new file mode 100644 index 00000000..eb679804 --- /dev/null +++ b/src/Ryujinx.Tests.Memory/TrackingTests.cs @@ -0,0 +1,509 @@ +using NUnit.Framework; +using Ryujinx.Memory.Tracking; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace Ryujinx.Memory.Tests +{ + public class TrackingTests + { + private const int RndCnt = 3; + + private const ulong MemorySize = 0x8000; + private const int PageSize = 4096; + + private MemoryBlock _memoryBlock; + private MemoryTracking _tracking; + private MockVirtualMemoryManager _memoryManager; + + [SetUp] + public void Setup() + { + _memoryBlock = new MemoryBlock(MemorySize); + _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize); + _tracking = new MemoryTracking(_memoryManager, PageSize); + } + + [TearDown] + public void Teardown() + { + _memoryBlock.Dispose(); + } + + private bool TestSingleWrite(RegionHandle handle, ulong address, ulong size) + { + handle.Reprotect(); + + _tracking.VirtualMemoryEvent(address, size, true); + + return handle.Dirty; + } + + [Test] + public void SingleRegion() + { + RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); + (ulong address, ulong size)? readTrackingTriggered = null; + handle.RegisterAction((address, size) => + { + readTrackingTriggered = (address, size); + }); + + bool dirtyInitial = handle.Dirty; + Assert.True(dirtyInitial); // Handle starts dirty. + + handle.Reprotect(); + + bool dirtyAfterReprotect = handle.Dirty; + Assert.False(dirtyAfterReprotect); // Handle is no longer dirty. + + _tracking.VirtualMemoryEvent(PageSize * 2, 4, true); + _tracking.VirtualMemoryEvent(PageSize * 2, 4, false); + + bool dirtyAfterUnrelatedReadWrite = handle.Dirty; + Assert.False(dirtyAfterUnrelatedReadWrite); // Not dirtied, as the write was to an unrelated address. + + Assert.IsNull(readTrackingTriggered); // Hasn't been triggered yet + + _tracking.VirtualMemoryEvent(0, 4, false); + + bool dirtyAfterRelatedRead = handle.Dirty; + Assert.False(dirtyAfterRelatedRead); // Only triggers on write. + Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered. + + readTrackingTriggered = null; + _tracking.VirtualMemoryEvent(0, 4, true); + + bool dirtyAfterRelatedWrite = handle.Dirty; + Assert.True(dirtyAfterRelatedWrite); // Dirty flag should now be set. + + _tracking.VirtualMemoryEvent(4, 4, true); + bool dirtyAfterRelatedWrite2 = handle.Dirty; + Assert.True(dirtyAfterRelatedWrite2); // Dirty flag should still be set. + + handle.Reprotect(); + + bool dirtyAfterReprotect2 = handle.Dirty; + Assert.False(dirtyAfterReprotect2); // Handle is no longer dirty. + + handle.Dispose(); + + bool dirtyAfterDispose = TestSingleWrite(handle, 0, 4); + Assert.False(dirtyAfterDispose); // Handle cannot be triggered when disposed + } + + [Test] + public void OverlappingRegions() + { + RegionHandle allHandle = _tracking.BeginTracking(0, PageSize * 16, 0); + allHandle.Reprotect(); + + (ulong address, ulong size)? readTrackingTriggeredAll = null; + Action registerReadAction = () => + { + readTrackingTriggeredAll = null; + allHandle.RegisterAction((address, size) => + { + readTrackingTriggeredAll = (address, size); + }); + }; + registerReadAction(); + + // Create 16 page sized handles contained within the allHandle. + RegionHandle[] containedHandles = new RegionHandle[16]; + + for (int i = 0; i < 16; i++) + { + containedHandles[i] = _tracking.BeginTracking((ulong)i * PageSize, PageSize, 0); + containedHandles[i].Reprotect(); + } + + for (int i = 0; i < 16; i++) + { + // No handles are dirty. + Assert.False(allHandle.Dirty); + Assert.IsNull(readTrackingTriggeredAll); + for (int j = 0; j < 16; j++) + { + Assert.False(containedHandles[j].Dirty); + } + + _tracking.VirtualMemoryEvent((ulong)i * PageSize, 1, true); + + // Only the handle covering the entire range and the relevant contained handle are dirty. + Assert.True(allHandle.Dirty); + Assert.AreEqual(readTrackingTriggeredAll, ((ulong)i * PageSize, 1UL)); // Triggered read tracking + for (int j = 0; j < 16; j++) + { + if (j == i) + { + Assert.True(containedHandles[j].Dirty); + } + else + { + Assert.False(containedHandles[j].Dirty); + } + } + + // Clear flags and reset read action. + registerReadAction(); + allHandle.Reprotect(); + containedHandles[i].Reprotect(); + } + } + + [Test] + public void PageAlignment( + [Values(1ul, 512ul, 2048ul, 4096ul, 65536ul)] [Random(1ul, 65536ul, RndCnt)] ulong address, + [Values(1ul, 4ul, 1024ul, 4096ul, 65536ul)] [Random(1ul, 65536ul, RndCnt)] ulong size) + { + ulong alignedStart = (address / PageSize) * PageSize; + ulong alignedEnd = ((address + size + PageSize - 1) / PageSize) * PageSize; + ulong alignedSize = alignedEnd - alignedStart; + + RegionHandle handle = _tracking.BeginTracking(address, size, 0); + + // Anywhere inside the pages the region is contained on should trigger. + + bool originalRangeTriggers = TestSingleWrite(handle, address, size); + Assert.True(originalRangeTriggers); + + bool alignedRangeTriggers = TestSingleWrite(handle, alignedStart, alignedSize); + Assert.True(alignedRangeTriggers); + + bool alignedStartTriggers = TestSingleWrite(handle, alignedStart, 1); + Assert.True(alignedStartTriggers); + + bool alignedEndTriggers = TestSingleWrite(handle, alignedEnd - 1, 1); + Assert.True(alignedEndTriggers); + + // Outside the tracked range should not trigger. + + bool alignedBeforeTriggers = TestSingleWrite(handle, alignedStart - 1, 1); + Assert.False(alignedBeforeTriggers); + + bool alignedAfterTriggers = TestSingleWrite(handle, alignedEnd, 1); + Assert.False(alignedAfterTriggers); + } + + [Test, Explicit, Timeout(1000)] + public void Multithreading() + { + // Multithreading sanity test + // Multiple threads can easily read/write memory regions from any existing handle. + // Handles can also be owned by different threads, though they should have one owner thread. + // Handles can be created and disposed at any time, by any thread. + + // This test should not throw or deadlock due to invalid state. + + const int threadCount = 1; + const int handlesPerThread = 16; + long finishedTime = 0; + + RegionHandle[] handles = new RegionHandle[threadCount * handlesPerThread]; + Random globalRand = new Random(); + + for (int i = 0; i < handles.Length; i++) + { + handles[i] = _tracking.BeginTracking((ulong)i * PageSize, PageSize, 0); + handles[i].Reprotect(); + } + + List<Thread> testThreads = new List<Thread>(); + + // Dirty flag consumer threads + int dirtyFlagReprotects = 0; + for (int i = 0; i < threadCount; i++) + { + int randSeed = i; + testThreads.Add(new Thread(() => + { + int handleBase = randSeed * handlesPerThread; + while (Stopwatch.GetTimestamp() < finishedTime) + { + Random random = new Random(randSeed); + RegionHandle handle = handles[handleBase + random.Next(handlesPerThread)]; + + if (handle.Dirty) + { + handle.Reprotect(); + Interlocked.Increment(ref dirtyFlagReprotects); + } + } + })); + } + + // Write trigger threads + int writeTriggers = 0; + for (int i = 0; i < threadCount; i++) + { + int randSeed = i; + testThreads.Add(new Thread(() => + { + Random random = new Random(randSeed); + ulong handleBase = (ulong)(randSeed * handlesPerThread * PageSize); + while (Stopwatch.GetTimestamp() < finishedTime) + { + _tracking.VirtualMemoryEvent(handleBase + (ulong)random.Next(PageSize * handlesPerThread), PageSize / 2, true); + Interlocked.Increment(ref writeTriggers); + } + })); + } + + // Handle create/delete threads + int handleLifecycles = 0; + for (int i = 0; i < threadCount; i++) + { + int randSeed = i; + testThreads.Add(new Thread(() => + { + int maxAddress = threadCount * handlesPerThread * PageSize; + Random random = new Random(randSeed + 512); + while (Stopwatch.GetTimestamp() < finishedTime) + { + RegionHandle handle = _tracking.BeginTracking((ulong)random.Next(maxAddress), (ulong)random.Next(65536), 0); + + handle.Dispose(); + + Interlocked.Increment(ref handleLifecycles); + } + })); + } + + finishedTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency / 2; // Run for 500ms; + + foreach (Thread thread in testThreads) + { + thread.Start(); + } + + foreach (Thread thread in testThreads) + { + thread.Join(); + } + + Assert.Greater(dirtyFlagReprotects, 10); + Assert.Greater(writeTriggers, 10); + Assert.Greater(handleLifecycles, 10); + } + + [Test] + public void ReadActionThreadConsumption() + { + // Read actions should only be triggered once for each registration. + // The implementation should use an interlocked exchange to make sure other threads can't get the action. + + RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); + + int triggeredCount = 0; + int registeredCount = 0; + int signalThreadsDone = 0; + bool isRegistered = false; + + Action registerReadAction = () => + { + registeredCount++; + handle.RegisterAction((address, size) => + { + isRegistered = false; + Interlocked.Increment(ref triggeredCount); + }); + }; + + const int threadCount = 16; + const int iterationCount = 10000; + Thread[] signalThreads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + int randSeed = i; + signalThreads[i] = new Thread(() => + { + Random random = new Random(randSeed); + for (int j = 0; j < iterationCount; j++) + { + _tracking.VirtualMemoryEvent((ulong)random.Next(PageSize), 4, false); + } + Interlocked.Increment(ref signalThreadsDone); + }); + } + + for (int i = 0; i < threadCount; i++) + { + signalThreads[i].Start(); + } + + while (signalThreadsDone != -1) + { + if (signalThreadsDone == threadCount) + { + signalThreadsDone = -1; + } + + if (!isRegistered) + { + isRegistered = true; + registerReadAction(); + } + } + + // The action should trigger exactly once for every registration, + // then we register once after all the threads signalling it cease. + Assert.AreEqual(registeredCount, triggeredCount + 1); + } + + [Test] + public void DisposeHandles() + { + // Ensure that disposed handles correctly remove their virtual and physical regions. + + RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); + handle.Reprotect(); + + Assert.AreEqual(1, _tracking.GetRegionCount()); + + handle.Dispose(); + + Assert.AreEqual(0, _tracking.GetRegionCount()); + + // Two handles, small entirely contains big. + // We expect there to be three regions after creating both, one for the small region and two covering the big one around it. + // Regions are always split to avoid overlapping, which is why there are three instead of two. + + RegionHandle handleSmall = _tracking.BeginTracking(PageSize, PageSize, 0); + RegionHandle handleBig = _tracking.BeginTracking(0, PageSize * 4, 0); + + Assert.AreEqual(3, _tracking.GetRegionCount()); + + // After disposing the big region, only the small one will remain. + handleBig.Dispose(); + + Assert.AreEqual(1, _tracking.GetRegionCount()); + + handleSmall.Dispose(); + + Assert.AreEqual(0, _tracking.GetRegionCount()); + } + + [Test] + public void ReadAndWriteProtection() + { + MemoryPermission protection = MemoryPermission.ReadAndWrite; + + _memoryManager.OnProtect += (va, size, newProtection) => + { + Assert.AreEqual((0, PageSize), (va, size)); // Should protect the exact region all the operations use. + protection = newProtection; + }; + + RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); + + // After creating the handle, there is no protection yet. + Assert.AreEqual(MemoryPermission.ReadAndWrite, protection); + + bool dirtyInitial = handle.Dirty; + Assert.True(dirtyInitial); // Handle starts dirty. + + handle.Reprotect(); + + // After a reprotect, there is write protection, which will set a dirty flag when any write happens. + Assert.AreEqual(MemoryPermission.Read, protection); + + (ulong address, ulong size)? readTrackingTriggered = null; + handle.RegisterAction((address, size) => + { + readTrackingTriggered = (address, size); + }); + + // Registering an action adds read/write protection. + Assert.AreEqual(MemoryPermission.None, protection); + + bool dirtyAfterReprotect = handle.Dirty; + Assert.False(dirtyAfterReprotect); // Handle is no longer dirty. + + // First we should read, which will trigger the action. This _should not_ remove write protection on the memory. + + _tracking.VirtualMemoryEvent(0, 4, false); + + bool dirtyAfterRead = handle.Dirty; + Assert.False(dirtyAfterRead); // Not dirtied, as this was a read. + + Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered. + + Assert.AreEqual(MemoryPermission.Read, protection); // Write protection is still present. + + readTrackingTriggered = null; + + // Now, perform a write. + + _tracking.VirtualMemoryEvent(0, 4, true); + + bool dirtyAfterWriteAfterRead = handle.Dirty; + Assert.True(dirtyAfterWriteAfterRead); // Should be dirty. + + Assert.AreEqual(MemoryPermission.ReadAndWrite, protection); // All protection is now be removed from the memory. + + Assert.IsNull(readTrackingTriggered); // Read tracking was removed when the action fired, as it can only fire once. + + handle.Dispose(); + } + + [Test] + public void PreciseAction() + { + RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); + + (ulong address, ulong size, bool write)? preciseTriggered = null; + handle.RegisterPreciseAction((address, size, write) => + { + preciseTriggered = (address, size, write); + + return true; + }); + + (ulong address, ulong size)? readTrackingTriggered = null; + handle.RegisterAction((address, size) => + { + readTrackingTriggered = (address, size); + }); + + handle.Reprotect(); + + _tracking.VirtualMemoryEvent(0, 4, false, precise: true); + + Assert.IsNull(readTrackingTriggered); // Hasn't been triggered - precise action returned true. + Assert.AreEqual(preciseTriggered, (0UL, 4UL, false)); // Precise action was triggered. + + _tracking.VirtualMemoryEvent(0, 4, true, precise: true); + + Assert.IsNull(readTrackingTriggered); // Still hasn't been triggered. + bool dirtyAfterPreciseActionTrue = handle.Dirty; + Assert.False(dirtyAfterPreciseActionTrue); // Not dirtied - precise action returned true. + Assert.AreEqual(preciseTriggered, (0UL, 4UL, true)); // Precise action was triggered. + + // Handle is now dirty. + handle.Reprotect(true); + preciseTriggered = null; + + _tracking.VirtualMemoryEvent(4, 4, true, precise: true); + Assert.AreEqual(preciseTriggered, (4UL, 4UL, true)); // Precise action was triggered even though handle was dirty. + + handle.Reprotect(); + handle.RegisterPreciseAction((address, size, write) => + { + preciseTriggered = (address, size, write); + + return false; // Now, we return false, which indicates that the regular read/write behaviours should trigger. + }); + + _tracking.VirtualMemoryEvent(8, 4, true, precise: true); + + Assert.AreEqual(readTrackingTriggered, (8UL, 4UL)); // Read action triggered, as precise action returned false. + bool dirtyAfterPreciseActionFalse = handle.Dirty; + Assert.True(dirtyAfterPreciseActionFalse); // Dirtied, as precise action returned false. + Assert.AreEqual(preciseTriggered, (8UL, 4UL, true)); // Precise action was triggered. + } + } +} diff --git a/src/Ryujinx.Tests/Ryujinx.Tests.csproj b/src/Ryujinx.Tests/Ryujinx.Tests.csproj index 5976e998..ab331ce5 100644 --- a/src/Ryujinx.Tests/Ryujinx.Tests.csproj +++ b/src/Ryujinx.Tests/Ryujinx.Tests.csproj @@ -27,7 +27,7 @@ <ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" /> <ProjectReference Include="..\Ryujinx.Cpu\Ryujinx.Cpu.csproj" /> <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" /> - <ProjectReference Include="..\Ryujinx.Memory.Tests\Ryujinx.Memory.Tests.csproj" /> + <ProjectReference Include="..\Ryujinx.Tests.Memory\Ryujinx.Tests.Memory.csproj" /> <ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" /> <ProjectReference Include="..\Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj" /> <ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" /> -- cgit v1.2.3-70-g09d2