/**************************************************************************** * Copyright 2019 Nreal Techonology Limited. All rights reserved. * * This file is part of NRSDK. * * https://www.nreal.ai/ * *****************************************************************************/ namespace NRKernal { using System.Text; using UnityEngine; using UnityEngine.Profiling; #if WINDOWS_UWP using Windows.System; #endif /// /// ABOUT: The VisualProfiler provides a drop in, single file, solution for viewing your Nreal /// Unity application's frame rate and memory usage. Missed frames are displayed over time to /// visually find problem areas. Memory is reported as current, peak and max usage in a bar graph. /// /// USAGE: To use this profiler simply add this script as a component of any GameObject in your /// Unity scene. The profiler is initially active and visible (toggle-able via the IsVisible /// property), but can be toggled via the enabled/disable voice commands keywords. /// /// NOTE: For improved rendering performance you can optionally include the "Nreal/Instanced- /// Colored" shader in your project along with the VisualProfiler. public class NRVisualProfiler : MonoBehaviour { /// The maximum length of the string. private static readonly int maxStringLength = 32; /// The maximum target frame rate. private static readonly int maxTargetFrameRate = 120; /// The maximum frame timings. private static readonly int maxFrameTimings = 128; /// The frame range. private static readonly int frameRange = 30; /// The default window rotation. private static readonly Vector2 defaultWindowRotation = new Vector2(10.0f, 20.0f); /// The default window scale. private static readonly Vector3 defaultWindowScale = new Vector3(0.2f, 0.04f, 1.0f); /// The used memory string. private static readonly string usedMemoryString = "Used: "; /// The peak memory string. private static readonly string peakMemoryString = "Peak: "; /// The limit memory string. private static readonly string limitMemoryString = "Limit: "; /// Gets or sets the window parent. /// The window parent. public Transform WindowParent { get; set; } = null; /// True if is visible, false if not. [Header("Profiler Settings")] [SerializeField, Tooltip("Is the profiler currently visible.")] private bool isVisible = true; /// Gets or sets a value indicating whether this object is visible. /// True if this object is visible, false if not. public bool IsVisible { get { return isVisible; } set { isVisible = value; } } /// The frame sample rate. [SerializeField, Tooltip("The amount of time, in seconds, to collect frames for frame rate calculation.")] private float frameSampleRate = 0.1f; /// Gets or sets the frame sample rate. /// The frame sample rate. public float FrameSampleRate { get { return frameSampleRate; } set { frameSampleRate = value; } } /// The window anchor. [Header("Window Settings")] [SerializeField, Tooltip("What part of the view port to anchor the window to.")] private TextAnchor windowAnchor = TextAnchor.LowerCenter; /// Gets or sets the window anchor. /// The window anchor. public TextAnchor WindowAnchor { get { return windowAnchor; } set { windowAnchor = value; } } /// The window offset. [SerializeField, Tooltip("The offset from the view port center applied based on the window anchor selection.")] private Vector2 windowOffset = new Vector2(0.1f, 0.1f); /// Gets or sets the window offset. /// The window offset. public Vector2 WindowOffset { get { return windowOffset; } set { windowOffset = value; } } /// The window scale. [SerializeField, Range(0.5f, 5.0f), Tooltip("Use to scale the window size up or down, can simulate a zooming effect.")] private float windowScale = 1.0f; /// Gets or sets the window scale. /// The window scale. public float WindowScale { get { return windowScale; } set { windowScale = Mathf.Clamp(value, 0.5f, 5.0f); } } /// The window follow speed. [SerializeField, Range(0.0f, 100.0f), Tooltip("How quickly to interpolate the window towards its target position and rotation.")] private float windowFollowSpeed = 5.0f; /// Gets or sets the window follow speed. /// The window follow speed. public float WindowFollowSpeed { get { return windowFollowSpeed; } set { windowFollowSpeed = Mathf.Abs(value); } } /// The toggle keyworlds. [Header("UI Settings")] [SerializeField, Tooltip("Voice commands to toggle the profiler on and off.")] private string[] toggleKeyworlds = new string[] { "Profiler", "Toggle Profiler", "Show Profiler", "Hide Profiler" }; /// The displayed decimal digits. [SerializeField, Range(0, 3), Tooltip("How many decimal places to display on numeric strings.")] private int displayedDecimalDigits = 1; /// The base color. [SerializeField, Tooltip("The color of the window backplate.")] private Color baseColor = new Color(80 / 256.0f, 80 / 256.0f, 80 / 256.0f, 1.0f); /// The target frame rate color. [SerializeField, Tooltip("The color to display on frames which meet or exceed the target frame rate.")] private Color targetFrameRateColor = new Color(127 / 256.0f, 186 / 256.0f, 0 / 256.0f, 1.0f); /// The missed frame rate color. [SerializeField, Tooltip("The color to display on frames which fall below the target frame rate.")] private Color missedFrameRateColor = new Color(242 / 256.0f, 80 / 256.0f, 34 / 256.0f, 1.0f); /// The memory used color. [SerializeField, Tooltip("The color to display for current memory usage values.")] private Color memoryUsedColor = new Color(0 / 256.0f, 164 / 256.0f, 239 / 256.0f, 1.0f); /// The memory peak color. [SerializeField, Tooltip("The color to display for peak (aka max) memory usage values.")] private Color memoryPeakColor = new Color(255 / 256.0f, 185 / 256.0f, 0 / 256.0f, 1.0f); /// The memory limit color. [SerializeField, Tooltip("The color to display for the platforms memory usage limit.")] private Color memoryLimitColor = new Color(150 / 256.0f, 150 / 256.0f, 150 / 256.0f, 1.0f); /// The window. private GameObject window; /// The CPU frame rate text. private TextMesh cpuFrameRateText; #if USING_XR_SDK && !UNITY_EDITOR /// The Dropped frame count in last one second. private TextMesh droppedFrameCount; private static readonly string droppedFrameCountString = "DroppedFrameCount: {0}"; #endif /// The GPU frame rate text. private TextMesh gpuFrameRateText; /// The used memory text. private TextMesh usedMemoryText; /// The peak memory text. private TextMesh peakMemoryText; /// The limit memory text. private TextMesh limitMemoryText; /// The used anchor. private Transform usedAnchor; /// The peak anchor. private Transform peakAnchor; /// The window horizontal rotation. private Quaternion windowHorizontalRotation; /// The window horizontal rotation inverse. private Quaternion windowHorizontalRotationInverse; /// The window vertical rotation. private Quaternion windowVerticalRotation; /// The window vertical rotation inverse. private Quaternion windowVerticalRotationInverse; /// The frame information matrices. private Matrix4x4[] frameInfoMatrices; /// List of colors of the frame informations. private Vector4[] frameInfoColors; /// The frame information property block. private MaterialPropertyBlock frameInfoPropertyBlock; /// Identifier for the color. private int colorID; /// Identifier for the parent matrix. private int parentMatrixID; /// Number of frames. private int frameCount; /// The stopwatch. private System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); /// The frame timings. private FrameTiming[] frameTimings = new FrameTiming[maxFrameTimings]; /// The CPU frame rate strings. private string[] cpuFrameRateStrings; /// The GPU frame rate strings. private string[] gpuFrameRateStrings; /// Buffer for string data. private char[] stringBuffer = new char[maxStringLength]; /// The memory usage. private ulong memoryUsage; /// The peak memory usage. private ulong peakMemoryUsage; /// The limit memory usage. private ulong limitMemoryUsage; /// Rendering resources. [SerializeField, HideInInspector] private Material defaultMaterial; /// The default instanced material. [SerializeField, HideInInspector] private Material defaultInstancedMaterial; /// The background material. private Material backgroundMaterial; /// The foreground material. private Material foregroundMaterial; /// The text material. private Material textMaterial; /// The quad mesh. private Mesh quadMesh; private Transform m_CenterCamera; private Transform CenterCamera { get { if (m_CenterCamera == null) { if (NRSessionManager.Instance.CenterCameraAnchor != null) { m_CenterCamera = NRSessionManager.Instance.CenterCameraAnchor; } else { m_CenterCamera = Camera.main.transform; } } return m_CenterCamera; } } /// The instance. private static NRVisualProfiler m_Instance = null; /// Gets the instance. /// The instance. public static NRVisualProfiler Instance { get { if (m_Instance == null) { GameObject go = new GameObject("VisualProfiler"); DontDestroyOnLoad(go); m_Instance = go.AddComponent(); } return m_Instance; } } /// Awakes this object. void Awake() { if (m_Instance != null && m_Instance != this) { DestroyImmediate(this.gameObject); return; } m_Instance = this; } /// Switches. /// True to flag. public void Switch(bool flag) { this.gameObject.SetActive(flag); } /// Resets this object. private void Reset() { if (defaultMaterial == null) { defaultMaterial = new Material(Shader.Find("Nreal/Instanced-Colored")); defaultMaterial.SetFloat("_ZWrite", 0.0f); defaultMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Disabled); defaultMaterial.renderQueue = 5000; } if (defaultInstancedMaterial == null) { Shader defaultInstancedShader = Shader.Find("Nreal/Instanced-Colored"); if (defaultInstancedShader != null) { defaultInstancedMaterial = new Material(defaultInstancedShader); defaultInstancedMaterial.enableInstancing = true; defaultInstancedMaterial.SetFloat("_ZWrite", 0.0f); defaultInstancedMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Disabled); defaultInstancedMaterial.renderQueue = 5000; } else { Debug.LogWarning("A shader supporting instancing could not be found for the VisualProfiler, falling back to traditional rendering. This may impact performance."); } } if (Application.isPlaying) { backgroundMaterial = new Material(defaultMaterial); foregroundMaterial = new Material(defaultMaterial); defaultMaterial.renderQueue = foregroundMaterial.renderQueue - 1; backgroundMaterial.renderQueue = defaultMaterial.renderQueue - 1; MeshRenderer meshRenderer = new GameObject().AddComponent().GetComponent(); textMaterial = new Material(meshRenderer.sharedMaterial); textMaterial.renderQueue = defaultMaterial.renderQueue; Destroy(meshRenderer.gameObject); MeshFilter quadMeshFilter = GameObject.CreatePrimitive(PrimitiveType.Quad).GetComponent(); if (defaultInstancedMaterial != null) { // Create a quad mesh with artificially large bounds to disable culling for instanced rendering. quadMesh = quadMeshFilter.mesh; quadMesh.bounds = new Bounds(Vector3.zero, Vector3.one * float.MaxValue); } else { quadMesh = quadMeshFilter.sharedMesh; } Destroy(quadMeshFilter.gameObject); } stopwatch.Reset(); stopwatch.Start(); } /// Starts this object. private void Start() { Reset(); BuildWindow(); BuildFrameRateStrings(); } /// Executes the 'destroy' action. private void OnDestroy() { Destroy(window); } /// Late update. private void LateUpdate() { if (window == null) { return; } // Update window transformation. if (window.activeSelf && CenterCamera != null) { float t = Time.deltaTime * windowFollowSpeed; window.transform.position = Vector3.Lerp(window.transform.position, CalculateWindowPosition(CenterCamera.transform), t); window.transform.rotation = Quaternion.Slerp(window.transform.rotation, CalculateWindowRotation(CenterCamera.transform), t); window.transform.localScale = defaultWindowScale * windowScale; } // Capture frame timings every frame and read from it depending on the frameSampleRate. FrameTimingManager.CaptureFrameTimings(); ++frameCount; float elapsedSeconds = stopwatch.ElapsedMilliseconds * 0.001f; if (elapsedSeconds >= frameSampleRate) { int cpuFrameRate = (int)(1.0f / (elapsedSeconds / frameCount)); int gpuFrameRate = 0; // Many platforms do not yet support the FrameTimingManager. When timing data is returned from the FrameTimingManager we will use // its timing data, else we will depend on the stopwatch. uint frameTimingsCount = FrameTimingManager.GetLatestTimings((uint)Mathf.Min(frameCount, maxFrameTimings), frameTimings); if (frameTimingsCount != 0) { float cpuFrameTime, gpuFrameTime; AverageFrameTiming(frameTimings, frameTimingsCount, out cpuFrameTime, out gpuFrameTime); cpuFrameRate = (int)(1.0f / (cpuFrameTime / frameCount)); gpuFrameRate = (int)(1.0f / (gpuFrameTime / frameCount)); } // Update frame rate text. cpuFrameRateText.text = cpuFrameRateStrings[Mathf.Clamp(cpuFrameRate, 0, maxTargetFrameRate)]; #if USING_XR_SDK && !UNITY_EDITOR int dropped_framecount = 0; NRSessionManager.Instance.XRDisplaySubsystem?.TryGetDroppedFrameCount(out dropped_framecount); droppedFrameCount.text = string.Format(droppedFrameCountString, dropped_framecount); #endif if (gpuFrameRate != 0) { gpuFrameRateText.gameObject.SetActive(true); gpuFrameRateText.text = gpuFrameRateStrings[Mathf.Clamp(gpuFrameRate, 0, maxTargetFrameRate)]; } // Update frame colors. for (int i = frameRange - 1; i > 0; --i) { frameInfoColors[i] = frameInfoColors[i - 1]; } // Ideally we would query a device specific API (like the HolographicFramePresentationReport) to detect missed frames. // But, many of these APIs are inaccessible in Unity. Currently missed frames are assumed when the average cpuFrameRate // is under the target frame rate. frameInfoColors[0] = (cpuFrameRate < ((int)(AppFrameRate) - 1)) ? missedFrameRateColor : targetFrameRateColor; frameInfoPropertyBlock.SetVectorArray(colorID, frameInfoColors); // Reset timers. frameCount = 0; stopwatch.Reset(); stopwatch.Start(); } // Draw frame info. if (window.activeSelf) { Matrix4x4 parentLocalToWorldMatrix = window.transform.localToWorldMatrix; //if (defaultInstancedMaterial != null) //{ // frameInfoPropertyBlock.SetMatrix(parentMatrixID, parentLocalToWorldMatrix); // Graphics.DrawMeshInstanced(quadMesh, 0, defaultInstancedMaterial, frameInfoMatrices, frameInfoMatrices.Length, frameInfoPropertyBlock, UnityEngine.Rendering.ShadowCastingMode.Off, false); //} //else //{ // If a instanced material is not available, fall back to non-instanced rendering. for (int i = 0; i < frameInfoMatrices.Length; ++i) { frameInfoPropertyBlock.SetColor(colorID, frameInfoColors[i]); Graphics.DrawMesh(quadMesh, parentLocalToWorldMatrix * frameInfoMatrices[i], defaultMaterial, 0, null, 0, frameInfoPropertyBlock, false, false, false); } //} } // Update memory statistics. ulong limit = AppMemoryUsageLimit; if (limit != limitMemoryUsage) { if (window.activeSelf && WillDisplayedMemoryUsageDiffer(limitMemoryUsage, limit, displayedDecimalDigits)) { MemoryUsageToString(stringBuffer, displayedDecimalDigits, limitMemoryText, limitMemoryString, limit); } limitMemoryUsage = limit; } ulong usage = AppMemoryUsage; if (usage != memoryUsage) { usedAnchor.localScale = new Vector3((float)usage / limitMemoryUsage, usedAnchor.localScale.y, usedAnchor.localScale.z); if (window.activeSelf && WillDisplayedMemoryUsageDiffer(memoryUsage, usage, displayedDecimalDigits)) { MemoryUsageToString(stringBuffer, displayedDecimalDigits, usedMemoryText, usedMemoryString, usage); } memoryUsage = usage; } if (memoryUsage > peakMemoryUsage) { peakAnchor.localScale = new Vector3((float)memoryUsage / limitMemoryUsage, peakAnchor.localScale.y, peakAnchor.localScale.z); if (window.activeSelf && WillDisplayedMemoryUsageDiffer(peakMemoryUsage, memoryUsage, displayedDecimalDigits)) { MemoryUsageToString(stringBuffer, displayedDecimalDigits, peakMemoryText, peakMemoryString, memoryUsage); } peakMemoryUsage = memoryUsage; } window.SetActive(isVisible); } /// Calculates the window position. /// The camera transform. /// The calculated window position. private Vector3 CalculateWindowPosition(Transform cameraTransform) { float windowDistance = Mathf.Max(16.0f / Camera.main.fieldOfView, Camera.main.nearClipPlane + 0.25f); Vector3 position = cameraTransform.position + (cameraTransform.forward * windowDistance); Vector3 horizontalOffset = cameraTransform.right * windowOffset.x; Vector3 verticalOffset = cameraTransform.up * windowOffset.y; switch (windowAnchor) { case TextAnchor.UpperLeft: position += verticalOffset - horizontalOffset; break; case TextAnchor.UpperCenter: position += verticalOffset; break; case TextAnchor.UpperRight: position += verticalOffset + horizontalOffset; break; case TextAnchor.MiddleLeft: position -= horizontalOffset; break; case TextAnchor.MiddleRight: position += horizontalOffset; break; case TextAnchor.LowerLeft: position -= verticalOffset + horizontalOffset; break; case TextAnchor.LowerCenter: position -= verticalOffset; break; case TextAnchor.LowerRight: position -= verticalOffset - horizontalOffset; break; } return position; } /// Calculates the window rotation. /// The camera transform. /// The calculated window rotation. private Quaternion CalculateWindowRotation(Transform cameraTransform) { Quaternion rotation = cameraTransform.rotation; switch (windowAnchor) { case TextAnchor.UpperLeft: rotation *= windowHorizontalRotationInverse * windowVerticalRotationInverse; break; case TextAnchor.UpperCenter: rotation *= windowHorizontalRotationInverse; break; case TextAnchor.UpperRight: rotation *= windowHorizontalRotationInverse * windowVerticalRotation; break; case TextAnchor.MiddleLeft: rotation *= windowVerticalRotationInverse; break; case TextAnchor.MiddleRight: rotation *= windowVerticalRotation; break; case TextAnchor.LowerLeft: rotation *= windowHorizontalRotation * windowVerticalRotationInverse; break; case TextAnchor.LowerCenter: rotation *= windowHorizontalRotation; break; case TextAnchor.LowerRight: rotation *= windowHorizontalRotation * windowVerticalRotation; break; } return rotation; } /// Builds the window. private void BuildWindow() { // Initialize property block state. colorID = Shader.PropertyToID("_Color"); parentMatrixID = Shader.PropertyToID("_ParentLocalToWorldMatrix"); WindowParent = transform; // Build the window root. { window = CreateQuad("VisualProfiler", null); window.transform.parent = WindowParent; InitializeRenderer(window, backgroundMaterial, colorID, baseColor); window.transform.localScale = defaultWindowScale; windowHorizontalRotation = Quaternion.AngleAxis(defaultWindowRotation.y, Vector3.right); windowHorizontalRotationInverse = Quaternion.Inverse(windowHorizontalRotation); windowVerticalRotation = Quaternion.AngleAxis(defaultWindowRotation.x, Vector3.up); windowVerticalRotationInverse = Quaternion.Inverse(windowVerticalRotation); } // Add frame rate text and frame indicators. { cpuFrameRateText = CreateText("CPUFrameRateText", new Vector3(-0.495f, 0.5f, 0.0f), window.transform, TextAnchor.UpperLeft, textMaterial, Color.white, string.Empty); #if USING_XR_SDK && !UNITY_EDITOR droppedFrameCount = CreateText("DroppedFrameCount", new Vector3(0, 0.5f, 0.0f), window.transform, TextAnchor.UpperLeft, textMaterial, Color.white, string.Empty); #endif gpuFrameRateText = CreateText("GPUFrameRateText", new Vector3(0.495f, 0.5f, 0.0f), window.transform, TextAnchor.UpperRight, textMaterial, Color.white, string.Empty); gpuFrameRateText.gameObject.SetActive(false); frameInfoMatrices = new Matrix4x4[frameRange]; frameInfoColors = new Vector4[frameRange]; Vector3 scale = new Vector3(1.0f / frameRange, 0.2f, 1.0f); Vector3 position = new Vector3(0.5f - (scale.x * 0.5f), 0.15f, 0.0f); for (int i = 0; i < frameRange; ++i) { frameInfoMatrices[i] = Matrix4x4.TRS(position, Quaternion.identity, new Vector3(scale.x * 0.8f, scale.y, scale.z)); position.x -= scale.x; frameInfoColors[i] = targetFrameRateColor; } frameInfoPropertyBlock = new MaterialPropertyBlock(); frameInfoPropertyBlock.SetVectorArray(colorID, frameInfoColors); } // Add memory usage text and bars. { usedMemoryText = CreateText("UsedMemoryText", new Vector3(-0.495f, 0.0f, 0.0f), window.transform, TextAnchor.UpperLeft, textMaterial, memoryUsedColor, usedMemoryString); peakMemoryText = CreateText("PeakMemoryText", new Vector3(0.0f, 0.0f, 0.0f), window.transform, TextAnchor.UpperCenter, textMaterial, memoryPeakColor, peakMemoryString); limitMemoryText = CreateText("LimitMemoryText", new Vector3(0.495f, 0.0f, 0.0f), window.transform, TextAnchor.UpperRight, textMaterial, Color.white, limitMemoryString); GameObject limitBar = CreateQuad("LimitBar", window.transform); InitializeRenderer(limitBar, defaultMaterial, colorID, memoryLimitColor); limitBar.transform.localScale = new Vector3(0.99f, 0.2f, 1.0f); limitBar.transform.localPosition = new Vector3(0.0f, -0.37f, 0.0f); { usedAnchor = CreateAnchor("UsedAnchor", limitBar.transform); GameObject bar = CreateQuad("UsedBar", usedAnchor); Material material = new Material(foregroundMaterial); material.renderQueue = material.renderQueue + 1; InitializeRenderer(bar, material, colorID, memoryUsedColor); bar.transform.localScale = Vector3.one; bar.transform.localPosition = new Vector3(0.5f, 0.0f, 0.0f); } { peakAnchor = CreateAnchor("PeakAnchor", limitBar.transform); GameObject bar = CreateQuad("PeakBar", peakAnchor); InitializeRenderer(bar, foregroundMaterial, colorID, memoryPeakColor); bar.transform.localScale = Vector3.one; bar.transform.localPosition = new Vector3(0.5f, 0.0f, 0.0f); } } window.SetActive(isVisible); } /// Builds frame rate strings. private void BuildFrameRateStrings() { cpuFrameRateStrings = new string[maxTargetFrameRate + 1]; gpuFrameRateStrings = new string[maxTargetFrameRate + 1]; string displayedDecimalFormat = string.Format("{{0:F{0}}}", displayedDecimalDigits); StringBuilder stringBuilder = new StringBuilder(32); StringBuilder milisecondStringBuilder = new StringBuilder(16); for (int i = 0; i < cpuFrameRateStrings.Length; ++i) { float miliseconds = (i == 0) ? 0.0f : (1.0f / i) * 1000.0f; milisecondStringBuilder.AppendFormat(displayedDecimalFormat, miliseconds); stringBuilder.AppendFormat("CPU: {0} fps ({1} ms)", i.ToString(), milisecondStringBuilder.ToString()); cpuFrameRateStrings[i] = stringBuilder.ToString(); stringBuilder.Length = 0; stringBuilder.AppendFormat("GPU: {0} fps ({1} ms)", i.ToString(), milisecondStringBuilder.ToString()); gpuFrameRateStrings[i] = stringBuilder.ToString(); milisecondStringBuilder.Length = 0; stringBuilder.Length = 0; } } /// Creates an anchor. /// The name. /// The parent. /// The new anchor. private static Transform CreateAnchor(string name, Transform parent) { Transform anchor = new GameObject(name).transform; anchor.parent = parent; anchor.localScale = Vector3.one; anchor.localPosition = new Vector3(-0.5f, 0.0f, 0.0f); return anchor; } /// Creates a quad. /// The name. /// The parent. /// The new quad. private static GameObject CreateQuad(string name, Transform parent) { GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad); Destroy(quad.GetComponent()); quad.name = name; quad.transform.parent = parent; return quad; } /// Creates a text. /// The name. /// The position. /// The parent. /// The anchor. /// The material. /// The color. /// The text. /// The new text. private static TextMesh CreateText(string name, Vector3 position, Transform parent, TextAnchor anchor, Material material, Color color, string text) { GameObject obj = new GameObject(name); obj.transform.localScale = Vector3.one * 0.0016f; obj.transform.parent = parent; obj.transform.localPosition = position; TextMesh textMesh = obj.AddComponent(); textMesh.fontSize = 48; textMesh.anchor = anchor; textMesh.color = color; textMesh.text = text; textMesh.richText = false; Renderer renderer = obj.GetComponent(); renderer.sharedMaterial = material; OptimizeRenderer(renderer); return textMesh; } /// Initializes the renderer. /// The object. /// The material. /// Identifier for the color. /// The color. /// A Renderer. private static Renderer InitializeRenderer(GameObject obj, Material material, int colorID, Color color) { Renderer renderer = obj.GetComponent(); renderer.sharedMaterial = material; MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock(); renderer.GetPropertyBlock(propertyBlock); propertyBlock.SetColor(colorID, color); renderer.SetPropertyBlock(propertyBlock); OptimizeRenderer(renderer); return renderer; } /// Optimize renderer. /// The renderer. private static void OptimizeRenderer(Renderer renderer) { renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; renderer.receiveShadows = false; renderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion; renderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off; renderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off; renderer.allowOcclusionWhenDynamic = false; } /// Memory usage to string. /// Buffer for string data. /// The displayed decimal digits. /// The text mesh. /// The prefix string. /// The memory usage. private static void MemoryUsageToString(char[] stringBuffer, int displayedDecimalDigits, TextMesh textMesh, string prefixString, ulong memoryUsage) { // Using a custom number to string method to avoid the overhead, and allocations, of built in string.Format/StringBuilder methods. // We can also make some assumptions since the domain of the input number (memoryUsage) is known. float memoryUsageMB = ConvertBytesToMegabytes(memoryUsage); int memoryUsageIntegerDigits = (int)memoryUsageMB; int memoryUsageFractionalDigits = (int)((memoryUsageMB - memoryUsageIntegerDigits) * Mathf.Pow(10.0f, displayedDecimalDigits)); int bufferIndex = 0; for (int i = 0; i < prefixString.Length; ++i) { stringBuffer[bufferIndex++] = prefixString[i]; } bufferIndex = MemoryItoA(memoryUsageIntegerDigits, stringBuffer, bufferIndex); stringBuffer[bufferIndex++] = '.'; if (memoryUsageFractionalDigits != 0) { bufferIndex = MemoryItoA(memoryUsageFractionalDigits, stringBuffer, bufferIndex); } else { for (int i = 0; i < displayedDecimalDigits; ++i) { stringBuffer[bufferIndex++] = '0'; } } stringBuffer[bufferIndex++] = 'M'; stringBuffer[bufferIndex++] = 'B'; textMesh.text = new string(stringBuffer, 0, bufferIndex); } /// Memory ito a. /// The value. /// Buffer for string data. /// Zero-based index of the buffer. /// An int. private static int MemoryItoA(int value, char[] stringBuffer, int bufferIndex) { int startIndex = bufferIndex; for (; value != 0; value /= 10) { stringBuffer[bufferIndex++] = (char)((char)(value % 10) + '0'); } char temp; for (int endIndex = bufferIndex - 1; startIndex < endIndex; ++startIndex, --endIndex) { temp = stringBuffer[startIndex]; stringBuffer[startIndex] = stringBuffer[endIndex]; stringBuffer[endIndex] = temp; } return bufferIndex; } /// Gets the application frame rate. /// The application frame rate. private static float AppFrameRate { get { // If the current XR SDK does not report refresh rate information, assume 60Hz. //float refreshRate = UnityEngine.XR.XRDevice.refreshRate; //return ((int)refreshRate == 0) ? 60.0f : refreshRate; return 45f; } } /// Average frame timing. /// The frame timings. /// Number of frame timings. /// [out] The CPU frame time. /// [out] The GPU frame time. private static void AverageFrameTiming(FrameTiming[] frameTimings, uint frameTimingsCount, out float cpuFrameTime, out float gpuFrameTime) { double cpuTime = 0.0f; double gpuTime = 0.0f; for (int i = 0; i < frameTimingsCount; ++i) { cpuTime += frameTimings[i].cpuFrameTime; gpuTime += frameTimings[i].gpuFrameTime; } cpuTime /= frameTimingsCount; gpuTime /= frameTimingsCount; cpuFrameTime = (float)(cpuTime * 0.001); gpuFrameTime = (float)(gpuTime * 0.001); } /// Gets the application memory usage. /// The application memory usage. private static ulong AppMemoryUsage { get { #if WINDOWS_UWP return MemoryManager.AppMemoryUsage; #else return (ulong)Profiler.GetTotalAllocatedMemoryLong(); #endif } } /// Gets the application memory usage limit. /// The application memory usage limit. private static ulong AppMemoryUsageLimit { get { #if WINDOWS_UWP return MemoryManager.AppMemoryUsageLimit; #else return ConvertMegabytesToBytes(SystemInfo.systemMemorySize); #endif } } /// Will displayed memory usage differ. /// The old usage. /// The new usage. /// The displayed decimal digits. /// True if it succeeds, false if it fails. private static bool WillDisplayedMemoryUsageDiffer(ulong oldUsage, ulong newUsage, int displayedDecimalDigits) { float oldUsageMBs = ConvertBytesToMegabytes(oldUsage); float newUsageMBs = ConvertBytesToMegabytes(newUsage); float decimalPower = Mathf.Pow(10.0f, displayedDecimalDigits); return (int)(oldUsageMBs * decimalPower) != (int)(newUsageMBs * decimalPower); } /// Convert megabytes to bytes. /// The megabytes. /// The megabytes converted to bytes. private static ulong ConvertMegabytesToBytes(int megabytes) { return ((ulong)megabytes * 1024UL) * 1024UL; } /// Convert bytes to megabytes. /// The bytes. /// The bytes converted to megabytes. private static float ConvertBytesToMegabytes(ulong bytes) { return (bytes / 1024.0f) / 1024.0f; } } }