A game about forced loneliness, made by TACStudios
1using System; 2using System.Diagnostics; 3using System.Runtime.CompilerServices; 4using Unity.Collections.LowLevel.Unsafe; 5using UnityEngine.Rendering; 6using System.Collections.Generic; 7 8namespace UnityEngine.Rendering.RenderGraphModule.NativeRenderPassCompiler 9{ 10 // Per pass info on inputs to the pass 11 [DebuggerDisplay("PassInputData: Res({resource.index})")] 12 internal struct PassInputData 13 { 14 public ResourceHandle resource; 15 } 16 17 // Per pass info on outputs to the pass 18 [DebuggerDisplay("PassOutputData: Res({resource.index})")] 19 internal struct PassOutputData 20 { 21 public ResourceHandle resource; 22 } 23 24 // Per pass fragment (attachment) info 25 [DebuggerDisplay("PassFragmentData: Res({resource.index}):{accessFlags}")] 26 internal struct PassFragmentData 27 { 28 public ResourceHandle resource; 29 public AccessFlags accessFlags; 30 public int mipLevel; 31 public int depthSlice; 32 33 [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 public override int GetHashCode() 35 { 36 var hash = resource.GetHashCode(); 37 hash = hash * 23 + accessFlags.GetHashCode(); 38 hash = hash * 23 + mipLevel.GetHashCode(); 39 hash = hash * 23 + depthSlice.GetHashCode(); 40 return hash; 41 } 42 43 public static bool EqualForMerge(PassFragmentData x, PassFragmentData y) 44 { 45 // We ignore the version for now we assume if one pass writes version x and the next y they can 46 // be merged in the same native render pass 47 return x.resource.index == y.resource.index && x.accessFlags == y.accessFlags && x.mipLevel == y.mipLevel && x.depthSlice == y.depthSlice; 48 } 49 } 50 51 // Per pass random write texture info 52 [DebuggerDisplay("PassRandomWriteData: Res({resource.index}):{index}:{preserveCounterValue}")] 53 internal struct PassRandomWriteData 54 { 55 public ResourceHandle resource; 56 public int index; 57 public bool preserveCounterValue; 58 59 public override int GetHashCode() 60 { 61 var hash = resource.GetHashCode(); 62 hash = hash * 23 + index.GetHashCode(); 63 return hash; 64 } 65 } 66 67 internal enum PassMergeState 68 { 69 None = -1, // this pass is "standalone", it will begin and end the NRP 70 Begin = 0, // Only Begin NRP is done by this pass, sequential passes will End the NRP (after 0 or more additional subpasses) 71 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 72 End = 2 // This pass is both a subpass and will end the current NRP 73 } 74 75 // Data per pass 76 internal struct PassData 77 { 78 // Warning, any field must initialized in both constructor and ResetAndInitialize function 79 80 public int passId; // Index of self in the passData list, can we calculate this somehow in c#? would use offsetof in c++ 81 public RenderGraphPassType type; 82 public bool hasFoveatedRasterization; 83 public int tag; // Arbitrary per node int used by various graph analysis tools 84 85 public PassMergeState mergeState; 86 public int nativePassIndex; // Index of the native pass this pass belongs to 87 public int nativeSubPassIndex; // Index of the native subpass this pass belongs to 88 89 public int firstInput; //base+offset in CompilerContextData.inputData (use the InputNodes iterator to iterate this more easily) 90 public int numInputs; 91 public int firstOutput; //base+offset in CompilerContextData.outputData (use the OutputNodes iterator to iterate this more easily) 92 public int numOutputs; 93 public int firstFragment; //base+offset in CompilerContextData.fragmentData (use the Fragments iterator to iterate this more easily) 94 public int numFragments; 95 public int firstFragmentInput; //base+offset in CompilerContextData.fragmentData (use the Fragment inputs iterator to iterate this more easily) 96 public int numFragmentInputs; 97 public int firstRandomAccessResource; //base+offset in CompilerContextData.randomWriteData (use the Fragment inputs iterator to iterate this more easily) 98 public int numRandomAccessResources; 99 public int firstCreate; //base+offset in CompilerContextData.createData (use the InputNodes iterator to iterate this more easily) 100 public int numCreated; 101 public int firstDestroy; //base+offset in CompilerContextData.destroyData (use the InputNodes iterator to iterate this more easily) 102 public int numDestroyed; 103 104 public int fragmentInfoWidth; 105 public int fragmentInfoHeight; 106 public int fragmentInfoVolumeDepth; 107 public int fragmentInfoSamples; 108 109 public int waitOnGraphicsFencePassId; // -1 if no fence wait is needed, otherwise the passId to wait on 110 111 public bool asyncCompute; 112 public bool hasSideEffects; 113 public bool culled; 114 public bool beginNativeSubpass; // If true this is the first graph pass of a merged native subpass 115 public bool fragmentInfoValid; 116 public bool fragmentInfoHasDepth; 117 public bool insertGraphicsFence; // Whether this pass should insert a fence into the command buffer 118 119 [MethodImpl(MethodImplOptions.AggressiveInlining)] 120 public Name GetName(CompilerContextData ctx) => ctx.GetFullPassName(passId); 121 122 public PassData(in RenderGraphPass pass, int passIndex) 123 { 124 passId = passIndex; 125 type = pass.type; 126 asyncCompute = pass.enableAsyncCompute; 127 hasSideEffects = !pass.allowPassCulling; 128 hasFoveatedRasterization = pass.enableFoveatedRasterization; 129 mergeState = PassMergeState.None; 130 nativePassIndex = -1; 131 nativeSubPassIndex = -1; 132 beginNativeSubpass = false; 133 134 culled = false; 135 tag = 0; 136 137 firstInput = 0; 138 numInputs = 0; 139 firstOutput = 0; 140 numOutputs = 0; 141 firstFragment = 0; 142 numFragments = 0; 143 firstRandomAccessResource = 0; 144 numRandomAccessResources = 0; 145 firstFragmentInput = 0; 146 numFragmentInputs = 0; 147 firstCreate = 0; 148 numCreated = 0; 149 firstDestroy = 0; 150 numDestroyed = 0; 151 152 fragmentInfoValid = false; 153 fragmentInfoWidth = 0; 154 fragmentInfoHeight = 0; 155 fragmentInfoVolumeDepth = 0; 156 fragmentInfoSamples = 0; 157 fragmentInfoHasDepth = false; 158 159 insertGraphicsFence = false; 160 waitOnGraphicsFencePassId = -1; 161 } 162 163 // Helper func to reset and initialize existing PassData struct directly in a data container without costly deep copy (~120bytes) when adding it 164 public void ResetAndInitialize(in RenderGraphPass pass, int passIndex) 165 { 166 passId = passIndex; 167 type = pass.type; 168 asyncCompute = pass.enableAsyncCompute; 169 hasSideEffects = !pass.allowPassCulling; 170 hasFoveatedRasterization = pass.enableFoveatedRasterization; 171 172 mergeState = PassMergeState.None; 173 nativePassIndex = -1; 174 nativeSubPassIndex = -1; 175 beginNativeSubpass = false; 176 177 culled = false; 178 tag = 0; 179 180 firstInput = 0; 181 numInputs = 0; 182 firstOutput = 0; 183 numOutputs = 0; 184 firstFragment = 0; 185 numFragments = 0; 186 firstFragmentInput = 0; 187 numFragmentInputs = 0; 188 firstRandomAccessResource = 0; 189 numRandomAccessResources = 0; 190 firstCreate = 0; 191 numCreated = 0; 192 firstDestroy = 0; 193 numDestroyed = 0; 194 195 fragmentInfoValid = false; 196 fragmentInfoWidth = 0; 197 fragmentInfoHeight = 0; 198 fragmentInfoVolumeDepth = 0; 199 fragmentInfoSamples = 0; 200 fragmentInfoHasDepth = false; 201 202 insertGraphicsFence = false; 203 waitOnGraphicsFencePassId = -1; 204 } 205 206 [MethodImpl(MethodImplOptions.AggressiveInlining)] 207 public readonly ReadOnlySpan<PassOutputData> Outputs(CompilerContextData ctx) 208 => ctx.outputData.MakeReadOnlySpan(firstOutput, numOutputs); 209 210 [MethodImpl(MethodImplOptions.AggressiveInlining)] 211 public readonly ReadOnlySpan<PassInputData> Inputs(CompilerContextData ctx) 212 => ctx.inputData.MakeReadOnlySpan(firstInput, numInputs); 213 214 [MethodImpl(MethodImplOptions.AggressiveInlining)] 215 public readonly ReadOnlySpan<PassFragmentData> Fragments(CompilerContextData ctx) 216 => ctx.fragmentData.MakeReadOnlySpan(firstFragment, numFragments); 217 218 [MethodImpl(MethodImplOptions.AggressiveInlining)] 219 public readonly ReadOnlySpan<PassFragmentData> FragmentInputs(CompilerContextData ctx) 220 => ctx.fragmentData.MakeReadOnlySpan(firstFragmentInput, numFragmentInputs); 221 222 [MethodImpl(MethodImplOptions.AggressiveInlining)] 223 public readonly ReadOnlySpan<ResourceHandle> FirstUsedResources(CompilerContextData ctx) 224 => ctx.createData.MakeReadOnlySpan(firstCreate, numCreated); 225 226 // Loop over this pass's random write textures returned as PassFragmentData 227 public ReadOnlySpan<PassRandomWriteData> RandomWriteTextures(CompilerContextData ctx) 228 => ctx.randomAccessResourceData.MakeReadOnlySpan(firstRandomAccessResource, numRandomAccessResources); 229 230 [MethodImpl(MethodImplOptions.AggressiveInlining)] 231 public readonly ReadOnlySpan<ResourceHandle> LastUsedResources(CompilerContextData ctx) 232 => ctx.destroyData.MakeReadOnlySpan(firstDestroy, numDestroyed); 233 234 private void SetupAndValidateFragmentInfo(ResourceHandle h, CompilerContextData ctx) 235 { 236#if DEVELOPMENT_BUILD || UNITY_EDITOR 237 if (h.type != RenderGraphResourceType.Texture) new Exception("Only textures can be used as a fragment attachment."); 238#endif 239 240 ref readonly var resInfo = ref ctx.UnversionedResourceData(h); 241 242#if DEVELOPMENT_BUILD || UNITY_EDITOR 243 if (resInfo.width == 0 || resInfo.height == 0 || resInfo.msaaSamples == 0) throw new Exception("GetRenderTargetInfo returned invalid results."); 244#endif 245 if (fragmentInfoValid) 246 { 247#if DEVELOPMENT_BUILD || UNITY_EDITOR 248 if (fragmentInfoWidth != resInfo.width || 249 fragmentInfoHeight != resInfo.height || 250 fragmentInfoVolumeDepth != resInfo.volumeDepth || 251 fragmentInfoSamples != resInfo.msaaSamples) 252 throw new Exception("Mismatch in Fragment dimensions"); 253#endif 254 } 255 else 256 { 257 fragmentInfoWidth = resInfo.width; 258 fragmentInfoHeight = resInfo.height; 259 fragmentInfoSamples = resInfo.msaaSamples; 260 fragmentInfoVolumeDepth = resInfo.volumeDepth; 261 fragmentInfoValid = true; 262 } 263 } 264 265 [MethodImpl(MethodImplOptions.AggressiveInlining)] 266 internal void AddFragment(ResourceHandle h, CompilerContextData ctx) 267 { 268 SetupAndValidateFragmentInfo(h, ctx); 269 numFragments++; 270 } 271 272 [MethodImpl(MethodImplOptions.AggressiveInlining)] 273 internal void AddFragmentInput(ResourceHandle h, CompilerContextData ctx) 274 { 275 SetupAndValidateFragmentInfo(h, ctx); 276 numFragmentInputs++; 277 } 278 279 internal void AddRandomAccessResource() 280 { 281 // This function is here for orthogonality with AddFragment/AddFragmentInput 282 // Random write textures can be arbitrary sizes and do not need to validate or set-up the fragment info 283 numRandomAccessResources++; 284 } 285 286 [MethodImpl(MethodImplOptions.AggressiveInlining)] 287 internal void AddFirstUse(ResourceHandle h, CompilerContextData ctx) 288 { 289 // Already registered? Skip it 290 foreach (ref readonly var res in FirstUsedResources(ctx)) 291 { 292 if (res.index == h.index && res.type == h.type) 293 return; 294 } 295 296 ctx.createData.Add(h); 297 int addedIndex = ctx.createData.LastIndex(); 298 299 // First item added, set up firstCreate 300 if (numCreated == 0) 301 { 302 firstCreate = addedIndex; 303 } 304 305 Debug.Assert(addedIndex == firstCreate + numCreated, "you can only incrementally set-up the Creation lists for all passes, AddCreation is called in an arbitrary non-incremental way"); 306 307 numCreated++; 308 } 309 310 [MethodImpl(MethodImplOptions.AggressiveInlining)] 311 internal void AddLastUse(ResourceHandle h, CompilerContextData ctx) 312 { 313 // Already registered? Skip it 314 foreach (ref readonly var res in LastUsedResources(ctx)) 315 { 316 if (res.index == h.index && res.type == h.type) 317 return; 318 } 319 320 ctx.destroyData.Add(h); 321 int addedIndex = ctx.destroyData.LastIndex(); 322 323 // First item added, set up firstDestroy 324 if (numDestroyed == 0) 325 { 326 firstDestroy = addedIndex; 327 } 328 329 Debug.Assert(addedIndex == firstDestroy + numDestroyed, "you can only incrementally set-up the Destruction lists for all passes, AddCreation is called in an arbitrary non-incremental way"); 330 numDestroyed++; 331 } 332 333 // Is the resource used as a fragment this pass. 334 // As it is ambiguous if this is an input our output version, the version is ignored 335 // This checks use of both MRT attachment as well as input attachment 336 [MethodImpl(MethodImplOptions.AggressiveInlining)] 337 internal readonly bool IsUsedAsFragment(ResourceHandle h, CompilerContextData ctx) 338 { 339 //Only textures can be used as a fragment attachment. 340 if (h.type != RenderGraphResourceType.Texture) return false; 341 342 // Only raster passes can have fragment attachments 343 if (type != RenderGraphPassType.Raster) return false; 344 345 foreach (ref readonly var fragment in Fragments(ctx)) 346 { 347 if (fragment.resource.index == h.index) 348 { 349 return true; 350 } 351 } 352 353 foreach (ref readonly var fragmentInput in FragmentInputs(ctx)) 354 { 355 if (fragmentInput.resource.index == h.index) 356 { 357 return true; 358 } 359 } 360 361 return false; 362 } 363 } 364 365 // Data per attachment of a native renderpass 366 [DebuggerDisplay("Res({handle.index}) : {loadAction} : {storeAction} : {memoryless}")] 367 internal struct NativePassAttachment 368 { 369 public ResourceHandle handle; 370 public UnityEngine.Rendering.RenderBufferLoadAction loadAction; 371 public UnityEngine.Rendering.RenderBufferStoreAction storeAction; 372 public bool memoryless; 373 public int mipLevel; 374 public int depthSlice; 375 } 376 377 internal enum LoadReason 378 { 379 InvalidReason, 380 LoadImported, 381 LoadPreviouslyWritten, 382 ClearImported, 383 ClearCreated, 384 FullyRewritten, 385 386 Count 387 } 388 389 [DebuggerDisplay("{reason} : {passId}")] 390 internal struct LoadAudit 391 { 392 public static readonly string[] LoadReasonMessages = { 393 "Invalid reason", 394 "The resource is imported in the graph and loaded to retrieve the existing buffer contents.", 395 "The resource is written by {pass} executed previously in the graph. The data is loaded.", 396 "The resource is imported in the graph but was imported with the 'clear on first use' option enabled. The data is cleared.", 397 "The resource is created in this pass and cleared on first use.", 398 "The pass indicated it will rewrite the full resource contents. Existing contents are not loaded or cleared.", 399 }; 400 401 public LoadReason reason; 402 public int passId; 403 404 public LoadAudit(LoadReason setReason, int setPassId = -1) 405 { 406#if UNITY_EDITOR 407 Debug.Assert(LoadReasonMessages.Length == (int)LoadReason.Count, 408 $"Make sure {nameof(LoadReasonMessages)} is in sync with {nameof(LoadReason)}"); 409#endif 410 411 reason = setReason; 412 passId = setPassId; 413 } 414 } 415 416 internal enum StoreReason 417 { 418 InvalidReason, 419 StoreImported, 420 StoreUsedByLaterPass, 421 DiscardImported, 422 DiscardUnused, 423 DiscardBindMs, 424 NoMSAABuffer, 425 426 Count 427 } 428 429 [DebuggerDisplay("{reason} : {passId} / MSAA {msaaReason} : {msaaPassId}")] 430 internal struct StoreAudit 431 { 432 public static readonly string[] StoreReasonMessages = { 433 "Invalid reason", 434 "The resource is imported in the graph. The data is stored so results are available outside the graph.", 435 "The resource is read by pass {pass} executed later in the graph. The data is stored.", 436 "The resource is imported but the import was with the 'discard on last use' option enabled. The data is discarded.", 437 "The resource is written by this pass but no later passes are using the results. The data is discarded.", 438 "The resource was created as MSAA only resource, the data can never be resolved.", 439 "The resource is a single sample resource, there is no multi-sample data to handle.", 440 }; 441 442 public StoreReason reason; 443 public int passId; 444 public StoreReason msaaReason; 445 public int msaaPassId; 446 447 public StoreAudit(StoreReason setReason, int setPassId = -1, StoreReason setMsaaReason = StoreReason.NoMSAABuffer, int setMsaaPassId = -1) 448 { 449#if UNITY_EDITOR 450 Debug.Assert(StoreReasonMessages.Length == (int)StoreReason.Count, 451 $"Make sure {nameof(StoreReasonMessages)} is in sync with {nameof(StoreReason)}"); 452#endif 453 454 reason = setReason; 455 passId = setPassId; 456 msaaReason = setMsaaReason; 457 msaaPassId = setMsaaPassId; 458 } 459 } 460 461 internal enum PassBreakReason 462 { 463 NotOptimized, // Optimize never ran on this pass 464 TargetSizeMismatch, // Target Sizes or msaa samples don't match 465 NextPassReadsTexture, // The next pass reads data written by this pass as a texture 466 NonRasterPass, // The next pass is a non-raster pass 467 DifferentDepthTextures, // The next pass uses a different depth texture (and we only allow one in a whole NRP) 468 AttachmentLimitReached, // Adding the next pass would have used more attachments than allowed 469 SubPassLimitReached, // Addind the next pass would have generated more subpasses than allowed 470 EndOfGraph, // The last pass in the graph was reached 471 FRStateMismatch, // One pass is using foveated rendering and the other not 472 Merged, // I actually got merged 473 474 Count 475 } 476 477 [DebuggerDisplay("{reason} : {breakPass}")] 478 internal struct PassBreakAudit 479 { 480 public PassBreakReason reason; 481 public int breakPass; 482 483 public PassBreakAudit(PassBreakReason reason, int breakPass) 484 { 485#if UNITY_EDITOR 486 Debug.Assert(BreakReasonMessages.Length == (int)PassBreakReason.Count, 487 $"Make sure {nameof(BreakReasonMessages)} is in sync with {nameof(PassBreakReason)}"); 488#endif 489 490 this.reason = reason; 491 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 492 } 493 494 public static readonly string[] BreakReasonMessages = { 495 "The native render pass optimizer never ran on this pass. Pass is standalone and not merged.", 496 "The render target sizes of the next pass do not match.", 497 "The next pass reads data output by this pass as a regular texture.", 498 "The next pass is not a raster render pass.", 499 "The next pass uses a different depth buffer. All passes in the native render pass need to use the same depth buffer.", 500 $"The limit of {FixedAttachmentArray<PassFragmentData>.MaxAttachments} native pass attachments would be exceeded when merging with the next pass.", 501 $"The limit of {NativePassCompiler.k_MaxSubpass} native subpasses would be exceeded when merging with the next pass.", 502 "This is the last pass in the graph, there are no other passes to merge.", 503 "The the next pass uses a different foveated rendering state", 504 "The next pass got merged into this pass.", 505 }; 506 } 507 508 // Data per native renderpass 509 internal struct NativePassData 510 { 511 public FixedAttachmentArray<LoadAudit> loadAudit; 512 public FixedAttachmentArray<StoreAudit> storeAudit; 513 public PassBreakAudit breakAudit; 514 515 public FixedAttachmentArray<PassFragmentData> fragments; 516 public FixedAttachmentArray<NativePassAttachment> attachments; 517 518 // Index of the first graph pass this native pass encapsulates 519 public int firstGraphPass; // Offset+count in context pass array 520 public int lastGraphPass; 521 public int numGraphPasses; 522 523 public int firstNativeSubPass; // Offset+count in context subpass array 524 public int numNativeSubPasses; 525 public int width; 526 public int height; 527 public int volumeDepth; 528 public int samples; 529 public bool hasDepth; 530 public bool hasFoveatedRasterization; 531 532 public NativePassData(ref PassData pass, CompilerContextData ctx) 533 { 534 firstGraphPass = pass.passId; 535 lastGraphPass = pass.passId; 536 numGraphPasses = 1; 537 firstNativeSubPass = -1;// Set up during compile 538 numNativeSubPasses = 0; 539 540 fragments = new FixedAttachmentArray<PassFragmentData>(); 541 attachments = new FixedAttachmentArray<NativePassAttachment>(); 542 543 width = pass.fragmentInfoWidth; 544 height = pass.fragmentInfoHeight; 545 volumeDepth = pass.fragmentInfoVolumeDepth; 546 samples = pass.fragmentInfoSamples; 547 hasDepth = pass.fragmentInfoHasDepth; 548 hasFoveatedRasterization = pass.hasFoveatedRasterization; 549 550 loadAudit = new FixedAttachmentArray<LoadAudit>(); 551 storeAudit = new FixedAttachmentArray<StoreAudit>(); 552 breakAudit = new PassBreakAudit(PassBreakReason.NotOptimized, -1); 553 554 foreach (ref readonly var fragment in pass.Fragments(ctx)) 555 { 556 fragments.Add(fragment); 557 } 558 559 foreach (ref readonly var fragment in pass.FragmentInputs(ctx)) 560 { 561 fragments.Add(fragment); 562 } 563 564 // Graph pass is added as the first native subpass 565 TryMergeNativeSubPass(ctx, ref this, ref pass); 566 } 567 568 public void Clear() 569 { 570 firstGraphPass = 0; 571 numGraphPasses = 0; 572 attachments.Clear(); 573 fragments.Clear(); 574 loadAudit.Clear(); 575 storeAudit.Clear(); 576 } 577 578 [MethodImpl(MethodImplOptions.AggressiveInlining)] 579 public readonly bool IsValid() 580 { 581 return numGraphPasses > 0; 582 } 583 584 [MethodImpl(MethodImplOptions.AggressiveInlining)] 585 public readonly ReadOnlySpan<PassData> GraphPasses(CompilerContextData ctx) 586 { 587 // When there's no pass being culled, we can directly return a Span of the Native List 588 if (lastGraphPass - firstGraphPass + 1 == numGraphPasses) 589 { 590 return ctx.passData.MakeReadOnlySpan(firstGraphPass, numGraphPasses); 591 } 592 593 var actualPasses = new PassData[numGraphPasses]; 594 595 for (int i = firstGraphPass, index = 0; i < lastGraphPass + 1; ++i) 596 { 597 var pass = ctx.passData[i]; 598 if (!pass.culled) 599 { 600 actualPasses[index++] = pass; 601 } 602 } 603 604 return actualPasses; 605 } 606 607 [MethodImpl(MethodImplOptions.AggressiveInlining)] 608 public readonly void GetGraphPassNames(CompilerContextData ctx, DynamicArray<Name> dest) 609 { 610 foreach (ref readonly var pass in GraphPasses(ctx)) 611 { 612 dest.Add(pass.GetName(ctx)); 613 } 614 } 615 616 // This function does not modify the current render graph state, it only evaluates and returns the correct PassBreakAudit 617 public static PassBreakAudit CanMerge(CompilerContextData contextData, int activeNativePassId, int passIdToMerge) 618 { 619 ref readonly var passToMerge = ref contextData.passData.ElementAt(passIdToMerge); 620 621 // Non raster passes (low level, compute,...) will break the native pass chain 622 // as they may need to do SetRendertarget or non-fragment work 623 if (passToMerge.type != RenderGraphPassType.Raster) 624 { 625 return new PassBreakAudit(PassBreakReason.NonRasterPass, passIdToMerge); 626 } 627 628 ref readonly var nativePass = ref contextData.nativePassData.ElementAt(activeNativePassId); 629 630 // If a pass has no fragment attachments a lot of the tests can be skipped 631 // You could argue that a raster pass with no fragments is not allowed but why not? 632 // it allow us to set some legacy unity state from fragment passes without breaking NRP 633 // Allowing this would means something like 634 // Fill Gbuffer - Set Shadow Globals - Do light pass would break as the shadow globals pass 635 // is a 0x0 pass with no rendertargets that just sets shadow global texture pointers 636 // By allowing this to be merged into the NRP we can actually ensure these passes are merged 637 bool hasFragments = (passToMerge.numFragments > 0 || passToMerge.numFragmentInputs > 0); 638 if (hasFragments) 639 { 640 // Easy early outs, sizes mismatch 641 if (nativePass.width != passToMerge.fragmentInfoWidth || 642 nativePass.height != passToMerge.fragmentInfoHeight || 643 nativePass.volumeDepth != passToMerge.fragmentInfoVolumeDepth || 644 nativePass.samples != passToMerge.fragmentInfoSamples) 645 { 646 return new PassBreakAudit(PassBreakReason.TargetSizeMismatch, passIdToMerge); 647 } 648 649 // Easy early outs, different depth buffers we only allow a single depth for the whole NRP for now ?!? 650 // Depth buffer is by-design always at index 0 651 if (nativePass.hasDepth && passToMerge.fragmentInfoHasDepth) 652 { 653 ref readonly var firstFragment = ref contextData.fragmentData.ElementAt(passToMerge.firstFragment); 654 if (nativePass.fragments[0].resource.index != firstFragment.resource.index) 655 { 656 return new PassBreakAudit(PassBreakReason.DifferentDepthTextures, passIdToMerge); 657 } 658 } 659 660 // We do not support foveation state changes within the renderpass due to platform limitation 661 if (nativePass.hasFoveatedRasterization != passToMerge.hasFoveatedRasterization) 662 { 663 return new PassBreakAudit(PassBreakReason.FRStateMismatch, passIdToMerge); 664 } 665 } 666 667 // Check the non-fragment inputs of this pass, if they are generated by the current open native pass we can't merge 668 // as we need to commit the pixels to the texture 669 foreach (ref readonly var input in passToMerge.Inputs(contextData)) 670 { 671 var inputResource = input.resource; 672 var writingPassId = contextData.resources[inputResource].writePassId; 673 // Is the writing pass enclosed in the current native renderpass 674 if (writingPassId >= nativePass.firstGraphPass && writingPassId < nativePass.lastGraphPass + 1) 675 { 676 // If it's not used as a fragment, it's used as some sort of texture read of load so we need so sync it out 677 if (!passToMerge.IsUsedAsFragment(inputResource, contextData)) 678 { 679 return new PassBreakAudit(PassBreakReason.NextPassReadsTexture, passIdToMerge); 680 } 681 } 682 } 683 684 // Gather which attachments to add to the current renderpass 685 var attachmentsToTryAdding = new FixedAttachmentArray<PassFragmentData>(); 686 687 // We can't have more than the maximum amount of attachments in a given native renderpass 688 int currAvailableAttachmentSlots = FixedAttachmentArray<PassFragmentData>.MaxAttachments - nativePass.fragments.size; 689 690 foreach (ref readonly var fragment in passToMerge.Fragments(contextData)) 691 { 692 bool alreadyAttached = false; 693 694 for (int i = 0; i < nativePass.fragments.size; ++i) 695 { 696 if (nativePass.fragments[i].resource.index == fragment.resource.index) 697 { 698 alreadyAttached = true; 699 break; 700 } 701 } 702 703 // This fragment is not attached to the native renderpass yet, we will need to attach it 704 if (!alreadyAttached) 705 { 706 // We already reached the maximum amount of attachments in this renderpass 707 // We can't add any new attachment, just start a new renderpass 708 if (currAvailableAttachmentSlots == 0) 709 { 710 return new PassBreakAudit(PassBreakReason.AttachmentLimitReached, passIdToMerge); 711 } 712 else 713 { 714 attachmentsToTryAdding.Add(fragment); 715 currAvailableAttachmentSlots--; 716 } 717 } 718 } 719 720 foreach (ref readonly var fragmentInput in passToMerge.FragmentInputs(contextData)) 721 { 722 bool alreadyAttached = false; 723 724 for (int i = 0; i < nativePass.fragments.size; ++i) 725 { 726 if (nativePass.fragments[i].resource.index == fragmentInput.resource.index) 727 { 728 alreadyAttached = true; 729 break; 730 } 731 } 732 733 // This fragment input is not attached to the native renderpass yet, we will need to attach it 734 if (!alreadyAttached) 735 { 736 // We already reached the maximum amount of attachments in this native renderpass 737 // We can't add any new attachment, just start a new renderpass 738 if (currAvailableAttachmentSlots == 0) 739 { 740 return new PassBreakAudit(PassBreakReason.AttachmentLimitReached, passIdToMerge); 741 } 742 else 743 { 744 attachmentsToTryAdding.Add(fragmentInput); 745 currAvailableAttachmentSlots--; 746 } 747 } 748 } 749 750 bool canMergeNativeSubPass = CanMergeNativeSubPass(contextData, nativePass, passToMerge); 751 if (!canMergeNativeSubPass && nativePass.numGraphPasses + 1 > NativePassCompiler.k_MaxSubpass) 752 { 753 return new PassBreakAudit(PassBreakReason.SubPassLimitReached, passIdToMerge); 754 } 755 756 // All is good! Pass can be merged into active native pass 757 return new PassBreakAudit(PassBreakReason.Merged, passIdToMerge); 758 } 759 760 // This function follows the structure of TryMergeNativeSubPass but only tests if the new native subpass can be 761 // merged with the last one, allowing for early returns. It does not modify the state 762 static bool CanMergeNativeSubPass(CompilerContextData contextData, NativePassData nativePass, PassData passToMerge) 763 { 764 // We have no output attachments, this is an "empty" raster pass doing only non-rendering command so skip it. 765 if (passToMerge.numFragments == 0 && passToMerge.numFragmentInputs == 0) 766 { 767 return true; 768 } 769 770 if (nativePass.numNativeSubPasses == 0) 771 { 772 return false; // nothing to merge with 773 } 774 775 ref readonly var fragmentList = ref nativePass.fragments; 776 ref readonly var lastPass = ref contextData.nativeSubPassData.ElementAt(nativePass.firstNativeSubPass + 777 nativePass.numNativeSubPasses - 1); 778 779 // NOTE: Not all graph subpasses get an actual native pass: 780 // - There could be passes that do only non-raster ops (like setglobal) and have no attachments. They don't get a native pass 781 // - Renderpasses that use exactly the same rendertargets at the previous pass use the same native pass. This is because 782 // nextSubpass is expensive on some platforms (even if its' essentially a no-op as it's using the same attachments). 783 SubPassFlags flags = SubPassFlags.None; 784 785 // If depth ends up being bound only because of merging we explicitly say that we will not write to it 786 // which could have been implied by leaving the flag to None 787 if (!passToMerge.fragmentInfoHasDepth && nativePass.hasDepth) 788 { 789 flags = SubPassFlags.ReadOnlyDepth; 790 } 791 792 // MRT attachments 793 { 794 int fragmentIdx = 0; 795 int colorOffset = (passToMerge.fragmentInfoHasDepth) ? -1 : 0; 796 int colorOutputsLength = passToMerge.numFragments + colorOffset; 797 798 if (colorOutputsLength != lastPass.colorOutputs.Length) 799 { 800 return false; 801 } 802 803 foreach (ref readonly var graphPassFragment in passToMerge.Fragments(contextData)) 804 { 805 // Check if we're handling the depth attachment 806 if (passToMerge.fragmentInfoHasDepth && fragmentIdx == 0) 807 { 808 flags = (graphPassFragment.accessFlags.HasFlag(AccessFlags.Write)) 809 ? SubPassFlags.None 810 : SubPassFlags.ReadOnlyDepth; 811 } 812 // It's a color attachment 813 else 814 { 815 // Find the index of this subpass's attachment in the native renderpass attachment list 816 int colorAttachmentIdx = -1; 817 for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) 818 { 819 if (fragmentList[fragmentId].resource.index == graphPassFragment.resource.index) 820 { 821 colorAttachmentIdx = fragmentId; 822 break; 823 } 824 } 825 826 if (colorAttachmentIdx < 0 || colorAttachmentIdx != lastPass.colorOutputs[fragmentIdx + colorOffset]) 827 { 828 return false; 829 } 830 } 831 832 fragmentIdx++; 833 } 834 } 835 836 // FB-fetch attachments 837 { 838 int inputIndex = 0; 839 int inputsLength = passToMerge.numFragmentInputs; 840 841 if (inputsLength != lastPass.inputs.Length) 842 { 843 return false; 844 } 845 846 foreach (ref readonly var graphFragmentInput in passToMerge.FragmentInputs(contextData)) 847 { 848 // Find the index of this subpass's attachment in the native renderpass attachment list 849 int inputAttachmentIdx = -1; 850 for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) 851 { 852 if (fragmentList[fragmentId].resource.index == graphFragmentInput.resource.index) 853 { 854 inputAttachmentIdx = fragmentId; 855 break; 856 } 857 } 858 859 // need to keep this - same comment as above 860 if (inputAttachmentIdx < 0 || inputAttachmentIdx != lastPass.inputs[inputIndex]) 861 { 862 return false; 863 } 864 865 inputIndex++; 866 } 867 } 868 869 // last check for flags 870 return flags == lastPass.flags; 871 } 872 873 // Try to merge the new graph pass into the last native sub pass or create a new one in the current native pass. 874 // Modifies the state 875 public static void TryMergeNativeSubPass(CompilerContextData contextData, ref NativePassData nativePass, ref PassData passToMerge) 876 { 877 ref readonly var fragmentList = ref nativePass.fragments; 878 879 // Only done once per native pass (on creation), should stay -1 if no fragments 880 if (nativePass is { numNativeSubPasses: 0, fragments: { size: > 0 } }) 881 { 882 nativePass.firstNativeSubPass = contextData.nativeSubPassData.Length; 883 } 884 885 // Fill out the subpass descriptors for the native renderpasses 886 // NOTE: Not all graph subpasses get an actual native pass: 887 // - There could be passes that do only non-raster ops (like setglobal) and have no attachments. They don't get a native pass 888 // - Renderpasses that use exactly the same rendertargets at the previous pass use the same native pass. This is because 889 // nextSubpass is expensive on some platforms (even if its' essentially a no-op as it's using the same attachments). 890 SubPassDescriptor desc = new SubPassDescriptor(); 891 892 // We have no output attachments, this is an "empty" raster pass doing only non-rendering command so skip it. 893 if (passToMerge.numFragments == 0 && passToMerge.numFragmentInputs == 0) 894 { 895 // We always merge it into the currently active 896 passToMerge.nativeSubPassIndex = nativePass.numNativeSubPasses - 1; 897 passToMerge.beginNativeSubpass = false; 898 return; 899 } 900 901 // If depth ends up being bound only because of merging we explicitly say that we will not write to it 902 // which could have been implied by leaving the flag to None 903 if (!passToMerge.fragmentInfoHasDepth && nativePass.hasDepth) 904 { 905 desc.flags = SubPassFlags.ReadOnlyDepth; 906 } 907 908 // MRT attachments 909 { 910 int fragmentIdx = 0; 911 int colorOffset = (passToMerge.fragmentInfoHasDepth) ? -1 : 0; 912 913 desc.colorOutputs = new AttachmentIndexArray(passToMerge.numFragments + colorOffset); 914 915 foreach (ref readonly var graphPassFragment in passToMerge.Fragments(contextData)) 916 { 917 // Check if we're handling the depth attachment 918 if (passToMerge.fragmentInfoHasDepth && fragmentIdx == 0) 919 { 920 desc.flags = (graphPassFragment.accessFlags.HasFlag(AccessFlags.Write)) 921 ? SubPassFlags.None 922 : SubPassFlags.ReadOnlyDepth; 923 } 924 // It's a color attachment 925 else 926 { 927 // Find the index of this subpass's attachment in the native renderpass attachment list 928 int colorAttachmentIdx = -1; 929 for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) 930 { 931 if (fragmentList[fragmentId].resource.index == graphPassFragment.resource.index) 932 { 933 colorAttachmentIdx = fragmentId; 934 break; 935 } 936 } 937 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 938 939 // Set up the color indexes 940 desc.colorOutputs[fragmentIdx + colorOffset] = colorAttachmentIdx; 941 } 942 943 fragmentIdx++; 944 } 945 } 946 947 // FB-fetch attachments 948 { 949 int inputIndex = 0; 950 951 desc.inputs = new AttachmentIndexArray(passToMerge.numFragmentInputs); 952 foreach (ref readonly var fragmentInput in passToMerge.FragmentInputs(contextData)) 953 { 954 // Find the index of this subpass's attachment in the native renderpass attachment list 955 int inputAttachmentIdx = -1; 956 for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) 957 { 958 if (fragmentList[fragmentId].resource.index == fragmentInput.resource.index) 959 { 960 inputAttachmentIdx = fragmentId; 961 break; 962 } 963 } 964 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 965 966 // Set up the color indexes 967 desc.inputs[inputIndex] = inputAttachmentIdx; 968 969 inputIndex++; 970 } 971 } 972 973 if (nativePass.numNativeSubPasses == 0 974 || !NativePassCompiler.IsSameNativeSubPass(ref desc, ref contextData.nativeSubPassData.ElementAt( 975 nativePass.firstNativeSubPass + 976 nativePass.numNativeSubPasses - 1))) 977 { 978 contextData.nativeSubPassData.Add(desc); 979 int idx = contextData.nativeSubPassData.LastIndex(); 980 Debug.Assert(idx == nativePass.firstNativeSubPass + nativePass.numNativeSubPasses); 981 982 nativePass.numNativeSubPasses++; 983 passToMerge.beginNativeSubpass = true; 984 } 985 else 986 { 987 passToMerge.beginNativeSubpass = false; 988 } 989 990 passToMerge.nativeSubPassIndex = nativePass.numNativeSubPasses - 1; 991 } 992 993 // In the case where we add a new graph pass with depth to a native pass that didn't have it, we need to update 994 // inputs in all the previous native subpasses of the native pass 995 static void UpdateNativeSubPassesAttachments(CompilerContextData contextData, ref NativePassData nativePass) 996 { 997 int lastVisitedNativeSubpassIdx = -1; 998 ref readonly var fragmentList = ref nativePass.fragments; 999 1000 var countPasses = nativePass.lastGraphPass - nativePass.firstGraphPass + 1; 1001 1002 // Do not iterate over the last graph pass as it is the one we are currently adding 1003 for (var graphPassIdx = 0; graphPassIdx < countPasses - 1; ++graphPassIdx) 1004 { 1005 // We only check the first graph pass of each existing native subpass - if other graph passes 1006 // have been merged into a native subpass, it's because they had the same attachments. 1007 ref readonly var currGraphPass = 1008 ref contextData.passData.ElementAt(nativePass.firstGraphPass + graphPassIdx); 1009 1010 // Already updated this native subpass 1011 if (currGraphPass.nativeSubPassIndex + nativePass.firstNativeSubPass == lastVisitedNativeSubpassIdx) 1012 { 1013 continue; 1014 } 1015 1016 // Shouldn't be necessary since we only check the first graph pass of each existing native subpass 1017 // But let's be safe and check anyway if the pass has been culled or not. 1018 if (currGraphPass.culled) 1019 { 1020 continue; 1021 } 1022 1023 lastVisitedNativeSubpassIdx = currGraphPass.nativeSubPassIndex + nativePass.firstNativeSubPass; 1024 ref var nativeSubPassDescriptor = 1025 ref contextData.nativeSubPassData.ElementAt(lastVisitedNativeSubpassIdx); 1026 1027 // If depth ends up being bound only because of merging we explicitly say that we will not write to it 1028 // which could have been implied by leaving the flag to None 1029 if (!currGraphPass.fragmentInfoHasDepth && nativePass.hasDepth) 1030 { 1031 nativeSubPassDescriptor.flags = SubPassFlags.ReadOnlyDepth; 1032 } 1033 1034 // MRT attachments 1035 { 1036 int fragmentIdx = 0; 1037 int colorOffset = (currGraphPass.fragmentInfoHasDepth) ? -1 : 0; 1038 1039 nativeSubPassDescriptor.colorOutputs = 1040 new AttachmentIndexArray(currGraphPass.numFragments + colorOffset); 1041 1042 foreach (ref readonly var graphPassFragment in currGraphPass.Fragments(contextData)) 1043 { 1044 // Check if we're handling the depth attachment 1045 if (currGraphPass.fragmentInfoHasDepth && fragmentIdx == 0) 1046 { 1047 nativeSubPassDescriptor.flags = (graphPassFragment.accessFlags.HasFlag(AccessFlags.Write)) 1048 ? SubPassFlags.None 1049 : SubPassFlags.ReadOnlyDepth; 1050 } 1051 // It's a color attachment 1052 else 1053 { 1054 // Find the index of this subpass's attachment in the native renderpass attachment list 1055 int colorAttachmentIdx = -1; 1056 for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) 1057 { 1058 if (fragmentList[fragmentId].resource.index == graphPassFragment.resource.index) 1059 { 1060 colorAttachmentIdx = fragmentId; 1061 break; 1062 } 1063 } 1064 1065 Debug.Assert(colorAttachmentIdx >= 1066 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 1067 1068 // Set up the color indexes 1069 nativeSubPassDescriptor.colorOutputs[fragmentIdx + colorOffset] = colorAttachmentIdx; 1070 } 1071 1072 fragmentIdx++; 1073 } 1074 } 1075 1076 } 1077 } 1078 1079 public static PassBreakAudit TryMerge(CompilerContextData contextData, int activeNativePassId, int passIdToMerge) 1080 { 1081 var passBreakAudit = CanMerge(contextData, activeNativePassId, passIdToMerge); 1082 1083 // Pass cannot be merged into active native pass 1084 if (passBreakAudit.reason != PassBreakReason.Merged) 1085 return passBreakAudit; 1086 1087 ref var passToMerge = ref contextData.passData.ElementAt(passIdToMerge); 1088 ref var nativePass = ref contextData.nativePassData.ElementAt(activeNativePassId); 1089 1090 passToMerge.mergeState = PassMergeState.SubPass; 1091 if (passToMerge.nativePassIndex >= 0) 1092 contextData.nativePassData.ElementAt(passToMerge.nativePassIndex).Clear(); 1093 passToMerge.nativePassIndex = activeNativePassId; 1094 1095 nativePass.numGraphPasses++; 1096 nativePass.lastGraphPass = passIdToMerge; 1097 1098 // Depth needs special handling if the native pass doesn't have depth and merges with a pass that does 1099 // as we require the depth attachment to be at index 0 1100 if (!nativePass.hasDepth && passToMerge.fragmentInfoHasDepth) 1101 { 1102 nativePass.hasDepth = true; 1103 nativePass.fragments.Add(contextData.fragmentData[passToMerge.firstFragment]); 1104 var size = nativePass.fragments.size; 1105 if (size > 1) 1106 (nativePass.fragments[0], nativePass.fragments[size-1]) = (nativePass.fragments[size-1], nativePass.fragments[0]); 1107 1108 // Must update indices from Native subPasses created before 1109 UpdateNativeSubPassesAttachments(contextData, ref nativePass); 1110 } 1111 1112 // Update versions and flags of existing attachments and 1113 // add any new attachments 1114 foreach (ref readonly var newAttach in passToMerge.Fragments(contextData)) 1115 { 1116 bool alreadyAttached = false; 1117 1118 for (int i = 0; i < nativePass.fragments.size; ++i) 1119 { 1120 ref var existingAttach = ref nativePass.fragments[i]; 1121 if (existingAttach.resource.index == newAttach.resource.index) 1122 { 1123 // Update the attached version access flags and version 1124 existingAttach.accessFlags |= newAttach.accessFlags; 1125#if DEVELOPMENT_BUILD || UNITY_EDITOR 1126 if (existingAttach.resource.version > newAttach.resource.version) 1127 throw new Exception("Adding an older version while a higher version is already registered with the pass."); 1128#endif 1129 existingAttach.resource.version = newAttach.resource.version; 1130 alreadyAttached = true; 1131 break; 1132 } 1133 } 1134 1135 if (!alreadyAttached) 1136 { 1137 nativePass.fragments.Add(newAttach); 1138 } 1139 } 1140 1141 foreach (ref readonly var newAttach in passToMerge.FragmentInputs(contextData)) 1142 { 1143 bool alreadyAttached = false; 1144 1145 for (int i = 0; i < nativePass.fragments.size; ++i) 1146 { 1147 ref var existingAttach = ref nativePass.fragments[i]; 1148 if (existingAttach.resource.index == newAttach.resource.index) 1149 { 1150 // Update the attached version access flags and version 1151 existingAttach.accessFlags |= newAttach.accessFlags; 1152#if DEVELOPMENT_BUILD || UNITY_EDITOR 1153 if (existingAttach.resource.version > newAttach.resource.version) 1154 throw new Exception("Adding an older version while a higher version is already registered with the pass."); 1155#endif 1156 existingAttach.resource.version = newAttach.resource.version; 1157 alreadyAttached = true; 1158 break; 1159 } 1160 } 1161 1162 if (!alreadyAttached) 1163 { 1164 nativePass.fragments.Add(newAttach); 1165 } 1166 } 1167 1168 TryMergeNativeSubPass(contextData, ref nativePass, ref passToMerge); 1169 1170 SetPassStatesForNativePass(contextData, activeNativePassId); 1171 1172 return passBreakAudit; 1173 } 1174 1175 [MethodImpl(MethodImplOptions.AggressiveInlining)] 1176 public static void SetPassStatesForNativePass(CompilerContextData contextData, int nativePassId) 1177 { 1178 ref readonly var nativePass = ref contextData.nativePassData.ElementAt(nativePassId); 1179 if (nativePass.numGraphPasses > 1) 1180 { 1181 contextData.passData.ElementAt(nativePass.firstGraphPass).mergeState = PassMergeState.Begin; 1182 1183 var countPasses = nativePass.lastGraphPass - nativePass.firstGraphPass + 1; 1184 1185 for (int i = 1; i < countPasses; i++) 1186 { 1187 var indexPass = nativePass.firstGraphPass + i; 1188 1189 // This pass was culled and should not be considere 1190 if (contextData.passData.ElementAt(indexPass).culled) 1191 { 1192 contextData.passData.ElementAt(indexPass).mergeState = PassMergeState.None; 1193 continue; 1194 } 1195 1196 contextData.passData.ElementAt(nativePass.firstGraphPass + i).mergeState = PassMergeState.SubPass; 1197 } 1198 1199 contextData.passData.ElementAt(nativePass.lastGraphPass).mergeState = PassMergeState.End; 1200 } 1201 else 1202 { 1203 contextData.passData.ElementAt(nativePass.firstGraphPass).mergeState = PassMergeState.None; 1204 } 1205 } 1206 } 1207}