using System; using System.Runtime.InteropServices; using FidelityFX; using FidelityFX.FSR2; using UnityEngine.Experimental.Rendering; namespace UnityEngine.Rendering.PostProcessing { internal abstract class Upscaler { public abstract void CreateContext(PostProcessRenderContext context, Upscaling config); public virtual void DestroyContext() { DestroyRenderTexture(ref _reactiveMask); DestroyConstantsBuffer(ref _reactiveMaskConstants); DestroyConstantsBuffer(ref _sharpeningConstants); } public abstract void Render(PostProcessRenderContext context, Upscaling config); private ConstantsBuffer _reactiveMaskConstants; private RenderTexture _reactiveMask; /// /// Generalized standalone version of the FSR2 reactive mask auto-generating pass that can be used without needing an active FSR2 context. /// This allows auto-generated reactive masks to be reused for other non-FSR upscaling techniques. /// protected Texture GenerateReactiveMask(CommandBuffer cmd, PostProcessRenderContext context, Upscaling config, GraphicsFormat format = GraphicsFormat.R8_UNorm) { ComputeShader shader = context.resources.computeShaders.fsr2Upscaler?.autoGenReactivePass; if (shader == null) return Texture2D.blackTexture; _reactiveMaskConstants ??= ConstantsBuffer.Create(); if (_reactiveMask == null) { // Use a persistent RT so it can easily be passed to native render plugins CreateRenderTexture(ref _reactiveMask, "Reactive Mask", config.MaxRenderSize, format, true); } Vector2Int scaledRenderSize = config.GetScaledRenderSize(context.camera); const int threadGroupWorkRegionDim = 8; int dispatchX = (scaledRenderSize.x + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; int dispatchY = (scaledRenderSize.y + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; cmd.BeginSample("Generate Reactive Mask"); _reactiveMaskConstants.Value.scale = config.generateReactiveParameters.scale; _reactiveMaskConstants.Value.threshold = config.generateReactiveParameters.cutoffThreshold; _reactiveMaskConstants.Value.binaryValue = config.generateReactiveParameters.binaryValue; _reactiveMaskConstants.Value.flags = (uint)config.generateReactiveParameters.flags; _reactiveMaskConstants.UpdateBufferData(cmd); int kernelIndex = shader.FindKernel("CS"); cmd.SetComputeTextureParam(shader, kernelIndex, Fsr2ShaderIDs.SrvOpaqueOnly, config.ColorOpaqueOnly); cmd.SetComputeTextureParam(shader, kernelIndex, Fsr2ShaderIDs.SrvInputColor, context.source); cmd.SetComputeTextureParam(shader, kernelIndex, Fsr2ShaderIDs.UavAutoReactive, _reactiveMask); cmd.SetComputeConstantBufferParam(shader, Fsr2ShaderIDs.CbGenReactive, _reactiveMaskConstants, 0, Marshal.SizeOf()); cmd.DispatchCompute(shader, kernelIndex, dispatchX, dispatchY, 1); cmd.EndSample("Generate Reactive Mask"); return _reactiveMask; } [Serializable, StructLayout(LayoutKind.Sequential)] internal struct GenerateReactiveConstants { public float scale; public float threshold; public float binaryValue; public uint flags; } private ConstantsBuffer _sharpeningConstants; private static readonly int CasInputColor = Shader.PropertyToID("r_input_color"); private static readonly int CasOutputColor = Shader.PropertyToID("rw_output_color"); private static readonly int CasConstantBuffer = Shader.PropertyToID("cbCAS"); /// /// Generalized standalone version of the CAS sharpening filter that can be applied after any non-FSR upscaling technique. /// protected void ApplySharpening(CommandBuffer cmd, PostProcessRenderContext context, in Vector2Int imageSize, float sharpness, RenderTargetIdentifier input, RenderTargetIdentifier output) { ComputeShader shader = context.resources.computeShaders.casSharpening; if (shader == null) { cmd.CopyTexture(input, output); return; } const int threadGroupWorkRegionDimRcas = 16; int threadGroupsX = (imageSize.x + threadGroupWorkRegionDimRcas - 1) / threadGroupWorkRegionDimRcas; int threadGroupsY = (imageSize.y + threadGroupWorkRegionDimRcas - 1) / threadGroupWorkRegionDimRcas; cmd.BeginSample("CAS Sharpening"); // Compute the constants _sharpeningConstants ??= ConstantsBuffer.Create(); int sharpnessIndex = Mathf.RoundToInt(Mathf.Clamp01(sharpness) * (CasConfigs.Length - 1)); _sharpeningConstants.Value = CasConfigs[sharpnessIndex]; _sharpeningConstants.UpdateBufferData(cmd); // Dispatch CAS int kernelIndex = shader.FindKernel("CS"); cmd.SetComputeTextureParam(shader, kernelIndex, CasInputColor, input); cmd.SetComputeTextureParam(shader, kernelIndex, CasOutputColor, output); cmd.SetComputeConstantBufferParam(shader, CasConstantBuffer, _sharpeningConstants, 0, Marshal.SizeOf()); cmd.DispatchCompute(shader, kernelIndex, threadGroupsX, threadGroupsY, 1); cmd.EndSample("CAS Sharpening"); } [Serializable, StructLayout(LayoutKind.Sequential)] private struct CasConstants { public CasConstants(uint sharpness, uint halfSharp) { // Since we don't use CAS for scaling, most of these values end up being constants scaling0 = scaling1 = 1065353216; scaling2 = scaling3 = 0; sharpness0 = sharpness; sharpness1 = halfSharp; sharpness2 = 1090519040; sharpness3 = 0; } public readonly uint scaling0; public readonly uint scaling1; public readonly uint scaling2; public readonly uint scaling3; public readonly uint sharpness0; public readonly uint sharpness1; public readonly uint sharpness2; public readonly uint sharpness3; } /// /// The FidelityFX C++ codebase uses floats bitwise converted to ints to pass sharpness parameters to the CAS shader. /// This is not possible in C# without enabling unsafe code compilation, so to avoid that we instead use a table of precomputed values. /// private static readonly CasConstants[] CasConfigs = { new(3187671040u, 45056u), new(3187831332u, 45075u), new(3187997869u, 45095u), new(3188171023u, 45117u), new(3188351197u, 45139u), new(3188538827u, 45161u), new(3188734385u, 45185u), new(3188938384u, 45210u), new(3189151382u, 45236u), new(3189373991u, 45263u), new(3189606873u, 45292u), new(3189850757u, 45322u), new(3190106443u, 45353u), new(3190374807u, 45386u), new(3190656816u, 45420u), new(3190953540u, 45456u), new(3191266159u, 45494u), new(3191595985u, 45535u), new(3191944482u, 45577u), new(3192313280u, 45622u), new(3192704205u, 45670u), }; protected bool CreateRenderTexture(ref RenderTexture rt, string name, in Vector2Int size, GraphicsFormat format, bool enableRandomWrite = false) { rt = new RenderTexture(size.x, size.y, 0, format) { name = name, enableRandomWrite = enableRandomWrite }; return rt.Create(); } protected bool CreateRenderTexture(ref RenderTexture rt, string name, in Vector2Int size, RenderTextureFormat format, bool enableRandomWrite = false) { rt = new RenderTexture(size.x, size.y, 0, format) { name = name, enableRandomWrite = enableRandomWrite }; return rt.Create(); } protected void DestroyRenderTexture(ref RenderTexture rt) { if (rt == null) return; rt.Release(); rt = null; } protected void DestroyConstantsBuffer(ref ConstantsBuffer cb) where TConst: struct { if (cb == null) return; cb.Destroy(); cb = null; } } }