using System; using System.Collections.Generic; using System.Diagnostics; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; namespace UnityEngine.Rendering.RenderGraphModule.NativeRenderPassCompiler { internal partial class NativePassCompiler : IDisposable { internal struct RenderGraphInputInfo { public RenderGraphResourceRegistry m_ResourcesForDebugOnly; public List m_RenderPasses; public string debugName; public bool disablePassCulling; public bool disablePassMerging; public RenderTextureUVOriginStrategy renderTextureUVOriginStrategy; } internal RenderGraphInputInfo graph; internal CompilerContextData contextData = null; internal CompilerContextData defaultContextData; internal CommandBuffer previousCommandBuffer; Stack m_HasSideEffectPassIdCullingStack; List> m_UnusedVersionedResourceIdCullingStacks; Dictionary> m_DelayedLastUseListPerPassMap; RenderGraphCompilationCache m_CompilationCache; RenderTargetIdentifier[][] m_TempMRTArrays = null; internal const int k_EstimatedPassCount = 100; internal const int k_MaxSubpass = 8; // Needs to match with RenderPassSetup.h NativeList m_BeginRenderPassAttachments; internal static bool s_ForceGenerateAuditsForTests = false; public NativePassCompiler(RenderGraphCompilationCache cache) { m_CompilationCache = cache; defaultContextData = new CompilerContextData(); m_HasSideEffectPassIdCullingStack = new Stack(k_EstimatedPassCount); m_UnusedVersionedResourceIdCullingStacks = new List>(); for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) m_UnusedVersionedResourceIdCullingStacks.Add(new Stack()); m_DelayedLastUseListPerPassMap = new Dictionary>(k_EstimatedPassCount); for (int passId = 0; passId < k_EstimatedPassCount; ++passId) m_DelayedLastUseListPerPassMap.Add(passId, new List()); m_TempMRTArrays = new RenderTargetIdentifier[RenderGraph.kMaxMRTCount][]; for (int i = 0; i < RenderGraph.kMaxMRTCount; ++i) m_TempMRTArrays[i] = new RenderTargetIdentifier[i + 1]; } // IDisposable implementation ~NativePassCompiler() => Cleanup(); public void Dispose() { Cleanup(); GC.SuppressFinalize(this); } public void Cleanup() { // If caching enabled, the two can be different contextData?.Dispose(); defaultContextData?.Dispose(); if (m_BeginRenderPassAttachments.IsCreated) { m_BeginRenderPassAttachments.Dispose(); } } public bool Initialize(RenderGraphResourceRegistry resources, List renderPasses, RenderGraphDebugParams debugParams, string debugName, bool useCompilationCaching, int graphHash, int frameIndex, RenderTextureUVOriginStrategy renderTextureUVOriginStrategy) { bool cached = false; if (!useCompilationCaching) contextData = defaultContextData; else cached = m_CompilationCache.GetCompilationCache(graphHash, frameIndex, out contextData); graph.m_ResourcesForDebugOnly = resources; graph.m_RenderPasses = renderPasses; graph.disablePassCulling = debugParams.disablePassCulling; graph.disablePassMerging = debugParams.disablePassMerging; graph.debugName = debugName; graph.renderTextureUVOriginStrategy = renderTextureUVOriginStrategy; Clear(clearContextData: !useCompilationCaching); return cached; } void HandleExtendedFeatureFlags() { for (int nativePassIndex = 0; nativePassIndex < contextData.nativePassData.Length; nativePassIndex++) { int firstNativeSubPass = contextData.nativePassData[nativePassIndex].firstNativeSubPass; // Does this native pass have any sub passes. if (firstNativeSubPass >= 0) { int firstGraphPass = contextData.nativePassData[nativePassIndex].firstGraphPass; int graphPassIndex = 0; for (int nativeSubPassIndex = 0; nativeSubPassIndex < contextData.nativePassData[nativePassIndex].numNativeSubPasses; nativeSubPassIndex++) { // Start with the MVPVV compatible flag set so that it can be used for the & operation later SubPassFlags extendedSubPassFlags = SubPassFlags.MultiviewRenderRegionsCompatible; // Iterate over all graph passes that got merged into this sub pass while ((graphPassIndex < contextData.nativePassData[nativePassIndex].numGraphPasses) && (contextData.passData[graphPassIndex + firstGraphPass].nativeSubPassIndex == nativeSubPassIndex)) { if (contextData.passData[graphPassIndex + firstGraphPass].extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.TileProperties)) { extendedSubPassFlags |= SubPassFlags.TileProperties; } // A native sub pass is MultiviewRenderRegionsCompatible only if all of its graph passes are compatible if (!contextData.passData[graphPassIndex + firstGraphPass].extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultiviewRenderRegionsCompatible)) { extendedSubPassFlags &= ~SubPassFlags.MultiviewRenderRegionsCompatible; } graphPassIndex++; } contextData.nativeSubPassData.ElementAt(firstNativeSubPass + nativeSubPassIndex).flags |= extendedSubPassFlags; } } } } public void Compile(RenderGraphResourceRegistry resources) { ValidatePasses(); SetupContextData(resources); BuildGraph(); CullUnusedRenderGraphPasses(); TryMergeNativePasses(); HandleExtendedFeatureFlags(); FindResourceUsageRangeAndSynchronization(); DetectMemoryLessResources(); PrepareNativeRenderPasses(); if (graph.renderTextureUVOriginStrategy == RenderTextureUVOriginStrategy.PropagateAttachmentOrientation) PropagateTextureUVOrigin(); } public void Clear(bool clearContextData) { if (clearContextData) contextData.Clear(); m_HasSideEffectPassIdCullingStack.Clear(); for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) m_UnusedVersionedResourceIdCullingStacks[type].Clear(); foreach (var resListPerPassId in m_DelayedLastUseListPerPassMap) resListPerPassId.Value.Clear(); m_DelayedLastUseListPerPassMap.Clear(); } void SetPassStatesForNativePass(int nativePassId) { NativePassData.SetPassStatesForNativePass(contextData, nativePassId); } internal enum NativeCompilerProfileId { NRPRGComp_PrepareNativePass, NRPRGComp_SetupContextData, NRPRGComp_BuildGraph, NRPRGComp_CullNodes, NRPRGComp_TryMergeNativePasses, NRPRGComp_FindResourceUsageRanges, NRPRGComp_DetectMemorylessResources, NRPRGComp_PropagateTextureUVOrigin, NRPRGComp_ExecuteInitializeResources, NRPRGComp_ExecuteBeginRenderpassCommand, NRPRGComp_ExecuteDestroyResources, } [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] void ValidatePasses() { if (RenderGraph.enableValidityChecks) { int tilePropertiesPassIndex = -1; for (int passId = 0; passId < graph.m_RenderPasses.Count; passId++) { if (graph.m_RenderPasses[passId].extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.TileProperties)) { if (tilePropertiesPassIndex > -1) { throw new Exception($"ExtendedFeatureFlags.TileProperties can only be set once per render graph (render graph {graph.debugName}, pass {graph.m_RenderPasses[passId].name}), previously set at (pass {graph.m_RenderPasses[tilePropertiesPassIndex].name})."); } tilePropertiesPassIndex = passId; } } } } void SetupContextData(RenderGraphResourceRegistry resources) { using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_SetupContextData))) { contextData.Initialize(resources, k_EstimatedPassCount); } } // Returns true if the RasterFragmentList is successfully set up bool TrySetupRasterFragmentList(ref PassData ctxPass, ref RenderGraphPass inputPass, out string errorMessage) { errorMessage = null; var ctx = contextData; // Grab offset in context fragment list to begin building the fragment list ctxPass.firstFragment = ctx.fragmentData.Length; // Depth attachment is always at index 0 if (inputPass.depthAccess.textureHandle.handle.IsValid()) { ctxPass.fragmentInfoHasDepth = true; if (ctx.TryAddToFragmentList(inputPass.depthAccess, ctxPass.firstFragment, ctxPass.numFragments, out errorMessage)) { ctxPass.TryAddFragment(inputPass.depthAccess.textureHandle.handle, ctx, out errorMessage); } if (errorMessage != null) { errorMessage = $"when trying to add depth attachment of type {inputPass.depthAccess.textureHandle.handle.type} at index {inputPass.depthAccess.textureHandle.handle.index} - {errorMessage}"; return false; } } for (var ci = 0; ci < inputPass.colorBufferMaxIndex + 1; ++ci) { // Skip unused color slots if (!inputPass.colorBufferAccess[ci].textureHandle.handle.IsValid()) continue; if (ctx.TryAddToFragmentList(inputPass.colorBufferAccess[ci], ctxPass.firstFragment, ctxPass.numFragments, out errorMessage)) { ctxPass.TryAddFragment(inputPass.colorBufferAccess[ci].textureHandle.handle, ctx, out errorMessage); } if (errorMessage != null) { errorMessage = $"when trying to add render attachment of type {inputPass.colorBufferAccess[ci].textureHandle.handle.type} at index {inputPass.colorBufferAccess[ci].textureHandle.handle.index} - {errorMessage}"; return false; } } // shading rate image - this is a specific type of attachment (more of an image resource that can't be sampled, only used by the rasterizer) if (inputPass.hasShadingRateImage && inputPass.shadingRateAccess.textureHandle.handle.IsValid()) { if (ctx.TryAddToFragmentList(inputPass.shadingRateAccess, ctxPass.firstFragment, ctxPass.numFragments, out errorMessage)) { ctxPass.shadingRateImageIndex = ctx.fragmentData.Length - 1; } if (errorMessage != null) { errorMessage = $"when trying to add VRS attachment of type {inputPass.shadingRateAccess.textureHandle.handle.type} at index {inputPass.shadingRateAccess.textureHandle.handle.index} - {errorMessage}"; return false; } } // Grab offset in context fragment list to begin building the fragment input list ctxPass.firstFragmentInput = ctx.fragmentData.Length; for (var ci = 0; ci < inputPass.fragmentInputMaxIndex + 1; ++ci) { // Skip unused fragment input slots if (!inputPass.fragmentInputAccess[ci].textureHandle.IsValid()) continue; if (ctx.TryAddToFragmentList(inputPass.fragmentInputAccess[ci], ctxPass.firstFragmentInput, ctxPass.numFragmentInputs, out errorMessage)) { ctxPass.TryAddFragmentInput(inputPass.fragmentInputAccess[ci].textureHandle.handle, ctx, out errorMessage); } if (errorMessage != null) { errorMessage = $"when trying to add input attachment of type {inputPass.fragmentInputAccess[ci].textureHandle.handle.type} at index {inputPass.fragmentInputAccess[ci].textureHandle.handle.index} - {errorMessage}"; return false; } } // Grab offset in context random write list to begin building the per pass random write lists ctxPass.firstRandomAccessResource = ctx.randomAccessResourceData.Length; for (var ci = 0; ci < inputPass.randomAccessResourceMaxIndex + 1; ++ci) { ref var uav = ref inputPass.randomAccessResource[ci]; // Skip unused random write slots if (!uav.h.IsValid()) continue; if (ctx.TryAddToRandomAccessResourceList(uav.h, ci, uav.preserveCounterValue, ctxPass.firstRandomAccessResource, ctxPass.numRandomAccessResources, out errorMessage)) { ctxPass.AddRandomAccessResource(); } if (errorMessage != null) { errorMessage = $"when trying to add random access attachment of type {uav.h.type} at index {uav.h.index} - {errorMessage}"; return false; } } // This is suspicious, there are frame buffer fetch inputs but nothing is output. We don't allow this for now. // In theory you could fb-fetch inputs and write something to a uav and output nothing? This needs to be investigated // so don't allow it for now. if (ctxPass.numFragments == 0) { Debug.Assert(ctxPass.numFragmentInputs == 0); } return true; } void BuildGraph() { var ctx = contextData; List passes = graph.m_RenderPasses; // Not clearing data, we will do it right after in the for loop // This is to prevent unnecessary costly copies of pass struct (128bytes) ctx.passData.ResizeUninitialized(passes.Count); // Build up the context graph and keep track of nodes we encounter that can't be culled using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_BuildGraph))) { for (int passId = 0; passId < passes.Count; passId++) { var inputPass = passes[passId]; #if DEVELOPMENT_BUILD || UNITY_EDITOR if (inputPass.type == RenderGraphPassType.Legacy) { throw new Exception(RenderGraph.RenderGraphExceptionMessages.UsingLegacyRenderGraph(inputPass.name)); } #endif // Accessing already existing passData in place in the container through reference to avoid deep copy // Make sure everything is reset and initialized or we will use obsolete data from previous frame ref var ctxPass = ref ctx.passData.ElementAt(passId); ctxPass.ResetAndInitialize(inputPass, passId); ctx.passNames.Add(new Name(inputPass.name, true)); if (ctxPass.hasSideEffects) { m_HasSideEffectPassIdCullingStack.Push(passId); } // Set up the list of fragment attachments for this pass // Note: This doesn't set up the resource reader/writer list as the fragment attachments // will also be in the pass read/write lists accordingly if (ctxPass.type == RenderGraphPassType.Raster) { if (!TrySetupRasterFragmentList(ref ctxPass, ref inputPass, out var errorMessage)) { throw new Exception($"In pass '{inputPass.name}', {errorMessage}"); } } // Set up per resource type read/write lists for this pass ctxPass.firstInput = ctx.inputData.Length; // Grab offset in context input list ctxPass.firstOutput = ctx.outputData.Length; // Grab offset in context output list for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) { var resourceWrite = inputPass.resourceWriteLists[type]; var resourceWriteCount = resourceWrite.Count; for (var i = 0; i < resourceWriteCount; ++i) { var resource = resourceWrite[i]; // Writing to an imported resource is a side effect so mark the pass if needed ref var resData = ref ctx.UnversionedResourceData(resource); if (resData.isImported) { if (!ctxPass.hasSideEffects) { ctxPass.hasSideEffects = true; m_HasSideEffectPassIdCullingStack.Push(passId); } } // Mark this pass as writing to this version of the resource ctx.resources[resource].SetWritingPass(ctx, resource, passId); ctx.outputData.Add(new PassOutputData(resource)); ctxPass.numOutputs++; } var resourceRead = inputPass.resourceReadLists[type]; var resourceReadCount = resourceRead.Count; for (var i = 0; i < resourceReadCount; ++i) { var resource = resourceRead[i]; // Mark this pass as reading from this version of the resource ctx.resources[resource].RegisterReadingPass(ctx, resource, passId, ctxPass.numInputs); ctx.inputData.Add(new PassInputData(resource)); ctxPass.numInputs++; } var resourceTrans = inputPass.transientResourceList[type]; var resourceTransCount = resourceTrans.Count; for (var i = 0; i < resourceTransCount; ++i) { var resource = resourceTrans[i]; // Mark this pass as reading from this version of the resource ctx.resources[resource].RegisterReadingPass(ctx, resource, passId, ctxPass.numInputs); ctx.inputData.Add(new PassInputData(resource)); ctxPass.numInputs++; // Mark this pass as writing to this version of the resource ctx.resources[resource].SetWritingPass(ctx, resource, passId); ctx.outputData.Add(new PassOutputData(resource)); ctxPass.numOutputs++; } // For raster passes, we do an extra step to monitor textures sampled in the pass // It can be a breaking change reason later on when building a native render pass if (type == (int)RenderGraphResourceType.Texture && ctxPass.type == RenderGraphPassType.Raster) { ctxPass.firstSampledOnlyRaster = ctx.sampledData.Length; foreach (ref readonly var input in ctxPass.Inputs(ctx)) { // Check if this input is the shading rate image if (!ctxPass.IsUsedAsFragment(input.resource, ctx)) { ctx.sampledData.Add(input.resource); ctxPass.numSampledOnlyRaster++; } } } } } } } void CullUnusedRenderGraphPasses() { using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_CullNodes))) { if (graph.disablePassCulling) return; // Must come first // TODO make another subfunction CullRenderGraphPassesWithNoSideEffect() for this first step of the culling stage var ctx = contextData; // Cull all passes first ctx.CullAllPasses(true); // Flood fill downstream algorithm using BFS, // starting from the passes with side effects (writting to imported texture, not allowed to be culled, globals modification...) // to all their dependencies while (m_HasSideEffectPassIdCullingStack.Count != 0) { int passId = m_HasSideEffectPassIdCullingStack.Pop(); ref var passData = ref ctx.passData.ElementAt(passId); // We already found this node through another dependency chain if (!passData.culled) continue; // Flow upstream from this node foreach (ref readonly var input in passData.Inputs(ctx)) { ref var inputVersionedDataRes = ref ctx.resources[input.resource]; if (inputVersionedDataRes.written) { m_HasSideEffectPassIdCullingStack.Push(inputVersionedDataRes.writePassId); } } // We need this node, don't cull it passData.culled = false; } // Update graph based on freshly culled nodes, remove any connection to them // We start from the latest passes to the first ones as we might need to decrement the version number of unwritten resources var numPasses = ctx.passData.Length; for (int passIndex = numPasses - 1; passIndex >= 0; passIndex--) { ref readonly var pass = ref ctx.passData.ElementAt(passIndex); // Remove the connections from the list so they won't be visited again if (pass.culled) { pass.DisconnectFromResources(ctx); } } // Second step of the algorithm, must come after // TODO: The resources culling step is currently disabled due to an issue: https://jira.unity3d.com/projects/SRP/issues/SRP-897 // Renabled the resource culling step after addressing the depth attachment problem above. // Renabled the relevent tests // CullRenderGraphPassesWritingOnlyUnusedResources(); } } void CullRenderGraphPassesWritingOnlyUnusedResources() { var ctx = contextData; var numPasses = ctx.passData.Length; for (int passIndex = 0; passIndex < numPasses; passIndex++) { ref var passData = ref ctx.passData.ElementAt(passIndex); // Use the generic tag to monitor the number of written resources that are used passData.tag = passData.numOutputs; // Find all resources that are written by a pass but not read at all and add them to the stacks foreach (ref readonly var output in passData.Outputs(ctx)) { ref readonly var outputResource = ref output.resource; ref var outputVersionedDataRes = ref ctx.resources[outputResource]; if (outputVersionedDataRes.numReaders == 0) m_UnusedVersionedResourceIdCullingStacks[outputResource.iType].Push(outputResource); } } // Go through each stack of unused resources and try to cull their producer for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) { var unusedVersionedResourceIdCullingStack = m_UnusedVersionedResourceIdCullingStacks[type]; // Goal is to find the producers of the unused resources and culled them if they only write to unused resources while (unusedVersionedResourceIdCullingStack.Count != 0) { var unusedResource = unusedVersionedResourceIdCullingStack.Pop(); ref var unusedUnversionedDataRes = ref ctx.resources.unversionedData[type].ElementAt(unusedResource.index); if (unusedUnversionedDataRes.isImported) continue; // Not always unused as someone can read it outside the graph ref var unusedVersionedDataRes = ref ctx.resources[unusedResource]; ref var producerData = ref ctx.passData.ElementAt(unusedVersionedDataRes.writePassId); if (producerData.culled) continue; // Producer has been culled already // Decrement the number of written resources that are used for this pass producerData.tag--; Debug.Assert(producerData.tag >= 0); // Producer is not necessary anymore, as it only writes to unused resources and has no side effects if (producerData.tag == 0 && !producerData.hasSideEffects) { producerData.culled = true; producerData.DisconnectFromResources(ctx, unusedVersionedResourceIdCullingStack, type); } else // Producer is (still) necessary, but we might need to remove the read of the previous version coming implicitly with the write of the current version { // We always add written resource to the stack so versionedIndex > 0 var prevVersionedRes = new ResourceHandle(unusedResource, unusedResource.version - 1); // If no explicit read is requested by the user (AccessFlag.Write only), we need to remove the implicit read // so that we cut cleanly the connection between previous version of the resource and current producer bool isImplicitRead = graph.m_RenderPasses[producerData.passId].implicitReadsList.Contains(prevVersionedRes); if (isImplicitRead) { ref var prevVersionedDataRes = ref ctx.resources[prevVersionedRes]; // Notify the previous version of this resource that it is not read anymore by this pass prevVersionedDataRes.RemoveReadingPass(ctx, prevVersionedRes, producerData.passId); // We also need to add the previous version of the resource to the stack IF no other pass than current producer needed it if (prevVersionedDataRes.written && prevVersionedDataRes.numReaders == 0) { unusedVersionedResourceIdCullingStack.Push(prevVersionedRes); } } } } } } void TryMergeNativePasses() { var ctx = contextData; using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_TryMergeNativePasses))) { // Try to merge raster passes into the currently active native pass. This will not do pass reordering yet // so it is simply greedy trying to add the next pass to a currently active one. // In the future we want to try adding any available (i.e. that has no data dependencies on any future results) future pass // and allow merging that into the pass thus greedily reordering passes. But reordering requires a lot of API validation // that ensures rendering behaves accordingly with reordered passes so we don't allow that for now. // !!! Compilation caching warning !!! // Merging of passes is highly dependent on render texture properties. // When caching the render graph compilation, we hash a subset of those render texture properties to make sure we recompile the graph if needed. // We only hash a subset for performance reason so if you add logic here that will change the behavior of pass merging, // make sure that the relevant properties are hashed properly. See RenderGraphPass.ComputeHash() int activeNativePassId = -1; #if UNITY_EDITOR || DEVELOPMENT_BUILD bool generatePassBreakAudits = RenderGraphDebugSession.hasActiveDebugSession || s_ForceGenerateAuditsForTests; #endif for (var passIdx = 0; passIdx < ctx.passData.Length; ++passIdx) { ref var passToAdd = ref ctx.passData.ElementAt(passIdx); // If the pass has been culled, just ignore it if (passToAdd.culled) { continue; } // If no active pass, there is nothing to merge... if (activeNativePassId == -1) { //If raster, start a new native pass with the current pass if (passToAdd.type == RenderGraphPassType.Raster) { // Allocate a stand-alone native renderpass based on the current pass ctx.nativePassData.Add(new NativePassData(ref passToAdd, ctx)); passToAdd.nativePassIndex = ctx.nativePassData.LastIndex(); activeNativePassId = passToAdd.nativePassIndex; } } // There is an native pass currently open, try to add the current graph pass to it else { var mergeTestResult = graph.disablePassMerging ? new PassBreakAudit(PassBreakReason.PassMergingDisabled, passIdx) : NativePassData.TryMerge(contextData, activeNativePassId, passIdx); // Merge failed, close current native render pass and create a new one if (mergeTestResult.reason != PassBreakReason.Merged) { SetPassStatesForNativePass(activeNativePassId); #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generatePassBreakAudits) { ref var nativePassData = ref contextData.nativePassData.ElementAt(activeNativePassId); nativePassData.breakAudit = mergeTestResult; } #endif if (mergeTestResult.reason == PassBreakReason.NonRasterPass) { // Non-raster pass, no active native pass at all activeNativePassId = -1; } else { // Raster but cannot be merged, allocate a new stand-alone native renderpass based on the current pass ctx.nativePassData.Add(new NativePassData(ref passToAdd, ctx)); passToAdd.nativePassIndex = ctx.nativePassData.LastIndex(); activeNativePassId = passToAdd.nativePassIndex; } } } } if (activeNativePassId >= 0) { // "Close" the last native pass by marking the last graph pass as end SetPassStatesForNativePass(activeNativePassId); #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generatePassBreakAudits) { ref var nativePassData = ref contextData.nativePassData.ElementAt(activeNativePassId); nativePassData.breakAudit = new PassBreakAudit(PassBreakReason.EndOfGraph, -1); } #endif } } } bool FindFirstPassIdOnGraphicsQueueAwaitingFenceGoingForward(ref PassData startAsyncPass, out int firstPassIdAwaiting) { var ctx = contextData; Debug.Assert(startAsyncPass.asyncCompute && !startAsyncPass.culled); firstPassIdAwaiting = startAsyncPass.awaitingMyGraphicsFencePassId; // This async pass has no one waiting for it, try the next async passes if (firstPassIdAwaiting == -1) { var nextPassIndex = startAsyncPass.passId + 1; var lastPassIndex = ctx.passData.Length - 1; // Find the first async pass that is synchronized by the graphics queue while (firstPassIdAwaiting == -1 && nextPassIndex <= lastPassIndex) { ref var nextPass = ref ctx.passData.ElementAt(nextPassIndex); if (nextPass.asyncCompute && !nextPass.culled) firstPassIdAwaiting = nextPass.awaitingMyGraphicsFencePassId; nextPassIndex++; } // We didn't find any fence, this should not happen? if (nextPassIndex > lastPassIndex) { // For now we fallback to the last pass of the graph firstPassIdAwaiting = lastPassIndex; return false; } } // Found a pass awaiting a fence return true; } int FindFirstNonCulledPassIdGoingBackward(int startPassId, bool startPassIsIncluded) { var ctx = contextData; Debug.Assert(startPassId >= 0 && startPassId < ctx.passData.Length); var currPassId = startPassIsIncluded ? startPassId : Math.Max(0, startPassId - 1); ref var currPass = ref ctx.passData.ElementAt(currPassId); // If this pre pass is culled, fallback to the first previous one not culled while (currPass.culled && currPassId > 0) { currPass = ref ctx.passData.ElementAt(--currPassId); } return currPass.passId; } void FindResourceUsageRangeAndSynchronization() { var ctx = contextData; using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_FindResourceUsageRanges))) { // Algorithm is in two steps, traversing the list of passes twice // First forward traversal: // - we find the passes that first use a resource // - we increase the refcount of the last version for each resource used // - we find where fences must be added in case of async compute/gfx queues and the related async pass dependencies for (int passIndex = 0; passIndex < ctx.passData.Length; passIndex++) { ref var pass = ref ctx.passData.ElementAt(passIndex); if (pass.culled) continue; // In case of an async pass, we need to extend the lifetime of the resource to the first pass on the graphics queue that waits for this async pass to be completed. // By doing so, we ensure that the resource will not be released back to the pool right after adding the async pass commands to the async queue, // as it could create a data race condition if a non async pass reuses the resource from the pool while the async pass is still processing it // As contextData must be filled incrementally per pass, we store temporarily these delayed releases in a list. // Here we clear this list before using it later in the second foward traversal. ClearDelayedLastUseListAtPass(passIndex); pass.waitOnGraphicsFencePassId = -1; pass.awaitingMyGraphicsFencePassId = -1; pass.insertGraphicsFence = false; // Loop over all the resources this pass needs (=inputs) foreach (ref readonly var input in pass.Inputs(ctx)) { var inputResource = input.resource; ref var pointTo = ref ctx.UnversionedResourceData(inputResource); ref var pointToVer = ref ctx.VersionedResourceData(inputResource); pointTo.lastUsePassID = -1; // If nobody else is using it yet, // mark this pass as the first using the resource. // It can happen that two passes use v0, e.g.: // pass1.UseTex(v0,Read) -> this will clear the pass but keep it at v0 // pass2.UseTex(v0,Read) -> "reads" v0 if (pointTo.firstUsePassID < 0) { pointTo.firstUsePassID = pass.passId; pass.AddFirstUse(inputResource, ctx); } // This pass uses the last version of a resource increase the ref count of this resource var last = pointTo.latestVersionNumber; if (last == inputResource.version) { pointTo.tag++; //Refcount of how many passes are using the last version of a resource } // Verify if this pass needs to wait on a fence due to its inputs // If no RG pass writes to the resource, no need to wait for anyone if (pointToVer.written) { ref var writingPass = ref ctx.passData.ElementAt(pointToVer.writePassId); if (writingPass.asyncCompute != pass.asyncCompute) { // Find the last pass on the opposite queue that the current pass must wait on var currWaitForPassId = pass.waitOnGraphicsFencePassId; pass.waitOnGraphicsFencePassId = Math.Max(writingPass.passId, currWaitForPassId); } } } //Also look at outputs (but with version 1) for edge case were we do a Write (but no read) to a texture and the pass is manually excluded from culling //As it isn't read it won't be in the inputs array with V0 foreach (ref readonly var output in pass.Outputs(ctx)) { var outputResource = output.resource; ref var pointTo = ref ctx.UnversionedResourceData(outputResource); ref var pointToVer = ref ctx.VersionedResourceData(outputResource); // If nobody else is using it yet (no explicit read), // Mark this pass as the first using the resource. // It can happen that two passes use v0, e.g.: // pass1.UseTex(v0, Write) -> implicit read of v0, writes v1 - culled because none explicitly reads v1 // pass3.UseTex(v1, Write) -> implicit read of v1, writes v2 - not culled because of unrelated reason if (pointTo.firstUsePassID < 0) { pointTo.firstUsePassID = pass.passId; pass.AddFirstUse(outputResource, ctx); } // This pass outputs the last version of a resource track that var last = pointTo.latestVersionNumber; if (last == outputResource.version) { Debug.Assert(pointTo.lastWritePassID == -1); // Only one can be the last writer pointTo.lastWritePassID = pass.passId; } // Resolve if this pass should insert a fence for its outputs var numReaders = pointToVer.numReaders; for (var i = 0; i < numReaders; ++i) { var readerIndex = ctx.resources.IndexReader(outputResource, i); ref var readerData = ref ctx.resources.readerData[outputResource.iType].ElementAt(readerIndex); ref var readerPass = ref ctx.passData.ElementAt(readerData.passId); if (pass.asyncCompute != readerPass.asyncCompute) { // A subsequent pass on the opposite queue will read this resource written by the current pass, // so this subsequent pass needs to wait for the completion of the current pass // to do so, the current pass will insert a fence on its queue after its execution pass.insertGraphicsFence = true; // Different async passes can wait for different resources // Find the first pass on the opposite queue that will wait for this fence var currFirstPassId = pass.awaitingMyGraphicsFencePassId; pass.awaitingMyGraphicsFencePassId = currFirstPassId == -1 ? readerData.passId : Math.Min(currFirstPassId, readerData.passId); } } } } // Second forward traversal: // - we decrease the refcount to detect which is the last pass using the last version of a resource, i.e when we can release it // - in case of async processing, we must delay the release to the first pass on gfx queue waiting for a fence for (int passIndex = 0; passIndex < ctx.passData.Length; passIndex++) { ref var pass = ref ctx.passData.ElementAt(passIndex); if (pass.culled) continue; bool isAsync = pass.asyncCompute; foreach (ref readonly var input in pass.Inputs(ctx)) { var inputResource = input.resource; ref var pointTo = ref ctx.UnversionedResourceData(inputResource); var last = pointTo.latestVersionNumber; if (last == inputResource.version) { var refC = pointTo.tag - 1; //Decrease refcount this pass is done using it if (refC == 0) // We're the last pass done using it, this pass should destroy it. { if (isAsync) { // If no fence found, we fallback to the last non culled pass of the graph on graphics queue, not ideal but safe bool foundFence = FindFirstPassIdOnGraphicsQueueAwaitingFenceGoingForward(ref pass, out int firstWaitingOrLastPassId); var delayLastUsedPassId = FindFirstNonCulledPassIdGoingBackward(firstWaitingOrLastPassId, !foundFence); pointTo.lastUsePassID = delayLastUsedPassId; AddDelayedLastUseToPass(inputResource, delayLastUsedPassId); } else { pointTo.lastUsePassID = pass.passId; pass.AddLastUse(inputResource, ctx); } } pointTo.tag = refC; } } // We're outputting a resource that is never used. // This can happen if this pass has multiple outputs and only a portion of them are used // as some are used, the whole pass is not culled but the unused output still should be freed foreach (ref readonly var output in pass.Outputs(ctx)) { var outputResource = output.resource; ref var pointTo = ref ctx.UnversionedResourceData(outputResource); ref var pointToVer = ref ctx.VersionedResourceData(outputResource); var last = pointTo.latestVersionNumber; if (last == outputResource.version && pointToVer.numReaders == 0) { if (isAsync) { // If no fence found, we fallback to the last non culled pass of the graph, not ideal but safe bool foundFence = FindFirstPassIdOnGraphicsQueueAwaitingFenceGoingForward(ref pass, out int firstWaitingOrLastPassId); var delayLastUsedPassId = FindFirstNonCulledPassIdGoingBackward(firstWaitingOrLastPassId, !foundFence); pointTo.lastUsePassID = delayLastUsedPassId; AddDelayedLastUseToPass(outputResource, delayLastUsedPassId); } else { pointTo.lastUsePassID = pass.passId; pass.AddLastUse(outputResource, ctx); } } } // Add any potential delayed resource releases to the contextData AddLastUseFromDelayedList(ref pass); } } } void ClearDelayedLastUseListAtPass(int passId) { if (m_DelayedLastUseListPerPassMap.TryGetValue(passId, out var lastUseListForPassId)) { lastUseListForPassId.Clear(); } } void AddDelayedLastUseToPass(in ResourceHandle releaseResource, int passId) { if (!m_DelayedLastUseListPerPassMap.TryGetValue(passId, out var lastUseListForPassId)) { lastUseListForPassId = new List(); m_DelayedLastUseListPerPassMap.Add(passId, lastUseListForPassId); } lastUseListForPassId.Add(releaseResource); } public void AddLastUseFromDelayedList(ref PassData passData) { if (m_DelayedLastUseListPerPassMap.TryGetValue(passData.passId, out var lastUseListForPassId)) { foreach (var resource in lastUseListForPassId) { passData.AddLastUse(resource, contextData); } lastUseListForPassId.Clear(); } } void PrepareNativeRenderPasses() { // Prepare all native render pass execution info: for (var passIdx = 0; passIdx < contextData.nativePassData.Length; ++passIdx) { ref var nativePassData = ref contextData.nativePassData.ElementAt(passIdx); DetermineLoadStoreActions(ref nativePassData); } } void PropagateTextureUVOrigin() { using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_PropagateTextureUVOrigin))) { // Work backwards through the native pass list and propagate the texture uv origin we store with to // any texture attachments that are not explicitly known (usually intermediate memoryless attachments). for (int passIdx = contextData.nativePassData.Length - 1; passIdx >= 0; --passIdx) { ref NativePassData nativePassData = ref contextData.nativePassData.ElementAt(passIdx); // Find a texture attachment that is storing to find out the orientation for this pass. int attachmentsCount = nativePassData.attachments.size; int firstStoreAttachmentIndex = 0; TextureUVOriginSelection storeUVOrigin = TextureUVOriginSelection.Unknown; for (int attIdx = 0; attIdx < attachmentsCount; ++attIdx) { ref NativePassAttachment nativePassAttachment = ref nativePassData.attachments[attIdx]; if (nativePassAttachment.storeAction != RenderBufferStoreAction.DontCare) { if (nativePassAttachment.handle.type == RenderGraphResourceType.Texture) // Only textures have orientation { ref ResourceUnversionedData resData = ref contextData.UnversionedResourceData(nativePassAttachment.handle); storeUVOrigin = resData.textureUVOrigin; // Inherit the orientation of the store if we are currently storing to an unknown orientation. firstStoreAttachmentIndex = attIdx; break; } } } // Update any texture attachments with an unknown uv origin to the one we are going to use for storing and validate // we don't have a mixture of uv origins on the texture attachment list as this would mean something is going to be // read/written upside down. for (int attIdx = 0; attIdx < attachmentsCount; ++attIdx) { ref NativePassAttachment nativePassAttachment = ref nativePassData.attachments[attIdx]; if (nativePassAttachment.handle.type == RenderGraphResourceType.Texture) { ref ResourceUnversionedData resData = ref contextData.UnversionedResourceData(nativePassAttachment.handle); if (storeUVOrigin != TextureUVOriginSelection.Unknown && resData.textureUVOrigin != TextureUVOriginSelection.Unknown && resData.textureUVOrigin != storeUVOrigin) { ref NativePassAttachment firstStoreNativePassAttachment = ref nativePassData.attachments[firstStoreAttachmentIndex]; var firstStoreAttachmentName = graph.m_ResourcesForDebugOnly.GetRenderGraphResourceName(firstStoreNativePassAttachment.handle); var name = graph.m_ResourcesForDebugOnly.GetRenderGraphResourceName(nativePassAttachment.handle); throw new InvalidOperationException($"From pass '{contextData.passNames[nativePassData.firstGraphPass]}' to pass '{contextData.passNames[nativePassData.lastGraphPass]}' when trying to store resource '{name}' of type {nativePassAttachment.handle.type} at index {nativePassAttachment.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOriginStore(firstStoreAttachmentName, storeUVOrigin, name, resData.textureUVOrigin)); } resData.textureUVOrigin = storeUVOrigin; } } } } } static bool IsGlobalTextureInPass(RenderGraphPass pass, in ResourceHandle handle) { foreach (var g in pass.setGlobalsList) { if (g.Item1.handle.index == handle.index) { return true; } } return false; } void DetectMemoryLessResources() { using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_DetectMemorylessResources))) { // No need to go further if we don't support memoryless textures if (!SystemInfo.supportsMemorylessTextures) return; // Native renderpasses and create/destroy lists have now been set-up. Detect memoryless resources, i.e resources that are created/destroyed // within the scope of an nrp foreach (ref readonly var nativePass in contextData.NativePasses) { // Loop over all created resources by this nrp var graphPasses = nativePass.GraphPasses(contextData, out var actualPasses); foreach (ref readonly var subPass in graphPasses) { foreach (ref readonly var createdRes in subPass.FirstUsedResources(contextData)) { ref var createInfo = ref contextData.UnversionedResourceData(createdRes); if (createdRes.type == RenderGraphResourceType.Texture && createInfo.isImported == false) { bool isGlobal = IsGlobalTextureInPass(graph.m_RenderPasses[subPass.passId], createdRes); // Note: You could think but what if the texture is used as a regular non-frambuffer attachment texture // surely it can't be memoryless then? // That is true, but it can never happen as the fact these passes got merged means the textures cannot be used // as regular textures halfway through a pass. If that were the case they would never have been merged in the first place. // Except! If the pass consists of a single pass, in that case a texture could be allocated and freed within the single pass // This is a somewhat degenerate case (e.g. a pass with culling forced off doing a uav write that is never used anywhere) // But to avoid execution errors we still need to create the resource in this case. // Check if it is in the destroy list of any of the subpasses > if yes > memoryless foreach (ref readonly var subPass2 in graphPasses) { foreach (ref readonly var destroyedRes in subPass2.LastUsedResources(contextData)) { ref var destInfo = ref contextData.UnversionedResourceData(destroyedRes); if (destroyedRes.type == RenderGraphResourceType.Texture && destInfo.isImported == false) { if (createdRes.index == destroyedRes.index && !isGlobal) { // If a single pass in the native pass we need to check fragment attachment otherwise we're good // we could always check this in theory but it's an optimization not to check it. if (nativePass.numNativeSubPasses > 1 || subPass2.IsUsedAsFragment(createdRes, contextData)) { createInfo.memoryLess = true; destInfo.memoryLess = true; } } } } } } } } if (actualPasses.IsCreated) actualPasses.Dispose(); } } } internal static bool IsSameNativeSubPass(ref SubPassDescriptor a, ref SubPassDescriptor b) { const SubPassFlags k_SubPassMergeIgnoreMask = ~(SubPassFlags.TileProperties | SubPassFlags.MultiviewRenderRegionsCompatible); // Mask out the flags we can ignore. SubPassFlags aflags = a.flags & k_SubPassMergeIgnoreMask; SubPassFlags bflags = b.flags & k_SubPassMergeIgnoreMask; if (aflags != bflags || a.colorOutputs.Length != b.colorOutputs.Length || a.inputs.Length != b.inputs.Length) { return false; } for (int i = 0; i < a.colorOutputs.Length; i++) { if (a.colorOutputs[i] != b.colorOutputs[i]) { return false; } } for (int i = 0; i < a.inputs.Length; i++) { if (a.inputs[i] != b.inputs[i]) { return false; } } return true; } private bool ExecuteInitializeResource(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, in PassData pass) { bool haveGfxCommandsBeenAddedToCmd = false; using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteInitializeResources))) { resources.forceManualClearOfResource = true; // For raster passes we need to create resources for all the subpasses at the beginning of the native renderpass if (pass.type == RenderGraphPassType.Raster && pass.nativePassIndex >= 0) { if (pass.mergeState == PassMergeState.Begin || pass.mergeState == PassMergeState.None) { ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex); var graphPasses = nativePass.GraphPasses(contextData, out var actualPasses); foreach (ref readonly var subPass in graphPasses) { foreach (ref readonly var res in subPass.FirstUsedResources(contextData)) { ref readonly var resInfo = ref contextData.UnversionedResourceData(res); bool usedAsFragmentThisPass = subPass.IsUsedAsFragment(res, contextData); // This resource is read for the first time as a regular texture and not as a framebuffer attachment // so if requested we need to explicitly clear it, as loadAction.clear only works on framebuffer attachments resources.forceManualClearOfResource = !usedAsFragmentThisPass; if (!resInfo.isImported) { // If the compiler has detected that this resource can be memoryless, // we need to update the texture descriptor that will be used to create the memoryless RTHandle. // Memoryless resources are created to allow implicit conversion from TextureHandle to RTHandle. // Such conversions can happen on users side when manipulating texture handles. if (resInfo.memoryLess) { resources.SetTextureAsMemoryLess(res); } // We create the resources from a pool // memoryless resources are also created but will not allocate in system memory haveGfxCommandsBeenAddedToCmd |= resources.CreatePooledResource(rgContext, res.iType, res.index); } else // Imported resource { if (resInfo.clear && !resInfo.memoryLess && resources.forceManualClearOfResource) { haveGfxCommandsBeenAddedToCmd |= resources.ClearResource(rgContext, res.iType, res.index); } } } } if (actualPasses.IsCreated) actualPasses.Dispose(); } } // Other passes just create them at the beginning of the individual pass else { foreach (ref readonly var create in pass.FirstUsedResources(contextData)) { ref readonly var pointTo = ref contextData.UnversionedResourceData(create); if (!pointTo.isImported) { haveGfxCommandsBeenAddedToCmd |= resources.CreatePooledResource(rgContext, create.iType, create.index); } else // Imported resource { if (pointTo.clear) { haveGfxCommandsBeenAddedToCmd |= resources.ClearResource(rgContext, create.iType, create.index); } } } } resources.forceManualClearOfResource = true; } return haveGfxCommandsBeenAddedToCmd; } #if UNITY_EDITOR || DEVELOPMENT_BUILD static LoadAudit s_EmptyLoadAudit = new LoadAudit(LoadReason.InvalidReason); static StoreAudit s_EmptyStoreAudit = new StoreAudit(StoreReason.InvalidReason); #endif void DetermineLoadStoreActions(ref NativePassData nativePass) { using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_PrepareNativePass))) { ref readonly var firstGraphPass = ref contextData.passData.ElementAt(nativePass.firstGraphPass); ref readonly var lastGraphPass = ref contextData.passData.ElementAt(nativePass.lastGraphPass); // Some passes don't do any rendering only state changes so just skip them // If these passes trigger any drawing the raster command buffer will warn users no render targets are set-up for their rendering if (nativePass.fragments.size <= 0) return; // Some sanity checks, these should not happen Debug.Assert(firstGraphPass.mergeState is PassMergeState.Begin or PassMergeState.None); Debug.Assert(lastGraphPass.mergeState is PassMergeState.End or PassMergeState.None); ref readonly var fragmentList = ref nativePass.fragments; // determine load store actions // This pass also contains the latest versions used within this pass // As we have no pass reordering for now the merged passes are always a consecutive list and we can simply do a range // check on the create/destroy passid to see if it's allocated/freed in this native renderpass #if UNITY_EDITOR || DEVELOPMENT_BUILD bool generateAudits = RenderGraphDebugSession.hasActiveDebugSession || s_ForceGenerateAuditsForTests; ref var currLoadAudit = ref s_EmptyLoadAudit; ref var currStoreAudit = ref s_EmptyStoreAudit; #endif for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) { ref readonly var fragment = ref fragmentList[fragmentId]; // Default values ResourceHandle handle = fragment.resource; bool memoryless = false; int mipLevel = fragment.mipLevel; int depthSlice = fragment.depthSlice; // Don't care by default RenderBufferLoadAction loadAction = RenderBufferLoadAction.DontCare; RenderBufferStoreAction storeAction = RenderBufferStoreAction.DontCare; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) { nativePass.loadAudit.Add(new LoadAudit(LoadReason.FullyRewritten)); currLoadAudit = ref nativePass.loadAudit[nativePass.loadAudit.size - 1]; // Get the last added element nativePass.storeAudit.Add(new StoreAudit(StoreReason.DiscardUnused)); currStoreAudit = ref nativePass.storeAudit[nativePass.storeAudit.size - 1]; // Similarly for storeAudit } #endif // Writing by-default has to preserve the contents, think rendering only a few small triangles on top of a big framebuffer // So it means we need to load/clear contents potentially. // If a user pass knows it will write all pixels in a buffer (like a blit) it can use the WriteAll/Discard usage to indicate this to the graph bool partialWrite = fragment.accessFlags.HasFlag(AccessFlags.Write) && !fragment.accessFlags.HasFlag(AccessFlags.Discard); ref readonly var resourceData = ref contextData.UnversionedResourceData(fragment.resource); bool isImported = resourceData.isImported; int destroyPassID = resourceData.lastUsePassID; bool usedAfterThisNativePass = (destroyPassID >= (nativePass.lastGraphPass + 1)); // Read or partial-write logic if (fragment.accessFlags.HasFlag(AccessFlags.Read) || partialWrite) { // The resource is already allocated before this pass so we need to load it if (resourceData.firstUsePassID < nativePass.firstGraphPass) { loadAction = RenderBufferLoadAction.Load; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currLoadAudit = new LoadAudit(LoadReason.LoadPreviouslyWritten, resourceData.firstUsePassID); #endif // Once we decide to load a resource, we must default to the Store action if the resource is used after the current native pass. // If we were to use the DontCare action in this case, the driver would be effectively be allowed to discard the // contents of the resource. This is true even when we're only performing reads on it. if (usedAfterThisNativePass) { storeAction = RenderBufferStoreAction.Store; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit(StoreReason.StoreUsedByLaterPass, destroyPassID); #endif } } // It's first used this native pass so we need to clear it so reads/partial writes return the correct clear value // the clear colors are part of the resource description and set-up when executing the graph we don't need to care about that here. else { if (isImported) { // Check if the user indicated he wanted clearing of his imported resource on it's first use by the graph if (resourceData.clear) { loadAction = RenderBufferLoadAction.Clear; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currLoadAudit = new LoadAudit(LoadReason.ClearImported); #endif } else { loadAction = RenderBufferLoadAction.Load; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currLoadAudit = new LoadAudit(LoadReason.LoadImported); #endif } } else { // Created by the graph internally clear on first read loadAction = RenderBufferLoadAction.Clear; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currLoadAudit = new LoadAudit(LoadReason.ClearCreated); #endif } } } // Write logic if (fragment.accessFlags.HasFlag(AccessFlags.Write)) { // Simple non-msaa case if (nativePass.samples <= 1) { if (usedAfterThisNativePass) { // The resource is still used after this native pass so we need to store it. storeAction = RenderBufferStoreAction.Store; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit(StoreReason.StoreUsedByLaterPass, destroyPassID); #endif } else { // This is the last native pass that uses the resource. // If it's imported, we store it because its contents may be used outside the graph. // Otherwise, we can safely discard its contents. // // The one exception to this, is the user declared discard flag which allows us to assume an imported // resource is not used outside the graph. if (isImported) { if (resourceData.discard) { storeAction = RenderBufferStoreAction.DontCare; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit(StoreReason.DiscardImported); #endif } else { storeAction = RenderBufferStoreAction.Store; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit(StoreReason.StoreImported); #endif } } else { storeAction = RenderBufferStoreAction.DontCare; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit(StoreReason.DiscardUnused); #endif } } } // Complex msaa case else { // The resource is still used after this native pass so we need to store it. // as we don't know what happens with them and assume the contents are somewhow used outside the graph // With MSAA we may access the resolved data for longer than the MSAA data so we track the destroyPass and lastPassThatNeedsUnresolved separately // In theory the opposite could also be true (use MSAA after resolve data is no longer needed) but we consider it sufficiently strange to not // consider it here. storeAction = RenderBufferStoreAction.DontCare; //Check if we're the last pass writing it by checking the output version of the current pass is the higherst version the resource will reach bool lastWriter = (resourceData.latestVersionNumber == fragment.resource.version); // Cheaper but same? = resourceData.lastWritePassID >= pass.firstGraphPass && resourceData.lastWritePassID < pass.firstGraphPass + pass.numSubPasses; bool isImportedLastWriter = isImported && lastWriter; // Used outside this native render pass, we need to store something if (destroyPassID >= nativePass.firstGraphPass + nativePass.numGraphPasses) { // Assume nothing is needed unless we are an imported texture (which doesn't require discarding) and we're the last ones writing it bool needsMSAASamples = isImportedLastWriter && !resourceData.discard; bool needsResolvedData = isImportedLastWriter && (resourceData.bindMS == false); int userPassID = 0; int msaaUserPassID = 0; // Check if we need msaa/resolved data by checking all the passes using this buffer // Partial writes will register themselves as readers so this should be adequate foreach (ref readonly var reader in contextData.Readers(fragment.resource)) { ref var readerPass = ref contextData.passData.ElementAt(reader.passId); bool isFragmentUsed = readerPass.IsUsedAsFragment(fragment.resource, contextData); // Unsafe pass - we cannot know how it is used, so we need to both store and resolve if (readerPass.type == RenderGraphPassType.Unsafe) { needsMSAASamples = true; needsResolvedData = !resourceData.bindMS; msaaUserPassID = reader.passId; userPassID = reader.passId; break; } // A fragment attachment use we need the msaa samples if (isFragmentUsed) { needsMSAASamples = true; msaaUserPassID = reader.passId; } else { // Used as a multisample-texture we need the msaa samples if (resourceData.bindMS) { needsMSAASamples = true; msaaUserPassID = reader.passId; } // Used as a regular non-multisample texture we need resolved data else { needsResolvedData = true; userPassID = reader.passId; } } } if (needsMSAASamples && needsResolvedData) { storeAction = RenderBufferStoreAction.StoreAndResolve; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit( (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), userPassID, (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), msaaUserPassID); #endif } else if (needsResolvedData) { storeAction = RenderBufferStoreAction.Resolve; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit( (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), userPassID, StoreReason.DiscardUnused); #endif } else if (needsMSAASamples) { storeAction = RenderBufferStoreAction.Store; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit( (resourceData.bindMS ? StoreReason.DiscardBindMs : StoreReason.DiscardUnused), -1, (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), msaaUserPassID); #endif } else { Debug.Assert(false, "Resource was not destroyed but nobody seems to be using it?!"); } } else if (isImportedLastWriter) { //It's an imported texture and we're the last ones writing it make sure to store the results // Used as a multisample-texture, we need the msaa samples only if (resourceData.bindMS) { if (resourceData.discard) { storeAction = RenderBufferStoreAction.DontCare; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit(StoreReason.DiscardImported); #endif } else { storeAction = RenderBufferStoreAction.Store; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit( StoreReason.DiscardBindMs, -1, StoreReason.StoreImported); #endif } } // Used as a regular non-multisample texture, we need samples as resolved data // we have no idea which one of them will be needed by the external users else { if (resourceData.discard) { // Depth attachment always comes first if existing bool isDepthAttachment = (nativePass.hasDepth && nativePass.attachments.size == 0); // For color attachment, we only discard the MSAA buffers and keep the resolve texture // This is a design decision due to the restrictive ImportResourceParams API, it could be revised later storeAction = isDepthAttachment ? RenderBufferStoreAction.DontCare : RenderBufferStoreAction.Resolve; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit( StoreReason.DiscardImported, -1, StoreReason.DiscardImported); #endif } else { storeAction = RenderBufferStoreAction.StoreAndResolve; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (generateAudits) currStoreAudit = new StoreAudit( StoreReason.StoreImported, -1, StoreReason.StoreImported); #endif } } } } } if (resourceData.memoryLess) { memoryless = true; #if UNITY_EDITOR || DEVELOPMENT_BUILD // Ensure load/store actions are actually valid for memory less if (loadAction == RenderBufferLoadAction.Load) throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_LoadingMemorylessResource); if (storeAction != RenderBufferStoreAction.DontCare) throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_ResolvignMemorylessResource); #endif } var newAttachment = new NativePassAttachment( handle, loadAction, storeAction, memoryless, mipLevel, depthSlice ); nativePass.attachments.Add(newAttachment); } } } [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] private void ValidateNativePass(in NativePassData nativePass, int width, int height, int depth, int samples, int attachmentCount) { if (RenderGraph.enableValidityChecks) { if (nativePass.attachments.size == 0 || nativePass.numNativeSubPasses == 0) throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_RenderPassIsEmpty); if (width == 0 || height == 0 || depth == 0 || samples == 0 || nativePass.numNativeSubPasses == 0 || attachmentCount == 0) throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_RenderPassHasInvalidProperties); } } [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] private void ValidateAttachment(in RenderTargetInfo attRenderTargetInfo, RenderGraphResourceRegistry resources, int nativePassWidth, int nativePassHeight, int nativePassMSAASamples, bool isVrs, bool isShaderResolve) { if (RenderGraph.enableValidityChecks) { if (isVrs) { var tileSize = ShadingRateImage.GetAllocTileSize(nativePassWidth, nativePassHeight); if (attRenderTargetInfo.width != tileSize.x || attRenderTargetInfo.height != tileSize.y || attRenderTargetInfo.msaaSamples != 1) { throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_ShadingRateImageAttachmentDoesNotMatch); } } else { if (attRenderTargetInfo.width != nativePassWidth || attRenderTargetInfo.height != nativePassHeight || (attRenderTargetInfo.msaaSamples != nativePassMSAASamples && !isShaderResolve)) throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_AttachmentsDoNotMatch); } } } internal unsafe void ExecuteBeginRenderPass(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, ref NativePassData nativePass) { using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteBeginRenderpassCommand))) { ref var attachments = ref nativePass.attachments; var attachmentCount = attachments.size; var width = nativePass.width; var height = nativePass.height; var volumeDepth = nativePass.volumeDepth; var samples = nativePass.samples; var isShaderResolve = nativePass.extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve); ValidateNativePass(nativePass, width, height, volumeDepth, samples, attachmentCount); ref var nativeSubPasses = ref contextData.nativeSubPassData; NativeArray nativeSubPassArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(nativeSubPasses.GetUnsafeReadOnlyPtr() + nativePass.firstNativeSubPass, nativePass.numNativeSubPasses, Allocator.None); #if ENABLE_UNITY_COLLECTIONS_CHECKS var safetyHandle = AtomicSafetyHandle.Create(); AtomicSafetyHandle.SetAllowReadOrWriteAccess(safetyHandle, true); NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeSubPassArray, safetyHandle); #endif if (nativePass.hasFoveatedRasterization) { rgContext.cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Enabled); } if (nativePass.hasShadingRateStates) { rgContext.cmd.SetShadingRateFragmentSize(nativePass.shadingRateFragmentSize); rgContext.cmd.SetShadingRateCombiner(ShadingRateCombinerStage.Primitive, nativePass.primitiveShadingRateCombiner); rgContext.cmd.SetShadingRateCombiner(ShadingRateCombinerStage.Fragment, nativePass.fragmentShadingRateCombiner); } // Filling the attachments array to be sent to the rendering command buffer if(!m_BeginRenderPassAttachments.IsCreated) m_BeginRenderPassAttachments = new NativeList(FixedAttachmentArray.MaxAttachments, Allocator.Persistent); m_BeginRenderPassAttachments.Resize(attachmentCount, NativeArrayOptions.UninitializedMemory); for (var i = 0; i < attachmentCount; ++i) { ref readonly var currAttachmentHandle = ref attachments[i].handle; resources.GetRenderTargetInfo(currAttachmentHandle, out var renderTargetInfo); bool isVrs = (i == nativePass.shadingRateImageIndex); ValidateAttachment(renderTargetInfo, resources, width, height, samples, isVrs, isShaderResolve); ref var currBeginAttachment = ref m_BeginRenderPassAttachments.ElementAt(i); currBeginAttachment = new AttachmentDescriptor(renderTargetInfo.format); // Set up the RT pointers var rtHandle = resources.GetTexture(currAttachmentHandle.index); //HACK: Always set the loadstore target even if StoreAction == DontCare or Resolve //and LoadAction == Clear or DontCare //in these cases you could argue setting the loadStoreTarget to NULL and only set the resolveTarget //but this confuses the backend (on vulkan) and in general is not how the lower level APIs tend to work. //because of the RenderTexture duality where we always bundle store+resolve targets as one RTex //it does become impossible to have a memoryless loadStore texture with a memoryfull resolve //but that is why we mark this as a hack and future work to fix. //The proper (and planned) solution would be to move away from the render texture duality. RenderTargetIdentifier rtidAllSlices = rtHandle; currBeginAttachment.loadStoreTarget = new RenderTargetIdentifier(rtidAllSlices, attachments[i].mipLevel, CubemapFace.Unknown, attachments[i].depthSlice); if (attachments[i].storeAction == RenderBufferStoreAction.Resolve || attachments[i].storeAction == RenderBufferStoreAction.StoreAndResolve) { currBeginAttachment.resolveTarget = rtHandle; } currBeginAttachment.loadAction = attachments[i].loadAction; currBeginAttachment.storeAction = attachments[i].storeAction; // Set up clear colors if we have a clear load action if (attachments[i].loadAction == RenderBufferLoadAction.Clear) { currBeginAttachment.clearColor = Color.red; currBeginAttachment.clearDepth = 1.0f; currBeginAttachment.clearStencil = 0; ref readonly var desc = ref resources.GetTextureResourceDesc(currAttachmentHandle, true); if (i == 0 && nativePass.hasDepth) { // TODO: There seems to be no clear depth specified ?!?! currBeginAttachment.clearDepth = 1.0f; // desc.clearDepth; } else { currBeginAttachment.clearColor = desc.clearColor; } } } if (nativePass.extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve)) { var lastSubpass = nativeSubPassArray[^1]; // All input attachments must be memoryless for the shader resolve enabled subpass. for (int i = 0; i < lastSubpass.inputs.Length; i++) { int inputIndex = lastSubpass.inputs[i]; ref var inputAttachment = ref m_BeginRenderPassAttachments.ElementAt(inputIndex); if (inputAttachment.storeAction != RenderBufferStoreAction.DontCare) { throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_MultisampledShaderResolveInputAttachmentNotMemoryless); } } // The last subpass in a native pass with shader resolve is required to be the subpass that handles the resolve, and this subpass can only have 1 color attachment. if (lastSubpass.colorOutputs.Length != 1) throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_MultisampledShaderResolveInvalidAttachmentSetup); if (SystemInfo.supportsMultisampledShaderResolve) { int attachmentIndex = lastSubpass.colorOutputs[0]; ref var currBeginAttachment = ref m_BeginRenderPassAttachments.ElementAt(attachmentIndex); currBeginAttachment.resolveTarget = currBeginAttachment.loadStoreTarget; currBeginAttachment.loadStoreTarget = new RenderTargetIdentifier(BuiltinRenderTextureType.None); currBeginAttachment.storeAction = RenderBufferStoreAction.Store; } } NativeArray attachmentDescArray = m_BeginRenderPassAttachments.AsArray(); var depthAttachmentIndex = nativePass.hasDepth ? 0 : -1; var graphPassNamesForDebugSpan = ReadOnlySpan.Empty; #if DEVELOPMENT_BUILD || UNITY_EDITOR if (RenderGraph.enableValidityChecks) { graphPassNamesForDebug.Clear(); nativePass.GetGraphPassNames(contextData, graphPassNamesForDebug); int utf8CStrDebugNameLength = 0; foreach (ref readonly Name graphPassName in graphPassNamesForDebug) { utf8CStrDebugNameLength += graphPassName.utf8ByteCount + 1; // +1 to add '/' between passes or the null terminator at the end } var nameBytes = stackalloc byte[utf8CStrDebugNameLength]; if (utf8CStrDebugNameLength > 0) { int startStr = 0; foreach (ref readonly var graphPassName in graphPassNamesForDebug) { int strByteCount = graphPassName.utf8ByteCount; System.Text.Encoding.UTF8.GetBytes(graphPassName.name.AsSpan(), new Span(nameBytes + startStr, strByteCount)); startStr += strByteCount; // Adding '/' in UTF8 nameBytes[startStr++] = (byte)(0x2F); } // Rewriting last '/' to be the null terminator nameBytes[utf8CStrDebugNameLength - 1] = (byte)0; } graphPassNamesForDebugSpan = new ReadOnlySpan(nameBytes, utf8CStrDebugNameLength); } #endif rgContext.cmd.BeginRenderPass(width, height, volumeDepth, samples, attachmentDescArray, depthAttachmentIndex, nativePass.shadingRateImageIndex, nativeSubPassArray, graphPassNamesForDebugSpan); #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.Release(safetyHandle); #endif CommandBuffer.ThrowOnSetRenderTarget = true; } } const int ArbitraryMaxNbMergedPasses = 16; DynamicArray graphPassNamesForDebug = new DynamicArray(ArbitraryMaxNbMergedPasses); private void ExecuteDestroyResource(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, ref PassData pass) { using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteDestroyResources))) { // Unsafe pass might soon use temporary render targets, // users can also use temporary data in their render graph execute nodes using public RenderGraphObjectPool API // In both cases, we need to release these resources after the node execution rgContext.renderGraphPool.ReleaseAllTempAlloc(); if (pass.type == RenderGraphPassType.Raster && pass.nativePassIndex >= 0) { // For raster passes we need to destroy resources after all the subpasses at the end of the native renderpass if (pass.mergeState == PassMergeState.End || pass.mergeState == PassMergeState.None) { ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex); var graphPasses = nativePass.GraphPasses(contextData, out var actualPasses); foreach (ref readonly var subPass in graphPasses) { foreach (ref readonly var res in subPass.LastUsedResources(contextData)) { ref readonly var resInfo = ref contextData.UnversionedResourceData(res); if (resInfo.isImported == false) { resources.ReleasePooledResource(rgContext, res.iType, res.index); } } } if (actualPasses.IsCreated) actualPasses.Dispose(); } } else { foreach (ref readonly var destroy in pass.LastUsedResources(contextData)) { ref readonly var pointTo = ref contextData.UnversionedResourceData(destroy); if (pointTo.isImported == false) { resources.ReleasePooledResource(rgContext, destroy.iType, destroy.index); } } } } } private void ExecuteSetRenderTargets(RenderGraphPass pass, InternalRenderGraphContext rgContext) { var depthBufferIsValid = pass.depthAccess.textureHandle.IsValid(); if (depthBufferIsValid || pass.colorBufferMaxIndex != -1) { var resources = graph.m_ResourcesForDebugOnly; var colorBufferAccess = pass.colorBufferAccess; if (pass.colorBufferMaxIndex > 0) { var mrtArray = m_TempMRTArrays[pass.colorBufferMaxIndex]; for (int i = 0; i <= pass.colorBufferMaxIndex; ++i) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (!colorBufferAccess[i].textureHandle.IsValid()) throw new InvalidOperationException($"In pass {pass.name}, when trying to use {colorBufferAccess[i].textureHandle.handle.type} attachment at index {colorBufferAccess[i].textureHandle.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidMRTSetup); #endif mrtArray[i] = resources.GetTexture(colorBufferAccess[i].textureHandle); } if (depthBufferIsValid) { CoreUtils.SetRenderTarget(rgContext.cmd, mrtArray, resources.GetTexture(pass.depthAccess.textureHandle)); } else { throw new InvalidOperationException($"In pass {pass.name} - " + RenderGraph.RenderGraphExceptionMessages.k_NoDepthBufferMRT); } } else { if (depthBufferIsValid) { if (pass.colorBufferMaxIndex > -1) { CoreUtils.SetRenderTarget(rgContext.cmd, resources.GetTexture(pass.colorBufferAccess[0].textureHandle), resources.GetTexture(pass.depthAccess.textureHandle)); } else { CoreUtils.SetRenderTarget(rgContext.cmd, resources.GetTexture(pass.depthAccess.textureHandle)); } } else { if (pass.colorBufferAccess[0].textureHandle.IsValid()) { CoreUtils.SetRenderTarget(rgContext.cmd, resources.GetTexture(pass.colorBufferAccess[0].textureHandle)); } else throw new InvalidOperationException($"In pass {pass.name} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidDepthAndColorTargets); } } } } internal unsafe void ExecuteSetRandomWriteTarget(in CommandBuffer cmd, RenderGraphResourceRegistry resources, int index, in ResourceHandle resource, bool preserveCounterValue = true) { if (resource.type == RenderGraphResourceType.Texture) { var tex = resources.GetTexture(resource.index); cmd.SetRandomWriteTarget(index, tex); } else if (resource.type == RenderGraphResourceType.Buffer) { var buff = resources.GetBuffer(resource.index); // Default is to preserve the value if (preserveCounterValue) { cmd.SetRandomWriteTarget(index, buff); } else { cmd.SetRandomWriteTarget(index, buff, false); } } else { var name = resources.GetRenderGraphResourceName(resource); throw new Exception($"When trying to use resource '{name}' of type {resource.type} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidResourceType); } } internal void ExecuteRenderGraphPass(ref InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, RenderGraphPass pass) { rgContext.executingPass = pass; if (!pass.HasRenderFunc()) { throw new InvalidOperationException($"In pass {pass.name} - " + RenderGraph.RenderGraphExceptionMessages.k_NoRenderFunction); } using (new ProfilingScope(rgContext.cmd, pass.customSampler)) { pass.Execute(rgContext); foreach (var tex in pass.setGlobalsList) { rgContext.cmd.SetGlobalTexture(tex.Item2, tex.Item1); } } } public void ExecuteGraph(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, in List passes) { bool inRenderPass = false; previousCommandBuffer = rgContext.cmd; // Having random access targets bound leads to all sorts of weird behavior so we clear them before executing the graph. rgContext.cmd.ClearRandomWriteTargets(); for (int passIndex = 0; passIndex < contextData.passData.Length; passIndex++) { ref var passData = ref contextData.passData.ElementAt(passIndex); if (passData.culled) continue; bool nrpBegan = false; bool haveGfxCommandsBeenAddedToCmdDuringResInit = ExecuteInitializeResource(rgContext, resources, passData); if (passData.type == RenderGraphPassType.Compute && passData.asyncCompute) { GraphicsFence previousFence = new GraphicsFence(); // We add a fence to the gfx cmd if the async compute cmd needs to wait for some resources to be cleared if (haveGfxCommandsBeenAddedToCmdDuringResInit) { previousFence = rgContext.cmd.CreateGraphicsFence(GraphicsFenceType.AsyncQueueSynchronisation, SynchronisationStageFlags.AllGPUOperations); } if (!rgContext.contextlessTesting) rgContext.renderContext.ExecuteCommandBuffer(rgContext.cmd); rgContext.cmd.Clear(); var asyncCmd = CommandBufferPool.Get("async cmd"); asyncCmd.SetExecutionFlags(CommandBufferExecutionFlags.AsyncCompute); rgContext.cmd = asyncCmd; if (haveGfxCommandsBeenAddedToCmdDuringResInit) { rgContext.cmd.WaitOnAsyncGraphicsFence(previousFence, SynchronisationStageFlags.PixelProcessing); } } // also make sure to insert fence=waits for multiple queue syncs if (passData.waitOnGraphicsFencePassId != -1) { rgContext.cmd.WaitOnAsyncGraphicsFence(contextData.fences[passData.waitOnGraphicsFencePassId], SynchronisationStageFlags.PixelProcessing); } if (passData.type == RenderGraphPassType.Raster && passData.mergeState <= PassMergeState.Begin) { if (passData.nativePassIndex >= 0) { ref var nativePass = ref contextData.nativePassData.ElementAt(passData.nativePassIndex); if (nativePass.fragments.size > 0) { ExecuteBeginRenderPass(rgContext, resources, ref nativePass); nrpBegan = true; inRenderPass = true; } } } else if (passData.type == RenderGraphPassType.Unsafe) { ExecuteSetRenderTargets(passes[passIndex], rgContext); } if (passData.mergeState >= PassMergeState.SubPass) { if (passData.beginNativeSubpass) { if (!inRenderPass) { throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_BeginNoActivePass); } rgContext.cmd.NextSubPass(); } } if (passData.numRandomAccessResources > 0) { foreach (ref readonly var randomWriteAttachment in passData.RandomWriteTextures(contextData)) { ExecuteSetRandomWriteTarget(rgContext.cmd, resources, randomWriteAttachment.index, randomWriteAttachment.resource); } } ExecuteRenderGraphPass(ref rgContext, resources, passes[passData.passId]); EndRenderGraphPass(ref rgContext, ref passData, ref inRenderPass, resources, nrpBegan); } } void EndRenderGraphPass(ref InternalRenderGraphContext rgContext, ref PassData passData, ref bool inRenderPass, RenderGraphResourceRegistry resources, bool nrpBegan) { // If we set any uavs clear them again so they are local to the pass if (passData.numRandomAccessResources > 0) { rgContext.cmd.ClearRandomWriteTargets(); } // should we insert a fence to sync between difference queues? if (passData.insertGraphicsFence) { var fence = rgContext.cmd.CreateAsyncGraphicsFence(); contextData.fences[passData.passId] = fence; } if (passData.type == RenderGraphPassType.Raster) { var hasRenderPassEnded = (passData.mergeState == PassMergeState.None && nrpBegan) || passData.mergeState == PassMergeState.End; if (hasRenderPassEnded) { if (passData.nativePassIndex >= 0) { ref var nativePass = ref contextData.nativePassData.ElementAt(passData.nativePassIndex); if (nativePass.fragments.size > 0) { if (!inRenderPass) { throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_NoActivePassForSubpass); } if (nativePass.hasFoveatedRasterization) { rgContext.cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Disabled); } rgContext.cmd.EndRenderPass(); CommandBuffer.ThrowOnSetRenderTarget = false; inRenderPass = false; // VRS ShadingRate(Image) cannot be set inside a render pass (cmdBuf). // ShadingRate is set before BeginRenderPass and here we ResetShadingRate after EndRenderPass. if (nativePass.hasShadingRateStates || nativePass.hasShadingRateImage) { rgContext.cmd.ResetShadingRate(); } } } } } else if (passData.type == RenderGraphPassType.Compute && passData.asyncCompute) { rgContext.renderContext.ExecuteCommandBufferAsync(rgContext.cmd, ComputeQueueType.Background); CommandBufferPool.Release(rgContext.cmd); rgContext.cmd = previousCommandBuffer; } ExecuteDestroyResource(rgContext, resources, ref passData); } } }