using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace UnityEngine.Rendering { /// /// Base class for Rendering Debugger Display Stats. /// Subclasses provide pipeline-specific lists of instances /// and register the corresponding debug UI widgets. /// public abstract class DebugDisplayStats { List m_CoreProfilingSamplers = GetProfilingSamplersToDisplay(typeof(CoreProfilingSamplers)); // 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 virtual void EnableProfilingRecorders() { AddAndEnableProfilingSamplers(m_CoreProfilingSamplers); } /// /// Add Profiling Samplers to recorded list and enable them. /// /// List of profiling samplers. protected void AddAndEnableProfilingSamplers(List samplers) { foreach (var sampler in samplers) { if (sampler == null) continue; m_RecordedSamplers.Add(sampler); sampler.enableRecording = true; } } /// /// Disable all active profiling recorders. /// public virtual void DisableProfilingRecorders() { foreach (var sampler in m_RecordedSamplers) sampler.enableRecording = false; m_RecordedSamplers.Clear(); } /// /// Add display stats widgets to the list provided. /// /// List to add the widgets to. public virtual void RegisterDebugUI(List list) { m_DebugFrameTiming.RegisterDebugUI(list); var detailedStatsFoldout = new DebugUI.Foldout { displayName = "Detailed Stats", opened = false, children = { new DebugUI.BoolField { displayName = "Update every second with average", getter = () => averageProfilerTimingsOverASecond, setter = value => averageProfilerTimingsOverASecond = value }, new DebugUI.BoolField { displayName = "Hide empty scopes", tooltip = "Hide profiling scopes where elapsed time in each category is zero", getter = () => hideEmptyScopes, setter = value => hideEmptyScopes = value } } }; detailedStatsFoldout.children.Add(BuildDetailedStatsList("Profiling Scopes", m_CoreProfilingSamplers)); list.Add(detailedStatsFoldout); } /// /// Update the timing data displayed in Display Stats panel. /// public virtual void Update() { m_DebugFrameTiming.UpdateFrameTiming(); UpdateDetailedStats(m_RecordedSamplers); } /// /// Collects all public static readonly ProfilingSampler fields from /// , skipping any decorated with /// . /// /// A static class containing ProfilingSampler fields /// (e.g. typeof(CoreProfilingSamplers)). /// List of ProfilingSampler instances to display. protected static List GetProfilingSamplersToDisplay(Type markersType) { var result = new List(); foreach (var field in markersType.GetFields(BindingFlags.Public | BindingFlags.Static)) { if (field.FieldType != typeof(ProfilingSampler)) continue; if (field.GetCustomAttribute() != null) continue; if (field.GetValue(null) is ProfilingSampler sampler) result.Add(sampler); } return result; } /// /// Update the detailed stats. /// /// List of samplers to update. protected void UpdateDetailedStats(List samplers) { m_HiddenSamplers.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_HiddenSamplers = 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; /// /// Frame Times measurements used for Display Stats. /// readonly DebugFrameTiming m_DebugFrameTiming = new(); /// /// List of markers for Detailed stats. /// readonly List m_RecordedSamplers = new(); /// /// 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; foldout.alternateRowColors = true; return foldout; } private void UpdateListOfAveragedProfilerTimings(bool needUpdatingAverages, List samplers) { foreach (var sampler in samplers) { // Accumulate. bool allValuesZero = true; if (m_AccumulatedTiming[(int)DebugProfilingType.CPU].TryGetValue(sampler, out var accCPUTiming)) { accCPUTiming.accumulatedValue += sampler.cpuElapsedTime; allValuesZero &= accCPUTiming.accumulatedValue == 0; } if (m_AccumulatedTiming[(int)DebugProfilingType.InlineCPU].TryGetValue(sampler, out var accInlineCPUTiming)) { accInlineCPUTiming.accumulatedValue += sampler.inlineCpuElapsedTime; allValuesZero &= accInlineCPUTiming.accumulatedValue == 0; } if (m_AccumulatedTiming[(int)DebugProfilingType.GPU].TryGetValue(sampler, 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_HiddenSamplers.Add(sampler); } } private float GetSamplerTiming(ProfilingSampler sampler, DebugProfilingType type) { if (averageProfilerTimingsOverASecond) { // Find the right accumulated dictionary if (m_AccumulatedTiming[(int)type].TryGetValue(sampler, out AccumulatedTiming accTiming)) return accTiming.lastAverage; } return (type == DebugProfilingType.CPU) ? sampler.cpuElapsedTime : ((type == DebugProfilingType.GPU) ? sampler.gpuElapsedTime : sampler.inlineCpuElapsedTime); } /// /// Create an observable list of widgets from Profiling Samplers /// /// Profiling Sampler collection. /// An observable list of widgets. protected ObservableList BuildProfilingSamplerWidgetList(IEnumerable samplers) { var result = new ObservableList(); DebugUI.Value CreateWidgetForSampler(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(sampler)) { accumulatedDictionary.Add(sampler, new AccumulatedTiming()); } return new() { formatString = "{0:F2}ms", refreshRate = 1.0f / 5.0f, getter = () => GetSamplerTiming(sampler, type) }; } foreach (var sampler in samplers) { if (sampler == null) continue; result.Add(new DebugUI.ValueTuple { displayName = sampler.name, isHiddenCallback = () => hideEmptyScopes && m_HiddenSamplers.Contains(sampler), values = Enum.GetValues(typeof(DebugProfilingType)).Cast() .Select(e => CreateWidgetForSampler(sampler, e)).ToArray() }); } return result; } } /// /// Base class for Rendering Debugger Display Stats. /// /// Type of ProfileId the pipeline uses [Obsolete("Use the non-generic DebugDisplayStats base class with ProfilingSampler lists. #from(6000.6)")] public abstract class DebugDisplayStats : DebugDisplayStats where TProfileId : Enum { /// /// 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] [Obsolete("Use GetProfilingSamplersToDisplay(Type) with a static marker class. #from(6000.6)")] 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 enum profile IDs to update. [Obsolete("Use UpdateDetailedStats(List) instead. #from(6000.6)")] protected void UpdateDetailedStats(List samplers) { UpdateDetailedStats(ConvertToSamplers(samplers)); } /// /// Helper function to build a list of sampler widgets for display stats. /// /// Title for the stats list foldout. /// List of enum profile IDs to display. /// Foldout containing the list of sampler widgets. [Obsolete("Use BuildDetailedStatsList(string, List) instead. #from(6000.6)")] protected DebugUI.Widget BuildDetailedStatsList(string title, List samplers) { return BuildDetailedStatsList(title, ConvertToSamplers(samplers)); } static List ConvertToSamplers(List ids) { var samplers = new List(ids.Count); foreach (var id in ids) { var sampler = ProfilingSampler.Get(id); if (sampler != null) samplers.Add(sampler); } return samplers; } } }