/ src / Ryujinx.Cpu / ManagedPageFlags.cs
ManagedPageFlags.cs
  1  using Ryujinx.Memory;
  2  using Ryujinx.Memory.Tracking;
  3  using System;
  4  using System.Runtime.CompilerServices;
  5  using System.Threading;
  6  
  7  namespace Ryujinx.Cpu
  8  {
  9      /// <summary>
 10      /// A page bitmap that keeps track of mapped state and tracking protection
 11      /// for managed memory accesses (not using host page protection).
 12      /// </summary>
 13      internal readonly struct ManagedPageFlags
 14      {
 15          public const int PageBits = 12;
 16          public const int PageSize = 1 << PageBits;
 17          public const int PageMask = PageSize - 1;
 18  
 19          private readonly ulong[] _pageBitmap;
 20  
 21          public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry.
 22          public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set.
 23  
 24          private enum ManagedPtBits : ulong
 25          {
 26              Unmapped = 0,
 27              Mapped,
 28              WriteTracked,
 29              ReadWriteTracked,
 30  
 31              MappedReplicated = 0x5555555555555555,
 32              WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa,
 33              ReadWriteTrackedReplicated = ulong.MaxValue,
 34          }
 35  
 36          public ManagedPageFlags(int addressSpaceBits)
 37          {
 38              int bits = Math.Max(0, addressSpaceBits - (PageBits + PageToPteShift));
 39              _pageBitmap = new ulong[1 << bits];
 40          }
 41  
 42          /// <summary>
 43          /// Computes the number of pages in a virtual address range.
 44          /// </summary>
 45          /// <param name="va">Virtual address of the range</param>
 46          /// <param name="size">Size of the range</param>
 47          /// <param name="startVa">The virtual address of the beginning of the first page</param>
 48          /// <remarks>This function does not differentiate between allocated and unallocated pages.</remarks>
 49          [MethodImpl(MethodImplOptions.AggressiveInlining)]
 50          private static int GetPagesCount(ulong va, ulong size, out ulong startVa)
 51          {
 52              // WARNING: Always check if ulong does not overflow during the operations.
 53              startVa = va & ~(ulong)PageMask;
 54              ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
 55  
 56              return (int)(vaSpan / PageSize);
 57          }
 58  
 59          /// <summary>
 60          /// Checks if the page at a given CPU virtual address is mapped.
 61          /// </summary>
 62          /// <param name="va">Virtual address to check</param>
 63          /// <returns>True if the address is mapped, false otherwise</returns>
 64          [MethodImpl(MethodImplOptions.AggressiveInlining)]
 65          public readonly bool IsMapped(ulong va)
 66          {
 67              ulong page = va >> PageBits;
 68  
 69              int bit = (int)((page & 31) << 1);
 70  
 71              int pageIndex = (int)(page >> PageToPteShift);
 72              ref ulong pageRef = ref _pageBitmap[pageIndex];
 73  
 74              ulong pte = Volatile.Read(ref pageRef);
 75  
 76              return ((pte >> bit) & 3) != 0;
 77          }
 78  
 79          [MethodImpl(MethodImplOptions.AggressiveInlining)]
 80          private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex)
 81          {
 82              startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1);
 83              endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1));
 84  
 85              pageIndex = (int)(pageStart >> PageToPteShift);
 86              pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift);
 87          }
 88  
 89          /// <summary>
 90          /// Checks if a memory range is mapped.
 91          /// </summary>
 92          /// <param name="va">Virtual address of the range</param>
 93          /// <param name="size">Size of the range in bytes</param>
 94          /// <returns>True if the entire range is mapped, false otherwise</returns>
 95          public readonly bool IsRangeMapped(ulong va, ulong size)
 96          {
 97              int pages = GetPagesCount(va, size, out _);
 98  
 99              if (pages == 1)
100              {
101                  return IsMapped(va);
102              }
103  
104              ulong pageStart = va >> PageBits;
105              ulong pageEnd = pageStart + (ulong)pages;
106  
107              GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
108  
109              // Check if either bit in each 2 bit page entry is set.
110              // OR the block with itself shifted down by 1, and check the first bit of each entry.
111  
112              ulong mask = BlockMappedMask & startMask;
113  
114              while (pageIndex <= pageEndIndex)
115              {
116                  if (pageIndex == pageEndIndex)
117                  {
118                      mask &= endMask;
119                  }
120  
121                  ref ulong pageRef = ref _pageBitmap[pageIndex++];
122                  ulong pte = Volatile.Read(ref pageRef);
123  
124                  pte |= pte >> 1;
125                  if ((pte & mask) != mask)
126                  {
127                      return false;
128                  }
129  
130                  mask = BlockMappedMask;
131              }
132  
133              return true;
134          }
135  
136          /// <summary>
137          /// Reprotect a region of virtual memory for tracking.
138          /// </summary>
139          /// <param name="va">Virtual address base</param>
140          /// <param name="size">Size of the region to protect</param>
141          /// <param name="protection">Memory protection to set</param>
142          public readonly void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
143          {
144              // Protection is inverted on software pages, since the default value is 0.
145              protection = (~protection) & MemoryPermission.ReadAndWrite;
146  
147              int pages = GetPagesCount(va, size, out va);
148              ulong pageStart = va >> PageBits;
149  
150              if (pages == 1)
151              {
152                  ulong protTag = protection switch
153                  {
154                      MemoryPermission.None => (ulong)ManagedPtBits.Mapped,
155                      MemoryPermission.Write => (ulong)ManagedPtBits.WriteTracked,
156                      _ => (ulong)ManagedPtBits.ReadWriteTracked,
157                  };
158  
159                  int bit = (int)((pageStart & 31) << 1);
160  
161                  ulong tagMask = 3UL << bit;
162                  ulong invTagMask = ~tagMask;
163  
164                  ulong tag = protTag << bit;
165  
166                  int pageIndex = (int)(pageStart >> PageToPteShift);
167                  ref ulong pageRef = ref _pageBitmap[pageIndex];
168  
169                  ulong pte;
170  
171                  do
172                  {
173                      pte = Volatile.Read(ref pageRef);
174                  }
175                  while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
176              }
177              else
178              {
179                  ulong pageEnd = pageStart + (ulong)pages;
180  
181                  GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
182  
183                  ulong mask = startMask;
184  
185                  ulong protTag = protection switch
186                  {
187                      MemoryPermission.None => (ulong)ManagedPtBits.MappedReplicated,
188                      MemoryPermission.Write => (ulong)ManagedPtBits.WriteTrackedReplicated,
189                      _ => (ulong)ManagedPtBits.ReadWriteTrackedReplicated,
190                  };
191  
192                  while (pageIndex <= pageEndIndex)
193                  {
194                      if (pageIndex == pageEndIndex)
195                      {
196                          mask &= endMask;
197                      }
198  
199                      ref ulong pageRef = ref _pageBitmap[pageIndex++];
200  
201                      ulong pte;
202                      ulong mappedMask;
203  
204                      // Change the protection of all 2 bit entries that are mapped.
205                      do
206                      {
207                          pte = Volatile.Read(ref pageRef);
208  
209                          mappedMask = pte | (pte >> 1);
210                          mappedMask |= (mappedMask & BlockMappedMask) << 1;
211                          mappedMask &= mask; // Only update mapped pages within the given range.
212                      }
213                      while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte);
214  
215                      mask = ulong.MaxValue;
216                  }
217              }
218          }
219  
220          /// <summary>
221          /// Alerts the memory tracking that a given region has been read from or written to.
222          /// This should be called before read/write is performed.
223          /// </summary>
224          /// <param name="tracking">Memory tracking structure to call when pages are protected</param>
225          /// <param name="va">Virtual address of the region</param>
226          /// <param name="size">Size of the region</param>
227          /// <param name="write">True if the region was written, false if read</param>
228          /// <param name="exemptId">Optional ID of the handles that should not be signalled</param>
229          /// <remarks>
230          /// This function also validates that the given range is both valid and mapped, and will throw if it is not.
231          /// </remarks>
232          [MethodImpl(MethodImplOptions.AggressiveInlining)]
233          public readonly void SignalMemoryTracking(MemoryTracking tracking, ulong va, ulong size, bool write, int? exemptId = null)
234          {
235              // Software table, used for managed memory tracking.
236  
237              int pages = GetPagesCount(va, size, out _);
238              ulong pageStart = va >> PageBits;
239  
240              if (pages == 1)
241              {
242                  ulong tag = (ulong)(write ? ManagedPtBits.WriteTracked : ManagedPtBits.ReadWriteTracked);
243  
244                  int bit = (int)((pageStart & 31) << 1);
245  
246                  int pageIndex = (int)(pageStart >> PageToPteShift);
247                  ref ulong pageRef = ref _pageBitmap[pageIndex];
248  
249                  ulong pte = Volatile.Read(ref pageRef);
250                  ulong state = ((pte >> bit) & 3);
251  
252                  if (state >= tag)
253                  {
254                      tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
255                      return;
256                  }
257                  else if (state == 0)
258                  {
259                      ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
260                  }
261              }
262              else
263              {
264                  ulong pageEnd = pageStart + (ulong)pages;
265  
266                  GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
267  
268                  ulong mask = startMask;
269  
270                  ulong anyTrackingTag = (ulong)ManagedPtBits.WriteTrackedReplicated;
271  
272                  while (pageIndex <= pageEndIndex)
273                  {
274                      if (pageIndex == pageEndIndex)
275                      {
276                          mask &= endMask;
277                      }
278  
279                      ref ulong pageRef = ref _pageBitmap[pageIndex++];
280  
281                      ulong pte = Volatile.Read(ref pageRef);
282                      ulong mappedMask = mask & BlockMappedMask;
283  
284                      ulong mappedPte = pte | (pte >> 1);
285                      if ((mappedPte & mappedMask) != mappedMask)
286                      {
287                          ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
288                      }
289  
290                      pte &= mask;
291                      if ((pte & anyTrackingTag) != 0) // Search for any tracking.
292                      {
293                          // Writes trigger any tracking.
294                          // Only trigger tracking from reads if both bits are set on any page.
295                          if (write || (pte & (pte >> 1) & BlockMappedMask) != 0)
296                          {
297                              tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
298                              break;
299                          }
300                      }
301  
302                      mask = ulong.MaxValue;
303                  }
304              }
305          }
306  
307          /// <summary>
308          /// Adds the given address mapping to the page table.
309          /// </summary>
310          /// <param name="va">Virtual memory address</param>
311          /// <param name="size">Size to be mapped</param>
312          public readonly void AddMapping(ulong va, ulong size)
313          {
314              int pages = GetPagesCount(va, size, out _);
315              ulong pageStart = va >> PageBits;
316              ulong pageEnd = pageStart + (ulong)pages;
317  
318              GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
319  
320              ulong mask = startMask;
321  
322              while (pageIndex <= pageEndIndex)
323              {
324                  if (pageIndex == pageEndIndex)
325                  {
326                      mask &= endMask;
327                  }
328  
329                  ref ulong pageRef = ref _pageBitmap[pageIndex++];
330  
331                  ulong pte;
332                  ulong mappedMask;
333  
334                  // Map all 2-bit entries that are unmapped.
335                  do
336                  {
337                      pte = Volatile.Read(ref pageRef);
338  
339                      mappedMask = pte | (pte >> 1);
340                      mappedMask |= (mappedMask & BlockMappedMask) << 1;
341                      mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged.
342                  }
343                  while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte);
344  
345                  mask = ulong.MaxValue;
346              }
347          }
348  
349          /// <summary>
350          /// Removes the given address mapping from the page table.
351          /// </summary>
352          /// <param name="va">Virtual memory address</param>
353          /// <param name="size">Size to be unmapped</param>
354          public readonly void RemoveMapping(ulong va, ulong size)
355          {
356              int pages = GetPagesCount(va, size, out _);
357              ulong pageStart = va >> PageBits;
358              ulong pageEnd = pageStart + (ulong)pages;
359  
360              GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
361  
362              startMask = ~startMask;
363              endMask = ~endMask;
364  
365              ulong mask = startMask;
366  
367              while (pageIndex <= pageEndIndex)
368              {
369                  if (pageIndex == pageEndIndex)
370                  {
371                      mask |= endMask;
372                  }
373  
374                  ref ulong pageRef = ref _pageBitmap[pageIndex++];
375                  ulong pte;
376  
377                  do
378                  {
379                      pte = Volatile.Read(ref pageRef);
380                  }
381                  while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte);
382  
383                  mask = 0;
384              }
385          }
386  
387          private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
388      }
389  }