using System.Collections.Generic; using UnityEngine; using UnityEngine.Profiling; namespace UnityEngine.AdaptivePerformance { /// /// Describes what action is needed to stabilize. /// public enum StateAction { /// /// No action is required. /// Stale, /// /// Recommended to increase quality. /// Increase, /// /// Quality must be decreased. /// Decrease, /// /// Quality must be decreased as soon as possible. /// FastDecrease, } /// /// System used for tracking device thermal state. /// internal class ThermalStateTracker { private float warningTemp = 1.0f; private float throttlingTemp = 1.0f; public ThermalStateTracker() { } public StateAction Update() { float thermalTrend = Holder.Instance.ThermalStatus.ThermalMetrics.TemperatureTrend; var thermalLevel = Holder.Instance.ThermalStatus.ThermalMetrics.TemperatureLevel; var warning = Holder.Instance.ThermalStatus.ThermalMetrics.WarningLevel; if (warning == WarningLevel.ThrottlingImminent && warningTemp == 1.0f) warningTemp = thermalLevel; // remember throttling imminent level if (warning == WarningLevel.Throttling && throttlingTemp == 1.0f) throttlingTemp = thermalLevel; // remember throttling level // Throttling needs to cool down a lot before changing to no warning if (warning == WarningLevel.Throttling || thermalLevel >= throttlingTemp) return StateAction.FastDecrease; // warm device if (warning == WarningLevel.ThrottlingImminent || thermalLevel >= warningTemp) { // halfway to throttling? if (thermalLevel > (warningTemp + throttlingTemp) / 2) return StateAction.Decrease; if (thermalTrend <= 0) return StateAction.Stale; else if (thermalTrend > 0.5) return StateAction.FastDecrease; else return StateAction.Decrease; } // normal operating conditions if (warning == WarningLevel.NoWarning && thermalLevel < warningTemp) { if (thermalTrend <= 0) return StateAction.Increase; else if (thermalTrend > 0.5) return StateAction.FastDecrease; else if (thermalTrend > 0.1) return StateAction.Decrease; } return StateAction.Stale; } } /// /// System used for tracking device performance state. /// internal class PerformanceStateTracker { private Queue m_Samples; private int m_SampleCapacity; public float Trend { get; set; } public PerformanceStateTracker(int sampleCapacity) { m_Samples = new Queue(sampleCapacity); m_SampleCapacity = sampleCapacity; } public StateAction Update() { var frameMs = Holder.Instance.PerformanceStatus.FrameTiming.AverageFrameTime; if (frameMs > 0) { var targetMs = 1f / AdaptivePerformanceManager.EffectiveTargetFrameRate(); var diffMs = (frameMs / targetMs) - 1; m_Samples.Enqueue(diffMs); if (m_Samples.Count > m_SampleCapacity) m_Samples.Dequeue(); } var trend = 0.0f; foreach (var sample in m_Samples) trend += sample; trend /= m_Samples.Count; Trend = trend; // It is underperforming heavily, we need to increase performance if (Trend >= 0.30) return StateAction.FastDecrease; // It is underperforming, we need to increase performance if (Trend >= 0.15) return StateAction.Decrease; // TODO: we need to way identify overperforming as currently AverageFrameTime is returned with vsync // return StaterAction.Increase; return StateAction.Stale; } } /// /// System used for tracking impact of scaler on CPU and GPU counters. /// internal class AdaptivePerformanceScalerEfficiencyTracker { private AdaptivePerformanceScaler m_Scaler; private float m_LastAverageGpuFrameTime; private float m_LastAverageCpuFrameTime; private bool m_IsApplied; public bool IsRunning { get => m_Scaler != null; } public void Start(AdaptivePerformanceScaler scaler, bool isApply) { Debug.Assert(!IsRunning, "AdaptivePerformanceScalerEfficiencyTracker is already running"); m_Scaler = scaler; m_LastAverageGpuFrameTime = Holder.Instance.PerformanceStatus.FrameTiming.AverageGpuFrameTime; m_LastAverageCpuFrameTime = Holder.Instance.PerformanceStatus.FrameTiming.AverageCpuFrameTime; m_IsApplied = true; } public void Stop() { var gpu = Holder.Instance.PerformanceStatus.FrameTiming.AverageGpuFrameTime - m_LastAverageGpuFrameTime; var cpu = Holder.Instance.PerformanceStatus.FrameTiming.AverageCpuFrameTime - m_LastAverageCpuFrameTime; var sign = m_IsApplied ? 1 : -1; m_Scaler.GpuImpact = sign * (int)(gpu * 1000); m_Scaler.CpuImpact = sign * (int)(cpu * 1000); m_Scaler = null; } } /// /// Higher level implementation of Adaptive performance that tracks performance and thermal states of the device and provides them to which use the information to increase or decrease performance levels. /// System acts as manager and handles the lifetime of the scalers in the scenes. /// public class AdaptivePerformanceIndexer { private List m_UnappliedScalers; private List m_AppliedScalers; private List m_DisabledScalers; private ThermalStateTracker m_ThermalStateTracker; private PerformanceStateTracker m_PerformanceStateTracker; private AdaptivePerformanceScalerEfficiencyTracker m_ScalerEfficiencyTracker; private IAdaptivePerformanceSettings m_Settings; const string m_FeatureName = "Indexer"; /// /// Time left until next action. /// public float TimeUntilNextAction { get; private set; } /// /// Current determined action needed from thermal state. /// Action will be ignored if is decreasing. /// public StateAction ThermalAction { get; private set; } /// /// Current determined action needed from performance state. /// Action will be ignored if is decreasing. /// public StateAction PerformanceAction { get; private set; } /// /// Returns all currently applied scalers. /// /// Output where scalers will be written. public void GetAppliedScalers(ref List scalers) { scalers.Clear(); scalers.AddRange(m_AppliedScalers); } /// /// Returns all currently unapplied scalers. /// /// Output where scalers will be written. public void GetUnappliedScalers(ref List scalers) { scalers.Clear(); scalers.AddRange(m_UnappliedScalers); } /// /// Returns all currently disabled scalers. /// /// Output where scalers will be written. public void GetDisabledScalers(ref List scalers) { scalers.Clear(); scalers.AddRange(m_DisabledScalers); } /// /// Returns all scalers independent of their state. /// /// Output where scalers will be written. public void GetAllRegisteredScalers(ref List scalers) { scalers.Clear(); scalers.AddRange(m_DisabledScalers); scalers.AddRange(m_UnappliedScalers); scalers.AddRange(m_AppliedScalers); } /// /// Unapply all currently active scalers. /// public void UnapplyAllScalers() { TimeUntilNextAction = m_Settings.indexerSettings.thermalActionDelay; while (m_AppliedScalers.Count != 0) { var scaler = m_AppliedScalers[0]; UnapplyScaler(scaler); } } internal void UpdateOverrideLevel(AdaptivePerformanceScaler scaler) { if (scaler.OverrideLevel == -1) return; while (scaler.OverrideLevel > scaler.CurrentLevel) ApplyScaler(scaler); while (scaler.OverrideLevel < scaler.CurrentLevel) UnapplyScaler(scaler); } internal void AddScaler(AdaptivePerformanceScaler scaler) { if (m_UnappliedScalers.Contains(scaler) || m_AppliedScalers.Contains(scaler)) return; m_UnappliedScalers.Add(scaler); } internal void RemoveScaler(AdaptivePerformanceScaler scaler) { if (m_UnappliedScalers.Contains(scaler)) { m_UnappliedScalers.Remove(scaler); return; } if (m_AppliedScalers.Contains(scaler)) { while (!scaler.NotLeveled) scaler.DecreaseLevel(); m_AppliedScalers.Remove(scaler); } } internal AdaptivePerformanceIndexer(ref IAdaptivePerformanceSettings settings) { m_Settings = settings; TimeUntilNextAction = m_Settings.indexerSettings.thermalActionDelay; m_ThermalStateTracker = new ThermalStateTracker(); m_PerformanceStateTracker = new PerformanceStateTracker(120); m_UnappliedScalers = new List(); m_AppliedScalers = new List(); m_DisabledScalers = new List(); m_ScalerEfficiencyTracker = new AdaptivePerformanceScalerEfficiencyTracker(); AdaptivePerformanceAnalytics.RegisterFeature(m_FeatureName, m_Settings.indexerSettings.active); } internal void Update() { if (Holder.Instance == null || !m_Settings.indexerSettings.active) return; DeactivateDisabledScalers(); ActivateEnabledScalers(); var thermalAction = m_ThermalStateTracker.Update(); var performanceAction = m_PerformanceStateTracker.Update(); ThermalAction = thermalAction; PerformanceAction = performanceAction; if (Profiler.enabled) CollectProfilerStats(); // Enforce minimum wait time between any scaler changes TimeUntilNextAction = Mathf.Max(TimeUntilNextAction - Time.deltaTime, 0); if (TimeUntilNextAction != 0) return; if (m_ScalerEfficiencyTracker.IsRunning) m_ScalerEfficiencyTracker.Stop(); if (thermalAction == StateAction.Increase && performanceAction == StateAction.Stale) { UnapplyHighestCostScaler(); TimeUntilNextAction = m_Settings.indexerSettings.thermalActionDelay; return; } if (thermalAction == StateAction.Stale && performanceAction == StateAction.Stale) { UnapplyHighestCostScaler(); TimeUntilNextAction = m_Settings.indexerSettings.thermalActionDelay; return; } if (thermalAction == StateAction.Decrease) { ApplyLowestCostScaler(); TimeUntilNextAction = m_Settings.indexerSettings.thermalActionDelay; return; } if (performanceAction == StateAction.Decrease) { ApplyLowestCostScaler(); TimeUntilNextAction = m_Settings.indexerSettings.performanceActionDelay; return; } if (thermalAction == StateAction.FastDecrease) { ApplyLowestCostScaler(); TimeUntilNextAction = m_Settings.indexerSettings.thermalActionDelay / 2; return; } if (performanceAction == StateAction.FastDecrease) { ApplyLowestCostScaler(); TimeUntilNextAction = m_Settings.indexerSettings.performanceActionDelay / 2; return; } } void CollectProfilerStats() { for (int i = m_UnappliedScalers.Count - 1; i >= 0; i--) { var scaler = m_UnappliedScalers[i]; AdaptivePerformanceProfilerStats.EmitScalerDataToProfilerStream(scaler.Name, scaler.Enabled, scaler.OverrideLevel, scaler.CurrentLevel, scaler.Scale, false, scaler.MaxLevel); } for (int i = m_AppliedScalers.Count - 1; i >= 0; i--) { var scaler = m_AppliedScalers[i]; AdaptivePerformanceProfilerStats.EmitScalerDataToProfilerStream(scaler.Name, scaler.Enabled, scaler.OverrideLevel, scaler.CurrentLevel, scaler.Scale, true, scaler.MaxLevel); } for (int i = m_DisabledScalers.Count - 1; i >= 0; i--) { var scaler = m_DisabledScalers[i]; AdaptivePerformanceProfilerStats.EmitScalerDataToProfilerStream(scaler.Name, scaler.Enabled, scaler.OverrideLevel, scaler.CurrentLevel, scaler.Scale, false, scaler.MaxLevel); } AdaptivePerformanceProfilerStats.FlushScalerDataToProfilerStream(); } void DeactivateDisabledScalers() { for (int i = m_UnappliedScalers.Count - 1; i >= 0; i--) { var scaler = m_UnappliedScalers[i]; if (!scaler.Enabled && !m_DisabledScalers.Contains(scaler)) { APLog.Debug($"[Indexer] Deactivated {scaler.Name} scaler."); scaler.Deactivate(); m_DisabledScalers.Add(scaler); m_UnappliedScalers.RemoveAt(i); } } for (int i = m_AppliedScalers.Count - 1; i >= 0; i--) { var scaler = m_AppliedScalers[i]; if (!scaler.Enabled && !m_DisabledScalers.Contains(scaler)) { APLog.Debug($"[Indexer] Deactivated {scaler.Name} scaler."); scaler.Deactivate(); m_DisabledScalers.Add(scaler); m_AppliedScalers.RemoveAt(i); } } } void ActivateEnabledScalers() { for (int i = m_DisabledScalers.Count - 1; i >= 0; i--) { var scaler = m_DisabledScalers[i]; if (scaler.Enabled) { scaler.Activate(); AddScaler(scaler); m_DisabledScalers.RemoveAt(i); APLog.Debug($"[Indexer] Activated {scaler.Name} scaler."); } } } private bool ApplyLowestCostScaler() { AdaptivePerformanceScaler result = null; var lowestCost = float.PositiveInfinity; foreach (var scaler in m_UnappliedScalers) { if (!scaler.Enabled) continue; if (scaler.OverrideLevel != -1) continue; var cost = scaler.CalculateCost(); if (lowestCost > cost) { result = scaler; lowestCost = cost; } } foreach (var scaler in m_AppliedScalers) { if (!scaler.Enabled) continue; if (scaler.OverrideLevel != -1) continue; if (scaler.IsMaxLevel) continue; var cost = scaler.CalculateCost(); if (lowestCost > cost) { result = scaler; lowestCost = cost; } } if (result != null) { m_ScalerEfficiencyTracker.Start(result, true); ApplyScaler(result); return true; } return false; } private void ApplyScaler(AdaptivePerformanceScaler scaler) { APLog.Debug($"[Indexer] Applying {scaler.Name} scaler at level {scaler.CurrentLevel} and try to increase level to {scaler.CurrentLevel+1}"); if (scaler.NotLeveled) { m_UnappliedScalers.Remove(scaler); m_AppliedScalers.Add(scaler); } scaler.IncreaseLevel(); } private bool UnapplyHighestCostScaler() { AdaptivePerformanceScaler result = null; var highestCost = float.NegativeInfinity; foreach (var scaler in m_AppliedScalers) { if (scaler.OverrideLevel != -1) continue; var cost = scaler.CalculateCost(); if (highestCost < cost) { result = scaler; highestCost = cost; } } if (result != null) { m_ScalerEfficiencyTracker.Start(result, false); UnapplyScaler(result); return true; } return false; } private void UnapplyScaler(AdaptivePerformanceScaler scaler) { APLog.Debug($"[Indexer] Unapplying {scaler.Name} scaler at level {scaler.CurrentLevel} and try to decrease level to {scaler.CurrentLevel-1}"); scaler.DecreaseLevel(); if (scaler.NotLeveled) { m_AppliedScalers.Remove(scaler); m_UnappliedScalers.Add(scaler); } } } }