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