#if ENABLE_UPSCALER_FRAMEWORK #nullable enable using System; using System.Collections.Generic; namespace UnityEngine.Rendering { /// /// Manages per-camera upscaler contexts. Handles context creation, caching, validation, and expiry. /// internal class UpscalerContextManager { private struct ContextKey : IEquatable { public ulong cameraId; public string upscalerName; public ContextKey(ulong cameraId, string upscalerName) { this.cameraId = cameraId; this.upscalerName = upscalerName; } public bool Equals(ContextKey other) { return cameraId == other.cameraId && upscalerName == other.upscalerName; } public override bool Equals(object? obj) => obj is ContextKey other && Equals(other); public override int GetHashCode() => HashCode.Combine(cameraId, upscalerName); } // Contexts unused for this many frames are automatically cleaned up. // 400 frames at 60 FPS ≈ 6.7 seconds. This threshold follows the pattern // established in HDRP's existing context management. private const int k_ContextExpiryFrames = 400; private readonly Dictionary m_Contexts = new(); private readonly List m_KeysToRemove = new(); // Reusable list for cleanup private readonly List m_InvalidatedContexts = new(); // Contexts pending cleanup /// /// Acquires a context for the specified camera and upscaler. /// Returns a cached context if valid, or creates a new one if missing or invalid. /// Also updates the context's last-used frame for expiry tracking. /// /// Unique ID for the camera. In XR scenarios, this must be unique per view (encode eye information into the ID). /// The upscaler to acquire a context for. /// The current upscaler options. /// The target display resolution. /// The context, or null for spatial upscalers that don't need context. public IUpscalerContext? AcquireContext( ulong cameraId, IUpscaler upscaler, UpscalerOptions options, Vector2Int displayResolution) { var key = new ContextKey(cameraId, upscaler.name); // Note: Time.frameCount may not advance in Editor when Game view is inactive. // This is acceptable because contexts are only acquired during active rendering, // and lastUsedFrame is updated on every AcquireContext call. If a more robust // solution is needed (e.g., for scene view upscaling), consider using // Time.realtimeSinceStartup or a pipeline-provided render counter. int currentFrame = Time.frameCount; if (m_Contexts.TryGetValue(key, out var existingContext)) { // Check if context is still valid bool resolutionValid = existingContext.createdForDisplayResolution == displayResolution; bool optionsValid = existingContext.IsValidForOptions(options); if (resolutionValid && optionsValid) { existingContext.lastUsedFrame = currentFrame; return existingContext; } // Context is invalid, queue it for cleanup and remove from cache m_InvalidatedContexts.Add(existingContext); m_Contexts.Remove(key); } // Create new context var newContext = upscaler.CreateContext(options, displayResolution); if (newContext != null) { newContext.lastUsedFrame = currentFrame; m_Contexts[key] = newContext; } else if (upscaler.isTemporal) { // Temporal upscalers should always return a context. Null indicates // a creation failure (e.g., plugin not loaded, GPU not supported). Debug.LogWarning($"[UpscalerContextManager] Temporal upscaler '{upscaler.name}' returned null context. " + "Upscaling may not function correctly."); } return newContext; } /// /// Removes contexts that haven't been used for more than k_ContextExpiryFrames frames. /// Also cleans up any contexts that were invalidated (due to resolution or option changes). /// Should be called once per frame from the render pipeline. /// public void CleanupExpiredContexts(CommandBuffer cmd) { // Clean up invalidated contexts first foreach (var context in m_InvalidatedContexts) { context.Cleanup(cmd); } m_InvalidatedContexts.Clear(); // Clean up expired contexts (see AcquireContext for Time.frameCount limitations) int currentFrame = Time.frameCount; m_KeysToRemove.Clear(); foreach (var kvp in m_Contexts) { var context = kvp.Value; int framesSinceLastUse = currentFrame - context.lastUsedFrame; if (framesSinceLastUse > k_ContextExpiryFrames) { context.Cleanup(cmd); m_KeysToRemove.Add(kvp.Key); } } foreach (var key in m_KeysToRemove) { m_Contexts.Remove(key); } } /// /// Cleans up all contexts. Called when the upscaling system is disposed. /// public void Dispose(CommandBuffer cmd) { // Clean up any pending invalidated contexts foreach (var context in m_InvalidatedContexts) { context.Cleanup(cmd); } m_InvalidatedContexts.Clear(); // Clean up all active contexts foreach (var kvp in m_Contexts) { kvp.Value.Cleanup(cmd); } m_Contexts.Clear(); } } public static class UpscalerRegistry { public static readonly Dictionary s_RegisteredUpscalers = new(); /// /// Registers an IUpscaler type without any custom options type. /// public static void Register(string id) where TUpscaler : IUpscaler, new() { s_RegisteredUpscalers[typeof(TUpscaler)] = (null, id); } /// /// Registers an IUpscaler type with its custom options type. /// public static void Register(string id) where TUpscaler : IUpscaler where TOptions : UpscalerOptions { s_RegisteredUpscalers[typeof(TUpscaler)] = (typeof(TOptions), id); } } public class Upscaling { #region private // The integration type is internally used by the SRP systems, which contain embedded upscaling passes in an uber-pass. // The external upscaler integrations are assumed to be standalone render passes. private enum UpscalerIntegrationType { StandalonePass, // The upscaler is executed as a standalone Render Graph pass. EmbeddedPass // The upscaler is baked into a pipeline-specific uber-pass (e.g., URP's Post-process pass). } private struct UpscalerEntry { readonly public IUpscaler Instance { get; } readonly public UpscalerIntegrationType IntegrationType { get; } readonly public bool IsEmbedded { get { return IntegrationType == UpscalerIntegrationType.EmbeddedPass; } } public UpscalerEntry(IUpscaler instance, UpscalerIntegrationType integrationType) { Instance = instance; IntegrationType = integrationType; } } private List m_Upscalers = new List(); private string[] m_UpscalerNamesCache; private int m_ActiveUpscalerIndex = -1; private readonly UpscalerContextManager m_ContextManager = new(); // Pipeline-wide ("global") options per upscaler, keyed by upscaler name; GetGlobalOptions() returns these. // Absent for upscalers registered without an options type (spatial/embedded) — their options are null. private readonly Dictionary m_GlobalOptions = new(); #endregion /// /// Returns the names of the upscalers registered to the upscaling system. /// public IReadOnlyList upscalerNames => m_UpscalerNamesCache; /// /// Returns the active IUpscaler instance, null if none is selected. /// public IUpscaler? activeUpscaler => (m_ActiveUpscalerIndex >= 0) ? m_Upscalers[m_ActiveUpscalerIndex].Instance : null; /// /// Returns true if the active upscaler is embedded in an uber pass. /// public bool activeUpscalerIsEmbedded => (m_ActiveUpscalerIndex >= 0) && m_Upscalers[m_ActiveUpscalerIndex].IsEmbedded; /// /// Initializes the Upscaling system. with the given list of upscaler options per upscaler type. /// /// The list of options from the RP asset. /// A set of types that the pipeline handles internally (e.g., Bilinear, Point in URP). /// /// An ordered list of Types. Upscalers matching these types will appear first /// in the list, in the order provided. All others appear after, alphabetically. /// public Upscaling( List upscalerOptions, HashSet? embeddedTypes = null, Type[]? priorityOrder = null ) { // 1. Instantiate the upscaler instances foreach (var kvp in UpscalerRegistry.s_RegisteredUpscalers) { Type upscalerType = kvp.Key; Type? optionsType = kvp.Value.OptionsType; // find any serialized options, if any provided by the package implementor int optionsIndex = upscalerOptions.FindIndex(o => o != null && o.GetType() == optionsType); bool optionsNotFound = optionsIndex == -1; UpscalerOptions? options = optionsNotFound ? null: upscalerOptions[optionsIndex]; // construct upscaler (always parameterless; options are owned by the framework, not the instance) IUpscaler upscaler = (IUpscaler)Activator.CreateInstance(upscalerType); // Create a fresh options object when the asset provided none, so a quality-mode upscaler always has one. // Upscalers without an options type (spatial/embedded) keep no entry, leaving their options null. if (options == null && optionsType != null) { options = (UpscalerOptions)ScriptableObject.CreateInstance(optionsType); options.upscalerName = upscaler.name; } if (options != null && string.IsNullOrEmpty(options.upscalerName)) { Debug.LogWarningFormat("[Upscaling] UpscalerOptions with empty upscalerName for {0}", upscaler.name); options.upscalerName = upscaler.name; } if (options != null) m_GlobalOptions[upscaler.name] = options; bool isEmbedded = embeddedTypes != null && embeddedTypes.Contains(upscalerType); m_Upscalers.Add(new UpscalerEntry(upscaler, isEmbedded ? UpscalerIntegrationType.EmbeddedPass : UpscalerIntegrationType.StandalonePass)); } // 2. Type-based sorting based on priorty order m_Upscalers.Sort((a, b) => { Type typeA = a.Instance.GetType(); Type typeB = b.Instance.GetType(); int indexA = -1; int indexB = -1; if (priorityOrder != null) { indexA = Array.IndexOf(priorityOrder, typeA); indexB = Array.IndexOf(priorityOrder, typeB); } // Priority Sort: If both are in the priority list, respect that order. if (indexA != -1 && indexB != -1) return indexA.CompareTo(indexB); // Mixed Sort: Priority items always come before non-priority items. if (indexA != -1) return -1; if (indexB != -1) return 1; // Fallback Sort: If neither are in the list (external upscalers), sort Alphabetically. return string.Compare(a.Instance.name, b.Instance.name, StringComparison.OrdinalIgnoreCase); }); // 3. Populate name cache m_UpscalerNamesCache = new string[m_Upscalers.Count]; for (int i = 0; i < m_Upscalers.Count; i++) { string name = m_Upscalers[i].Instance.name; m_UpscalerNamesCache[i] = name; } } /// /// Sets the active upscaler by name, returns whether an upscaler with the given name was found. /// public bool SetActiveUpscaler(string name) { int index = Array.IndexOf(m_UpscalerNamesCache, name); if (index == -1) { m_ActiveUpscalerIndex = -1; return false; } m_ActiveUpscalerIndex = index; // TODO (Apoorva): We need to allow the IUpscaler itself to decide whether it can run. E.g. // DLSS might need a certain version of Windows, and a compatible GPU. We should add an // overrideable function to IUpscaler so that the active IUpscaler can return a bool // indicating support. return true; } /// /// Returns the index of the upscalerName. -1 is returned if upscalerName is not in the name cache. /// public int IndexOf(string upscalerName) { return Array.IndexOf(m_UpscalerNamesCache, upscalerName); } /// /// Returns the registered IUpscaler instance with the given name, or null if none matches. Resolves the same /// name as (the registered upscaler name). /// public IUpscaler? GetIUpscalerByName(string upscalerName) { int index = IndexOf(upscalerName); return index >= 0 ? m_Upscalers[index].Instance : null; } /// /// Returns null if no IUpscaler exists for given type /// public IUpscaler? GetIUpscalerOfType() where T : IUpscaler { if(!UpscalerRegistry.s_RegisteredUpscalers.ContainsKey(typeof(T))) return null; foreach (UpscalerEntry entry in m_Upscalers) if (entry.Instance.GetType() == typeof(T)) return entry.Instance; Debug.LogErrorFormat($"Upscaler type {typeof(T)} not found"); return null; } /// /// Returns null if no IUpscaler exists for given type /// public IUpscaler? GetIUpscalerOfType(Type T) { if (!UpscalerRegistry.s_RegisteredUpscalers.ContainsKey(T)) return null; foreach (UpscalerEntry entry in m_Upscalers) if (entry.Instance.GetType() == T) return entry.Instance; Debug.LogErrorFormat($"Upscaler type {T} not found"); return null; } #region Options /// /// Returns the pipeline-wide ("global") options for the given upscaler — the live, user-configured options /// object the pipeline supplied (e.g. an RP-asset sub-asset), shared by all cameras — or null if the /// upscaler was registered without an options type (e.g. spatial/embedded upscalers). /// /// The upscaler whose global options to fetch. /// The pipeline-wide options, or null if the upscaler has no options type. /// /// This is not a copy and not factory defaults: editing the returned object (inspector or C# API) is reflected /// here, because it is the same serialized instance. Options are owned by the framework, not the /// instance. /// /// Per-camera options (future) layer on top of this: add an override-aware /// ResolveOptions(IUpscaler, UpscalerOptions perCameraOverride) returning /// perCameraOverride ?? GetGlobalOptions(upscaler), and have the call sites pass the camera's serialized /// override (read from the pipeline-specific camera component — core cannot see it). That is a non-breaking /// addition and this method stays as the global fallback. Not added now because no per-camera override source /// exists yet (it would be a no-op). /// /// public UpscalerOptions? GetGlobalOptions(IUpscaler upscaler) { return upscaler != null && m_GlobalOptions.TryGetValue(upscaler.name, out var options) ? options : null; } #endregion #region Context Management /// /// Acquires an upscaler context for the specified camera. /// Returns a cached context if valid, or creates a new one if missing or invalid. /// Also updates the context's last-used frame for expiry tracking. /// /// Unique ID for the camera. In XR scenarios, this must be unique per view (encode eye information into the ID). /// The upscaler to acquire a context for. /// The current upscaler options. /// The target display resolution. /// The context, or null for spatial upscalers that don't need context. public IUpscalerContext? AcquireContext( ulong cameraId, IUpscaler upscaler, UpscalerOptions options, Vector2Int displayResolution) { return m_ContextManager.AcquireContext(cameraId, upscaler, options, displayResolution); } /// /// Cleans up contexts that haven't been used for a while (400 frames). /// Should be called once per frame from the render pipeline. /// /// The command buffer to record cleanup commands into. public void CleanupExpiredContexts(CommandBuffer cmd) { m_ContextManager.CleanupExpiredContexts(cmd); } /// /// Cleans up all contexts. Call when the upscaling system is torn down (e.g. when the render /// pipeline is disposed) /// /// The command buffer to record cleanup commands into. public void Dispose(CommandBuffer cmd) { m_ContextManager.Dispose(cmd); } #endregion } } #endif