#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