// 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

    /// <summary>
    /// Adaptive Quality dynamically changes rendering settings to maintain VR framerate while maximizing GPU utilization.
    /// </summary>
    /// <remarks>
    ///   > **Only Compatible With Unity 5.4 and above**
    ///
    /// There are two goals:
    /// <list type="bullet">
    /// <item> <description>Reduce the chances of dropping frames and reprojecting</description> </item>
    /// <item> <description>Increase quality when there are idle GPU cycles</description> </item>
    /// </list>
    /// <para />
    /// This script currently changes the following to reach these goals:
    /// <list type="bullet">
    /// <item> <description>Rendering resolution and viewport size (aka Dynamic Resolution)</description> </item>
    /// </list>
    /// <para />
    /// In the future it could be changed to also change the following:
    /// <list type="bullet">
    /// <item> <description>MSAA level</description> </item>
    /// <item> <description>Fixed Foveated Rendering</description> </item>
    /// <item> <description>Radial Density Masking</description> </item>
    /// <item> <description>(Non-fixed) Foveated Rendering (once HMDs support eye tracking)</description> </item>
    /// </list>
    /// <para />
    /// 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.
    /// <para />
    /// In more detail:
    /// <list type="bullet">
    /// <item> <description>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</description> </item>
    /// <item> <description>In the `.cs` file: Before using the material (eg. `Graphics.Blit`) add
    /// `material.SetFloat("_InverseOfRenderViewportScale", 1.0f / VRSettings.renderViewportScale)`</description> </item>
    /// </list>
    /// </remarks>
    /// <example>
    /// `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.
    /// </example>
    [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

        /// <summary>
        /// All the calculated render scales.
        /// </summary>
        /// <remarks>
        /// The elements of this collection are to be interpreted as modifiers to the recommended render target
        /// resolution provided by the current `VRDevice`.
        /// </remarks>
        public readonly ReadOnlyCollection<float> renderScales;

        /// <summary>
        /// The current render scale.
        /// </summary>
        /// <remarks>
        /// A render scale of `1.0` represents the recommended render target resolution provided by the current `VRDevice`.
        /// </remarks>
        public static float CurrentRenderScale
        {
            get { return VRTK_SharedMethods.GetEyeTextureResolutionScale() * XRSettings.renderViewportScale; }
        }

        /// <summary>
        /// The recommended render target resolution provided by the current `VRDevice`.
        /// </summary>
        public Vector2 defaultRenderTargetResolution
        {
            get { return RenderTargetResolutionForRenderScale(allRenderScales[defaultRenderViewportScaleLevel]); }
        }

        /// <summary>
        /// The current render target resolution.
        /// </summary>
        public Vector2 currentRenderTargetResolution
        {
            get { return RenderTargetResolutionForRenderScale(CurrentRenderScale); }
        }

        #endregion

        #region Private fields

        /// <summary>
        /// The frame duration in milliseconds to fallback to if the current `VRDevice` specifies no refresh rate.
        /// </summary>
        private const float DefaultFrameDurationInMilliseconds = 1000f / 90f;

        private readonly AdaptiveSetting<int> renderViewportScaleSetting = new AdaptiveSetting<int>(0, 30, 10);
        private readonly AdaptiveSetting<int> renderScaleSetting = new AdaptiveSetting<int>(0, 180, 90);

        private readonly List<float> allRenderScales = new List<float>();
        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();
        }

        /// <summary>
        /// Calculates and returns the render target resolution for a given render scale.
        /// </summary>
        /// <param name="renderScale">
        /// The render scale to calculate the render target resolution with.
        /// </param>
        /// <returns>
        /// The render target resolution for `renderScale`.
        /// </returns>
        public static Vector2 RenderTargetResolutionForRenderScale(float renderScale)
        {
            return new Vector2((int)(XRSettings.eyeTextureWidth / VRTK_SharedMethods.GetEyeTextureResolutionScale() * renderScale),
                               (int)(XRSettings.eyeTextureHeight / VRTK_SharedMethods.GetEyeTextureResolutionScale() * renderScale));
        }

        /// <summary>
        /// Calculates and returns the biggest allowed maximum render scale to be used for `maximumRenderScale`
        /// given the current `maximumRenderTargetDimension`.
        /// </summary>
        /// <returns>
        /// The biggest allowed maximum render scale.
        /// </returns>
        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);
        }

        /// <summary>
        /// A summary of this script by listing all the calculated render scales with their
        /// corresponding render target resolution.
        /// </summary>
        /// <returns>
        /// The summary.
        /// </returns>
        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<MeshFilter>().mesh = mesh;

                debugVisualizationQuadMaterial = Resources.Load<Material>("AdaptiveQualityDebugVisualization");
                debugVisualizationQuad.AddComponent<MeshRenderer>().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<T>
        {
            public T currentValue
            {
                get { return _currentValue; }
                set
                {
                    if (!EqualityComparer<T>.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
