From defa313acccb2935ba8410177c1b7e9e792106dc Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:47:15 +0300 Subject: [PATCH] [GpuMemory] Answer CPU-dirty range queries without the tracker lock MayHaveCpuDirtyPages reads the per-block summary and the region masks without taking the region lock; a true answer still goes through the locked path. Regions also bump a CPU-dirty epoch whenever a page becomes CPU-dirty. --- src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs | 5 ++++ .../GpuMemory/GuestPageTracker.cs | 28 +++++++++++++++++++ src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs | 6 ++++ .../Memory/GpuMemory/GuestPageTrackerTests.cs | 19 +++++++++++++ 4 files changed, 58 insertions(+) diff --git a/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs b/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs index 0703365..7b2f301 100644 --- a/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs +++ b/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs @@ -12,6 +12,11 @@ public sealed class CpuDirtySummary { private const int WordBits = 64; private readonly long[] _words = new long[(TrackerLayout.BlockCount + WordBits - 1) / WordBits]; + private long _epoch; + + public long Epoch => Volatile.Read(ref _epoch); + + public void NoteDirtied() => Interlocked.Increment(ref _epoch); public void Set(ulong block, bool dirty) { diff --git a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs index 0ea4961..b3f1c5b 100644 --- a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs +++ b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs @@ -35,6 +35,34 @@ public sealed class GuestPageTracker }); } + public bool MayHaveCpuDirtyPages(ulong vaddr, ulong size) + { + if (IsKnownCpuClean(vaddr, size)) + { + return false; + } + + var remaining = size; + var index = vaddr / BlockBytes; + var offset = vaddr % BlockBytes; + while (remaining != 0) + { + var bytes = Math.Min(BlockBytes - offset, remaining); + if (Volatile.Read(ref _regions[index]) is not { } region || region.IsModified(WriteOrigin.Cpu, offset, bytes)) + { + return true; + } + + remaining -= bytes; + offset = 0; + index++; + } + + return false; + } + + public long CpuDirtyEpoch => _cpuDirtySummary.Epoch; + public bool HasGpuDirtyPages(ulong vaddr, ulong size) { RejectUploadCallbackReentry(); diff --git a/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs b/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs index 8e4d87a..7dfe95c 100644 --- a/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs +++ b/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs @@ -33,6 +33,7 @@ public sealed class TrackedRegion _writable.Fill(); _readable.Fill(); _summary?.Set(BaseAddress / BlockBytes, dirty: true); + _summary?.NoteDirtied(); } public RegionLock Lock { get; } = new(); @@ -212,6 +213,11 @@ public sealed class TrackedRegion private void UpdateCpuProtection(bool track) { _summary?.Set(BaseAddress / BlockBytes, _cpuDirty.Any); + if (_summary is not null && (_cpuDirty & ~_writable).Any) + { + _summary.NoteDirtied(); + } + var mask = _cpuDirty ^ _writable; _writable = _cpuDirty; if (mask.None) diff --git a/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs index 90b964a..f706ceb 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs @@ -153,6 +153,25 @@ public sealed class GuestPageTrackerTests : IDisposable Release(address, Page * 2); } + [NativePageProtectionFact] + public void LockFreeCpuDirtyQueryAgreesWithTheLockedQuery() + { + var address = Allocate(2); + Assert.True(_tracker.MayHaveCpuDirtyPages(address, Page * 2)); + + _tracker.ForEachUploadRange(address, Page * 2, false, NoRange, NoUpload, preserveCpuWriteHotPages: false); + Assert.False(_tracker.HasCpuDirtyPages(address, Page * 2)); + Assert.False(_tracker.MayHaveCpuDirtyPages(address, Page * 2)); + + _tracker.MarkCpuDirtyPages(address + Page + 16, 32); + Assert.False(_tracker.MayHaveCpuDirtyPages(address, Page)); + Assert.True(_tracker.MayHaveCpuDirtyPages(address, Page * 2)); + Assert.True(_tracker.MayHaveCpuDirtyPages(address + Page + 64, 4)); + + _tracker.UntrackMemory(address, Page * 2); + Release(address, Page * 2); + } + // The lock-free block summary must agree with the precise page masks through every // CPU-dirty transition: upload, explicit mark, write-fault invalidation and GPU writes. [NativePageProtectionFact]