// Adaptive Quality|Utilities|90100 // Adapted from The Lab Renderer's ValveCamera.cs, available at // https://github.com/ValveSoftware/the_lab_renderer/blob/ae64c48a8ccbe5406aba1e39b160d4f2f7156c2c/Assets/TheLabRenderer/Scripts/ValveCamera.cs // For The Lab Renderer's license see THIRD_PARTY_NOTICES. // **Only Compatible With Unity 5.4 and above** #if (UNITY_5_4_OR_NEWER) namespace VRTK { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using UnityEngine; #if UNITY_2017_2_OR_NEWER using UnityEngine.XR; #else using XRSettings = UnityEngine.VR.VRSettings; using XRDevice = UnityEngine.VR.VRDevice; #endif /// /// Adaptive Quality dynamically changes rendering settings to maintain VR framerate while maximizing GPU utilization. /// /// /// > **Only Compatible With Unity 5.4 and above** /// /// There are two goals: /// /// Reduce the chances of dropping frames and reprojecting /// Increase quality when there are idle GPU cycles /// /// /// This script currently changes the following to reach these goals: /// /// Rendering resolution and viewport size (aka Dynamic Resolution) /// /// /// In the future it could be changed to also change the following: /// /// MSAA level /// Fixed Foveated Rendering /// Radial Density Masking /// (Non-fixed) Foveated Rendering (once HMDs support eye tracking) /// /// /// Some shaders, especially Image Effects, need to be modified to work with the changed render scale. To fix them /// pass `1.0f / VRSettings.renderViewportScale` into the shader and scale all incoming UV values with it in the vertex /// program. Do this by using `Material.SetFloat` to set the value in the script that configures the shader. /// /// In more detail: /// /// In the `.shader` file: Add a new runtime-set property value `float _InverseOfRenderViewportScale` /// and add `vertexInput.texcoord *= _InverseOfRenderViewportScale` to the start of the vertex program /// In the `.cs` file: Before using the material (eg. `Graphics.Blit`) add /// `material.SetFloat("_InverseOfRenderViewportScale", 1.0f / VRSettings.renderViewportScale)` /// /// /// /// `VRTK/Examples/039_CameraRig_AdaptiveQuality` displays the frames per second in the centre of the headset view. /// The debug visualization of this script is displayed near the top edge of the headset view. /// Pressing the trigger generates a new sphere and pressing the touchpad generates ten new spheres. /// Eventually when lots of spheres are present the FPS will drop and demonstrate the script. /// [AddComponentMenu("VRTK/Scripts/Utilities/VRTK_AdaptiveQuality")] public sealed class VRTK_AdaptiveQuality : MonoBehaviour { #region Public fields [Tooltip("Toggles whether to show the debug overlay.\n\n" + "Each square represents a different level on the quality scale. Levels increase from left to right," + " the first green box that is lit above represents the recommended render target resolution provided by the" + " current `VRDevice`, the box that is lit below in cyan represents the current resolution and the filled box" + " represents the current viewport scale. The yellow boxes represent resolutions below the recommended render target resolution.\n" + "The currently lit box becomes red whenever the user is likely seeing reprojection in the HMD since the" + " application isn't maintaining VR framerate. If lit, the box all the way on the left is almost always lit" + " red because it represents the lowest render scale with reprojection on.")] public bool drawDebugVisualization; [Tooltip("Toggles whether to allow keyboard shortcuts to control this script.\n\n" + " * The supported shortcuts are:\n" + " * `Shift+F1`: Toggle debug visualization on/off\n" + " * `Shift+F2`: Toggle usage of override render scale on/off\n" + " * `Shift+F3`: Decrease override render scale level\n" + " * `Shift+F4`: Increase override render scale level")] public bool allowKeyboardShortcuts = true; [Tooltip("Toggles whether to allow command line arguments to control this script at startup of the standalone build.\n\n" + " * The supported command line arguments all begin with '-' and are:\n" + " * `-noaq`: Disable adaptive quality\n" + " * `-aqminscale X`: Set minimum render scale to X\n" + " * `-aqmaxscale X`: Set maximum render scale to X\n" + " * `-aqmaxres X`: Set maximum render target dimension to X\n" + " * `-aqfillratestep X`: Set render scale fill rate step size in percent to X (X from 1 to 100)\n" + " * `-aqoverride X`: Set override render scale level to X\n" + " * `-vrdebug`: Enable debug visualization\n" + " * `-msaa X`: Set MSAA level to X")] public bool allowCommandLineArguments = true; [Tooltip("The MSAA level to use.")] [Header("Quality")] [Range(0, 8)] public int msaaLevel = 4; [Tooltip("Toggles whether the render viewport scale is dynamically adjusted to maintain VR framerate.\n\n" + "If unchecked, the renderer will render at the recommended resolution provided by the current `VRDevice`.")] public bool scaleRenderViewport = true; [Tooltip("The minimum and maximum allowed render scale.")] [MinMaxRange(0.01f, 5f)] public Limits2D renderScaleLimits = new Limits2D(0.8f, 1.4f); [Obsolete("`VRTK_AdaptiveQuality.minimumRenderScale` has been replaced with the `VRTK_AdaptiveQuality.renderScaleLimits`. This parameter will be removed in a future version of VRTK.")] [ObsoleteInspector] public float minimumRenderScale = 0.8f; [Obsolete("`VRTK_AdaptiveQuality.maximumRenderScale` has been replaced with the `VRTK_AdaptiveQuality.renderScaleLimits`. This parameter will be removed in a future version of VRTK.")] [ObsoleteInspector] public float maximumRenderScale = 1.4f; [Tooltip("The maximum allowed render target dimension.\n\n" + "This puts an upper limit on the size of the render target regardless of the maximum render scale.")] public int maximumRenderTargetDimension = 4096; [Tooltip("The fill rate step size in percent by which the render scale levels will be calculated.")] [Range(1, 100)] public int renderScaleFillRateStepSizeInPercent = 15; [Tooltip("Toggles whether the render target resolution is dynamically adjusted to maintain VR framerate.\n\n" + "If unchecked, the renderer will use the maximum target resolution specified by `maximumRenderScale`.")] public bool scaleRenderTargetResolution = false; [Tooltip("Toggles whether to override the used render viewport scale level.")] [Header("Override")] [NonSerialized] public bool overrideRenderViewportScale; [Tooltip("The render viewport scale level to override the current one with.")] [NonSerialized] public int overrideRenderViewportScaleLevel; #endregion #region Public readonly fields & properties /// /// All the calculated render scales. /// /// /// The elements of this collection are to be interpreted as modifiers to the recommended render target /// resolution provided by the current `VRDevice`. /// public readonly ReadOnlyCollection renderScales; /// /// The current render scale. /// /// /// A render scale of `1.0` represents the recommended render target resolution provided by the current `VRDevice`. /// public static float CurrentRenderScale { get { return VRTK_SharedMethods.GetEyeTextureResolutionScale() * XRSettings.renderViewportScale; } } /// /// The recommended render target resolution provided by the current `VRDevice`. /// public Vector2 defaultRenderTargetResolution { get { return RenderTargetResolutionForRenderScale(allRenderScales[defaultRenderViewportScaleLevel]); } } /// /// The current render target resolution. /// public Vector2 currentRenderTargetResolution { get { return RenderTargetResolutionForRenderScale(CurrentRenderScale); } } #endregion #region Private fields /// /// The frame duration in milliseconds to fallback to if the current `VRDevice` specifies no refresh rate. /// private const float DefaultFrameDurationInMilliseconds = 1000f / 90f; private readonly AdaptiveSetting renderViewportScaleSetting = new AdaptiveSetting(0, 30, 10); private readonly AdaptiveSetting renderScaleSetting = new AdaptiveSetting(0, 180, 90); private readonly List allRenderScales = new List(); private int defaultRenderViewportScaleLevel; private float previousMinimumRenderScale; private float previousMaximumRenderScale; private float previousRenderScaleFillRateStepSizeInPercent; private readonly Timing timing = new Timing(); private int lastRenderViewportScaleLevelBelowRenderScaleLevelFrameCount; private bool interleavedReprojectionEnabled; private bool hmdDisplayIsOnDesktop; private float singleFrameDurationInMilliseconds; private GameObject debugVisualizationQuad; private Material debugVisualizationQuadMaterial; #endregion public VRTK_AdaptiveQuality() { renderScales = allRenderScales.AsReadOnly(); } /// /// Calculates and returns the render target resolution for a given render scale. /// /// /// The render scale to calculate the render target resolution with. /// /// /// The render target resolution for `renderScale`. /// public static Vector2 RenderTargetResolutionForRenderScale(float renderScale) { return new Vector2((int)(XRSettings.eyeTextureWidth / VRTK_SharedMethods.GetEyeTextureResolutionScale() * renderScale), (int)(XRSettings.eyeTextureHeight / VRTK_SharedMethods.GetEyeTextureResolutionScale() * renderScale)); } /// /// Calculates and returns the biggest allowed maximum render scale to be used for `maximumRenderScale` /// given the current `maximumRenderTargetDimension`. /// /// /// The biggest allowed maximum render scale. /// public float BiggestAllowedMaximumRenderScale() { if (XRSettings.eyeTextureWidth == 0 || XRSettings.eyeTextureHeight == 0) { return renderScaleLimits.maximum; } float maximumHorizontalRenderScale = maximumRenderTargetDimension * VRTK_SharedMethods.GetEyeTextureResolutionScale() / XRSettings.eyeTextureWidth; float maximumVerticalRenderScale = maximumRenderTargetDimension * VRTK_SharedMethods.GetEyeTextureResolutionScale() / XRSettings.eyeTextureHeight; return Mathf.Min(maximumHorizontalRenderScale, maximumVerticalRenderScale); } /// /// A summary of this script by listing all the calculated render scales with their /// corresponding render target resolution. /// /// /// The summary. /// public override string ToString() { StringBuilder stringBuilder = new StringBuilder("Adaptive Quality\n"); stringBuilder.AppendLine("Render Scale:"); stringBuilder.AppendLine("Level - Resolution - Multiplier"); for (int index = 0; index < allRenderScales.Count; index++) { float renderScale = allRenderScales[index]; Vector2 renderTargetResolution = RenderTargetResolutionForRenderScale(renderScale); stringBuilder.AppendFormat( "{0, 3} {1, 5}x{2, -5} {3, -8}", index, (int)renderTargetResolution.x, (int)renderTargetResolution.y, renderScale); if (index == 0) { stringBuilder.Append(" (Interleaved reprojection hint)"); } else if (index == defaultRenderViewportScaleLevel) { stringBuilder.Append(" (Default)"); } if (index == renderViewportScaleSetting.currentValue) { stringBuilder.Append(" (Current Viewport)"); } if (index == renderScaleSetting.currentValue) { stringBuilder.Append(" (Current Target Resolution)"); } if (index != allRenderScales.Count - 1) { stringBuilder.AppendLine(); } } return stringBuilder.ToString(); } #region MonoBehaviour methods private void Awake() { VRTK_SDKManager.AttemptAddBehaviourToToggleOnLoadedSetupChange(this); } private void OnEnable() { Camera.onPreCull += OnCameraPreCull; hmdDisplayIsOnDesktop = VRTK_SDK_Bridge.IsDisplayOnDesktop(); singleFrameDurationInMilliseconds = XRDevice.refreshRate > 0.0f ? 1000.0f / XRDevice.refreshRate : DefaultFrameDurationInMilliseconds; HandleCommandLineArguments(); if (!Application.isEditor) { OnValidate(); } } private void OnDisable() { Camera.onPreCull -= OnCameraPreCull; SetRenderScale(1.0f, 1.0f); } private void OnDestroy() { VRTK_SDKManager.AttemptRemoveBehaviourToToggleOnLoadedSetupChange(this); } private void OnValidate() { renderScaleLimits.minimum = Mathf.Max(0.01f, renderScaleLimits.minimum); renderScaleLimits.maximum = Mathf.Max(renderScaleLimits.minimum, renderScaleLimits.maximum); maximumRenderTargetDimension = Mathf.Max(2, maximumRenderTargetDimension); renderScaleFillRateStepSizeInPercent = Mathf.Max(1, renderScaleFillRateStepSizeInPercent); msaaLevel = msaaLevel == 1 ? 0 : Mathf.Clamp(Mathf.ClosestPowerOfTwo(msaaLevel), 0, 8); } private void Update() { HandleKeyPresses(); UpdateRenderScaleLevels(); CreateOrDestroyDebugVisualization(); UpdateDebugVisualization(); timing.SaveCurrentFrameTiming(); } #if UNITY_5_4_1 || UNITY_5_4_2 || UNITY_5_4_3 || UNITY_5_5_OR_NEWER private void LateUpdate() { UpdateRenderScale(); } #endif private void OnCameraPreCull(Camera camera) { if (camera.transform != VRTK_SDK_Bridge.GetHeadsetCamera()) { return; } #if !(UNITY_5_4_1 || UNITY_5_4_2 || UNITY_5_4_3 || UNITY_5_5_OR_NEWER) UpdateRenderScale(); #endif UpdateMSAALevel(); } #endregion private void HandleCommandLineArguments() { if (!allowCommandLineArguments) { return; } string[] commandLineArguments = VRTK_SharedMethods.GetCommandLineArguements(); for (int index = 0; index < commandLineArguments.Length; index++) { string argument = commandLineArguments[index]; string nextArgument = index + 1 < commandLineArguments.Length ? commandLineArguments[index + 1] : ""; switch (argument) { case CommandLineArguments.Disable: scaleRenderViewport = false; break; case CommandLineArguments.MinimumRenderScale: renderScaleLimits.minimum = float.Parse(nextArgument); break; case CommandLineArguments.MaximumRenderScale: renderScaleLimits.maximum = float.Parse(nextArgument); break; case CommandLineArguments.MaximumRenderTargetDimension: maximumRenderTargetDimension = int.Parse(nextArgument); break; case CommandLineArguments.RenderScaleFillRateStepSizeInPercent: renderScaleFillRateStepSizeInPercent = int.Parse(nextArgument); break; case CommandLineArguments.OverrideRenderScaleLevel: overrideRenderViewportScale = true; overrideRenderViewportScaleLevel = int.Parse(nextArgument); break; case CommandLineArguments.DrawDebugVisualization: drawDebugVisualization = true; break; case CommandLineArguments.MSAALevel: msaaLevel = int.Parse(nextArgument); break; } } } private void HandleKeyPresses() { if (!allowKeyboardShortcuts || !KeyboardShortcuts.Modifiers.Any(Input.GetKey)) { return; } if (Input.GetKeyDown(KeyboardShortcuts.ToggleDrawDebugVisualization)) { drawDebugVisualization = !drawDebugVisualization; } else if (Input.GetKeyDown(KeyboardShortcuts.ToggleOverrideRenderScale)) { overrideRenderViewportScale = !overrideRenderViewportScale; } else if (Input.GetKeyDown(KeyboardShortcuts.DecreaseOverrideRenderScaleLevel)) { overrideRenderViewportScaleLevel = ClampRenderScaleLevel(overrideRenderViewportScaleLevel - 1); } else if (Input.GetKeyDown(KeyboardShortcuts.IncreaseOverrideRenderScaleLevel)) { overrideRenderViewportScaleLevel = ClampRenderScaleLevel(overrideRenderViewportScaleLevel + 1); } } private void UpdateMSAALevel() { if (QualitySettings.antiAliasing != msaaLevel) { QualitySettings.antiAliasing = msaaLevel; } } #region Render scale methods private void UpdateRenderScaleLevels() { if (Mathf.Abs(previousMinimumRenderScale - renderScaleLimits.minimum) <= float.Epsilon && Mathf.Abs(previousMaximumRenderScale - renderScaleLimits.maximum) <= float.Epsilon && Mathf.Abs(previousRenderScaleFillRateStepSizeInPercent - renderScaleFillRateStepSizeInPercent) <= float.Epsilon) { return; } previousMinimumRenderScale = renderScaleLimits.minimum; previousMaximumRenderScale = renderScaleLimits.maximum; previousRenderScaleFillRateStepSizeInPercent = renderScaleFillRateStepSizeInPercent; allRenderScales.Clear(); // Respect maximumRenderTargetDimension float allowedMaximumRenderScale = BiggestAllowedMaximumRenderScale(); float renderScaleToAdd = Mathf.Min(renderScaleLimits.minimum, allowedMaximumRenderScale); // Add min scale as the reprojection scale allRenderScales.Add(renderScaleToAdd); // Add all entries while (renderScaleToAdd <= renderScaleLimits.maximum) { allRenderScales.Add(renderScaleToAdd); renderScaleToAdd = Mathf.Sqrt((renderScaleFillRateStepSizeInPercent + 100) / 100.0f * renderScaleToAdd * renderScaleToAdd); if (renderScaleToAdd > allowedMaximumRenderScale) { // Too large break; } } // Figure out default render viewport scale level for debug visualization defaultRenderViewportScaleLevel = Mathf.Clamp( allRenderScales.FindIndex(renderScale => renderScale >= 1.0f), 1, allRenderScales.Count - 1); renderViewportScaleSetting.currentValue = defaultRenderViewportScaleLevel; renderScaleSetting.currentValue = defaultRenderViewportScaleLevel; overrideRenderViewportScaleLevel = ClampRenderScaleLevel(overrideRenderViewportScaleLevel); } private void UpdateRenderScale() { if (allRenderScales.Count == 0) { return; } if (!scaleRenderViewport) { renderViewportScaleSetting.currentValue = defaultRenderViewportScaleLevel; renderScaleSetting.currentValue = defaultRenderViewportScaleLevel; SetRenderScale(1.0f, 1.0f); return; } // Rendering in low resolution means adaptive quality needs to scale back the render scale target to free up gpu cycles bool renderInLowResolution = VRTK_SDK_Bridge.ShouldAppRenderWithLowResources(); // Thresholds float allowedSingleFrameDurationInMilliseconds = renderInLowResolution ? singleFrameDurationInMilliseconds * 0.75f : singleFrameDurationInMilliseconds; float lowThresholdInMilliseconds = 0.7f * allowedSingleFrameDurationInMilliseconds; float extrapolationThresholdInMilliseconds = 0.85f * allowedSingleFrameDurationInMilliseconds; float highThresholdInMilliseconds = 0.9f * allowedSingleFrameDurationInMilliseconds; int newRenderViewportScaleLevel = renderViewportScaleSetting.currentValue; // Rapidly reduce render viewport scale level if cost of last 1 or 3 frames, or the predicted next frame's cost are expensive if (timing.WasFrameTimingBad( 1, highThresholdInMilliseconds, renderViewportScaleSetting.lastChangeFrameCount, renderViewportScaleSetting.decreaseFrameCost) || timing.WasFrameTimingBad( 3, highThresholdInMilliseconds, renderViewportScaleSetting.lastChangeFrameCount, renderViewportScaleSetting.decreaseFrameCost) || timing.WillFrameTimingBeBad( extrapolationThresholdInMilliseconds, highThresholdInMilliseconds, renderViewportScaleSetting.lastChangeFrameCount, renderViewportScaleSetting.decreaseFrameCost)) { // Always drop 2 levels except when dropping from level 2 (level 0 is for reprojection) newRenderViewportScaleLevel = ClampRenderScaleLevel(renderViewportScaleSetting.currentValue == 2 ? 1 : renderViewportScaleSetting.currentValue - 2); } // Rapidly increase render viewport scale level if last 12 frames are cheap else if (timing.WasFrameTimingGood( 12, lowThresholdInMilliseconds, renderViewportScaleSetting.lastChangeFrameCount - renderViewportScaleSetting.increaseFrameCost, renderViewportScaleSetting.increaseFrameCost)) { newRenderViewportScaleLevel = ClampRenderScaleLevel(renderViewportScaleSetting.currentValue + 2); } // Slowly increase render viewport scale level if last 6 frames are cheap else if (timing.WasFrameTimingGood( 6, lowThresholdInMilliseconds, renderViewportScaleSetting.lastChangeFrameCount, renderViewportScaleSetting.increaseFrameCost)) { // Only increase by 1 level to prevent frame drops caused by adjusting newRenderViewportScaleLevel = ClampRenderScaleLevel(renderViewportScaleSetting.currentValue + 1); } // Apply and remember when render viewport scale level changed if (newRenderViewportScaleLevel != renderViewportScaleSetting.currentValue) { if (renderViewportScaleSetting.currentValue >= renderScaleSetting.currentValue && newRenderViewportScaleLevel < renderScaleSetting.currentValue) { lastRenderViewportScaleLevelBelowRenderScaleLevelFrameCount = Time.frameCount; } renderViewportScaleSetting.currentValue = newRenderViewportScaleLevel; } // Ignore frame timings if overriding if (overrideRenderViewportScale) { renderViewportScaleSetting.currentValue = overrideRenderViewportScaleLevel; } // Force on interleaved reprojection for level 0 which is just a replica of level 1 with reprojection enabled float additionalViewportScale = 1.0f; if (!hmdDisplayIsOnDesktop) { if (renderViewportScaleSetting.currentValue == 0) { if (interleavedReprojectionEnabled && timing.GetFrameTiming(0) < singleFrameDurationInMilliseconds * 0.85f) { interleavedReprojectionEnabled = false; } else if (timing.GetFrameTiming(0) > singleFrameDurationInMilliseconds * 0.925f) { interleavedReprojectionEnabled = true; } } else { interleavedReprojectionEnabled = false; } VRTK_SDK_Bridge.ForceInterleavedReprojectionOn(interleavedReprojectionEnabled); } // Not running in direct mode! Interleaved reprojection not supported, so scale down the viewport some more else if (renderViewportScaleSetting.currentValue == 0) { additionalViewportScale = 0.8f; } int newRenderScaleLevel = renderScaleSetting.currentValue; int levelInBetween = (renderViewportScaleSetting.currentValue - renderScaleSetting.currentValue) / 2; // Increase render scale level to the level in between if (renderScaleSetting.currentValue < renderViewportScaleSetting.currentValue && Time.frameCount >= renderScaleSetting.lastChangeFrameCount + renderScaleSetting.increaseFrameCost) { newRenderScaleLevel = ClampRenderScaleLevel(renderScaleSetting.currentValue + Mathf.Max(1, levelInBetween)); } // Decrease render scale level else if (renderScaleSetting.currentValue > renderViewportScaleSetting.currentValue && Time.frameCount >= renderScaleSetting.lastChangeFrameCount + renderScaleSetting.decreaseFrameCost && Time.frameCount >= lastRenderViewportScaleLevelBelowRenderScaleLevelFrameCount + renderViewportScaleSetting.increaseFrameCost) { // Slowly decrease render scale level to level in between if last 6 frames are cheap, otherwise rapidly newRenderScaleLevel = timing.WasFrameTimingGood(6, lowThresholdInMilliseconds, 0, 0) ? ClampRenderScaleLevel(renderScaleSetting.currentValue + Mathf.Min(-1, levelInBetween)) : renderViewportScaleSetting.currentValue; } // Apply and remember when render scale level changed renderScaleSetting.currentValue = newRenderScaleLevel; if (!scaleRenderTargetResolution) { renderScaleSetting.currentValue = allRenderScales.Count - 1; } float newRenderScale = allRenderScales[renderScaleSetting.currentValue]; float newRenderViewportScale = allRenderScales[Mathf.Min(renderViewportScaleSetting.currentValue, renderScaleSetting.currentValue)] / newRenderScale * additionalViewportScale; SetRenderScale(newRenderScale, newRenderViewportScale); } private static void SetRenderScale(float renderScale, float renderViewportScale) { if (Mathf.Abs(VRTK_SharedMethods.GetEyeTextureResolutionScale() - renderScale) > float.Epsilon) { VRTK_SharedMethods.SetEyeTextureResolutionScale(renderScale); } if (Mathf.Abs(XRSettings.renderViewportScale - renderViewportScale) > float.Epsilon) { XRSettings.renderViewportScale = renderViewportScale; } } private int ClampRenderScaleLevel(int renderScaleLevel) { return Mathf.Clamp(renderScaleLevel, 0, allRenderScales.Count - 1); } #endregion #region Debug visualization methods private void CreateOrDestroyDebugVisualization() { if (!Application.isPlaying) { return; } if (enabled && drawDebugVisualization && debugVisualizationQuad == null) { Mesh mesh = new Mesh { vertices = new[] { new Vector3(-0.5f, 0.9f, 1.0f), new Vector3(-0.5f, 1.0f, 1.0f), new Vector3(0.5f, 1.0f, 1.0f), new Vector3(0.5f, 0.9f, 1.0f) }, uv = new[] { new Vector2(0.0f, 0.0f), new Vector2(0.0f, 1.0f), new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0.0f) }, triangles = new[] { 0, 1, 2, 0, 2, 3 } }; #if !UNITY_5_5_OR_NEWER mesh.Optimize(); #endif mesh.UploadMeshData(true); debugVisualizationQuad = new GameObject(VRTK_SharedMethods.GenerateVRTKObjectName(true, "AdaptiveQualityDebugVisualizationQuad")); debugVisualizationQuad.transform.parent = VRTK_DeviceFinder.HeadsetTransform(); debugVisualizationQuad.transform.localPosition = Vector3.forward; debugVisualizationQuad.transform.localRotation = Quaternion.identity; debugVisualizationQuad.AddComponent().mesh = mesh; debugVisualizationQuadMaterial = Resources.Load("AdaptiveQualityDebugVisualization"); debugVisualizationQuad.AddComponent().material = debugVisualizationQuadMaterial; } else if ((!enabled || !drawDebugVisualization) && debugVisualizationQuad != null) { Destroy(debugVisualizationQuad); debugVisualizationQuad = null; debugVisualizationQuadMaterial = null; } } private void UpdateDebugVisualization() { if (!drawDebugVisualization || debugVisualizationQuadMaterial == null) { return; } int lastFrameIsInBudget = (interleavedReprojectionEnabled || VRTK_SharedMethods.GetGPUTimeLastFrame() > singleFrameDurationInMilliseconds ? 0 : 1); debugVisualizationQuadMaterial.SetInt(ShaderPropertyIDs.RenderScaleLevelsCount, allRenderScales.Count); debugVisualizationQuadMaterial.SetInt(ShaderPropertyIDs.DefaultRenderViewportScaleLevel, defaultRenderViewportScaleLevel); debugVisualizationQuadMaterial.SetInt(ShaderPropertyIDs.CurrentRenderViewportScaleLevel, renderViewportScaleSetting.currentValue); debugVisualizationQuadMaterial.SetInt(ShaderPropertyIDs.CurrentRenderScaleLevel, renderScaleSetting.currentValue); debugVisualizationQuadMaterial.SetInt(ShaderPropertyIDs.LastFrameIsInBudget, lastFrameIsInBudget); } #endregion #region Private helper classes private sealed class AdaptiveSetting { public T currentValue { get { return _currentValue; } set { if (!EqualityComparer.Default.Equals(value, _currentValue)) { lastChangeFrameCount = Time.frameCount; } previousValue = _currentValue; _currentValue = value; } } public T previousValue { get; private set; } public int lastChangeFrameCount { get; private set; } public readonly int increaseFrameCost; public readonly int decreaseFrameCost; private T _currentValue; public AdaptiveSetting(T currentValue, int increaseFrameCost, int decreaseFrameCost) { previousValue = currentValue; this.currentValue = currentValue; this.increaseFrameCost = increaseFrameCost; this.decreaseFrameCost = decreaseFrameCost; } } private static class CommandLineArguments { public const string Disable = "-noaq"; public const string MinimumRenderScale = "-aqminscale"; public const string MaximumRenderScale = "-aqmaxscale"; public const string MaximumRenderTargetDimension = "-aqmaxres"; public const string RenderScaleFillRateStepSizeInPercent = "-aqfillratestep"; public const string OverrideRenderScaleLevel = "-aqoverride"; public const string DrawDebugVisualization = "-vrdebug"; public const string MSAALevel = "-msaa"; } private static class KeyboardShortcuts { public static readonly KeyCode[] Modifiers = { KeyCode.LeftShift, KeyCode.RightShift }; public const KeyCode ToggleDrawDebugVisualization = KeyCode.F1; public const KeyCode ToggleOverrideRenderScale = KeyCode.F2; public const KeyCode DecreaseOverrideRenderScaleLevel = KeyCode.F3; public const KeyCode IncreaseOverrideRenderScaleLevel = KeyCode.F4; } private static class ShaderPropertyIDs { public static readonly int RenderScaleLevelsCount = Shader.PropertyToID("_RenderScaleLevelsCount"); public static readonly int DefaultRenderViewportScaleLevel = Shader.PropertyToID("_DefaultRenderViewportScaleLevel"); public static readonly int CurrentRenderViewportScaleLevel = Shader.PropertyToID("_CurrentRenderViewportScaleLevel"); public static readonly int CurrentRenderScaleLevel = Shader.PropertyToID("_CurrentRenderScaleLevel"); public static readonly int LastFrameIsInBudget = Shader.PropertyToID("_LastFrameIsInBudget"); } private sealed class Timing { private readonly float[] buffer = new float[12]; private int bufferIndex; public void SaveCurrentFrameTiming() { bufferIndex = (bufferIndex + 1) % buffer.Length; buffer[bufferIndex] = VRTK_SharedMethods.GetGPUTimeLastFrame(); } public float GetFrameTiming(int framesAgo) { return buffer[(bufferIndex - framesAgo + buffer.Length) % buffer.Length]; } public bool WasFrameTimingBad(int framesAgo, float thresholdInMilliseconds, int lastChangeFrameCount, int changeFrameCost) { if (!AreFramesAvailable(framesAgo, lastChangeFrameCount, changeFrameCost)) { // Too early to know return false; } for (int frame = 0; frame < framesAgo; frame++) { if (GetFrameTiming(frame) <= thresholdInMilliseconds) { return false; } } return true; } public bool WasFrameTimingGood(int framesAgo, float thresholdInMilliseconds, int lastChangeFrameCount, int changeFrameCost) { if (!AreFramesAvailable(framesAgo, lastChangeFrameCount, changeFrameCost)) { // Too early to know return false; } for (int frame = 0; frame < framesAgo; frame++) { if (GetFrameTiming(frame) > thresholdInMilliseconds) { return false; } } return true; } public bool WillFrameTimingBeBad(float extrapolationThresholdInMilliseconds, float thresholdInMilliseconds, int lastChangeFrameCount, int changeFrameCost) { if (!AreFramesAvailable(2, lastChangeFrameCount, changeFrameCost)) { // Too early to know return false; } // Predict next frame's cost using linear extrapolation: max(frame-1 to frame+1, frame-2 to frame+1) float frameMinus0Timing = GetFrameTiming(0); if (frameMinus0Timing <= extrapolationThresholdInMilliseconds) { return false; } float delta = frameMinus0Timing - GetFrameTiming(1); if (!AreFramesAvailable(3, lastChangeFrameCount, changeFrameCost)) { delta = Mathf.Max(delta, (frameMinus0Timing - GetFrameTiming(2)) / 2f); } return frameMinus0Timing + delta > thresholdInMilliseconds; } private static bool AreFramesAvailable(int framesAgo, int lastChangeFrameCount, int changeFrameCost) { return Time.frameCount >= framesAgo + lastChangeFrameCount + changeFrameCost; } } #endregion } } #endif