/ src / Ryujinx.Graphics.Gpu / Memory / BufferBackingState.cs
BufferBackingState.cs
  1  using Ryujinx.Graphics.GAL;
  2  using System;
  3  using System.Collections.Generic;
  4  
  5  namespace Ryujinx.Graphics.Gpu.Memory
  6  {
  7      /// <summary>
  8      /// Type of backing memory.
  9      /// In ascending order of priority when merging multiple buffer backing states.
 10      /// </summary>
 11      internal enum BufferBackingType
 12      {
 13          HostMemory,
 14          DeviceMemory,
 15          DeviceMemoryWithFlush
 16      }
 17  
 18      /// <summary>
 19      /// Keeps track of buffer usage to decide what memory heap that buffer memory is placed on.
 20      /// Dedicated GPUs prefer certain types of resources to be device local,
 21      /// and if we need data to be read back, we might prefer that they're in host memory.
 22      /// 
 23      /// The measurements recorded here compare to a set of heruristics (thresholds and conditions)
 24      /// that appear to produce good performance in most software.
 25      /// </summary>
 26      internal struct BufferBackingState
 27      {
 28          private const int DeviceLocalSizeThreshold = 256 * 1024; // 256kb
 29  
 30          private const int SetCountThreshold = 100;
 31          private const int WriteCountThreshold = 50;
 32          private const int FlushCountThreshold = 5;
 33          private const int DeviceLocalForceExpiry = 100;
 34  
 35          public readonly bool IsDeviceLocal => _activeType != BufferBackingType.HostMemory;
 36  
 37          private readonly SystemMemoryType _systemMemoryType;
 38          private BufferBackingType _activeType;
 39          private BufferBackingType _desiredType;
 40  
 41          private bool _canSwap;
 42  
 43          private int _setCount;
 44          private int _writeCount;
 45          private int _flushCount;
 46          private int _flushTemp;
 47          private int _lastFlushWrite;
 48          private int _deviceLocalForceCount;
 49  
 50          private readonly int _size;
 51  
 52          /// <summary>
 53          /// Initialize the buffer backing state for a given parent buffer.
 54          /// </summary>
 55          /// <param name="context">GPU context</param>
 56          /// <param name="parent">Parent buffer</param>
 57          /// <param name="stage">Initial buffer stage</param>
 58          /// <param name="baseBuffers">Buffers to inherit state from</param>
 59          public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, IEnumerable<Buffer> baseBuffers = null)
 60          {
 61              _size = (int)parent.Size;
 62              _systemMemoryType = context.Capabilities.MemoryType;
 63  
 64              // Backend managed is always auto, unified memory is always host.
 65              _desiredType = BufferBackingType.HostMemory;
 66              _canSwap = _systemMemoryType != SystemMemoryType.BackendManaged && _systemMemoryType != SystemMemoryType.UnifiedMemory;
 67  
 68              if (_canSwap)
 69              {
 70                  // Might want to start certain buffers as being device local,
 71                  // and the usage might also lock those buffers into being device local.
 72  
 73                  BufferStage storageFlags = stage & BufferStage.StorageMask;
 74  
 75                  if (parent.Size > DeviceLocalSizeThreshold && baseBuffers == null)
 76                  {
 77                      _desiredType = BufferBackingType.DeviceMemory;
 78                  }
 79  
 80                  if (storageFlags != 0)
 81                  {
 82                      // Storage buffer bindings may require special treatment.
 83  
 84                      var rawStage = stage & BufferStage.StageMask;
 85  
 86                      if (rawStage == BufferStage.Fragment)
 87                      {
 88                          // Fragment read should start device local.
 89  
 90                          _desiredType = BufferBackingType.DeviceMemory;
 91  
 92                          if (storageFlags != BufferStage.StorageRead)
 93                          {
 94                              // Fragment write should stay device local until the use doesn't happen anymore.
 95  
 96                              _deviceLocalForceCount = DeviceLocalForceExpiry;
 97                          }
 98                      }
 99  
100                      // TODO: Might be nice to force atomic access to be device local for any stage.
101                  }
102  
103                  if (baseBuffers != null)
104                  {
105                      foreach (Buffer buffer in baseBuffers)
106                      {
107                          CombineState(buffer.BackingState);
108                      }
109                  }
110              }
111          }
112  
113          /// <summary>
114          /// Combine buffer backing types, selecting the one with highest priority.
115          /// </summary>
116          /// <param name="left">First buffer backing type</param>
117          /// <param name="right">Second buffer backing type</param>
118          /// <returns>Combined buffer backing type</returns>
119          private static BufferBackingType CombineTypes(BufferBackingType left, BufferBackingType right)
120          {
121              return (BufferBackingType)Math.Max((int)left, (int)right);
122          }
123  
124          /// <summary>
125          /// Combine the state from the given buffer backing state with this one,
126          /// so that the state isn't lost when migrating buffers.
127          /// </summary>
128          /// <param name="oldState">Buffer state to combine into this state</param>
129          private void CombineState(BufferBackingState oldState)
130          {
131              _setCount += oldState._setCount;
132              _writeCount += oldState._writeCount;
133              _flushCount += oldState._flushCount;
134              _flushTemp += oldState._flushTemp;
135              _lastFlushWrite = -1;
136              _deviceLocalForceCount = Math.Max(_deviceLocalForceCount, oldState._deviceLocalForceCount);
137  
138              _canSwap &= oldState._canSwap;
139  
140              _desiredType = CombineTypes(_desiredType, oldState._desiredType);
141          }
142  
143          /// <summary>
144          /// Get the buffer access for the desired backing type, and record that type as now being active.
145          /// </summary>
146          /// <param name="parent">Parent buffer</param>
147          /// <returns>Buffer access</returns>
148          public BufferAccess SwitchAccess(Buffer parent)
149          {
150              BufferAccess access = parent.SparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default;
151  
152              bool isBackendManaged = _systemMemoryType == SystemMemoryType.BackendManaged;
153  
154              if (!isBackendManaged)
155              {
156                  switch (_desiredType)
157                  {
158                      case BufferBackingType.HostMemory:
159                          access |= BufferAccess.HostMemory;
160                          break;
161                      case BufferBackingType.DeviceMemory:
162                          access |= BufferAccess.DeviceMemory;
163                          break;
164                      case BufferBackingType.DeviceMemoryWithFlush:
165                          access |= BufferAccess.DeviceMemoryMapped;
166                          break;
167                  }
168              }
169  
170              _activeType = _desiredType;
171  
172              return access;
173          }
174  
175          /// <summary>
176          /// Record when data has been uploaded to the buffer.
177          /// </summary>
178          public void RecordSet()
179          {
180              _setCount++;
181  
182              ConsiderUseCounts();
183          }
184  
185          /// <summary>
186          /// Record when data has been flushed from the buffer.
187          /// </summary>
188          public void RecordFlush()
189          {
190              if (_lastFlushWrite != _writeCount)
191              {
192                  // If it's on the same page as the last flush, ignore it.
193                  _lastFlushWrite = _writeCount;
194                  _flushCount++;
195              }
196          }
197  
198          /// <summary>
199          /// Determine if the buffer backing should be changed.
200          /// </summary>
201          /// <returns>True if the desired backing type is different from the current type</returns>
202          public readonly bool ShouldChangeBacking()
203          {
204              return _desiredType != _activeType;
205          }
206  
207          /// <summary>
208          /// Determine if the buffer backing should be changed, considering a new use with the given buffer stage.
209          /// </summary>
210          /// <param name="stage">Buffer stage for the use</param>
211          /// <returns>True if the desired backing type is different from the current type</returns>
212          public bool ShouldChangeBacking(BufferStage stage)
213          {
214              if (!_canSwap)
215              {
216                  return false;
217              }
218  
219              BufferStage storageFlags = stage & BufferStage.StorageMask;
220  
221              if (storageFlags != 0)
222              {
223                  if (storageFlags != BufferStage.StorageRead)
224                  {
225                      // Storage write.
226                      _writeCount++;
227  
228                      var rawStage = stage & BufferStage.StageMask;
229  
230                      if (rawStage == BufferStage.Fragment)
231                      {
232                          // Switch to device memory, swap back only if this use disappears.
233  
234                          _desiredType = CombineTypes(_desiredType, BufferBackingType.DeviceMemory);
235                          _deviceLocalForceCount = DeviceLocalForceExpiry;
236  
237                          // TODO: Might be nice to force atomic access to be device local for any stage.
238                      }
239                  }
240  
241                  ConsiderUseCounts();
242              }
243  
244              return _desiredType != _activeType;
245          }
246  
247          /// <summary>
248          /// Evaluate the current counts to determine what the buffer's desired backing type is.
249          /// This method depends on heuristics devised by testing a variety of software.
250          /// </summary>
251          private void ConsiderUseCounts()
252          {
253              if (_canSwap)
254              {
255                  if (_writeCount >= WriteCountThreshold || _setCount >= SetCountThreshold || _flushCount >= FlushCountThreshold)
256                  {
257                      if (_deviceLocalForceCount > 0 && --_deviceLocalForceCount != 0)
258                      {
259                          // Some buffer usage demanded that the buffer stay device local.
260                          // The desired type was selected when this counter was set.
261                      }
262                      else if (_flushCount > 0 || _flushTemp-- > 0)
263                      {
264                          // Buffers that flush should ideally be mapped in host address space for easy copies.
265                          // If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages).
266                          // If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached.
267                          _desiredType = _size > DeviceLocalSizeThreshold ? BufferBackingType.DeviceMemoryWithFlush : BufferBackingType.HostMemory;
268                      }
269                      else if (_writeCount >= WriteCountThreshold)
270                      {
271                          // Buffers that are written often should ideally be in the device local heap. (Storage buffers)
272                          _desiredType = BufferBackingType.DeviceMemory;
273                      }
274                      else if (_setCount > SetCountThreshold)
275                      {
276                          // Buffers that have their data set often should ideally be host mapped. (Constant buffers)
277                          _desiredType = BufferBackingType.HostMemory;
278                      }
279  
280                      // It's harder for a buffer that is flushed to revert to another type of mapping.
281                      if (_flushCount > 0)
282                      {
283                          _flushTemp = 1000;
284                      }
285  
286                      _lastFlushWrite = -1;
287                      _flushCount = 0;
288                      _writeCount = 0;
289                      _setCount = 0;
290                  }
291              }
292          }
293      }
294  }