Add Unity Pipeline package support

This commit is contained in:
ud18010
2026-07-31 17:34:04 +08:00
parent d1a526094e
commit 786b7ffff9
590 changed files with 51995 additions and 2 deletions
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Unity.Pipeline.Commands;
namespace Unity.Pipeline.Editor.Commands.Observability
{
/// <summary>
/// Read-only observability commands over the captured Editor console (CLI-198). Logs are captured
/// continuously by <see cref="ConsoleLogBuffer"/>; these commands let an agent read them back as
/// structured data and clear the buffer (and the Unity console) between runs.
/// </summary>
public static class ConsoleCommands
{
[CliCommand("get_console_logs", "Read recently captured Editor console logs (structured).")]
public static object GetConsoleLogs(
[CliArg("severity", "Filter: all | log | warning | error. 'all' = every entry; 'log' = Log only; 'warning' = Warning only; 'error' = Error/Exception/Assert only.")] string severity = "all",
[CliArg("limit", "Max entries to return (most-recent first), capped at 1000.")] int limit = 100)
{
// Snapshot is oldest-first; filter by tier, then take the most-recent `limit` entries
// and present them newest-first.
var snapshot = ConsoleLogBuffer.Snapshot();
var filtered = new List<ConsoleLogEntryDto>(snapshot.Count);
foreach (var entry in snapshot)
{
if (MatchesSeverity(entry.Type, severity))
filtered.Add(entry);
}
var clampedLimit = Math.Min(Math.Max(limit, 1), ConsoleLogBuffer.MaxEntries);
var logs = new List<ConsoleLogEntryDto>(Math.Min(clampedLimit, filtered.Count));
for (var i = filtered.Count - 1; i >= 0 && logs.Count < clampedLimit; i--)
logs.Add(filtered[i]);
return new
{
total = filtered.Count,
returned = logs.Count,
logs
};
}
[CliCommand("clear_console", "Clear the captured log buffer and the Unity Editor console.")]
public static object ClearConsole()
{
ConsoleLogBuffer.Clear();
// Best-effort: also clear Unity's own console window. UnityEditor.LogEntries is internal,
// so reach it via reflection and swallow any failure (API is not part of the public contract).
// LogEntries.Clear is a static, non-public method, so include NonPublic in the binding flags —
// GetMethod("Clear") with no flags only finds public methods and would silently return null.
try
{
Type.GetType("UnityEditor.LogEntries,UnityEditor")
?.GetMethod("Clear", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
?.Invoke(null, null);
}
catch
{
// Ignore — clearing the Editor console is auxiliary; the buffer is already cleared.
}
return new { cleared = true };
}
/// <summary>
/// Map a Unity <see cref="UnityEngine.LogType"/> name onto the requested severity tier.
/// The tiers are distinct: "all" matches every entry; "log" matches Log only;
/// "warning" matches Warning only; "error" matches Error/Exception/Assert only.
/// Any unknown value falls back to "all" (matches everything).
/// </summary>
private static bool MatchesSeverity(string type, string severity)
{
switch (severity?.ToLowerInvariant())
{
case "log":
return type == "Log";
case "warning":
return type == "Warning";
case "error":
return type == "Error" || type == "Exception" || type == "Assert";
case "all":
default:
return true;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b7dd929510db8d248b64b05361bfabb6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.Observability
{
/// <summary>
/// A single captured Editor console entry. Returned (most-recent-first) by the
/// <c>get_console_logs</c> command so an agent can read structured log output without scraping
/// the Editor UI. Mirrors the fields Unity surfaces on <see cref="Application.logMessageReceivedThreaded"/>.
/// </summary>
[Serializable]
public class ConsoleLogEntryDto
{
/// <summary>Unity <see cref="LogType"/> name, e.g. "Log", "Warning", "Error", "Exception", "Assert".</summary>
[JsonProperty("type")]
public string Type { get; set; }
/// <summary>The logged message text.</summary>
[JsonProperty("message")]
public string Message { get; set; }
/// <summary>Captured stack trace, if Unity provided one for the entry.</summary>
[JsonProperty("stackTrace")]
public string StackTrace { get; set; }
/// <summary>Capture time in UTC, ISO-8601 round-trip ("o") format.</summary>
[JsonProperty("timestampUtc")]
public string TimestampUtc { get; set; }
}
/// <summary>
/// Captures Editor console output into a bounded, thread-safe ring buffer so it can be read back
/// structurally over the pipeline (CLI-198). Subscribes to
/// <see cref="Application.logMessageReceivedThreaded"/> on load — the threaded variant is used
/// because Unity may raise log callbacks off the main thread, and the handler is fully lock-guarded.
///
/// The buffer holds at most <see cref="MaxEntries"/> entries; the oldest is dropped when full.
/// </summary>
public static class ConsoleLogBuffer
{
/// <summary>Maximum number of entries retained; oldest entries are dropped past this.</summary>
public const int MaxEntries = 1000;
private static readonly object m_Lock = new object();
private static readonly Queue<ConsoleLogEntryDto> m_Entries = new Queue<ConsoleLogEntryDto>(MaxEntries);
/// <summary>
/// Subscribe to Unity's threaded log callback as soon as the Editor domain loads, so capture
/// is live before any command runs. Idempotent across domain reloads (handler is removed
/// first to avoid double-subscription if Init somehow runs twice).
/// </summary>
[InitializeOnLoadMethod]
private static void Init()
{
Application.logMessageReceivedThreaded -= OnLogMessageThreaded;
Application.logMessageReceivedThreaded += OnLogMessageThreaded;
}
private static void OnLogMessageThreaded(string condition, string stackTrace, LogType type)
{
var entry = new ConsoleLogEntryDto
{
Type = type.ToString(),
Message = condition,
StackTrace = stackTrace,
TimestampUtc = DateTime.UtcNow.ToString("o")
};
lock (m_Lock)
{
if (m_Entries.Count >= MaxEntries)
m_Entries.Dequeue();
m_Entries.Enqueue(entry);
}
}
/// <summary>
/// Take a point-in-time copy of the buffer in chronological (oldest-first) order. The copy is
/// detached from the live buffer, so callers can filter/reverse it without holding the lock.
/// </summary>
public static IReadOnlyList<ConsoleLogEntryDto> Snapshot()
{
lock (m_Lock)
{
return new List<ConsoleLogEntryDto>(m_Entries);
}
}
/// <summary>Number of entries currently retained in the buffer.</summary>
public static int Count
{
get
{
lock (m_Lock)
{
return m_Entries.Count;
}
}
}
/// <summary>Drop all captured entries.</summary>
public static void Clear()
{
lock (m_Lock)
{
m_Entries.Clear();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d212c06f2c819e04c892f7e41520f975
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,171 @@
using System;
using Newtonsoft.Json;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
namespace Unity.Pipeline.Editor.Commands.Observability
{
/// <summary>
/// Render statistics for the most recently rendered Editor frame, read from
/// <see cref="UnityStats"/>. These reflect the last frame the Editor drew (Game/Scene view), not
/// an averaged measurement — treat them as a snapshot.
/// </summary>
[Serializable]
public class RenderStats
{
/// <summary>Draw calls issued for the last rendered frame.</summary>
[JsonProperty("drawCalls")]
public int DrawCalls { get; set; }
/// <summary>Total batches (dynamic + static + instanced) for the last rendered frame.</summary>
[JsonProperty("batches")]
public int Batches { get; set; }
/// <summary>SetPass calls (shader pass switches) for the last rendered frame.</summary>
[JsonProperty("setPassCalls")]
public int SetPassCalls { get; set; }
/// <summary>Triangles submitted for the last rendered frame.</summary>
[JsonProperty("triangles")]
public int Triangles { get; set; }
/// <summary>Vertices submitted for the last rendered frame.</summary>
[JsonProperty("vertices")]
public int Vertices { get; set; }
}
/// <summary>
/// Process memory usage read from <see cref="Profiler"/>. Values are in bytes and are always
/// available at runtime regardless of whether the Profiler window is open.
/// </summary>
[Serializable]
public class MemoryStats
{
/// <summary>Total memory currently allocated by Unity (bytes).</summary>
[JsonProperty("totalAllocatedBytes")]
public long TotalAllocatedBytes { get; set; }
/// <summary>Total memory reserved by Unity (bytes).</summary>
[JsonProperty("totalReservedBytes")]
public long TotalReservedBytes { get; set; }
/// <summary>Mono managed heap memory in use (bytes).</summary>
[JsonProperty("monoUsedBytes")]
public long MonoUsedBytes { get; set; }
/// <summary>Mono managed heap size reserved (bytes).</summary>
[JsonProperty("monoHeapBytes")]
public long MonoHeapBytes { get; set; }
}
/// <summary>
/// Latest CPU/GPU frame timing from <see cref="FrameTimingManager"/>. The Frame Timing Manager
/// must have a timing captured for <see cref="Available"/> to be true; when unavailable the
/// timing fields are left at zero.
/// </summary>
[Serializable]
public class FrameTimingStats
{
/// <summary>True when a frame timing was captured and the milliseconds fields are meaningful.</summary>
[JsonProperty("available")]
public bool Available { get; set; }
/// <summary>Total CPU frame time in milliseconds.</summary>
[JsonProperty("cpuFrameTimeMs")]
public double CpuFrameTimeMs { get; set; }
/// <summary>Total GPU frame time in milliseconds.</summary>
[JsonProperty("gpuFrameTimeMs")]
public double GpuFrameTimeMs { get; set; }
/// <summary>CPU main-thread frame time in milliseconds.</summary>
[JsonProperty("cpuMainThreadFrameTimeMs")]
public double CpuMainThreadFrameTimeMs { get; set; }
}
/// <summary>
/// Composite performance snapshot returned by <c>get_performance_stats</c>: render counters,
/// process memory, and the latest frame timing.
/// </summary>
[Serializable]
public class PerformanceStats
{
/// <summary>Render counters for the last rendered Editor frame.</summary>
[JsonProperty("render")]
public RenderStats Render { get; set; }
/// <summary>Process memory usage at capture time.</summary>
[JsonProperty("memory")]
public MemoryStats Memory { get; set; }
/// <summary>Latest CPU/GPU frame timing, if one was captured.</summary>
[JsonProperty("frameTiming")]
public FrameTimingStats FrameTiming { get; set; }
}
/// <summary>
/// Read-only telemetry command (CLI-198) that snapshots render, memory, and frame-timing stats so
/// an agent can reason about Editor performance without opening the Profiler or Stats overlay.
/// </summary>
public static class PerformanceCommands
{
[CliCommand("get_performance_stats", "Read render, memory, and frame-timing stats (structured, read-only).")]
public static PerformanceStats GetPerformanceStats()
{
return new PerformanceStats
{
Render = new RenderStats
{
// UnityStats reflects the last frame rendered by the Editor (Game/Scene view).
DrawCalls = UnityStats.drawCalls,
Batches = UnityStats.dynamicBatches + UnityStats.staticBatches + UnityStats.instancedBatches,
SetPassCalls = UnityStats.setPassCalls,
Triangles = UnityStats.triangles,
Vertices = UnityStats.vertices
},
Memory = new MemoryStats
{
TotalAllocatedBytes = Profiler.GetTotalAllocatedMemoryLong(),
TotalReservedBytes = Profiler.GetTotalReservedMemoryLong(),
MonoUsedBytes = Profiler.GetMonoUsedSizeLong(),
MonoHeapBytes = Profiler.GetMonoHeapSizeLong()
},
FrameTiming = CaptureFrameTiming()
};
}
/// <summary>
/// Capture the latest CPU/GPU frame timing. The Frame Timing Manager only yields data once a
/// timing has been captured (and platform support exists), so any failure or empty result is
/// reported as <see cref="FrameTimingStats.Available"/> = false rather than throwing.
/// </summary>
private static FrameTimingStats CaptureFrameTiming()
{
try
{
FrameTimingManager.CaptureFrameTimings();
var timings = new FrameTiming[1];
uint received = FrameTimingManager.GetLatestTimings(1, timings);
if (received > 0)
{
return new FrameTimingStats
{
Available = true,
CpuFrameTimeMs = timings[0].cpuFrameTime,
GpuFrameTimeMs = timings[0].gpuFrameTime,
CpuMainThreadFrameTimeMs = timings[0].cpuMainThreadFrameTime
};
}
}
catch
{
// Frame timing is platform-dependent and may be unsupported; report as unavailable.
}
return new FrameTimingStats { Available = false };
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ae9262a38e20733409c3a084cb810bdc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: