// When defined, ProfilingSampler creates ProfilerRecorder instances to expose per-frame CPU/GPU // timing and sample-count properties (cpuElapsedTime, gpuElapsedTime, inlineCpuElapsedTime, …). // Undefine to strip all recorder allocations from builds where profiling overhead is unacceptable. #define UNITY_USE_RECORDER using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Profiling; using Unity.Profiling.LowLevel; namespace UnityEngine.Rendering { class TProfilingSampler : ProfilingSampler where TEnum : Enum { internal static readonly Dictionary> samples = new Dictionary>(); static TProfilingSampler() { var names = Enum.GetNames(typeof(TEnum)); var values = Enum.GetValues(typeof(TEnum)); for (int i = 0; i < names.Length; i++) { var sample = new TProfilingSampler(names[i]); samples.Add((TEnum)values.GetValue(i), sample); } } public TProfilingSampler(string name) : base(name) { } } /// /// Wraps a ProfilerMarker and its associated ProfilerRecorder instances /// to provide per-frame CPU and GPU timing for a named profiling scope. /// Use together with to bracket code you want to measure. /// /// /// Timing properties (, , /// , and their sample-count counterparts) are only /// populated after is set to true and at least one /// frame has elapsed. /// ProfilerRecorder instances are allocated lazily on the first = true call; /// samplers that never enable recording hold no native resources. /// Recording support requires the UNITY_USE_RECORDER define (enabled by default); /// without it all timing properties return zero and no ProfilerRecorder is ever allocated. /// [IgnoredByDeepProfiler] public class ProfilingSampler : IDisposable { /// /// Get the sampler for the corresponding enumeration value. /// /// Type of the enumeration. /// Enumeration value. /// The for the given enumeration value, /// or null in non-development Player builds. public static ProfilingSampler Get(TEnum marker) where TEnum : Enum { #if ENABLE_PROFILER TProfilingSampler.samples.TryGetValue(marker, out var sampler); return sampler; #else return null; #endif } /// /// Creates a new under ProfilerCategory.Render /// with the specified MarkerFlags. /// /// /// Registers the following ProfilerMarkers: /// /// /// — The primary marker. Appears in the Profiler under /// ProfilerCategory.Render. Drives , , /// and (when GPU sampling is active) and . /// Use the primary marker name to measure CommandBuffer execution timings on the render thread. /// /// /// "Inl_" — The inline marker. Also under ProfilerCategory.Render, /// but without SampleGPU. Sampled directly by and on the /// calling thread. Drives and . /// /// /// When no verbosity bits (bits 10–12) are set in , /// MarkerFlags.SampleGPU is added to the primary marker automatically so that GPU timing is /// captured by default for user-visible markers. /// Pass any Verbosity* flag (e.g. MarkerFlags.VerbosityAdvanced) to suppress GPU recording. /// ProfilerRecorder instances (which hold native handles) are not allocated in the constructor; /// they are created lazily the first time is set to true. /// Samplers that never enable recording add no native-resource overhead. /// /// Name of the profiling sampler. /// Verbosity flags (e.g. MarkerFlags.Default, /// MarkerFlags.VerbosityAdvanced). /// A new , or null in non-development Player builds. public static ProfilingSampler Create(string name, MarkerFlags flags) { #if ENABLE_PROFILER return new ProfilingSampler(name, flags); #else return null; #endif } /// /// Creates a with under /// ProfilerCategory.Render. MarkerFlags.SampleGPU is added /// automatically because no verbosity level is set. /// /// Name shown in the Unity Profiler for this marker. public ProfilingSampler(string name) : this(name, MarkerFlags.Default) { } /// /// Creates a with the specified name and flags under ProfilerCategory.Render. /// Private to ensure we only create ProfilingSamplers with the Create() method, /// which handles the ENABLE_PROFILER conditional and ensures no overhead in non-development Players. /// /// Name shown in the Unity Profiler for this marker. /// Marker flags controlling verbosity and GPU sampling. private ProfilingSampler(string name, MarkerFlags flags) { #if ENABLE_PROFILER #if UNITY_USE_RECORDER const MarkerFlags k_VerbosityMask = (MarkerFlags)0x1C00; var renderFlags = (flags & k_VerbosityMask) == 0 ? flags | MarkerFlags.SampleGPU : flags; m_SampleGpu = (renderFlags & MarkerFlags.SampleGPU) != 0; #else var renderFlags = flags; #endif m_Marker = new ProfilerMarker(ProfilerCategory.Render, name, renderFlags); m_InlineMarker = new ProfilerMarker(ProfilerCategory.Render, $"Inl_{name}", flags); #endif this.name = name; // ProfilerRecorder allocation deferred to first enableRecording = true. } /// /// Begins the profiling block. Records both a command-buffer marker (if /// is non-null) and an inline CPU marker. /// /// Command buffer to receive the GPU-visible marker. /// Pass null for CPU-only inline profiling. [Conditional("ENABLE_PROFILER")] [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)] public void Begin(CommandBuffer cmd) { #if ENABLE_PROFILER cmd?.BeginSample(m_Marker); m_InlineMarker.Begin(); #endif } /// /// Begins the profiling block with a Unity Object context associated with the sample. /// Records both a command-buffer marker (if is non-null) and an /// inline CPU marker. /// /// Command buffer to receive the GPU-visible marker. /// Pass null for CPU-only inline profiling. /// Unity Object (e.g. Texture, Mesh, Material) to associate /// with this sample. The Profiler displays it in the sample hierarchy. [Conditional("ENABLE_PROFILER")] [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)] public void Begin(CommandBuffer cmd, Object contextObject) { #if ENABLE_PROFILER cmd?.BeginSample(m_Marker, contextObject); m_InlineMarker.Begin(contextObject); #endif } /// /// Ends the profiling block started by call. /// /// The same command buffer passed to call, /// or null if no command buffer was used. [Conditional("ENABLE_PROFILER")] [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)] public void End(CommandBuffer cmd) { #if ENABLE_PROFILER cmd?.EndSample(m_Marker); m_InlineMarker.End(); #endif } internal bool IsValid() { #if ENABLE_PROFILER return m_Marker.Handle != IntPtr.Zero; #else return false; #endif } #if ENABLE_PROFILER internal ProfilerMarker m_Marker; internal ProfilerMarker m_InlineMarker; #if UNITY_USE_RECORDER internal ProfilerRecorder m_Recorder; internal ProfilerRecorder m_GpuRecorder; internal ProfilerRecorder m_InlineRecorder; readonly bool m_SampleGpu; #endif #endif /// /// Name of this sampler as it appears in the Unity Profiler. /// public string name { get; private set; } /// /// Enables or disables ProfilerRecorder data collection for this sampler. /// /// /// Setting to true for the first time allocates the underlying ProfilerRecorder instances /// and starts collection; timing properties reflect the previous frame's data after /// at least one frame has elapsed. /// Setting back to false stops collection but keeps the recorders allocated for reuse. /// Has no effect when UNITY_USE_RECORDER is not defined. /// public bool enableRecording { set { #if UNITY_USE_RECORDER && ENABLE_PROFILER if (value) { const ProfilerRecorderOptions recorderDefaultOptions = ProfilerRecorderOptions.WrapAroundWhenCapacityReached | ProfilerRecorderOptions.SumAllSamplesInFrame | // TODO: Shared recorder flag. Remove once we transition fully from Enums to static ProfilerSamplers. // Allows to reuse the same underlying recorder for multiple ProfilingSampler instances with the same marker, // which is important to avoid disruptions in Graphics Performance Tests during transition. (ProfilerRecorderOptions)(1 << 7); if (!m_Recorder.Valid) { m_Recorder = new ProfilerRecorder(m_Marker, 1, recorderDefaultOptions); if (m_SampleGpu) m_GpuRecorder = new ProfilerRecorder(m_Marker, 1, recorderDefaultOptions | ProfilerRecorderOptions.GpuRecorder); m_InlineRecorder = new ProfilerRecorder(m_InlineMarker, 1, recorderDefaultOptions); } m_Recorder.Start(); if (m_GpuRecorder.Valid) m_GpuRecorder.Start(); m_InlineRecorder.Start(); } else { if (m_Recorder.Valid) m_Recorder.Stop(); if (m_GpuRecorder.Valid) m_GpuRecorder.Stop(); if (m_InlineRecorder.Valid) m_InlineRecorder.Stop(); } #endif } } /// /// Releases the native ProfilerRecorder resources held by this sampler. /// This is a no-op if was never set to true. /// public void Dispose() { #if ENABLE_PROFILER Dispose(true); GC.SuppressFinalize(this); #endif } #if ENABLE_PROFILER /// Finalizer — releases recorders when no explicit call was made. ~ProfilingSampler() => Dispose(false); #endif void Dispose(bool disposing) { #if UNITY_USE_RECORDER && ENABLE_PROFILER if (m_Recorder.Valid) m_Recorder.Dispose(); if (m_GpuRecorder.Valid) m_GpuRecorder.Dispose(); if (m_InlineRecorder.Valid) m_InlineRecorder.Dispose(); #endif } #if UNITY_USE_RECORDER && ENABLE_PROFILER const float k_NanosecondsToMilliseconds = 1.0f / 1000000.0f; /// /// GPU elapsed time in milliseconds for the previous frame. /// /// /// Only non-zero for user-visible markers (those created without verbosity flags), because /// MarkerFlags.SampleGPU is only added automatically in that case. /// Always returns 0 for markers created with a Verbosity* flag. /// public float gpuElapsedTime => (m_GpuRecorder.Valid && m_GpuRecorder.IsRunning) ? m_GpuRecorder.LastValue * k_NanosecondsToMilliseconds : 0.0f; /// /// Number of times this sampler was hit on the GPU in the previous frame. /// /// Subject to the same MarkerFlags.SampleGPU condition as . public int gpuSampleCount => (m_GpuRecorder.Valid && m_GpuRecorder.IsRunning) ? (int)m_GpuRecorder.GetSample(0).Count : 0; /// /// CPU elapsed time in milliseconds for command-buffer execution in the previous frame. /// /// /// Measures time spent replaying the command buffer on the CPU render thread. /// For inline (non-command-buffer) CPU time use . /// public float cpuElapsedTime => (m_Recorder.Valid && m_Recorder.IsRunning) ? m_Recorder.LastValue * k_NanosecondsToMilliseconds : 0.0f; /// /// Number of times this sampler was hit via a command buffer on the CPU in the previous frame. /// public int cpuSampleCount => (m_Recorder.Valid && m_Recorder.IsRunning) ? (int)m_Recorder.GetSample(0).Count : 0; /// /// CPU elapsed time in milliseconds for direct (inline) / /// calls in the previous frame. /// /// /// Reflects time recorded by and called directly on the /// calling thread, not via a command buffer. For command-buffer time use . /// public float inlineCpuElapsedTime => (m_InlineRecorder.Valid && m_InlineRecorder.IsRunning) ? m_InlineRecorder.LastValue * k_NanosecondsToMilliseconds : 0.0f; /// /// Number of times this sampler was hit via direct inline calls in the previous frame. /// public int inlineCpuSampleCount => (m_InlineRecorder.Valid && m_InlineRecorder.IsRunning) ? (int)m_InlineRecorder.GetSample(0).Count : 0; #else /// GPU Elapsed time in milliseconds. public float gpuElapsedTime => 0.0f; /// Number of times the Profiling Sampler has hit on the GPU public int gpuSampleCount => 0; /// CPU Elapsed time in milliseconds (Command Buffer execution). public float cpuElapsedTime => 0.0f; /// Number of times the Profiling Sampler has hit on the CPU in the command buffer. public int cpuSampleCount => 0; /// CPU Elapsed time in milliseconds (Direct execution). public float inlineCpuElapsedTime => 0.0f; /// Number of times the Profiling Sampler has hit on the CPU. public int inlineCpuSampleCount => 0; #endif // Keep the constructor private ProfilingSampler() { } } /// /// RAII scope that calls on construction and /// on disposal, ensuring markers are always balanced. /// Use in a using statement to guarantee is called. /// /// This struct is a no-op in non-development Players. [IgnoredByDeepProfiler] public struct ProfilingScope : IDisposable { #if ENABLE_PROFILER ProfilingSampler m_Sampler; CommandBuffer m_Cmd; #endif /// /// Creates a profiling scope without a command buffer (inline CPU profiling only). /// /// The sampler that provides the underlying marker. /// May be null; the scope is a no-op in that case. [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)] public ProfilingScope(ProfilingSampler sampler) : this((CommandBuffer)null, sampler) { } /// /// Creates an inline CPU-only profiling scope with a Unity Object context associated /// with the sample. No command buffer is involved; the marker is emitted directly /// on the CPU timeline. /// /// The sampler that provides the underlying marker. /// May be null; the scope is a no-op in that case. /// Unity Object (e.g. Texture, Mesh, Material) to associate /// with this sample. The Profiler displays it in the sample hierarchy. [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)] public ProfilingScope(ProfilingSampler sampler, Object contextObject) : this((CommandBuffer)null, sampler, contextObject) { } /// /// Creates a profiling scope that records markers into as well as inline on the CPU. /// /// /// Do not use with a named . A named command buffer inserts its own /// scope marker on execution, which orphans the markers added here: the begin and end appear /// inside different named-buffer execution brackets and will be mismatched in the Profiler timeline. /// /// Command buffer to receive the GPU-visible begin/end markers. /// The sampler that provides the underlying marker. /// May be null; the scope is a no-op in that case. public ProfilingScope(CommandBuffer cmd, ProfilingSampler sampler) { #if ENABLE_PROFILER sampler?.Begin(cmd); m_Sampler = sampler; m_Cmd = cmd; #endif } /// /// Creates a profiling scope that records markers into with a /// Unity Object context associated with the sample. /// /// /// Do not use with a named . A named command buffer inserts /// its own scope marker on execution, which orphans the markers added here. /// /// Command buffer to receive the GPU-visible begin/end markers. /// The sampler that provides the underlying marker. /// May be null; the scope is a no-op in that case. /// Unity Object (e.g. Texture, Mesh, Material) to associate /// with this sample. The Profiler displays it in the sample hierarchy. public ProfilingScope(CommandBuffer cmd, ProfilingSampler sampler, Object contextObject) { #if ENABLE_PROFILER sampler?.Begin(cmd, contextObject); m_Sampler = sampler; m_Cmd = cmd; #endif } /// /// Creates a profiling scope that records markers into as well as inline on the CPU. /// /// /// Do not use with a named . A named command buffer inserts its own /// scope marker on execution, which orphans the markers added here: the begin and end appear /// inside different named-buffer execution brackets and will be mismatched in the Profiler timeline. /// /// Command buffer to receive the GPU-visible begin/end markers. /// The sampler that provides the underlying marker. /// May be null; the scope is a no-op in that case. public ProfilingScope(BaseCommandBuffer cmd, ProfilingSampler sampler) { #if ENABLE_PROFILER sampler?.Begin(cmd.m_WrappedCommandBuffer); m_Sampler = sampler; m_Cmd = cmd.m_WrappedCommandBuffer; #endif } /// /// Creates a profiling scope that records markers into as well as inline on the CPU, /// and associates a Unity Object with the sample for identification in the Profiler. /// /// /// Do not use with a named . A named command buffer inserts its own /// scope marker on execution, which orphans the markers added here: the begin and end appear /// inside different named-buffer execution brackets and will be mismatched in the Profiler timeline. /// /// BaseCommandBuffer (e.g. RasterCommandBuffer, UnsafeCommandBuffer) to receive the GPU-visible begin/end markers. /// The sampler that provides the underlying marker. /// May be null; the scope is a no-op in that case. /// Unity Object (e.g. Texture, Mesh, Material) to associate /// with this sample. The Profiler displays it in the sample hierarchy. [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)] public ProfilingScope(BaseCommandBuffer cmd, ProfilingSampler sampler, Object contextObject) : this(cmd.m_WrappedCommandBuffer, sampler, contextObject) { } /// /// Ends the profiling scope by calling . Safe to call multiple times. /// public void Dispose() { #if ENABLE_PROFILER if (m_Sampler == null) return; m_Sampler.End(m_Cmd); m_Sampler = null; #endif } } }