using System; using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.Rendering; using System.Collections.Generic; using Unity.Collections; namespace UnityEngine.Rendering.RenderGraphModule.NativeRenderPassCompiler { // Per pass info on inputs to the pass [DebuggerDisplay("PassInputData: Res({resource.index})")] internal readonly struct PassInputData { public readonly ResourceHandle resource; public PassInputData(ResourceHandle resource) { this.resource = resource; } } // Per pass info on outputs to the pass [DebuggerDisplay("PassOutputData: Res({resource.index})")] internal readonly struct PassOutputData { public readonly ResourceHandle resource; public PassOutputData(ResourceHandle resource) { this.resource = resource; } } // Per pass fragment (attachment) info [DebuggerDisplay("PassFragmentData: Res({resource.index}):{accessFlags}")] internal readonly struct PassFragmentData { public readonly ResourceHandle resource; public readonly AccessFlags accessFlags; public readonly int mipLevel; public readonly int depthSlice; public PassFragmentData(ResourceHandle handle, AccessFlags flags, int mipLevel, int depthSlice) { resource = handle; accessFlags = flags; this.mipLevel = mipLevel; this.depthSlice = depthSlice; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { var hash = resource.GetHashCode(); hash = hash * 23 + accessFlags.GetHashCode(); hash = hash * 23 + mipLevel.GetHashCode(); hash = hash * 23 + depthSlice.GetHashCode(); return hash; } // If you modify this, check if struct RenderPassSetup::Attachment in "GfxDevice\RenderPassSetup.h" also needs changes [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool SameSubResource(in PassFragmentData x, in PassFragmentData y) { // We ignore the version for now we assume if one pass writes version x and the next y they can // be merged in the same native render pass // We also do not look at the access flags as they get OR-ed together when adding subpasses to the native pass so the access flags // will always cover the required access (and thus possibly more if required by other passes) return x.resource.index == y.resource.index && x.mipLevel == y.mipLevel && x.depthSlice == y.depthSlice; } } // Per pass random write texture info [DebuggerDisplay("PassRandomWriteData: Res({resource.index}):{index}:{preserveCounterValue}")] internal readonly struct PassRandomWriteData { public readonly ResourceHandle resource; public readonly int index; public readonly bool preserveCounterValue; public PassRandomWriteData(ResourceHandle resource, int index, bool preserveCounterValue) { this.resource = resource; this.index = index; this.preserveCounterValue = preserveCounterValue; } public override int GetHashCode() { var hash = resource.GetHashCode(); hash = hash * 23 + index.GetHashCode(); return hash; } } internal enum PassMergeState { None = -1, // this pass is "standalone", it will begin and end the NRP Begin = 0, // Only Begin NRP is done by this pass, sequential passes will End the NRP (after 0 or more additional subpasses) SubPass = 1, // This pass is a subpass only. Begin has been called by a previous pass and end will be called by a pass after this one End = 2 // This pass is both a subpass and will end the current NRP } // Data per pass internal struct PassData { // Warning, any field must initialized in both constructor and ResetAndInitialize function public int passId; // Index of self in the passData list, can we calculate this somehow in c#? would use offsetof in c++ public RenderGraphPassType type; public bool hasFoveatedRasterization; public int tag; // Arbitrary per node int used by various graph analysis tools public ShadingRateFragmentSize shadingRateFragmentSize; public ShadingRateCombiner primitiveShadingRateCombiner; public ShadingRateCombiner fragmentShadingRateCombiner; public PassMergeState mergeState; public int nativePassIndex; // Index of the native pass this pass belongs to public int nativeSubPassIndex; // Index of the native subpass this pass belongs to public int firstInput; //base+offset in CompilerContextData.inputData (use the InputNodes iterator to iterate this more easily) public int numInputs; public int firstOutput; //base+offset in CompilerContextData.outputData (use the OutputNodes iterator to iterate this more easily) public int numOutputs; public int firstFragment; //base+offset in CompilerContextData.fragmentData (use the Fragments iterator to iterate this more easily) public int numFragments; public int firstFragmentInput; //base+offset in CompilerContextData.fragmentData (use the Fragment inputs iterator to iterate this more easily) public int numFragmentInputs; public int firstRandomAccessResource; //base+offset in CompilerContextData.randomWriteData (use the Fragment inputs iterator to iterate this more easily) public int numRandomAccessResources; public int firstCreate; //base+offset in CompilerContextData.createData (use the InputNodes iterator to iterate this more easily) public int numCreated; public int firstDestroy; //base+offset in CompilerContextData.destroyData (use the InputNodes iterator to iterate this more easily) public int numDestroyed; public int shadingRateImageIndex; //base+offset in CompilerContextData.fragmentData (there is no iterator, there are always 2 if shading rate is used) public int fragmentInfoWidth; public int fragmentInfoHeight; public int fragmentInfoVolumeDepth; public int fragmentInfoSamples; public int waitOnGraphicsFencePassId; // -1 if no fence wait is needed, otherwise the passId to wait on public bool asyncCompute; public bool hasSideEffects; public bool culled; public bool beginNativeSubpass; // If true this is the first graph pass of a merged native subpass public bool fragmentInfoValid; public bool fragmentInfoHasDepth; public bool fragmentInfoHasShadingRateImage => shadingRateImageIndex > 0; public bool insertGraphicsFence; // Whether this pass should insert a fence into the command buffer public bool hasShadingRateStates; [MethodImpl(MethodImplOptions.AggressiveInlining)] public Name GetName(CompilerContextData ctx) => ctx.GetFullPassName(passId); public PassData(in RenderGraphPass pass, int passIndex) { passId = passIndex; type = pass.type; asyncCompute = pass.enableAsyncCompute; hasSideEffects = !pass.allowPassCulling; hasFoveatedRasterization = pass.enableFoveatedRasterization; mergeState = PassMergeState.None; nativePassIndex = -1; nativeSubPassIndex = -1; beginNativeSubpass = false; culled = false; tag = 0; firstInput = 0; numInputs = 0; firstOutput = 0; numOutputs = 0; firstFragment = 0; numFragments = 0; firstRandomAccessResource = 0; numRandomAccessResources = 0; firstFragmentInput = 0; numFragmentInputs = 0; firstCreate = 0; numCreated = 0; firstDestroy = 0; numDestroyed = 0; fragmentInfoValid = false; fragmentInfoWidth = 0; fragmentInfoHeight = 0; fragmentInfoVolumeDepth = 0; fragmentInfoSamples = 0; fragmentInfoHasDepth = false; insertGraphicsFence = false; waitOnGraphicsFencePassId = -1; hasShadingRateStates = pass.hasShadingRateStates; shadingRateFragmentSize = pass.shadingRateFragmentSize; primitiveShadingRateCombiner = pass.primitiveShadingRateCombiner; fragmentShadingRateCombiner = pass.fragmentShadingRateCombiner; shadingRateImageIndex = -1; } // Helper func to reset and initialize existing PassData struct directly in a data container without costly deep copy (~120bytes) when adding it public void ResetAndInitialize(in RenderGraphPass pass, int passIndex) { passId = passIndex; type = pass.type; asyncCompute = pass.enableAsyncCompute; hasSideEffects = !pass.allowPassCulling; hasFoveatedRasterization = pass.enableFoveatedRasterization; mergeState = PassMergeState.None; nativePassIndex = -1; nativeSubPassIndex = -1; beginNativeSubpass = false; culled = false; tag = 0; firstInput = 0; numInputs = 0; firstOutput = 0; numOutputs = 0; firstFragment = 0; numFragments = 0; firstFragmentInput = 0; numFragmentInputs = 0; firstRandomAccessResource = 0; numRandomAccessResources = 0; firstCreate = 0; numCreated = 0; firstDestroy = 0; numDestroyed = 0; fragmentInfoValid = false; fragmentInfoWidth = 0; fragmentInfoHeight = 0; fragmentInfoVolumeDepth = 0; fragmentInfoSamples = 0; fragmentInfoHasDepth = false; insertGraphicsFence = false; waitOnGraphicsFencePassId = -1; hasShadingRateStates = pass.hasShadingRateStates; shadingRateFragmentSize = pass.shadingRateFragmentSize; primitiveShadingRateCombiner = pass.primitiveShadingRateCombiner; fragmentShadingRateCombiner = pass.fragmentShadingRateCombiner; shadingRateImageIndex = -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan Outputs(CompilerContextData ctx) => ctx.outputData.MakeReadOnlySpan(firstOutput, numOutputs); [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan Inputs(CompilerContextData ctx) => ctx.inputData.MakeReadOnlySpan(firstInput, numInputs); [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan Fragments(CompilerContextData ctx) => ctx.fragmentData.MakeReadOnlySpan(firstFragment, numFragments); [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly PassFragmentData ShadingRateImage(CompilerContextData ctx) => ctx.fragmentData[shadingRateImageIndex]; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan FragmentInputs(CompilerContextData ctx) => ctx.fragmentData.MakeReadOnlySpan(firstFragmentInput, numFragmentInputs); [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan FirstUsedResources(CompilerContextData ctx) => ctx.createData.MakeReadOnlySpan(firstCreate, numCreated); // Loop over this pass's random write textures returned as PassFragmentData public ReadOnlySpan RandomWriteTextures(CompilerContextData ctx) => ctx.randomAccessResourceData.MakeReadOnlySpan(firstRandomAccessResource, numRandomAccessResources); [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan LastUsedResources(CompilerContextData ctx) => ctx.destroyData.MakeReadOnlySpan(firstDestroy, numDestroyed); private void SetupAndValidateFragmentInfo(ResourceHandle h, CompilerContextData ctx) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (h.type != RenderGraphResourceType.Texture) new Exception("Only textures can be used as a fragment attachment."); #endif ref readonly var resInfo = ref ctx.UnversionedResourceData(h); #if DEVELOPMENT_BUILD || UNITY_EDITOR if (resInfo.width == 0 || resInfo.height == 0 || resInfo.msaaSamples == 0) throw new Exception("GetRenderTargetInfo returned invalid results."); #endif if (fragmentInfoValid) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (fragmentInfoWidth != resInfo.width || fragmentInfoHeight != resInfo.height || fragmentInfoVolumeDepth != resInfo.volumeDepth || fragmentInfoSamples != resInfo.msaaSamples) throw new Exception("Mismatch in Fragment dimensions"); #endif } else { fragmentInfoWidth = resInfo.width; fragmentInfoHeight = resInfo.height; fragmentInfoSamples = resInfo.msaaSamples; fragmentInfoVolumeDepth = resInfo.volumeDepth; fragmentInfoValid = true; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AddFragment(ResourceHandle h, CompilerContextData ctx) { SetupAndValidateFragmentInfo(h, ctx); numFragments++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AddFragmentInput(ResourceHandle h, CompilerContextData ctx) { SetupAndValidateFragmentInfo(h, ctx); numFragmentInputs++; } internal void AddRandomAccessResource() { // This function is here for orthogonality with AddFragment/AddFragmentInput // Random write textures can be arbitrary sizes and do not need to validate or set-up the fragment info numRandomAccessResources++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AddFirstUse(ResourceHandle h, CompilerContextData ctx) { // Already registered? Skip it foreach (ref readonly var res in FirstUsedResources(ctx)) { if (res.index == h.index && res.type == h.type) return; } ctx.createData.Add(h); int addedIndex = ctx.createData.LastIndex(); // First item added, set up firstCreate if (numCreated == 0) { firstCreate = addedIndex; } Debug.Assert(addedIndex == firstCreate + numCreated, "you can only incrementally set-up the Creation lists for all passes, AddCreation is called in an arbitrary non-incremental way"); numCreated++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AddLastUse(ResourceHandle h, CompilerContextData ctx) { // Already registered? Skip it foreach (ref readonly var res in LastUsedResources(ctx)) { if (res.index == h.index && res.type == h.type) return; } ctx.destroyData.Add(h); int addedIndex = ctx.destroyData.LastIndex(); // First item added, set up firstDestroy if (numDestroyed == 0) { firstDestroy = addedIndex; } Debug.Assert(addedIndex == firstDestroy + numDestroyed, "you can only incrementally set-up the Destruction lists for all passes, AddCreation is called in an arbitrary non-incremental way"); numDestroyed++; } // Is the resource used as a fragment this pass. // As it is ambiguous if this is an input our output version, the version is ignored // This checks use of both MRT attachment as well as input attachment [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly bool IsUsedAsFragment(ResourceHandle h, CompilerContextData ctx) { //Only textures can be used as a fragment attachment. if (h.type != RenderGraphResourceType.Texture) return false; // Only raster passes can have fragment attachments if (type != RenderGraphPassType.Raster) return false; foreach (ref readonly var fragment in Fragments(ctx)) { if (fragment.resource.index == h.index) { return true; } } foreach (ref readonly var fragmentInput in FragmentInputs(ctx)) { if (fragmentInput.resource.index == h.index) { return true; } } return false; } } // Data per attachment of a native renderpass [DebuggerDisplay("Res({handle.index}) : {loadAction} : {storeAction} : {memoryless}")] internal readonly struct NativePassAttachment { public readonly ResourceHandle handle; public readonly RenderBufferLoadAction loadAction; public readonly RenderBufferStoreAction storeAction; public readonly bool memoryless; public readonly int mipLevel; public readonly int depthSlice; public NativePassAttachment(ResourceHandle handle, RenderBufferLoadAction loadAction, RenderBufferStoreAction storeAction, bool memoryless, int mipLevel, int depthSlice) { this.handle = handle; this.loadAction = loadAction; this.storeAction = storeAction; this.memoryless = memoryless; this.mipLevel = mipLevel; this.depthSlice = depthSlice; } } internal enum LoadReason { InvalidReason, LoadImported, LoadPreviouslyWritten, ClearImported, ClearCreated, FullyRewritten, Count } [DebuggerDisplay("{reason} : {passId}")] internal readonly struct LoadAudit { public static readonly string[] LoadReasonMessages = { "Invalid reason", "The resource is imported in the graph and loaded to retrieve the existing buffer contents.", "The resource is written by {pass} executed previously in the graph. The data is loaded.", "The resource is imported in the graph but was imported with the 'clear on first use' option enabled. The data is cleared.", "The resource is created in this pass and cleared on first use.", "The pass indicated it will rewrite the full resource contents. Existing contents are not loaded or cleared.", }; public readonly LoadReason reason; public readonly int passId; public LoadAudit(LoadReason setReason, int setPassId = -1) { #if UNITY_EDITOR Debug.Assert(LoadReasonMessages.Length == (int)LoadReason.Count, $"Make sure {nameof(LoadReasonMessages)} is in sync with {nameof(LoadReason)}"); #endif reason = setReason; passId = setPassId; } } internal enum StoreReason { InvalidReason, StoreImported, StoreUsedByLaterPass, DiscardImported, DiscardUnused, DiscardBindMs, NoMSAABuffer, Count } [DebuggerDisplay("{reason} : {passId} / MSAA {msaaReason} : {msaaPassId}")] internal readonly struct StoreAudit { public static readonly string[] StoreReasonMessages = { "Invalid reason", "The resource is imported in the graph. The data is stored so results are available outside the graph.", "The resource is read by pass {pass} executed later in the graph. The data is stored.", "The resource is imported but the import was with the 'discard on last use' option enabled. The data is discarded.", "The resource is written by this pass but no later passes are using the results. The data is discarded.", "The resource was created as MSAA only resource, the data can never be resolved.", "The resource is a single sample resource, there is no multi-sample data to handle.", }; public readonly StoreReason reason; public readonly int passId; public readonly StoreReason msaaReason; public readonly int msaaPassId; public StoreAudit(StoreReason setReason, int setPassId = -1, StoreReason setMsaaReason = StoreReason.NoMSAABuffer, int setMsaaPassId = -1) { #if UNITY_EDITOR Debug.Assert(StoreReasonMessages.Length == (int)StoreReason.Count, $"Make sure {nameof(StoreReasonMessages)} is in sync with {nameof(StoreReason)}"); #endif reason = setReason; passId = setPassId; msaaReason = setMsaaReason; msaaPassId = setMsaaPassId; } } internal enum PassBreakReason { NotOptimized, // Optimize never ran on this pass TargetSizeMismatch, // Target Sizes or msaa samples don't match NextPassReadsTexture, // The next pass reads data written by this pass as a texture NextPassTargetsTexture, // The next pass targets the texture that this pass is reading NonRasterPass, // The next pass is a non-raster pass DifferentDepthTextures, // The next pass uses a different depth texture (and we only allow one in a whole NRP) AttachmentLimitReached, // Adding the next pass would have used more attachments than allowed SubPassLimitReached, // Addind the next pass would have generated more subpasses than allowed EndOfGraph, // The last pass in the graph was reached FRStateMismatch, // One pass is using foveated rendering and the other not DifferentShadingRateImages, // The next pass uses a different shading rate image (and we only allow one in a whole NRP) DifferentShadingRateStates, // The next pass uses different shading rate states (and we only allow one set in a whole NRP) PassMergingDisabled, // Wasn't merged because pass merging is disabled Merged, // I actually got merged Count } [DebuggerDisplay("{reason} : {breakPass}")] internal readonly struct PassBreakAudit { public readonly PassBreakReason reason; public readonly int breakPass; public PassBreakAudit(PassBreakReason reason, int breakPass) { #if UNITY_EDITOR Debug.Assert(BreakReasonMessages.Length == (int)PassBreakReason.Count, $"Make sure {nameof(BreakReasonMessages)} is in sync with {nameof(PassBreakReason)}"); #endif this.reason = reason; this.breakPass = breakPass; // This is not so simple as finding the next pass as it might be culled etc, so we store it to be sure we get the right pass } public static readonly string[] BreakReasonMessages = { "The native render pass optimizer never ran on this pass. Pass is standalone and not merged.", "The render target sizes of the next pass do not match.", "The next pass reads data output by this pass as a regular texture.", "The next pass uses a texture sampled in this pass as a render target.", "The next pass is not a raster render pass.", "The next pass uses a different depth buffer. All passes in the native render pass need to use the same depth buffer.", $"The limit of {FixedAttachmentArray.MaxAttachments} native pass attachments would be exceeded when merging with the next pass.", $"The limit of {NativePassCompiler.k_MaxSubpass} native subpasses would be exceeded when merging with the next pass.", "This is the last pass in the graph, there are no other passes to merge.", "The next pass uses a different foveated rendering state", "The next pass uses a different shading rate image", "The next pass uses a different shading rate rendering state", "Pass merging is disabled so this pass was not merged", "The next pass got merged into this pass.", }; } // Data per native renderpass internal struct NativePassData { public FixedAttachmentArray loadAudit; public FixedAttachmentArray storeAudit; public PassBreakAudit breakAudit; public FixedAttachmentArray fragments; public FixedAttachmentArray attachments; // Index of the first graph pass this native pass encapsulates public int firstGraphPass; // Offset+count in context pass array public int lastGraphPass; public int numGraphPasses; public int firstNativeSubPass; // Offset+count in context subpass array public int numNativeSubPasses; public int width; public int height; public int volumeDepth; public int samples; public int shadingRateImageIndex; public bool hasDepth; public bool hasFoveatedRasterization; public bool hasShadingRateImage => shadingRateImageIndex >= 0; public bool hasShadingRateStates; public ShadingRateFragmentSize shadingRateFragmentSize; public ShadingRateCombiner primitiveShadingRateCombiner; public ShadingRateCombiner fragmentShadingRateCombiner; public NativePassData(ref PassData pass, CompilerContextData ctx) { firstGraphPass = pass.passId; lastGraphPass = pass.passId; numGraphPasses = 1; firstNativeSubPass = -1; // Set up during compile numNativeSubPasses = 0; fragments = new FixedAttachmentArray(); attachments = new FixedAttachmentArray(); width = pass.fragmentInfoWidth; height = pass.fragmentInfoHeight; volumeDepth = pass.fragmentInfoVolumeDepth; samples = pass.fragmentInfoSamples; hasDepth = pass.fragmentInfoHasDepth; hasFoveatedRasterization = pass.hasFoveatedRasterization; loadAudit = new FixedAttachmentArray(); storeAudit = new FixedAttachmentArray(); breakAudit = new PassBreakAudit(PassBreakReason.NotOptimized, -1); foreach (ref readonly var fragment in pass.Fragments(ctx)) { fragments.Add(fragment); } foreach (ref readonly var fragment in pass.FragmentInputs(ctx)) { fragments.Add(fragment); } // Shading rate and foveation are distinct systems and mutually exclusive. // Foveation always taking precedence over VRS. if (pass.fragmentInfoHasShadingRateImage && !hasFoveatedRasterization) { shadingRateImageIndex = fragments.size; fragments.Add(pass.ShadingRateImage(ctx)); } else shadingRateImageIndex = -1; hasShadingRateStates = pass.hasShadingRateStates && !hasFoveatedRasterization; shadingRateFragmentSize = pass.shadingRateFragmentSize; primitiveShadingRateCombiner = pass.primitiveShadingRateCombiner; fragmentShadingRateCombiner = pass.fragmentShadingRateCombiner; // Graph pass is added as the first native subpass TryMergeNativeSubPass(ctx, ref this, ref pass); } // Gets the best SubPassFlag for a pass that originally had no depth attachment, that we want to merge with this pass. public SubPassFlags GetSubPassFlagForMerging() { // We should not be calling this method if native pass doesn't have depth. if (hasDepth == false) { throw new Exception("SubPassFlag for merging can not be determined if native pass doesn't have a depth attachment"); } // Only do this for mobile using Vulkan. #if (PLATFORM_ANDROID) if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Vulkan) { // Depth attachment is always at index 0. return (fragments[0].accessFlags.HasFlag(AccessFlags.Write)) ? SubPassFlags.None : SubPassFlags.ReadOnlyDepth; } else { return SubPassFlags.ReadOnlyDepth; } #else // By default flag this subpass as ReadOnlyDepth. return SubPassFlags.ReadOnlyDepth; #endif } public void Clear() { firstGraphPass = 0; numGraphPasses = 0; attachments.Clear(); fragments.Clear(); loadAudit.Clear(); storeAudit.Clear(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool IsValid() { return numGraphPasses > 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan GraphPasses(CompilerContextData ctx) { // When there's no pass being culled, we can directly return a Span of the Native List if (lastGraphPass - firstGraphPass + 1 == numGraphPasses) { return ctx.passData.MakeReadOnlySpan(firstGraphPass, numGraphPasses); } var actualPasses = new NativeArray(numGraphPasses, Allocator.Temp, NativeArrayOptions.UninitializedMemory); for (int i = firstGraphPass, index = 0; i < lastGraphPass + 1; ++i) { var pass = ctx.passData[i]; if (!pass.culled) { actualPasses[index++] = pass; } } return actualPasses; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void GetGraphPassNames(CompilerContextData ctx, DynamicArray dest) { foreach (ref readonly var pass in GraphPasses(ctx)) { dest.Add(pass.GetName(ctx)); } } // This function does not modify the current render graph state, it only evaluates and returns the correct PassBreakAudit public static PassBreakAudit CanMerge(CompilerContextData contextData, int activeNativePassId, int passIdToMerge) { ref var passToMerge = ref contextData.passData.ElementAt(passIdToMerge); // Non raster passes (low level, compute,...) will break the native pass chain // as they may need to do SetRendertarget or non-fragment work if (passToMerge.type != RenderGraphPassType.Raster) { return new PassBreakAudit(PassBreakReason.NonRasterPass, passIdToMerge); } ref var nativePass = ref contextData.nativePassData.ElementAt(activeNativePassId); // If a pass has no fragment attachments a lot of the tests can be skipped // You could argue that a raster pass with no fragments is not allowed but why not? // it allow us to set some legacy unity state from fragment passes without breaking NRP // Allowing this would means something like // Fill Gbuffer - Set Shadow Globals - Do light pass would break as the shadow globals pass // is a 0x0 pass with no rendertargets that just sets shadow global texture pointers // By allowing this to be merged into the NRP we can actually ensure these passes are merged bool hasFragments = (passToMerge.numFragments > 0 || passToMerge.numFragmentInputs > 0); if (hasFragments) { // Easy early outs, sizes mismatch if (nativePass.width != passToMerge.fragmentInfoWidth || nativePass.height != passToMerge.fragmentInfoHeight || nativePass.volumeDepth != passToMerge.fragmentInfoVolumeDepth || nativePass.samples != passToMerge.fragmentInfoSamples) { return new PassBreakAudit(PassBreakReason.TargetSizeMismatch, passIdToMerge); } // Easy early outs, different depth buffers we only allow a single depth for the whole NRP for now ?!? // Depth buffer is by-design always at index 0 if (nativePass.hasDepth && passToMerge.fragmentInfoHasDepth) { ref readonly var firstFragment = ref contextData.fragmentData.ElementAt(passToMerge.firstFragment); if (nativePass.fragments[0].resource.index != firstFragment.resource.index) { return new PassBreakAudit(PassBreakReason.DifferentDepthTextures, passIdToMerge); } } // We do not support foveation state changes within the renderpass due to platform limitation if (nativePass.hasFoveatedRasterization != passToMerge.hasFoveatedRasterization) { return new PassBreakAudit(PassBreakReason.FRStateMismatch, passIdToMerge); } // Different shading rate images; only allow one per NRP if (nativePass.hasShadingRateImage != passToMerge.fragmentInfoHasShadingRateImage) { return new PassBreakAudit(PassBreakReason.DifferentShadingRateImages, passIdToMerge); } if (nativePass.hasShadingRateImage) { var passToMergeSRI = passToMerge.ShadingRateImage(contextData); var nativePassSRI = nativePass.fragments[nativePass.shadingRateImageIndex]; if (nativePassSRI.resource.index != passToMergeSRI.resource.index) { return new PassBreakAudit(PassBreakReason.DifferentShadingRateImages, passIdToMerge); } } // Different shading rate states; only allow one set per NRP if (nativePass.hasShadingRateStates != passToMerge.hasShadingRateStates) { return new PassBreakAudit(PassBreakReason.DifferentShadingRateStates, passIdToMerge); } if (nativePass.hasShadingRateStates) { if (nativePass.shadingRateFragmentSize != passToMerge.shadingRateFragmentSize || nativePass.primitiveShadingRateCombiner != passToMerge.primitiveShadingRateCombiner || nativePass.fragmentShadingRateCombiner != passToMerge.fragmentShadingRateCombiner) { return new PassBreakAudit(PassBreakReason.DifferentShadingRateStates, passIdToMerge); } } } // Check the non-fragment inputs of this pass, if they are generated by the current open native pass we can't merge // as we need to commit the pixels to the texture foreach (ref readonly var input in passToMerge.Inputs(contextData)) { var inputResource = input.resource; var writingPassId = contextData.resources[inputResource].writePassId; // Is the writing pass enclosed in the current native renderpass if (writingPassId >= nativePass.firstGraphPass && writingPassId < nativePass.lastGraphPass + 1) { // If it's not used as a fragment, it's used as some sort of texture read of load so we need so sync it out if (!passToMerge.IsUsedAsFragment(inputResource, contextData)) { return new PassBreakAudit(PassBreakReason.NextPassReadsTexture, passIdToMerge); } } } // Gather which attachments to add to the current renderpass var attachmentsToTryAdding = new FixedAttachmentArray(); // We can't have more than the maximum amount of attachments in a given native renderpass int currAvailableAttachmentSlots = FixedAttachmentArray.MaxAttachments - nativePass.fragments.size; foreach (ref readonly var fragment in passToMerge.Fragments(contextData)) { bool alreadyAttached = false; for (int i = 0; i < nativePass.fragments.size; ++i) { if (PassFragmentData.SameSubResource(nativePass.fragments[i], fragment)) { alreadyAttached = true; break; } } // This fragment is not attached to the native renderpass yet, we will need to attach it if (!alreadyAttached) { // We already reached the maximum amount of attachments in this renderpass // We can't add any new attachment, just start a new renderpass if (currAvailableAttachmentSlots == 0) { return new PassBreakAudit(PassBreakReason.AttachmentLimitReached, passIdToMerge); } else { attachmentsToTryAdding.Add(fragment); currAvailableAttachmentSlots--; } } // Check if this fragment is already sampled in the native renderpass not as a fragment but as an input for (int i = nativePass.firstGraphPass; i <= nativePass.lastGraphPass; ++i) { ref var earlierPassData = ref contextData.passData.ElementAt(i); foreach (ref readonly var earlierInput in earlierPassData.Inputs(contextData)) { // If this fragment is already used in current native render pass if (earlierInput.resource.index == fragment.resource.index) { // If it's not used as a fragment, it's used as some sort of texture read of load so we need to sync it out if (!earlierPassData.IsUsedAsFragment(earlierInput.resource, contextData)) { return new PassBreakAudit(PassBreakReason.NextPassTargetsTexture, passIdToMerge); } } } } } foreach (ref readonly var fragmentInput in passToMerge.FragmentInputs(contextData)) { bool alreadyAttached = false; for (int i = 0; i < nativePass.fragments.size; ++i) { if (PassFragmentData.SameSubResource(nativePass.fragments[i], fragmentInput)) { alreadyAttached = true; break; } } // This fragment input is not attached to the native renderpass yet, we will need to attach it if (!alreadyAttached) { // We already reached the maximum amount of attachments in this native renderpass // We can't add any new attachment, just start a new renderpass if (currAvailableAttachmentSlots == 0) { return new PassBreakAudit(PassBreakReason.AttachmentLimitReached, passIdToMerge); } else { attachmentsToTryAdding.Add(fragmentInput); currAvailableAttachmentSlots--; } } } // We check first if we are at risk of having too many subpasses, // only then we do the costlier subpass merging check, short circuiting it whenever possible bool canAddAnExtraSubpass = (nativePass.numGraphPasses < NativePassCompiler.k_MaxSubpass); if (!canAddAnExtraSubpass && !CanMergeNativeSubPass(contextData, ref nativePass, ref passToMerge)) { return new PassBreakAudit(PassBreakReason.SubPassLimitReached, passIdToMerge); } // All is good! Pass can be merged into active native pass return new PassBreakAudit(PassBreakReason.Merged, passIdToMerge); } // This function follows the structure of TryMergeNativeSubPass but only tests if the new native subpass can be // merged with the last one, allowing for early returns. It does not modify the state // ref for nativePass is used for performance reasons. The method should not modify nativePass. static bool CanMergeNativeSubPass(CompilerContextData contextData, ref NativePassData nativePass, ref PassData passToMerge) { // We have no output attachments, this is an "empty" raster pass doing only non-rendering command so skip it. if (passToMerge.numFragments == 0 && passToMerge.numFragmentInputs == 0) { return true; } // Nothing to merge with if (nativePass.numNativeSubPasses == 0) { return false; } ref var lastPass = ref contextData.nativeSubPassData.ElementAt(nativePass.firstNativeSubPass + nativePass.numNativeSubPasses - 1); bool currRenderGraphPassHasDepth = passToMerge.fragmentInfoHasDepth; // If any output attachments, they must match existing ones in the subpass // Doing an early return if the count differs int colorOffset = currRenderGraphPassHasDepth ? -1 : 0; int colorOutputsLength = passToMerge.numFragments + colorOffset; if (colorOutputsLength != lastPass.colorOutputs.Length) { return false; } // If any input attachments, they must match existing ones in the subpass // Doing an early return if the count differs int inputsLength = passToMerge.numFragmentInputs; if (inputsLength != lastPass.inputs.Length) { return false; } SubPassFlags flags = SubPassFlags.None; // If depth ends up being bound only because of merging if (!currRenderGraphPassHasDepth && nativePass.hasDepth) { // Set SubPassFlags to best match the pass we are trying to merge with flags = nativePass.GetSubPassFlagForMerging(); } ref readonly var fragmentList = ref nativePass.fragments; // MRT attachments int fragmentIdx = 0; foreach (ref readonly var graphPassFragment in passToMerge.Fragments(contextData)) { // Check if we're handling the depth attachment if (currRenderGraphPassHasDepth && fragmentIdx == 0) { flags = (graphPassFragment.accessFlags.HasFlag(AccessFlags.Write)) ? SubPassFlags.None : SubPassFlags.ReadOnlyDepth; } // It's a color attachment else { // Find the index of this subpass attachment in the native renderpass attachment list int colorAttachmentIdx = -1; for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) { if (PassFragmentData.SameSubResource(fragmentList[fragmentId], graphPassFragment)) { colorAttachmentIdx = fragmentId; break; } } if (colorAttachmentIdx < 0 || colorAttachmentIdx != lastPass.colorOutputs[fragmentIdx + colorOffset]) { return false; } } fragmentIdx++; } // FB-fetch attachments int inputIndex = 0; foreach (ref readonly var graphFragmentInput in passToMerge.FragmentInputs(contextData)) { // Find the index of this subpass attachment in the native renderpass attachment list int inputAttachmentIdx = -1; for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) { if (PassFragmentData.SameSubResource(fragmentList[fragmentId], graphFragmentInput)) { inputAttachmentIdx = fragmentId; break; } } if (inputAttachmentIdx < 0 || inputAttachmentIdx != lastPass.inputs[inputIndex]) { return false; } inputIndex++; } // last check for flags return flags == lastPass.flags; } // Try to merge the new graph pass into the last native sub pass or create a new one in the current native pass. // Modifies the state public static void TryMergeNativeSubPass(CompilerContextData contextData, ref NativePassData nativePass, ref PassData passToMerge) { ref var fragmentList = ref nativePass.fragments; // Only done once per native pass (on creation), should stay -1 if no fragments if (nativePass.numNativeSubPasses == 0 && nativePass.fragments.size > 0) { nativePass.firstNativeSubPass = contextData.nativeSubPassData.Length; } // Fill out the subpass descriptors for the native renderpasses // NOTE: Not all graph subpasses get an actual native pass: // - There could be passes that do only non-raster ops (like setglobal) and have no attachments. They don't get a native pass // - Renderpasses that use exactly the same rendertargets at the previous pass use the same native pass. This is because // nextSubpass is expensive on some platforms (even if its' essentially a no-op as it's using the same attachments). SubPassDescriptor desc = new SubPassDescriptor(); // We have no output attachments, this is an "empty" raster pass doing only non-rendering command so skip it. if (passToMerge.numFragments == 0 && passToMerge.numFragmentInputs == 0) { // We always merge it into the currently active passToMerge.nativeSubPassIndex = nativePass.numNativeSubPasses - 1; passToMerge.beginNativeSubpass = false; return; } // If depth ends up being bound only because of merging if (!passToMerge.fragmentInfoHasDepth && nativePass.hasDepth) { // Set SubPassFlags to best match the pass we are trying to merge with desc.flags = nativePass.GetSubPassFlagForMerging(); } // MRT attachments { int fragmentIdx = 0; int colorOffset = (passToMerge.fragmentInfoHasDepth) ? -1 : 0; desc.colorOutputs = new AttachmentIndexArray(passToMerge.numFragments + colorOffset); foreach (ref readonly var graphPassFragment in passToMerge.Fragments(contextData)) { // Check if we're handling the depth attachment if (passToMerge.fragmentInfoHasDepth && fragmentIdx == 0) { desc.flags = (graphPassFragment.accessFlags.HasFlag(AccessFlags.Write)) ? SubPassFlags.None : SubPassFlags.ReadOnlyDepth; } // It's a color attachment else { // Find the index of this subpass's attachment in the native renderpass attachment list int colorAttachmentIdx = -1; for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) { if (PassFragmentData.SameSubResource(fragmentList[fragmentId], graphPassFragment)) { colorAttachmentIdx = fragmentId; break; } } Debug.Assert(colorAttachmentIdx >= 0); // If this is not the case it means we are using an attachment in a sub pass that is not part of the native pass !?!? clear bug // Set up the color indexes desc.colorOutputs[fragmentIdx + colorOffset] = colorAttachmentIdx; } fragmentIdx++; } } // FB-fetch attachments { int inputIndex = 0; desc.inputs = new AttachmentIndexArray(passToMerge.numFragmentInputs); foreach (ref readonly var fragmentInput in passToMerge.FragmentInputs(contextData)) { // Find the index of this subpass's attachment in the native renderpass attachment list int inputAttachmentIdx = -1; for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) { if (PassFragmentData.SameSubResource(fragmentList[fragmentId], fragmentInput)) { inputAttachmentIdx = fragmentId; break; } } Debug.Assert(inputAttachmentIdx >= 0); // If this is not the case it means we are using an attachment in a sub pass that is not part of the native pass !?!? clear bug // Set up the color indexes desc.inputs[inputIndex] = inputAttachmentIdx; inputIndex++; } } // Shading rate images { if (passToMerge.fragmentInfoHasShadingRateImage) { desc.flags |= SubPassFlags.UseShadingRateImage; } } if (nativePass.numNativeSubPasses == 0 || !NativePassCompiler.IsSameNativeSubPass(ref desc, ref contextData.nativeSubPassData.ElementAt( nativePass.firstNativeSubPass + nativePass.numNativeSubPasses - 1))) { contextData.nativeSubPassData.Add(desc); Debug.Assert(contextData.nativeSubPassData.LastIndex() == nativePass.firstNativeSubPass + nativePass.numNativeSubPasses); nativePass.numNativeSubPasses++; passToMerge.beginNativeSubpass = true; } else { passToMerge.beginNativeSubpass = false; } passToMerge.nativeSubPassIndex = nativePass.numNativeSubPasses - 1; } // Call this function while merging a graph pass and you need to add the depth attachment used by this graph pass // Make sure to call it before adding the rest of the graph pass attachments and its generated subpass void AddDepthAttachmentFirstDuringMerge(CompilerContextData contextData, in PassFragmentData depthAttachment) { // Native pass can only have a single depth attachment Debug.Assert(!hasDepth); fragments.Add(depthAttachment); hasDepth = true; var size = fragments.size; // If depth is the only attachment of the native pass, we are done if (size == 1) return; // size > 1 // In this case, we are adding depth attachment to a native pass with other existing attachments int prevDepthIdx = size - 1; // Depth must always been the first attachment, so we switch the previous first one with the recently added depth attachment (fragments[0], fragments[prevDepthIdx]) = (fragments[prevDepthIdx], fragments[0]); var depthFlag = GetSubPassFlagForMerging(); // We also need to increment the attachment indices of all the previous subpasses of this native pass. // Otherwise the existing subpasses will point to the wrong attachments with depth being set as the first one for (var nativeSubPassIndex = firstNativeSubPass; nativeSubPassIndex < firstNativeSubPass + numNativeSubPasses; nativeSubPassIndex++) { ref var subPassDesc = ref contextData.nativeSubPassData.ElementAt(nativeSubPassIndex); // If depth ends up being bound only because of merging // Set SubPassFlags to best match the pass we are trying to merge with subPassDesc.flags = depthFlag; // Updating subpass color outputs for (int i = 0; i < subPassDesc.colorOutputs.Length; i++) { if (subPassDesc.colorOutputs[i] == 0) { subPassDesc.colorOutputs[i] = prevDepthIdx; } } // Updating subpass color inputs (framebuffer fetch) for (int i = 0; i < subPassDesc.inputs.Length; i++) { if (subPassDesc.inputs[i] == 0) { subPassDesc.inputs[i] = prevDepthIdx; } } } // We also need to update the shading rate image index (VRS) if (hasShadingRateImage && shadingRateImageIndex == 0) { shadingRateImageIndex = prevDepthIdx; } } public static PassBreakAudit TryMerge(CompilerContextData contextData, int activeNativePassId, int passIdToMerge) { var passBreakAudit = CanMerge(contextData, activeNativePassId, passIdToMerge); // Pass cannot be merged into active native pass if (passBreakAudit.reason != PassBreakReason.Merged) return passBreakAudit; ref var passToMerge = ref contextData.passData.ElementAt(passIdToMerge); ref var nativePass = ref contextData.nativePassData.ElementAt(activeNativePassId); passToMerge.mergeState = PassMergeState.SubPass; if (passToMerge.nativePassIndex >= 0) contextData.nativePassData.ElementAt(passToMerge.nativePassIndex).Clear(); passToMerge.nativePassIndex = activeNativePassId; nativePass.numGraphPasses++; nativePass.lastGraphPass = passIdToMerge; // Depth needs special handling if the native pass doesn't have depth and merges with a graph pass that does // as we require the depth attachment to be at index 0 if (!nativePass.hasDepth && passToMerge.fragmentInfoHasDepth) { nativePass.AddDepthAttachmentFirstDuringMerge(contextData, contextData.fragmentData[passToMerge.firstFragment]); } // Update versions and flags of existing attachments and // add any new attachments foreach (ref readonly var newAttach in passToMerge.Fragments(contextData)) { bool alreadyAttached = false; for (int i = 0; i < nativePass.fragments.size; ++i) { ref var existingAttach = ref nativePass.fragments[i]; if (!PassFragmentData.SameSubResource(existingAttach, newAttach)) continue; var newAttachAccessFlags = newAttach.accessFlags; // If the existing attachment accessFlag has Discard flag, remove Read flag from newAttach flags, as the content has not to be Loaded if (existingAttach.accessFlags.HasFlag(AccessFlags.Discard)) newAttachAccessFlags &= ~AccessFlags.Read; #if DEVELOPMENT_BUILD || UNITY_EDITOR if (existingAttach.resource.version > newAttach.resource.version) throw new Exception("Adding an older version while a higher version is already registered with the pass."); #endif existingAttach = new PassFragmentData( new ResourceHandle(existingAttach.resource, newAttach.resource.version), existingAttach.accessFlags | newAttachAccessFlags, existingAttach.mipLevel, existingAttach.depthSlice); alreadyAttached = true; break; } if (!alreadyAttached) { nativePass.fragments.Add(newAttach); } } foreach (ref readonly var newAttach in passToMerge.FragmentInputs(contextData)) { bool alreadyAttached = false; for (int i = 0; i < nativePass.fragments.size; ++i) { ref var existingAttach = ref nativePass.fragments[i]; if (!PassFragmentData.SameSubResource(existingAttach, newAttach)) continue; var newAttachAccessFlags = newAttach.accessFlags; // If the existing attachment accessFlag has Discard flag, remove Read flag from newAttach flags, as the content has not to be Loaded if (existingAttach.accessFlags.HasFlag(AccessFlags.Discard)) newAttachAccessFlags &= ~AccessFlags.Read; #if DEVELOPMENT_BUILD || UNITY_EDITOR if (existingAttach.resource.version > newAttach.resource.version) throw new Exception("Adding an older version while a higher version is already registered with the pass."); #endif existingAttach = new PassFragmentData( new ResourceHandle(existingAttach.resource, newAttach.resource.version), existingAttach.accessFlags | newAttachAccessFlags, existingAttach.mipLevel, existingAttach.depthSlice); alreadyAttached = true; break; } if (!alreadyAttached) { nativePass.fragments.Add(newAttach); } } TryMergeNativeSubPass(contextData, ref nativePass, ref passToMerge); SetPassStatesForNativePass(contextData, activeNativePassId); return passBreakAudit; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetPassStatesForNativePass(CompilerContextData contextData, int nativePassId) { ref readonly var nativePass = ref contextData.nativePassData.ElementAt(nativePassId); if (nativePass.numGraphPasses > 1) { contextData.passData.ElementAt(nativePass.firstGraphPass).mergeState = PassMergeState.Begin; var countPasses = nativePass.lastGraphPass - nativePass.firstGraphPass + 1; for (int i = 1; i < countPasses; i++) { var indexPass = nativePass.firstGraphPass + i; // This pass was culled and should not be considere if (contextData.passData.ElementAt(indexPass).culled) { contextData.passData.ElementAt(indexPass).mergeState = PassMergeState.None; continue; } contextData.passData.ElementAt(nativePass.firstGraphPass + i).mergeState = PassMergeState.SubPass; } contextData.passData.ElementAt(nativePass.lastGraphPass).mergeState = PassMergeState.End; } else { contextData.passData.ElementAt(nativePass.firstGraphPass).mergeState = PassMergeState.None; } } } }