Add Unity Pipeline package support
This commit is contained in:
+208
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Profiling;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Runtime.Telemetry
|
||||
{
|
||||
/// <summary>
|
||||
/// Samples per-frame timing on the main thread so the runtime telemetry command can report fps and
|
||||
/// frame-time statistics over a rolling window.
|
||||
///
|
||||
/// A Player build has no Editor profiler window to read from and no <c>EditorApplication.update</c>
|
||||
/// to drive sampling, so <see cref="RuntimePipelineManager"/> owns one shared instance and feeds it
|
||||
/// every frame from its <c>Update</c>. The sampler is deliberately allocation-free on the hot path:
|
||||
/// each frame time is written into a fixed ring buffer and only reduced to a <see cref="FrameStatsSnapshot"/>
|
||||
/// on demand when a telemetry request arrives (far rarer than the frame rate).
|
||||
///
|
||||
/// Sampling uses <c>Time.unscaledDeltaTime</c> so reported fps reflects real wall-clock frame pacing
|
||||
/// independent of <c>Time.timeScale</c> — a paused or slow-motion game still reports its true fps.
|
||||
/// </summary>
|
||||
public sealed class FrameStatsSampler : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The process-wide sampler fed by the active <see cref="RuntimePipelineManager"/>. Null when no
|
||||
/// manager is present (e.g. EditMode tests, or a scene without the component). Telemetry consumers
|
||||
/// must treat a null sampler as "frame stats unavailable" rather than failing — memory and counter
|
||||
/// data that do not depend on per-frame sampling can still be reported without it.
|
||||
/// </summary>
|
||||
public static FrameStatsSampler Shared { get; set; }
|
||||
|
||||
private readonly float[] m_FrameTimesMs;
|
||||
private int m_Head;
|
||||
private int m_Count;
|
||||
private float m_LastFrameTimeMs;
|
||||
|
||||
// ProfilerRecorders pull counters straight from the profiler stream. They are started lazily on
|
||||
// the first snapshot (creation must happen on the main thread) and disposed with the sampler.
|
||||
// Recorders that do not resolve on this platform/Unity version report Valid == false and are
|
||||
// simply skipped when building a snapshot.
|
||||
private readonly List<CounterRecorder> m_Counters = new List<CounterRecorder>();
|
||||
private bool m_CountersStarted;
|
||||
|
||||
private struct CounterRecorder
|
||||
{
|
||||
public string Name;
|
||||
public ProfilerRecorder Recorder;
|
||||
}
|
||||
|
||||
/// <summary>Number of frames the rolling window can hold.</summary>
|
||||
public int Capacity => m_FrameTimesMs.Length;
|
||||
|
||||
/// <summary>Number of frames currently recorded (ramps up to <see cref="Capacity"/>).</summary>
|
||||
public int SampleCount => m_Count;
|
||||
|
||||
public FrameStatsSampler(int windowFrames = 120)
|
||||
{
|
||||
if (windowFrames < 1)
|
||||
windowFrames = 1;
|
||||
m_FrameTimesMs = new float[windowFrames];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record one completed frame. Call once per frame from the main thread.
|
||||
/// <paramref name="deltaSeconds"/> is the unscaled delta time of the frame just completed
|
||||
/// (typically <c>Time.unscaledDeltaTime</c>).
|
||||
/// </summary>
|
||||
public void Sample(float deltaSeconds)
|
||||
{
|
||||
var ms = deltaSeconds * 1000f;
|
||||
m_LastFrameTimeMs = ms;
|
||||
m_FrameTimesMs[m_Head] = ms;
|
||||
m_Head = (m_Head + 1) % m_FrameTimesMs.Length;
|
||||
if (m_Count < m_FrameTimesMs.Length)
|
||||
m_Count++;
|
||||
|
||||
// FrameTimingManager only yields data for frames in which timings were captured, so the
|
||||
// capture call has to happen on the per-frame path, not when a snapshot is requested.
|
||||
FrameTimingManager.CaptureFrameTimings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduce the current window to an immutable snapshot. Must be called on the main thread (it reads
|
||||
/// profiler recorders and the frame timing manager).
|
||||
/// </summary>
|
||||
public FrameStatsSnapshot GetSnapshot()
|
||||
{
|
||||
var snap = new FrameStatsSnapshot();
|
||||
|
||||
if (m_Count > 0)
|
||||
{
|
||||
float sum = 0f, min = float.MaxValue, max = 0f;
|
||||
for (int i = 0; i < m_Count; i++)
|
||||
{
|
||||
var v = m_FrameTimesMs[i];
|
||||
sum += v;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
|
||||
var avg = sum / m_Count;
|
||||
snap.Available = true;
|
||||
snap.SampleWindow = m_Count;
|
||||
snap.AverageFrameTimeMs = avg;
|
||||
snap.MinFrameTimeMs = min;
|
||||
snap.MaxFrameTimeMs = max;
|
||||
snap.LastFrameTimeMs = m_LastFrameTimeMs;
|
||||
snap.Fps = avg > 0f ? 1000f / avg : 0f;
|
||||
}
|
||||
|
||||
ReadFrameTimingManager(snap);
|
||||
ReadCounters(snap);
|
||||
return snap;
|
||||
}
|
||||
|
||||
// CPU/GPU frame time from FrameTimingManager. Only populated when the platform reports timings
|
||||
// (requires "Frame Timing Stats" in Player settings / -enable-frame-timing-stats); otherwise the
|
||||
// snapshot's GpuTimingAvailable stays false rather than reporting misleading zeros.
|
||||
private static void ReadFrameTimingManager(FrameStatsSnapshot snap)
|
||||
{
|
||||
var timings = new FrameTiming[1];
|
||||
var captured = FrameTimingManager.GetLatestTimings(1, timings);
|
||||
if (captured > 0)
|
||||
{
|
||||
snap.GpuTimingAvailable = true;
|
||||
snap.CpuFrameTimeMs = timings[0].cpuFrameTime;
|
||||
snap.GpuFrameTimeMs = timings[0].gpuFrameTime;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadCounters(FrameStatsSnapshot snap)
|
||||
{
|
||||
EnsureCountersStarted();
|
||||
var counters = new Dictionary<string, double>();
|
||||
foreach (var c in m_Counters)
|
||||
{
|
||||
if (c.Recorder.Valid)
|
||||
counters[c.Name] = c.Recorder.LastValue;
|
||||
}
|
||||
snap.Counters = counters;
|
||||
}
|
||||
|
||||
private void EnsureCountersStarted()
|
||||
{
|
||||
if (m_CountersStarted)
|
||||
return;
|
||||
m_CountersStarted = true;
|
||||
|
||||
// Common render/memory counters. Stat names that do not exist on the running platform resolve
|
||||
// to invalid recorders and are filtered out at snapshot time.
|
||||
TryAddCounter("DrawCalls", ProfilerCategory.Render, "Draw Calls Count");
|
||||
TryAddCounter("SetPassCalls", ProfilerCategory.Render, "SetPass Calls Count");
|
||||
TryAddCounter("Triangles", ProfilerCategory.Render, "Triangles Count");
|
||||
TryAddCounter("Vertices", ProfilerCategory.Render, "Vertices Count");
|
||||
TryAddCounter("GcAllocInFrame", ProfilerCategory.Memory, "GC Allocated In Frame");
|
||||
}
|
||||
|
||||
private void TryAddCounter(string reportedName, ProfilerCategory category, string statName)
|
||||
{
|
||||
m_Counters.Add(new CounterRecorder
|
||||
{
|
||||
Name = reportedName,
|
||||
Recorder = ProfilerRecorder.StartNew(category, statName)
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var c in m_Counters)
|
||||
{
|
||||
if (c.Recorder.Valid)
|
||||
c.Recorder.Dispose();
|
||||
}
|
||||
m_Counters.Clear();
|
||||
m_CountersStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable point-in-time reduction of <see cref="FrameStatsSampler"/>'s rolling window plus the
|
||||
/// counters/frame-timing read at snapshot time. Serialized as the frame-stats portion of the runtime
|
||||
/// telemetry response.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class FrameStatsSnapshot
|
||||
{
|
||||
/// <summary>True when at least one frame has been sampled (i.e. a manager is feeding the sampler).</summary>
|
||||
public bool Available { get; set; }
|
||||
|
||||
/// <summary>Number of frames included in the averages.</summary>
|
||||
public int SampleWindow { get; set; }
|
||||
|
||||
/// <summary>Frames per second derived from the average frame time over the window.</summary>
|
||||
public float Fps { get; set; }
|
||||
|
||||
public float AverageFrameTimeMs { get; set; }
|
||||
public float MinFrameTimeMs { get; set; }
|
||||
public float MaxFrameTimeMs { get; set; }
|
||||
public float LastFrameTimeMs { get; set; }
|
||||
|
||||
/// <summary>True when FrameTimingManager returned CPU/GPU timings for a recent frame.</summary>
|
||||
public bool GpuTimingAvailable { get; set; }
|
||||
public double CpuFrameTimeMs { get; set; }
|
||||
public double GpuFrameTimeMs { get; set; }
|
||||
|
||||
/// <summary>Valid profiler counters by reported name (e.g. DrawCalls, Triangles).</summary>
|
||||
public Dictionary<string, double> Counters { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e2b7696d6f845cba4f7c776006707d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for Pipeline server functionality in Unity Player builds.
|
||||
/// Create this asset and place in Resources folder to enable runtime Pipeline support.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "RuntimePipelineConfig", menuName = "Pipeline/Runtime Configuration", order = 1)]
|
||||
public class RuntimePipelineConfig : ScriptableObject
|
||||
{
|
||||
[Header("Server Configuration")]
|
||||
[Tooltip("Enable Pipeline HTTP server in Player builds. SECURITY WARNING: Only enable in development/QA builds, never production without proper security measures.")]
|
||||
public bool enableInBuilds = false;
|
||||
|
||||
[Tooltip("HTTP port for Pipeline server. Use 0 for auto-assignment from range 7900-7999.")]
|
||||
[Range(0, 65535)]
|
||||
public int port = 0;
|
||||
|
||||
[Header("Advanced")]
|
||||
[Tooltip("Request timeout in milliseconds. Higher values allow longer-running commands.")]
|
||||
[Range(1000, 60000)]
|
||||
public int requestTimeoutMs = 30000;
|
||||
|
||||
[Tooltip("Enable detailed logging of all remote requests for security auditing.")]
|
||||
public bool enableAuditLogging = true;
|
||||
|
||||
/// <summary>
|
||||
/// Validate the configuration for correctness.
|
||||
/// Called by build processor to ensure safe deployment.
|
||||
/// </summary>
|
||||
public ValidationResult Validate()
|
||||
{
|
||||
if (!enableInBuilds)
|
||||
return ValidationResult.Success("Runtime Pipeline disabled");
|
||||
|
||||
// Port validation
|
||||
if (port != 0 && (port < 7900 || port > 7999))
|
||||
{
|
||||
return ValidationResult.Warning(
|
||||
$"Port {port} is outside recommended runtime range 7900-7999. May conflict with Editor instances.");
|
||||
}
|
||||
|
||||
return ValidationResult.Success("Configuration is valid");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from configuration validation.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public string Level { get; set; } // "success", "warning", "error"
|
||||
public string Message { get; set; }
|
||||
|
||||
public static ValidationResult Success(string message = "Valid") =>
|
||||
new ValidationResult { IsValid = true, Level = "success", Message = message };
|
||||
|
||||
public static ValidationResult Warning(string message) =>
|
||||
new ValidationResult { IsValid = true, Level = "warning", Message = message };
|
||||
|
||||
public static ValidationResult Error(string message) =>
|
||||
new ValidationResult { IsValid = false, Level = "error", Message = message };
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0fd19cf31d54e3848a1075f001aed64f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Unity.Pipeline.Config;
|
||||
using Unity.Pipeline.Commands;
|
||||
using Unity.Pipeline.HotReload;
|
||||
using Unity.Pipeline.Runtime.Telemetry;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Unified Pipeline runtime manager that handles both server creation and event processing.
|
||||
/// Replaces the need for separate RuntimePipelineConfig and RuntimePipelineProcessor.
|
||||
///
|
||||
/// Simply add this component to a GameObject in your scene to enable Pipeline runtime support.
|
||||
/// Only one instance should exist in your project.
|
||||
/// </summary>
|
||||
[AddComponentMenu("Pipeline/Runtime Pipeline Manager")]
|
||||
[DisallowMultipleComponent]
|
||||
public class RuntimePipelineManager : MonoBehaviour
|
||||
{
|
||||
[Header("Server Configuration")]
|
||||
[Tooltip("Enable Pipeline HTTP server in Player builds. SECURITY WARNING: Only enable in development/QA builds, never production without proper security measures.")]
|
||||
public bool enableInBuilds = false;
|
||||
|
||||
[Tooltip("HTTP port for Pipeline server. Use 0 for auto-assignment from range 7900-7999.")]
|
||||
[Range(0, 65535)]
|
||||
public int port = 0;
|
||||
|
||||
[Header("Advanced")]
|
||||
[Tooltip("Request timeout in milliseconds. Higher values allow longer-running commands.")]
|
||||
[Range(1000, 60000)]
|
||||
public int requestTimeoutMs = 30000;
|
||||
|
||||
[Tooltip("Enable detailed logging of all remote requests for security auditing.")]
|
||||
public bool enableAuditLogging = true;
|
||||
|
||||
[Header("Runtime")]
|
||||
[Tooltip("Enable automatic server startup when the component starts (for runtime builds only).")]
|
||||
public bool autoStart = true;
|
||||
|
||||
[Tooltip("Maximum work items to process per frame to maintain performance.")]
|
||||
[Range(1, 50)]
|
||||
public int maxWorkItemsPerFrame = 10;
|
||||
|
||||
// Absolute roots this build is allowed to hot reload source files from (Assets + loaded
|
||||
// package locations). Baked at build time by the build processor (a running Player cannot
|
||||
// resolve the project layout) and published to HotReloadFileScope when the server starts.
|
||||
// Hidden from the inspector: it is build-injected, not user-edited.
|
||||
[SerializeField, HideInInspector] private List<string> m_AllowedReloadRoots = new List<string>();
|
||||
|
||||
// Internal state
|
||||
private RuntimePipelineServer m_Server;
|
||||
private RuntimePipelineConfig m_RuntimeConfig;
|
||||
|
||||
// True only for the instance that installed FrameStatsSampler.Shared, so a rejected duplicate's
|
||||
// OnDestroy never tears down the real sampler.
|
||||
private bool m_OwnsSampler;
|
||||
|
||||
/// <summary>
|
||||
/// Get the runtime server instance if it's running.
|
||||
/// </summary>
|
||||
public RuntimePipelineServer Server => m_Server;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the server is currently running.
|
||||
/// </summary>
|
||||
public bool IsServerRunning => m_Server != null && m_Server.IsRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Get the actual port the server is running on.
|
||||
/// </summary>
|
||||
public int ActualPort => m_Server?.Port ?? 0;
|
||||
|
||||
public RuntimePipelineConfig Config => m_RuntimeConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Absolute roots this build may hot reload source files from. Baked at build time.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AllowedReloadRoots => m_AllowedReloadRoots;
|
||||
|
||||
/// <summary>
|
||||
/// Set the allowed hot reload roots. Called by the build processor to bake the project's
|
||||
/// Assets and package locations into the build.
|
||||
/// </summary>
|
||||
public void SetAllowedReloadRoots(IEnumerable<string> roots)
|
||||
{
|
||||
m_AllowedReloadRoots = roots != null ? new List<string>(roots) : new List<string>();
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Ensure only one instance exists
|
||||
var existing = PipelineUtils.FindObjectsByType<RuntimePipelineManager>();
|
||||
if (existing.Length > 1)
|
||||
{
|
||||
Debug.LogWarning($"Pipeline: Multiple RuntimePipelineManager instances found. Destroying duplicate on '{name}'.");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't destroy on load for persistent operation across scenes
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
// Set up command discovery (the server owns its own dispatcher, initialized on Start).
|
||||
CommandRegistry.SetDiscovery(null); // Triggers reflection-based discovery
|
||||
|
||||
// Auto-discover and register hot-reloadable methods. Hot reload requires this manager to
|
||||
// be present (it owns the communication workflow), so discovery lives here rather than in
|
||||
// each gameplay script's Awake.
|
||||
RegisterDiscoveredHotReloadMethods();
|
||||
|
||||
// Own the process-wide frame-stats sampler. A Player has no profiler window or
|
||||
// EditorApplication.update to drive sampling, so the manager feeds it from Update (below) and
|
||||
// the runtime_status telemetry command reads FrameStatsSampler.Shared. Created here (rather than
|
||||
// on server start) so fps history is already warm by the time an agent connects.
|
||||
FrameStatsSampler.Shared = new FrameStatsSampler();
|
||||
m_OwnsSampler = true;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (autoStart && enableInBuilds)
|
||||
{
|
||||
StartServer();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scan loaded user assemblies for methods tagged [HotReload] (in-place workflow) and
|
||||
/// [HotReloadWithOverrides] (helper workflow) and register them as reload targets. This replaces any
|
||||
/// per-script RegisterReloadableType call in Awake.
|
||||
/// </summary>
|
||||
private static void RegisterDiscoveredHotReloadMethods()
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic
|
||||
| BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||
|
||||
int registered = 0;
|
||||
|
||||
foreach (var assembly in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
if (!ShouldScanForHotReload(assembly))
|
||||
continue;
|
||||
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types; // best effort: keep the types that did load
|
||||
}
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type == null)
|
||||
continue;
|
||||
|
||||
foreach (var method in type.GetMethods(flags))
|
||||
{
|
||||
var inPlace = method.GetCustomAttribute<HotReloadAttribute>();
|
||||
if (inPlace != null)
|
||||
{
|
||||
// The weaver keys dispatch on TypeName.MethodName, so register with the
|
||||
// default id (no custom Id) to match.
|
||||
HotReloadRegistry.RegisterReloadableMethod(
|
||||
method, new HotReloadWithOverridesAttribute { RequireMainThread = inPlace.RequireMainThread });
|
||||
registered++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var reloadable = method.GetCustomAttribute<HotReloadWithOverridesAttribute>();
|
||||
if (reloadable != null)
|
||||
{
|
||||
HotReloadRegistry.RegisterReloadableMethod(method, reloadable);
|
||||
registered++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (registered > 0)
|
||||
Debug.Log($"Pipeline: Auto-discovered and registered {registered} hot-reloadable method(s).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skip engine/framework assemblies that cannot contain user hot-reload methods.
|
||||
/// </summary>
|
||||
private static bool ShouldScanForHotReload(Assembly assembly)
|
||||
{
|
||||
if (assembly.IsDynamic)
|
||||
return false;
|
||||
|
||||
var name = assembly.GetName().Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
|
||||
foreach (var prefix in s_HotReloadSkipPrefixes)
|
||||
{
|
||||
if (name.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly string[] s_HotReloadSkipPrefixes =
|
||||
{
|
||||
"System", "mscorlib", "netstandard", "Microsoft", "Mono.", "nunit",
|
||||
"UnityEngine", "UnityEditor", "Unity.Collections", "Unity.Burst",
|
||||
"Unity.Mathematics", "Newtonsoft", "log4net", "ICSharpCode",
|
||||
};
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Feed the frame-stats sampler once per frame on the main thread. Uses unscaled delta so
|
||||
// reported fps reflects real frame pacing regardless of Time.timeScale.
|
||||
FrameStatsSampler.Shared?.Sample(Time.unscaledDeltaTime);
|
||||
|
||||
// Pump this server's own dispatcher for main-thread operations (player builds have no
|
||||
// EditorApplication.update to auto-pump it).
|
||||
m_Server?.Dispatcher.ProcessWorkQueue(maxWorkItemsPerFrame);
|
||||
|
||||
// Drive the watchdog (player builds have no EditorApplication.update). No-op unless the
|
||||
// server's watchdog is enabled and armed.
|
||||
m_Server?.WatchdogTick();
|
||||
}
|
||||
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
StopServer();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// Dispose the sampler's profiler recorders and drop the shared reference, but only if this
|
||||
// manager is the one that installed it (a rejected duplicate must not tear down the real one).
|
||||
if (m_OwnsSampler)
|
||||
{
|
||||
FrameStatsSampler.Shared?.Dispose();
|
||||
FrameStatsSampler.Shared = null;
|
||||
m_OwnsSampler = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually start the Pipeline server.
|
||||
/// </summary>
|
||||
public void StartServer()
|
||||
{
|
||||
if (m_Server != null && m_Server.IsRunning)
|
||||
{
|
||||
Debug.LogWarning("Pipeline: Server already started or initialization attempted.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
System.Console.WriteLine("Pipeline: Starting runtime server from RuntimePipelineManager...");
|
||||
|
||||
if (!enableInBuilds)
|
||||
{
|
||||
Debug.LogWarning("Pipeline: Runtime server disabled in configuration (enableInBuilds = false)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create runtime config from component settings
|
||||
m_RuntimeConfig = CreateRuntimeConfig();
|
||||
|
||||
// Validate configuration
|
||||
var validation = m_RuntimeConfig.Validate();
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
Debug.LogError($"Pipeline: Invalid runtime configuration: {validation.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (validation.Level == "warning")
|
||||
{
|
||||
Debug.LogWarning($"Pipeline: Runtime configuration warning: {validation.Message}");
|
||||
}
|
||||
|
||||
// Create runtime server
|
||||
m_Server = new RuntimePipelineServer(m_RuntimeConfig);
|
||||
|
||||
// Start server on configured port
|
||||
var serverPort = port == 0 ? 0 : port;
|
||||
m_Server.Start(serverPort);
|
||||
|
||||
if (m_Server.IsRunning)
|
||||
{
|
||||
// The hot-reload registry marshals main-thread overrides through a dispatcher
|
||||
// when hot-reloaded code is invoked from a background thread at runtime. Inject
|
||||
// this server's dispatcher here (a player has exactly one manager), so editor and
|
||||
// per-test servers never clobber the global registry's dispatcher.
|
||||
Unity.Pipeline.HotReload.HotReloadRegistry.Dispatcher = m_Server.Dispatcher;
|
||||
|
||||
// Publish the build-baked allowed roots so reload commands can validate that
|
||||
// incoming files are inside the project before compiling and injecting them.
|
||||
Unity.Pipeline.HotReload.HotReloadRegistry.AllowedReloadRoots = m_AllowedReloadRoots;
|
||||
|
||||
System.Console.WriteLine($"Pipeline: Runtime server started successfully on port {m_Server.Port}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Pipeline: Failed to start runtime server");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Pipeline: Runtime initialization failed: {ex.Message}");
|
||||
Debug.LogError($"Pipeline: Stack trace: {ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually stop the Pipeline server.
|
||||
/// </summary>
|
||||
public void StopServer()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_Server != null && m_Server.IsRunning)
|
||||
{
|
||||
System.Console.WriteLine("Pipeline: Stopping runtime server...");
|
||||
// Drop the registry's reference to this server's (about to be shut down) dispatcher.
|
||||
if (Unity.Pipeline.HotReload.HotReloadRegistry.Dispatcher == m_Server.Dispatcher)
|
||||
Unity.Pipeline.HotReload.HotReloadRegistry.Dispatcher = null;
|
||||
m_Server.Stop(); // Stop() shuts down the server's own dispatcher.
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Pipeline: Runtime shutdown error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a RuntimePipelineConfig from the component settings.
|
||||
/// </summary>
|
||||
private RuntimePipelineConfig CreateRuntimeConfig()
|
||||
{
|
||||
var config = ScriptableObject.CreateInstance<RuntimePipelineConfig>();
|
||||
|
||||
// Copy settings from component to config
|
||||
config.enableInBuilds = this.enableInBuilds;
|
||||
config.port = this.port;
|
||||
config.requestTimeoutMs = this.requestTimeoutMs;
|
||||
config.enableAuditLogging = this.enableAuditLogging;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate the current configuration.
|
||||
/// </summary>
|
||||
public ValidationResult ValidateConfiguration()
|
||||
{
|
||||
if (m_RuntimeConfig == null)
|
||||
m_RuntimeConfig = CreateRuntimeConfig();
|
||||
|
||||
return m_RuntimeConfig.Validate();
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd4ab1c9091e0194196b5f5d51cb932f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Unity.Pipeline.Config;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline
|
||||
{
|
||||
public class RuntimePipelineServer : BasePipelineServer
|
||||
{
|
||||
private RuntimePipelineConfig m_Config;
|
||||
private RuntimeInstanceDescriptor m_InstanceDescriptor;
|
||||
|
||||
public override DateTime StartedAt => m_InstanceDescriptor == null ? new DateTime() : m_InstanceDescriptor.StartedAt;
|
||||
|
||||
public RuntimePipelineServer(RuntimePipelineConfig config)
|
||||
{
|
||||
m_Config = config;
|
||||
}
|
||||
|
||||
protected override (int basePort, int maxPort) GetPortRange()
|
||||
{
|
||||
return (7900, 7949); // Runtime production (test runtime servers use 7950-7999)
|
||||
}
|
||||
|
||||
protected override void CreateInstanceDescriptor()
|
||||
{
|
||||
// Create and write instance descriptor for CLI discovery
|
||||
m_InstanceDescriptor = RuntimeInstanceDescriptor.CreateCurrent(Port, m_Config);
|
||||
RuntimeInstanceDescriptor.WriteToWorkingDirectory(m_InstanceDescriptor);
|
||||
}
|
||||
|
||||
protected override void DeleteInstanceDescriptor()
|
||||
{
|
||||
// Clean up instance descriptor file
|
||||
if (m_InstanceDescriptor != null)
|
||||
{
|
||||
RuntimeInstanceDescriptor.RemoveFromWorkingDirectory();
|
||||
}
|
||||
m_InstanceDescriptor = null;
|
||||
}
|
||||
|
||||
protected override void UpdateHeartBeat()
|
||||
{
|
||||
// Update heartbeat in instance descriptor
|
||||
if (m_InstanceDescriptor != null)
|
||||
{
|
||||
m_InstanceDescriptor.LastHeartbeat = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
RuntimeInstanceDescriptor.WriteToWorkingDirectory(m_InstanceDescriptor);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to update instance descriptor: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override object GetServerStatus()
|
||||
{
|
||||
UpdateHeartBeat();
|
||||
return new
|
||||
{
|
||||
status = m_InstanceDescriptor == null ? "error" : "ready",
|
||||
lastHeartbeat = m_InstanceDescriptor?.LastHeartbeat
|
||||
};
|
||||
}
|
||||
|
||||
protected override string GetToken()
|
||||
{
|
||||
return m_InstanceDescriptor.EvalToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4599450d651dea74cb4b90289360aa3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user