Files
2026-11-01 23:39:55 +02:00

209 lines
9.4 KiB
C#

#if ENABLE_UPSCALER_FRAMEWORK
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine.Scripting;
using static UnityEngine.Rendering.DynamicResolutionHandler;
namespace UnityEngine.Rendering
{
#nullable enable
/// <summary>
/// Base ScriptableObject for holding configuration data for a specific upscaler.
/// Concrete implementations (e.g., DLSSOptions, FSR2Options) should inherit from this.
/// </summary>
[Serializable]
public class UpscalerOptions : ScriptableObject
{
/// <summary>
/// The unique identifier or display name of the upscaler associated with these options.
/// </summary>
public string upscalerName
{
get => m_UpscalerName;
set => m_UpscalerName = value;
}
/// <summary>
/// Determines at which stage in the render pipeline the upscaling pass is injected.
/// </summary>
public UpsamplerScheduleType injectionPoint
{
get => m_InjectionPoint;
set => m_InjectionPoint = value;
}
/// <summary>
/// Selects how the render resolution is determined for this upscaler. Only meaningful for upscalers that
/// expose a quality mode (<see cref="IUpscaler.hasQualityMode"/>); for the others it is dormant and the
/// upscaler always behaves as <see cref="UpscalerResolutionMode.CustomScaling"/>.
/// </summary>
public UpscalerResolutionMode resolutionMode
{
get => m_ResolutionMode;
set => m_ResolutionMode = value;
}
[SerializeField, HideInInspector]
private string m_UpscalerName = "";
[SerializeField, HideInInspector] // hide in inspector for URP, HDRP manually renders it
private UpsamplerScheduleType m_InjectionPoint = UpsamplerScheduleType.BeforePost;
[SerializeField]
[Tooltip("Determines how the render resolution is selected: from the upscaler's quality preset (Quality " +
"Mode), or driven by the pipeline's own render scaling / dynamic resolution (Custom Scaling). " +
"Ignored by upscalers without a quality mode.")]
private UpscalerResolutionMode m_ResolutionMode = UpscalerResolutionMode.QualityMode;
#if UNITY_EDITOR
/// <summary>
/// Validates and auto-populates the upscaler options list within a Render Pipeline Asset.
/// It ensures that for every registered upscaler, a corresponding Options sub-asset exists.
/// </summary>
/// <param name="parentRPAsset">The Render Pipeline Asset that will contain these options as sub-assets.</param>
/// <param name="optionsListProp">The SerializedProperty representing the list/array of UpscalerOptions.</param>
/// <returns>True if the property was modified (cleaned up or populated), false otherwise.</returns>
public static bool ValidateSerializedUpscalerOptionReferencesWithinRPAsset(ScriptableObject parentRPAsset, SerializedProperty optionsListProp)
{
if (parentRPAsset == null)
{
Debug.LogError("[Auto-Populate] Parent asset is null.");
return false;
}
if (optionsListProp == null || !optionsListProp.isArray)
{
Debug.LogError($"[Auto-Populate] Provided SerializedProperty '{optionsListProp?.name ?? "null"}' is not a valid list for upscaler options.");
return false;
}
bool propertyModified = false;
// Track valid objects to identify orphans later
HashSet<Object> validReferencedObjects = new HashSet<Object>();
HashSet<Type> typesInList = new HashSet<Type>();
// =================================================================================
// PASS 1: Clean up the Serialized List (Nulls & Duplicate Types)
// =================================================================================
for (int i = optionsListProp.arraySize - 1; i >= 0; i--)
{
SerializedProperty elementProp = optionsListProp.GetArrayElementAtIndex(i);
Object objRef = elementProp.objectReferenceValue;
// 1. Remove Nulls
if (objRef == null)
{
// TODO: find a way to properly remove abandoned upscaler options.
// there's no good way to remove null options from the RPAsset with Delete/Destroy calls.
// one alternative is to copy non-null entries into memory and rewrite the RPAsset.
//
// optionsListProp.DeleteArrayElementAtIndex(i);
// propertyModified = true;
// Debug.LogWarning($"[RP Asset] Removed null upscaler option from active list in '{parentRPAsset.name}'.");
continue;
}
// 2. Check for Duplicate Types within the active list
// (e.g., The list somehow has two FSR2Options. We keep the first, remove the rest).
Type objType = objRef.GetType();
if (typesInList.Contains(objType))
{
optionsListProp.DeleteArrayElementAtIndex(i);
propertyModified = true;
Debug.LogWarning($"[RP Asset] Removed duplicate active reference of type {objType.Name} from list in '{parentRPAsset.name}'.");
// We do NOT add to validReferencedObjects, so it will be caught in Pass 2 and destroyed.
}
else
{
typesInList.Add(objType);
validReferencedObjects.Add(objRef);
}
}
// =================================================================================
// PASS 2: Ghost Hunting (Destroy orphaned Sub-Assets)
// =================================================================================
// Load ALL assets contained within the file
string assetPath = AssetDatabase.GetAssetPath(parentRPAsset);
Object[] allSubAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
List<Object> assetsToDestroy = new List<Object>();
foreach (Object subAsset in allSubAssets)
{
// We only care about UpscalerOptions. Ignore the main RPAsset or other embedded data.
// We also check if it is HideInHierarchy/HideInInspector, which usually indicates a sub-asset.
if (subAsset is UpscalerOptions)
{
// If this asset exists in the file, but is NOT in our 'validReferencedObjects' set, it is an orphan.
if (!validReferencedObjects.Contains(subAsset))
{
assetsToDestroy.Add(subAsset);
}
}
}
if (assetsToDestroy.Count > 0)
{
Debug.Log($"[RP Asset] Found {assetsToDestroy.Count} orphaned/duplicate sub-assets in '{parentRPAsset.name}'. Cleaning up...");
foreach (Object orphan in assetsToDestroy)
{
Debug.Log($"[RP Asset] Destroying orphan: {orphan.name} ({orphan.GetType().Name})");
// Crucial: Object.DestroyImmediate with 'true' allows destroying assets in the Editor.
// This removes the YAML block entirely.
Object.DestroyImmediate(orphan, true);
}
propertyModified = true;
}
// =================================================================================
// PASS 3: Populate Missing Required Options
// =================================================================================
foreach (var kvp in UpscalerRegistry.s_RegisteredUpscalers)
{
Type upscalerType = kvp.Key;
Type? optionsType = kvp.Value.OptionsType;
if (optionsType == null)
continue;
string upscalerName = kvp.Value.ID;
// Check if we already have this type in our tracked types from Pass 1
bool foundExisting = typesInList.Contains(optionsType);
if (!foundExisting)
{
UpscalerOptions newOption = (UpscalerOptions)ScriptableObject.CreateInstance(optionsType);
newOption.hideFlags = HideFlags.HideInHierarchy; // Standard for embedded sub-assets
newOption.name = optionsType.Name; // Give it a clean name in the file
newOption.upscalerName = upscalerName;
// Add the physical object to the main asset file
AssetDatabase.AddObjectToAsset(newOption, parentRPAsset);
// Add the reference to the list
optionsListProp.arraySize++;
optionsListProp.GetArrayElementAtIndex(optionsListProp.arraySize - 1).objectReferenceValue = newOption;
propertyModified = true;
Debug.Log($"[RP Asset] Created missing upscaler option on asset '{parentRPAsset.name}': {optionsType.Name} for ID: {upscalerName}");
}
}
return propertyModified;
}
#endif
}
#nullable disable
}
#endif // ENABLE_UPSCALER_FRAMEWORK