/ src / Ryujinx.Graphics.OpenGL / Queries / CounterQueue.cs
CounterQueue.cs
  1  using OpenTK.Graphics.OpenGL;
  2  using Ryujinx.Graphics.GAL;
  3  using System;
  4  using System.Collections.Generic;
  5  using System.Threading;
  6  
  7  namespace Ryujinx.Graphics.OpenGL.Queries
  8  {
  9      class CounterQueue : IDisposable
 10      {
 11          private const int QueryPoolInitialSize = 100;
 12  
 13          public CounterType Type { get; }
 14          public bool Disposed { get; private set; }
 15  
 16          private readonly Queue<CounterQueueEvent> _events = new();
 17          private CounterQueueEvent _current;
 18  
 19          private ulong _accumulatedCounter;
 20          private int _waiterCount;
 21  
 22          private readonly object _lock = new();
 23  
 24          private readonly Queue<BufferedQuery> _queryPool;
 25          private readonly AutoResetEvent _queuedEvent = new(false);
 26          private readonly AutoResetEvent _wakeSignal = new(false);
 27          private readonly AutoResetEvent _eventConsumed = new(false);
 28  
 29          private readonly Thread _consumerThread;
 30  
 31          internal CounterQueue(CounterType type)
 32          {
 33              Type = type;
 34  
 35              QueryTarget glType = GetTarget(Type);
 36  
 37              _queryPool = new Queue<BufferedQuery>(QueryPoolInitialSize);
 38              for (int i = 0; i < QueryPoolInitialSize; i++)
 39              {
 40                  _queryPool.Enqueue(new BufferedQuery(glType));
 41              }
 42  
 43              _current = new CounterQueueEvent(this, glType, 0);
 44  
 45              _consumerThread = new Thread(EventConsumer);
 46              _consumerThread.Start();
 47          }
 48  
 49          private void EventConsumer()
 50          {
 51              while (!Disposed)
 52              {
 53                  CounterQueueEvent evt = null;
 54                  lock (_lock)
 55                  {
 56                      if (_events.Count > 0)
 57                      {
 58                          evt = _events.Dequeue();
 59                      }
 60                  }
 61  
 62                  if (evt == null)
 63                  {
 64                      _queuedEvent.WaitOne(); // No more events to go through, wait for more.
 65                  }
 66                  else
 67                  {
 68                      // Spin-wait rather than sleeping if there are any waiters, by passing null instead of the wake signal.
 69                      evt.TryConsume(ref _accumulatedCounter, true, _waiterCount == 0 ? _wakeSignal : null);
 70                  }
 71  
 72                  if (_waiterCount > 0)
 73                  {
 74                      _eventConsumed.Set();
 75                  }
 76              }
 77          }
 78  
 79          internal BufferedQuery GetQueryObject()
 80          {
 81              // Creating/disposing query objects on a context we're sharing with will cause issues.
 82              // So instead, make a lot of query objects on the main thread and reuse them.
 83  
 84              lock (_lock)
 85              {
 86                  if (_queryPool.Count > 0)
 87                  {
 88                      BufferedQuery result = _queryPool.Dequeue();
 89                      return result;
 90                  }
 91                  else
 92                  {
 93                      return new BufferedQuery(GetTarget(Type));
 94                  }
 95              }
 96          }
 97  
 98          internal void ReturnQueryObject(BufferedQuery query)
 99          {
100              lock (_lock)
101              {
102                  _queryPool.Enqueue(query);
103              }
104          }
105  
106          public CounterQueueEvent QueueReport(EventHandler<ulong> resultHandler, float divisor, ulong lastDrawIndex, bool hostReserved)
107          {
108              CounterQueueEvent result;
109              ulong draws = lastDrawIndex - _current.DrawIndex;
110  
111              lock (_lock)
112              {
113                  // A query's result only matters if more than one draw was performed during it.
114                  // Otherwise, dummy it out and return 0 immediately.
115  
116                  if (hostReserved)
117                  {
118                      // This counter event is guaranteed to be available for host conditional rendering.
119                      _current.ReserveForHostAccess();
120                  }
121  
122                  _current.Complete(draws > 0, divisor);
123                  _events.Enqueue(_current);
124  
125                  _current.OnResult += resultHandler;
126  
127                  result = _current;
128  
129                  _current = new CounterQueueEvent(this, GetTarget(Type), lastDrawIndex);
130              }
131  
132              _queuedEvent.Set();
133  
134              return result;
135          }
136  
137          public void QueueReset()
138          {
139              lock (_lock)
140              {
141                  _current.Clear();
142              }
143          }
144  
145          private static QueryTarget GetTarget(CounterType type)
146          {
147              return type switch
148              {
149                  CounterType.SamplesPassed => QueryTarget.SamplesPassed,
150                  CounterType.PrimitivesGenerated => QueryTarget.PrimitivesGenerated,
151                  CounterType.TransformFeedbackPrimitivesWritten => QueryTarget.TransformFeedbackPrimitivesWritten,
152                  _ => QueryTarget.SamplesPassed,
153              };
154          }
155  
156          public void Flush(bool blocking)
157          {
158              if (!blocking)
159              {
160                  // Just wake the consumer thread - it will update the queries.
161                  _wakeSignal.Set();
162                  return;
163              }
164  
165              lock (_lock)
166              {
167                  // Tell the queue to process all events.
168                  while (_events.Count > 0)
169                  {
170                      CounterQueueEvent flush = _events.Peek();
171                      if (!flush.TryConsume(ref _accumulatedCounter, true))
172                      {
173                          return; // If not blocking, then return when we encounter an event that is not ready yet.
174                      }
175                      _events.Dequeue();
176                  }
177              }
178          }
179  
180          public void FlushTo(CounterQueueEvent evt)
181          {
182              // Flush the counter queue on the main thread.
183  
184              Interlocked.Increment(ref _waiterCount);
185  
186              _wakeSignal.Set();
187  
188              while (!evt.Disposed)
189              {
190                  _eventConsumed.WaitOne(1);
191              }
192  
193              Interlocked.Decrement(ref _waiterCount);
194          }
195  
196          public void Dispose()
197          {
198              lock (_lock)
199              {
200                  while (_events.Count > 0)
201                  {
202                      CounterQueueEvent evt = _events.Dequeue();
203  
204                      evt.Dispose();
205                  }
206  
207                  Disposed = true;
208              }
209  
210              _queuedEvent.Set();
211  
212              _consumerThread.Join();
213  
214              foreach (BufferedQuery query in _queryPool)
215              {
216                  query.Dispose();
217              }
218  
219              _queuedEvent.Dispose();
220              _wakeSignal.Dispose();
221              _eventConsumed.Dispose();
222          }
223      }
224  }