A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4
5namespace UnityEngine.Rendering
6{
7 /// <summary>
8 /// Base class for Rendering Debugger Display Stats.
9 /// </summary>
10 /// <typeparam name="TProfileId">Type of ProfileId the pipeline uses</typeparam>
11 public abstract class DebugDisplayStats<TProfileId> where TProfileId : Enum
12 {
13 // Accumulate values to avg over one second.
14 private class AccumulatedTiming
15 {
16 public float accumulatedValue = 0;
17 public float lastAverage = 0;
18
19 internal void UpdateLastAverage(int frameCount)
20 {
21 lastAverage = accumulatedValue / frameCount;
22 accumulatedValue = 0.0f;
23 }
24 }
25
26 private enum DebugProfilingType
27 {
28 CPU,
29 InlineCPU,
30 GPU
31 }
32
33 /// <summary>
34 /// Enable profiling recorders.
35 /// </summary>
36 public abstract void EnableProfilingRecorders();
37
38 /// <summary>
39 /// Disable all active profiling recorders.
40 /// </summary>
41 public abstract void DisableProfilingRecorders();
42
43 /// <summary>
44 /// Add display stats widgets to the list provided.
45 /// </summary>
46 /// <param name="list">List to add the widgets to.</param>
47 public abstract void RegisterDebugUI(List<DebugUI.Widget> list);
48
49 /// <summary>
50 /// Update the timing data displayed in Display Stats panel.
51 /// </summary>
52 public abstract void Update();
53
54 /// <summary>
55 /// Helper function to get all TProfilerId values of a given type to show in Detailed Stats section.
56 /// </summary>
57 /// <returns>List of TProfileId values excluding ones marked with [HideInDebugUI]</returns>
58 protected List<TProfileId> GetProfilerIdsToDisplay()
59 {
60 List<TProfileId> ids = new();
61 var type = typeof(TProfileId);
62
63 var enumValues = Enum.GetValues(type);
64 foreach (var enumValue in enumValues)
65 {
66 var memberInfos = type.GetMember(enumValue.ToString());
67 var enumValueMemberInfo = memberInfos.First(m => m.DeclaringType == type);
68 var hiddenAttribute = Attribute.GetCustomAttribute(enumValueMemberInfo, typeof(HideInDebugUIAttribute));
69 if (hiddenAttribute == null)
70 ids.Add((TProfileId)enumValue);
71 }
72
73 return ids;
74 }
75
76 /// <summary>
77 /// Update the detailed stats
78 /// </summary>
79 /// <param name="samplers">List of samplers to update</param>
80 protected void UpdateDetailedStats(List<TProfileId> samplers)
81 {
82 m_HiddenProfileIds.Clear();
83
84 m_TimeSinceLastAvgValue += Time.unscaledDeltaTime;
85 m_AccumulatedFrames++;
86 bool needUpdatingAverages = m_TimeSinceLastAvgValue >= k_AccumulationTimeInSeconds;
87
88 UpdateListOfAveragedProfilerTimings(needUpdatingAverages, samplers);
89
90 if (needUpdatingAverages)
91 {
92 m_TimeSinceLastAvgValue = 0.0f;
93 m_AccumulatedFrames = 0;
94 }
95 }
96
97 private static readonly string[] k_DetailedStatsColumnLabels = {"CPU", "CPUInline", "GPU"};
98 private Dictionary<TProfileId, AccumulatedTiming>[] m_AccumulatedTiming = { new(), new(), new() };
99 private float m_TimeSinceLastAvgValue = 0.0f;
100 private int m_AccumulatedFrames = 0;
101 private HashSet<TProfileId> m_HiddenProfileIds = new();
102
103 private const float k_AccumulationTimeInSeconds = 1.0f;
104
105 /// <summary> Whether to display timings averaged over a second instead of updating every frame. </summary>
106 protected bool averageProfilerTimingsOverASecond = false;
107
108 /// <summary> Whether to hide empty scopes from UI. </summary>
109 protected bool hideEmptyScopes = true;
110
111 /// <summary>
112 /// Helper function to build a list of sampler widgets for display stats
113 /// </summary>
114 /// <param name="title">Title for the stats list foldout</param>
115 /// <param name="samplers">List of samplers to display</param>
116 /// <returns>Foldout containing the list of sampler widgets</returns>
117 protected DebugUI.Widget BuildDetailedStatsList(string title, List<TProfileId> samplers)
118 {
119 var foldout = new DebugUI.Foldout(title, BuildProfilingSamplerWidgetList(samplers), k_DetailedStatsColumnLabels);
120 foldout.opened = true;
121 return foldout;
122 }
123
124 private void UpdateListOfAveragedProfilerTimings(bool needUpdatingAverages, List<TProfileId> samplers)
125 {
126 foreach (var samplerId in samplers)
127 {
128 var sampler = ProfilingSampler.Get(samplerId);
129
130 // Accumulate.
131 bool allValuesZero = true;
132 if (m_AccumulatedTiming[(int) DebugProfilingType.CPU].TryGetValue(samplerId, out var accCPUTiming))
133 {
134 accCPUTiming.accumulatedValue += sampler.cpuElapsedTime;
135 allValuesZero &= accCPUTiming.accumulatedValue == 0;
136 }
137
138 if (m_AccumulatedTiming[(int)DebugProfilingType.InlineCPU].TryGetValue(samplerId, out var accInlineCPUTiming))
139 {
140 accInlineCPUTiming.accumulatedValue += sampler.inlineCpuElapsedTime;
141 allValuesZero &= accInlineCPUTiming.accumulatedValue == 0;
142 }
143
144 if (m_AccumulatedTiming[(int)DebugProfilingType.GPU].TryGetValue(samplerId, out var accGPUTiming))
145 {
146 accGPUTiming.accumulatedValue += sampler.gpuElapsedTime;
147 allValuesZero &= accGPUTiming.accumulatedValue == 0;
148 }
149
150 if (needUpdatingAverages)
151 {
152 accCPUTiming?.UpdateLastAverage(m_AccumulatedFrames);
153 accInlineCPUTiming?.UpdateLastAverage(m_AccumulatedFrames);
154 accGPUTiming?.UpdateLastAverage(m_AccumulatedFrames);
155 }
156
157 // Update visibility status based on whether each accumulated value of this scope is zero
158 if (allValuesZero)
159 m_HiddenProfileIds.Add(samplerId);
160 }
161 }
162
163 private float GetSamplerTiming(TProfileId samplerId, ProfilingSampler sampler, DebugProfilingType type)
164 {
165 if (averageProfilerTimingsOverASecond)
166 {
167 // Find the right accumulated dictionary
168 if (m_AccumulatedTiming[(int)type].TryGetValue(samplerId, out AccumulatedTiming accTiming))
169 return accTiming.lastAverage;
170 }
171
172 return (type == DebugProfilingType.CPU)
173 ? sampler.cpuElapsedTime
174 : ((type == DebugProfilingType.GPU) ? sampler.gpuElapsedTime : sampler.inlineCpuElapsedTime);
175 }
176
177 private ObservableList<DebugUI.Widget> BuildProfilingSamplerWidgetList(IEnumerable<TProfileId> samplers)
178 {
179 var result = new ObservableList<DebugUI.Widget>();
180
181 DebugUI.Value CreateWidgetForSampler(TProfileId samplerId, ProfilingSampler sampler,
182 DebugProfilingType type)
183 {
184 // Find the right accumulated dictionary and add it there if not existing yet.
185 var accumulatedDictionary = m_AccumulatedTiming[(int)type];
186 if (!accumulatedDictionary.ContainsKey(samplerId))
187 {
188 accumulatedDictionary.Add(samplerId, new AccumulatedTiming());
189 }
190
191 return new()
192 {
193 formatString = "{0:F2}ms",
194 refreshRate = 1.0f / 5.0f,
195 getter = () => GetSamplerTiming(samplerId, sampler, type)
196 };
197 }
198
199 foreach (var samplerId in samplers)
200 {
201 var sampler = ProfilingSampler.Get(samplerId);
202
203 // In non-dev build ProfilingSampler.Get always returns null.
204 if (sampler == null)
205 continue;
206
207 sampler.enableRecording = true;
208
209 result.Add(new DebugUI.ValueTuple
210 {
211 displayName = sampler.name,
212 isHiddenCallback = () =>
213 {
214 if (hideEmptyScopes && m_HiddenProfileIds.Contains(samplerId))
215 return true;
216 return false;
217 },
218 values = Enum.GetValues(typeof(DebugProfilingType)).Cast<DebugProfilingType>()
219 .Select(e => CreateWidgetForSampler(samplerId, sampler, e)).ToArray()
220 });
221 }
222
223 return result;
224 }
225 }
226}