using System; using System.Collections.Generic; using UnityEngine.Rendering; namespace UnityEngine.Experimental.Rendering.RenderGraphModule { /// /// The mode that determines the size of a Texture. /// #region Resource Descriptors public enum TextureSizeMode { Explicit, Scale, Functor } /// /// Descriptor used to create texture resources /// public struct TextureDesc { public TextureSizeMode sizeMode; public int width; public int height; public int slices; public Vector2 scale; public ScaleFunc func; public DepthBits depthBufferBits; public GraphicsFormat colorFormat; public FilterMode filterMode; public TextureWrapMode wrapMode; public TextureDimension dimension; public bool enableRandomWrite; public bool useMipMap; public bool autoGenerateMips; public bool isShadowMap; public int anisoLevel; public float mipMapBias; public bool enableMSAA; // Only supported for Scale and Functor size mode public MSAASamples msaaSamples; // Only supported for Explicit size mode public bool bindTextureMS; public bool useDynamicScale; public RenderTextureMemoryless memoryless; public string name; // Initial state. Those should not be used in the hash public bool clearBuffer; public Color clearColor; void InitDefaultValues(bool dynamicResolution, bool xrReady) { useDynamicScale = dynamicResolution; // XR Ready if (xrReady) { slices = TextureXR.slices; dimension = TextureXR.dimension; } else { slices = 1; dimension = TextureDimension.Tex2D; } } /// /// TextureDesc constructor for a texture using explicit size /// /// Texture width /// Texture height /// Use dynamic resolution /// Set this to true if the Texture is a render texture in an XR setting. public TextureDesc(int width, int height, bool dynamicResolution = false, bool xrReady = false) : this() { // Size related init sizeMode = TextureSizeMode.Explicit; this.width = width; this.height = height; // Important default values not handled by zero construction in this() msaaSamples = MSAASamples.None; InitDefaultValues(dynamicResolution, xrReady); } /// /// TextureDesc constructor for a texture using a fixed scaling /// /// RTHandle scale used for this texture /// Use dynamic resolution /// Set this to true if the Texture is a render texture in an XR setting. public TextureDesc(Vector2 scale, bool dynamicResolution = false, bool xrReady = false) : this() { // Size related init sizeMode = TextureSizeMode.Scale; this.scale = scale; // Important default values not handled by zero construction in this() msaaSamples = MSAASamples.None; dimension = TextureDimension.Tex2D; InitDefaultValues(dynamicResolution, xrReady); } /// /// TextureDesc constructor for a texture using a functor for scaling /// /// Function used to determnine the texture size /// Use dynamic resolution /// Set this to true if the Texture is a render texture in an XR setting. public TextureDesc(ScaleFunc func, bool dynamicResolution = false, bool xrReady = false) : this() { // Size related init sizeMode = TextureSizeMode.Functor; this.func = func; // Important default values not handled by zero construction in this() msaaSamples = MSAASamples.None; dimension = TextureDimension.Tex2D; InitDefaultValues(dynamicResolution, xrReady); } /// /// Copy constructor /// /// public TextureDesc(TextureDesc input) { this = input; } /// /// Hash function /// /// The texture descriptor hash. public override int GetHashCode() { int hashCode = 17; unchecked { switch (sizeMode) { case TextureSizeMode.Explicit: hashCode = hashCode * 23 + width; hashCode = hashCode * 23 + height; hashCode = hashCode * 23 + (int)msaaSamples; break; case TextureSizeMode.Functor: if (func != null) hashCode = hashCode * 23 + func.GetHashCode(); hashCode = hashCode * 23 + (enableMSAA ? 1 : 0); break; case TextureSizeMode.Scale: hashCode = hashCode * 23 + scale.x.GetHashCode(); hashCode = hashCode * 23 + scale.y.GetHashCode(); hashCode = hashCode * 23 + (enableMSAA ? 1 : 0); break; } hashCode = hashCode * 23 + mipMapBias.GetHashCode(); hashCode = hashCode * 23 + slices; hashCode = hashCode * 23 + (int)depthBufferBits; hashCode = hashCode * 23 + (int)colorFormat; hashCode = hashCode * 23 + (int)filterMode; hashCode = hashCode * 23 + (int)wrapMode; hashCode = hashCode * 23 + (int)dimension; hashCode = hashCode * 23 + (int)memoryless; hashCode = hashCode * 23 + anisoLevel; hashCode = hashCode * 23 + (enableRandomWrite ? 1 : 0); hashCode = hashCode * 23 + (useMipMap ? 1 : 0); hashCode = hashCode * 23 + (autoGenerateMips ? 1 : 0); hashCode = hashCode * 23 + (isShadowMap ? 1 : 0); hashCode = hashCode * 23 + (bindTextureMS ? 1 : 0); hashCode = hashCode * 23 + (useDynamicScale ? 1 : 0); } return hashCode; } } #endregion /// /// The RenderGraphResourceRegistry holds all resource allocated during Render Graph execution. /// public class RenderGraphResourceRegistry { static readonly ShaderTagId s_EmptyName = new ShaderTagId(""); #region Resources internal struct TextureResource { public TextureDesc desc; public bool imported; public RTHandle rt; public int cachedHash; public int firstWritePassIndex; public int lastReadPassIndex; public int shaderProperty; public bool wasReleased; internal TextureResource(RTHandle rt, int shaderProperty) : this() { Reset(); this.rt = rt; imported = true; this.shaderProperty = shaderProperty; } internal TextureResource(in TextureDesc desc, int shaderProperty) : this() { Reset(); this.desc = desc; this.shaderProperty = shaderProperty; } void Reset() { imported = false; rt = null; cachedHash = -1; firstWritePassIndex = int.MaxValue; lastReadPassIndex = -1; wasReleased = false; } } internal struct RendererListResource { public RendererListDesc desc; public RendererList rendererList; internal RendererListResource(in RendererListDesc desc) { this.desc = desc; this.rendererList = new RendererList(); // Invalid by default } } #endregion #region Helpers class ResourceArray { // No List<> here because we want to be able to access and update elements by ref // And we want to avoid allocation so TextureResource stays a struct T[] m_ResourceArray = new T[32]; int m_ResourcesCount = 0; public void Clear() { m_ResourcesCount = 0; } public int Add(T value) { int index = m_ResourcesCount; // Grow array if needed; if (index >= m_ResourceArray.Length) { var newArray = new T[m_ResourceArray.Length * 2]; Array.Copy(m_ResourceArray, newArray, m_ResourceArray.Length); m_ResourceArray = newArray; } m_ResourceArray[index] = value; m_ResourcesCount++; return index; } public ref T this[int index] { get { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (index >= m_ResourcesCount) throw new IndexOutOfRangeException(); #endif return ref m_ResourceArray[index]; } } } #endregion ResourceArray m_TextureResources = new ResourceArray(); Dictionary> m_TexturePool = new Dictionary>(); ResourceArray m_RendererListResources = new ResourceArray(); RTHandleSystem m_RTHandleSystem = new RTHandleSystem(); RenderGraphDebugParams m_RenderGraphDebug; RenderGraphLogger m_Logger; // Diagnostic only List<(int, RTHandle)> m_AllocatedTextures = new List<(int, RTHandle)>(); #region Public Interface /// /// Returns the RTHandle associated with the provided resource handle. /// /// Handle to a texture resource. /// The RTHandle associated with the provided resource handle. public RTHandle GetTexture(in RenderGraphResource handle) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (handle.type != RenderGraphResourceType.Texture) throw new InvalidOperationException("Trying to access a RenderGraphResource that is not a texture."); var res = m_TextureResources[handle.handle]; if (res.rt == null && !res.wasReleased) throw new InvalidOperationException(string.Format("Trying to access texture \"{0}\" that was never created. Check that it was written at least once before trying to get it.", res.desc.name)); if (res.rt == null && res.wasReleased) throw new InvalidOperationException(string.Format("Trying to access texture \"{0}\" that was already released. Check that the last pass where it's read is after this one.", res.desc.name)); #endif return m_TextureResources[handle.handle].rt; } /// /// Returns the RendererList associated with the provided resource handle. /// /// Handle to a Renderer List resource. /// The Renderer List associated with the provided resource handle. public RendererList GetRendererList(in RenderGraphResource handle) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (handle.type != RenderGraphResourceType.RendererList) throw new InvalidOperationException("Trying to access a RenderGraphResource that is not a RendererList."); #endif return m_RendererListResources[handle.handle].rendererList; } #endregion #region Internal Interface private RenderGraphResourceRegistry() { } internal RenderGraphResourceRegistry(bool supportMSAA, MSAASamples initialSampleCount, RenderGraphDebugParams renderGraphDebug, RenderGraphLogger logger) { m_RTHandleSystem.Initialize(1, 1, supportMSAA, initialSampleCount); m_RenderGraphDebug = renderGraphDebug; m_Logger = logger; } internal void SetRTHandleReferenceSize(int width, int height, MSAASamples msaaSamples) { m_RTHandleSystem.SetReferenceSize(width, height, msaaSamples); } internal RTHandleProperties GetRTHandleProperties() { return m_RTHandleSystem.rtHandleProperties; } // Texture Creation/Import APIs are internal because creation should only go through RenderGraph internal RenderGraphMutableResource ImportTexture(RTHandle rt, int shaderProperty = 0) { int newHandle = m_TextureResources.Add(new TextureResource(rt, shaderProperty)); return new RenderGraphMutableResource(newHandle, RenderGraphResourceType.Texture); } internal RenderGraphMutableResource CreateTexture(in TextureDesc desc, int shaderProperty = 0) { ValidateTextureDesc(desc); int newHandle = m_TextureResources.Add(new TextureResource(desc, shaderProperty)); return new RenderGraphMutableResource(newHandle, RenderGraphResourceType.Texture); } internal void UpdateTextureFirstWrite(RenderGraphResource tex, int passIndex) { ref var res = ref GetTextureResource(tex); res.firstWritePassIndex = Math.Min(passIndex, res.firstWritePassIndex); //// We increment lastRead index here so that a resource used only for a single pass can be released at the end of said pass. //// This will also keep the resource alive as long as it is written to. //// Typical example is a depth buffer that may never be explicitly read from but is necessary all along /// // PROBLEM: Increasing last read on write operation will keep the target alive even if it's not used at all so it's not good. // If we don't do it though, it means that client code cannot write "by default" into a target as it will try to write to an already released target. // Example: // DepthPrepass: Writes to Depth and Normal buffers (pass will create normal buffer) // ObjectMotion: Writes to MotionVectors and Normal => Exception because NormalBuffer is already released as it not used. // => Solution includes : Shader Combination (without MRT for example) / Dummy Targets //res.lastReadPassIndex = Math.Max(passIndex, res.lastReadPassIndex); } internal void UpdateTextureLastRead(RenderGraphResource tex, int passIndex) { ref var res = ref GetTextureResource(tex); res.lastReadPassIndex = Math.Max(passIndex, res.lastReadPassIndex); } ref TextureResource GetTextureResource(RenderGraphResource res) { return ref m_TextureResources[res.handle]; } internal TextureDesc GetTextureResourceDesc(RenderGraphResource res) { return m_TextureResources[res.handle].desc; } internal RenderGraphResource CreateRendererList(in RendererListDesc desc) { ValidateRendererListDesc(desc); int newHandle = m_RendererListResources.Add(new RendererListResource(desc)); return new RenderGraphResource(newHandle, RenderGraphResourceType.RendererList); } internal void CreateAndClearTexturesForPass(RenderGraphContext rgContext, int passIndex, List textures) { foreach (var rgResource in textures) { ref var resource = ref GetTextureResource(rgResource); if (!resource.imported && resource.firstWritePassIndex == passIndex) { CreateTextureForPass(ref resource); if (resource.desc.clearBuffer || m_RenderGraphDebug.clearRenderTargetsAtCreation) { // Commented because string.Format causes garbage //using (new ProfilingSample(rgContext.cmd, string.Format("RenderGraph: Clear Buffer {0}", resourceDescMoved.desc.name))) bool debugClear = m_RenderGraphDebug.clearRenderTargetsAtCreation && !resource.desc.clearBuffer; var name = debugClear ? "RenderGraph: Clear Buffer (Debug)" : "RenderGraph: Clear Buffer"; using (new ProfilingSample(rgContext.cmd, name)) { var clearFlag = resource.desc.depthBufferBits != DepthBits.None ? ClearFlag.Depth : ClearFlag.Color; var clearColor = debugClear ? Color.magenta : resource.desc.clearColor; CoreUtils.SetRenderTarget(rgContext.cmd, resource.rt, clearFlag, clearColor); } } LogTextureCreation(resource.rt, resource.desc.clearBuffer || m_RenderGraphDebug.clearRenderTargetsAtCreation); } } } void CreateTextureForPass(ref TextureResource resource) { var desc = resource.desc; int hashCode = desc.GetHashCode(); if(resource.rt != null) throw new InvalidOperationException(string.Format("Trying to create an already created texture ({0}). Texture was probably declared for writing more than once.", resource.desc.name)); resource.rt = null; if (!TryGetRenderTarget(hashCode, out resource.rt)) { // Note: Name used here will be the one visible in the memory profiler so it means that whatever is the first pass that actually allocate the texture will set the name. // TODO: Find a way to display name by pass. switch (desc.sizeMode) { case TextureSizeMode.Explicit: resource.rt = m_RTHandleSystem.Alloc(desc.width, desc.height, desc.slices, desc.depthBufferBits, desc.colorFormat, desc.filterMode, desc.wrapMode, desc.dimension, desc.enableRandomWrite, desc.useMipMap, desc.autoGenerateMips, desc.isShadowMap, desc.anisoLevel, desc.mipMapBias, desc.msaaSamples, desc.bindTextureMS, desc.useDynamicScale, desc.memoryless, desc.name); break; case TextureSizeMode.Scale: resource.rt = m_RTHandleSystem.Alloc(desc.scale, desc.slices, desc.depthBufferBits, desc.colorFormat, desc.filterMode, desc.wrapMode, desc.dimension, desc.enableRandomWrite, desc.useMipMap, desc.autoGenerateMips, desc.isShadowMap, desc.anisoLevel, desc.mipMapBias, desc.enableMSAA, desc.bindTextureMS, desc.useDynamicScale, desc.memoryless, desc.name); break; case TextureSizeMode.Functor: resource.rt = m_RTHandleSystem.Alloc(desc.func, desc.slices, desc.depthBufferBits, desc.colorFormat, desc.filterMode, desc.wrapMode, desc.dimension, desc.enableRandomWrite, desc.useMipMap, desc.autoGenerateMips, desc.isShadowMap, desc.anisoLevel, desc.mipMapBias, desc.enableMSAA, desc.bindTextureMS, desc.useDynamicScale, desc.memoryless, desc.name); break; } } //// Try to update name when re-using a texture. //// TODO: Check if that actually works. //resource.rt.name = desc.name; #if DEVELOPMENT_BUILD || UNITY_EDITOR if (hashCode != -1) { m_AllocatedTextures.Add((hashCode, resource.rt)); } #endif resource.cachedHash = hashCode; } void SetGlobalTextures(RenderGraphContext rgContext, List textures, bool bindDummyTexture) { foreach (var resource in textures) { var resourceDesc = GetTextureResource(resource); if (resourceDesc.shaderProperty != 0) { if (resourceDesc.rt == null) { throw new InvalidOperationException(string.Format("Trying to set Global Texture parameter for \"{0}\" which was never created.\nCheck that at least one write operation happens before reading it.", resourceDesc.desc.name)); } rgContext.cmd.SetGlobalTexture(resourceDesc.shaderProperty, bindDummyTexture ? TextureXR.GetMagentaTexture() : resourceDesc.rt); } } } internal void PreRenderPassSetGlobalTextures(RenderGraphContext rgContext, List textures) { SetGlobalTextures(rgContext, textures, false); } internal void PostRenderPassUnbindGlobalTextures(RenderGraphContext rgContext, List textures) { SetGlobalTextures(rgContext, textures, true); } internal void ReleaseTexturesForPass(RenderGraphContext rgContext, int passIndex, List readTextures, List writtenTextures) { foreach (var resource in readTextures) { ref var resourceDesc = ref GetTextureResource(resource); if (!resourceDesc.imported && resourceDesc.lastReadPassIndex == passIndex) { if (m_RenderGraphDebug.clearRenderTargetsAtRelease) { using (new ProfilingSample(rgContext.cmd, "RenderGraph: Clear Buffer (Debug)")) { var clearFlag = resourceDesc.desc.depthBufferBits != DepthBits.None ? ClearFlag.Depth : ClearFlag.Color; CoreUtils.SetRenderTarget(rgContext.cmd, GetTexture(resource), clearFlag, Color.magenta); } } ReleaseTextureForPass(resource); } } // If a resource was created for only a single pass, we don't want users to have to declare explicitly the read operation. // So to do that, we also update lastReadIndex on resource writes. // This means that we need to check written resources for destruction too foreach (var resource in writtenTextures) { ref var resourceDesc = ref GetTextureResource(resource); // <= because a texture that is only declared as written in a single pass (and read implicitly in the same pass) will have the default lastReadPassIndex at -1 if (!resourceDesc.imported && resourceDesc.lastReadPassIndex <= passIndex) { ReleaseTextureForPass(resource); } } } void ReleaseTextureForPass(RenderGraphResource res) { Debug.Assert(res.type == RenderGraphResourceType.Texture); ref var resource = ref m_TextureResources[res.handle]; // This can happen because we release texture in two passes (see ReleaseTexturesForPass) and texture can be present in both passes if (resource.rt != null) { LogTextureRelease(resource.rt); ReleaseTextureResource(resource.cachedHash, resource.rt); resource.cachedHash = -1; resource.rt = null; resource.wasReleased = true; } } void ReleaseTextureResource(int hash, RTHandle rt) { if (!m_TexturePool.TryGetValue(hash, out var stack)) { stack = new Stack(); m_TexturePool.Add(hash, stack); } stack.Push(rt); #if DEVELOPMENT_BUILD || UNITY_EDITOR m_AllocatedTextures.Remove((hash, rt)); #endif } void ValidateTextureDesc(in TextureDesc desc) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (desc.colorFormat == GraphicsFormat.None && desc.depthBufferBits == DepthBits.None) { throw new ArgumentException("Texture was created with an invalid color format."); } if (desc.dimension == TextureDimension.None) { throw new ArgumentException("Texture was created with an invalid texture dimension."); } if (desc.slices == 0) { throw new ArgumentException("Texture was created with a slices parameter value of zero."); } if (desc.sizeMode == TextureSizeMode.Explicit) { if (desc.width == 0 || desc.height == 0) throw new ArgumentException("Texture using Explicit size mode was create with either width or height at zero."); if (desc.enableMSAA) throw new ArgumentException("enableMSAA TextureDesc parameter is not supported for textures using Explicit size mode."); } if (desc.sizeMode == TextureSizeMode.Scale || desc.sizeMode == TextureSizeMode.Functor) { if (desc.msaaSamples != MSAASamples.None) throw new ArgumentException("msaaSamples TextureDesc parameter is not supported for textures using Scale or Functor size mode."); } #endif } void ValidateRendererListDesc(in RendererListDesc desc) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (desc.passName != ShaderTagId.none && desc.passNames != null || desc.passName == ShaderTagId.none && desc.passNames == null) { throw new ArgumentException("Renderer List creation descriptor must contain either a single passName or an array of passNames."); } if (desc.renderQueueRange.lowerBound == 0 && desc.renderQueueRange.upperBound == 0) { throw new ArgumentException("Renderer List creation descriptor must have a valid RenderQueueRange."); } if (desc.camera == null) { throw new ArgumentException("Renderer List creation descriptor must have a valid Camera."); } #endif } bool TryGetRenderTarget(int hashCode, out RTHandle rt) { if (m_TexturePool.TryGetValue(hashCode, out var stack) && stack.Count > 0) { rt = stack.Pop(); return true; } rt = null; return false; } internal void CreateRendererLists(List rendererLists) { // For now we just create a simple structure // but when the proper API is available in trunk we'll kick off renderer lists creation jobs here. foreach (var rendererList in rendererLists) { Debug.Assert(rendererList.type == RenderGraphResourceType.RendererList); ref var rendererListResource = ref m_RendererListResources[rendererList.handle]; ref var desc = ref rendererListResource.desc; RendererList newRendererList = RendererList.Create(desc); rendererListResource.rendererList = newRendererList; } } internal void Clear() { LogResources(); m_TextureResources.Clear(); m_RendererListResources.Clear(); #if DEVELOPMENT_BUILD || UNITY_EDITOR if (m_AllocatedTextures.Count != 0) { Debug.LogWarning("RenderGraph: Not all textures were released."); List<(int, RTHandle)> tempList = new List<(int, RTHandle)>(m_AllocatedTextures); foreach (var value in tempList) { ReleaseTextureResource(value.Item1, value.Item2); } } #endif } internal void Cleanup() { foreach (var value in m_TexturePool) { foreach (var rt in value.Value) { m_RTHandleSystem.Release(rt); } } } void LogTextureCreation(RTHandle rt, bool cleared) { if (m_RenderGraphDebug.logFrameInformation) { m_Logger.LogLine("Created Texture: {0} (Cleared: {1})", rt.rt.name, cleared); } } void LogTextureRelease(RTHandle rt) { if (m_RenderGraphDebug.logFrameInformation) { m_Logger.LogLine("Released Texture: {0}", rt.rt.name); } } void LogResources() { if (m_RenderGraphDebug.logResources) { m_Logger.LogLine("==== Allocated Resources ====\n"); List allocationList = new List(); foreach (var stack in m_TexturePool) { foreach (var rt in stack.Value) { allocationList.Add(rt.rt.name); } } allocationList.Sort(); int index = 0; foreach (var element in allocationList) m_Logger.LogLine("[{0}] {1}", index++, element); } } #endregion } }