using System; using System.Collections.Generic; using System.Linq; namespace UnityEngine.Rendering { /// /// Base class for Rendering Debugger Display Stats. /// /// Type of ProfileId the pipeline uses public abstract class DebugDisplayStats where TProfileId : Enum { // Accumulate values to avg over one second. private class AccumulatedTiming { public float accumulatedValue = 0; public float lastAverage = 0; internal void UpdateLastAverage(int frameCount) { lastAverage = accumulatedValue / frameCount; accumulatedValue = 0.0f; } } private enum DebugProfilingType { CPU, InlineCPU, GPU } /// /// Enable profiling recorders. /// public abstract void EnableProfilingRecorders(); /// /// Disable all active profiling recorders. /// public abstract void DisableProfilingRecorders(); /// /// Add display stats widgets to the list provided. /// /// List to add the widgets to. public abstract void RegisterDebugUI(List list); /// /// Update the timing data displayed in Display Stats panel. /// public abstract void Update(); /// /// Helper function to get all TProfilerId values of a given type to show in Detailed Stats section. /// /// List of TProfileId values excluding ones marked with [HideInDebugUI] protected List GetProfilerIdsToDisplay() { List ids = new(); var type = typeof(TProfileId); var enumValues = Enum.GetValues(type); foreach (var enumValue in enumValues) { var memberInfos = type.GetMember(enumValue.ToString()); var enumValueMemberInfo = memberInfos.First(m => m.DeclaringType == type); var hiddenAttribute = Attribute.GetCustomAttribute(enumValueMemberInfo, typeof(HideInDebugUIAttribute)); if (hiddenAttribute == null) ids.Add((TProfileId)enumValue); } return ids; } /// /// Update the detailed stats /// /// List of samplers to update protected void UpdateDetailedStats(List samplers) { m_HiddenProfileIds.Clear(); m_TimeSinceLastAvgValue += Time.unscaledDeltaTime; m_AccumulatedFrames++; bool needUpdatingAverages = m_TimeSinceLastAvgValue >= k_AccumulationTimeInSeconds; UpdateListOfAveragedProfilerTimings(needUpdatingAverages, samplers); if (needUpdatingAverages) { m_TimeSinceLastAvgValue = 0.0f; m_AccumulatedFrames = 0; } } private static readonly string[] k_DetailedStatsColumnLabels = {"CPU", "CPUInline", "GPU"}; private Dictionary[] m_AccumulatedTiming = { new(), new(), new() }; private float m_TimeSinceLastAvgValue = 0.0f; private int m_AccumulatedFrames = 0; private HashSet m_HiddenProfileIds = new(); private const float k_AccumulationTimeInSeconds = 1.0f; /// Whether to display timings averaged over a second instead of updating every frame. protected bool averageProfilerTimingsOverASecond = false; /// Whether to hide empty scopes from UI. protected bool hideEmptyScopes = true; /// /// Helper function to build a list of sampler widgets for display stats /// /// Title for the stats list foldout /// List of samplers to display /// Foldout containing the list of sampler widgets protected DebugUI.Widget BuildDetailedStatsList(string title, List samplers) { var foldout = new DebugUI.Foldout(title, BuildProfilingSamplerWidgetList(samplers), k_DetailedStatsColumnLabels); foldout.opened = true; return foldout; } private void UpdateListOfAveragedProfilerTimings(bool needUpdatingAverages, List samplers) { foreach (var samplerId in samplers) { var sampler = ProfilingSampler.Get(samplerId); // Accumulate. bool allValuesZero = true; if (m_AccumulatedTiming[(int) DebugProfilingType.CPU].TryGetValue(samplerId, out var accCPUTiming)) { accCPUTiming.accumulatedValue += sampler.cpuElapsedTime; allValuesZero &= accCPUTiming.accumulatedValue == 0; } if (m_AccumulatedTiming[(int)DebugProfilingType.InlineCPU].TryGetValue(samplerId, out var accInlineCPUTiming)) { accInlineCPUTiming.accumulatedValue += sampler.inlineCpuElapsedTime; allValuesZero &= accInlineCPUTiming.accumulatedValue == 0; } if (m_AccumulatedTiming[(int)DebugProfilingType.GPU].TryGetValue(samplerId, out var accGPUTiming)) { accGPUTiming.accumulatedValue += sampler.gpuElapsedTime; allValuesZero &= accGPUTiming.accumulatedValue == 0; } if (needUpdatingAverages) { accCPUTiming?.UpdateLastAverage(m_AccumulatedFrames); accInlineCPUTiming?.UpdateLastAverage(m_AccumulatedFrames); accGPUTiming?.UpdateLastAverage(m_AccumulatedFrames); } // Update visibility status based on whether each accumulated value of this scope is zero if (allValuesZero) m_HiddenProfileIds.Add(samplerId); } } private float GetSamplerTiming(TProfileId samplerId, ProfilingSampler sampler, DebugProfilingType type) { if (averageProfilerTimingsOverASecond) { // Find the right accumulated dictionary if (m_AccumulatedTiming[(int)type].TryGetValue(samplerId, out AccumulatedTiming accTiming)) return accTiming.lastAverage; } return (type == DebugProfilingType.CPU) ? sampler.cpuElapsedTime : ((type == DebugProfilingType.GPU) ? sampler.gpuElapsedTime : sampler.inlineCpuElapsedTime); } private ObservableList BuildProfilingSamplerWidgetList(IEnumerable samplers) { var result = new ObservableList(); DebugUI.Value CreateWidgetForSampler(TProfileId samplerId, ProfilingSampler sampler, DebugProfilingType type) { // Find the right accumulated dictionary and add it there if not existing yet. var accumulatedDictionary = m_AccumulatedTiming[(int)type]; if (!accumulatedDictionary.ContainsKey(samplerId)) { accumulatedDictionary.Add(samplerId, new AccumulatedTiming()); } return new() { formatString = "{0:F2}ms", refreshRate = 1.0f / 5.0f, getter = () => GetSamplerTiming(samplerId, sampler, type) }; } foreach (var samplerId in samplers) { var sampler = ProfilingSampler.Get(samplerId); // In non-dev build ProfilingSampler.Get always returns null. if (sampler == null) continue; sampler.enableRecording = true; result.Add(new DebugUI.ValueTuple { displayName = sampler.name, isHiddenCallback = () => { if (hideEmptyScopes && m_HiddenProfileIds.Contains(samplerId)) return true; return false; }, values = Enum.GetValues(typeof(DebugProfilingType)).Cast() .Select(e => CreateWidgetForSampler(samplerId, sampler, e)).ToArray() }); } return result; } } }