Files
Test/Library/PackageCache/com.unity.render-pipelines.core@13460d8ea9e5/Runtime/Debugging/DebugUI.cs
T
2026-11-01 23:39:55 +02:00

822 lines
32 KiB
C#

#if ENABLE_UIELEMENTS_MODULE && UNITY_ENABLE_CHECKS
#define ENABLE_RENDERING_DEBUGGER_UI
#endif
using System;
using System.Collections.Generic;
using UnityEngine.Assertions;
#if ENABLE_RENDERING_DEBUGGER_UI
using UnityEngine.UIElements;
#endif
namespace UnityEngine.Rendering
{
#if ENABLE_RENDERING_DEBUGGER_UI
internal static class DebugUIExtensions
{
public static List<(Label, VisualElement)> CreatePanels(List<DebugUI.Panel> panels, DebugUI.Context context)
{
List<(Label, VisualElement)> returnList = new();
foreach (var p in panels)
{
var tab = new Label(p.displayName)
{
name = p.displayName + "_Tab",
focusable = true
};
tab.AddToClassList("debug-window-tab-item");
var panel = p.Create(context);
returnList.Add((tab, panel));
}
return returnList;
}
/// <summary>
/// Utility to add a scheduled item to the panel's tracker so it can be disabled when the widget is not visible.
/// </summary>
/// <param name="widget">Widget that contains the scheduler</param>
/// <param name="element">VisualElement whose scheduler is used to create the ScheduledItem</param>
/// <param name="scheduledItemCreator">Callback that creates the ScheduledItem.</param>
internal static void ScheduleTracked(this DebugUI.Widget widget, VisualElement element, Func<IVisualElementScheduledItem> scheduledItemCreator)
{
ScheduleTracked(widget, element, widget.m_Context, scheduledItemCreator);
}
/// <summary>
/// Utility to add a scheduled item to the panel's tracker, using an explicit context for Add/Remove
/// rather than reading widget.m_Context lazily. Use this when the same widget instance may be
/// rendered into multiple panels (which mutates m_Context), and you want this scheduled item
/// tracked under a specific context.
/// </summary>
internal static void ScheduleTracked(this DebugUI.Widget widget, VisualElement element, DebugUI.Context context, Func<IVisualElementScheduledItem> scheduledItemCreator)
{
element.RegisterCallback<AttachToPanelEvent>(_ =>
{
var scheduledItem = scheduledItemCreator.Invoke();
DebugManager.instance.schedulerTracker.Add(context, widget, scheduledItem);
scheduledItem.Pause(); // All schedulers start paused, and are enabled/disabled by DebugManager based on widget visibility.
});
element.RegisterCallback<DetachFromPanelEvent>(_ =>
{
DebugManager.instance.schedulerTracker.Remove(context, widget, element);
});
}
public static bool IsAnyRuntimeContext(this DebugUI.Context context)
{
return (context == DebugUI.Context.Runtime) ||
(context == DebugUI.Context.RuntimePersistent);
}
}
#endif
/// <summary>
/// The `DebugUI` class defines a collection of debug UI components that are useful for debugging and profiling Rendering features
/// </summary>
/// <remarks>
/// Widgets can be added to the UI, customized, and manipulated at runtime or during editor sessions. The class supports various
/// widget types, including buttons, read-only value fields, and progress bars. It also provides mechanisms for organizing these
/// widgets into containers. Each widget has a set of flags to control its visibility and behavior depending on whether it is
/// used in the editor or at runtime. The widgets can also contain callbacks for conditional visibility at runtime.
/// Important Notes:
/// - Widgets can be nested inside containers, allowing for organized groupings of debug UI elements.
/// - Widgets may be runtime-only, editor-only, or both, allowing them to behave differently depending on the application's
/// state (e.g., whether it is in the editor or playing at runtime).
/// - `DebugUI` also includes helper methods for widget initialization, such as compact initialization using the `NameAndTooltip` struct.
///
/// This API lets you do the following:
/// - Specify widget behavior such as "EditorOnly" and "RuntimeOnly".
/// - Show dynamic data with optional formatting.
/// - Specify delegate functions to show or hide widgets
///
/// <b>Related Resources:</b>
/// - [Debug UI Overview](https://docs.unity3d.com/6000.0/Documentation/Manual/urp/features/rendering-debugger.html)
/// - [Rendering Debugger Controls](https://docs.unity3d.com/6000.0/Documentation/Manual/urp/features/rendering-debugger-add-controls.html).
/// - [Using Rendering Debugger](https://docs.unity.cn/Packages/com.unity.render-pipelines.high-definition@16.0/manual/use-the-rendering-debugger.html).
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.Rendering;
///
/// [ExecuteInEditMode]
/// public class DebugUIExample : MonoBehaviour
/// {
/// void OnEnable()
/// {
/// // Create a button widget that logs a message when you select it
/// DebugUI.Button button = new DebugUI.Button
/// {
/// displayName = "Log Message Button",
/// action = () => Debug.Log("Button selected")
/// };
///
/// // Create a value widget that displays the current time
/// DebugUI.Value timeValue = new DebugUI.Value
/// {
/// // Set the display label
/// displayName = "Current Time",
///
/// // Set the format for the time
/// getter = () => System.DateTime.Now.ToString("HH:mm:ss"),
///
/// // Set the value to refresh every second
/// refreshRate = 1f
/// };
///
/// // Add widgets a new Rendering Debugger panel
/// var panel = DebugManager.instance.GetPanel("My Custom Panel", createIfNull: true);
///
/// // Add the widgets to the panel
/// panel.children.Add(button);
/// panel.children.Add(timeValue);
/// }
/// }
/// </code>
/// </example>
public partial class DebugUI
{
/// <summary>
/// A column of checkboxes for enabling and disabling flags.
/// </summary>
[Flags]
public enum Flags
{
/// <summary>
/// None.
/// </summary>
None = 0,
/// <summary>
/// This widget is Editor only.
/// </summary>
EditorOnly = 1 << 1,
/// <summary>
/// This widget is Runtime only.
/// </summary>
RuntimeOnly = 1 << 2,
/// <summary>
/// This widget will force the Debug Editor Window refresh.
/// </summary>
[Obsolete("This is no longer used. #from(6000.5)")]
EditorForceUpdate = 1 << 3,
/// <summary>
/// This widget will appear in the section "Frequently Used"
/// </summary>
[Obsolete("This is no longer used. #from(6000.5)")]
FrequentlyUsed = 1 << 4
}
/// <summary>
/// Context in which the widget is used.
/// </summary>
public enum Context
{
/// <summary>Invalid context</summary>
Invalid = -1,
/// <summary>Editor context</summary>
Editor = 0,
/// <summary>Runtime context</summary>
Runtime = 1,
/// <summary>Runtime persistent context</summary>
RuntimePersistent = 2,
/// <summary>Count</summary>
Count
}
/// <summary>
/// Base class for all debug UI widgets.
/// </summary>
public abstract class Widget
{
/// <summary>
/// The order of the widget
/// </summary>
public int order { get; set; } = 0;
// Set to null until it's added to a panel, be careful
/// <summary>
/// Panels containing the widget.
/// </summary>
protected Panel m_Panel;
/// <summary>
/// Panels containing the widget.
/// </summary>
public virtual Panel panel
{
get { return m_Panel; }
internal set { m_Panel = value; }
}
#if ENABLE_RENDERING_DEBUGGER_UI
/// <summary>
/// Creates the VisualElement for the widget that will be added to the panel UI.
/// </summary>
/// <returns>The widget's VisualElement</returns>
protected abstract VisualElement Create();
/// <summary>
/// Controls the way the widget is hidden.
/// When set to false (default), the widget is removed from the layout (DisplayStyle.None).
/// When set to true, the widget is not rendered but takes space in the layout (Visibility.Hidden).
/// </summary>
internal bool keepLayoutWhenHidden { private get; set; }
internal bool m_IsHiddenBySearchFilter;
internal string m_AdditionalSearchText = string.Empty;
protected internal bool m_RequiresLegacyStateHandling = false;
void UpdateElementVisibility()
{
if (m_IsHiddenBySearchFilter)
{
m_VisualElement.style.display = DisplayStyle.None;
return;
}
if (keepLayoutWhenHidden)
{
m_VisualElement.style.display = DisplayStyle.Flex;
m_VisualElement.style.visibility = isHidden ? Visibility.Hidden : Visibility.Visible;
}
else
{
m_VisualElement.style.display = isHidden ? DisplayStyle.None : DisplayStyle.Flex;
}
}
int ComputeIndentationLevel()
{
int indentLevel = 0;
Container currentParent = parent as Container;
while (currentParent != null)
{
indentLevel++;
currentParent = currentParent.parent as Container;
}
return indentLevel;
}
// Once the element is attached to the panel, recalculate the base field label width
// whenever the window is resized, or the tab/panel divider is moved.
internal void CustomAlignBaseFieldLabelWhenResized(VisualElement label)
{
if (m_Context == Context.Editor)
{
label.RegisterCallbackOnce<AttachToPanelEvent>(panelEvt =>
{
// Use the tab container element (we want to exclude tabs)
var contentPanel = panelEvt.destinationPanel.visualTree.Q<VisualElement>("debug-window-tab-container");
contentPanel.RegisterCallback<GeometryChangedEvent>(geometryEvt =>
{
CustomAlignBaseFieldLabel(label, geometryEvt.newRect.width);
});
});
}
}
// We need to do base field label alignment manually in two cases:
// 1. In runtime context, where UIE.BaseField<T> alignment logic is disabled
// 2. In any context, for elements that do not inherit from UIE.BaseField<T>, but we still want
// to align them, such as DebugUI.Value and DebugUI.ProgressBar.
internal void CustomAlignBaseFieldLabel(VisualElement label, float width = 0f)
{
if (label != null)
{
int indentationLevel = ComputeIndentationLevel();
if (m_Context == Context.Runtime)
{
// NOTE: In the runtime we don't care about the window width, just use a fixed label width.
const float kLabelBaseWidth = 330f;
const float kIndentSize = 8f; // Should match .debug-window-container-content margin-left
float totalPadding = indentationLevel*kIndentSize;
label.style.minWidth = kLabelBaseWidth - totalPadding;
}
else
{
// This is trying to match the BaseField field alignment logic as closely as possible.
// For the magic numbers & full logic see UIElements/Core/Controls/InputField/BaseField.cs
const float kLabelWidthRatio = 0.45f; // Should match UIElements.BaseField m_LabelWidthRatio
const float kLabelExtraPadding = 37.0f; // Should match UIElements.BaseField m_LabelExtraPadding
const float kIndentSize = 8f;
float totalPadding = indentationLevel*kIndentSize + kLabelExtraPadding;
label.style.minWidth = Mathf.Ceil(width * kLabelWidthRatio) - totalPadding;
}
}
}
internal VisualElement ToVisualElement(Context context)
{
m_Context = context;
m_VisualElement = Create();
if (m_VisualElement == null)
{
Debug.LogWarning($"Unable to create a Visual Element for type {GetType()}");
return null;
}
m_VisualElement.AddToClassList("unity-inspector-element");
m_VisualElement.name = displayName;
m_VisualElement.tooltip = tooltip;
#if UNITY_EDITOR
// Support for legacy state handling
if (this is ISupportsLegacyStateHandling legacyStateWidget)
{
m_RequiresLegacyStateHandling = legacyStateWidget.RequiresLegacyStateHandling();
}
#endif
UpdateElementVisibility();
this.ScheduleTracked(m_VisualElement, () => m_VisualElement.schedule.Execute(UpdateElementVisibility).Every(100));
// In runtime window, figure out indentation level to ensure field alignment
if (context == Context.Runtime)
{
m_VisualElement.RegisterCallbackOnce<AttachToPanelEvent>(_ =>
{
CustomAlignBaseFieldLabel(m_VisualElement.Q<Label>(classes: "unity-base-field__label"));
});
}
// In runtime UI, attach focus handler for keyboard navigation & pinning
if (context.IsAnyRuntimeContext())
{
VisualElement focusTarget = m_VisualElement;
if (this is Foldout)
{
focusTarget = m_VisualElement.Q<Toggle>();
}
focusTarget?.RegisterCallback<FocusInEvent>(evt =>
{
var targetElement = evt.target as VisualElement;
if (DebugManager.instance.m_RuntimeDebugWindow?.IsPopupOpen() ?? false)
{
if (targetElement != null)
{
var elementThatGetFocus = evt.target as VisualElement;
var focusController = targetElement.panel?.focusController;
if (focusController != null)
{
// Blur the entering focus to restore it to the previous one.
elementThatGetFocus.Blur();
}
}
evt.StopImmediatePropagation();
}
else if (targetElement != null)
{
// Ensure focused element is visible in the scroll view
var scrollView = targetElement.GetFirstAncestorOfType<ScrollView>();
scrollView.ScrollTo(targetElement);
}
}, TrickleDown.TrickleDown);
focusTarget?.RegisterCallback<FocusEvent>(_ =>
{
DebugManager.instance.selectedWidget = this;
});
}
return m_VisualElement;
}
// Root visual element of the widget, valid after it has been initialized by calling ToVisualElement.
internal VisualElement m_VisualElement = null;
// Context of the widget, valid after it has been initialized by calling ToVisualElement.
internal Context m_Context = Context.Invalid;
#endif
/// <summary>
/// Increment button implementation for numeric fields
/// </summary>
/// <param name="fast">Whether to use fast increment</param>
internal virtual void OnIncrement(bool fast) { }
/// <summary>
/// Decrement button implementation for numeric fields
/// </summary>
/// <param name="fast">Whether to use fast decrement</param>
internal virtual void OnDecrement(bool fast) { }
/// <summary>
/// Parent container.
/// </summary>
protected IContainer m_Parent;
/// <summary>
/// Parent container.
/// </summary>
public virtual IContainer parent
{
get { return m_Parent; }
internal set { m_Parent = value; }
}
/// <summary>
/// Flags for the widget.
/// </summary>
public Flags flags { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string displayName { get; set; }
/// <summary>
/// Tooltip.
/// </summary>
public string tooltip { get; set; }
/// <summary>
/// Path of the widget.
/// </summary>
public string queryPath { get; private set; }
/// <summary>
/// True if the widget is Editor only.
/// </summary>
public bool isEditorOnly => flags.HasFlag(Flags.EditorOnly);
/// <summary>
/// True if the widget is Runtime only.
/// </summary>
public bool isRuntimeOnly => flags.HasFlag(Flags.RuntimeOnly);
/// <summary>
/// True if the widget is inactive in the editor (i.e. widget is runtime only and the application is not 'Playing').
/// </summary>
public bool isInactiveInEditor => (isRuntimeOnly && !Application.isPlaying);
/// <summary>
/// Optional delegate that can be used to conditionally hide widgets at runtime (e.g. due to state of other widgets).
/// </summary>
public Func<bool> isHiddenCallback;
/// <summary>
/// If <see cref="isHiddenCallback">shouldHideDelegate</see> has been set and returns true, the widget is hidden from the UI.
/// </summary>
public bool isHidden => isHiddenCallback?.Invoke() ?? false;
internal virtual void GenerateQueryPath()
{
queryPath = displayName.Trim();
if (m_Parent != null)
queryPath = m_Parent.queryPath + " -> " + queryPath;
}
/// <summary>
/// Helper struct to allow more compact initialization of widgets.
/// </summary>
public struct NameAndTooltip
{
/// <summary>
/// The name
/// </summary>
public string name;
/// <summary>
/// The tooltip
/// </summary>
public string tooltip;
}
/// <summary>
/// Helper setter to allow more compact initialization of widgets.
/// </summary>
public NameAndTooltip nameAndTooltip
{
set
{
displayName = value.name;
tooltip = value.tooltip;
}
}
}
/// <summary>
/// Interface for widgets that can contain other widgets.
/// </summary>
public interface IContainer
{
/// <summary>
/// List of children of the container.
/// </summary>
ObservableList<Widget> children { get; }
/// <summary>
/// Display name of the container.
/// </summary>
string displayName { get; set; }
/// <summary>
/// Path of the container.
/// </summary>
string queryPath { get; }
}
/// <summary>
/// Any widget that implements this will be considered for serialization (only if the setter is set and thus is not read-only)
/// </summary>
[Obsolete("This interface is no longer used. #from(6000.5)")]
public interface IValueField
{
/// <summary>
/// Return the value of the field.
/// </summary>
/// <returns>Value of the field.</returns>
[Obsolete("This method is no longer used. #from(6000.5)")]
object GetValue();
/// <summary>
/// Set the value of the field.
/// </summary>
/// <param name="value">Input value.</param>
[Obsolete("This method is no longer used. #from(6000.5)")]
void SetValue(object value);
/// <summary>
/// Function used to validate the value when setting it.
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>Validated value.</returns>
[Obsolete("This method is no longer used. #from(6000.5)")]
object ValidateValue(object value);
}
// Miscellaneous
/// <summary>
/// Button widget.
/// </summary>
public class Button : Widget
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var button = new UIElements.Button
{
text = displayName
};
button.AddToClassList("debug-window-button");
button.AddToClassList("debug-window-search-filter-target");
button.clicked += () => action();
return button;
}
#endif
/// <summary>
/// Action performed by the button.
/// </summary>
public Action action { get; set; }
}
/// <summary>
/// A field that displays a read-only value.
/// </summary>
public class Value : Widget
{
/// <summary>
/// If is a header, and it should be displayed with bold font
/// </summary>
public bool isHeader { get; set; }
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var container = new VisualElement();
container.AddToClassList("debug-window-value-container");
var nameLabel = new Label() { text = displayName };
nameLabel.AddToClassList("debug-window-value-name");
nameLabel.AddToClassList("debug-window-search-filter-target");
var valueLabel = new Label() { text = FormatString(GetValue()) };
valueLabel.AddToClassList("debug-window-value-value");
valueLabel.focusable = true;
if (isHeader)
{
valueLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
}
this.ScheduleTracked(valueLabel, () => valueLabel.schedule.Execute(() =>
{
valueLabel.text = FormatString(GetValue());
}).Every((long)(refreshRate * 1000.0f)));
CustomAlignBaseFieldLabelWhenResized(nameLabel);
container.Add(nameLabel);
container.Add(valueLabel);
return container;
}
#endif
/// <summary>
/// Getter for the Value.
/// </summary>
public Func<object> getter { get; set; }
/// <summary>
/// Refresh rate for the read-only value (runtime only)
/// </summary>
public float refreshRate = 0.1f;
/// <summary>
/// Optional C# numeric format string, using following syntax: "{0[:numericFormatString]}"
/// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings
/// and https://docs.microsoft.com/en-us/dotnet/standard/base-types/composite-formatting
/// Example: 123.45678 with formatString "{0:F2} ms" --> "123.45 ms".
/// </summary>
public string formatString = null;
/// <summary>
/// Constructor.
/// </summary>
public Value()
{
displayName = "";
}
/// <summary>
/// Returns the value of the widget.
/// </summary>
/// <returns>The value of the widget.</returns>
public virtual object GetValue()
{
Assert.IsNotNull(getter);
return getter();
}
/// <summary>
/// Returns the formatted value string for display purposes.
/// </summary>
/// <param name="value">Value to be formatted.</param>
/// <returns>The formatted value string.</returns>
public virtual string FormatString(object value)
{
return string.IsNullOrEmpty(formatString) ? $"{value}" : string.Format(formatString, value);
}
}
/// <summary>
/// A progress bar that displays values between 0% and 100%.
/// </summary>
public class ProgressBarValue : Value
{
/// <summary>
/// Minimum value.
/// </summary>
public float min = 0f;
/// <summary>
/// Maximum value.
/// </summary>
public float max = 1f;
/// <summary>
/// Get the current progress string, remapped to [0, 1] range, representing the progress between min and max.
/// </summary>
/// <param name="value">Value to be formatted.</param>
/// <returns>Formatted progress percentage string between 0% and 100%.</returns>
public override string FormatString(object value)
{
static float Remap01(float v, float x0, float y0) => (v - x0) / (y0 - x0);
float clamped = Mathf.Clamp((float)value, min, max);
float percentage = Remap01(clamped, min, max);
return $"{percentage:P1}";
}
#if ENABLE_RENDERING_DEBUGGER_UI
protected override VisualElement Create()
{
var container = new VisualElement();
container.AddToClassList(UIElements.BaseField<float>.ussClassName);
container.style.flexDirection = FlexDirection.Row;
var label = new Label();
label.AddToClassList(UIElements.BaseField<float>.labelUssClassName);
label.text = displayName;
container.Add(label);
var progressBar = new UIElements.ProgressBar()
{
lowValue = min,
highValue = max
};
progressBar.style.flexGrow = 1;
progressBar.schedule.Execute(() =>
{
progressBar.title = FormatString(GetValue());
progressBar.value = (float)GetValue();
}).Every((long)(refreshRate * 1000.0f));
CustomAlignBaseFieldLabelWhenResized(label);
container.AddToClassList(UIElements.BaseField<float>.alignedFieldUssClassName);
container.Add(progressBar);
return container;
}
#endif
}
/// <summary>
/// An array of read-only values that Unity displays in a horizontal row.
/// </summary>
public class ValueTuple : Widget
{
internal const int k_LabelWidthEditor = 280;
internal const int k_LabelWidthRuntime = 340;
internal static int GetLabelWidth(Context ctx) => ctx == Context.Editor ? k_LabelWidthEditor : k_LabelWidthRuntime;
/// <summary>
/// If is a header, and it should be displayed with bold font
/// </summary>
public bool isHeader { get; set; }
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var valueContainer = new UIElements.VisualElement();
valueContainer.AddToClassList("debug-window-valuetuple");
var label = new Label(displayName) { style = { minWidth = ValueTuple.GetLabelWidth(m_Context) }, };
label.AddToClassList("debug-window-search-filter-target");
if (isHeader)
label.style.unityFontStyleAndWeight = FontStyle.Bold;
valueContainer.Add(label);
if (!isHeader)
valueContainer.focusable = true;
foreach (var value in values)
{
value.isHeader = isHeader;
var valueVisualElement = value.ToVisualElement(m_Context);
var valueLabel = valueVisualElement.Q<Label>(className: "debug-window-value-name");
if (valueLabel != null)
valueLabel.style.display = DisplayStyle.None; // With ValueTuples we don't want to show individual value names
valueContainer.Add(valueVisualElement);
}
return valueContainer;
}
#endif
/// <summary>
/// Number of elements in the tuple.
/// </summary>
public int numElements
{
get
{
Assert.IsTrue(values.Length > 0);
return values.Length;
}
}
/// <summary>
/// Value widgets.
/// </summary>
public Value[] values;
/// <summary>
/// Refresh rate for the read-only values (runtime only)
/// </summary>
public float refreshRate
{
get
{
if (values != null && values.Length > 0 && values[0] != null)
{
return values[0].refreshRate;
}
return 0.1f;
}
}
/// <summary>
/// The currently pinned element index, or -1 if none are pinned.
/// </summary>
public int pinnedElementIndex = -1;
}
}
}