/ src / Ryujinx.Memory / Tracking / RegionHandle.cs
RegionHandle.cs
  1  using System;
  2  using System.Collections.Generic;
  3  using System.Linq;
  4  using System.Threading;
  5  
  6  namespace Ryujinx.Memory.Tracking
  7  {
  8      /// <summary>
  9      /// A tracking handle for a given region of virtual memory. The Dirty flag is updated whenever any changes are made,
 10      /// and an action can be performed when the region is read to or written from.
 11      /// </summary>
 12      public class RegionHandle : IRegionHandle
 13      {
 14          /// <summary>
 15          /// If more than this number of checks have been performed on a dirty flag since its last reprotect,
 16          /// then it is dirtied infrequently.
 17          /// </summary>
 18          private const int CheckCountForInfrequent = 3;
 19  
 20          /// <summary>
 21          /// Number of frequent dirty/consume in a row to make this handle volatile.
 22          /// </summary>
 23          private const int VolatileThreshold = 5;
 24  
 25          public bool Dirty
 26          {
 27              get
 28              {
 29                  return Bitmap.IsSet(DirtyBit);
 30              }
 31              protected set
 32              {
 33                  Bitmap.Set(DirtyBit, value);
 34              }
 35          }
 36  
 37          internal int SequenceNumber { get; set; }
 38          internal int Id { get; }
 39  
 40          public bool Unmapped { get; private set; }
 41  
 42          public ulong Address { get; }
 43          public ulong Size { get; }
 44          public ulong EndAddress { get; }
 45  
 46          public ulong RealAddress { get; }
 47          public ulong RealSize { get; }
 48          public ulong RealEndAddress { get; }
 49  
 50          internal IMultiRegionHandle Parent { get; set; }
 51  
 52          private event Action OnDirty;
 53  
 54          private readonly object _preActionLock = new();
 55          private RegionSignal _preAction; // Action to perform before a read or write. This will block the memory access.
 56          private PreciseRegionSignal _preciseAction; // Action to perform on a precise read or write.
 57          private readonly List<VirtualRegion> _regions;
 58          private readonly List<VirtualRegion> _guestRegions;
 59          private readonly List<VirtualRegion> _allRegions;
 60          private readonly MemoryTracking _tracking;
 61          private bool _disposed;
 62  
 63          private int _checkCount = 0;
 64          private int _volatileCount = 0;
 65          private bool _volatile;
 66  
 67          internal MemoryPermission RequiredPermission
 68          {
 69              get
 70              {
 71                  // If this is unmapped, allow reprotecting as RW as it can't be dirtied.
 72                  // This is required for the partial unmap cases where part of the data are still being accessed.
 73                  if (Unmapped)
 74                  {
 75                      return MemoryPermission.ReadAndWrite;
 76                  }
 77  
 78                  if (_preAction != null)
 79                  {
 80                      return MemoryPermission.None;
 81                  }
 82  
 83                  return Dirty ? MemoryPermission.ReadAndWrite : MemoryPermission.Read;
 84              }
 85          }
 86  
 87          internal RegionSignal PreAction => _preAction;
 88  
 89          internal ConcurrentBitmap Bitmap;
 90          internal int DirtyBit;
 91  
 92          /// <summary>
 93          /// Create a new bitmap backed region handle. The handle is registered with the given tracking object,
 94          /// and will be notified of any changes to the specified region.
 95          /// </summary>
 96          /// <param name="tracking">Tracking object for the target memory block</param>
 97          /// <param name="address">Virtual address of the region to track</param>
 98          /// <param name="size">Size of the region to track</param>
 99          /// <param name="realAddress">The real, unaligned address of the handle</param>
100          /// <param name="realSize">The real, unaligned size of the handle</param>
101          /// <param name="bitmap">The bitmap the dirty flag for this handle is stored in</param>
102          /// <param name="bit">The bit index representing the dirty flag for this handle</param>
103          /// <param name="id">Handle ID</param>
104          /// <param name="flags">Region flags</param>
105          /// <param name="mapped">True if the region handle starts mapped</param>
106          internal RegionHandle(
107              MemoryTracking tracking,
108              ulong address,
109              ulong size,
110              ulong realAddress,
111              ulong realSize,
112              ConcurrentBitmap bitmap,
113              int bit,
114              int id,
115              RegionFlags flags,
116              bool mapped = true)
117          {
118              Bitmap = bitmap;
119              DirtyBit = bit;
120  
121              Dirty = mapped;
122  
123              Id = id;
124  
125              Unmapped = !mapped;
126              Address = address;
127              Size = size;
128              EndAddress = address + size;
129  
130              RealAddress = realAddress;
131              RealSize = realSize;
132              RealEndAddress = realAddress + realSize;
133  
134              _tracking = tracking;
135  
136              _regions = tracking.GetVirtualRegionsForHandle(address, size, false);
137              _guestRegions = GetGuestRegions(tracking, address, size, flags);
138              _allRegions = new List<VirtualRegion>(_regions.Count + _guestRegions.Count);
139  
140              InitializeRegions();
141          }
142  
143          /// <summary>
144          /// Create a new region handle. The handle is registered with the given tracking object,
145          /// and will be notified of any changes to the specified region.
146          /// </summary>
147          /// <param name="tracking">Tracking object for the target memory block</param>
148          /// <param name="address">Virtual address of the region to track</param>
149          /// <param name="size">Size of the region to track</param>
150          /// <param name="realAddress">The real, unaligned address of the handle</param>
151          /// <param name="realSize">The real, unaligned size of the handle</param>
152          /// <param name="id">Handle ID</param>
153          /// <param name="flags">Region flags</param>
154          /// <param name="mapped">True if the region handle starts mapped</param>
155          internal RegionHandle(MemoryTracking tracking, ulong address, ulong size, ulong realAddress, ulong realSize, int id, RegionFlags flags, bool mapped = true)
156          {
157              Bitmap = new ConcurrentBitmap(1, mapped);
158  
159              Id = id;
160  
161              Unmapped = !mapped;
162  
163              Address = address;
164              Size = size;
165              EndAddress = address + size;
166  
167              RealAddress = realAddress;
168              RealSize = realSize;
169              RealEndAddress = realAddress + realSize;
170  
171              _tracking = tracking;
172  
173              _regions = tracking.GetVirtualRegionsForHandle(address, size, false);
174              _guestRegions = GetGuestRegions(tracking, address, size, flags);
175              _allRegions = new List<VirtualRegion>(_regions.Count + _guestRegions.Count);
176  
177              InitializeRegions();
178          }
179  
180          private List<VirtualRegion> GetGuestRegions(MemoryTracking tracking, ulong address, ulong size, RegionFlags flags)
181          {
182              ulong guestAddress;
183              ulong guestSize;
184  
185              if (flags.HasFlag(RegionFlags.UnalignedAccess))
186              {
187                  (guestAddress, guestSize) = tracking.GetUnalignedSafeRegion(address, size);
188              }
189              else
190              {
191                  (guestAddress, guestSize) = (address, size);
192              }
193  
194              return tracking.GetVirtualRegionsForHandle(guestAddress, guestSize, true);
195          }
196  
197          private void InitializeRegions()
198          {
199              _allRegions.AddRange(_regions);
200              _allRegions.AddRange(_guestRegions);
201  
202              foreach (var region in _allRegions)
203              {
204                  region.Handles.Add(this);
205              }
206          }
207  
208          /// <summary>
209          /// Replace the bitmap and bit index used to track dirty state.
210          /// </summary>
211          /// <remarks>
212          /// The tracking lock should be held when this is called, to ensure neither bitmap is modified.
213          /// </remarks>
214          /// <param name="bitmap">The bitmap the dirty flag for this handle is stored in</param>
215          /// <param name="bit">The bit index representing the dirty flag for this handle</param>
216          internal void ReplaceBitmap(ConcurrentBitmap bitmap, int bit)
217          {
218              // Assumes the tracking lock is held, so nothing else can signal right now.
219  
220              var oldBitmap = Bitmap;
221              var oldBit = DirtyBit;
222  
223              bitmap.Set(bit, Dirty);
224  
225              Bitmap = bitmap;
226              DirtyBit = bit;
227  
228              Dirty |= oldBitmap.IsSet(oldBit);
229          }
230  
231          /// <summary>
232          /// Clear the volatile state of this handle.
233          /// </summary>
234          private void ClearVolatile()
235          {
236              _volatileCount = 0;
237              _volatile = false;
238          }
239  
240          /// <summary>
241          /// Check if this handle is dirty, or if it is volatile. (changes very often)
242          /// </summary>
243          /// <returns>True if the handle is dirty or volatile, false otherwise</returns>
244          public bool DirtyOrVolatile()
245          {
246              _checkCount++;
247              return _volatile || Dirty;
248          }
249  
250          /// <summary>
251          /// Signal that a memory action occurred within this handle's virtual regions.
252          /// </summary>
253          /// <param name="address">Address accessed</param>
254          /// <param name="size">Size of the region affected in bytes</param>
255          /// <param name="write">Whether the region was written to or read</param>
256          /// <param name="handleIterable">Reference to the handles being iterated, in case the list needs to be copied</param>
257          internal void Signal(ulong address, ulong size, bool write, ref IList<RegionHandle> handleIterable)
258          {
259              // If this handle was already unmapped (even if just partially),
260              // then we have nothing to do until it is mapped again.
261              // The pre-action should be still consumed to avoid flushing on remap.
262              if (Unmapped)
263              {
264                  Interlocked.Exchange(ref _preAction, null);
265                  return;
266              }
267  
268              if (_preAction != null)
269              {
270                  // Limit the range to within this handle.
271                  ulong maxAddress = Math.Max(address, RealAddress);
272                  ulong minEndAddress = Math.Min(address + size, RealAddress + RealSize);
273  
274                  // Copy the handles list in case it changes when we're out of the lock.
275                  if (handleIterable is List<RegionHandle>)
276                  {
277                      handleIterable = handleIterable.ToArray();
278                  }
279  
280                  // Temporarily release the tracking lock while we're running the action.
281                  Monitor.Exit(_tracking.TrackingLock);
282  
283                  try
284                  {
285                      lock (_preActionLock)
286                      {
287                          _preAction?.Invoke(maxAddress, minEndAddress - maxAddress);
288  
289                          // The action is removed after it returns, to ensure that the null check above succeeds when
290                          // it's still in progress rather than continuing and possibly missing a required data flush.
291                          Interlocked.Exchange(ref _preAction, null);
292                      }
293                  }
294                  finally
295                  {
296                      Monitor.Enter(_tracking.TrackingLock);
297                  }
298              }
299  
300              if (write)
301              {
302                  bool oldDirty = Dirty;
303                  Dirty = true;
304                  if (!oldDirty)
305                  {
306                      OnDirty?.Invoke();
307                  }
308                  Parent?.SignalWrite();
309              }
310          }
311  
312          /// <summary>
313          /// Signal that a precise memory action occurred within this handle's virtual regions.
314          /// If there is no precise action, or the action returns false, the normal signal handler will be called.
315          /// </summary>
316          /// <param name="address">Address accessed</param>
317          /// <param name="size">Size of the region affected in bytes</param>
318          /// <param name="write">Whether the region was written to or read</param>
319          /// <param name="handleIterable">Reference to the handles being iterated, in case the list needs to be copied</param>
320          /// <returns>True if a precise action was performed and returned true, false otherwise</returns>
321          internal bool SignalPrecise(ulong address, ulong size, bool write, ref IList<RegionHandle> handleIterable)
322          {
323              if (!Unmapped && _preciseAction != null && _preciseAction(address, size, write))
324              {
325                  return true;
326              }
327  
328              Signal(address, size, write, ref handleIterable);
329  
330              return false;
331          }
332  
333          /// <summary>
334          /// Force this handle to be dirty, without reprotecting.
335          /// </summary>
336          public void ForceDirty()
337          {
338              Dirty = true;
339          }
340  
341          /// <summary>
342          /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write.
343          /// </summary>
344          /// <param name="asDirty">True if the handle should be reprotected as dirty, rather than have it cleared</param>
345          /// <param name="consecutiveCheck">True if this reprotect is the result of consecutive dirty checks</param>
346          public void Reprotect(bool asDirty, bool consecutiveCheck = false)
347          {
348              if (_volatile)
349              {
350                  return;
351              }
352  
353              Dirty = asDirty;
354  
355              bool protectionChanged = false;
356  
357              lock (_tracking.TrackingLock)
358              {
359                  foreach (VirtualRegion region in _allRegions)
360                  {
361                      protectionChanged |= region.UpdateProtection();
362                  }
363              }
364  
365              if (!protectionChanged)
366              {
367                  // Counteract the check count being incremented when this handle was forced dirty.
368                  // It doesn't count for protected write tracking.
369  
370                  _checkCount--;
371              }
372              else if (!asDirty)
373              {
374                  if (consecutiveCheck || (_checkCount > 0 && _checkCount < CheckCountForInfrequent))
375                  {
376                      if (++_volatileCount >= VolatileThreshold && _preAction == null)
377                      {
378                          _volatile = true;
379                          return;
380                      }
381                  }
382                  else
383                  {
384                      _volatileCount = 0;
385                  }
386  
387                  _checkCount = 0;
388              }
389          }
390  
391          /// <summary>
392          /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write.
393          /// </summary>
394          /// <param name="asDirty">True if the handle should be reprotected as dirty, rather than have it cleared</param>
395          public void Reprotect(bool asDirty = false)
396          {
397              Reprotect(asDirty, false);
398          }
399  
400          /// <summary>
401          /// Register an action to perform when the tracked region is read or written.
402          /// The action is automatically removed after it runs.
403          /// </summary>
404          /// <param name="action">Action to call on read or write</param>
405          public void RegisterAction(RegionSignal action)
406          {
407              ClearVolatile();
408  
409              lock (_preActionLock)
410              {
411                  RegionSignal lastAction = Interlocked.Exchange(ref _preAction, action);
412  
413                  if (lastAction == null && action != lastAction)
414                  {
415                      lock (_tracking.TrackingLock)
416                      {
417                          foreach (VirtualRegion region in _allRegions)
418                          {
419                              region.UpdateProtection();
420                          }
421                      }
422                  }
423              }
424          }
425  
426          /// <summary>
427          /// Register an action to perform when a precise access occurs (one with exact address and size).
428          /// If the action returns true, read/write tracking are skipped.
429          /// </summary>
430          /// <param name="action">Action to call on read or write</param>
431          public void RegisterPreciseAction(PreciseRegionSignal action)
432          {
433              _preciseAction = action;
434          }
435  
436          /// <summary>
437          /// Register an action to perform when the region is written to.
438          /// This action will not be removed when it is called - it is called each time the dirty flag is set.
439          /// </summary>
440          /// <param name="action">Action to call on dirty</param>
441          public void RegisterDirtyEvent(Action action)
442          {
443              OnDirty += action;
444          }
445  
446          /// <summary>
447          /// Add a child virtual region to this handle.
448          /// </summary>
449          /// <param name="region">Virtual region to add as a child</param>
450          internal void AddChild(VirtualRegion region)
451          {
452              if (region.Guest)
453              {
454                  _guestRegions.Add(region);
455              }
456              else
457              {
458                  _regions.Add(region);
459              }
460  
461              _allRegions.Add(region);
462          }
463  
464          /// <summary>
465          /// Signal that this handle has been mapped or unmapped.
466          /// </summary>
467          /// <param name="mapped">True if the handle has been mapped, false if unmapped</param>
468          internal void SignalMappingChanged(bool mapped)
469          {
470              if (Unmapped == mapped)
471              {
472                  Unmapped = !mapped;
473  
474                  if (Unmapped)
475                  {
476                      ClearVolatile();
477                      Dirty = false;
478                  }
479              }
480          }
481  
482          /// <summary>
483          /// Check if this region overlaps with another.
484          /// </summary>
485          /// <param name="address">Base address</param>
486          /// <param name="size">Size of the region</param>
487          /// <returns>True if overlapping, false otherwise</returns>
488          public bool OverlapsWith(ulong address, ulong size)
489          {
490              return Address < address + size && address < EndAddress;
491          }
492  
493          /// <summary>
494          /// Determines if this handle's memory range matches another exactly.
495          /// </summary>
496          /// <param name="other">The other handle</param>
497          /// <returns>True on a match, false otherwise</returns>
498          public bool RangeEquals(RegionHandle other)
499          {
500              return RealAddress == other.RealAddress && RealSize == other.RealSize;
501          }
502  
503          /// <summary>
504          /// Dispose the handle. Within the tracking lock, this removes references from virtual regions.
505          /// </summary>
506          public void Dispose()
507          {
508              ObjectDisposedException.ThrowIf(_disposed, this);
509  
510              GC.SuppressFinalize(this);
511  
512              _disposed = true;
513  
514              lock (_tracking.TrackingLock)
515              {
516                  foreach (VirtualRegion region in _allRegions)
517                  {
518                      region.RemoveHandle(this);
519                  }
520              }
521          }
522      }
523  }