3
0

RenderPass.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <Atom/RHI/CommandList.h>
  9. #include <Atom/RHI/FrameGraphAttachmentInterface.h>
  10. #include <Atom/RHI/FrameGraphBuilder.h>
  11. #include <Atom/RHI/FrameGraphCompileContext.h>
  12. #include <Atom/RHI/FrameGraphExecuteContext.h>
  13. #include <Atom/RHI/RHIUtils.h>
  14. #include <Atom/RHI.Reflect/ImageScopeAttachmentDescriptor.h>
  15. #include <Atom/RHI.Reflect/RenderAttachmentLayoutBuilder.h>
  16. #include <Atom/RHI.Reflect/Size.h>
  17. #include <Atom/RPI.Public/Base.h>
  18. #include <Atom/RPI.Reflect/Pass/RenderPassData.h>
  19. #include <Atom/RPI.Public/GpuQuery/Query.h>
  20. #include <Atom/RPI.Public/Pass/PassUtils.h>
  21. #include <Atom/RPI.Public/Pass/RenderPass.h>
  22. #include <Atom/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.h>
  23. #include <Atom/RPI.Public/RenderPipeline.h>
  24. #include <Atom/RPI.Public/Scene.h>
  25. #include <Atom/RPI.Public/View.h>
  26. namespace AZ
  27. {
  28. namespace RPI
  29. {
  30. RenderPass::RenderPass(const PassDescriptor& descriptor)
  31. : Pass(descriptor)
  32. {
  33. // Read view tag from pass data
  34. const RenderPassData* passData = PassUtils::GetPassData<RenderPassData>(descriptor);
  35. if (passData && !passData->m_pipelineViewTag.IsEmpty())
  36. {
  37. SetPipelineViewTag(passData->m_pipelineViewTag);
  38. }
  39. if (passData && passData->m_bindViewSrg)
  40. {
  41. m_flags.m_bindViewSrg = true;
  42. }
  43. if (passData && passData->m_deviceIndex >= 0 && passData->m_deviceIndex < RHI::RHISystemInterface::Get()->GetDeviceCount())
  44. {
  45. m_deviceIndex = passData->m_deviceIndex;
  46. }
  47. }
  48. RenderPass::~RenderPass()
  49. {
  50. }
  51. RHI::RenderAttachmentConfiguration RenderPass::GetRenderAttachmentConfiguration() const
  52. {
  53. RHI::RenderAttachmentLayoutBuilder builder;
  54. auto* layoutBuilder = builder.AddSubpass();
  55. for (size_t slotIndex = 0; slotIndex < m_attachmentBindings.size(); ++slotIndex)
  56. {
  57. const PassAttachmentBinding& binding = m_attachmentBindings[slotIndex];
  58. if (!binding.GetAttachment())
  59. {
  60. continue;
  61. }
  62. // Handle the depth-stencil attachment. There should be only one.
  63. if (binding.m_scopeAttachmentUsage == RHI::ScopeAttachmentUsage::DepthStencil)
  64. {
  65. layoutBuilder->DepthStencilAttachment(binding.GetAttachment()->m_descriptor.m_image.m_format);
  66. continue;
  67. }
  68. // Handle shading rate attachment. There should be only one.
  69. if (binding.m_scopeAttachmentUsage == RHI::ScopeAttachmentUsage::ShadingRate)
  70. {
  71. layoutBuilder->ShadingRateAttachment(binding.GetAttachment()->m_descriptor.m_image.m_format);
  72. continue;
  73. }
  74. // Skip bindings that aren't outputs or inputOutputs
  75. if (binding.m_slotType != PassSlotType::Output && binding.m_slotType != PassSlotType::InputOutput)
  76. {
  77. continue;
  78. }
  79. if (binding.m_scopeAttachmentUsage == RHI::ScopeAttachmentUsage::RenderTarget)
  80. {
  81. RHI::Format format = binding.GetAttachment()->m_descriptor.m_image.m_format;
  82. layoutBuilder->RenderTargetAttachment(format);
  83. }
  84. }
  85. RHI::RenderAttachmentLayout layout;
  86. [[maybe_unused]] RHI::ResultCode result = builder.End(layout);
  87. AZ_Assert(result == RHI::ResultCode::Success, "RenderPass [%s] failed to create render attachment layout", GetPathName().GetCStr());
  88. return RHI::RenderAttachmentConfiguration{ layout, 0 };
  89. }
  90. RHI::MultisampleState RenderPass::GetMultisampleState() const
  91. {
  92. RHI::MultisampleState outputMultiSampleState;
  93. bool wasSet = false;
  94. for (size_t slotIndex = 0; slotIndex < m_attachmentBindings.size(); ++slotIndex)
  95. {
  96. const PassAttachmentBinding& binding = m_attachmentBindings[slotIndex];
  97. if (binding.m_slotType != PassSlotType::Output && binding.m_slotType != PassSlotType::InputOutput)
  98. {
  99. continue;
  100. }
  101. if (!binding.GetAttachment())
  102. {
  103. continue;
  104. }
  105. if (binding.m_scopeAttachmentUsage == RHI::ScopeAttachmentUsage::RenderTarget
  106. || binding.m_scopeAttachmentUsage == RHI::ScopeAttachmentUsage::DepthStencil)
  107. {
  108. if (!wasSet)
  109. {
  110. // save multi-sample state found in the first output color attachment
  111. outputMultiSampleState = binding.GetAttachment()->m_descriptor.m_image.m_multisampleState;
  112. wasSet = true;
  113. }
  114. else if (PassValidation::IsEnabled())
  115. {
  116. // return false directly if the current output color attachment has different multi-sample state then previous ones
  117. if (outputMultiSampleState != binding.GetAttachment()->m_descriptor.m_image.m_multisampleState)
  118. {
  119. AZ_Error("RPI", false, "Pass %s has different multi-sample states within its color attachments", GetPathName().GetCStr());
  120. break;
  121. }
  122. }
  123. else
  124. {
  125. break;
  126. }
  127. }
  128. }
  129. return outputMultiSampleState;
  130. }
  131. void RenderPass::InitializeInternal()
  132. {
  133. if (m_shaderResourceGroup != nullptr)
  134. {
  135. Name autoBind = Name("AutoBind");
  136. Name noBind = Name("NoBind");
  137. for (PassAttachmentBinding& binding : m_attachmentBindings)
  138. {
  139. const Name& shaderName = binding.m_shaderInputName;
  140. PassAttachment* attachment = binding.GetAttachment().get();
  141. if (shaderName == autoBind)
  142. {
  143. binding.m_shaderInputIndex = PassAttachmentBinding::ShaderInputAutoBind;
  144. }
  145. else if (shaderName == noBind)
  146. {
  147. binding.m_shaderInputIndex = PassAttachmentBinding::ShaderInputNoBind;
  148. }
  149. else if (attachment)
  150. {
  151. if (attachment->GetAttachmentType() == RHI::AttachmentType::Image)
  152. {
  153. RHI::ShaderInputImageIndex idx = m_shaderResourceGroup->FindShaderInputImageIndex(shaderName);
  154. AZ_Error("Pass System", idx.IsValid(), "[Pass %s] Could not retrieve Shader Image Index for SRG variable'%s'", GetName().GetCStr(), shaderName.GetCStr());
  155. binding.m_shaderInputIndex = idx.IsValid() ? static_cast<int16_t>(idx.GetIndex()) : PassAttachmentBinding::ShaderInputNoBind;
  156. }
  157. else if (attachment->GetAttachmentType() == RHI::AttachmentType::Buffer)
  158. {
  159. RHI::ShaderInputBufferIndex idx = m_shaderResourceGroup->FindShaderInputBufferIndex(shaderName);
  160. AZ_Error("Pass System", idx.IsValid(), "[Pass %s] Could not retrieve Shader Buffer Index for SRG variable '%s'", GetName().GetCStr(), shaderName.GetCStr());
  161. binding.m_shaderInputIndex = idx.IsValid() ? static_cast<int16_t>(idx.GetIndex()) : PassAttachmentBinding::ShaderInputNoBind;
  162. }
  163. }
  164. else
  165. {
  166. AZ_Error( "Pass System", AZ::RHI::IsNullRHI(), "[Pass %s] Could not bind shader buffer index '%s' because it has no attachment.", GetName().GetCStr(), shaderName.GetCStr());
  167. binding.m_shaderInputIndex = PassAttachmentBinding::ShaderInputNoBind;
  168. }
  169. }
  170. }
  171. }
  172. void RenderPass::FrameBeginInternal(FramePrepareParams params)
  173. {
  174. m_timestampResult = AZ::RPI::TimestampResult();
  175. if (GetScopeId().IsEmpty())
  176. {
  177. InitScope(RHI::ScopeId(GetPathName()), m_hardwareQueueClass, m_deviceIndex);
  178. }
  179. params.m_frameGraphBuilder->ImportScopeProducer(*this);
  180. // Read back the ScopeQueries submitted from previous frames
  181. ReadbackScopeQueryResults();
  182. CollectSrgs();
  183. PassSystemInterface::Get()->IncrementFrameRenderPassCount();
  184. }
  185. void RenderPass::FrameEndInternal()
  186. {
  187. ResetSrgs();
  188. }
  189. void RenderPass::SetupFrameGraphDependencies(RHI::FrameGraphInterface frameGraph)
  190. {
  191. DeclareAttachmentsToFrameGraph(frameGraph);
  192. DeclarePassDependenciesToFrameGraph(frameGraph);
  193. AddScopeQueryToFrameGraph(frameGraph);
  194. }
  195. void RenderPass::BuildCommandList(const RHI::FrameGraphExecuteContext& context)
  196. {
  197. BeginScopeQuery(context);
  198. BuildCommandListInternal(context);
  199. EndScopeQuery(context);
  200. m_lastDeviceIndex = context.GetDeviceIndex();
  201. }
  202. void RenderPass::DeclarePassDependenciesToFrameGraph(RHI::FrameGraphInterface frameGraph) const
  203. {
  204. for (Pass* pass : m_executeAfterPasses)
  205. {
  206. RenderPass* renderPass = azrtti_cast<RenderPass*>(pass);
  207. if (renderPass)
  208. {
  209. frameGraph.ExecuteAfter(renderPass->GetScopeId());
  210. }
  211. }
  212. for (Pass* pass : m_executeBeforePasses)
  213. {
  214. RenderPass* renderPass = azrtti_cast<RenderPass*>(pass);
  215. if (renderPass)
  216. {
  217. frameGraph.ExecuteBefore(renderPass->GetScopeId());
  218. }
  219. }
  220. }
  221. void RenderPass::BindAttachment(const RHI::FrameGraphCompileContext& context, PassAttachmentBinding& binding, int16_t& imageIndex, int16_t& bufferIndex)
  222. {
  223. PassAttachment* attachment = binding.GetAttachment().get();
  224. if (attachment)
  225. {
  226. int16_t inputIndex = binding.m_shaderInputIndex;
  227. uint16_t arrayIndex = binding.m_shaderInputArrayIndex;
  228. if (attachment->GetAttachmentType() == RHI::AttachmentType::Image)
  229. {
  230. if (inputIndex == PassAttachmentBinding::ShaderInputAutoBind)
  231. {
  232. inputIndex = imageIndex;
  233. }
  234. const RHI::ImageView* imageView =
  235. context.GetImageView(attachment->GetAttachmentId(), binding.m_unifiedScopeDesc.GetImageViewDescriptor(), binding.m_scopeAttachmentUsage);
  236. if (binding.m_shaderImageDimensionsNameIndex.HasName())
  237. {
  238. RHI::Size size = attachment->m_descriptor.m_image.m_size;
  239. AZ::Vector4 imageDimensions;
  240. imageDimensions.SetX(float(size.m_width));
  241. imageDimensions.SetY(float(size.m_height));
  242. imageDimensions.SetZ(1.0f / float(size.m_width));
  243. imageDimensions.SetW(1.0f / float(size.m_height));
  244. [[maybe_unused]]
  245. bool success = m_shaderResourceGroup->SetConstant(binding.m_shaderImageDimensionsNameIndex, imageDimensions);
  246. AZ_Assert(success, "Pass [%s] Could not find float4 constant [%s] in Shader Resource Group [%s]",
  247. GetPathName().GetCStr(),
  248. binding.m_shaderImageDimensionsNameIndex.GetNameForDebug().GetCStr(),
  249. m_shaderResourceGroup->GetDatabaseName());
  250. }
  251. if (binding.m_shaderInputIndex != PassAttachmentBinding::ShaderInputNoBind &&
  252. binding.m_scopeAttachmentUsage != RHI::ScopeAttachmentUsage::RenderTarget &&
  253. binding.m_scopeAttachmentUsage != RHI::ScopeAttachmentUsage::DepthStencil)
  254. {
  255. m_shaderResourceGroup->SetImageView(RHI::ShaderInputImageIndex(inputIndex), imageView, arrayIndex);
  256. ++imageIndex;
  257. }
  258. }
  259. else if (attachment->GetAttachmentType() == RHI::AttachmentType::Buffer)
  260. {
  261. if (binding.m_shaderInputIndex == PassAttachmentBinding::ShaderInputNoBind)
  262. {
  263. return;
  264. }
  265. if (inputIndex == PassAttachmentBinding::ShaderInputAutoBind)
  266. {
  267. inputIndex = bufferIndex;
  268. }
  269. const RHI::BufferView* bufferView = context.GetBufferView(attachment->GetAttachmentId(), binding.m_scopeAttachmentUsage);
  270. m_shaderResourceGroup->SetBufferView(RHI::ShaderInputBufferIndex(inputIndex), bufferView, arrayIndex);
  271. ++bufferIndex;
  272. }
  273. }
  274. }
  275. void RenderPass::BindPassSrg(const RHI::FrameGraphCompileContext& context, [[maybe_unused]] Data::Instance<ShaderResourceGroup>& shaderResourceGroup)
  276. {
  277. AZ_Assert(m_shaderResourceGroup != nullptr, "Passing a null shader resource group to RenderPass::BindPassSrg");
  278. int16_t imageIndex = 0;
  279. int16_t bufferIndex = 0;
  280. // Bind the input attachments to the SRG
  281. for (uint32_t idx = 0; idx < GetInputCount(); ++idx)
  282. {
  283. PassAttachmentBinding& binding = GetInputBinding(idx);
  284. AZ_Assert(binding.m_scopeAttachmentUsage != RHI::ScopeAttachmentUsage::RenderTarget,
  285. "Attachment bindings that are inputs cannot have their type set to 'RenderTarget'. Binding in question is %s on pass %s.",
  286. binding.m_name.GetCStr(),
  287. GetPathName().GetCStr());
  288. BindAttachment(context, binding, imageIndex, bufferIndex);
  289. }
  290. // Bind the input/output attachments to the SRG
  291. for (uint32_t idx = 0; idx < GetInputOutputCount(); ++idx)
  292. {
  293. PassAttachmentBinding& binding = GetInputOutputBinding(idx);
  294. BindAttachment(context, binding, imageIndex, bufferIndex);
  295. }
  296. // Bind the output attachments to the SRG
  297. for (uint32_t idx = 0; idx < GetOutputCount(); ++idx)
  298. {
  299. PassAttachmentBinding& binding = GetOutputBinding(idx);
  300. BindAttachment(context, binding, imageIndex, bufferIndex);
  301. }
  302. }
  303. ViewPtr RenderPass::GetView() const
  304. {
  305. if (m_pipeline)
  306. {
  307. return m_pipeline->GetFirstView(GetPipelineViewTag());
  308. }
  309. return nullptr;
  310. }
  311. void RenderPass::CollectSrgs()
  312. {
  313. // Scene srg
  314. const RHI::ShaderResourceGroup* sceneSrg = m_pipeline->GetScene()->GetRHIShaderResourceGroup();
  315. BindSrg(sceneSrg);
  316. // View srg
  317. if (m_flags.m_bindViewSrg)
  318. {
  319. ViewPtr view = GetView();
  320. if (view)
  321. {
  322. BindSrg(view->GetRHIShaderResourceGroup());
  323. }
  324. }
  325. // Pass srg
  326. if (m_shaderResourceGroup)
  327. {
  328. BindSrg(m_shaderResourceGroup->GetRHIShaderResourceGroup());
  329. }
  330. }
  331. void RenderPass::ResetSrgs()
  332. {
  333. m_shaderResourceGroupsToBind.clear();
  334. }
  335. void RenderPass::BindSrg(const RHI::ShaderResourceGroup* srg)
  336. {
  337. if (srg)
  338. {
  339. m_shaderResourceGroupsToBind[aznumeric_caster(srg->GetBindingSlot())] = srg;
  340. }
  341. }
  342. void RenderPass::SetSrgsForDraw(const RHI::FrameGraphExecuteContext& context)
  343. {
  344. for (auto itr : m_shaderResourceGroupsToBind)
  345. {
  346. context.GetCommandList()->SetShaderResourceGroupForDraw(*(itr.second->GetDeviceShaderResourceGroup(context.GetDeviceIndex())));
  347. }
  348. }
  349. void RenderPass::SetSrgsForDispatch(const RHI::FrameGraphExecuteContext& context)
  350. {
  351. for (auto itr : m_shaderResourceGroupsToBind)
  352. {
  353. context.GetCommandList()->SetShaderResourceGroupForDispatch(
  354. *(itr.second->GetDeviceShaderResourceGroup(context.GetDeviceIndex())));
  355. }
  356. }
  357. void RenderPass::SetPipelineViewTag(const PipelineViewTag& viewTag)
  358. {
  359. if (m_viewTag != viewTag)
  360. {
  361. m_viewTag = viewTag;
  362. if (m_pipeline)
  363. {
  364. m_pipeline->MarkPipelinePassChanges(PipelinePassChanges::PipelineViewTagChanged);
  365. }
  366. }
  367. m_flags.m_bindViewSrg = !viewTag.IsEmpty();
  368. }
  369. TimestampResult RenderPass::GetTimestampResultInternal() const
  370. {
  371. return m_timestampResult;
  372. }
  373. PipelineStatisticsResult RenderPass::GetPipelineStatisticsResultInternal() const
  374. {
  375. return m_statisticsResult;
  376. }
  377. Data::Instance<RPI::ShaderResourceGroup> RenderPass::GetShaderResourceGroup()
  378. {
  379. return m_shaderResourceGroup;
  380. }
  381. RHI::Ptr<Query> RenderPass::GetQuery(ScopeQueryType queryType)
  382. {
  383. uint32_t typeIndex = static_cast<uint32_t>(queryType);
  384. if (!m_scopeQueries[typeIndex])
  385. {
  386. RHI::Ptr<Query> query;
  387. switch (queryType)
  388. {
  389. case ScopeQueryType::Timestamp:
  390. query = GpuQuerySystemInterface::Get()->CreateQuery(
  391. RHI::QueryType::Timestamp, RHI::QueryPoolScopeAttachmentType::Global, RHI::ScopeAttachmentAccess::Write);
  392. break;
  393. case ScopeQueryType::PipelineStatistics:
  394. query = GpuQuerySystemInterface::Get()->CreateQuery(
  395. RHI::QueryType::PipelineStatistics, RHI::QueryPoolScopeAttachmentType::Global, RHI::ScopeAttachmentAccess::Write);
  396. break;
  397. }
  398. m_scopeQueries[typeIndex] = query;
  399. }
  400. return m_scopeQueries[typeIndex];
  401. }
  402. template<typename Func>
  403. inline void RenderPass::ExecuteOnTimestampQuery(Func&& func)
  404. {
  405. if (IsTimestampQueryEnabled())
  406. {
  407. auto query = GetQuery(ScopeQueryType::Timestamp);
  408. if (query)
  409. {
  410. func(query);
  411. }
  412. }
  413. }
  414. template<typename Func>
  415. inline void RenderPass::ExecuteOnPipelineStatisticsQuery(Func&& func)
  416. {
  417. if (IsPipelineStatisticsQueryEnabled())
  418. {
  419. auto query = GetQuery(ScopeQueryType::PipelineStatistics);
  420. if (query)
  421. {
  422. func(query);
  423. }
  424. }
  425. }
  426. void RenderPass::AddScopeQueryToFrameGraph(RHI::FrameGraphInterface frameGraph)
  427. {
  428. const auto addToFrameGraph = [&frameGraph](RHI::Ptr<Query> query)
  429. {
  430. query->AddToFrameGraph(frameGraph);
  431. };
  432. ExecuteOnTimestampQuery(addToFrameGraph);
  433. ExecuteOnPipelineStatisticsQuery(addToFrameGraph);
  434. }
  435. void RenderPass::BeginScopeQuery(const RHI::FrameGraphExecuteContext& context)
  436. {
  437. const auto beginQuery = [&context, this](RHI::Ptr<Query> query)
  438. {
  439. if (query->BeginQuery(context) == QueryResultCode::Fail)
  440. {
  441. AZ_UNUSED(this); // Prevent unused warning in release builds
  442. AZ_WarningOnce("RenderPass", false, "BeginScopeQuery failed. Make sure AddScopeQueryToFrameGraph was called in SetupFrameGraphDependencies"
  443. " for this pass: %s", this->RTTI_GetTypeName());
  444. }
  445. };
  446. if (context.GetCommandListIndex() == 0)
  447. {
  448. ExecuteOnTimestampQuery(beginQuery);
  449. ExecuteOnPipelineStatisticsQuery(beginQuery);
  450. }
  451. }
  452. void RenderPass::EndScopeQuery(const RHI::FrameGraphExecuteContext& context)
  453. {
  454. const auto endQuery = [&context](RHI::Ptr<Query> query)
  455. {
  456. query->EndQuery(context);
  457. };
  458. // This scope query implementation should be replaced by
  459. // [ATOM-5407] [RHI][Core] - Add GPU timestamp and pipeline statistic support for scopes
  460. // For timestamp query, it's okay to execute across different command lists
  461. if (context.GetCommandListIndex() == context.GetCommandListCount() - 1)
  462. {
  463. ExecuteOnTimestampQuery(endQuery);
  464. }
  465. // For all the other types of queries except timestamp, the query start and end has to be in the same command list
  466. // Here only tracks the PipelineStatistics for the first command list due to that we don't know how many queries are
  467. // needed when AddScopeQueryToFrameGraph is called.
  468. // This implementation leads to an issue that we may not get accurate pipeline statistic data
  469. // for passes which were executed with more than one command list
  470. if (context.GetCommandListIndex() == 0)
  471. {
  472. ExecuteOnPipelineStatisticsQuery(endQuery);
  473. }
  474. }
  475. void RenderPass::ReadbackScopeQueryResults()
  476. {
  477. ExecuteOnTimestampQuery([this](RHI::Ptr<Query> query)
  478. {
  479. const uint32_t TimestampResultQueryCount = 2u;
  480. uint64_t timestampResult[TimestampResultQueryCount] = {0};
  481. query->GetLatestResult(&timestampResult, sizeof(uint64_t) * TimestampResultQueryCount, m_lastDeviceIndex);
  482. m_timestampResult = TimestampResult(timestampResult[0], timestampResult[1], RHI::HardwareQueueClass::Graphics);
  483. });
  484. ExecuteOnPipelineStatisticsQuery([this](RHI::Ptr<Query> query)
  485. {
  486. query->GetLatestResult(&m_statisticsResult, sizeof(PipelineStatisticsResult), m_lastDeviceIndex);
  487. });
  488. }
  489. } // namespace RPI
  490. } // namespace AZ