Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions Editor/UrpUpscalerBuildResources.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace VisualPinball.Unity.Urp
{
/// <summary>Keeps STP's runtime resources when users can select it after the build.</summary>
public sealed class UrpUpscalerBuildResources : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
// After CorePreprocessBuild (+50), before URPPreprocessBuild (+100) snapshots the list.
public int callbackOrder => int.MinValue + 75;
private static UniversalRenderPipelineAsset _retentionAsset;
private static List<RenderPipelineAsset> _buildAssets;

public void OnPreprocessBuild(BuildReport report)
{
Cleanup();
var assets = CoreBuildData.instance.renderPipelineAssets;
UniversalRenderPipelineAsset source = null;
foreach (var asset in assets) {
if (asset is UniversalRenderPipelineAsset urp) {
if (urp.isStpUsed) return;
source ??= urp;
}
}
if (source == null) return;
// Only the transient build-data list changes. Project assets and quality tiers stay untouched.
_retentionAsset = Object.Instantiate(source);
_retentionAsset.name = "URP runtime STP resource retention";
_retentionAsset.hideFlags = HideFlags.HideAndDontSave;
_retentionAsset.upscalingFilter = UpscalingFilterSelection.STP;
#if ENABLE_UPSCALER_FRAMEWORK
_retentionAsset.upscalerName = "Spatial-Temporal Post-Processing";
#endif
_buildAssets = assets;
assets.Add(_retentionAsset);
EditorApplication.update += CleanupAfterBuild;
AssemblyReloadEvents.beforeAssemblyReload += Cleanup;
}

public void OnPostprocessBuild(BuildReport report) => Cleanup();

private static void CleanupAfterBuild()
{
// Postprocess callbacks may be skipped on failure/cancellation.
if (!BuildPipeline.isBuildingPlayer) Cleanup();
}

private static void Cleanup()
{
EditorApplication.update -= CleanupAfterBuild;
AssemblyReloadEvents.beforeAssemblyReload -= Cleanup;
if (_buildAssets != null && _retentionAsset != null) _buildAssets.Remove(_retentionAsset);
_buildAssets = null;
if (_retentionAsset != null) Object.DestroyImmediate(_retentionAsset);
_retentionAsset = null;
}
}
}
2 changes: 2 additions & 0 deletions Editor/UrpUpscalerBuildResources.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
"VisualPinball.Unity.Editor",
"VisualPinball.Engine.Unity.Urp",
"Unity.RenderPipelines.Core.Runtime",
"Unity.RenderPipelines.Universal.Runtime"
"Unity.RenderPipelines.Universal.Runtime",
"Unity.RenderPipelines.Core.Editor"
],
"includePlatforms": [
"Editor"
],
"includePlatforms": [ "Editor" ],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
Expand Down
171 changes: 167 additions & 4 deletions Runtime/UrpGraphicsApplier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,183 @@ namespace VisualPinball.Unity.Urp
{
/// <summary>
/// Applies the host app's <see cref="VpeGraphicsSettings"/> to URP at runtime: the game camera's
/// anti-aliasing and the global Volume's bloom. The other effects are URP renderer features, which
/// the renderer assets configure.
/// anti-aliasing, global Volume bloom and the active pipeline's SSR/SSAO renderer features.
/// </summary>
public sealed class UrpGraphicsApplier : IVpeGraphicsApplier
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Register() => VpeGraphics.Register(new UrpGraphicsApplier());
private static void Register()
{
RestoreUpscalerState();
_upscaler = VpeUpscaler.Off;
_antiAliasing = -1;
VpeGraphics.Register(new UrpGraphicsApplier());
VpeGraphics.UpscalerSupport = IsUpscalerSupported;
VpeGraphics.RenderScaleSource = () => GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset p ? p.renderScale : 1f;
#if UNITY_EDITOR
UnityEditor.EditorApplication.playModeStateChanged -= OnEditorPlayModeChanged;
UnityEditor.EditorApplication.playModeStateChanged += OnEditorPlayModeChanged;
#endif
}

#if UNITY_EDITOR
private static readonly System.Collections.Generic.Dictionary<ScriptableRendererFeature, bool> OriginalFeatureStates = new();

private static void OnEditorPlayModeChanged(UnityEditor.PlayModeStateChange state)
{
if (state != UnityEditor.PlayModeStateChange.ExitingPlayMode) return;
foreach (var item in OriginalFeatureStates) {
if (item.Key != null) item.Key.SetActive(item.Value);
}
OriginalFeatureStates.Clear();
RestoreUpscalerState();
}
#endif

// the user's choice (-1 = none yet, the camera keeps its own)
private static int _antiAliasing = -1;
private static VpeUpscaler _upscaler;
private static int _upscalerQuality;

private struct PipelineState
{
public float Scale;
public int Msaa;
public UpscalingFilterSelection Filter;
#if ENABLE_UPSCALER_FRAMEWORK
public string Name;
#endif
}
private struct CameraState { public bool PostProcessing, DynamicResolution; public VpeUpscaler LastUpscaler; public float LastScale; }
private static readonly System.Collections.Generic.Dictionary<UniversalRenderPipelineAsset, PipelineState> PipelineStates = new();
private static readonly System.Collections.Generic.Dictionary<Camera, CameraState> CameraStates = new();

private static bool IsUpscalerSupported(VpeUpscaler upscaler) => upscaler switch {
VpeUpscaler.Off => true,
VpeUpscaler.Fsr1 => FSRUtils.IsSupported(),
VpeUpscaler.Stp => STP.IsSupported(),
_ => false,
};

// Ratios are absolute output-resolution fractions, not multiplied by the preset's render scale.
private static float UpscalerScale(int quality) => Mathf.Clamp(quality, 0, 3) switch {
0 => 1f / 1.5f,
1 => 1f / 1.7f,
2 => 0.5f,
_ => 1f / 3f,
};

private static void RestoreUpscalerState()
{
foreach (var item in PipelineStates) {
if (item.Key == null) continue;
RestorePipeline(item.Key, item.Value);
}
PipelineStates.Clear();
foreach (var item in CameraStates) {
if (item.Key == null) continue;
item.Key.allowDynamicResolution = item.Value.DynamicResolution;
if (item.Key.TryGetComponent<UniversalAdditionalCameraData>(out var data)) data.renderPostProcessing = item.Value.PostProcessing;
}
CameraStates.Clear();
}

private static void RestorePipeline(UniversalRenderPipelineAsset pipeline, PipelineState original)
{
pipeline.renderScale = original.Scale;
pipeline.msaaSampleCount = original.Msaa;
pipeline.upscalingFilter = original.Filter;
#if ENABLE_UPSCALER_FRAMEWORK
pipeline.upscalerName = original.Name;
#endif
}

private static VpeUpscaler ApplyUpscaler(Camera camera, UniversalAdditionalCameraData data)
{
if (GraphicsSettings.currentRenderPipeline is not UniversalRenderPipelineAsset pipeline) return VpeUpscaler.Off;
if (!PipelineStates.TryGetValue(pipeline, out var original)) {
original = new PipelineState { Scale = pipeline.renderScale, Msaa = pipeline.msaaSampleCount, Filter = pipeline.upscalingFilter };
#if ENABLE_UPSCALER_FRAMEWORK
original.Name = pipeline.upscalerName;
#endif
PipelineStates.Add(pipeline, original);
}
if (!CameraStates.TryGetValue(camera, out var cameraOriginal)) {
cameraOriginal = new CameraState { PostProcessing = data.renderPostProcessing, DynamicResolution = camera.allowDynamicResolution, LastScale = original.Scale };
CameraStates.Add(camera, cameraOriginal);
}
var effective = IsUpscalerSupported(_upscaler) ? _upscaler : VpeUpscaler.Off;
// STP shares URP TAA's restriction against camera stacks and overlay cameras.
if (effective == VpeUpscaler.Stp && (data.renderType == CameraRenderType.Overlay || data.cameraStack?.Count > 0 || CabinetProjection.EffectiveAntiAliasing(camera, VpeAntiAliasing.Taa) != VpeAntiAliasing.Taa)) effective = VpeUpscaler.Off;
if (effective == VpeUpscaler.Off) {
RestorePipeline(pipeline, original);
if (cameraOriginal.LastUpscaler != VpeUpscaler.Off || !Mathf.Approximately(cameraOriginal.LastScale, original.Scale)) data.resetHistory = true;
cameraOriginal.LastUpscaler = VpeUpscaler.Off;
cameraOriginal.LastScale = original.Scale;
CameraStates[camera] = cameraOriginal;
data.renderPostProcessing = cameraOriginal.PostProcessing;
camera.allowDynamicResolution = cameraOriginal.DynamicResolution;
return effective;
}
pipeline.renderScale = UpscalerScale(_upscalerQuality);
pipeline.msaaSampleCount = effective == VpeUpscaler.Stp ? 1 : original.Msaa;
pipeline.upscalingFilter = effective == VpeUpscaler.Stp ? UpscalingFilterSelection.STP : UpscalingFilterSelection.FSR;
#if ENABLE_UPSCALER_FRAMEWORK
pipeline.upscalerName = effective == VpeUpscaler.Stp ? "Spatial-Temporal Post-Processing" : "FidelityFX Super Resolution 1.0";
#endif
data.renderPostProcessing = true;
camera.allowDynamicResolution = false;
if (cameraOriginal.LastUpscaler != effective || !Mathf.Approximately(cameraOriginal.LastScale, pipeline.renderScale)) data.resetHistory = true;
cameraOriginal.LastUpscaler = effective;
cameraOriginal.LastScale = pipeline.renderScale;
CameraStates[camera] = cameraOriginal;
return effective;
}

public void Apply(VpeGraphicsSettings settings)
{
_antiAliasing = Mathf.Clamp(settings.AntiAliasing, 0, 3);
_upscaler = (VpeUpscaler)settings.Upscaler;
_upscalerQuality = Mathf.Clamp(settings.UpscalerQuality, 0, 3);
ApplyCameraAntialiasing(Camera.main);
ApplyVolume(settings);
ApplyRendererFeatures(settings);
}

private static void ApplyRendererFeatures(VpeGraphicsSettings settings)
{
if (GraphicsSettings.currentRenderPipeline is not UniversalRenderPipelineAsset pipeline) {
return;
}
foreach (var renderer in pipeline.rendererDataList) {
if (renderer == null) {
continue;
}
foreach (var feature in renderer.rendererFeatures) {
if (feature == null) {
continue;
}
// SSR is an optional third-party assembly; match its type rather than its editable asset name.
switch (feature.GetType().FullName) {
case "UniversalScreenSpaceReflection.ScreenSpaceReflection":
SetFeatureActive(feature, settings.ScreenSpaceReflections);
break;
case "UnityEngine.Rendering.Universal.ScreenSpaceAmbientOcclusion":
SetFeatureActive(feature, settings.AmbientOcclusion);
break;
}
}
}
}

private static void SetFeatureActive(ScriptableRendererFeature feature, bool enabled)
{
#if UNITY_EDITOR
if (Application.isPlaying && !OriginalFeatureStates.ContainsKey(feature)) {
OriginalFeatureStates.Add(feature, feature.isActive);
}
#endif
feature.SetActive(enabled);
}

/// <summary>The chosen anti-aliasing, or what the camera's cabinet projection allows instead.</summary>
Expand All @@ -49,8 +210,10 @@ internal static void ApplyCameraAntialiasing(Camera camera)
if (camera == null || !camera.TryGetComponent<UniversalAdditionalCameraData>(out var data)) {
return;
}
var upscaler = ApplyUpscaler(camera, data);
if (_antiAliasing >= 0) {
var aa = CabinetProjection.EffectiveAntiAliasing(camera, (VpeAntiAliasing)_antiAliasing);
var requested = upscaler == VpeUpscaler.Stp ? VpeAntiAliasing.Taa : (VpeAntiAliasing)_antiAliasing;
var aa = CabinetProjection.EffectiveAntiAliasing(camera, requested);
data.antialiasing = aa switch {
VpeAntiAliasing.Fxaa => AntialiasingMode.FastApproximateAntialiasing,
VpeAntiAliasing.Smaa => AntialiasingMode.SubpixelMorphologicalAntiAliasing,
Expand Down