mirror of
https://github.com/Ryujinx/Ryujinx.git
synced 2025-06-28 13:40:47 -07:00
Memory Read/Write Tracking using Region Handles (#1272)
* WIP Range Tracking - Texture invalidation seems to have large problems - Buffer/Pool invalidation may have problems - Mirror memory tracking puts an additional `add` in compiled code, we likely just want to make HLE access slower if this is the final solution. - Native project is in the messiest possible location. - [HACK] JIT memory access always uses native "fast" path - [HACK] Trying some things with texture invalidation and views. It works :) Still a few hacks, messy things, slow things More work in progress stuff (also move to memory project) Quite a bit faster now. - Unmapping GPU VA and CPU VA will now correctly update write tracking regions, and invalidate textures for the former. - The Virtual range list is now non-overlapping like the physical one. - Fixed some bugs where regions could leak. - Introduced a weird bug that I still need to track down (consistent invalid buffer in MK8 ribbon road) Move some stuff. I think we'll eventually just put the dll and so for this in a nuget package. Fix rebase. [WIP] MultiRegionHandle variable size ranges - Avoid reprotecting regions that change often (needs some tweaking) - There's still a bug in buffers, somehow. - Might want different api for minimum granularity Fix rebase issue Commit everything needed for software only tracking. Remove native components. Remove more native stuff. Cleanup Use a separate window for the background context, update opentk. (fixes linux) Some experimental changes Should get things working up to scratch - still need to try some things with flush/modification and res scale. Include address with the region action. Initial work to make range tracking work Still a ton of bugs Fix some issues with the new stuff. * Fix texture flush instability There's still some weird behaviour, but it's much improved without this. (textures with cpu modified data were flushing over it) * Find the destination texture for Buffer->Texture full copy Greatly improves performance for nvdec videos (with range tracking) * Further improve texture tracking * Disable Memory Tracking for view parents This is a temporary approach to better match behaviour on master (where invalidations would be soaked up by views, rather than trigger twice) The assumption is that when views are created to a texture, they will cover all of its data anyways. Of course, this can easily be improved in future. * Introduce some tracking tests. WIP * Complete base tests. * Add more tests for multiregion, fix existing test. * Cleanup Part 1 * Remove unnecessary code from memory tracking * Fix some inconsistencies with 3D texture rule. * Add dispose tests. * Use a background thread for the background context. Rather than setting and unsetting a context as current, doing the work on a dedicated thread with signals seems to be a bit faster. Also nerf the multithreading test a bit. * Copy to texture with matching alignment This extends the copy to work for some videos with unusual size, such as tutorial videos in SMO. It will only occur if the destination texture already exists at XCount size. * Track reads for buffer copies. Synchronize new buffers before copying overlaps. * Remove old texture flushing mechanisms. Range tracking all the way, baby. * Wake the background thread when disposing. Avoids a deadlock when games are closed. * Address Feedback 1 * Separate TextureCopy instance for background thread Also `BackgroundContextWorker.InBackground` for a more sensible idenfifier for if we're in a background thread. * Add missing XML docs. * Address Feedback * Maybe I should start drinking coffee. * Some more feedback. * Remove flush warning, Refocus window after making background context
This commit is contained in:
23
Ryujinx.Memory.Tests/MockVirtualMemoryManager.cs
Normal file
23
Ryujinx.Memory.Tests/MockVirtualMemoryManager.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Ryujinx.Memory.Tracking;
|
||||
|
||||
namespace Ryujinx.Memory.Tests
|
||||
{
|
||||
class MockVirtualMemoryManager : IVirtualMemoryManager
|
||||
{
|
||||
public bool NoMappings;
|
||||
|
||||
public MockVirtualMemoryManager(ulong size, int pageSize)
|
||||
{
|
||||
}
|
||||
|
||||
public (ulong address, ulong size)[] GetPhysicalRegions(ulong va, ulong size)
|
||||
{
|
||||
return NoMappings ? new (ulong address, ulong size)[0] : new (ulong address, ulong size)[] { (va, size) };
|
||||
}
|
||||
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
283
Ryujinx.Memory.Tests/MultiRegionTrackingTests.cs
Normal file
283
Ryujinx.Memory.Tests/MultiRegionTrackingTests.cs
Normal file
@ -0,0 +1,283 @@
|
||||
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, _memoryBlock, 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) :
|
||||
(IMultiRegionHandle)_tracking.BeginGranularTracking(address, size, granularity);
|
||||
}
|
||||
|
||||
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, 0), _tracking.GetRegionCounts());
|
||||
|
||||
IMultiRegionHandle handleLow = GetGranular(smart, 0, PageSize * pageCount, PageSize);
|
||||
PreparePages(handleLow, pageCount);
|
||||
|
||||
Assert.AreEqual((pageCount, pageCount), _tracking.GetRegionCounts());
|
||||
|
||||
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, totalPages), _tracking.GetRegionCounts());
|
||||
|
||||
handleLow.Dispose(); // After disposing one, the pages for the other remain.
|
||||
|
||||
Assert.AreEqual((pageCount, pageCount), _tracking.GetRegionCounts());
|
||||
|
||||
handleHigh.Dispose(); // After disposing the other, there are no pages left.
|
||||
|
||||
Assert.AreEqual((0, 0), _tracking.GetRegionCounts());
|
||||
}
|
||||
}
|
||||
}
|
425
Ryujinx.Memory.Tests/TrackingTests.cs
Normal file
425
Ryujinx.Memory.Tests/TrackingTests.cs
Normal file
@ -0,0 +1,425 @@
|
||||
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, _memoryBlock, PageSize);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
_memoryBlock.Dispose();
|
||||
}
|
||||
|
||||
private bool TestSingleWrite(RegionHandle handle, ulong address, ulong size, bool physical = false)
|
||||
{
|
||||
handle.Reprotect();
|
||||
if (physical)
|
||||
{
|
||||
_tracking.PhysicalMemoryEvent(address, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tracking.VirtualMemoryEvent(address, size, true);
|
||||
}
|
||||
return handle.Dirty;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SingleRegion()
|
||||
{
|
||||
RegionHandle handle = _tracking.BeginTracking(0, PageSize);
|
||||
(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
|
||||
|
||||
bool dirtyAfterDispose2 = TestSingleWrite(handle, 0, 4, true);
|
||||
Assert.False(dirtyAfterDispose2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlappingRegions()
|
||||
{
|
||||
RegionHandle allHandle = _tracking.BeginTracking(0, PageSize * 16);
|
||||
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);
|
||||
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);
|
||||
|
||||
// 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, 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);
|
||||
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));
|
||||
|
||||
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);
|
||||
|
||||
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 PhysicalMemoryMapping()
|
||||
{
|
||||
// Tracking is done in the virtual space usually, but we also support tracking on physical regions.
|
||||
// The physical regions that make up a virtual region are determined when the region is created,
|
||||
// or when a mapping changes.
|
||||
|
||||
// These tests verify that the region cannot be signalled after unmapping, and can after remapping.
|
||||
|
||||
RegionHandle handle = _tracking.BeginTracking(PageSize, PageSize);
|
||||
|
||||
Assert.True(handle.Dirty);
|
||||
|
||||
bool trackedWriteTriggers = TestSingleWrite(handle, PageSize, 1, true);
|
||||
Assert.True(trackedWriteTriggers);
|
||||
|
||||
_memoryManager.NoMappings = true;
|
||||
_tracking.Unmap(PageSize, PageSize);
|
||||
bool unmappedWriteTriggers = TestSingleWrite(handle, PageSize, 1, true);
|
||||
Assert.False(unmappedWriteTriggers);
|
||||
|
||||
_memoryManager.NoMappings = false;
|
||||
_tracking.Map(PageSize, PageSize, PageSize);
|
||||
bool remappedWriteTriggers = TestSingleWrite(handle, PageSize, 1, true);
|
||||
Assert.True(remappedWriteTriggers);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisposeHandles()
|
||||
{
|
||||
// Ensure that disposed handles correctly remove their virtual and physical regions.
|
||||
|
||||
RegionHandle handle = _tracking.BeginTracking(0, PageSize);
|
||||
handle.Reprotect();
|
||||
|
||||
Assert.AreEqual((1, 1), _tracking.GetRegionCounts());
|
||||
|
||||
handle.Dispose();
|
||||
|
||||
Assert.AreEqual((0, 0), _tracking.GetRegionCounts());
|
||||
|
||||
// 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);
|
||||
RegionHandle handleBig = _tracking.BeginTracking(0, PageSize * 4);
|
||||
|
||||
Assert.AreEqual((3, 3), _tracking.GetRegionCounts());
|
||||
|
||||
// After disposing the big region, only the small one will remain.
|
||||
handleBig.Dispose();
|
||||
|
||||
Assert.AreEqual((1, 1), _tracking.GetRegionCounts());
|
||||
|
||||
handleSmall.Dispose();
|
||||
|
||||
Assert.AreEqual((0, 0), _tracking.GetRegionCounts());
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user