You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1531 lines
70 KiB

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<PassOutputData> Outputs(CompilerContextData ctx)
=> ctx.outputData.MakeReadOnlySpan(firstOutput, numOutputs);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ReadOnlySpan<PassInputData> Inputs(CompilerContextData ctx)
=> ctx.inputData.MakeReadOnlySpan(firstInput, numInputs);
// RenderAttachments - MRT colors and depth
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ReadOnlySpan<PassFragmentData> Fragments(CompilerContextData ctx)
=> ctx.fragmentData.MakeReadOnlySpan(firstFragment, numFragments);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ReadOnlySpan<ResourceHandle> 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<PassFragmentData> FragmentInputs(CompilerContextData ctx)
=> ctx.fragmentData.MakeReadOnlySpan(firstFragmentInput, numFragmentInputs);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ReadOnlySpan<ResourceHandle> FirstUsedResources(CompilerContextData ctx)
=> ctx.createData.MakeReadOnlySpan(firstCreate, numCreated);
// Loop over this pass's random write textures returned as PassFragmentData
public ReadOnlySpan<PassRandomWriteData> RandomWriteTextures(CompilerContextData ctx)
=> ctx.randomAccessResourceData.MakeReadOnlySpan(firstRandomAccessResource, numRandomAccessResources);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ReadOnlySpan<ResourceHandle> 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<ResourceHandle> 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<PassFragmentData>.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> loadAudit;
public FixedAttachmentArray<StoreAudit> storeAudit;
public PassBreakAudit breakAudit;
public FixedAttachmentArray<PassFragmentData> fragments;
public FixedAttachmentArray<NativePassAttachment> 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<PassFragmentData>();
attachments = new FixedAttachmentArray<NativePassAttachment>();
width = pass.fragmentInfoWidth;
height = pass.fragmentInfoHeight;
volumeDepth = pass.fragmentInfoVolumeDepth;
samples = pass.fragmentInfoSamples;
hasDepth = pass.fragmentInfoHasDepth;
hasFoveatedRasterization = pass.hasFoveatedRasterization;
extendedFeatureFlags = pass.extendedFeatureFlags;
loadAudit = new FixedAttachmentArray<LoadAudit>();
storeAudit = new FixedAttachmentArray<StoreAudit>();
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<PassData> GraphPasses(CompilerContextData ctx, out NativeArray<PassData> 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<PassData>(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<Name> 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<PassFragmentData>();
// We can't have more than the maximum amount of attachments in a given native renderpass
int currAvailableAttachmentSlots = FixedAttachmentArray<PassFragmentData>.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<int>.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<PassFragmentData> 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;
}
}
}
}