A game about forced loneliness, made by TACStudios
1using System;
2using System.Diagnostics;
3using System.Collections.Generic;
4using Unity.Collections;
5using Unity.Collections.LowLevel.Unsafe;
6using UnityEngine.Rendering;
7
8namespace UnityEngine.Rendering.RenderGraphModule.NativeRenderPassCompiler
9{
10 internal partial class NativePassCompiler : IDisposable
11 {
12 internal struct RenderGraphInputInfo
13 {
14 public RenderGraphResourceRegistry m_ResourcesForDebugOnly;
15 public List<RenderGraphPass> m_RenderPasses;
16 public string debugName;
17 public bool disableCulling;
18 }
19
20 internal RenderGraphInputInfo graph;
21 internal CompilerContextData contextData = null;
22 internal CompilerContextData defaultContextData;
23 internal CommandBuffer previousCommandBuffer;
24 Stack<int> toVisitPassIds;
25
26 RenderGraphCompilationCache m_CompilationCache;
27
28 internal const int k_EstimatedPassCount = 100;
29 internal const int k_MaxSubpass = 8; // Needs to match with RenderPassSetup.h
30
31 NativeList<AttachmentDescriptor> m_BeginRenderPassAttachments;
32
33 public NativePassCompiler(RenderGraphCompilationCache cache)
34 {
35 m_CompilationCache = cache;
36 defaultContextData = new CompilerContextData(k_EstimatedPassCount);
37 toVisitPassIds = new Stack<int>(k_EstimatedPassCount);
38 m_BeginRenderPassAttachments = new NativeList<AttachmentDescriptor>(FixedAttachmentArray<AttachmentDescriptor>.MaxAttachments, Allocator.Persistent);
39 }
40
41 // IDisposable implementation
42
43 bool m_Disposed;
44
45 ~NativePassCompiler() => Cleanup();
46
47 public void Dispose()
48 {
49 Cleanup();
50 GC.SuppressFinalize(this);
51 }
52
53 void Cleanup()
54 {
55 if (!m_Disposed)
56 {
57 m_BeginRenderPassAttachments.Dispose();
58 m_Disposed = true;
59 }
60 }
61
62 public bool Initialize(RenderGraphResourceRegistry resources, List<RenderGraphPass> renderPasses, bool disableCulling, string debugName, bool useCompilationCaching, int graphHash, int frameIndex)
63 {
64 bool cached = false;
65 if (!useCompilationCaching)
66 contextData = defaultContextData;
67 else
68 cached = m_CompilationCache.GetCompilationCache(graphHash, frameIndex, out contextData);
69
70 graph.m_ResourcesForDebugOnly = resources;
71 graph.m_RenderPasses = renderPasses;
72 graph.disableCulling = disableCulling;
73 graph.debugName = debugName;
74
75 Clear(clearContextData: !useCompilationCaching);
76
77 return cached;
78 }
79
80 public void Compile(RenderGraphResourceRegistry resources)
81 {
82 SetupContextData(resources);
83
84 BuildGraph();
85
86 CullUnusedRenderPasses();
87
88 TryMergeNativePasses();
89
90 FindResourceUsageRanges();
91
92 DetectMemoryLessResources();
93
94 PrepareNativeRenderPasses();
95 }
96
97 public void Clear(bool clearContextData)
98 {
99 if (clearContextData)
100 contextData.Clear();
101 toVisitPassIds.Clear();
102 }
103
104 void SetPassStatesForNativePass(int nativePassId)
105 {
106 NativePassData.SetPassStatesForNativePass(contextData, nativePassId);
107 }
108
109 internal enum NativeCompilerProfileId
110 {
111 NRPRGComp_PrepareNativePass,
112 NRPRGComp_SetupContextData,
113 NRPRGComp_BuildGraph,
114 NRPRGComp_CullNodes,
115 NRPRGComp_TryMergeNativePasses,
116 NRPRGComp_FindResourceUsageRanges,
117 NRPRGComp_DetectMemorylessResources,
118 NRPRGComp_ExecuteCreateResources,
119 NRPRGComp_ExecuteBeginRenderpassCommand,
120 NRPRGComp_ExecuteDestroyResources,
121 }
122
123 void SetupContextData(RenderGraphResourceRegistry resources)
124 {
125 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_SetupContextData)))
126 {
127 contextData.Initialize(resources);
128 }
129 }
130
131 void BuildGraph()
132 {
133 var ctx = contextData;
134 List<RenderGraphPass> passes = graph.m_RenderPasses;
135
136 // Not clearing data, we will do it right after in the for loop
137 // This is to prevent unnecessary costly copies of pass struct (128bytes)
138 ctx.passData.ResizeUninitialized(passes.Count);
139
140 // Build up the context graph and keep track of nodes we encounter that can't be culled
141 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_BuildGraph)))
142 {
143 for (int passId = 0; passId < passes.Count; passId++)
144 {
145 var inputPass = passes[passId];
146
147#if DEVELOPMENT_BUILD || UNITY_EDITOR
148 if (inputPass.type == RenderGraphPassType.Legacy)
149 {
150 throw new Exception("Pass '" + inputPass.name + "' is using the legacy rendergraph API." +
151 " You cannot use legacy passes with the native render pass compiler." +
152 " Please do not use AddPass on the rendergrpah but use one of the more specific pass types such as AddRasterRenderPass");
153 }
154#endif
155
156 // Accessing already existing passData in place in the container through reference to avoid deep copy
157 // Make sure everything is reset and initialized or we will use obsolete data from previous frame
158 ref var ctxPass = ref ctx.passData.ElementAt(passId);
159 ctxPass.ResetAndInitialize(inputPass, passId);
160
161 ctx.passNames.Add(new Name(inputPass.name, true));
162
163 if (ctxPass.hasSideEffects)
164 {
165 toVisitPassIds.Push(passId);
166 }
167
168 // Set up the list of fragment attachments for this pass
169 // Note: This doesn't set up the resource reader/writer list as the fragment attachments
170 // will also be in the pass read/write lists accordingly
171 if (ctxPass.type == RenderGraphPassType.Raster)
172 {
173 // Grab offset in context fragment list to begin building the fragment list
174 ctxPass.firstFragment = ctx.fragmentData.Length;
175
176 // Depth attachment is always at index 0
177 if (inputPass.depthAccess.textureHandle.handle.IsValid())
178 {
179 ctxPass.fragmentInfoHasDepth = true;
180
181 if (ctx.AddToFragmentList(inputPass.depthAccess, ctxPass.firstFragment, ctxPass.numFragments))
182 {
183 ctxPass.AddFragment(inputPass.depthAccess.textureHandle.handle, ctx);
184 }
185 }
186
187 for (var ci = 0; ci < inputPass.colorBufferMaxIndex + 1; ++ci)
188 {
189 // Skip unused color slots
190 if (!inputPass.colorBufferAccess[ci].textureHandle.handle.IsValid()) continue;
191
192 if (ctx.AddToFragmentList(inputPass.colorBufferAccess[ci], ctxPass.firstFragment, ctxPass.numFragments))
193 {
194 ctxPass.AddFragment(inputPass.colorBufferAccess[ci].textureHandle.handle, ctx);
195 }
196 }
197
198 // Grab offset in context fragment list to begin building the fragment input list
199 ctxPass.firstFragmentInput = ctx.fragmentData.Length;
200
201 for (var ci = 0; ci < inputPass.fragmentInputMaxIndex + 1; ++ci)
202 {
203 // Skip unused fragment input slots
204 if (!inputPass.fragmentInputAccess[ci].textureHandle.IsValid()) continue;
205
206 var resource = inputPass.fragmentInputAccess[ci].textureHandle;
207 if (ctx.AddToFragmentList(inputPass.fragmentInputAccess[ci], ctxPass.firstFragmentInput, ctxPass.numFragmentInputs))
208 {
209 ctxPass.AddFragmentInput(inputPass.fragmentInputAccess[ci].textureHandle.handle, ctx);
210 }
211 }
212
213 // Grab offset in context random write list to begin building the per pass random write lists
214 ctxPass.firstRandomAccessResource = ctx.randomAccessResourceData.Length;
215
216 for (var ci = 0; ci < passes[passId].randomAccessResourceMaxIndex + 1; ++ci)
217 {
218 ref var uav = ref passes[passId].randomAccessResource[ci];
219
220 // Skip unused random write slots
221 if (!uav.h.IsValid()) continue;
222
223 if (ctx.AddToRandomAccessResourceList(uav.h, ci, uav.preserveCounterValue, ctxPass.firstRandomAccessResource, ctxPass.numRandomAccessResources))
224 {
225 ctxPass.AddRandomAccessResource();
226 }
227 }
228
229 // This is suspicious, there are frame buffer fetch inputs but nothing is output. We don't allow this for now.
230 // In theory you could fb-fetch inputs and write something to a uav and output nothing? This needs to be investigated
231 // so don't allow it for now.
232 if (ctxPass.numFragments == 0)
233 {
234 Debug.Assert(ctxPass.numFragmentInputs == 0);
235 }
236 }
237
238 // Set up per resource type read/write lists for this pass
239 ctxPass.firstInput = ctx.inputData.Length; // Grab offset in context input list
240 ctxPass.firstOutput = ctx.outputData.Length; // Grab offset in context output list
241 for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type)
242 {
243 var resourceWrite = inputPass.resourceWriteLists[type];
244 var resourceWriteCount = resourceWrite.Count;
245 for (var i = 0; i < resourceWriteCount; ++i)
246 {
247 var resource = resourceWrite[i];
248
249 // Writing to an imported resource is a side effect so mark the pass if needed
250 ref var resData = ref ctx.UnversionedResourceData(resource);
251 if (resData.isImported)
252 {
253 if (ctxPass.hasSideEffects == false)
254 {
255 ctxPass.hasSideEffects = true;
256 toVisitPassIds.Push(passId);
257 }
258 }
259
260 // Mark this pass as writing to this version of the resource
261 ctx.resources[resource].SetWritingPass(ctx, resource, passId);
262
263 ctx.outputData.Add(new PassOutputData
264 {
265 resource = resource,
266 });
267
268 ctxPass.numOutputs++;
269 }
270
271 var resourceRead = inputPass.resourceReadLists[type];
272 var resourceReadCount = resourceRead.Count;
273 for (var i = 0; i < resourceReadCount; ++i)
274 {
275 var resource = resourceRead[i];
276
277 // Mark this pass as reading from this version of the resource
278 ctx.resources[resource].RegisterReadingPass(ctx, resource, passId, ctxPass.numInputs);
279
280 ctx.inputData.Add(new PassInputData
281 {
282 resource = resource,
283 });
284
285 ctxPass.numInputs++;
286 }
287
288 var resourceTrans = inputPass.transientResourceList[type];
289 var resourceTransCount = resourceTrans.Count;
290
291 for (var i = 0; i < resourceTransCount; ++i)
292 {
293 var resource = resourceTrans[i];
294
295 // Mark this pass as reading from this version of the resource
296 ctx.resources[resource].RegisterReadingPass(ctx, resource, passId, ctxPass.numInputs);
297
298 ctx.inputData.Add(new PassInputData
299 {
300 resource = resource,
301 });
302
303 ctxPass.numInputs++;
304
305 // Mark this pass as writing to this version of the resource
306 ctx.resources[resource].SetWritingPass(ctx, resource, passId);
307
308 ctx.outputData.Add(new PassOutputData
309 {
310 resource = resource,
311 });
312
313 ctxPass.numOutputs++;
314 }
315 }
316 }
317 }
318 }
319
320 void CullUnusedRenderPasses()
321 {
322 var ctx = contextData;
323
324 // Source = input of the graph and starting point that takes no inputs itself : e.g. z-prepass
325 // Sink = output of the graph and end point e.g. rendered frame
326 // Usually sinks will have to be pinned or write to an external resource or the whole graph would get culled :-)
327
328 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_CullNodes)))
329 {
330 // No need to go further if we don't enable culling
331 if (graph.disableCulling)
332 return;
333
334 // Cull all passes first
335 ctx.CullAllPasses(true);
336
337 // Flood fill downstream algorithm using BFS,
338 // starting from the pinned nodes to all their dependencies
339 while (toVisitPassIds.Count != 0)
340 {
341 int passId = toVisitPassIds.Pop();
342
343 ref var passData = ref ctx.passData.ElementAt(passId);
344
345 // We already found this node through another dependency chain
346 if (!passData.culled) continue;
347
348 // Flow upstream from this node
349 foreach (ref readonly var input in passData.Inputs(ctx))
350 {
351 int inputPassIndex = ctx.resources[input.resource].writePassId;
352 toVisitPassIds.Push(inputPassIndex);
353 }
354
355 // We need this node, don't cull it
356 passData.culled = false;
357 }
358
359 // Update graph based on freshly culled nodes, remove any connection to them
360 var numPasses = ctx.passData.Length;
361 for (int passIndex = 0; passIndex < numPasses; passIndex++)
362 {
363 ref readonly var pass = ref ctx.passData.ElementAt(passIndex);
364
365 // Remove the connections from the list so they won't be visited again
366 if (pass.culled)
367 {
368 foreach (ref readonly var input in pass.Inputs(ctx))
369 {
370 var inputResource = input.resource;
371 ctx.resources[inputResource].RemoveReadingPass(ctx, inputResource, pass.passId);
372 }
373 }
374 }
375 }
376 }
377
378 void TryMergeNativePasses()
379 {
380 var ctx = contextData;
381
382 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_TryMergeNativePasses)))
383 {
384 // Try to merge raster passes into the currently active native pass. This will not do pass reordering yet
385 // so it is simply greedy trying to add the next pass to a currently active one.
386 // In the future we want to try adding any available (i.e. that has no data dependencies on any future results) future pass
387 // and allow merging that into the pass thus greedily reordering passes. But reordering requires a lot of API validation
388 // that ensures rendering behaves accordingly with reordered passes so we don't allow that for now.
389
390 // !!! Compilation caching warning !!!
391 // Merging of passes is highly dependent on render texture properties.
392 // When caching the render graph compilation, we hash a subset of those render texture properties to make sure we recompile the graph if needed.
393 // We only hash a subset for performance reason so if you add logic here that will change the behavior of pass merging,
394 // make sure that the relevant properties are hashed properly. See RenderGraphPass.ComputeHash()
395
396 int activeNativePassId = -1;
397
398 for (var passIdx = 0; passIdx < ctx.passData.Length; ++passIdx)
399 {
400 ref var passToAdd = ref ctx.passData.ElementAt(passIdx);
401
402 // If the pass has been culled, just ignore it
403 if (passToAdd.culled)
404 {
405 continue;
406 }
407
408 // If no active pass, there is nothing to merge...
409 if (activeNativePassId == -1)
410 {
411 //If raster, start a new native pass with the current pass
412 if (passToAdd.type == RenderGraphPassType.Raster)
413 {
414 // Allocate a stand-alone native renderpass based on the current pass
415 ctx.nativePassData.Add(new NativePassData(ref passToAdd, ctx));
416 passToAdd.nativePassIndex = ctx.nativePassData.LastIndex();
417 activeNativePassId = passToAdd.nativePassIndex;
418 }
419 }
420 // There is an native pass currently open, try to add the current graph pass to it
421 else
422 {
423 var mergeTestResult = NativePassData.TryMerge(contextData, activeNativePassId, passIdx);
424
425 // Merge failed, close current native render pass and create a new one
426 if (mergeTestResult.reason != PassBreakReason.Merged)
427 {
428 SetPassStatesForNativePass(activeNativePassId);
429#if UNITY_EDITOR
430 ref var nativePassData = ref contextData.nativePassData.ElementAt(activeNativePassId);
431 nativePassData.breakAudit = mergeTestResult;
432#endif
433 if (mergeTestResult.reason == PassBreakReason.NonRasterPass)
434 {
435 // Non-raster pass, no active native pass at all
436 activeNativePassId = -1;
437 }
438 else
439 {
440 // Raster but cannot be merged, allocate a new stand-alone native renderpass based on the current pass
441 ctx.nativePassData.Add(new NativePassData(ref passToAdd, ctx));
442 passToAdd.nativePassIndex = ctx.nativePassData.LastIndex();
443 activeNativePassId = passToAdd.nativePassIndex;
444 }
445 }
446 }
447 }
448
449 if (activeNativePassId >= 0)
450 {
451 // "Close" the last native pass by marking the last graph pass as end
452 SetPassStatesForNativePass(activeNativePassId);
453#if UNITY_EDITOR
454 ref var nativePassData = ref contextData.nativePassData.ElementAt(activeNativePassId);
455 nativePassData.breakAudit = new PassBreakAudit(PassBreakReason.EndOfGraph, -1);
456#endif
457 }
458 }
459 }
460
461 void FindResourceUsageRanges()
462 {
463 var ctx = contextData;
464
465 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_FindResourceUsageRanges)))
466 {
467 // Find the passes that last use a resource
468 // First do a forward-pass increasing the refcount
469 // followed by another forward-pass decreasing it again (-> if we get 0, we must be the last ones using it)
470
471 // TODO: I have a feeling this can be done more optimally by walking the graph instead of looping over all the passes twice
472 // to be investigated
473 // It also won't work if we start reordering passes.
474 for (int passIndex = 0; passIndex < ctx.passData.Length; passIndex++)
475 {
476 ref var pass = ref ctx.passData.ElementAt(passIndex);
477 if (pass.culled)
478 continue;
479
480 // Loop over all the resources this pass needs (=inputs)
481 foreach (ref readonly var input in pass.Inputs(ctx))
482 {
483 var inputResource = input.resource;
484 ref var pointTo = ref ctx.UnversionedResourceData(inputResource);
485 pointTo.lastUsePassID = -1;
486
487 // If we use version 0 and nobody else is using it yet,
488 // mark this pass as the first using the resource.
489 // It can happen that two passes use v0, e.g.:
490 // pass1.UseTex(v0,Read) -> this will clear the pass but keep it at v0
491 // pass2.UseTex(v0,Read) -> "reads" v0
492 if (inputResource.version == 0)
493 {
494 if (pointTo.firstUsePassID < 0)
495 {
496 pointTo.firstUsePassID = pass.passId;
497 pass.AddFirstUse(inputResource, ctx);
498 }
499 }
500
501 // This pass uses the last version of a resource increase the ref count of this resource
502 var last = pointTo.latestVersionNumber;
503 if (last == inputResource.version)
504 {
505 pointTo.tag++; //Refcount of how many passes are using the last version of a resource
506 }
507 }
508
509 //Also look at outputs (but with version 1) for edge case were we do a Write (but no read) to a texture and the pass is manually excluded from culling
510 //As it isn't read it won't be in the inputs array with V0
511
512 foreach (ref readonly var output in pass.Outputs(ctx))
513 {
514 var outputResource = output.resource;
515 ref var pointTo = ref ctx.UnversionedResourceData(outputResource);
516 if (outputResource.version == 1)
517 {
518 // If we use version 0 and nobody else is using it yet,
519 // Mark this pass as the first using the resource.
520 // It can happen that two passes use v0, e.g.:
521 // pass1.UseTex(v0,Read) -> this will clear the pass but keep it at v0
522 // pass3.UseTex(v0,Read/Write) -> wites v0, brings it to v1 from here on
523 if (pointTo.firstUsePassID < 0)
524 {
525 pointTo.firstUsePassID = pass.passId;
526 pass.AddFirstUse(outputResource, ctx);
527 }
528 }
529
530 // This pass outputs the last version of a resource track that
531 var last = pointTo.latestVersionNumber;
532 if (last == outputResource.version)
533 {
534 Debug.Assert(pointTo.lastWritePassID == -1); // Only one can be the last writer
535 pointTo.lastWritePassID = pass.passId;
536 }
537 }
538 }
539
540 for (int passIndex = 0; passIndex < ctx.passData.Length; passIndex++)
541 {
542 ref var pass = ref ctx.passData.ElementAt(passIndex);
543 if (pass.culled)
544 continue;
545
546 pass.waitOnGraphicsFencePassId = -1;
547 pass.insertGraphicsFence = false;
548
549 foreach (ref readonly var input in pass.Inputs(ctx))
550 {
551 var inputResource = input.resource;
552 ref var pointTo = ref ctx.UnversionedResourceData(inputResource);
553 var last = pointTo.latestVersionNumber;
554 if (last == inputResource.version)
555 {
556 var refC = pointTo.tag - 1;//Decrease refcount this pass is done using it
557 if (refC == 0)// We're the last pass done using it, this pass should destroy it.
558 {
559 pointTo.lastUsePassID = pass.passId;
560 pass.AddLastUse(inputResource, ctx);
561 }
562
563 pointTo.tag = refC;
564 }
565
566 // Resolve if this pass needs to wait on a fence due to its inputs
567 if (pass.waitOnGraphicsFencePassId == -1)
568 {
569 ref var pointToVer = ref ctx.VersionedResourceData(inputResource);
570 ref var wPass = ref ctx.passData.ElementAt(pointToVer.writePassId);
571 if (wPass.asyncCompute != pass.asyncCompute)
572 {
573 pass.waitOnGraphicsFencePassId = wPass.passId;
574 }
575 }
576 }
577
578 // We're outputting a resource that is never used.
579 // This can happen if this pass has multiple outputs and only a portion of them are used
580 // as some are used, the whole pass is not culled but the unused output still should be freed
581 foreach (ref readonly var output in pass.Outputs(ctx))
582 {
583 var outputResource = output.resource;
584 ref var pointTo = ref ctx.UnversionedResourceData(outputResource);
585 ref var pointToVer = ref ctx.VersionedResourceData(outputResource);
586 var last = pointTo.latestVersionNumber;
587 if (last == outputResource.version && pointToVer.numReaders == 0)
588 {
589 pointTo.lastUsePassID = pass.passId;
590 pass.AddLastUse(outputResource, ctx);
591 }
592
593 // Resolve if this pass should insert a fence for its outputs
594 var numReaders = pointToVer.numReaders;
595 for (var i = 0; i < numReaders; ++i)
596 {
597 var depIdx = ResourcesData.IndexReader(outputResource, i);
598 ref var dep = ref ctx.resources.readerData[outputResource.iType].ElementAt(depIdx);
599 ref var depPass = ref ctx.passData.ElementAt(dep.passId);
600 if (pass.asyncCompute != depPass.asyncCompute)
601 {
602 pass.insertGraphicsFence = true;
603 break;
604 }
605 }
606 }
607 }
608 }
609 }
610
611 void PrepareNativeRenderPasses()
612 {
613 // Prepare all native render pass execution info:
614 for (var passIdx = 0; passIdx < contextData.nativePassData.Length; ++passIdx)
615 {
616 ref var nativePassData = ref contextData.nativePassData.ElementAt(passIdx);
617 DetermineLoadStoreActions(ref nativePassData);
618 }
619 }
620
621 static bool IsGlobalTextureInPass(RenderGraphPass pass, ResourceHandle handle)
622 {
623 foreach (var g in pass.setGlobalsList)
624 {
625 if (g.Item1.handle.index == handle.index)
626 {
627 return true;
628 }
629 }
630 return false;
631 }
632
633 void DetectMemoryLessResources()
634 {
635 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_DetectMemorylessResources)))
636 {
637 // Native renderpasses and create/destroy lists have now been set-up. Detect memoryless resources, i.e resources that are created/destroyed
638 // within the scope of an nrp
639 foreach (ref readonly var nativePass in contextData.NativePasses)
640 {
641 // Loop over all created resources by this nrp
642 var graphPasses = nativePass.GraphPasses(contextData);
643 foreach (ref readonly var subPass in graphPasses)
644 {
645 foreach (ref readonly var createdRes in subPass.FirstUsedResources(contextData))
646 {
647 ref var createInfo = ref contextData.UnversionedResourceData(createdRes);
648 if (createdRes.type == RenderGraphResourceType.Texture && createInfo.isImported == false)
649 {
650 bool isGlobal = IsGlobalTextureInPass(graph.m_RenderPasses[subPass.passId], createdRes);
651
652 // Note: You could think but what if the texture is used as a regular non-frambuffer attachment texture
653 // surely it can't be memoryless then?
654 // That is true, but it can never happen as the fact these passes got merged means the textures cannot be used
655 // as regular textures halfway through a pass. If that were the case they would never have been merged in the first place.
656 // Except! If the pass consists of a single pass, in that case a texture could be allocated and freed within the single pass
657 // This is a somewhat degenerate case (e.g. a pass with culling forced off doing a uav write that is never used anywhere)
658 // But to avoid execution errors we still need to create the resource in this case.
659
660 // Check if it is in the destroy list of any of the subpasses > if yes > memoryless
661 foreach (ref readonly var subPass2 in graphPasses)
662 {
663 foreach (ref readonly var destroyedRes in subPass2.LastUsedResources(contextData))
664 {
665 ref var destInfo = ref contextData.UnversionedResourceData(destroyedRes);
666 if (destroyedRes.type == RenderGraphResourceType.Texture && destInfo.isImported == false)
667 {
668 if (createdRes.index == destroyedRes.index && !isGlobal)
669 {
670 // If a single pass in the native pass we need to check fragment attachment otherwise we're good
671 // we could always check this in theory but it's an optimization not to check it.
672 if (nativePass.numNativeSubPasses > 1 || subPass2.IsUsedAsFragment(createdRes, contextData))
673 {
674 createInfo.memoryLess = true;
675 destInfo.memoryLess = true;
676 }
677 }
678 }
679 }
680 }
681 }
682 }
683 }
684 }
685 }
686 }
687
688 internal static bool IsSameNativeSubPass(ref SubPassDescriptor a, ref SubPassDescriptor b)
689 {
690 if (a.flags != b.flags
691 || a.colorOutputs.Length != b.colorOutputs.Length
692 || a.inputs.Length != b.inputs.Length)
693 {
694 return false;
695 }
696
697 for (int i = 0; i < a.colorOutputs.Length; i++)
698 {
699 if (a.colorOutputs[i] != b.colorOutputs[i])
700 {
701 return false;
702 }
703 }
704
705 for (int i = 0; i < a.inputs.Length; i++)
706 {
707 if (a.inputs[i] != b.inputs[i])
708 {
709 return false;
710 }
711 }
712
713 return true;
714 }
715
716
717 private void ExecuteCreateRessource(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, in PassData pass)
718 {
719 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteCreateResources)))
720 {
721 resources.forceManualClearOfResource = true;
722
723 // For raster passes we need to create resources for all the subpasses at the beginning of the native renderpass
724 if (pass.type == RenderGraphPassType.Raster && pass.nativePassIndex >= 0)
725 {
726 if (pass.mergeState == PassMergeState.Begin || pass.mergeState == PassMergeState.None)
727 {
728 ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex);
729 foreach (ref readonly var subPass in nativePass.GraphPasses(contextData))
730 {
731 foreach (ref readonly var res in subPass.FirstUsedResources(contextData))
732 {
733 ref readonly var resInfo = ref contextData.UnversionedResourceData(res);
734 if (resInfo.isImported == false && resInfo.memoryLess == false)
735 {
736 bool usedAsFragmentThisPass = subPass.IsUsedAsFragment(res, contextData);
737
738 // This resource is read for the first time as a regular texture and not as a framebuffer attachment
739 // so we need to explicitly clear it, as loadAction.clear only works on framebuffer attachments
740 // TODO: Should this be a performance warning?? Maybe rare enough in practice?
741 resources.forceManualClearOfResource = !usedAsFragmentThisPass;
742 resources.CreatePooledResource(rgContext, res.iType, res.index);
743 }
744 }
745 }
746 }
747 }
748 // Other passes just create them at the beginning of the individual pass
749 else
750 {
751 foreach (ref readonly var create in pass.FirstUsedResources(contextData))
752 {
753 ref readonly var pointTo = ref contextData.UnversionedResourceData(create);
754 if (pointTo.isImported == false)
755 {
756 resources.CreatePooledResource(rgContext, create.iType, create.index);
757 }
758 }
759 }
760
761 resources.forceManualClearOfResource = true;
762 }
763 }
764
765
766 void DetermineLoadStoreActions(ref NativePassData nativePass)
767 {
768 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_PrepareNativePass)))
769 {
770 ref readonly var firstGraphPass = ref contextData.passData.ElementAt(nativePass.firstGraphPass);
771 ref readonly var lastGraphPass = ref contextData.passData.ElementAt(nativePass.lastGraphPass);
772
773 // Some passes don't do any rendering only state changes so just skip them
774 // If these passes trigger any drawing the raster command buffer will warn users no render targets are set-up for their rendering
775 if (nativePass.fragments.size <= 0)
776 {
777 return;
778 }
779
780 // Some sanity checks, these should not happen
781 Debug.Assert(firstGraphPass.mergeState == PassMergeState.Begin || firstGraphPass.mergeState == PassMergeState.None);
782 Debug.Assert(lastGraphPass.mergeState == PassMergeState.End || lastGraphPass.mergeState == PassMergeState.None);
783 ref readonly var fragmentList = ref nativePass.fragments;
784
785 // determine load store actions
786 // This pass also contains the latest versions used within this pass
787 // As we have no pass reordering for now the merged passes are always a consecutive list and we can simply do a range
788 // check on the create/destroy passid to see if it's allocated/freed in this native renderpass
789 for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId)
790 {
791 ref readonly var fragment = ref fragmentList[fragmentId];
792
793 int idx = nativePass.attachments.Add(new NativePassAttachment());
794 ref var currAttachment = ref nativePass.attachments[idx];
795
796#if UNITY_EDITOR
797 nativePass.loadAudit.Add(new LoadAudit(LoadReason.FullyRewritten));
798 ref var currLoadAudit = ref nativePass.loadAudit[idx];
799
800 nativePass.storeAudit.Add(new StoreAudit(StoreReason.DiscardUnused));
801 ref var currStoreAudit = ref nativePass.storeAudit[idx];
802#endif
803 currAttachment.handle = fragment.resource;
804 currAttachment.mipLevel = fragment.mipLevel;
805 currAttachment.depthSlice = fragment.depthSlice;
806
807 // Don't care by default
808 currAttachment.loadAction = UnityEngine.Rendering.RenderBufferLoadAction.DontCare;
809 currAttachment.storeAction = UnityEngine.Rendering.RenderBufferStoreAction.DontCare;
810
811 // Writing by-default has to preserve the contents, think rendering only a few small triangles on top of a big framebuffer
812 // So it means we need to load/clear contents potentially.
813 // If a user pass knows it will write all pixels in a buffer (like a blit) it can use the WriteAll/Discard usage to indicate this to the graph
814 bool partialWrite = fragment.accessFlags.HasFlag(AccessFlags.Write) && !fragment.accessFlags.HasFlag(AccessFlags.Discard);
815
816 ref readonly var resourceData = ref contextData.UnversionedResourceData(fragment.resource);
817 bool isImported = resourceData.isImported;
818
819 int destroyPassID = resourceData.lastUsePassID;
820 bool usedAfterThisNativePass = (destroyPassID >= (nativePass.lastGraphPass + 1));
821
822 if (fragment.accessFlags.HasFlag(AccessFlags.Read) || partialWrite)
823 {
824 // The resource is already allocated before this pass so we need to load it
825 if (resourceData.firstUsePassID < nativePass.firstGraphPass)
826 {
827 currAttachment.loadAction = UnityEngine.Rendering.RenderBufferLoadAction.Load;
828#if UNITY_EDITOR
829 currLoadAudit = new LoadAudit(LoadReason.LoadPreviouslyWritten, resourceData.firstUsePassID);
830#endif
831
832 // Once we decide to load a resource, we must default to the Store action if the resource is used after the current native pass.
833 // If we were to use the DontCare action in this case, the driver would be effectively be allowed to discard the
834 // contents of the resource. This is true even when we're only performing reads on it.
835 if (usedAfterThisNativePass)
836 {
837 currAttachment.storeAction = RenderBufferStoreAction.Store;
838#if UNITY_EDITOR
839 currStoreAudit = new StoreAudit(StoreReason.StoreUsedByLaterPass, destroyPassID);
840#endif
841 }
842
843 }
844 // It's first used this native pass so we need to clear it so reads/partial writes return the correct clear value
845 // the clear colors are part of the resource description and set-up when executing the graph we don't need to care about that here.
846 else
847 {
848 if (isImported)
849 {
850 // Check if the user indicated he wanted clearing of his imported resource on it's first use by the graph
851 if (resourceData.clear)
852 {
853 currAttachment.loadAction = UnityEngine.Rendering.RenderBufferLoadAction.Clear;
854#if UNITY_EDITOR
855 currLoadAudit = new LoadAudit(LoadReason.ClearImported);
856#endif
857 }
858 else
859 {
860 currAttachment.loadAction = UnityEngine.Rendering.RenderBufferLoadAction.Load;
861#if UNITY_EDITOR
862 currLoadAudit = new LoadAudit(LoadReason.LoadImported);
863#endif
864 }
865 }
866 else
867 {
868 // Created by the graph internally clear on first read
869 currAttachment.loadAction = UnityEngine.Rendering.RenderBufferLoadAction.Clear;
870#if UNITY_EDITOR
871 currLoadAudit = new LoadAudit(LoadReason.ClearCreated);
872#endif
873 }
874 }
875 }
876
877 if (fragment.accessFlags.HasFlag(AccessFlags.Write))
878 {
879 // Simple non-msaa case
880 if (nativePass.samples <= 1)
881 {
882 if (usedAfterThisNativePass)
883 {
884 // The resource is still used after this native pass so we need to store it.
885 currAttachment.storeAction = RenderBufferStoreAction.Store;
886#if UNITY_EDITOR
887 currStoreAudit = new StoreAudit(StoreReason.StoreUsedByLaterPass, destroyPassID);
888#endif
889 }
890 else
891 {
892 // This is the last native pass that uses the resource.
893 // If it's imported, we store it because its contents may be used outside the graph.
894 // Otherwise, we can safely discard its contents.
895 //
896 // The one exception to this, is the user declared discard flag which allows us to assume an imported
897 // resource is not used outside the graph.
898 if (isImported)
899 {
900 if (resourceData.discard)
901 {
902 currAttachment.storeAction = RenderBufferStoreAction.DontCare;
903#if UNITY_EDITOR
904 currStoreAudit = new StoreAudit(StoreReason.DiscardImported);
905#endif
906 }
907 else
908 {
909 currAttachment.storeAction = RenderBufferStoreAction.Store;
910#if UNITY_EDITOR
911 currStoreAudit = new StoreAudit(StoreReason.StoreImported);
912#endif
913 }
914 }
915 else
916 {
917 currAttachment.storeAction = RenderBufferStoreAction.DontCare;
918#if UNITY_EDITOR
919 currStoreAudit = new StoreAudit(StoreReason.DiscardUnused);
920#endif
921 }
922 }
923 }
924 // Complex msaa case
925 else
926 {
927 // The resource is still used after this native pass so we need to store it.
928 // as we don't know what happens with them and assume the contents are somewhow used outside the graph
929 // With MSAA we may access the resolved data for longer than the MSAA data so we track the destroyPass and lastPassThatNeedsUnresolved separately
930 // In theory the opposite could also be true (use MSAA after resolve data is no longer needed) but we consider it sufficiently strange to not
931 // consider it here.
932
933 currAttachment.storeAction = RenderBufferStoreAction.DontCare;
934
935 //Check if we're the last pass writing it by checking the output version of the current pass is the higherst version the resource will reach
936 bool lastWriter = (resourceData.latestVersionNumber == fragment.resource.version); // Cheaper but same? = resourceData.lastWritePassID >= pass.firstGraphPass && resourceData.lastWritePassID < pass.firstGraphPass + pass.numSubPasses;
937 bool isImportedLastWriter = isImported && lastWriter;
938
939 // Used outside this native render pass, we need to store something
940 if (destroyPassID >= nativePass.firstGraphPass + nativePass.numGraphPasses)
941 {
942 // Assume nothing is needed unless we are an imported texture (which doesn't require discarding) and we're the last ones writing it
943 bool needsMSAASamples = isImportedLastWriter && !resourceData.discard;
944 bool needsResolvedData = isImportedLastWriter && (resourceData.bindMS == false);//bindMS never resolves
945 int userPassID = 0;
946 int msaaUserPassID = 0;
947
948 // Check if we need msaa/resolved data by checking all the passes using this buffer
949 // Partial writes will register themselves as readers so this should be adequate
950 foreach (ref readonly var reader in contextData.Readers(fragment.resource))
951 {
952 ref var readerPass = ref contextData.passData.ElementAt(reader.passId);
953 bool isFragment = readerPass.IsUsedAsFragment(fragment.resource, contextData);
954
955 // Unsafe pass - we cannot know how it is used, so we need to both store and resolve
956 if (readerPass.type == RenderGraphPassType.Unsafe)
957 {
958 needsMSAASamples = true;
959 needsResolvedData = !resourceData.bindMS;
960 msaaUserPassID = reader.passId;
961 userPassID = reader.passId;
962 break;
963 }
964 // A fragment attachment use we need the msaa samples
965 else if (isFragment)
966 {
967 needsMSAASamples = true;
968 msaaUserPassID = reader.passId;
969 }
970 else
971 {
972 // Used as a multisample-texture we need the msaa samples
973 if (resourceData.bindMS)
974 {
975 needsMSAASamples = true;
976 msaaUserPassID = reader.passId;
977 }
978 // Used as a regular non-multisample texture we need resolved data
979 else
980 {
981 needsResolvedData = true;
982 userPassID = reader.passId;
983 }
984 }
985 }
986
987 if (needsMSAASamples && needsResolvedData)
988 {
989 currAttachment.storeAction = RenderBufferStoreAction.StoreAndResolve;
990#if UNITY_EDITOR
991 currStoreAudit = new StoreAudit(
992 (isImportedLastWriter) ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass, userPassID,
993 (isImportedLastWriter) ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass, msaaUserPassID);
994#endif
995 }
996 else if (needsResolvedData)
997 {
998 currAttachment.storeAction = RenderBufferStoreAction.Resolve;
999#if UNITY_EDITOR
1000 currStoreAudit = new StoreAudit((isImportedLastWriter) ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass, userPassID, StoreReason.DiscardUnused);
1001#endif
1002 }
1003 else if (needsMSAASamples)
1004 {
1005 currAttachment.storeAction = RenderBufferStoreAction.Store;
1006#if UNITY_EDITOR
1007 currStoreAudit = new StoreAudit(
1008 (resourceData.bindMS) ? StoreReason.DiscardBindMs : StoreReason.DiscardUnused, -1,
1009 (isImportedLastWriter) ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass, msaaUserPassID);
1010#endif
1011 }
1012 else
1013 {
1014 Debug.Assert(false, "Resource was not destroyed but nobody seems to be using it!??");
1015 }
1016 }
1017 else if (isImportedLastWriter)
1018 {
1019 //It's an imported texture and we're the last ones writing it make sure to store the results
1020
1021 // Used as a multisample-texture, we need the msaa samples only
1022 if (resourceData.bindMS)
1023 {
1024 if (resourceData.discard)
1025 {
1026 currAttachment.storeAction = RenderBufferStoreAction.DontCare;
1027#if UNITY_EDITOR
1028 currStoreAudit = new StoreAudit(StoreReason.DiscardImported);
1029#endif
1030 }
1031 else
1032 {
1033 currAttachment.storeAction = RenderBufferStoreAction.Store;
1034#if UNITY_EDITOR
1035 currStoreAudit = new StoreAudit(
1036 StoreReason.DiscardBindMs, -1, StoreReason.StoreImported);
1037#endif
1038 }
1039 }
1040 // Used as a regular non-multisample texture, we need samples as resolved data
1041 // we have no idea which one of them will be needed by the external users
1042 else
1043 {
1044 if (resourceData.discard)
1045 {
1046 // Depth attachment always comes first if existing
1047 bool isDepthAttachment = (nativePass.hasDepth && idx == 0);
1048
1049 // For color attachment, we only discard the MSAA buffers and keep the resolve texture
1050 // This is a design decision due to the restrictive ImportResourceParams API, it could be revised later
1051 currAttachment.storeAction = isDepthAttachment ? RenderBufferStoreAction.DontCare : RenderBufferStoreAction.Resolve;
1052#if UNITY_EDITOR
1053 currStoreAudit = new StoreAudit(
1054 StoreReason.DiscardImported, -1, StoreReason.DiscardImported);
1055#endif
1056 }
1057 else
1058 {
1059 currAttachment.storeAction = RenderBufferStoreAction.StoreAndResolve;
1060#if UNITY_EDITOR
1061 currStoreAudit = new StoreAudit(
1062 StoreReason.StoreImported, -1, StoreReason.StoreImported);
1063#endif
1064 }
1065 }
1066 }
1067 }
1068 }
1069
1070 if (resourceData.memoryLess)
1071 {
1072 currAttachment.memoryless = true;
1073#if DEVELOPMENT_BUILD || UNITY_EDITOR
1074 // Ensure load/store actions are actually valid for memory less
1075 if (currAttachment.loadAction == RenderBufferLoadAction.Load) throw new Exception("Resource was marked as memoryless but is trying to load.");
1076 if (currAttachment.storeAction != RenderBufferStoreAction.DontCare) throw new Exception("Resource was marked as memoryless but is trying to store or resolve.");
1077#endif
1078 }
1079 }
1080 }
1081 }
1082
1083 [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
1084 private void ValidateNativePass(in NativePassData nativePass, int width, int height, int depth, int samples, int attachmentCount)
1085 {
1086 if (RenderGraph.enableValidityChecks)
1087 {
1088 if (nativePass.attachments.size == 0 || nativePass.numNativeSubPasses == 0)
1089 throw new Exception("Empty render pass");
1090
1091 if (width == 0 || height == 0 || depth == 0 || samples == 0 || nativePass.numNativeSubPasses == 0 || attachmentCount == 0)
1092 throw new Exception("Invalid render pass properties. One or more properties are zero.");
1093 }
1094 }
1095
1096 [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
1097 private void ValidateAttachmentRenderTarget(in RenderTargetInfo attRenderTargetInfo, RenderGraphResourceRegistry resources, int nativePassWidth, int nativePassHeight, int nativePassMSAASamples)
1098 {
1099 if(RenderGraph.enableValidityChecks)
1100 {
1101 if (attRenderTargetInfo.width != nativePassWidth || attRenderTargetInfo.height != nativePassHeight || attRenderTargetInfo.msaaSamples != nativePassMSAASamples)
1102 throw new Exception("Low level rendergraph error: Attachments in renderpass do not match!");
1103 }
1104 }
1105
1106 internal unsafe void ExecuteBeginRenderPass(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, ref NativePassData nativePass)
1107 {
1108 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteBeginRenderpassCommand)))
1109 {
1110 ref var attachments = ref nativePass.attachments;
1111 var attachmentCount = attachments.size;
1112
1113 ref readonly var firstGraphPass = ref contextData.passData.ElementAt(nativePass.firstGraphPass);
1114 var w = firstGraphPass.fragmentInfoWidth;
1115 var h = firstGraphPass.fragmentInfoHeight;
1116 var d = firstGraphPass.fragmentInfoVolumeDepth;
1117 var s = firstGraphPass.fragmentInfoSamples;
1118
1119 ValidateNativePass(nativePass, w, h, d, s, attachmentCount);
1120
1121 ref var nativeSubPasses = ref contextData.nativeSubPassData;
1122 NativeArray<SubPassDescriptor> nativeSubPassArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<SubPassDescriptor>(nativeSubPasses.GetUnsafeReadOnlyPtr<SubPassDescriptor>() + nativePass.firstNativeSubPass, nativePass.numNativeSubPasses, Allocator.None);
1123
1124#if ENABLE_UNITY_COLLECTIONS_CHECKS
1125 var safetyHandle = AtomicSafetyHandle.Create();
1126 AtomicSafetyHandle.SetAllowReadOrWriteAccess(safetyHandle, true);
1127 NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeSubPassArray, safetyHandle);
1128#endif
1129
1130 if (nativePass.hasFoveatedRasterization)
1131 {
1132 rgContext.cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Enabled);
1133 }
1134
1135 // Filling the attachments array to be sent to the rendering command buffer
1136 m_BeginRenderPassAttachments.Resize(attachmentCount, NativeArrayOptions.UninitializedMemory);
1137 for (var i = 0; i < attachmentCount; ++i)
1138 {
1139 ref var currAttachmentHandle = ref attachments[i].handle;
1140
1141 resources.GetRenderTargetInfo(currAttachmentHandle, out var renderTargetInfo);
1142 ValidateAttachmentRenderTarget(renderTargetInfo, resources, w, h, s);
1143
1144 ref var currBeginAttachment = ref m_BeginRenderPassAttachments.ElementAt(i);
1145 currBeginAttachment = new AttachmentDescriptor(renderTargetInfo.format);
1146
1147 // Set up the RT pointers
1148 if (attachments[i].memoryless == false)
1149 {
1150 var rtHandle = resources.GetTexture(currAttachmentHandle.index);
1151
1152 //HACK: Always set the loadstore target even if StoreAction == DontCare or Resolve
1153 //and LoadAction == Clear or DontCare
1154 //in these cases you could argue setting the loadStoreTarget to NULL and only set the resolveTarget
1155 //but this confuses the backend (on vulkan) and in general is not how the lower level APIs tend to work.
1156 //because of the RenderTexture duality where we always bundle store+resolve targets as one RTex
1157 //it does become impossible to have a memoryless loadStore texture with a memoryfull resolve
1158 //but that is why we mark this as a hack and future work to fix.
1159 //The proper (and planned) solution would be to move away from the render texture duality.
1160 RenderTargetIdentifier rtidAllSlices = rtHandle;
1161 currBeginAttachment.loadStoreTarget = new RenderTargetIdentifier(rtidAllSlices, attachments[i].mipLevel, CubemapFace.Unknown, attachments[i].depthSlice);
1162
1163 if (attachments[i].storeAction == RenderBufferStoreAction.Resolve ||
1164 attachments[i].storeAction == RenderBufferStoreAction.StoreAndResolve)
1165 {
1166 currBeginAttachment.resolveTarget = rtHandle;
1167 }
1168 }
1169 // In the memoryless case it's valid to not set both loadStoreTarget/and resolveTarget as the backend will allocate a transient one
1170
1171 currBeginAttachment.loadAction = attachments[i].loadAction;
1172 currBeginAttachment.storeAction = attachments[i].storeAction;
1173
1174 // Set up clear colors if we have a clear load action
1175 if (attachments[i].loadAction == RenderBufferLoadAction.Clear)
1176 {
1177 currBeginAttachment.clearColor = Color.red;
1178 currBeginAttachment.clearDepth = 1.0f;
1179 currBeginAttachment.clearStencil = 0;
1180 var desc = resources.GetTextureResourceDesc(currAttachmentHandle, true);
1181 if (i == 0 && nativePass.hasDepth)
1182 {
1183 // TODO: There seems to be no clear depth specified ?!?!
1184 currBeginAttachment.clearDepth = 1.0f;// desc.clearDepth;
1185 }
1186 else
1187 {
1188 currBeginAttachment.clearColor = desc.clearColor;
1189 }
1190 }
1191 }
1192
1193 NativeArray<AttachmentDescriptor> attachmentDescArray = m_BeginRenderPassAttachments.AsArray();
1194
1195 var depthAttachmentIndex = nativePass.hasDepth ? 0 : -1;
1196
1197 var graphPassNamesForDebugSpan = ReadOnlySpan<byte>.Empty;
1198#if DEVELOPMENT_BUILD || UNITY_EDITOR
1199 if(RenderGraph.enableValidityChecks)
1200 {
1201 graphPassNamesForDebug.Clear();
1202
1203 nativePass.GetGraphPassNames(contextData, graphPassNamesForDebug);
1204
1205 int utf8CStrDebugNameLength = 0;
1206 foreach (ref readonly Name graphPassName in graphPassNamesForDebug)
1207 {
1208 utf8CStrDebugNameLength += graphPassName.utf8ByteCount + 1; // +1 to add '/' between passes or the null terminator at the end
1209 }
1210
1211 var nameBytes = stackalloc byte[utf8CStrDebugNameLength];
1212 if (utf8CStrDebugNameLength > 0)
1213 {
1214 int startStr = 0;
1215 foreach (ref readonly var graphPassName in graphPassNamesForDebug)
1216 {
1217 int strByteCount = graphPassName.utf8ByteCount;
1218 System.Text.Encoding.UTF8.GetBytes(graphPassName.name.AsSpan(), new Span<byte>(nameBytes + startStr, strByteCount));
1219 startStr += strByteCount;
1220 // Adding '/' in UTF8
1221 nameBytes[startStr++] = (byte)(0x2F);
1222 }
1223 // Rewriting last '/' to be the null terminator
1224 nameBytes[utf8CStrDebugNameLength - 1] = (byte)0;
1225 }
1226
1227 graphPassNamesForDebugSpan = new ReadOnlySpan<byte>(nameBytes, utf8CStrDebugNameLength);
1228 }
1229#endif
1230
1231 rgContext.cmd.BeginRenderPass(w, h, d, s, attachmentDescArray, depthAttachmentIndex, nativeSubPassArray, graphPassNamesForDebugSpan);
1232
1233#if ENABLE_UNITY_COLLECTIONS_CHECKS
1234 AtomicSafetyHandle.Release(safetyHandle);
1235#endif
1236 CommandBuffer.ThrowOnSetRenderTarget = true;
1237 }
1238 }
1239
1240 const int ArbitraryMaxNbMergedPasses = 16;
1241 DynamicArray<Name> graphPassNamesForDebug = new DynamicArray<Name>(ArbitraryMaxNbMergedPasses);
1242
1243 private void ExecuteDestroyResource(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, ref PassData pass)
1244 {
1245 using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteDestroyResources)))
1246 {
1247 if (pass.type == RenderGraphPassType.Raster && pass.nativePassIndex >= 0)
1248 {
1249 // For raster passes we need to destroy resources after all the subpasses at the end of the native renderpass
1250 if (pass.mergeState == PassMergeState.End || pass.mergeState == PassMergeState.None)
1251 {
1252 ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex);
1253 foreach (ref readonly var subPass in nativePass.GraphPasses(contextData))
1254 {
1255 foreach (ref readonly var res in subPass.LastUsedResources(contextData))
1256 {
1257 ref readonly var resInfo = ref contextData.UnversionedResourceData(res);
1258 if (resInfo.isImported == false && resInfo.memoryLess == false)
1259 {
1260 resources.ReleasePooledResource(rgContext, res.iType, res.index);
1261 }
1262 }
1263 }
1264 }
1265 }
1266 else
1267 {
1268 foreach (ref readonly var destroy in pass.LastUsedResources(contextData))
1269 {
1270 ref readonly var pointTo = ref contextData.UnversionedResourceData(destroy);
1271 if (pointTo.isImported == false)
1272 {
1273 resources.ReleasePooledResource(rgContext, destroy.iType, destroy.index);
1274 }
1275 }
1276 }
1277 }
1278 }
1279
1280 internal unsafe void SetRandomWriteTarget(in CommandBuffer cmd, RenderGraphResourceRegistry resources, int index, ResourceHandle resource, bool preserveCounterValue = true)
1281 {
1282 if (resource.type == RenderGraphResourceType.Texture)
1283 {
1284 var tex = resources.GetTexture(resource.index);
1285 cmd.SetRandomWriteTarget(index, tex);
1286 }
1287 else if (resource.type == RenderGraphResourceType.Buffer)
1288 {
1289 var buff = resources.GetBuffer(resource.index);
1290 // Default is to preserve the value
1291 if (preserveCounterValue)
1292 {
1293 cmd.SetRandomWriteTarget(index, buff);
1294 }
1295 else
1296 {
1297 cmd.SetRandomWriteTarget(index, buff, false);
1298 }
1299
1300 }
1301 else
1302 {
1303 throw new Exception($"Invalid resource type {resource.type}, expected texture or buffer");
1304 }
1305 }
1306
1307 internal void ExecuteGraphNode(ref InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, RenderGraphPass pass)
1308 {
1309#if THROW_ON_SETRENDERTARGET_DEBUG
1310 if (pass.type == RenderGraphPassType.Raster)
1311 {
1312 CommandBuffer.ThrowOnSetRenderTarget = true;
1313 }
1314#endif
1315 try
1316 {
1317 rgContext.executingPass = pass;
1318
1319 if (!pass.HasRenderFunc())
1320 {
1321 throw new InvalidOperationException(string.Format("RenderPass {0} was not provided with an execute function.", pass.name));
1322 }
1323
1324 using (new ProfilingScope(rgContext.cmd, pass.customSampler))
1325 {
1326 pass.Execute(rgContext);
1327
1328 foreach (var tex in pass.setGlobalsList)
1329 {
1330 rgContext.cmd.SetGlobalTexture(tex.Item2, tex.Item1);
1331 }
1332 }
1333 }
1334 finally
1335 {
1336#if THROW_ON_SETRENDERTARGET_DEBUG
1337 if (pass.type == RenderGraphPassType.Raster)
1338 {
1339 CommandBuffer.ThrowOnSetRenderTarget = false;
1340 }
1341#endif
1342 }
1343 }
1344
1345 public void ExecuteGraph(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, in List<RenderGraphPass> passes)
1346 {
1347 bool inRenderPass = false;
1348 previousCommandBuffer = rgContext.cmd;
1349
1350 // Having random access targets bound leads to all sorts of weird behavior so we clear them before executing the graph.
1351 rgContext.cmd.ClearRandomWriteTargets();
1352
1353 for (int passIndex = 0; passIndex < contextData.passData.Length; passIndex++)
1354 {
1355 ref var pass = ref contextData.passData.ElementAt(passIndex);
1356
1357 //Fix low level passes being merged into nrp giving errors
1358 //because of the "isRaster" check below
1359
1360 if (pass.culled)
1361 continue;
1362
1363 var isRaster = pass.type == RenderGraphPassType.Raster;
1364
1365 ExecuteCreateRessource(rgContext, resources, pass);
1366
1367 var isAsyncCompute = pass.type == RenderGraphPassType.Compute && pass.asyncCompute == true;
1368 if (isAsyncCompute)
1369 {
1370 if (rgContext.contextlessTesting == false)
1371 rgContext.renderContext.ExecuteCommandBuffer(rgContext.cmd);
1372 rgContext.cmd.Clear();
1373
1374 var asyncCmd = CommandBufferPool.Get("async cmd");
1375 asyncCmd.SetExecutionFlags(CommandBufferExecutionFlags.AsyncCompute);
1376 rgContext.cmd = asyncCmd;
1377 }
1378
1379 // also make sure to insert fence=waits for multiple queue syncs
1380 if (pass.waitOnGraphicsFencePassId != -1)
1381 {
1382 var fence = contextData.fences[pass.waitOnGraphicsFencePassId];
1383 rgContext.cmd.WaitOnAsyncGraphicsFence(fence);
1384 }
1385
1386 var nrpBegan = false;
1387 if (isRaster == true && pass.mergeState <= PassMergeState.Begin)
1388 {
1389 if (pass.nativePassIndex >= 0)
1390 {
1391 ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex);
1392 if (nativePass.fragments.size > 0)
1393 {
1394 ExecuteBeginRenderPass(rgContext, resources, ref nativePass);
1395 nrpBegan = true;
1396 inRenderPass = true;
1397 }
1398 }
1399 }
1400
1401 if (pass.mergeState >= PassMergeState.SubPass)
1402 {
1403 if (pass.beginNativeSubpass)
1404 {
1405 if (!inRenderPass)
1406 {
1407 throw new Exception("Compiler error: Pass is marked as beginning a native sub pass but no pass is currently active.");
1408 }
1409 rgContext.cmd.NextSubPass();
1410 }
1411 }
1412
1413 if (pass.numRandomAccessResources > 0)
1414 {
1415 foreach (var randomWriteAttachment in pass.RandomWriteTextures(contextData))
1416 {
1417 SetRandomWriteTarget(rgContext.cmd, resources, randomWriteAttachment.index, randomWriteAttachment.resource);
1418 }
1419 }
1420
1421#if THROW_ON_SETRENDERTARGET_DEBUG
1422 if (passes[pass.passId].type == RenderGraphPassType.Raster)
1423 {
1424 CommandBuffer.ThrowOnSetRenderTarget = true;
1425 }
1426#endif
1427
1428 ExecuteGraphNode(ref rgContext, resources, passes[pass.passId]);
1429
1430 // If we set any uavs clear them again so they are local to the pass
1431 if (pass.numRandomAccessResources > 0)
1432 {
1433 rgContext.cmd.ClearRandomWriteTargets();
1434 }
1435
1436 // should we insert a fence to sync between difference queues?
1437 if (pass.insertGraphicsFence)
1438 {
1439 var fence = rgContext.cmd.CreateAsyncGraphicsFence();
1440 contextData.fences[pass.passId] = fence;
1441 }
1442
1443 if (isRaster)
1444 {
1445 if ((pass.mergeState == PassMergeState.None && nrpBegan)
1446 || pass.mergeState == PassMergeState.End)
1447 {
1448 if (pass.nativePassIndex >= 0)
1449 {
1450 ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex);
1451 if (nativePass.fragments.size > 0)
1452 {
1453 if (!inRenderPass)
1454 {
1455 throw new Exception("Compiler error: Generated a subpass pass but no pass is currently active.");
1456 }
1457
1458 if (nativePass.hasFoveatedRasterization)
1459 {
1460 rgContext.cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Disabled);
1461 }
1462
1463 rgContext.cmd.EndRenderPass();
1464 CommandBuffer.ThrowOnSetRenderTarget = false;
1465 inRenderPass = false;
1466 }
1467 }
1468 }
1469 }
1470 else if (isAsyncCompute)
1471 {
1472 rgContext.renderContext.ExecuteCommandBufferAsync(rgContext.cmd, ComputeQueueType.Background);
1473 CommandBufferPool.Release(rgContext.cmd);
1474 rgContext.cmd = previousCommandBuffer;
1475 }
1476
1477 ExecuteDestroyResource(rgContext, resources, ref pass);
1478 }
1479 }
1480 }
1481}