/ src / Ryujinx.Graphics.Vulkan / StagingBuffer.cs
StagingBuffer.cs
  1  using Ryujinx.Common;
  2  using Ryujinx.Common.Logging;
  3  using Ryujinx.Graphics.GAL;
  4  using System;
  5  using System.Collections.Generic;
  6  using System.Diagnostics;
  7  
  8  namespace Ryujinx.Graphics.Vulkan
  9  {
 10      readonly struct StagingBufferReserved
 11      {
 12          public readonly BufferHolder Buffer;
 13          public readonly int Offset;
 14          public readonly int Size;
 15  
 16          public StagingBufferReserved(BufferHolder buffer, int offset, int size)
 17          {
 18              Buffer = buffer;
 19              Offset = offset;
 20              Size = size;
 21          }
 22      }
 23  
 24      class StagingBuffer : IDisposable
 25      {
 26          private const int BufferSize = 32 * 1024 * 1024;
 27  
 28          private int _freeOffset;
 29          private int _freeSize;
 30  
 31          private readonly VulkanRenderer _gd;
 32          private readonly BufferHolder _buffer;
 33          private readonly int _resourceAlignment;
 34  
 35          public readonly BufferHandle Handle;
 36  
 37          private readonly struct PendingCopy
 38          {
 39              public FenceHolder Fence { get; }
 40              public int Size { get; }
 41  
 42              public PendingCopy(FenceHolder fence, int size)
 43              {
 44                  Fence = fence;
 45                  Size = size;
 46                  fence.Get();
 47              }
 48          }
 49  
 50          private readonly Queue<PendingCopy> _pendingCopies;
 51  
 52          public StagingBuffer(VulkanRenderer gd, BufferManager bufferManager)
 53          {
 54              _gd = gd;
 55              Handle = bufferManager.CreateWithHandle(gd, BufferSize, out _buffer);
 56              _pendingCopies = new Queue<PendingCopy>();
 57              _freeSize = BufferSize;
 58              _resourceAlignment = (int)gd.Capabilities.MinResourceAlignment;
 59          }
 60  
 61          public void PushData(CommandBufferPool cbp, CommandBufferScoped? cbs, Action endRenderPass, BufferHolder dst, int dstOffset, ReadOnlySpan<byte> data)
 62          {
 63              bool isRender = cbs != null;
 64              CommandBufferScoped scoped = cbs ?? cbp.Rent();
 65  
 66              // Must push all data to the buffer. If it can't fit, split it up.
 67  
 68              endRenderPass?.Invoke();
 69  
 70              while (data.Length > 0)
 71              {
 72                  if (_freeSize < data.Length)
 73                  {
 74                      FreeCompleted();
 75                  }
 76  
 77                  while (_freeSize == 0)
 78                  {
 79                      if (!WaitFreeCompleted(cbp))
 80                      {
 81                          if (isRender)
 82                          {
 83                              _gd.FlushAllCommands();
 84                              scoped = cbp.Rent();
 85                              isRender = false;
 86                          }
 87                          else
 88                          {
 89                              scoped = cbp.ReturnAndRent(scoped);
 90                          }
 91                      }
 92                  }
 93  
 94                  int chunkSize = Math.Min(_freeSize, data.Length);
 95  
 96                  PushDataImpl(scoped, dst, dstOffset, data[..chunkSize]);
 97  
 98                  dstOffset += chunkSize;
 99                  data = data[chunkSize..];
100              }
101  
102              if (!isRender)
103              {
104                  scoped.Dispose();
105              }
106          }
107  
108          private void PushDataImpl(CommandBufferScoped cbs, BufferHolder dst, int dstOffset, ReadOnlySpan<byte> data)
109          {
110              var srcBuffer = _buffer.GetBuffer();
111              var dstBuffer = dst.GetBuffer(cbs.CommandBuffer, dstOffset, data.Length, true);
112  
113              int offset = _freeOffset;
114              int capacity = BufferSize - offset;
115              if (capacity < data.Length)
116              {
117                  _buffer.SetDataUnchecked(offset, data[..capacity]);
118                  _buffer.SetDataUnchecked(0, data[capacity..]);
119  
120                  BufferHolder.Copy(_gd, cbs, srcBuffer, dstBuffer, offset, dstOffset, capacity);
121                  BufferHolder.Copy(_gd, cbs, srcBuffer, dstBuffer, 0, dstOffset + capacity, data.Length - capacity);
122              }
123              else
124              {
125                  _buffer.SetDataUnchecked(offset, data);
126  
127                  BufferHolder.Copy(_gd, cbs, srcBuffer, dstBuffer, offset, dstOffset, data.Length);
128              }
129  
130              _freeOffset = (offset + data.Length) & (BufferSize - 1);
131              _freeSize -= data.Length;
132              Debug.Assert(_freeSize >= 0);
133  
134              _pendingCopies.Enqueue(new PendingCopy(cbs.GetFence(), data.Length));
135          }
136  
137          public bool TryPushData(CommandBufferScoped cbs, Action endRenderPass, BufferHolder dst, int dstOffset, ReadOnlySpan<byte> data)
138          {
139              if (data.Length > BufferSize)
140              {
141                  return false;
142              }
143  
144              if (_freeSize < data.Length)
145              {
146                  FreeCompleted();
147  
148                  if (_freeSize < data.Length)
149                  {
150                      return false;
151                  }
152              }
153  
154              endRenderPass?.Invoke();
155  
156              PushDataImpl(cbs, dst, dstOffset, data);
157  
158              return true;
159          }
160  
161          private StagingBufferReserved ReserveDataImpl(CommandBufferScoped cbs, int size, int alignment)
162          {
163              // Assumes the caller has already determined that there is enough space.
164              int offset = BitUtils.AlignUp(_freeOffset, alignment);
165              int padding = offset - _freeOffset;
166  
167              int capacity = Math.Min(_freeSize, BufferSize - offset);
168              int reservedLength = size + padding;
169              if (capacity < size)
170              {
171                  offset = 0; // Place at start.
172                  reservedLength += capacity;
173              }
174  
175              _freeOffset = (_freeOffset + reservedLength) & (BufferSize - 1);
176              _freeSize -= reservedLength;
177              Debug.Assert(_freeSize >= 0);
178  
179              _pendingCopies.Enqueue(new PendingCopy(cbs.GetFence(), reservedLength));
180  
181              return new StagingBufferReserved(_buffer, offset, size);
182          }
183  
184          private int GetContiguousFreeSize(int alignment)
185          {
186              int alignedFreeOffset = BitUtils.AlignUp(_freeOffset, alignment);
187              int padding = alignedFreeOffset - _freeOffset;
188  
189              // Free regions:
190              // - Aligned free offset to end (minimum free size - padding)
191              // - 0 to _freeOffset + freeSize wrapped (only if free area contains 0)
192  
193              int endOffset = (_freeOffset + _freeSize) & (BufferSize - 1);
194  
195              return Math.Max(
196                  Math.Min(_freeSize - padding, BufferSize - alignedFreeOffset),
197                  endOffset <= _freeOffset ? Math.Min(_freeSize, endOffset) : 0
198                  );
199          }
200  
201          /// <summary>
202          /// Reserve a range on the staging buffer for the current command buffer and upload data to it.
203          /// </summary>
204          /// <param name="cbs">Command buffer to reserve the data on</param>
205          /// <param name="size">The minimum size the reserved data requires</param>
206          /// <param name="alignment">The required alignment for the buffer offset</param>
207          /// <returns>The reserved range of the staging buffer</returns>
208          public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment)
209          {
210              if (size > BufferSize)
211              {
212                  return null;
213              }
214  
215              // Temporary reserved data cannot be fragmented.
216  
217              if (GetContiguousFreeSize(alignment) < size)
218              {
219                  FreeCompleted();
220  
221                  if (GetContiguousFreeSize(alignment) < size)
222                  {
223                      Logger.Debug?.PrintMsg(LogClass.Gpu, $"Staging buffer out of space to reserve data of size {size}.");
224                      return null;
225                  }
226              }
227  
228              return ReserveDataImpl(cbs, size, alignment);
229          }
230  
231          /// <summary>
232          /// Reserve a range on the staging buffer for the current command buffer and upload data to it.
233          /// Uses the most permissive byte alignment.
234          /// </summary>
235          /// <param name="cbs">Command buffer to reserve the data on</param>
236          /// <param name="size">The minimum size the reserved data requires</param>
237          /// <returns>The reserved range of the staging buffer</returns>
238          public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size)
239          {
240              return TryReserveData(cbs, size, _resourceAlignment);
241          }
242  
243          private bool WaitFreeCompleted(CommandBufferPool cbp)
244          {
245              if (_pendingCopies.TryPeek(out var pc))
246              {
247                  if (!pc.Fence.IsSignaled())
248                  {
249                      if (cbp.IsFenceOnRentedCommandBuffer(pc.Fence))
250                      {
251                          return false;
252                      }
253  
254                      pc.Fence.Wait();
255                  }
256  
257                  var dequeued = _pendingCopies.Dequeue();
258                  Debug.Assert(dequeued.Fence == pc.Fence);
259                  _freeSize += pc.Size;
260                  pc.Fence.Put();
261              }
262  
263              return true;
264          }
265  
266          public void FreeCompleted()
267          {
268              FenceHolder signalledFence = null;
269              while (_pendingCopies.TryPeek(out var pc) && (pc.Fence == signalledFence || pc.Fence.IsSignaled()))
270              {
271                  signalledFence = pc.Fence; // Already checked - don't need to do it again.
272                  var dequeued = _pendingCopies.Dequeue();
273                  Debug.Assert(dequeued.Fence == pc.Fence);
274                  _freeSize += pc.Size;
275                  pc.Fence.Put();
276              }
277          }
278  
279          protected virtual void Dispose(bool disposing)
280          {
281              if (disposing)
282              {
283                  _gd.BufferManager.Delete(Handle);
284  
285                  while (_pendingCopies.TryDequeue(out var pc))
286                  {
287                      pc.Fence.Put();
288                  }
289              }
290          }
291  
292          public void Dispose()
293          {
294              Dispose(true);
295          }
296      }
297  }