A game about forced loneliness, made by TACStudios
1#if ENABLE_INPUT_SYSTEM && ENABLE_INPUT_SYSTEM_PACKAGE
2#define USE_INPUT_SYSTEM
3#endif
4
5using System;
6using System.Collections.Generic;
7using System.Linq;
8using UnityEditor.Callbacks;
9using UnityEditor.Rendering.Analytics;
10using UnityEditorInternal;
11using UnityEngine;
12using UnityEngine.Assertions;
13using UnityEngine.Rendering;
14
15using PackageInfo = UnityEditor.PackageManager.PackageInfo;
16
17namespace UnityEditor.Rendering
18{
19#pragma warning disable 414
20
21 [Serializable]
22 sealed class WidgetStateDictionary : SerializedDictionary<string, DebugState> { }
23
24 sealed class DebugWindowSettings : ScriptableObject
25 {
26 // Keep these settings in a separate scriptable object so we can handle undo/redo on them
27 // without the rest of the debug window interfering
28 public int currentStateHash;
29
30 public int selectedPanel
31 {
32 get => Mathf.Max(0, DebugManager.instance.PanelIndex(selectedPanelDisplayName));
33 set
34 {
35 var displayName = DebugManager.instance.PanelDiplayName(value);
36 if (!string.IsNullOrEmpty(displayName))
37 selectedPanelDisplayName = displayName;
38 }
39 }
40
41 public string selectedPanelDisplayName;
42
43 void OnEnable()
44 {
45 hideFlags = HideFlags.HideAndDontSave;
46 }
47 }
48
49 sealed class DebugWindow : EditorWindowWithHelpButton, IHasCustomMenu
50 {
51 static Styles s_Styles;
52 static GUIStyle s_SplitterLeft;
53
54 static float splitterPos = 150f;
55 const float minSideBarWidth = 100;
56 const float minContentWidth = 100;
57 bool dragging = false;
58
59 [SerializeField]
60 WidgetStateDictionary m_WidgetStates;
61
62 [SerializeField]
63 DebugWindowSettings m_Settings;
64
65 bool m_IsDirty;
66
67 Vector2 m_PanelScroll;
68 Vector2 m_ContentScroll;
69
70 static bool s_TypeMapDirty;
71 static Dictionary<Type, Type> s_WidgetStateMap; // DebugUI.Widget type -> DebugState type
72 static Dictionary<Type, DebugUIDrawer> s_WidgetDrawerMap; // DebugUI.Widget type -> DebugUIDrawer
73
74 public static bool open
75 {
76 get => DebugManager.instance.displayEditorUI;
77 private set => DebugManager.instance.displayEditorUI = value;
78 }
79
80 protected override void OnHelpButtonClicked()
81 {
82 //Deduce documentation url and open it in browser
83 var url = GetSpecificURL() ?? GetDefaultURL();
84 Application.OpenURL(url);
85 }
86
87 string GetDefaultURL()
88 {
89 //Find package info of the current CoreRP package
90 return $"https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@{DocumentationInfo.version}/manual/Rendering-Debugger.html";
91 }
92
93 string GetSpecificURL()
94 {
95 //Find package info of the current RenderPipeline
96 var currentPipeline = GraphicsSettings.currentRenderPipeline;
97 if (currentPipeline == null)
98 return null;
99
100 if (!DocumentationUtils.TryGetPackageInfoForType(currentPipeline.GetType(), out var packageName, out var version))
101 return null;
102
103 return packageName switch
104 {
105 "com.unity.render-pipelines.universal" => $"https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@{version}/manual/features/rendering-debugger.html",
106 "com.unity.render-pipelines.high-definition" => $"https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@{version}/manual/Render-Pipeline-Debug-Window.html",
107 _ => null
108 };
109 }
110
111 [DidReloadScripts]
112 static void OnEditorReload()
113 {
114 s_TypeMapDirty = true;
115
116 //find if it where open, relink static event end propagate the info
117 open = (Resources.FindObjectsOfTypeAll<DebugWindow>()?.Length ?? 0) > 0;
118 }
119
120 static void RebuildTypeMaps()
121 {
122 // Map states to widget (a single state can map to several widget types if the value to
123 // serialize is the same)
124 var attrType = typeof(DebugStateAttribute);
125 var stateTypes = CoreUtils.GetAllTypesDerivedFrom<DebugState>()
126 .Where(
127 t => t.IsDefined(attrType, false)
128 && !t.IsAbstract
129 );
130
131 s_WidgetStateMap = new Dictionary<Type, Type>();
132
133 foreach (var stateType in stateTypes)
134 {
135 var attr = (DebugStateAttribute)stateType.GetCustomAttributes(attrType, false)[0];
136
137 foreach (var t in attr.types)
138 s_WidgetStateMap.Add(t, stateType);
139 }
140
141 // Drawers
142 attrType = typeof(DebugUIDrawerAttribute);
143 var types = CoreUtils.GetAllTypesDerivedFrom<DebugUIDrawer>()
144 .Where(
145 t => t.IsDefined(attrType, false)
146 && !t.IsAbstract
147 );
148
149 s_WidgetDrawerMap = new Dictionary<Type, DebugUIDrawer>();
150
151 foreach (var t in types)
152 {
153 var attr = (DebugUIDrawerAttribute)t.GetCustomAttributes(attrType, false)[0];
154 var inst = (DebugUIDrawer)Activator.CreateInstance(t);
155 s_WidgetDrawerMap.Add(attr.type, inst);
156 }
157
158 // Done
159 s_TypeMapDirty = false;
160 }
161
162 [MenuItem("Window/Analysis/Rendering Debugger", priority = 10005)]
163 static void Init()
164 {
165 var window = GetWindow<DebugWindow>();
166 window.titleContent = Styles.windowTitle;
167 }
168
169 [MenuItem("Window/Analysis/Rendering Debugger", validate = true)]
170 static bool ValidateMenuItem()
171 {
172 return RenderPipelineManager.currentPipeline != null;
173 }
174
175 void OnEnable()
176 {
177 open = true;
178
179 DebugManager.instance.refreshEditorRequested = false;
180
181 hideFlags = HideFlags.HideAndDontSave;
182 autoRepaintOnSceneChange = true;
183
184 if (m_Settings == null)
185 m_Settings = CreateInstance<DebugWindowSettings>();
186
187 // States are ScriptableObjects (necessary for Undo/Redo) but are not saved on disk so when the editor is closed then reopened, any existing debug window will have its states set to null
188 // Since we don't care about persistence in this case, we just re-init everything.
189 if (m_WidgetStates == null || !AreWidgetStatesValid())
190 m_WidgetStates = new WidgetStateDictionary();
191
192 if (s_WidgetStateMap == null || s_WidgetDrawerMap == null || s_TypeMapDirty)
193 RebuildTypeMaps();
194
195 Undo.undoRedoPerformed += OnUndoRedoPerformed;
196 DebugManager.instance.onSetDirty += MarkDirty;
197
198 // First init
199 UpdateWidgetStates();
200
201 EditorApplication.update -= Repaint;
202 var panels = DebugManager.instance.panels;
203 var selectedPanelIndex = m_Settings.selectedPanel;
204 if (selectedPanelIndex >= 0
205 && selectedPanelIndex < panels.Count
206 && panels[selectedPanelIndex].editorForceUpdate)
207 EditorApplication.update += Repaint;
208
209 GraphicsToolLifetimeAnalytic.WindowOpened<DebugWindow>();
210 }
211
212 // Note: this won't get called if the window is opened when the editor itself is closed
213 void OnDestroy()
214 {
215 open = false;
216 DebugManager.instance.onSetDirty -= MarkDirty;
217 Undo.ClearUndo(m_Settings);
218
219 DestroyWidgetStates();
220 }
221
222 private void OnDisable()
223 {
224 GraphicsToolLifetimeAnalytic.WindowClosed<DebugWindow>();
225 }
226
227 public void DestroyWidgetStates()
228 {
229 if (m_WidgetStates == null)
230 return;
231
232 // Clear all the states from memory
233 foreach (var state in m_WidgetStates)
234 {
235 var s = state.Value;
236 Undo.ClearUndo(s); // Don't leave dangling states in the global undo/redo stack
237 DestroyImmediate(s);
238 }
239
240 m_WidgetStates.Clear();
241 }
242
243 public void ReloadWidgetStates()
244 {
245 if (m_WidgetStates == null)
246 return;
247
248 // Clear states from memory that don't have a corresponding widget
249 List<string> keysToRemove = new ();
250 foreach (var state in m_WidgetStates)
251 {
252 var widget = DebugManager.instance.GetItem(state.Key);
253 if (widget == null)
254 {
255 var s = state.Value;
256 Undo.ClearUndo(s); // Don't leave dangling states in the global undo/redo stack
257 DestroyImmediate(s);
258 keysToRemove.Add(state.Key);
259 }
260 }
261
262 // Cleanup null entries because they can break the dictionary serialization
263 foreach (var key in keysToRemove)
264 {
265 m_WidgetStates.Remove(key);
266 }
267
268 UpdateWidgetStates();
269 }
270
271 bool AreWidgetStatesValid()
272 {
273 foreach (var state in m_WidgetStates)
274 {
275 if (state.Value == null)
276 {
277 return false;
278 }
279 }
280 return true;
281 }
282
283 void MarkDirty()
284 {
285 m_IsDirty = true;
286 }
287
288 // We use item states to keep a cached value of each serializable debug items in order to
289 // handle domain reloads, play mode entering/exiting and undo/redo
290 // Note: no removal of orphan states
291 void UpdateWidgetStates()
292 {
293 foreach (var panel in DebugManager.instance.panels)
294 UpdateWidgetStates(panel);
295 }
296
297 void UpdateWidgetStates(DebugUI.IContainer container)
298 {
299 // Skip runtime only containers, we won't draw them so no need to serialize them either
300 if (container is DebugUI.Widget actualWidget && actualWidget.isInactiveInEditor)
301 return;
302
303 // Recursively update widget states
304 foreach (var widget in container.children)
305 {
306 // Skip non-serializable widgets but still traverse them in case one of their
307 // children needs serialization support
308 if (widget is DebugUI.IValueField valueField)
309 {
310 // Skip runtime & readonly only items
311 if (widget.isInactiveInEditor)
312 return;
313
314 string guid = widget.queryPath;
315 if (!m_WidgetStates.TryGetValue(guid, out var state) || state == null)
316 {
317 var widgetType = widget.GetType();
318 if (s_WidgetStateMap.TryGetValue(widgetType, out Type stateType))
319 {
320 Assert.IsNotNull(stateType);
321 var inst = (DebugState)CreateInstance(stateType);
322 inst.queryPath = guid;
323 inst.SetValue(valueField.GetValue(), valueField);
324 m_WidgetStates[guid] = inst;
325 }
326 }
327 }
328
329 // Recurse if the widget is a container
330 if (widget is DebugUI.IContainer containerField)
331 UpdateWidgetStates(containerField);
332 }
333 }
334
335 public void ApplyStates(bool forceApplyAll = false)
336 {
337 if (!forceApplyAll && DebugState.m_CurrentDirtyState != null)
338 {
339 ApplyState(DebugState.m_CurrentDirtyState.queryPath, DebugState.m_CurrentDirtyState);
340 DebugState.m_CurrentDirtyState = null;
341 return;
342 }
343
344 foreach (var state in m_WidgetStates)
345 ApplyState(state.Key, state.Value);
346
347 DebugState.m_CurrentDirtyState = null;
348 }
349
350 void ApplyState(string queryPath, DebugState state)
351 {
352 if (!(DebugManager.instance.GetItem(queryPath) is DebugUI.IValueField widget))
353 return;
354
355 widget.SetValue(state.GetValue());
356 }
357
358 void OnUndoRedoPerformed()
359 {
360 int stateHash = ComputeStateHash();
361
362 // Something has been undone / redone, re-apply states to the debug tree
363 if (stateHash != m_Settings.currentStateHash)
364 {
365 ApplyStates(true);
366 m_Settings.currentStateHash = stateHash;
367 }
368
369 Repaint();
370 }
371
372 int ComputeStateHash()
373 {
374 unchecked
375 {
376 int hash = 13;
377
378 foreach (var state in m_WidgetStates)
379 hash = hash * 23 + state.Value.GetHashCode();
380
381 return hash;
382 }
383 }
384
385 void Update()
386 {
387 // If the render pipeline asset has been reloaded we force-refresh widget states in case
388 // some debug values need to be refresh/recreated as well (e.g. frame settings on HD)
389 if (DebugManager.instance.refreshEditorRequested)
390 {
391 ReloadWidgetStates();
392 m_IsDirty = true;
393 DebugManager.instance.refreshEditorRequested = false;
394 }
395
396 int? requestedPanelIndex = DebugManager.instance.GetRequestedEditorWindowPanelIndex();
397 if (requestedPanelIndex != null)
398 {
399 m_Settings.selectedPanel = requestedPanelIndex.Value;
400 }
401
402 if (m_IsDirty)
403 {
404 UpdateWidgetStates();
405 ApplyStates();
406 m_IsDirty = false;
407 }
408 }
409
410 void OnGUI()
411 {
412 if (s_Styles == null)
413 {
414 s_Styles = new Styles();
415 s_SplitterLeft = new GUIStyle();
416 }
417
418 var panels = DebugManager.instance.panels;
419 int itemCount = panels.Count(x => !x.isInactiveInEditor && x.children.Count(w => !w.isInactiveInEditor) > 0);
420
421 if (itemCount == 0)
422 {
423 EditorGUILayout.HelpBox("No debug item found.", MessageType.Info);
424 return;
425 }
426
427 // Background color
428 var wrect = position;
429 wrect.x = 0;
430 wrect.y = 0;
431 var oldColor = GUI.color;
432 GUI.color = s_Styles.skinBackgroundColor;
433 GUI.DrawTexture(wrect, EditorGUIUtility.whiteTexture);
434 GUI.color = oldColor;
435
436
437 GUILayout.BeginHorizontal(EditorStyles.toolbar);
438 GUILayout.FlexibleSpace();
439 if (GUILayout.Button(Styles.resetButtonContent, EditorStyles.toolbarButton))
440 {
441 DebugManager.instance.Reset();
442 DestroyWidgetStates();
443 UpdateWidgetStates();
444 InternalEditorUtility.RepaintAllViews();
445 }
446
447 GUILayout.EndHorizontal();
448
449 using (new EditorGUILayout.HorizontalScope())
450 {
451 // Side bar
452 using (var scrollScope = new EditorGUILayout.ScrollViewScope(m_PanelScroll, s_Styles.sectionScrollView, GUILayout.Width(splitterPos)))
453 {
454 if (m_Settings.selectedPanel >= panels.Count)
455 m_Settings.selectedPanel = 0;
456
457 // Validate container id
458 while (panels[m_Settings.selectedPanel].isInactiveInEditor || panels[m_Settings.selectedPanel].children.Count(x => !x.isInactiveInEditor) == 0)
459 {
460 m_Settings.selectedPanel++;
461
462 if (m_Settings.selectedPanel >= panels.Count)
463 m_Settings.selectedPanel = 0;
464 }
465
466 // Root children are containers
467 for (int i = 0; i < panels.Count; i++)
468 {
469 var panel = panels[i];
470
471 if (panel.isInactiveInEditor)
472 continue;
473
474 if (panel.children.Count(x => !x.isInactiveInEditor) == 0)
475 continue;
476
477 var elementRect = GUILayoutUtility.GetRect(EditorGUIUtility.TrTextContent(panel.displayName), s_Styles.sectionElement, GUILayout.ExpandWidth(true));
478
479 if (m_Settings.selectedPanel == i && Event.current.type == EventType.Repaint)
480 s_Styles.selected.Draw(elementRect, false, false, false, false);
481
482 EditorGUI.BeginChangeCheck();
483 GUI.Toggle(elementRect, m_Settings.selectedPanel == i, panel.displayName, s_Styles.sectionElement);
484 if (EditorGUI.EndChangeCheck())
485 {
486 Undo.RegisterCompleteObjectUndo(m_Settings, $"Debug Panel '{panel.displayName}' Selection");
487 var previousPanel = m_Settings.selectedPanel >= 0 && m_Settings.selectedPanel < panels.Count
488 ? panels[m_Settings.selectedPanel]
489 : null;
490 if (previousPanel != null && previousPanel.editorForceUpdate && !panel.editorForceUpdate)
491 EditorApplication.update -= Repaint;
492 else if ((previousPanel == null || !previousPanel.editorForceUpdate) && panel.editorForceUpdate)
493 EditorApplication.update += Repaint;
494 m_Settings.selectedPanel = i;
495 }
496 }
497
498 m_PanelScroll = scrollScope.scrollPosition;
499 }
500
501 Rect splitterRect = new Rect(splitterPos - 3, 0, 6, Screen.height);
502 GUI.Box(splitterRect, "", s_SplitterLeft);
503
504 const float topMargin = 2f;
505 GUILayout.Space(topMargin);
506
507 // Main section - traverse current container
508 using (var changedScope = new EditorGUI.ChangeCheckScope())
509 {
510 using (new EditorGUILayout.VerticalScope())
511 {
512 const float leftMargin = 4f;
513 GUILayout.Space(leftMargin);
514 var selectedPanel = panels[m_Settings.selectedPanel];
515
516 using (var scrollScope = new EditorGUILayout.ScrollViewScope(m_ContentScroll))
517 {
518 TraverseContainerGUI(selectedPanel);
519 m_ContentScroll = scrollScope.scrollPosition;
520 }
521 }
522
523 if (changedScope.changed)
524 {
525 m_Settings.currentStateHash = ComputeStateHash();
526 DebugManager.instance.ReDrawOnScreenDebug();
527 }
528 }
529
530 // Splitter events
531 if (Event.current != null)
532 {
533 switch (Event.current.rawType)
534 {
535 case EventType.MouseDown:
536 if (splitterRect.Contains(Event.current.mousePosition))
537 {
538 dragging = true;
539 }
540 break;
541 case EventType.MouseDrag:
542 if (dragging)
543 {
544 splitterPos += Event.current.delta.x;
545 splitterPos = Mathf.Clamp(splitterPos, minSideBarWidth, Screen.width - minContentWidth);
546 Repaint();
547 }
548 break;
549 case EventType.MouseUp:
550 if (dragging)
551 {
552 dragging = false;
553 }
554 break;
555 }
556 }
557 EditorGUIUtility.AddCursorRect(splitterRect, MouseCursor.ResizeHorizontal);
558 }
559 }
560
561 void OnWidgetGUI(DebugUI.Widget widget)
562 {
563 if (widget.isInactiveInEditor || widget.isHidden)
564 return;
565
566 // State will be null for stateless widget
567 m_WidgetStates.TryGetValue(widget.queryPath, out DebugState state);
568
569 GUILayout.Space(4);
570
571 if (!s_WidgetDrawerMap.TryGetValue(widget.GetType(), out DebugUIDrawer drawer))
572 {
573 EditorGUILayout.LabelField("Drawer not found (" + widget.GetType() + ").");
574 }
575 else
576 {
577 drawer.Begin(widget, state);
578
579 if (drawer.OnGUI(widget, state))
580 {
581 if (widget is DebugUI.IContainer container)
582 TraverseContainerGUI(container);
583 }
584
585 drawer.End(widget, state);
586 }
587 }
588
589 void TraverseContainerGUI(DebugUI.IContainer container)
590 {
591 // /!\ SHAAAAAAAME ALERT /!\
592 // A container can change at runtime because of the way IMGUI works and how we handle
593 // onValueChanged on widget so we have to take this into account while iterating
594 try
595 {
596 foreach (var widget in container.children)
597 OnWidgetGUI(widget);
598 }
599 catch (InvalidOperationException)
600 {
601 Repaint();
602 }
603 }
604
605 public class Styles
606 {
607 public static float s_DefaultLabelWidth = 0.5f;
608
609 public static GUIContent windowTitle { get; } = EditorGUIUtility.TrTextContent("Rendering Debugger");
610
611 public static GUIContent resetButtonContent { get; } = EditorGUIUtility.TrTextContent("Reset");
612
613 public static GUIStyle foldoutHeaderStyle { get; } = new GUIStyle(EditorStyles.foldoutHeader)
614 {
615 fixedHeight = 20,
616 fontStyle = FontStyle.Bold,
617 margin = new RectOffset(0, 0, 0, 0)
618 };
619
620 public static GUIStyle labelWithZeroValueStyle { get; } = new GUIStyle(EditorStyles.label);
621
622 public readonly GUIStyle sectionScrollView = "PreferencesSectionBox";
623 public readonly GUIStyle sectionElement = new GUIStyle("PreferencesSection");
624 public readonly GUIStyle selected = "OL SelectedRow";
625 public readonly GUIStyle sectionHeader = new GUIStyle(EditorStyles.largeLabel);
626 public readonly Color skinBackgroundColor;
627
628 public static GUIStyle centeredLeft = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft };
629 public static float singleRowHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
630
631 public static int foldoutColumnWidth = 70;
632
633 public Styles()
634 {
635 Color textColorDarkSkin = new Color32(210, 210, 210, 255);
636 Color textColorLightSkin = new Color32(102, 102, 102, 255);
637 Color backgroundColorDarkSkin = new Color32(38, 38, 38, 128);
638 Color backgroundColorLightSkin = new Color32(128, 128, 128, 96);
639
640 sectionScrollView = new GUIStyle(sectionScrollView);
641 sectionScrollView.overflow.bottom += 1;
642
643 sectionElement.alignment = TextAnchor.MiddleLeft;
644
645 sectionHeader.fontStyle = FontStyle.Bold;
646 sectionHeader.fontSize = 18;
647 sectionHeader.margin.top = 10;
648 sectionHeader.margin.left += 1;
649 sectionHeader.normal.textColor = EditorGUIUtility.isProSkin ? textColorDarkSkin : textColorLightSkin;
650 skinBackgroundColor = EditorGUIUtility.isProSkin ? backgroundColorDarkSkin : backgroundColorLightSkin;
651
652 labelWithZeroValueStyle.normal.textColor = Color.gray;
653 }
654 }
655
656 public void AddItemsToMenu(GenericMenu menu)
657 {
658 menu.AddItem(EditorGUIUtility.TrTextContent("Expand All"), false, () => SetExpanded(true));
659 menu.AddItem(EditorGUIUtility.TrTextContent("Collapse All"), false, () => SetExpanded(false));
660 }
661
662 void SetExpanded(bool value)
663 {
664 var panels = DebugManager.instance.panels;
665 foreach (var p in panels)
666 {
667 foreach (var w in p.children)
668 {
669 if (w.GetType() == typeof(DebugUI.Foldout))
670 {
671 if (m_WidgetStates.TryGetValue(w.queryPath, out DebugState state))
672 {
673 var foldout = (DebugUI.Foldout)w;
674 state.SetValue(value, foldout);
675 foldout.SetValue(value);
676 }
677 }
678 }
679 }
680 }
681 }
682
683#pragma warning restore 414
684}