492 lines
17 KiB
C#
492 lines
17 KiB
C#
#if ENABLE_UIELEMENTS_MODULE && UNITY_ENABLE_CHECKS
|
|
#define ENABLE_RENDERING_DEBUGGER_UI
|
|
#endif
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
namespace UnityEngine.Rendering
|
|
{
|
|
/// <summary>
|
|
/// Implementing this interface enables integration with Unity's Rendering debugger, providing a way to manage and control the state of debug data.
|
|
/// </summary>
|
|
///
|
|
/// <remarks>
|
|
/// Use the `IDebugData` interface to register custom debug data. You can reset the data when necessary, which makes it suitable for debugging scenarios
|
|
/// where you need to clear or reset specific data. For example, when the application state changes or during gameplay session resets,
|
|
/// or when the **Reset** button is selected in the **Rendering Debugger** window in the Editor or at runtime.
|
|
/// </remarks>
|
|
///
|
|
/// <example>
|
|
/// <code>
|
|
/// public class MyDebugData : IDebugData
|
|
/// {
|
|
/// private int _value;
|
|
///
|
|
/// /// Constructor of the debug data that will receive the reset callback
|
|
/// public MyDebugData()
|
|
/// {
|
|
/// _value = 0;
|
|
/// }
|
|
///
|
|
/// public Action GetReset()
|
|
/// {
|
|
/// /// Specify the callback when the reset operation is being called.
|
|
/// return () => _value = 0; // Resets the value to 0
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public interface IDebugData
|
|
{
|
|
/// <summary>Provides the reset callback for resetting the debug data.</summary>
|
|
/// <returns>The reset callback</returns>
|
|
Action GetReset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The <see cref="DebugManager"/> class provides a centralized manager for handling Unity's Rendering debugger.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The DebugManager allows you to register, unregister, and manipulate debug data, panels, and widgets for runtime or editor debugging.
|
|
/// - Register and manage debug panels and widgets.
|
|
/// - Refresh and reset the debug UI based on runtime changes.
|
|
/// - Provides a global instance for easy access from anywhere in the codebase.
|
|
/// </remarks>
|
|
///
|
|
/// <example>
|
|
/// <code>
|
|
/// {
|
|
/// /// Create a list to store the new DebugUI widgets
|
|
/// /// by creating different DebugUI.Values that display a label with a value.
|
|
/// var list = new System.Collections.Generic.List<DebugUI.Widget>
|
|
/// {
|
|
/// new DebugUI.Value { displayName = "Lighting Intensity", getter = () => 1.0f, setter = value => Debug.Log($"Lighting Intensity set to {value}") },
|
|
/// new DebugUI.Value { displayName = "Light Color", getter = () => Color.white, setter = value => Debug.Log($"Light Color set to {value}") }
|
|
/// };
|
|
/// var items = list.ToArray();
|
|
///
|
|
/// /// Obtain the panel from the DebugManager instance, and add the Widgets that we want to display there.
|
|
/// var panel = DebugManager.instance.GetPanel("Lighting", true);
|
|
/// panel.children.AddRange(items);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public sealed partial class DebugManager
|
|
{
|
|
static readonly Lazy<DebugManager> s_Instance = new Lazy<DebugManager>(() => new DebugManager());
|
|
/// <summary>
|
|
/// Global instance of the DebugManager.
|
|
/// </summary>
|
|
public static DebugManager instance => s_Instance.Value;
|
|
|
|
ReadOnlyCollection<DebugUI.Panel> m_ReadOnlyPanels;
|
|
readonly List<DebugUI.Panel> m_Panels = new List<DebugUI.Panel>();
|
|
|
|
void UpdateReadOnlyCollection()
|
|
{
|
|
m_Panels.Sort();
|
|
m_ReadOnlyPanels = m_Panels.AsReadOnly();
|
|
}
|
|
|
|
/// <summary>
|
|
/// List of currently registered debug panels.
|
|
/// </summary>
|
|
public ReadOnlyCollection<DebugUI.Panel> panels
|
|
{
|
|
get
|
|
{
|
|
if (m_ReadOnlyPanels == null)
|
|
UpdateReadOnlyCollection();
|
|
return m_ReadOnlyPanels;
|
|
}
|
|
}
|
|
|
|
void ForEachWidgetInContainer(DebugUI.Widget widget, Action<DebugUI.Widget> action)
|
|
{
|
|
action(widget);
|
|
|
|
if (widget is DebugUI.IContainer container)
|
|
{
|
|
foreach (var child in container.children)
|
|
ForEachWidgetInContainer(child, action);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterate over all widgets in all panels and invoke the provided action on each widget.
|
|
/// </summary>
|
|
/// <param name="action">Action to invoke.</param>
|
|
internal void ForEachWidget(Action<DebugUI.Widget> action)
|
|
{
|
|
foreach (var panel in m_Panels)
|
|
foreach (var widget in panel.children)
|
|
ForEachWidgetInContainer(widget, action);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback called when the runtime UI changed.
|
|
/// </summary>
|
|
public event Action<bool> onDisplayRuntimeUIChanged = delegate { };
|
|
|
|
/// <summary>
|
|
/// Callback called when the debug window is dirty.
|
|
/// </summary>
|
|
public event Action onSetDirty = delegate { };
|
|
|
|
event Action resetData;
|
|
|
|
/// <summary>
|
|
/// Event invoked when debug UI needs to be recreated (panels torn down and rebuilt).
|
|
/// </summary>
|
|
internal event Action onRecreateDebugUI;
|
|
|
|
/// <summary>
|
|
/// Event invoked when a panel selection is requested.
|
|
/// </summary>
|
|
internal event Action<string> onPanelSelectionRequested;
|
|
|
|
string m_RequestedPanel;
|
|
|
|
#if ENABLE_RENDERING_DEBUGGER_UI
|
|
internal RuntimeDebugWindow m_RuntimeDebugWindow;
|
|
RuntimePersistentDebugUI m_RuntimePersistentDebugUI;
|
|
|
|
internal SchedulerTracker schedulerTracker { get; } = new();
|
|
#endif
|
|
|
|
internal DebugUI.Widget selectedWidget { get; set; }
|
|
|
|
/// <summary>
|
|
/// Is any debug window or UI currently active.
|
|
/// </summary>
|
|
public bool isAnyDebugUIActive
|
|
{
|
|
get
|
|
{
|
|
return
|
|
displayRuntimeUI || displayPersistentRuntimeUI
|
|
#if UNITY_EDITOR
|
|
|| displayEditorUI
|
|
#endif
|
|
;
|
|
}
|
|
}
|
|
|
|
DebugManager()
|
|
{
|
|
#if UNITY_ENABLE_CHECKS
|
|
RegisterDebugInputs();
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recreates the debug UI for all panels. Use this when the panel structure needs to be rebuilt
|
|
/// (e.g., when widgets need to be added/removed dynamically based on feature availability).
|
|
/// For simple value refreshes, the UI automatically updates through widget getters.
|
|
/// </summary>
|
|
public void RecreateDebugUI()
|
|
{
|
|
onRecreateDebugUI?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset the debug window.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
DebugDisplaySerializer.SaveFoldoutStates();
|
|
DebugDisplaySerializer.Clear();
|
|
resetData?.Invoke();
|
|
#if ENABLE_RENDERING_DEBUGGER_UI
|
|
displayPersistentRuntimeUI = false;
|
|
ForEachWidget(w => { if (w is DebugUI.ValueTuple vt) vt.pinnedElementIndex = -1; });
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register debug data.
|
|
/// </summary>
|
|
/// <param name="data">Data to be registered.</param>
|
|
public void RegisterData(IDebugData data) => resetData += data.GetReset();
|
|
|
|
/// <summary>
|
|
/// Register debug data.
|
|
/// </summary>
|
|
/// <param name="data">Data to be registered.</param>
|
|
public void UnregisterData(IDebugData data) => resetData -= data.GetReset();
|
|
|
|
int GetMaxPinnedIndex(DebugUI.Container container)
|
|
{
|
|
int pinnedIndex = -1;
|
|
foreach (var child in container.children)
|
|
{
|
|
if (child is DebugUI.ValueTuple valueTuple && valueTuple.pinnedElementIndex > pinnedIndex)
|
|
{
|
|
pinnedIndex = valueTuple.pinnedElementIndex;
|
|
}
|
|
}
|
|
return pinnedIndex;
|
|
}
|
|
|
|
internal void TogglePersistent()
|
|
{
|
|
if (selectedWidget != null)
|
|
TogglePersistent(selectedWidget);
|
|
}
|
|
|
|
void TogglePersistent(DebugUI.Widget widget, int? forceTupleIndex = null)
|
|
{
|
|
#if ENABLE_RENDERING_DEBUGGER_UI
|
|
if (widget == null)
|
|
return;
|
|
|
|
displayPersistentRuntimeUI = true;
|
|
|
|
|
|
switch (widget)
|
|
{
|
|
case DebugUI.Value value:
|
|
m_RuntimePersistentDebugUI.Toggle(value);
|
|
break;
|
|
case DebugUI.ValueTuple valueTuple:
|
|
m_RuntimePersistentDebugUI.Toggle(valueTuple, forceTupleIndex);
|
|
break;
|
|
case DebugUI.Container container:
|
|
// When container is toggled, we make sure that if there are ValueTuples, they all get the same element index.
|
|
int pinnedIndex = GetMaxPinnedIndex(container);
|
|
foreach (var child in container.children)
|
|
{
|
|
if (child is DebugUI.Value || child is DebugUI.ValueTuple)
|
|
TogglePersistent(child, pinnedIndex);
|
|
}
|
|
break;
|
|
default:
|
|
Debug.Log("Only readonly items can be made persistent.");
|
|
break;
|
|
}
|
|
|
|
if (m_RuntimePersistentDebugUI != null && m_RuntimePersistentDebugUI.IsEmpty())
|
|
displayPersistentRuntimeUI = false;
|
|
#endif
|
|
}
|
|
|
|
void OnPanelDirty(DebugUI.Panel panel)
|
|
{
|
|
onSetDirty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the panel index
|
|
/// </summary>
|
|
/// <param name="displayName">The displayname for the panel</param>
|
|
/// <returns>The index for the panel or -1 if not found.</returns>
|
|
public int PanelIndex([DisallowNull] string displayName)
|
|
{
|
|
displayName ??= string.Empty;
|
|
|
|
for (int i = 0; i < m_Panels.Count; ++i)
|
|
{
|
|
if (displayName.Equals(m_Panels[i].displayName, StringComparison.InvariantCultureIgnoreCase))
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the panel display name
|
|
/// </summary>
|
|
/// <param name="panelIndex">The panelIndex for the panel to get the name</param>
|
|
/// <returns>The display name of the panel, or empty string otherwise</returns>
|
|
public string PanelDisplayName(int panelIndex)
|
|
{
|
|
if (panelIndex < 0 || panelIndex > m_Panels.Count - 1)
|
|
return string.Empty;
|
|
|
|
return m_Panels[panelIndex].displayName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request DebugWindow to open the specified panel.
|
|
/// </summary>
|
|
/// <param name="panelName">Name of window panel to activate.</param>
|
|
public void RequestPanelSelection(string panelName)
|
|
{
|
|
int panelIndex = FindPanelIndex(panelName);
|
|
if (panelIndex != -1)
|
|
{
|
|
onPanelSelectionRequested?.Invoke(panelName);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"No panel with name {panelName} has been registered.");
|
|
}
|
|
}
|
|
|
|
// TODO: Optimally we should use a query path here instead of a display name
|
|
/// <summary>
|
|
/// Returns a debug panel.
|
|
/// </summary>
|
|
/// <param name="displayName">Name of the debug panel.</param>
|
|
/// <param name="createIfNull">Create the panel if it does not exists.</param>
|
|
/// <param name="groupIndex">Group index.</param>
|
|
/// <param name="overrideIfExist">Replace an existing panel.</param>
|
|
/// <returns>The requested debug panel or null if it does not exist and createIfNull is set to false</returns>
|
|
public DebugUI.Panel GetPanel(string displayName, bool createIfNull = false, int groupIndex = 0, bool overrideIfExist = false)
|
|
{
|
|
int panelIndex = PanelIndex(displayName);
|
|
DebugUI.Panel p = panelIndex >= 0 ? m_Panels[panelIndex] : null;
|
|
|
|
if (p != null)
|
|
{
|
|
if (overrideIfExist)
|
|
{
|
|
p.onSetDirty -= OnPanelDirty;
|
|
RemovePanel(p);
|
|
p = null;
|
|
}
|
|
else
|
|
return p;
|
|
}
|
|
|
|
if (createIfNull)
|
|
{
|
|
p = new DebugUI.Panel { displayName = displayName, groupIndex = groupIndex };
|
|
p.onSetDirty += OnPanelDirty;
|
|
m_Panels.Add(p);
|
|
UpdateReadOnlyCollection();
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the index of the panel from it's display name.
|
|
/// </summary>
|
|
/// <param name="displayName">The display name of the panel to find.</param>
|
|
/// <returns>The index of the panel in the list. -1 if not found.</returns>
|
|
public int FindPanelIndex(string displayName)
|
|
=> m_Panels.FindIndex(p => p.displayName == displayName);
|
|
|
|
// TODO: Use a query path here as well instead of a display name
|
|
/// <summary>
|
|
/// Remove a debug panel.
|
|
/// </summary>
|
|
/// <param name="displayName">Name of the debug panel to remove.</param>
|
|
public void RemovePanel(string displayName)
|
|
{
|
|
DebugUI.Panel panel = null;
|
|
|
|
foreach (var p in m_Panels)
|
|
{
|
|
if (p.displayName == displayName)
|
|
{
|
|
p.onSetDirty -= OnPanelDirty;
|
|
panel = p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
RemovePanel(panel);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a debug panel.
|
|
/// </summary>
|
|
/// <param name="panel">Reference to the debug panel to remove.</param>
|
|
public void RemovePanel(DebugUI.Panel panel)
|
|
{
|
|
if (panel == null)
|
|
return;
|
|
|
|
m_Panels.Remove(panel);
|
|
UpdateReadOnlyCollection();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="DebugUI.Widget[]"/> matching the given <see cref="DebugUI.Flags"/>
|
|
/// </summary>
|
|
/// <param name="flags">The flags of the widget</param>
|
|
/// <returns>Reference to the requested debug item.</returns>
|
|
public DebugUI.Widget[] GetItems(DebugUI.Flags flags)
|
|
{
|
|
using (ListPool<DebugUI.Widget>.Get(out var temp))
|
|
{
|
|
foreach (var panel in m_Panels)
|
|
{
|
|
var widgets = GetItemsFromContainer(flags, panel);
|
|
temp.AddRange(widgets);
|
|
}
|
|
|
|
return temp.ToArray();
|
|
}
|
|
}
|
|
|
|
internal DebugUI.Widget[] GetItemsFromContainer(DebugUI.Flags flags, DebugUI.IContainer container)
|
|
{
|
|
using (ListPool<DebugUI.Widget>.Get(out var temp))
|
|
{
|
|
foreach (var child in container.children)
|
|
{
|
|
if (child.flags.HasFlag(flags))
|
|
{
|
|
temp.Add(child);
|
|
continue;
|
|
}
|
|
|
|
if (child is DebugUI.IContainer containerChild)
|
|
{
|
|
temp.AddRange(GetItemsFromContainer(flags, containerChild));
|
|
}
|
|
}
|
|
|
|
return temp.ToArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a Debug Item.
|
|
/// </summary>
|
|
/// <param name="queryPath">Path of the debug item.</param>
|
|
/// <returns>Reference to the requested debug item.</returns>
|
|
public DebugUI.Widget GetItem(string queryPath)
|
|
{
|
|
foreach (var panel in m_Panels)
|
|
{
|
|
var w = GetItem(queryPath, panel);
|
|
if (w != null)
|
|
return w;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a debug item from a specific container.
|
|
/// </summary>
|
|
/// <param name="queryPath">Path of the debug item.</param>
|
|
/// <param name="container">Container to query.</param>
|
|
/// <returns>Reference to the requested debug item.</returns>
|
|
DebugUI.Widget GetItem(string queryPath, DebugUI.IContainer container)
|
|
{
|
|
foreach (var child in container.children)
|
|
{
|
|
if (child.queryPath == queryPath)
|
|
return child;
|
|
|
|
if (child is DebugUI.IContainer containerChild)
|
|
{
|
|
var w = GetItem(queryPath, containerChild);
|
|
if (w != null)
|
|
return w;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|