using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Collections; using UnityEngine.Experimental.Rendering; 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(in 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(in 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(in 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(in 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 ExtendedFeatureFlags extendedFeatureFlags; 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 firstSampledOnlyRaster; //base+offset in CompilerContextData.sampledData (use the Sampled iterator to iterate this more easily) public int numSampledOnlyRaster; 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 highest/latest passId executed on the opposite (async compute or gfx) queue to wait on public int awaitingMyGraphicsFencePassId; // -1 if no pass is awaiting the fence generated by this pass, otherwise the lowest/earliest passId executed on the opposite queue waiting for this pass to be completed 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; extendedFeatureFlags = pass.extendedFeatureFlags; 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; firstSampledOnlyRaster = 0; numSampledOnlyRaster = 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; awaitingMyGraphicsFencePassId = -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; extendedFeatureFlags = pass.extendedFeatureFlags; 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; firstSampledOnlyRaster = 0; numSampledOnlyRaster = 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; awaitingMyGraphicsFencePassId = -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); // RenderAttachments - MRT colors and depth [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan Fragments(CompilerContextData ctx) => ctx.fragmentData.MakeReadOnlySpan(firstFragment, numFragments); [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan SampledTexturesIfRaster(CompilerContextData ctx) => ctx.sampledData.MakeReadOnlySpan(firstSampledOnlyRaster, numSampledOnlyRaster); // ShadingRateImageAttachment [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly PassFragmentData ShadingRateImage(CompilerContextData ctx) => ctx.fragmentData[shadingRateImageIndex]; // RenderInputAttachments [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 bool TrySetupAndValidateFragmentInfo(in ResourceHandle h, CompilerContextData ctx, out string errorMessage) { errorMessage = null; #if DEVELOPMENT_BUILD || UNITY_EDITOR if (h.type != RenderGraphResourceType.Texture) { errorMessage = RenderGraph.RenderGraphExceptionMessages.k_NonTextureAsAttachmentError; return false; } #endif ref readonly var resInfo = ref ctx.UnversionedResourceData(h); #if DEVELOPMENT_BUILD || UNITY_EDITOR if (resInfo.width == 0 || resInfo.height == 0 || resInfo.msaaSamples == 0) { errorMessage = RenderGraph.RenderGraphExceptionMessages.k_InvalidGetRenderTargetInfoResultsError; return false; } #endif if (RenderGraph.enableValidityChecks && fragmentInfoValid) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (fragmentInfoWidth != resInfo.width || fragmentInfoHeight != resInfo.height || fragmentInfoVolumeDepth != resInfo.volumeDepth) { var name = resInfo.GetName(ctx, h); if (string.IsNullOrEmpty(name)) name = "unnamed fragment"; errorMessage = RenderGraph.RenderGraphExceptionMessages.MismatchInDimensions(name, fragmentInfoWidth, fragmentInfoHeight, fragmentInfoVolumeDepth, resInfo); return false; } if (fragmentInfoSamples != resInfo.msaaSamples && !extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve)) { var name = resInfo.GetName(ctx, h); if (string.IsNullOrEmpty(name)) name = "unnamed fragment"; errorMessage = RenderGraph.RenderGraphExceptionMessages.MismatchInMSAASamlpes(name, fragmentInfoSamples, resInfo.msaaSamples); return false; } #endif } else { fragmentInfoWidth = resInfo.width; fragmentInfoHeight = resInfo.height; fragmentInfoSamples = resInfo.msaaSamples; fragmentInfoVolumeDepth = resInfo.volumeDepth; fragmentInfoValid = true; } return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void TryAddFragment(in ResourceHandle h, CompilerContextData ctx, out string errorMessage) { if (TrySetupAndValidateFragmentInfo(h, ctx, out errorMessage)) numFragments++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void TryAddFragmentInput(in ResourceHandle h, CompilerContextData ctx, out string errorMessage) { if (TrySetupAndValidateFragmentInfo(h, ctx, out errorMessage)) 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(in 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, RenderGraph.RenderGraphExceptionMessages.k_NonIncrementalCreationCall); numCreated++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AddLastUse(in 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, RenderGraph.RenderGraphExceptionMessages.k_NonIncrementalDestructionCall); 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(in 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; } internal void DisconnectFromResources(CompilerContextData ctx, Stack unusedVersionedResourceIdCullingStack = null, int type = 0) { // If the culled pass was supposed to generate the latest version of a given resource, // we need to decrement the latestVersionNumber of this resource // because its last version will never be created due to its producer being culled foreach (ref readonly var output in Outputs(ctx)) { ref readonly var outputResource = ref output.resource; bool isOutputLastVersion = (outputResource.version == ctx.UnversionedResourceData(outputResource).latestVersionNumber); if (isOutputLastVersion) ctx.UnversionedResourceData(outputResource).latestVersionNumber--; } // Notifying the versioned resources that this pass is no longer reading them foreach (ref readonly var input in Inputs(ctx)) { ref readonly var inputResource = ref input.resource; ref var inputVersionedDataResource = ref ctx.resources[inputResource]; inputVersionedDataResource.RemoveReadingPass(ctx, inputResource, passId); // If a resource of the same type is not used anymore, adding it to the stack if (unusedVersionedResourceIdCullingStack != null && inputResource.iType == type && inputVersionedDataResource.written && inputVersionedDataResource.numReaders == 0) { unusedVersionedResourceIdCullingStack.Push(inputResource); } } } } // 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(in 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) MultisampledShaderResolveMustBeLastPass, // The current pass has MultisampledShaderResolve specified and so must be the last pass ExtendedFeatureFlagsIncompatible, // Handles the case where flags added via SetExtendedFeatureFlags are not compatible 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", "The current merged pass uses multisampled shader resolve and so can't have any more passes merged into it.", "Extended feature flags are incompatible", "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 ExtendedFeatureFlags extendedFeatureFlags; 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; extendedFeatureFlags = pass.extendedFeatureFlags; 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(RenderGraph.RenderGraphExceptionMessages.k_CannotDetermineSubPassFlagNoDepth); } // 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, out NativeArray actualPasses) { // When there's no pass being culled, we can directly return a Span of the Native List if (lastGraphPass - firstGraphPass + 1 == numGraphPasses) { actualPasses = default; return ctx.passData.MakeReadOnlySpan(firstGraphPass, numGraphPasses); } 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) { var span = GraphPasses(ctx, out var actualPasses); foreach (ref readonly var pass in span) { dest.Add(pass.GetName(ctx)); } if (actualPasses.IsCreated) actualPasses.Dispose(); } static bool CanMergeMSAASamples(ref NativePassData nativePass, ref PassData passToMerge) { return (nativePass.samples == passToMerge.fragmentInfoSamples) || (passToMerge.fragmentInfoSamples == 1 && passToMerge.extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve)); } static bool AreExtendedFeatureFlagsCompatible(ExtendedFeatureFlags flags0, ExtendedFeatureFlags flags1) { // Which of the newly added flags are incompatible? return true; } // 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 || !CanMergeMSAASamples(ref nativePass, ref passToMerge)) { 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); } if (!AreExtendedFeatureFlagsCompatible(nativePass.extendedFeatureFlags, passToMerge.extendedFeatureFlags)) { return new PassBreakAudit(PassBreakReason.ExtendedFeatureFlagsIncompatible, 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); } } // If we have MSAA shader resolve set we must be the last subpass, so we can't merge anything else. if (nativePass.extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve)) { return new PassBreakAudit(PassBreakReason.MultisampledShaderResolveMustBeLastPass, passIdToMerge); } } // Check the non-fragment textures 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 sampledTexture in passToMerge.SampledTexturesIfRaster(contextData)) { ref readonly var sampledDataVersioned = ref contextData.VersionedResourceData(sampledTexture); // If the writing pass is culled, we don't need to break the native pass merge // because the texture won't actually be written to, so there's no read-after-write conflict bool isWritingPassCulled = contextData.passData[sampledDataVersioned.writePassId].culled; if (!isWritingPassCulled) { bool isWrittenInCurrNativePass = sampledDataVersioned.written && (sampledDataVersioned.writePassId >= nativePass.firstGraphPass && sampledDataVersioned.writePassId < nativePass.lastGraphPass + 1); if (isWrittenInCurrNativePass) { // It's used as some sort of texture read or load so we need to break the current native render pass // as we can't sample and write to it in the same native render pass 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; // Early exit: only build the HashSet and check if we actually have fragments to check against it if (passToMerge.numFragments > 0) { // Temporary cache of sampled textures in current Native Render Pass for conflict detection against fragments using (HashSetPool.Get(out var tempSampledTextures)) { var graphPasses = nativePass.GraphPasses(contextData, out var actualPasses); foreach (ref readonly var graphPass in graphPasses) { if (graphPass.numSampledOnlyRaster > 0) // Skip passes with no sampled textures { foreach (ref readonly var earlierInput in graphPass.SampledTexturesIfRaster(contextData)) { tempSampledTextures.Add(earlierInput.index); } } } if (actualPasses.IsCreated) actualPasses.Dispose(); 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 as a standard texture // Before looking in the HashSet check if there is any sampled texture if (tempSampledTextures.Contains(fragment.resource.index)) return new PassBreakAudit(PassBreakReason.NextPassTargetsTexture, passIdToMerge); } } // Close the using block for HashSetPool } 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--; } } } // Determines if the pixel storage limit is reached after adding the new 'pass to merge' attachments to the current native render pass. if (TotalAttachmentsSizeExceedPixelStorageLimit(contextData, ref nativePass, ref attachmentsToTryAdding)) { return new PassBreakAudit(PassBreakReason.AttachmentLimitReached, passIdToMerge); } // 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); } static bool TotalAttachmentsSizeExceedPixelStorageLimit(CompilerContextData contextData, ref NativePassData nativePass, ref FixedAttachmentArray attachmentsToTryAdding) { // TODO: We are currently only checking for iOS GPU Family 1 to 3 since the storage size is much more restricted (16 bytes for Family 1 and 32 for Family 2 & 3). // This is temporary. Later on, we should check all iOS GPU Families but also Android (Vulkan) to avoid the same potential restrictions. if (Application.platform == RuntimePlatform.IPhonePlayer && SystemInfo.maxTiledPixelStorageSize <= 32) { int totalSize = 0; // Iterate over current attachments for (int i = 0; i < nativePass.fragments.size; ++i) { ref readonly var unvResource = ref contextData.UnversionedResourceData(nativePass.fragments[i].resource); totalSize += SystemInfo.GetTiledRenderTargetStorageSize(unvResource.graphicsFormat, unvResource.msaaSamples); } // Iterate over new attachments to add for (int i = 0; i < attachmentsToTryAdding.size; ++i) { ref readonly var unvResource = ref contextData.UnversionedResourceData(attachmentsToTryAdding[i].resource); totalSize += SystemInfo.GetTiledRenderTargetStorageSize(unvResource.graphicsFormat, unvResource.msaaSamples); } return totalSize > SystemInfo.maxTiledPixelStorageSize; } return false; } // 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 SRI index (used for VRS) // This is unlikely to happen because the SRI is always added last, after color and input attachments 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; // Shader resolve needs special handling as we can merge it into a native pass that doesn't have it, it should be the last pass // but non fragment passes might get merged in after it and as those don't actually generate subpasses that should be fine. if (passToMerge.extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve)) nativePass.extendedFeatureFlags |= ExtendedFeatureFlags.MultisampledShaderResolve; // 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(RenderGraph.RenderGraphExceptionMessages.k_AddingOlderAttachmentVersion); #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(RenderGraph.RenderGraphExceptionMessages.k_AddingOlderAttachmentVersion); #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 considered 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; } } } }