using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; namespace UnityEngine.Rendering.RenderGraphModule { abstract class IRenderGraphResourcePool { public abstract void PurgeUnusedResources(int currentFrameIndex); public abstract void Cleanup(); public abstract void CheckFrameAllocation(bool onException, int frameIndex); // When disabled, resources released in the current execution cannot be reused until the next execution. // This is useful for Frame Debugger to ensure each pass uses unique resources within an execution. public bool IntraFrameMemoryAliasing { get; set; } = true; } /// /// Encapsulates a pooled resource along with its frame and execution tracking metadata. /// This allows the pool to track when resources were released for proper reuse and purging decisions. /// readonly struct PooledResourceEntry where Type : class { public readonly Type resource; public readonly int frameIndex; public readonly int executionCount; public PooledResourceEntry(Type resource, int frameIndex, int executionCount) { this.resource = resource; this.frameIndex = frameIndex; this.executionCount = executionCount; } } abstract class RenderGraphResourcePool : IRenderGraphResourcePool where Type : class { // 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>>(); // This list allows us to determine if all resources were correctly released in the frame when validity checks are enabled. // This is useful to warn in case of user error or avoid leaks when a render graph execution error occurs for example. List<(int, Type)> m_FrameAllocatedResources = new List<(int, Type)>(); const int kStaleResourceLifetime = 10; // Release the GPU resource itself protected abstract void ReleaseInternalResource(Type res); protected abstract string GetResourceName(in Type res); protected abstract long GetResourceSize(in Type res); protected abstract string GetResourceTypeName(); protected abstract ulong GetSortIndex(Type res); public void ReleaseResource(int hash, Type resource, int currentFrameIndex, int currentExecutionCount) { if (!m_ResourcePool.TryGetValue(hash, out var list)) { list = new SortedList>(); m_ResourcePool.Add(hash, list); } list.Add(GetSortIndex(resource), new PooledResourceEntry(resource, currentFrameIndex, currentExecutionCount)); } public bool TryGetResource(int hashCode, out Type resource, int currentFrameIndex, int currentExecutionCount) { if (m_ResourcePool.TryGetValue(hashCode, out SortedList> list) && list.Count > 0) { // When "intra frame memory aliasing" mode is disabled, skip resources that were released in the current execution. // They will only be available for reuse in the next execution. if (!IntraFrameMemoryAliasing) { // Iterate from the end to find a resource not from the current execution. for (int i = list.Count - 1; i >= 0; i--) { var entry = list.Values[i]; // Allow reuse if the resource was released in a previous frame (normal case), // or in a previous execution within the same frame (Frame Debugger cascade-repaints). // This handles both standard frame-to-frame reuse and rapid repaints in Frame Debugger. if (entry.frameIndex < currentFrameIndex || entry.executionCount < currentExecutionCount) { resource = entry.resource; list.RemoveAt(i); return true; } } // All available resources are from the current execution, so we need to create a new one. } else { // Normal mode: take the most recent resource. var index = list.Count - 1; resource = list.Values[index].resource; list.RemoveAt(index); // O(1) since it's the last element. return true; } } resource = null; return false; } public override void Cleanup() { // Removing the actual graphics resources foreach (var kvp in m_ResourcePool) { foreach (var res in kvp.Value) { ReleaseInternalResource(res.Value.resource); } } // Clearing the lists and the pool itself m_ResourcePool.Clear(); // Clearing it, if not done already m_FrameAllocatedResources.Clear(); } [Conditional("UNITY_ENABLE_CHECKS")] public void RegisterFrameAllocation(int hash, Type value) { if (RenderGraph.enableValidityChecks && hash != -1) m_FrameAllocatedResources.Add((hash, value)); } [Conditional("UNITY_ENABLE_CHECKS")] public void UnregisterFrameAllocation(int hash, Type value) { if (RenderGraph.enableValidityChecks && hash != -1) m_FrameAllocatedResources.Remove((hash, value)); } public override void CheckFrameAllocation(bool onException, int frameIndex) { #if UNITY_ENABLE_CHECKS // conditional not working with override if (RenderGraph.enableValidityChecks) { // In case of exception we need to release all resources to the pool to avoid leaking. // If it's not an exception then it's a user error so we need to log the problem. if (m_FrameAllocatedResources.Count != 0) { string logMessage = ""; if (!onException) logMessage = $"RenderGraph: Not all resources of type {GetResourceTypeName()} were released. This can be caused by a resources being allocated but never read by any pass."; foreach (var value in m_FrameAllocatedResources) { if (!onException) logMessage = $"{logMessage}\n\t{GetResourceName(value.Item2)}"; // When releasing on exception/error, use -1 as executionCount marker. // This ensures the resource goes back to the pool but won't interfere with normal execution tracking. ReleaseResource(value.Item1, value.Item2, frameIndex, -1); } if (!onException) // If onException is true, logMessage is "" Debug.LogWarning(logMessage); } // If an error occurred during execution, it's expected that textures are not all released so we clear the tracking list. m_FrameAllocatedResources.Clear(); } #endif } public float GetMemorySizeInMB() { float totalSize = 0; foreach (var kvp in m_ResourcePool) { foreach (var res in kvp.Value) { totalSize += GetResourceSize(res.Value.resource) / (1024.0f * 1024.0f); } } return totalSize; } public int GetNumResourcesAvailable() { int totalResources = 0; foreach (var kvp in m_ResourcePool) { totalResources += kvp.Value.Count; } return totalResources; } static readonly List s_ToRemoveList = new List(32); public override void PurgeUnusedResources(int currentFrameIndex) { foreach (var kvp in m_ResourcePool) { s_ToRemoveList.Clear(); var list = kvp.Value; var keys = list.Keys; var values = list.Values; for (int i = 0; i < list.Count; ++i) { var value = values[i]; var key = keys[i]; // When a resource hasn't been used for a few frames, we release it for good to reduce memory footprint. // We wait a few frames because 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. if (value.frameIndex + kStaleResourceLifetime < currentFrameIndex) { ReleaseInternalResource(value.resource); // Adding stale resource to the remove list s_ToRemoveList.Add(key); } } // Removing the stale resource from the pool for (int j = 0; j < s_ToRemoveList.Count; ++j) list.Remove(s_ToRemoveList[j]); } } } }