VisualizationHelpers.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. using System;
  2. using UnityEngine.InputSystem.Utilities;
  3. using Unity.Collections.LowLevel.Unsafe;
  4. using UnityEngine.InputSystem.LowLevel;
  5. ////REVIEW: for vector2 visualizers of sticks, it could be useful to also visualize deadzones and raw values
  6. namespace UnityEngine.InputSystem.Samples
  7. {
  8. internal static class VisualizationHelpers
  9. {
  10. public enum Axis { X, Y, Z }
  11. public abstract class Visualizer
  12. {
  13. public abstract void OnDraw(Rect rect);
  14. public abstract void AddSample(object value, double time);
  15. }
  16. public abstract class ValueVisualizer<TValue> : Visualizer
  17. where TValue : struct
  18. {
  19. public RingBuffer<TValue> samples;
  20. public RingBuffer<GUIContent> samplesText;
  21. protected ValueVisualizer(int numSamples = 10)
  22. {
  23. samples = new RingBuffer<TValue>(numSamples);
  24. samplesText = new RingBuffer<GUIContent>(numSamples);
  25. }
  26. public override void AddSample(object value, double time)
  27. {
  28. var v = default(TValue);
  29. if (value != null)
  30. {
  31. if (!(value is TValue val))
  32. throw new ArgumentException(
  33. $"Expecting value of type '{typeof(TValue).Name}' but value of type '{value?.GetType().Name}' instead",
  34. nameof(value));
  35. v = val;
  36. }
  37. samples.Append(v);
  38. samplesText.Append(new GUIContent(v.ToString()));
  39. }
  40. }
  41. // Visualizes integer and real type primitives.
  42. public class ScalarVisualizer<TValue> : ValueVisualizer<TValue>
  43. where TValue : struct
  44. {
  45. public TValue limitMin;
  46. public TValue limitMax;
  47. public TValue min;
  48. public TValue max;
  49. public ScalarVisualizer(int numSamples = 10)
  50. : base(numSamples)
  51. {
  52. }
  53. public override void OnDraw(Rect rect)
  54. {
  55. // For now, only draw the current value.
  56. DrawRectangle(rect, new Color(1, 1, 1, 0.1f));
  57. if (samples.count == 0)
  58. return;
  59. var sample = samples[samples.count - 1];
  60. if (Compare(sample, default) == 0)
  61. return;
  62. if (Compare(limitMin, default) != 0)
  63. {
  64. // Two-way visualization with positive and negative side.
  65. throw new NotImplementedException();
  66. }
  67. else
  68. {
  69. // One-way visualization with only positive side.
  70. var ratio = Divide(sample, limitMax);
  71. var fillRect = rect;
  72. fillRect.width = rect.width * ratio;
  73. DrawRectangle(fillRect, new Color(0, 1, 0, 0.75f));
  74. var valuePos = new Vector2(fillRect.xMax, fillRect.y + fillRect.height / 2);
  75. DrawText(samplesText[samples.count - 1], valuePos, ValueTextStyle);
  76. }
  77. }
  78. public override void AddSample(object value, double time)
  79. {
  80. base.AddSample(value, time);
  81. if (value != null)
  82. {
  83. var val = (TValue)value;
  84. if (Compare(min, val) > 0)
  85. min = val;
  86. if (Compare(max, val) < 0)
  87. max = val;
  88. }
  89. }
  90. private static unsafe int Compare(TValue left, TValue right)
  91. {
  92. var leftPtr = UnsafeUtility.AddressOf(ref left);
  93. var rightPtr = UnsafeUtility.AddressOf(ref right);
  94. if (typeof(TValue) == typeof(int))
  95. return ((int*)leftPtr)->CompareTo(*(int*)rightPtr);
  96. if (typeof(TValue) == typeof(float))
  97. return ((float*)leftPtr)->CompareTo(*(float*)rightPtr);
  98. throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
  99. }
  100. private static unsafe void Subtract(ref TValue left, TValue right)
  101. {
  102. var leftPtr = UnsafeUtility.AddressOf(ref left);
  103. var rightPtr = UnsafeUtility.AddressOf(ref right);
  104. if (typeof(TValue) == typeof(int))
  105. *(int*)leftPtr = *(int*)leftPtr - *(int*)rightPtr;
  106. if (typeof(TValue) == typeof(float))
  107. *(float*)leftPtr = *(float*)leftPtr - *(float*)rightPtr;
  108. throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
  109. }
  110. private static unsafe float Divide(TValue left, TValue right)
  111. {
  112. var leftPtr = UnsafeUtility.AddressOf(ref left);
  113. var rightPtr = UnsafeUtility.AddressOf(ref right);
  114. if (typeof(TValue) == typeof(int))
  115. return (float)*(int*)leftPtr / *(int*)rightPtr;
  116. if (typeof(TValue) == typeof(float))
  117. return *(float*)leftPtr / *(float*)rightPtr;
  118. throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
  119. }
  120. }
  121. ////TODO: allow asymmetric center (i.e. center not being a midpoint of rectangle)
  122. ////TODO: enforce proper proportion between X and Y; it's confusing that X and Y can have different units yet have the same length
  123. public class Vector2Visualizer : ValueVisualizer<Vector2>
  124. {
  125. // Our value space extends radially from the center, i.e. we have
  126. // 360 discrete directions. Sampling at that granularity doesn't work
  127. // super well in visualizations so we quantize to 3 degree increments.
  128. public Vector2[] maximums = new Vector2[360 / 3];
  129. public Vector2 limits = new Vector2(1, 1);
  130. private GUIContent limitsXText;
  131. private GUIContent limitsYText;
  132. public Vector2Visualizer(int numSamples = 10)
  133. : base(numSamples)
  134. {
  135. }
  136. public override void AddSample(object value, double time)
  137. {
  138. base.AddSample(value, time);
  139. if (value != null)
  140. {
  141. // Keep track of radial maximums.
  142. var vector = (Vector2)value;
  143. var angle = Vector2.SignedAngle(Vector2.right, vector);
  144. if (angle < 0)
  145. angle = 360 + angle;
  146. var angleInt = Mathf.FloorToInt(angle) / 3;
  147. if (vector.sqrMagnitude > maximums[angleInt].sqrMagnitude)
  148. maximums[angleInt] = vector;
  149. // Extend limits if value is out of range.
  150. var limitX = Mathf.Max(Mathf.Abs(vector.x), limits.x);
  151. var limitY = Mathf.Max(Mathf.Abs(vector.y), limits.y);
  152. if (!Mathf.Approximately(limitX, limits.x))
  153. {
  154. limits.x = limitX;
  155. limitsXText = null;
  156. }
  157. if (!Mathf.Approximately(limitY, limits.y))
  158. {
  159. limits.y = limitY;
  160. limitsYText = null;
  161. }
  162. }
  163. }
  164. public override void OnDraw(Rect rect)
  165. {
  166. DrawRectangle(rect, new Color(1, 1, 1, 0.1f));
  167. DrawAxis(Axis.X, rect, new Color(0, 1, 0, 0.75f));
  168. DrawAxis(Axis.Y, rect, new Color(0, 1, 0, 0.75f));
  169. var sampleCount = samples.count;
  170. if (sampleCount == 0)
  171. return;
  172. // If limits aren't (1,1), show the actual values.
  173. if (limits != new Vector2(1, 1))
  174. {
  175. if (limitsXText == null)
  176. limitsXText = new GUIContent(limits.x.ToString());
  177. if (limitsYText == null)
  178. limitsYText = new GUIContent(limits.y.ToString());
  179. var limitsXSize = ValueTextStyle.CalcSize(limitsXText);
  180. var limitsXPos = new Vector2(rect.x - limitsXSize.x, rect.y - 5);
  181. var limitsYPos = new Vector2(rect.xMax, rect.yMax);
  182. DrawText(limitsXText, limitsXPos, ValueTextStyle);
  183. DrawText(limitsYText, limitsYPos, ValueTextStyle);
  184. }
  185. // Draw maximums.
  186. var numMaximums = 0;
  187. var firstMaximumPos = default(Vector2);
  188. var lastMaximumPos = default(Vector2);
  189. for (var i = 0; i < 360 / 3; ++i)
  190. {
  191. var value = maximums[i];
  192. if (value == default)
  193. continue;
  194. var valuePos = PixelPosForValue(value, rect);
  195. if (numMaximums > 0)
  196. DrawLine(lastMaximumPos, valuePos, new Color(1, 1, 1, 0.25f));
  197. else
  198. firstMaximumPos = valuePos;
  199. lastMaximumPos = valuePos;
  200. ++numMaximums;
  201. }
  202. if (numMaximums > 1)
  203. DrawLine(lastMaximumPos, firstMaximumPos, new Color(1, 1, 1, 0.25f));
  204. // Draw samples.
  205. var alphaStep = 1f / sampleCount;
  206. var alpha = 1f;
  207. for (var i = sampleCount - 1; i >= 0; --i) // Go newest to oldest.
  208. {
  209. var value = samples[i];
  210. var valueRect = RectForValue(value, rect);
  211. DrawRectangle(valueRect, new Color(1, 0, 0, alpha));
  212. alpha -= alphaStep;
  213. }
  214. // Print value of most recent sample. Draw last so
  215. // we draw over the other stuff.
  216. var lastSample = samples[sampleCount - 1];
  217. var lastSamplePos = PixelPosForValue(lastSample, rect);
  218. lastSamplePos.x += 3;
  219. lastSamplePos.y += 3;
  220. DrawText(samplesText[sampleCount - 1], lastSamplePos, ValueTextStyle);
  221. }
  222. private Rect RectForValue(Vector2 value, Rect rect)
  223. {
  224. var pos = PixelPosForValue(value, rect);
  225. return new Rect(pos.x - 1, pos.y - 1, 2, 2);
  226. }
  227. private Vector2 PixelPosForValue(Vector2 value, Rect rect)
  228. {
  229. var center = rect.center;
  230. var x = Mathf.Abs(value.x) / limits.x * Mathf.Sign(value.x);
  231. var y = Mathf.Abs(value.y) / limits.y * Mathf.Sign(value.y) * -1; // GUI Y is upside down.
  232. var xInPixels = x * rect.width / 2;
  233. var yInPixels = y * rect.height / 2;
  234. return new Vector2(center.x + xInPixels,
  235. center.y + yInPixels);
  236. }
  237. }
  238. // Y axis is time, X axis can be multiple visualizations.
  239. public class TimelineVisualizer : Visualizer
  240. {
  241. public bool showLegend { get; set; }
  242. public bool showLimits { get; set; }
  243. public TimeUnit timeUnit { get; set; } = TimeUnit.Seconds;
  244. public GUIContent valueUnit { get; set; }
  245. ////REVIEW: should this be per timeline?
  246. public int timelineCount => m_Timelines != null ? m_Timelines.Length : 0;
  247. public int historyDepth { get; set; } = 100;
  248. public Vector2 limitsY
  249. {
  250. get => m_LimitsY;
  251. set
  252. {
  253. m_LimitsY = value;
  254. m_LimitsYMin = null;
  255. m_LimitsYMax = null;
  256. }
  257. }
  258. public TimelineVisualizer(float totalTimeUnitsShown = 4)
  259. {
  260. m_TotalTimeUnitsShown = totalTimeUnitsShown;
  261. }
  262. public override void OnDraw(Rect rect)
  263. {
  264. var endTime = Time.realtimeSinceStartup;
  265. var startTime = endTime - m_TotalTimeUnitsShown;
  266. var endFrame = InputState.updateCount;
  267. var startFrame = endFrame - (int)m_TotalTimeUnitsShown;
  268. for (var i = 0; i < timelineCount; ++i)
  269. {
  270. var timeline = m_Timelines[i];
  271. var sampleCount = timeUnit == TimeUnit.Frames
  272. ? timeline.frameSamples.count
  273. : timeline.timeSamples.count;
  274. // Set up clip rect so that we can do stuff like render lines to samples
  275. // falling outside the render rectangle and have them get clipped.
  276. GUI.BeginGroup(rect);
  277. var plotType = timeline.plotType;
  278. var lastPos = default(Vector2);
  279. var timeUnitsPerPixel = rect.width / m_TotalTimeUnitsShown;
  280. var color = m_Timelines[i].color;
  281. for (var n = sampleCount - 1; n >= 0; --n)
  282. {
  283. var sample = timeUnit == TimeUnit.Frames
  284. ? timeline.frameSamples[n].value
  285. : timeline.timeSamples[n].value;
  286. ////TODO: respect limitsY
  287. float y;
  288. if (sample.isEmpty)
  289. y = 0.5f;
  290. else
  291. y = sample.ToSingle();
  292. y /= limitsY.y;
  293. var deltaTime = timeUnit == TimeUnit.Frames
  294. ? timeline.frameSamples[n].frame - startFrame
  295. : timeline.timeSamples[n].time - startTime;
  296. var pos = new Vector2(deltaTime * timeUnitsPerPixel, rect.height - y * rect.height);
  297. if (plotType == PlotType.LineGraph)
  298. {
  299. if (n != sampleCount - 1)
  300. {
  301. DrawLine(lastPos, pos, color, 2);
  302. if (pos.x < 0)
  303. break;
  304. }
  305. }
  306. else if (plotType == PlotType.BarChart)
  307. {
  308. ////TODO: make rectangles have a progressively stronger hue or saturation
  309. var barRect = new Rect(pos.x, pos.y, timeUnitsPerPixel, y * limitsY.y * rect.height);
  310. DrawRectangle(barRect, color);
  311. }
  312. lastPos = pos;
  313. }
  314. GUI.EndGroup();
  315. }
  316. if (showLegend && timelineCount > 0)
  317. {
  318. var legendRect = rect;
  319. legendRect.x += rect.width + 2;
  320. legendRect.width = 400;
  321. legendRect.height = ValueTextStyle.CalcHeight(m_Timelines[0].name, 400);
  322. for (var i = 0; i < m_Timelines.Length; ++i)
  323. {
  324. var colorTagRect = legendRect;
  325. colorTagRect.width = 5;
  326. var labelRect = legendRect;
  327. labelRect.x += 8;
  328. labelRect.width -= 8;
  329. DrawRectangle(colorTagRect, m_Timelines[i].color);
  330. DrawText(m_Timelines[i].name, labelRect.position, ValueTextStyle);
  331. legendRect.y += labelRect.height + 2;
  332. }
  333. }
  334. if (showLimits)
  335. {
  336. if (m_LimitsYMax == null)
  337. m_LimitsYMax = new GUIContent(m_LimitsY.y.ToString());
  338. if (m_LimitsYMin == null)
  339. m_LimitsYMin = new GUIContent(m_LimitsY.x.ToString());
  340. DrawText(m_LimitsYMax, new Vector2(rect.x + rect.width, rect.y), ValueTextStyle);
  341. DrawText(m_LimitsYMin, new Vector2(rect.x + rect.width, rect.y + rect.height), ValueTextStyle);
  342. }
  343. }
  344. public override void AddSample(object value, double time)
  345. {
  346. if (timelineCount == 0)
  347. throw new InvalidOperationException("Must have set up a timeline first");
  348. AddSample(0, PrimitiveValue.FromObject(value), (float)time);
  349. }
  350. public int AddTimeline(string name, Color color, PlotType plotType = default)
  351. {
  352. var timeline = new Timeline
  353. {
  354. name = new GUIContent(name),
  355. color = color,
  356. plotType = plotType,
  357. };
  358. if (timeUnit == TimeUnit.Frames)
  359. timeline.frameSamples = new RingBuffer<FrameSample>(historyDepth);
  360. else
  361. timeline.timeSamples = new RingBuffer<TimeSample>(historyDepth);
  362. var index = timelineCount;
  363. Array.Resize(ref m_Timelines, timelineCount + 1);
  364. m_Timelines[index] = timeline;
  365. return index;
  366. }
  367. public int GetTimeline(string name)
  368. {
  369. for (var i = 0; i < timelineCount; ++i)
  370. if (string.Compare(m_Timelines[i].name.text, name, StringComparison.InvariantCultureIgnoreCase) == 0)
  371. return i;
  372. return -1;
  373. }
  374. // Add a time-based sample.
  375. public void AddSample(int timelineIndex, PrimitiveValue value, float time)
  376. {
  377. m_Timelines[timelineIndex].timeSamples.Append(new TimeSample
  378. {
  379. value = value,
  380. time = time
  381. });
  382. }
  383. // Add a frame-based sample.
  384. public ref PrimitiveValue GetOrCreateSample(int timelineIndex, int frame)
  385. {
  386. ref var timeline = ref m_Timelines[timelineIndex];
  387. ref var samples = ref timeline.frameSamples;
  388. var count = samples.count;
  389. if (count > 0)
  390. {
  391. if (samples[count - 1].frame == frame)
  392. return ref samples[count - 1].value;
  393. Debug.Assert(samples[count - 1].frame < frame, "Frame numbers must be ascending");
  394. }
  395. return ref samples.Append(new FrameSample {frame = frame}).value;
  396. }
  397. private float m_TotalTimeUnitsShown;
  398. private Vector2 m_LimitsY = new Vector2(-1, 1);
  399. private GUIContent m_LimitsYMin;
  400. private GUIContent m_LimitsYMax;
  401. private Timeline[] m_Timelines;
  402. private struct TimeSample
  403. {
  404. public PrimitiveValue value;
  405. public float time;
  406. }
  407. private struct FrameSample
  408. {
  409. public PrimitiveValue value;
  410. public int frame;
  411. }
  412. private struct Timeline
  413. {
  414. public GUIContent name;
  415. public Color color;
  416. public RingBuffer<TimeSample> timeSamples;
  417. public RingBuffer<FrameSample> frameSamples;
  418. public PrimitiveValue minValue;
  419. public PrimitiveValue maxValue;
  420. public PlotType plotType;
  421. }
  422. public enum PlotType
  423. {
  424. LineGraph,
  425. BarChart,
  426. }
  427. public enum TimeUnit
  428. {
  429. Seconds,
  430. Frames,
  431. }
  432. }
  433. public static void DrawAxis(Axis axis, Rect rect, Color color = default, float width = 1)
  434. {
  435. Vector2 start, end, tickOffset;
  436. switch (axis)
  437. {
  438. case Axis.X:
  439. start = new Vector2(rect.x, rect.y + rect.height / 2);
  440. end = new Vector2(start.x + rect.width, rect.y + rect.height / 2);
  441. tickOffset = new Vector2(0, 3);
  442. break;
  443. case Axis.Y:
  444. start = new Vector2(rect.x + rect.width / 2, rect.y);
  445. end = new Vector2(start.x, rect.y + rect.height);
  446. tickOffset = new Vector2(3, 0);
  447. break;
  448. case Axis.Z:
  449. // From bottom left corner to upper right corner.
  450. start = new Vector2(rect.x, rect.yMax);
  451. end = new Vector2(rect.xMax, rect.y);
  452. tickOffset = new Vector2(1.5f, 1.5f);
  453. break;
  454. default:
  455. throw new NotImplementedException();
  456. }
  457. ////TODO: label limits
  458. DrawLine(start, end, color, width);
  459. DrawLine(start - tickOffset, start + tickOffset, color, width);
  460. DrawLine(end - tickOffset, end + tickOffset, color, width);
  461. }
  462. public static void DrawRectangle(Rect rect, Color color)
  463. {
  464. var savedColor = GUI.color;
  465. GUI.color = color;
  466. GUI.DrawTexture(rect, OnePixTex);
  467. GUI.color = savedColor;
  468. }
  469. public static void DrawText(string text, Vector2 pos, GUIStyle style)
  470. {
  471. var content = new GUIContent(text);
  472. DrawText(content, pos, style);
  473. }
  474. public static void DrawText(GUIContent text, Vector2 pos, GUIStyle style)
  475. {
  476. var content = new GUIContent(text);
  477. var size = style.CalcSize(content);
  478. var rect = new Rect(pos.x, pos.y, size.x, size.y);
  479. style.Draw(rect, content, false, false, false, false);
  480. }
  481. // Adapted from http://wiki.unity3d.com/index.php?title=DrawLine
  482. public static void DrawLine(Vector2 pointA, Vector2 pointB, Color color = default, float width = 1)
  483. {
  484. // Save the current GUI matrix, since we're going to make changes to it.
  485. var matrix = GUI.matrix;
  486. // Store current GUI color, so we can switch it back later,
  487. // and set the GUI color to the color parameter
  488. var savedColor = GUI.color;
  489. GUI.color = color;
  490. // Determine the angle of the line.
  491. var angle = Vector3.Angle(pointB - pointA, Vector2.right);
  492. // Vector3.Angle always returns a positive number.
  493. // If pointB is above pointA, then angle needs to be negative.
  494. if (pointA.y > pointB.y)
  495. angle = -angle;
  496. // Use ScaleAroundPivot to adjust the size of the line.
  497. // We could do this when we draw the texture, but by scaling it here we can use
  498. // non-integer values for the width and length (such as sub 1 pixel widths).
  499. // Note that the pivot point is at +.5 from pointA.y, this is so that the width of the line
  500. // is centered on the origin at pointA.
  501. GUIUtility.ScaleAroundPivot(new Vector2((pointB - pointA).magnitude, width), new Vector2(pointA.x, pointA.y + 0.5f));
  502. // Set the rotation for the line.
  503. // The angle was calculated with pointA as the origin.
  504. GUIUtility.RotateAroundPivot(angle, pointA);
  505. // Finally, draw the actual line.
  506. // We're really only drawing a 1x1 texture from pointA.
  507. // The matrix operations done with ScaleAroundPivot and RotateAroundPivot will make this
  508. // render with the proper width, length, and angle.
  509. GUI.DrawTexture(new Rect(pointA.x, pointA.y, 1, 1), OnePixTex);
  510. // We're done. Restore the GUI matrix and GUI color to whatever they were before.
  511. GUI.matrix = matrix;
  512. GUI.color = savedColor;
  513. }
  514. private static Texture2D s_OnePixTex;
  515. private static GUIStyle s_ValueTextStyle;
  516. internal static GUIStyle ValueTextStyle
  517. {
  518. get
  519. {
  520. if (s_ValueTextStyle == null)
  521. {
  522. s_ValueTextStyle = new GUIStyle();
  523. s_ValueTextStyle.fontSize -= 2;
  524. s_ValueTextStyle.normal.textColor = Color.white;
  525. }
  526. return s_ValueTextStyle;
  527. }
  528. }
  529. internal static Texture2D OnePixTex
  530. {
  531. get
  532. {
  533. if (s_OnePixTex == null)
  534. s_OnePixTex = new Texture2D(1, 1);
  535. return s_OnePixTex;
  536. }
  537. }
  538. public struct RingBuffer<TValue>
  539. {
  540. public TValue[] array;
  541. public int head;
  542. public int count;
  543. public RingBuffer(int size)
  544. {
  545. array = new TValue[size];
  546. head = 0;
  547. count = 0;
  548. }
  549. public ref TValue Append(TValue value)
  550. {
  551. int index;
  552. var bufferSize = array.Length;
  553. if (count < bufferSize)
  554. {
  555. Debug.Assert(head == 0, "Head can't have moved if buffer isn't full yet");
  556. index = count;
  557. ++count;
  558. }
  559. else
  560. {
  561. // Buffer is full. Bump head.
  562. index = (head + count) % bufferSize;
  563. ++head;
  564. }
  565. array[index] = value;
  566. return ref array[index];
  567. }
  568. public ref TValue this[int index]
  569. {
  570. get
  571. {
  572. if (index < 0 || index >= count)
  573. throw new ArgumentOutOfRangeException(nameof(index));
  574. return ref array[(head + index) % array.Length];
  575. }
  576. }
  577. }
  578. }
  579. }