|
|
|
|
//#define LOGGING
|
|
|
|
|
|
|
|
|
|
#if UNITY_4_6 || UNITY_4_7 || UNITY_5_0 || UNITY_5_1
|
|
|
|
|
#define LEGACY_UI
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
namespace SRDebugger.UI.Controls
|
|
|
|
|
{
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using Services;
|
|
|
|
|
using SRF;
|
|
|
|
|
using SRF.Service;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
[ExecuteInEditMode]
|
|
|
|
|
[RequireComponent(typeof (RectTransform))]
|
|
|
|
|
[RequireComponent(typeof (CanvasRenderer))]
|
|
|
|
|
public class ProfilerGraphControl : Graphic
|
|
|
|
|
{
|
|
|
|
|
public enum VerticalAlignments
|
|
|
|
|
{
|
|
|
|
|
Top,
|
|
|
|
|
Bottom
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public VerticalAlignments VerticalAlignment = VerticalAlignments.Bottom;
|
|
|
|
|
|
|
|
|
|
private static readonly float[] ScaleSteps =
|
|
|
|
|
{
|
|
|
|
|
1f/200f,
|
|
|
|
|
1f/160f,
|
|
|
|
|
1f/120f,
|
|
|
|
|
1f/100f,
|
|
|
|
|
1f/60f,
|
|
|
|
|
1f/30f,
|
|
|
|
|
1f/20f,
|
|
|
|
|
1f/12f,
|
|
|
|
|
1f/6f
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Resize the y-axis to fit the nearest useful fps value
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool FloatingScale;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// If not using FloatingScale, use the target FPS set by Application.targetFrameRate for TargetFps
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool TargetFpsUseApplication;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Toggle drawing of the various axes
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool DrawAxes = true;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// If FloatingScale is disabled, use this value to determine y-axis
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int TargetFps = 60;
|
|
|
|
|
|
|
|
|
|
public bool Clip = true;
|
|
|
|
|
|
|
|
|
|
public const float DataPointMargin = 2f;
|
|
|
|
|
public const float DataPointVerticalMargin = 2f;
|
|
|
|
|
|
|
|
|
|
public const float DataPointWidth = 4f;
|
|
|
|
|
|
|
|
|
|
public int VerticalPadding = 10;
|
|
|
|
|
|
|
|
|
|
public const int LineCount = 3;
|
|
|
|
|
|
|
|
|
|
public Color[] LineColours = new Color[0];
|
|
|
|
|
|
|
|
|
|
private IProfilerService _profilerService;
|
|
|
|
|
|
|
|
|
|
private ProfilerGraphAxisLabel[] _axisLabels;
|
|
|
|
|
|
|
|
|
|
private Rect _clipBounds;
|
|
|
|
|
|
|
|
|
|
#if LEGACY_UI
|
|
|
|
|
private List<UIVertex> _vbo;
|
|
|
|
|
#else
|
|
|
|
|
private readonly List<Vector3> _meshVertices = new List<Vector3>();
|
|
|
|
|
private readonly List<Color32> _meshVertexColors = new List<Color32>();
|
|
|
|
|
private readonly List<int> _meshTriangles = new List<int>();
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
protected override void Awake()
|
|
|
|
|
{
|
|
|
|
|
base.Awake();
|
|
|
|
|
_profilerService = SRServiceManager.GetService<IProfilerService>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void Start()
|
|
|
|
|
{
|
|
|
|
|
base.Start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void Update()
|
|
|
|
|
{
|
|
|
|
|
SetVerticesDirty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if LEGACY_UI
|
|
|
|
|
protected override void OnFillVBO(List<UIVertex> vbo)
|
|
|
|
|
#else
|
|
|
|
|
[System.ObsoleteAttribute]
|
|
|
|
|
protected override void OnPopulateMesh(Mesh m)
|
|
|
|
|
#endif
|
|
|
|
|
{
|
|
|
|
|
#if LEGACY_UI
|
|
|
|
|
_vbo = vbo;
|
|
|
|
|
#else
|
|
|
|
|
_meshVertices.Clear();
|
|
|
|
|
_meshVertexColors.Clear();
|
|
|
|
|
_meshTriangles.Clear();
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#if LOGGING
|
|
|
|
|
if(!Application.isPlaying)
|
|
|
|
|
Debug.Log("Draw");
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
var graphWidth = rectTransform.rect.width;
|
|
|
|
|
var graphHeight = rectTransform.rect.height;
|
|
|
|
|
|
|
|
|
|
_clipBounds = new Rect(0, 0, graphWidth, graphHeight);
|
|
|
|
|
|
|
|
|
|
var targetFps = TargetFps;
|
|
|
|
|
|
|
|
|
|
if (Application.isPlaying && TargetFpsUseApplication && Application.targetFrameRate > 0)
|
|
|
|
|
{
|
|
|
|
|
targetFps = Application.targetFrameRate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var maxValue = 1f/targetFps;
|
|
|
|
|
|
|
|
|
|
// Holds the index of the nearest 'useful' FPS step
|
|
|
|
|
var fpsStep = -1;
|
|
|
|
|
|
|
|
|
|
var maxFrameTime = FloatingScale ? CalculateMaxFrameTime() : 1f/targetFps;
|
|
|
|
|
|
|
|
|
|
if (FloatingScale)
|
|
|
|
|
{
|
|
|
|
|
for (var i = 0; i < ScaleSteps.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var step = ScaleSteps[i];
|
|
|
|
|
|
|
|
|
|
if (maxFrameTime < step*1.1f)
|
|
|
|
|
{
|
|
|
|
|
maxValue = step;
|
|
|
|
|
fpsStep = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fall back on the largest one
|
|
|
|
|
if (fpsStep < 0)
|
|
|
|
|
{
|
|
|
|
|
fpsStep = ScaleSteps.Length - 1;
|
|
|
|
|
maxValue = ScaleSteps[fpsStep];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Search for the next scale step after the user-provided step
|
|
|
|
|
for (var i = 0; i < ScaleSteps.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var step = ScaleSteps[i];
|
|
|
|
|
|
|
|
|
|
if (maxFrameTime > step)
|
|
|
|
|
{
|
|
|
|
|
fpsStep = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var verticalScale = (graphHeight - (VerticalPadding*2))/maxValue;
|
|
|
|
|
|
|
|
|
|
// Number of data points that can fit into the graph space
|
|
|
|
|
var availableDataPoints = CalculateVisibleDataPointCount();
|
|
|
|
|
|
|
|
|
|
// Reallocate vertex array if insufficient length (or not yet created)
|
|
|
|
|
var sampleCount = GetFrameBufferCurrentSize();
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < sampleCount; i++)
|
|
|
|
|
{
|
|
|
|
|
// Break loop if all visible data points have been drawn
|
|
|
|
|
if (i >= availableDataPoints)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When using right-alignment, read from the end of the profiler buffer
|
|
|
|
|
var frame = GetFrame(sampleCount - i - 1);
|
|
|
|
|
|
|
|
|
|
// Left-hand x coord
|
|
|
|
|
var lx = graphWidth - DataPointWidth*i - DataPointWidth - graphWidth/2f;
|
|
|
|
|
|
|
|
|
|
DrawDataPoint(lx, verticalScale, frame);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (DrawAxes)
|
|
|
|
|
{
|
|
|
|
|
if (!FloatingScale)
|
|
|
|
|
{
|
|
|
|
|
DrawAxis(maxValue, maxValue*verticalScale, GetAxisLabel(0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var axisCount = 2;
|
|
|
|
|
var j = 0;
|
|
|
|
|
|
|
|
|
|
if (!FloatingScale)
|
|
|
|
|
{
|
|
|
|
|
j++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (var i = fpsStep; i >= 0; --i)
|
|
|
|
|
{
|
|
|
|
|
if (j >= axisCount)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DrawAxis(ScaleSteps[i], ScaleSteps[i]*verticalScale, GetAxisLabel(j));
|
|
|
|
|
++j;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if !LEGACY_UI
|
|
|
|
|
|
|
|
|
|
m.Clear();
|
|
|
|
|
m.SetVertices(_meshVertices);
|
|
|
|
|
m.SetColors(_meshVertexColors);
|
|
|
|
|
m.SetTriangles(_meshTriangles, 0);
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void DrawDataPoint(float xPosition, float verticalScale, ProfilerFrame frame)
|
|
|
|
|
{
|
|
|
|
|
// Right-hand x-coord
|
|
|
|
|
var rx = Mathf.Min(_clipBounds.width/2f, xPosition + DataPointWidth - DataPointMargin);
|
|
|
|
|
|
|
|
|
|
var currentLineHeight = 0f;
|
|
|
|
|
|
|
|
|
|
for (var j = 0; j < LineCount; j++)
|
|
|
|
|
{
|
|
|
|
|
var lineIndex = j;
|
|
|
|
|
|
|
|
|
|
var value = 0f;
|
|
|
|
|
|
|
|
|
|
if (j == 0)
|
|
|
|
|
{
|
|
|
|
|
value = (float) frame.UpdateTime;
|
|
|
|
|
}
|
|
|
|
|
else if (j == 1)
|
|
|
|
|
{
|
|
|
|
|
value = (float) frame.RenderTime;
|
|
|
|
|
}
|
|
|
|
|
else if (j == 2)
|
|
|
|
|
{
|
|
|
|
|
value = (float) frame.OtherTime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value *= verticalScale;
|
|
|
|
|
|
|
|
|
|
if (value.ApproxZero() || value - DataPointVerticalMargin*2f < 0f)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lower y-coord
|
|
|
|
|
var ly = currentLineHeight + DataPointVerticalMargin - rectTransform.rect.height/2f;
|
|
|
|
|
|
|
|
|
|
if (VerticalAlignment == VerticalAlignments.Top)
|
|
|
|
|
{
|
|
|
|
|
ly = rectTransform.rect.height/2f - currentLineHeight - DataPointVerticalMargin;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upper y-coord
|
|
|
|
|
var uy = ly + value - DataPointVerticalMargin;
|
|
|
|
|
|
|
|
|
|
if (VerticalAlignment == VerticalAlignments.Top)
|
|
|
|
|
{
|
|
|
|
|
uy = ly - value + DataPointVerticalMargin;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var c = LineColours[lineIndex];
|
|
|
|
|
|
|
|
|
|
AddRect(new Vector3(Mathf.Max(-_clipBounds.width/2f, xPosition), ly),
|
|
|
|
|
new Vector3(Mathf.Max(-_clipBounds.width/2f, xPosition), uy), new Vector3(rx, uy),
|
|
|
|
|
new Vector3(rx, ly), c);
|
|
|
|
|
|
|
|
|
|
currentLineHeight += value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void DrawAxis(float frameTime, float yPosition, ProfilerGraphAxisLabel label)
|
|
|
|
|
{
|
|
|
|
|
#if LOGGING
|
|
|
|
|
if(!Application.isPlaying)
|
|
|
|
|
Debug.Log("Draw Axis: {0}".Fmt(yPosition));
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
var lx = -rectTransform.rect.width*0.5f;
|
|
|
|
|
var rx = -lx;
|
|
|
|
|
|
|
|
|
|
var uy = yPosition - rectTransform.rect.height*0.5f + 0.5f;
|
|
|
|
|
var ly = yPosition - rectTransform.rect.height*0.5f - 0.5f;
|
|
|
|
|
|
|
|
|
|
var c = new Color(1f, 1f, 1f, 0.4f);
|
|
|
|
|
|
|
|
|
|
AddRect(new Vector3(lx, ly), new Vector3(lx, uy), new Vector3(rx, uy), new Vector3(rx, ly), c);
|
|
|
|
|
|
|
|
|
|
if (label != null)
|
|
|
|
|
{
|
|
|
|
|
label.SetValue(frameTime, yPosition);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void AddRect(Vector3 tl, Vector3 tr, Vector3 bl, Vector3 br, Color c)
|
|
|
|
|
{
|
|
|
|
|
#if LEGACY_UI
|
|
|
|
|
|
|
|
|
|
var v = UIVertex.simpleVert;
|
|
|
|
|
v.color = c;
|
|
|
|
|
|
|
|
|
|
v.position = tl;
|
|
|
|
|
_vbo.Add(v);
|
|
|
|
|
|
|
|
|
|
v.position = tr;
|
|
|
|
|
_vbo.Add(v);
|
|
|
|
|
|
|
|
|
|
v.position = bl;
|
|
|
|
|
_vbo.Add(v);
|
|
|
|
|
|
|
|
|
|
v.position = br;
|
|
|
|
|
_vbo.Add(v);
|
|
|
|
|
|
|
|
|
|
#else
|
|
|
|
|
|
|
|
|
|
// New UI system uses triangles
|
|
|
|
|
|
|
|
|
|
_meshVertices.Add(tl);
|
|
|
|
|
_meshVertices.Add(tr);
|
|
|
|
|
_meshVertices.Add(bl);
|
|
|
|
|
_meshVertices.Add(br);
|
|
|
|
|
|
|
|
|
|
_meshTriangles.Add(_meshVertices.Count - 4); // tl
|
|
|
|
|
_meshTriangles.Add(_meshVertices.Count - 3); // tr
|
|
|
|
|
_meshTriangles.Add(_meshVertices.Count - 1); // br
|
|
|
|
|
|
|
|
|
|
_meshTriangles.Add(_meshVertices.Count - 2); // bl
|
|
|
|
|
_meshTriangles.Add(_meshVertices.Count - 1); // br
|
|
|
|
|
_meshTriangles.Add(_meshVertices.Count - 3); // tr
|
|
|
|
|
|
|
|
|
|
_meshVertexColors.Add(c);
|
|
|
|
|
_meshVertexColors.Add(c);
|
|
|
|
|
_meshVertexColors.Add(c);
|
|
|
|
|
_meshVertexColors.Add(c);
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected ProfilerFrame GetFrame(int i)
|
|
|
|
|
{
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
|
|
|
|
if (!Application.isPlaying)
|
|
|
|
|
{
|
|
|
|
|
return TestData[i];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return _profilerService.FrameBuffer[i];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected int CalculateVisibleDataPointCount()
|
|
|
|
|
{
|
|
|
|
|
return Mathf.RoundToInt(rectTransform.rect.width/DataPointWidth);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected int GetFrameBufferCurrentSize()
|
|
|
|
|
{
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
|
|
|
|
if (!Application.isPlaying)
|
|
|
|
|
{
|
|
|
|
|
return TestData.Length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return _profilerService.FrameBuffer.Count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected int GetFrameBufferMaxSize()
|
|
|
|
|
{
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
|
|
|
|
if (!Application.isPlaying)
|
|
|
|
|
{
|
|
|
|
|
return TestData.Length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return _profilerService.FrameBuffer.Capacity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected float CalculateMaxFrameTime()
|
|
|
|
|
{
|
|
|
|
|
var frameCount = GetFrameBufferCurrentSize();
|
|
|
|
|
var c = Mathf.Min(CalculateVisibleDataPointCount(), frameCount);
|
|
|
|
|
|
|
|
|
|
var max = 0d;
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < c; i++)
|
|
|
|
|
{
|
|
|
|
|
var frameNumber = frameCount - i - 1;
|
|
|
|
|
|
|
|
|
|
var t = GetFrame(frameNumber);
|
|
|
|
|
|
|
|
|
|
if (t.FrameTime > max)
|
|
|
|
|
{
|
|
|
|
|
max = t.FrameTime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (float) max;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ProfilerGraphAxisLabel GetAxisLabel(int index)
|
|
|
|
|
{
|
|
|
|
|
if (_axisLabels == null || !Application.isPlaying)
|
|
|
|
|
{
|
|
|
|
|
_axisLabels = GetComponentsInChildren<ProfilerGraphAxisLabel>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_axisLabels.Length > index)
|
|
|
|
|
{
|
|
|
|
|
return _axisLabels[index];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Debug.LogWarning("[SRDebugger.Profiler] Not enough axis labels in pool");
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Editor Only test data
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
|
|
|
|
private ProfilerFrame[] TestData
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (_testData == null)
|
|
|
|
|
{
|
|
|
|
|
_testData = GenerateSampleData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _testData;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ProfilerFrame[] _testData;
|
|
|
|
|
|
|
|
|
|
protected static ProfilerFrame[] GenerateSampleData()
|
|
|
|
|
{
|
|
|
|
|
var sampleCount = 200;
|
|
|
|
|
|
|
|
|
|
var data = new ProfilerFrame[sampleCount];
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < sampleCount; i++)
|
|
|
|
|
{
|
|
|
|
|
var frame = new ProfilerFrame();
|
|
|
|
|
|
|
|
|
|
for (var j = 0; j < 3; j++)
|
|
|
|
|
{
|
|
|
|
|
var v = 0d;
|
|
|
|
|
|
|
|
|
|
if (j == 0)
|
|
|
|
|
{
|
|
|
|
|
v = Mathf.PerlinNoise(i/200f, 0);
|
|
|
|
|
}
|
|
|
|
|
else if (j == 1)
|
|
|
|
|
{
|
|
|
|
|
v = Mathf.PerlinNoise(0, i/200f);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
v = Random.Range(0, 1f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v *= (1f/60f)*0.333f;
|
|
|
|
|
|
|
|
|
|
// Simulate spikes
|
|
|
|
|
if (Random.value > 0.8f)
|
|
|
|
|
{
|
|
|
|
|
v *= Random.Range(1.2f, 1.8f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (j == 2)
|
|
|
|
|
{
|
|
|
|
|
v *= 0.1f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (j == 0)
|
|
|
|
|
{
|
|
|
|
|
frame.UpdateTime = v;
|
|
|
|
|
}
|
|
|
|
|
else if (j == 1)
|
|
|
|
|
{
|
|
|
|
|
frame.RenderTime = v;
|
|
|
|
|
}
|
|
|
|
|
else if (j == 2)
|
|
|
|
|
{
|
|
|
|
|
frame.FrameTime = frame.RenderTime + frame.UpdateTime + v;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data[i] = frame;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data[0] = new ProfilerFrame
|
|
|
|
|
{
|
|
|
|
|
FrameTime = 0.005,
|
|
|
|
|
RenderTime = 0.005,
|
|
|
|
|
UpdateTime = 0.005
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
data[sampleCount - 1] = new ProfilerFrame
|
|
|
|
|
{
|
|
|
|
|
FrameTime = 1d/60d,
|
|
|
|
|
RenderTime = 0.007,
|
|
|
|
|
UpdateTime = 0.007
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|