/ src / Ryujinx.Graphics.Vulkan / ShaderCollection.cs
ShaderCollection.cs
  1  using Ryujinx.Common.Logging;
  2  using Ryujinx.Graphics.GAL;
  3  using Silk.NET.Vulkan;
  4  using System;
  5  using System.Collections.Generic;
  6  using System.Collections.ObjectModel;
  7  using System.Linq;
  8  using System.Threading.Tasks;
  9  
 10  namespace Ryujinx.Graphics.Vulkan
 11  {
 12      class ShaderCollection : IProgram
 13      {
 14          private readonly PipelineShaderStageCreateInfo[] _infos;
 15          private readonly Shader[] _shaders;
 16  
 17          private readonly PipelineLayoutCacheEntry _plce;
 18  
 19          public PipelineLayout PipelineLayout => _plce.PipelineLayout;
 20  
 21          public bool HasMinimalLayout { get; }
 22          public bool UsePushDescriptors { get; }
 23          public bool IsCompute { get; }
 24          public bool HasTessellationControlShader => (Stages & (1u << 3)) != 0;
 25  
 26          public bool UpdateTexturesWithoutTemplate { get; }
 27  
 28          public uint Stages { get; }
 29  
 30          public PipelineStageFlags IncoherentBufferWriteStages { get; }
 31          public PipelineStageFlags IncoherentTextureWriteStages { get; }
 32  
 33          public ResourceBindingSegment[][] ClearSegments { get; }
 34          public ResourceBindingSegment[][] BindingSegments { get; }
 35          public DescriptorSetTemplate[] Templates { get; }
 36  
 37          public ProgramLinkStatus LinkStatus { get; private set; }
 38  
 39          public readonly SpecDescription[] SpecDescriptions;
 40  
 41          public bool IsLinked
 42          {
 43              get
 44              {
 45                  if (LinkStatus == ProgramLinkStatus.Incomplete)
 46                  {
 47                      CheckProgramLink(true);
 48                  }
 49  
 50                  return LinkStatus == ProgramLinkStatus.Success;
 51              }
 52          }
 53  
 54          private HashTableSlim<PipelineUid, Auto<DisposablePipeline>> _graphicsPipelineCache;
 55          private HashTableSlim<SpecData, Auto<DisposablePipeline>> _computePipelineCache;
 56  
 57          private readonly VulkanRenderer _gd;
 58          private Device _device;
 59          private bool _initialized;
 60  
 61          private ProgramPipelineState _state;
 62          private DisposableRenderPass _dummyRenderPass;
 63          private readonly Task _compileTask;
 64          private bool _firstBackgroundUse;
 65  
 66          public ShaderCollection(
 67              VulkanRenderer gd,
 68              Device device,
 69              ShaderSource[] shaders,
 70              ResourceLayout resourceLayout,
 71              SpecDescription[] specDescription = null,
 72              bool isMinimal = false)
 73          {
 74              _gd = gd;
 75              _device = device;
 76  
 77              if (specDescription != null && specDescription.Length != shaders.Length)
 78              {
 79                  throw new ArgumentException($"{nameof(specDescription)} array length must match {nameof(shaders)} array if provided");
 80              }
 81  
 82              gd.Shaders.Add(this);
 83  
 84              var internalShaders = new Shader[shaders.Length];
 85  
 86              _infos = new PipelineShaderStageCreateInfo[shaders.Length];
 87  
 88              SpecDescriptions = specDescription;
 89  
 90              LinkStatus = ProgramLinkStatus.Incomplete;
 91  
 92              uint stages = 0;
 93  
 94              for (int i = 0; i < shaders.Length; i++)
 95              {
 96                  var shader = new Shader(gd.Api, device, shaders[i]);
 97  
 98                  stages |= 1u << shader.StageFlags switch
 99                  {
100                      ShaderStageFlags.FragmentBit => 1,
101                      ShaderStageFlags.GeometryBit => 2,
102                      ShaderStageFlags.TessellationControlBit => 3,
103                      ShaderStageFlags.TessellationEvaluationBit => 4,
104                      _ => 0,
105                  };
106  
107                  if (shader.StageFlags == ShaderStageFlags.ComputeBit)
108                  {
109                      IsCompute = true;
110                  }
111  
112                  internalShaders[i] = shader;
113              }
114  
115              _shaders = internalShaders;
116  
117              bool usePushDescriptors = !isMinimal &&
118                  VulkanConfiguration.UsePushDescriptors &&
119                  _gd.Capabilities.SupportsPushDescriptors &&
120                  !IsCompute &&
121                  !HasPushDescriptorsBug(gd) &&
122                  CanUsePushDescriptors(gd, resourceLayout, IsCompute);
123  
124              ReadOnlyCollection<ResourceDescriptorCollection> sets = usePushDescriptors ?
125                  BuildPushDescriptorSets(gd, resourceLayout.Sets) : resourceLayout.Sets;
126  
127              _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, sets, usePushDescriptors);
128  
129              HasMinimalLayout = isMinimal;
130              UsePushDescriptors = usePushDescriptors;
131  
132              Stages = stages;
133  
134              ClearSegments = BuildClearSegments(sets);
135              BindingSegments = BuildBindingSegments(resourceLayout.SetUsages, out bool usesBufferTextures);
136              Templates = BuildTemplates(usePushDescriptors);
137              (IncoherentBufferWriteStages, IncoherentTextureWriteStages) = BuildIncoherentStages(resourceLayout.SetUsages);
138  
139              // Updating buffer texture bindings using template updates crashes the Adreno driver on Windows.
140              UpdateTexturesWithoutTemplate = gd.IsQualcommProprietary && usesBufferTextures;
141  
142              _compileTask = Task.CompletedTask;
143              _firstBackgroundUse = false;
144          }
145  
146          public ShaderCollection(
147              VulkanRenderer gd,
148              Device device,
149              ShaderSource[] sources,
150              ResourceLayout resourceLayout,
151              ProgramPipelineState state,
152              bool fromCache) : this(gd, device, sources, resourceLayout)
153          {
154              _state = state;
155  
156              _compileTask = BackgroundCompilation();
157              _firstBackgroundUse = !fromCache;
158          }
159  
160          private static bool HasPushDescriptorsBug(VulkanRenderer gd)
161          {
162              // Those GPUs/drivers do not work properly with push descriptors, so we must force disable them.
163              return gd.IsNvidiaPreTuring || (gd.IsIntelArc && gd.IsIntelWindows);
164          }
165  
166          private static bool CanUsePushDescriptors(VulkanRenderer gd, ResourceLayout layout, bool isCompute)
167          {
168              // If binding 3 is immediately used, use an alternate set of reserved bindings.
169              ReadOnlyCollection<ResourceUsage> uniformUsage = layout.SetUsages[0].Usages;
170              bool hasBinding3 = uniformUsage.Any(x => x.Binding == 3);
171              int[] reserved = isCompute ? Array.Empty<int>() : gd.GetPushDescriptorReservedBindings(hasBinding3);
172  
173              // Can't use any of the reserved usages.
174              for (int i = 0; i < uniformUsage.Count; i++)
175              {
176                  var binding = uniformUsage[i].Binding;
177  
178                  if (reserved.Contains(binding) ||
179                      binding >= Constants.MaxPushDescriptorBinding ||
180                      binding >= gd.Capabilities.MaxPushDescriptors + reserved.Count(id => id < binding))
181                  {
182                      return false;
183                  }
184              }
185  
186              return true;
187          }
188  
189          private static ReadOnlyCollection<ResourceDescriptorCollection> BuildPushDescriptorSets(
190              VulkanRenderer gd,
191              ReadOnlyCollection<ResourceDescriptorCollection> sets)
192          {
193              // The reserved bindings were selected when determining if push descriptors could be used.
194              int[] reserved = gd.GetPushDescriptorReservedBindings(false);
195  
196              var result = new ResourceDescriptorCollection[sets.Count];
197  
198              for (int i = 0; i < sets.Count; i++)
199              {
200                  if (i == 0)
201                  {
202                      // Push descriptors apply here. Remove reserved bindings.
203                      ResourceDescriptorCollection original = sets[i];
204  
205                      var pdUniforms = new ResourceDescriptor[original.Descriptors.Count];
206                      int j = 0;
207  
208                      foreach (ResourceDescriptor descriptor in original.Descriptors)
209                      {
210                          if (reserved.Contains(descriptor.Binding))
211                          {
212                              // If the binding is reserved, set its descriptor count to 0.
213                              pdUniforms[j++] = new ResourceDescriptor(
214                                  descriptor.Binding,
215                                  0,
216                                  descriptor.Type,
217                                  descriptor.Stages);
218                          }
219                          else
220                          {
221                              pdUniforms[j++] = descriptor;
222                          }
223                      }
224  
225                      result[i] = new ResourceDescriptorCollection(new(pdUniforms));
226                  }
227                  else
228                  {
229                      result[i] = sets[i];
230                  }
231              }
232  
233              return new(result);
234          }
235  
236          private static ResourceBindingSegment[][] BuildClearSegments(ReadOnlyCollection<ResourceDescriptorCollection> sets)
237          {
238              ResourceBindingSegment[][] segments = new ResourceBindingSegment[sets.Count][];
239  
240              for (int setIndex = 0; setIndex < sets.Count; setIndex++)
241              {
242                  List<ResourceBindingSegment> currentSegments = new();
243  
244                  ResourceDescriptor currentDescriptor = default;
245                  int currentCount = 0;
246  
247                  for (int index = 0; index < sets[setIndex].Descriptors.Count; index++)
248                  {
249                      ResourceDescriptor descriptor = sets[setIndex].Descriptors[index];
250  
251                      if (currentDescriptor.Binding + currentCount != descriptor.Binding ||
252                          currentDescriptor.Type != descriptor.Type ||
253                          currentDescriptor.Stages != descriptor.Stages ||
254                          currentDescriptor.Count > 1 ||
255                          descriptor.Count > 1)
256                      {
257                          if (currentCount != 0)
258                          {
259                              currentSegments.Add(new ResourceBindingSegment(
260                                  currentDescriptor.Binding,
261                                  currentCount,
262                                  currentDescriptor.Type,
263                                  currentDescriptor.Stages,
264                                  currentDescriptor.Count > 1));
265                          }
266  
267                          currentDescriptor = descriptor;
268                          currentCount = descriptor.Count;
269                      }
270                      else
271                      {
272                          currentCount += descriptor.Count;
273                      }
274                  }
275  
276                  if (currentCount != 0)
277                  {
278                      currentSegments.Add(new ResourceBindingSegment(
279                          currentDescriptor.Binding,
280                          currentCount,
281                          currentDescriptor.Type,
282                          currentDescriptor.Stages,
283                          currentDescriptor.Count > 1));
284                  }
285  
286                  segments[setIndex] = currentSegments.ToArray();
287              }
288  
289              return segments;
290          }
291  
292          private static ResourceBindingSegment[][] BuildBindingSegments(ReadOnlyCollection<ResourceUsageCollection> setUsages, out bool usesBufferTextures)
293          {
294              usesBufferTextures = false;
295  
296              ResourceBindingSegment[][] segments = new ResourceBindingSegment[setUsages.Count][];
297  
298              for (int setIndex = 0; setIndex < setUsages.Count; setIndex++)
299              {
300                  List<ResourceBindingSegment> currentSegments = new();
301  
302                  ResourceUsage currentUsage = default;
303                  int currentCount = 0;
304  
305                  for (int index = 0; index < setUsages[setIndex].Usages.Count; index++)
306                  {
307                      ResourceUsage usage = setUsages[setIndex].Usages[index];
308  
309                      if (usage.Type == ResourceType.BufferTexture)
310                      {
311                          usesBufferTextures = true;
312                      }
313  
314                      if (currentUsage.Binding + currentCount != usage.Binding ||
315                          currentUsage.Type != usage.Type ||
316                          currentUsage.Stages != usage.Stages ||
317                          currentUsage.ArrayLength > 1 ||
318                          usage.ArrayLength > 1)
319                      {
320                          if (currentCount != 0)
321                          {
322                              currentSegments.Add(new ResourceBindingSegment(
323                                  currentUsage.Binding,
324                                  currentCount,
325                                  currentUsage.Type,
326                                  currentUsage.Stages,
327                                  currentUsage.ArrayLength > 1));
328                          }
329  
330                          currentUsage = usage;
331                          currentCount = usage.ArrayLength;
332                      }
333                      else
334                      {
335                          currentCount++;
336                      }
337                  }
338  
339                  if (currentCount != 0)
340                  {
341                      currentSegments.Add(new ResourceBindingSegment(
342                          currentUsage.Binding,
343                          currentCount,
344                          currentUsage.Type,
345                          currentUsage.Stages,
346                          currentUsage.ArrayLength > 1));
347                  }
348  
349                  segments[setIndex] = currentSegments.ToArray();
350              }
351  
352              return segments;
353          }
354  
355          private DescriptorSetTemplate[] BuildTemplates(bool usePushDescriptors)
356          {
357              var templates = new DescriptorSetTemplate[BindingSegments.Length];
358  
359              for (int setIndex = 0; setIndex < BindingSegments.Length; setIndex++)
360              {
361                  if (usePushDescriptors && setIndex == 0)
362                  {
363                      // Push descriptors get updated using templates owned by the pipeline layout.
364                      continue;
365                  }
366  
367                  ResourceBindingSegment[] segments = BindingSegments[setIndex];
368  
369                  if (segments != null && segments.Length > 0)
370                  {
371                      templates[setIndex] = new DescriptorSetTemplate(
372                          _gd,
373                          _device,
374                          segments,
375                          _plce,
376                          IsCompute ? PipelineBindPoint.Compute : PipelineBindPoint.Graphics,
377                          setIndex);
378                  }
379              }
380  
381              return templates;
382          }
383  
384          private PipelineStageFlags GetPipelineStages(ResourceStages stages)
385          {
386              PipelineStageFlags result = 0;
387  
388              if ((stages & ResourceStages.Compute) != 0)
389              {
390                  result |= PipelineStageFlags.ComputeShaderBit;
391              }
392  
393              if ((stages & ResourceStages.Vertex) != 0)
394              {
395                  result |= PipelineStageFlags.VertexShaderBit;
396              }
397  
398              if ((stages & ResourceStages.Fragment) != 0)
399              {
400                  result |= PipelineStageFlags.FragmentShaderBit;
401              }
402  
403              if ((stages & ResourceStages.Geometry) != 0)
404              {
405                  result |= PipelineStageFlags.GeometryShaderBit;
406              }
407  
408              if ((stages & ResourceStages.TessellationControl) != 0)
409              {
410                  result |= PipelineStageFlags.TessellationControlShaderBit;
411              }
412  
413              if ((stages & ResourceStages.TessellationEvaluation) != 0)
414              {
415                  result |= PipelineStageFlags.TessellationEvaluationShaderBit;
416              }
417  
418              return result;
419          }
420  
421          private (PipelineStageFlags Buffer, PipelineStageFlags Texture) BuildIncoherentStages(ReadOnlyCollection<ResourceUsageCollection> setUsages)
422          {
423              PipelineStageFlags buffer = PipelineStageFlags.None;
424              PipelineStageFlags texture = PipelineStageFlags.None;
425  
426              foreach (var set in setUsages)
427              {
428                  foreach (var range in set.Usages)
429                  {
430                      if (range.Write)
431                      {
432                          PipelineStageFlags stages = GetPipelineStages(range.Stages);
433  
434                          switch (range.Type)
435                          {
436                              case ResourceType.Image:
437                                  texture |= stages;
438                                  break;
439                              case ResourceType.StorageBuffer:
440                              case ResourceType.BufferImage:
441                                  buffer |= stages;
442                                  break;
443                          }
444                      }
445                  }
446              }
447  
448              return (buffer, texture);
449          }
450  
451          private async Task BackgroundCompilation()
452          {
453              await Task.WhenAll(_shaders.Select(shader => shader.CompileTask));
454  
455              if (Array.Exists(_shaders, shader => shader.CompileStatus == ProgramLinkStatus.Failure))
456              {
457                  LinkStatus = ProgramLinkStatus.Failure;
458  
459                  return;
460              }
461  
462              try
463              {
464                  if (IsCompute)
465                  {
466                      CreateBackgroundComputePipeline();
467                  }
468                  else
469                  {
470                      CreateBackgroundGraphicsPipeline();
471                  }
472              }
473              catch (VulkanException e)
474              {
475                  Logger.Error?.PrintMsg(LogClass.Gpu, $"Background Compilation failed: {e.Message}");
476  
477                  LinkStatus = ProgramLinkStatus.Failure;
478              }
479          }
480  
481          private void EnsureShadersReady()
482          {
483              if (!_initialized)
484              {
485                  CheckProgramLink(true);
486  
487                  ProgramLinkStatus resultStatus = ProgramLinkStatus.Success;
488  
489                  for (int i = 0; i < _shaders.Length; i++)
490                  {
491                      var shader = _shaders[i];
492  
493                      if (shader.CompileStatus != ProgramLinkStatus.Success)
494                      {
495                          resultStatus = ProgramLinkStatus.Failure;
496                      }
497  
498                      _infos[i] = shader.GetInfo();
499                  }
500  
501                  // If the link status was already set as failure by background compilation, prefer that decision.
502                  if (LinkStatus != ProgramLinkStatus.Failure)
503                  {
504                      LinkStatus = resultStatus;
505                  }
506  
507                  _initialized = true;
508              }
509          }
510  
511          public PipelineShaderStageCreateInfo[] GetInfos()
512          {
513              EnsureShadersReady();
514  
515              return _infos;
516          }
517  
518          protected DisposableRenderPass CreateDummyRenderPass()
519          {
520              if (_dummyRenderPass.Value.Handle != 0)
521              {
522                  return _dummyRenderPass;
523              }
524  
525              return _dummyRenderPass = _state.ToRenderPass(_gd, _device);
526          }
527  
528          public void CreateBackgroundComputePipeline()
529          {
530              PipelineState pipeline = new();
531              pipeline.Initialize();
532  
533              pipeline.Stages[0] = _shaders[0].GetInfo();
534              pipeline.StagesCount = 1;
535              pipeline.PipelineLayout = PipelineLayout;
536  
537              pipeline.CreateComputePipeline(_gd, _device, this, (_gd.Pipeline as PipelineBase).PipelineCache);
538              pipeline.Dispose();
539          }
540  
541          public void CreateBackgroundGraphicsPipeline()
542          {
543              // To compile shaders in the background in Vulkan, we need to create valid pipelines using the shader modules.
544              // The GPU provides pipeline state via the GAL that can be converted into our internal Vulkan pipeline state.
545              // This should match the pipeline state at the time of the first draw. If it doesn't, then it'll likely be
546              // close enough that the GPU driver will reuse the compiled shader for the different state.
547  
548              // First, we need to create a render pass object compatible with the one that will be used at runtime.
549              // The active attachment formats have been provided by the abstraction layer.
550              var renderPass = CreateDummyRenderPass();
551  
552              PipelineState pipeline = _state.ToVulkanPipelineState(_gd);
553  
554              // Copy the shader stage info to the pipeline.
555              var stages = pipeline.Stages.AsSpan();
556  
557              for (int i = 0; i < _shaders.Length; i++)
558              {
559                  stages[i] = _shaders[i].GetInfo();
560              }
561  
562              pipeline.HasTessellationControlShader = HasTessellationControlShader;
563              pipeline.StagesCount = (uint)_shaders.Length;
564              pipeline.PipelineLayout = PipelineLayout;
565  
566              pipeline.CreateGraphicsPipeline(_gd, _device, this, (_gd.Pipeline as PipelineBase).PipelineCache, renderPass.Value, throwOnError: true);
567              pipeline.Dispose();
568          }
569  
570          public ProgramLinkStatus CheckProgramLink(bool blocking)
571          {
572              if (LinkStatus == ProgramLinkStatus.Incomplete)
573              {
574                  ProgramLinkStatus resultStatus = ProgramLinkStatus.Success;
575  
576                  foreach (Shader shader in _shaders)
577                  {
578                      if (shader.CompileStatus == ProgramLinkStatus.Incomplete)
579                      {
580                          if (blocking)
581                          {
582                              // Wait for this shader to finish compiling.
583                              shader.WaitForCompile();
584  
585                              if (shader.CompileStatus != ProgramLinkStatus.Success)
586                              {
587                                  resultStatus = ProgramLinkStatus.Failure;
588                              }
589                          }
590                          else
591                          {
592                              return ProgramLinkStatus.Incomplete;
593                          }
594                      }
595                  }
596  
597                  if (!_compileTask.IsCompleted)
598                  {
599                      if (blocking)
600                      {
601                          _compileTask.Wait();
602  
603                          if (LinkStatus == ProgramLinkStatus.Failure)
604                          {
605                              return ProgramLinkStatus.Failure;
606                          }
607                      }
608                      else
609                      {
610                          return ProgramLinkStatus.Incomplete;
611                      }
612                  }
613  
614                  return resultStatus;
615              }
616  
617              return LinkStatus;
618          }
619  
620          public byte[] GetBinary()
621          {
622              return null;
623          }
624  
625          public DescriptorSetTemplate GetPushDescriptorTemplate(long updateMask)
626          {
627              return _plce.GetPushDescriptorTemplate(IsCompute ? PipelineBindPoint.Compute : PipelineBindPoint.Graphics, updateMask);
628          }
629  
630          public void AddComputePipeline(ref SpecData key, Auto<DisposablePipeline> pipeline)
631          {
632              (_computePipelineCache ??= new()).Add(ref key, pipeline);
633          }
634  
635          public void AddGraphicsPipeline(ref PipelineUid key, Auto<DisposablePipeline> pipeline)
636          {
637              (_graphicsPipelineCache ??= new()).Add(ref key, pipeline);
638          }
639  
640          public bool TryGetComputePipeline(ref SpecData key, out Auto<DisposablePipeline> pipeline)
641          {
642              if (_computePipelineCache == null)
643              {
644                  pipeline = default;
645                  return false;
646              }
647  
648              if (_computePipelineCache.TryGetValue(ref key, out pipeline))
649              {
650                  return true;
651              }
652  
653              return false;
654          }
655  
656          public bool TryGetGraphicsPipeline(ref PipelineUid key, out Auto<DisposablePipeline> pipeline)
657          {
658              if (_graphicsPipelineCache == null)
659              {
660                  pipeline = default;
661                  return false;
662              }
663  
664              if (!_graphicsPipelineCache.TryGetValue(ref key, out pipeline))
665              {
666                  if (_firstBackgroundUse)
667                  {
668                      Logger.Warning?.Print(LogClass.Gpu, "Background pipeline compile missed on draw - incorrect pipeline state?");
669                      _firstBackgroundUse = false;
670                  }
671  
672                  return false;
673              }
674  
675              _firstBackgroundUse = false;
676  
677              return true;
678          }
679  
680          public void UpdateDescriptorCacheCommandBufferIndex(int commandBufferIndex)
681          {
682              _plce.UpdateCommandBufferIndex(commandBufferIndex);
683          }
684  
685          public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(int setIndex, out bool isNew)
686          {
687              return _plce.GetNewDescriptorSetCollection(setIndex, out isNew);
688          }
689  
690          public Auto<DescriptorSetCollection> GetNewManualDescriptorSetCollection(CommandBufferScoped cbs, int setIndex, out int cacheIndex)
691          {
692              return _plce.GetNewManualDescriptorSetCollection(cbs, setIndex, out cacheIndex);
693          }
694  
695          public void UpdateManualDescriptorSetCollectionOwnership(CommandBufferScoped cbs, int setIndex, int cacheIndex)
696          {
697              _plce.UpdateManualDescriptorSetCollectionOwnership(cbs, setIndex, cacheIndex);
698          }
699  
700          public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex)
701          {
702              _plce.ReleaseManualDescriptorSetCollection(setIndex, cacheIndex);
703          }
704  
705          public bool HasSameLayout(ShaderCollection other)
706          {
707              return other != null && _plce == other._plce;
708          }
709  
710          protected virtual void Dispose(bool disposing)
711          {
712              if (disposing)
713              {
714                  if (!_gd.Shaders.Remove(this))
715                  {
716                      return;
717                  }
718  
719                  for (int i = 0; i < _shaders.Length; i++)
720                  {
721                      _shaders[i].Dispose();
722                  }
723  
724                  if (_graphicsPipelineCache != null)
725                  {
726                      foreach (Auto<DisposablePipeline> pipeline in _graphicsPipelineCache.Values)
727                      {
728                          pipeline?.Dispose();
729                      }
730                  }
731  
732                  if (_computePipelineCache != null)
733                  {
734                      foreach (Auto<DisposablePipeline> pipeline in _computePipelineCache.Values)
735                      {
736                          pipeline.Dispose();
737                      }
738                  }
739  
740                  for (int i = 0; i < Templates.Length; i++)
741                  {
742                      Templates[i]?.Dispose();
743                  }
744  
745                  if (_dummyRenderPass.Value.Handle != 0)
746                  {
747                      _dummyRenderPass.Dispose();
748                  }
749              }
750          }
751  
752          public void Dispose()
753          {
754              Dispose(true);
755          }
756      }
757  }