Files
Test/Library/PackageCache/com.unity.render-pipelines.core@13460d8ea9e5/Runtime/Upscaling/Upscaling.cs
T
2026-11-01 23:39:55 +02:00

453 lines
20 KiB
C#

#if ENABLE_UPSCALER_FRAMEWORK
#nullable enable
using System;
using System.Collections.Generic;
namespace UnityEngine.Rendering
{
/// <summary>
/// Manages per-camera upscaler contexts. Handles context creation, caching, validation, and expiry.
/// </summary>
internal class UpscalerContextManager
{
private struct ContextKey : IEquatable<ContextKey>
{
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<ContextKey, IUpscalerContext> m_Contexts = new();
private readonly List<ContextKey> m_KeysToRemove = new(); // Reusable list for cleanup
private readonly List<IUpscalerContext> m_InvalidatedContexts = new(); // Contexts pending cleanup
/// <summary>
/// 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.
/// </summary>
/// <param name="cameraId">Unique ID for the camera. In XR scenarios, this must be unique per view (encode eye information into the ID).</param>
/// <param name="upscaler">The upscaler to acquire a context for.</param>
/// <param name="options">The current upscaler options.</param>
/// <param name="displayResolution">The target display resolution.</param>
/// <returns>The context, or null for spatial upscalers that don't need context.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
/// Cleans up all contexts. Called when the upscaling system is disposed.
/// </summary>
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<Type, (Type? OptionsType, string ID)> s_RegisteredUpscalers = new();
/// <summary>
/// Registers an IUpscaler type without any custom options type.
/// </summary>
public static void Register<TUpscaler>(string id) where TUpscaler : IUpscaler, new()
{
s_RegisteredUpscalers[typeof(TUpscaler)] = (null, id);
}
/// <summary>
/// Registers an IUpscaler type with its custom options type.
/// </summary>
public static void Register<TUpscaler, TOptions>(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<UpscalerEntry> m_Upscalers = new List<UpscalerEntry>();
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<string, UpscalerOptions> m_GlobalOptions = new();
#endregion
/// <summary>
/// Returns the names of the upscalers registered to the upscaling system.
/// </summary>
public IReadOnlyList<string> upscalerNames => m_UpscalerNamesCache;
/// <summary>
/// Returns the active IUpscaler instance, null if none is selected.
/// </summary>
public IUpscaler? activeUpscaler => (m_ActiveUpscalerIndex >= 0) ? m_Upscalers[m_ActiveUpscalerIndex].Instance : null;
/// <summary>
/// Returns true if the active upscaler is embedded in an uber pass.
/// </summary>
public bool activeUpscalerIsEmbedded => (m_ActiveUpscalerIndex >= 0) && m_Upscalers[m_ActiveUpscalerIndex].IsEmbedded;
/// <summary>
/// Initializes the Upscaling system. with the given list of upscaler options per upscaler type.
/// </summary>
/// <param name="upscalerOptions">The list of options from the RP asset.</param>
/// <param name="embeddedTypes">A set of types that the pipeline handles internally (e.g., Bilinear, Point in URP).</param>
/// <param name="priorityOrder">
/// An ordered list of Types. Upscalers matching these types will appear first
/// in the list, in the order provided. All others appear after, alphabetically.
/// </param>
public Upscaling(
List<UpscalerOptions> upscalerOptions,
HashSet<Type>? 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;
}
}
/// <summary>
/// Sets the active upscaler by name, returns whether an upscaler with the given name was found.
/// </summary>
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;
}
/// <summary>
/// Returns the index of the upscalerName. -1 is returned if upscalerName is not in the name cache.
/// </summary>
public int IndexOf(string upscalerName)
{
return Array.IndexOf(m_UpscalerNamesCache, upscalerName);
}
/// <summary>
/// Returns the registered IUpscaler instance with the given name, or null if none matches. Resolves the same
/// name as <see cref="SetActiveUpscaler"/> (the registered upscaler name).
/// </summary>
public IUpscaler? GetIUpscalerByName(string upscalerName)
{
int index = IndexOf(upscalerName);
return index >= 0 ? m_Upscalers[index].Instance : null;
}
/// <summary>
/// Returns null if no IUpscaler exists for given type
/// </summary>
public IUpscaler? GetIUpscalerOfType<T>() 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;
}
/// <summary>
/// Returns null if no IUpscaler exists for given type
/// </summary>
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
/// <summary>
/// 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 <c>null</c> if the
/// upscaler was registered without an options type (e.g. spatial/embedded upscalers).
/// </summary>
/// <param name="upscaler">The upscaler whose global options to fetch.</param>
/// <returns>The pipeline-wide options, or null if the upscaler has no options type.</returns>
/// <remarks>
/// 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
/// <see cref="IUpscaler"/> instance.
/// <para>
/// Per-camera options (future) layer on top of this: add an override-aware
/// <c>ResolveOptions(IUpscaler, UpscalerOptions perCameraOverride)</c> returning
/// <c>perCameraOverride ?? GetGlobalOptions(upscaler)</c>, 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).
/// </para>
/// </remarks>
public UpscalerOptions? GetGlobalOptions(IUpscaler upscaler)
{
return upscaler != null && m_GlobalOptions.TryGetValue(upscaler.name, out var options) ? options : null;
}
#endregion
#region Context Management
/// <summary>
/// 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.
/// </summary>
/// <param name="cameraId">Unique ID for the camera. In XR scenarios, this must be unique per view (encode eye information into the ID).</param>
/// <param name="upscaler">The upscaler to acquire a context for.</param>
/// <param name="options">The current upscaler options.</param>
/// <param name="displayResolution">The target display resolution.</param>
/// <returns>The context, or null for spatial upscalers that don't need context.</returns>
public IUpscalerContext? AcquireContext(
ulong cameraId,
IUpscaler upscaler,
UpscalerOptions options,
Vector2Int displayResolution)
{
return m_ContextManager.AcquireContext(cameraId, upscaler, options, displayResolution);
}
/// <summary>
/// Cleans up contexts that haven't been used for a while (400 frames).
/// Should be called once per frame from the render pipeline.
/// </summary>
/// <param name="cmd">The command buffer to record cleanup commands into.</param>
public void CleanupExpiredContexts(CommandBuffer cmd)
{
m_ContextManager.CleanupExpiredContexts(cmd);
}
/// <summary>
/// Cleans up all contexts. Call when the upscaling system is torn down (e.g. when the render
/// pipeline is disposed)
/// </summary>
/// <param name="cmd">The command buffer to record cleanup commands into.</param>
public void Dispose(CommandBuffer cmd)
{
m_ContextManager.Dispose(cmd);
}
#endregion
}
}
#endif