diff --git a/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs b/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs index 7b2f301..fa083f1 100644 --- a/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs +++ b/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs @@ -33,4 +33,6 @@ public sealed class CpuDirtySummary } public bool IsDirty(ulong block) => (Volatile.Read(ref _words[block / WordBits]) & (1L << (int)(block % WordBits))) != 0; + + public ulong Word(int index) => (ulong)Volatile.Read(ref _words[index]); } diff --git a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs index b3f1c5b..0ac893a 100644 --- a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs +++ b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs @@ -15,6 +15,7 @@ public sealed class GuestPageTracker private readonly PageGuard _pages; private readonly TrackedRegion?[] _regions = new TrackedRegion?[TrackerLayout.BlockCount]; + private readonly long[] _presentRegions = new long[(TrackerLayout.BlockCount + 63) / 64]; private readonly object _regionGate = new(); private readonly CpuDirtySummary _cpuDirtySummary = new(); @@ -448,19 +449,42 @@ public sealed class GuestPageTracker var last = (end - 1) / BlockBytes; var runStart = 0UL; var inRun = false; - for (var index = vaddr / BlockBytes; index <= last; index++) + var index = vaddr / BlockBytes; + var loadedWord = -1; + var possiblyDirty = 0UL; + while (index <= last) { - var possiblyDirty = Volatile.Read(ref _regions[index]) == null || _cpuDirtySummary.IsDirty(index); - if (possiblyDirty && !inRun) + var word = (int)(index / 64); + if (word != loadedWord) { - runStart = Math.Max(vaddr, index * BlockBytes); - inRun = true; + loadedWord = word; + possiblyDirty = ~(ulong)Volatile.Read(ref _presentRegions[word]) | _cpuDirtySummary.Word(word); } - else if (!possiblyDirty && inRun) + + var shift = (int)(index % 64); + var pending = (inRun ? ~possiblyDirty : possiblyDirty) >> shift; + if (pending == 0) + { + index = ((ulong)word + 1) * 64; + continue; + } + + index += (ulong)System.Numerics.BitOperations.TrailingZeroCount(pending); + if (index > last) + { + break; + } + + if (inRun) { visit(runStart, index * BlockBytes - runStart); inRun = false; } + else + { + runStart = Math.Max(vaddr, index * BlockBytes); + inRun = true; + } } if (inRun) @@ -524,6 +548,7 @@ public sealed class GuestPageTracker var created = new TrackedRegion(_pages, index * BlockBytes, _cpuDirtySummary); Volatile.Write(ref _regions[index], created); + Interlocked.Or(ref _presentRegions[index / 64], 1L << (int)(index % 64)); return created; } } diff --git a/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs index f706ceb..58a8fec 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs @@ -238,6 +238,32 @@ public sealed class GuestPageTrackerTests : IDisposable Release(address, Region * 3); } + [NativePageProtectionFact] + public void PossiblyCpuDirtyRangesCrossSummaryWords() + { + var address = AllocateAligned(Region * 2, Region * 64); + List<(ulong Address, ulong Size)> Runs(ulong start, ulong size) + { + var runs = new List<(ulong Address, ulong Size)>(); + _tracker.ForEachPossiblyCpuDirtyRange(start, size, (runAddress, runSize) => runs.Add((runAddress, runSize))); + return runs; + } + + _tracker.ForEachUploadRange(address, Region * 2, false, NoRange, NoUpload, preserveCpuWriteHotPages: false); + Assert.Equal([(address - Region * 2 + Page, Region * 2 - Page)], Runs(address - Region * 2 + Page, Region * 4 - Page * 2)); + + _tracker.MarkCpuDirtyPages(address + Region + Page, 8); + Assert.Equal([(address - Region * 2, Region * 2), (address + Region, Region - Page)], Runs(address - Region * 2, Region * 4 - Page)); + Assert.Equal([(address + Region, Region)], Runs(address, Region * 2)); + Assert.Equal([(address + Region, Region * 64)], Runs(address, Region * 65)); + + _tracker.ForEachUploadRange(address, Region * 2, false, NoRange, NoUpload, preserveCpuWriteHotPages: false); + Assert.Empty(Runs(address, Region * 2)); + Assert.Equal([(address + Region * 2, Region * 63)], Runs(address, Region * 65)); + _tracker.UntrackMemory(address, Region * 2); + Release(address, Region * 2); + } + [NativePageProtectionFact] public void RangeInvalidationBatchesOwnershipTransferAcrossRegions() {