using System; using System.Collections.Generic; using System.Linq; using System.Reflection.Metadata; using System.Threading; namespace Ryujinx.Memory.Tracking { /// /// A tracking handle for a given region of virtual memory. The Dirty flag is updated whenever any changes are made, /// and an action can be performed when the region is read to or written from. /// public class RegionHandle : IRegionHandle { /// /// If more than this number of checks have been performed on a dirty flag since its last reprotect, /// then it is dirtied infrequently. /// private const int CheckCountForInfrequent = 3; /// /// Number of frequent dirty/consume in a row to make this handle volatile. /// private const int VolatileThreshold = 5; public bool Dirty { get { return Bitmap.IsSet(DirtyBit); } protected set { Bitmap.Set(DirtyBit, value); } } internal int SequenceNumber { get; set; } internal int Id { get; } public bool Unmapped { get; private set; } public ulong Address { get; } public ulong Size { get; } public ulong EndAddress { get; } public ulong RealAddress { get; } public ulong RealSize { get; } public ulong RealEndAddress { get; } internal IMultiRegionHandle Parent { get; set; } private event Action _onDirty; private readonly object _preActionLock = new(); private RegionSignal _preAction; // Action to perform before a read or write. This will block the memory access. private PreciseRegionSignal _preciseAction; // Action to perform on a precise read or write. private readonly List _regions; private readonly MemoryTracking _tracking; private bool _disposed; private int _checkCount = 0; private int _volatileCount = 0; private bool _volatile; internal MemoryPermission RequiredPermission { get { // If this is unmapped, allow reprotecting as RW as it can't be dirtied. // This is required for the partial unmap cases where part of the data are still being accessed. if (Unmapped) { return MemoryPermission.ReadAndWrite; } if (_preAction != null) { return MemoryPermission.None; } return Dirty ? MemoryPermission.ReadAndWrite : MemoryPermission.Read; } } internal RegionSignal PreAction => _preAction; internal ConcurrentBitmap Bitmap; internal int DirtyBit; /// /// Create a new bitmap backed region handle. The handle is registered with the given tracking object, /// and will be notified of any changes to the specified region. /// /// Tracking object for the target memory block /// Virtual address of the region to track /// Size of the region to track /// The real, unaligned address of the handle /// The real, unaligned size of the handle /// The bitmap the dirty flag for this handle is stored in /// The bit index representing the dirty flag for this handle /// Handle ID /// True if the region handle starts mapped internal RegionHandle( MemoryTracking tracking, ulong address, ulong size, ulong realAddress, ulong realSize, ConcurrentBitmap bitmap, int bit, int id, bool mapped = true) { Bitmap = bitmap; DirtyBit = bit; Dirty = mapped; Id = id; Unmapped = !mapped; Address = address; Size = size; EndAddress = address + size; RealAddress = realAddress; RealSize = realSize; RealEndAddress = realAddress + realSize; _tracking = tracking; _regions = tracking.GetVirtualRegionsForHandle(address, size); foreach (var region in _regions) { region.Handles.Add(this); } } /// /// Create a new region handle. The handle is registered with the given tracking object, /// and will be notified of any changes to the specified region. /// /// Tracking object for the target memory block /// Virtual address of the region to track /// Size of the region to track /// The real, unaligned address of the handle /// The real, unaligned size of the handle /// Handle ID /// True if the region handle starts mapped internal RegionHandle(MemoryTracking tracking, ulong address, ulong size, ulong realAddress, ulong realSize, int id, bool mapped = true) { Bitmap = new ConcurrentBitmap(1, mapped); Id = id; Unmapped = !mapped; Address = address; Size = size; EndAddress = address + size; RealAddress = realAddress; RealSize = realSize; RealEndAddress = realAddress + realSize; _tracking = tracking; _regions = tracking.GetVirtualRegionsForHandle(address, size); foreach (var region in _regions) { region.Handles.Add(this); } } /// /// Replace the bitmap and bit index used to track dirty state. /// /// /// The tracking lock should be held when this is called, to ensure neither bitmap is modified. /// /// The bitmap the dirty flag for this handle is stored in /// The bit index representing the dirty flag for this handle internal void ReplaceBitmap(ConcurrentBitmap bitmap, int bit) { // Assumes the tracking lock is held, so nothing else can signal right now. var oldBitmap = Bitmap; var oldBit = DirtyBit; bitmap.Set(bit, Dirty); Bitmap = bitmap; DirtyBit = bit; Dirty |= oldBitmap.IsSet(oldBit); } /// /// Clear the volatile state of this handle. /// private void ClearVolatile() { _volatileCount = 0; _volatile = false; } /// /// Check if this handle is dirty, or if it is volatile. (changes very often) /// /// True if the handle is dirty or volatile, false otherwise public bool DirtyOrVolatile() { _checkCount++; return _volatile || Dirty; } /// /// Signal that a memory action occurred within this handle's virtual regions. /// /// Address accessed /// Size of the region affected in bytes /// Whether the region was written to or read /// Reference to the handles being iterated, in case the list needs to be copied internal void Signal(ulong address, ulong size, bool write, ref IList handleIterable) { // If this handle was already unmapped (even if just partially), // then we have nothing to do until it is mapped again. // The pre-action should be still consumed to avoid flushing on remap. if (Unmapped) { Interlocked.Exchange(ref _preAction, null); return; } if (_preAction != null) { // Limit the range to within this handle. ulong maxAddress = Math.Max(address, RealAddress); ulong minEndAddress = Math.Min(address + size, RealAddress + RealSize); // Copy the handles list in case it changes when we're out of the lock. if (handleIterable is List) { handleIterable = handleIterable.ToArray(); } // Temporarily release the tracking lock while we're running the action. Monitor.Exit(_tracking.TrackingLock); try { lock (_preActionLock) { _preAction?.Invoke(maxAddress, minEndAddress - maxAddress); // The action is removed after it returns, to ensure that the null check above succeeds when // it's still in progress rather than continuing and possibly missing a required data flush. Interlocked.Exchange(ref _preAction, null); } } finally { Monitor.Enter(_tracking.TrackingLock); } } if (write) { bool oldDirty = Dirty; Dirty = true; if (!oldDirty) { _onDirty?.Invoke(); } Parent?.SignalWrite(); } } /// /// Signal that a precise memory action occurred within this handle's virtual regions. /// If there is no precise action, or the action returns false, the normal signal handler will be called. /// /// Address accessed /// Size of the region affected in bytes /// Whether the region was written to or read /// Reference to the handles being iterated, in case the list needs to be copied /// True if a precise action was performed and returned true, false otherwise internal bool SignalPrecise(ulong address, ulong size, bool write, ref IList handleIterable) { if (!Unmapped && _preciseAction != null && _preciseAction(address, size, write)) { return true; } Signal(address, size, write, ref handleIterable); return false; } /// /// Force this handle to be dirty, without reprotecting. /// public void ForceDirty() { Dirty = true; } /// /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write. /// /// True if the handle should be reprotected as dirty, rather than have it cleared /// True if this reprotect is the result of consecutive dirty checks public void Reprotect(bool asDirty, bool consecutiveCheck = false) { if (_volatile) return; Dirty = asDirty; bool protectionChanged = false; lock (_tracking.TrackingLock) { foreach (VirtualRegion region in _regions) { protectionChanged |= region.UpdateProtection(); } } if (!protectionChanged) { // Counteract the check count being incremented when this handle was forced dirty. // It doesn't count for protected write tracking. _checkCount--; } else if (!asDirty) { if (consecutiveCheck || (_checkCount > 0 && _checkCount < CheckCountForInfrequent)) { if (++_volatileCount >= VolatileThreshold && _preAction == null) { _volatile = true; return; } } else { _volatileCount = 0; } _checkCount = 0; } } /// /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write. /// /// True if the handle should be reprotected as dirty, rather than have it cleared public void Reprotect(bool asDirty = false) { Reprotect(asDirty, false); } /// /// Register an action to perform when the tracked region is read or written. /// The action is automatically removed after it runs. /// /// Action to call on read or write public void RegisterAction(RegionSignal action) { ClearVolatile(); lock (_preActionLock) { RegionSignal lastAction = Interlocked.Exchange(ref _preAction, action); if (lastAction == null && action != lastAction) { lock (_tracking.TrackingLock) { foreach (VirtualRegion region in _regions) { region.UpdateProtection(); } } } } } /// /// Register an action to perform when a precise access occurs (one with exact address and size). /// If the action returns true, read/write tracking are skipped. /// /// Action to call on read or write public void RegisterPreciseAction(PreciseRegionSignal action) { _preciseAction = action; } /// /// Register an action to perform when the region is written to. /// This action will not be removed when it is called - it is called each time the dirty flag is set. /// /// Action to call on dirty public void RegisterDirtyEvent(Action action) { _onDirty += action; } /// /// Add a child virtual region to this handle. /// /// Virtual region to add as a child internal void AddChild(VirtualRegion region) { _regions.Add(region); } /// /// Signal that this handle has been mapped or unmapped. /// /// True if the handle has been mapped, false if unmapped internal void SignalMappingChanged(bool mapped) { if (Unmapped == mapped) { Unmapped = !mapped; if (Unmapped) { ClearVolatile(); Dirty = false; } } } /// /// Check if this region overlaps with another. /// /// Base address /// Size of the region /// True if overlapping, false otherwise public bool OverlapsWith(ulong address, ulong size) { return Address < address + size && address < EndAddress; } /// /// Determines if this handle's memory range matches another exactly. /// /// The other handle /// True on a match, false otherwise public bool RangeEquals(RegionHandle other) { return RealAddress == other.RealAddress && RealSize == other.RealSize; } /// /// Dispose the handle. Within the tracking lock, this removes references from virtual regions. /// public void Dispose() { ObjectDisposedException.ThrowIf(_disposed, this); _disposed = true; lock (_tracking.TrackingLock) { foreach (VirtualRegion region in _regions) { region.RemoveHandle(this); } } } } }