using System; #if UNITY_EDITOR using System.IO; #endif using UnityEngine; namespace Unity.Pipeline.Config { /// /// Configuration for Pipeline server functionality in Unity Player builds. Authored settings /// live in a JSON file under ProjectSettings/ (see /) — /// never as an asset under Assets/, so nothing about this package appears in the user's /// Project window. At real Player build time, PipelineRuntimeBuildProcessor bakes the current /// settings into a transient Resources asset (deleted again after the build) so the Player /// can find them via Resources.Load. /// public class RuntimePipelineConfig : ScriptableObject { /// Master switch. The server only starts when true. Off by default for safety. [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; /// Listen port. 0 auto-assigns from 7900-7999. [Header("Server")] [Tooltip("HTTP port for Pipeline server. Use 0 for auto-assignment from range 7900-7999.")] public int port = 0; /// Per-request timeout, in milliseconds. [Tooltip("Request timeout in milliseconds. Higher values allow longer-running commands.")] [Range(1000, 60000)] public int requestTimeoutMs = 30000; /// Log remote requests for auditing. [Header("Runtime Behavior")] [Tooltip("Enable detailed logging of all remote requests for security auditing.")] public bool enableAuditLogging = true; /// Start the server automatically when the Player boots (or Play Mode is entered). [Tooltip("Start the server automatically when the Player boots (or Play Mode is entered).")] public bool autoStart = true; /// Dispatcher work items processed per frame. [Tooltip("Maximum work items the dispatcher processes per frame to maintain performance.")] [Range(1, 50)] public int maxWorkItemsPerFrame = 10; // Plain int field (not [Range]) so the Inspector shows a text field rather than an // unusable 0-65535 slider; the bound is still enforced here, same pattern as // EditorPipelineManager.OnValidate's m_WatchdogIntervalSeconds clamp. private void OnValidate() { port = Mathf.Clamp(port, 0, 65535); } /// Resource name the build-time-generated transient asset is saved under for to find in a Player build. public const string ResourceName = "RuntimePipelineConfig"; #if UNITY_EDITOR /// Project-relative path of the authored settings file (never under Assets/). private const string SettingsFilePath = "ProjectSettings/Packages/com.unity.pipeline/RuntimePipelineConfig.json"; #endif /// /// Load the current settings, or null if none exist yet. In the Editor (including Play /// Mode) this reads the authored ProjectSettings/ JSON file directly; in a real Player /// build it reads the build-time-generated transient Resources asset. /// /// The current settings, or null if none exist yet. public static RuntimePipelineConfig Load() { #if UNITY_EDITOR var path = GetAbsoluteSettingsPath(); if (!File.Exists(path)) return null; var config = CreateInstance(); try { JsonUtility.FromJsonOverwrite(File.ReadAllText(path), config); } catch (Exception ex) { Debug.LogWarning($"Pipeline: could not read runtime settings at '{path}' ({ex.Message}); using no config. Delete the file to reset."); DestroyImmediate(config); return null; } return config; #else return Resources.Load(ResourceName); #endif } #if UNITY_EDITOR private static int s_LiveMaxWorkItemsPerFrameCache = 10; private static DateTime s_LiveMaxWorkItemsPerFrameCacheFileTime; /// /// Cheap, per-frame-callable read of just maxWorkItemsPerFrame from the authored settings /// file, for RuntimePipelineDriver.Update() to poll without paying Load()'s full /// CreateInstance+JSON-parse cost every frame. Re-parses only when the file's last-write /// time changes, so a Project Settings edit (including while in Play Mode) takes effect on /// the very next frame, while an idle frame costs only a single file-timestamp check. /// Editor-only: a Player's config is frozen at build time, so there is nothing to poll for. /// public static int GetLiveMaxWorkItemsPerFrame(int fallback) { var path = GetAbsoluteSettingsPath(); if (!File.Exists(path)) return fallback; var writeTime = File.GetLastWriteTimeUtc(path); if (writeTime != s_LiveMaxWorkItemsPerFrameCacheFileTime) { var config = Load(); if (config != null) { s_LiveMaxWorkItemsPerFrameCache = config.maxWorkItemsPerFrame; DestroyImmediate(config); } s_LiveMaxWorkItemsPerFrameCacheFileTime = writeTime; } return s_LiveMaxWorkItemsPerFrameCache; } /// Persist this instance's current field values to the ProjectSettings/ JSON file. public void Save() { var path = GetAbsoluteSettingsPath(); Directory.CreateDirectory(Path.GetDirectoryName(path)); var previousWrite = File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MinValue; File.WriteAllText(path, JsonUtility.ToJson(this, true)); // GetLiveMaxWorkItemsPerFrame and the settings page detect changes by last-write time. // Windows stamps writes with the system clock tick (~15 ms), so two saves in quick // succession can share a stamp and the second one goes unnoticed. Make every save // observable by moving the stamp forward when the file system did not. if (File.GetLastWriteTimeUtc(path) <= previousWrite) File.SetLastWriteTimeUtc(path, previousWrite.AddMilliseconds(1)); } /// /// Last-write-time of the authored settings file (UTC), or DateTime.MinValue if it doesn't /// exist yet. Lets a long-lived caller (the Project Settings page) detect that the file /// changed underneath it — e.g. a build's "Disable Pipeline" security-dialog choice, or a /// set_runtime_pipeline_settings CLI call — without reloading from disk on every frame. /// public static DateTime GetSettingsFileWriteTimeUtc() { var path = GetAbsoluteSettingsPath(); return File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MinValue; } /// Delete the authored settings file, if any. For test cleanup only. public static void DeleteSettingsFileForTesting() { var path = GetAbsoluteSettingsPath(); if (File.Exists(path)) File.Delete(path); } private static string GetAbsoluteSettingsPath() { // Application.dataPath is ".../Assets"; ProjectSettings is a sibling directory. var projectRoot = Path.GetDirectoryName(Application.dataPath); return Path.Combine(projectRoot, SettingsFilePath); } #endif /// /// Validate the configuration for correctness. /// Called by build processor to ensure safe deployment. /// /// The validation result. 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"); } } /// /// Result from configuration validation. /// [Serializable] public class ValidationResult { /// False only for results. public bool IsValid { get; set; } /// "success", "warning", or "error". public string Level { get; set; } // "success", "warning", "error" /// Human-readable validation message. public string Message { get; set; } /// Create a successful validation result. /// Human-readable message. /// A valid, "success"-level result. public static ValidationResult Success(string message = "Valid") => new ValidationResult { IsValid = true, Level = "success", Message = message }; /// Create a valid-but-noteworthy validation result. /// Human-readable message. /// A valid, "warning"-level result. public static ValidationResult Warning(string message) => new ValidationResult { IsValid = true, Level = "warning", Message = message }; /// Create a failed validation result. /// Human-readable message. /// An invalid, "error"-level result. public static ValidationResult Error(string message) => new ValidationResult { IsValid = false, Level = "error", Message = message }; } }