using System; using System.Text; using System.Diagnostics; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.Rendering; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Experimental.Rendering; namespace UnityEngine.Rendering.Universal { // a customized version of RenderGraphResourcePool from SRP core internal class RTHandleResourcePool { // Dictionary tracks resources by hash and stores resources with same hash in a List (list instead of a stack because we need to be able to remove stale allocations, potentially in the middle of the stack). // The list needs to be sorted otherwise you could get inconsistent resource usage from one frame to another. protected Dictionary> m_ResourcePool = new Dictionary>(); protected List m_RemoveList = new List(32); // Used to remove stale resources as there is no RemoveAll on SortedLists protected static int s_CurrentStaleResourceCount = 0; // Keep stale resources alive for 3 frames protected static int s_StaleResourceLifetime = 3; // Store max 32 rtHandles // 1080p * 32bpp * 32 = 265.4mb protected static int s_StaleResourceMaxCapacity = 32; /// /// Controls the resource pool's max stale resource capacity. /// Increasing the capacity may have a negative impact on the memory usage. /// Increasing the capacity may reduce the runtime RTHandle realloc cost in multi view/multi camera setup. /// Setting capacity will purge the current pool. It is recommended to setup the capacity upfront and not changing it during the runtime. /// Setting capacity won't do anything if new capacity is the same to the current capacity. /// internal int staleResourceCapacity { get { return s_StaleResourceMaxCapacity; } set { if (s_StaleResourceMaxCapacity != value) { s_StaleResourceMaxCapacity = value; Cleanup(); } } } // Add no longer used resouce to pool // Return true if resource is added to pool successfully, return false otherwise. internal bool AddResourceToPool(in TextureDesc texDesc, RTHandle resource, int currentFrameIndex) { if (s_CurrentStaleResourceCount >= s_StaleResourceMaxCapacity) return false; int hashCode = GetHashCodeWithNameHash(texDesc); if (!m_ResourcePool.TryGetValue(hashCode, out var list)) { // Init list with max capacity to avoid runtime GC.Alloc when calling list.Add(resize list) list = new SortedList(s_StaleResourceMaxCapacity); m_ResourcePool.Add(hashCode, list); } list.Add(resource.GetInstanceID(), (resource, currentFrameIndex)); s_CurrentStaleResourceCount++; return true; } // Get resource from the pool using TextureDesc as key // Return true if resource successfully retried resource from the pool, return false otherwise. internal bool TryGetResource(in TextureDesc texDesc, out RTHandle resource, bool usepool = true) { int hashCode = GetHashCodeWithNameHash(texDesc); if (usepool && m_ResourcePool.TryGetValue(hashCode, out SortedList list) && list.Count > 0) { resource = list.Values[list.Count - 1].resource; list.RemoveAt(list.Count - 1); // O(1) since it's the last element. s_CurrentStaleResourceCount--; return true; } resource = null; return false; } // Release all resources in pool. internal void Cleanup() { foreach (var kvp in m_ResourcePool) { foreach (var res in kvp.Value) { res.Value.resource.Release(); } } m_ResourcePool.Clear(); s_CurrentStaleResourceCount = 0; } static protected bool ShouldReleaseResource(int lastUsedFrameIndex, int currentFrameIndex) { // We need to have a delay of a few frames before releasing resources for good. // Indeed, when having multiple off-screen cameras, they are rendered in a separate SRP render call and thus with a different frame index than main camera // This causes texture to be deallocated/reallocated every frame if the two cameras don't need the same buffers. return (lastUsedFrameIndex + s_StaleResourceLifetime) < currentFrameIndex; } // Release resources that are not used in last couple frames. internal void PurgeUnusedResources(int currentFrameIndex) { // Update the frame index for the lambda. Static because we don't want to capture. m_RemoveList.Clear(); foreach (var kvp in m_ResourcePool) { // WARNING: No foreach here. Sorted list GetEnumerator generates garbage... var list = kvp.Value; var keys = list.Keys; var values = list.Values; for (int i = 0; i < list.Count; ++i) { var value = values[i]; if (ShouldReleaseResource(value.frameIndex, currentFrameIndex)) { value.resource.Release(); m_RemoveList.Add(keys[i]); s_CurrentStaleResourceCount--; } } foreach (var key in m_RemoveList) list.Remove(key); } } internal void LogDebugInfo() { var sb = new StringBuilder(); sb.AppendFormat("RTHandleResourcePool for frame {0}, Total stale resources {1}", Time.frameCount, s_CurrentStaleResourceCount); sb.AppendLine(); foreach (var kvp in m_ResourcePool) { var list = kvp.Value; var keys = list.Keys; var values = list.Values; for (int i = 0; i < list.Count; ++i) { var value = values[i]; sb.AppendFormat("Resrouce in pool: Name {0} Last active frame index {1} Size {2} x {3} x {4}", value.resource.name, value.frameIndex, value.resource.rt.descriptor.width, value.resource.rt.descriptor.height, value.resource.rt.descriptor.volumeDepth ); sb.AppendLine(); } } Debug.Log(sb); } // NOTE: Only allow reusing resource with the same name. // This is because some URP code uses texture name as key to bind input texture (GBUFFER_2). Different name will result in URP bind texture to different shader input slot. // Ideally if URP code uses shaderPropertyID(instead of name string), we can relax the restriction here. internal int GetHashCodeWithNameHash(in TextureDesc texDesc) { int hashCode = texDesc.GetHashCode(); hashCode = hashCode * 23 + texDesc.name.GetHashCode(); return hashCode; } internal static TextureDesc CreateTextureDesc(RenderTextureDescriptor desc, TextureSizeMode textureSizeMode = TextureSizeMode.Explicit, int anisoLevel = 1, float mipMapBias = 0, FilterMode filterMode = FilterMode.Point, TextureWrapMode wrapMode = TextureWrapMode.Clamp, string name = "") { Assertions.Assert.IsFalse(desc.graphicsFormat != GraphicsFormat.None && desc.depthStencilFormat != GraphicsFormat.None, "The RenderTextureDescriptor used to create a TextureDesc contains both graphicsFormat and depthStencilFormat which is not allowed."); var format = (desc.depthStencilFormat != GraphicsFormat.None) ? desc.depthStencilFormat : desc.graphicsFormat; TextureDesc rgDesc = new TextureDesc(desc.width, desc.height); rgDesc.sizeMode = textureSizeMode; rgDesc.slices = desc.volumeDepth; rgDesc.format = format; rgDesc.filterMode = filterMode; rgDesc.wrapMode = wrapMode; rgDesc.dimension = desc.dimension; rgDesc.enableRandomWrite = desc.enableRandomWrite; rgDesc.useMipMap = desc.useMipMap; rgDesc.autoGenerateMips = desc.autoGenerateMips; rgDesc.isShadowMap = desc.shadowSamplingMode != ShadowSamplingMode.None; rgDesc.anisoLevel = anisoLevel; rgDesc.mipMapBias = mipMapBias; rgDesc.msaaSamples = (MSAASamples)desc.msaaSamples; rgDesc.bindTextureMS = desc.bindMS; rgDesc.useDynamicScale = desc.useDynamicScale; rgDesc.memoryless = RenderTextureMemoryless.None; rgDesc.vrUsage = VRTextureUsage.None; rgDesc.name = name; return rgDesc; } } }