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}