FrameCaptureSystemComponent.cpp 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  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 "FrameCaptureSystemComponent.h"
  9. #include <Atom/RHI/RHIUtils.h>
  10. #include <Atom/RPI.Public/Pass/PassSystemInterface.h>
  11. #include <Atom/RPI.Public/Pass/PassFilter.h>
  12. #include <Atom/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.h>
  13. #include <Atom/RPI.Public/Pass/Specific/SwapChainPass.h>
  14. #include <Atom/RPI.Public/ViewportContextManager.h>
  15. #include <Atom/Utils/DdsFile.h>
  16. #include <Atom/Utils/PpmFile.h>
  17. #include <Atom/Utils/PngFile.h>
  18. #include <Atom/Utils/ImageComparison.h>
  19. #include <AzCore/std/parallel/lock.h>
  20. #include <AzCore/Serialization/Json/JsonUtils.h>
  21. #include <AzCore/Jobs/JobFunction.h>
  22. #include <AzCore/Jobs/JobCompletion.h>
  23. #include <AzCore/IO/SystemFile.h>
  24. #include <AzCore/RTTI/BehaviorContext.h>
  25. #include <AzCore/Script/ScriptContextAttributes.h>
  26. #include <AzCore/Serialization/SerializeContext.h>
  27. #include <AzCore/Task/TaskGraph.h>
  28. #include <AzFramework/IO/LocalFileIO.h>
  29. #include <AzFramework/StringFunc/StringFunc.h>
  30. #include <AzCore/Preprocessor/EnumReflectUtils.h>
  31. #include <AzCore/Console/Console.h>
  32. #include <tiffio.h>
  33. namespace AZ
  34. {
  35. namespace Render
  36. {
  37. AZ_ENUM_DEFINE_REFLECT_UTILITIES(FrameCaptureResult);
  38. void FrameCaptureError::Reflect(ReflectContext* context)
  39. {
  40. if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
  41. {
  42. serializeContext->Class<FrameCaptureError>()
  43. ->Version(1)
  44. ->Field("ErrorMessage", &FrameCaptureError::m_errorMessage);
  45. }
  46. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  47. {
  48. behaviorContext->Class<FrameCaptureError>("FrameCaptureError")
  49. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  50. ->Attribute(AZ::Script::Attributes::Module, "utils")
  51. ->Property("ErrorMessage", BehaviorValueProperty(&FrameCaptureError::m_errorMessage))
  52. ->Attribute(AZ::Script::Attributes::Alias, "error_message");
  53. }
  54. }
  55. void FrameCaptureTestError::Reflect(ReflectContext* context)
  56. {
  57. if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
  58. {
  59. serializeContext->Class<FrameCaptureTestError>()
  60. ->Version(1)
  61. ->Field("ErrorMessage", &FrameCaptureTestError::m_errorMessage);
  62. }
  63. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  64. {
  65. behaviorContext->Class<FrameCaptureTestError>("FrameCaptureTestError")
  66. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  67. ->Attribute(AZ::Script::Attributes::Module, "utils")
  68. ->Property("ErrorMessage", BehaviorValueProperty(&FrameCaptureTestError::m_errorMessage))
  69. ->Attribute(AZ::Script::Attributes::Alias, "error_message");
  70. }
  71. }
  72. AZ_CVAR(unsigned int,
  73. r_pngCompressionLevel,
  74. 3, // A compression level of 3 seems like the best default in terms of file size and saving speeds
  75. nullptr,
  76. ConsoleFunctorFlags::Null,
  77. "Sets the compression level for saving png screenshots. Valid values are from 0 to 8"
  78. );
  79. AZ_CVAR(int,
  80. r_pngCompressionNumThreads,
  81. 8, // Number of threads to use for the png r<->b channel data swap
  82. nullptr,
  83. ConsoleFunctorFlags::Null,
  84. "Sets the number of threads for saving png screenshots. Valid values are from 1 to 128, although less than or equal the number of hw threads is recommended"
  85. );
  86. FrameCaptureOutputResult PngFrameCaptureOutput(
  87. const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  88. {
  89. AZStd::shared_ptr<AZStd::vector<uint8_t>> buffer = readbackResult.m_dataBuffer;
  90. RHI::Format format = readbackResult.m_imageDescriptor.m_format;
  91. // convert bgra to rgba by swapping channels
  92. const int numChannels = AZ::RHI::GetFormatComponentCount(readbackResult.m_imageDescriptor.m_format);
  93. if (format == RHI::Format::B8G8R8A8_UNORM)
  94. {
  95. format = RHI::Format::R8G8B8A8_UNORM;
  96. buffer = AZStd::make_shared<AZStd::vector<uint8_t>>(readbackResult.m_dataBuffer->size());
  97. AZStd::copy(readbackResult.m_dataBuffer->begin(), readbackResult.m_dataBuffer->end(), buffer->begin());
  98. const int numThreads = r_pngCompressionNumThreads;
  99. const int numPixelsPerThread = static_cast<int>(buffer->size() / numChannels / numThreads);
  100. AZ::TaskGraphActiveInterface* taskGraphActiveInterface = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
  101. bool taskGraphActive = taskGraphActiveInterface && taskGraphActiveInterface->IsTaskGraphActive();
  102. if (taskGraphActive)
  103. {
  104. static const AZ::TaskDescriptor pngTaskDescriptor{"PngWriteOutChannelSwap", "Graphics"};
  105. AZ::TaskGraph taskGraph{ "FrameCapturePngWriteOut" };
  106. for (int i = 0; i < numThreads; ++i)
  107. {
  108. int startPixel = i * numPixelsPerThread;
  109. taskGraph.AddTask(
  110. pngTaskDescriptor,
  111. [&, startPixel]()
  112. {
  113. for (int pixelOffset = 0; pixelOffset < numPixelsPerThread; ++pixelOffset)
  114. {
  115. if (startPixel * numChannels + numChannels < buffer->size())
  116. {
  117. AZStd::swap(
  118. buffer->data()[(startPixel + pixelOffset) * numChannels],
  119. buffer->data()[(startPixel + pixelOffset) * numChannels + 2]
  120. );
  121. }
  122. }
  123. });
  124. }
  125. AZ::TaskGraphEvent taskGraphFinishedEvent{ "FrameCapturePngWriteOutWait" };
  126. taskGraph.Submit(&taskGraphFinishedEvent);
  127. taskGraphFinishedEvent.Wait();
  128. }
  129. else
  130. {
  131. AZ::JobCompletion jobCompletion;
  132. for (int i = 0; i < numThreads; ++i)
  133. {
  134. int startPixel = i * numPixelsPerThread;
  135. AZ::Job* job = AZ::CreateJobFunction(
  136. [&, startPixel]()
  137. {
  138. for (int pixelOffset = 0; pixelOffset < numPixelsPerThread; ++pixelOffset)
  139. {
  140. if (startPixel * numChannels + numChannels < buffer->size())
  141. {
  142. AZStd::swap(
  143. buffer->data()[(startPixel + pixelOffset) * numChannels],
  144. buffer->data()[(startPixel + pixelOffset) * numChannels + 2]
  145. );
  146. }
  147. }
  148. }, true, nullptr);
  149. job->SetDependent(&jobCompletion);
  150. job->Start();
  151. }
  152. jobCompletion.StartAndWaitForCompletion();
  153. }
  154. }
  155. Utils::PngFile image = Utils::PngFile::Create(readbackResult.m_imageDescriptor.m_size, format, *buffer);
  156. Utils::PngFile::SaveSettings saveSettings;
  157. if (auto console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
  158. {
  159. console->GetCvarValue("r_pngCompressionLevel", saveSettings.m_compressionLevel);
  160. }
  161. // We should probably strip alpha to save space, especially for automated test screenshots. Alpha is left in to maintain
  162. // prior behavior, changing this is out of scope for the current task. Note, it would have bit of a cascade effect where
  163. // AtomSampleViewer's ScriptReporter assumes an RGBA image.
  164. saveSettings.m_stripAlpha = false;
  165. if(image && image.Save(outputFilePath.c_str(), saveSettings))
  166. {
  167. return FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt};
  168. }
  169. return FrameCaptureOutputResult{FrameCaptureResult::InternalError, "Unable to save frame capture output to '" + outputFilePath + "'"};
  170. }
  171. FrameCaptureOutputResult TiffFrameCaptureOutput(
  172. const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  173. {
  174. AZStd::shared_ptr<AZStd::vector<uint8_t>> buffer = readbackResult.m_dataBuffer;
  175. const uint32_t width = readbackResult.m_imageDescriptor.m_size.m_width;
  176. const uint32_t height = readbackResult.m_imageDescriptor.m_size.m_height;
  177. const uint32_t numChannels = AZ::RHI::GetFormatComponentCount(readbackResult.m_imageDescriptor.m_format);
  178. const uint32_t bytesPerChannel = AZ::RHI::GetFormatSize(readbackResult.m_imageDescriptor.m_format) / numChannels;
  179. const uint32_t bitsPerChannel = bytesPerChannel * 8;
  180. TIFF* out = TIFFOpen(outputFilePath.c_str(), "w");
  181. TIFFSetField(out, TIFFTAG_IMAGEWIDTH, width);
  182. TIFFSetField(out, TIFFTAG_IMAGELENGTH, height);
  183. TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, numChannels);
  184. TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, bitsPerChannel);
  185. TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
  186. TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
  187. TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
  188. TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
  189. TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); // interpret each pixel as a float
  190. size_t pitch = width * numChannels * bytesPerChannel;
  191. AZ_Assert((pitch * height) == buffer->size(), "Image buffer does not match allocated bytes for tiff saving.")
  192. unsigned char* raster = (unsigned char*)_TIFFmalloc((tsize_t)(pitch * height));
  193. memcpy(raster, buffer->data(), pitch * height);
  194. bool success = true;
  195. for (uint32_t h = 0; h < height; ++h)
  196. {
  197. size_t offset = h * pitch;
  198. int err = TIFFWriteScanline(out, raster + offset, h, 0);
  199. if (err < 0)
  200. {
  201. success = false;
  202. break;
  203. }
  204. }
  205. _TIFFfree(raster);
  206. TIFFClose(out);
  207. return success ? FrameCaptureOutputResult{ FrameCaptureResult::Success, AZStd::nullopt }
  208. : FrameCaptureOutputResult{ FrameCaptureResult::InternalError, "Unable to save tif frame capture output to " + outputFilePath };
  209. }
  210. class FrameCaptureNotificationBusHandler final
  211. : public FrameCaptureNotificationBus::MultiHandler // Use multi handler as it has to handle all use cases
  212. , public AZ::BehaviorEBusHandler
  213. {
  214. public:
  215. AZ_EBUS_BEHAVIOR_BINDER(FrameCaptureNotificationBusHandler, "{68D1D94C-7055-4D32-8E22-BEEEBA0940C4}", AZ::SystemAllocator, OnFrameCaptureFinished);
  216. void OnFrameCaptureFinished(FrameCaptureResult result, const AZStd::string& info) override
  217. {
  218. Call(FN_OnFrameCaptureFinished, result, info);
  219. }
  220. static void Reflect(AZ::ReflectContext* context)
  221. {
  222. if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
  223. {
  224. FrameCaptureResultReflect(*serializeContext);
  225. }
  226. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  227. {
  228. //[GFX_TODO][ATOM-13424] Replace this with a utility in AZ_ENUM_DEFINE_REFLECT_UTILITIES
  229. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::None)>("FrameCaptureResult_None")
  230. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  231. ->Attribute(AZ::Script::Attributes::Module, "atom");
  232. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::Success)>("FrameCaptureResult_Success")
  233. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  234. ->Attribute(AZ::Script::Attributes::Module, "atom");
  235. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::FileWriteError)>("FrameCaptureResult_FileWriteError")
  236. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  237. ->Attribute(AZ::Script::Attributes::Module, "atom");
  238. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::InvalidArgument)>("FrameCaptureResult_InvalidArgument")
  239. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  240. ->Attribute(AZ::Script::Attributes::Module, "atom");
  241. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::UnsupportedFormat)>("FrameCaptureResult_UnsupportedFormat")
  242. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  243. ->Attribute(AZ::Script::Attributes::Module, "atom");
  244. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::InternalError)>("FrameCaptureResult_InternalError")
  245. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  246. ->Attribute(AZ::Script::Attributes::Module, "atom");
  247. behaviorContext->EBus<FrameCaptureNotificationBus>("FrameCaptureNotificationBus")
  248. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  249. ->Attribute(AZ::Script::Attributes::Module, "atom")
  250. ->Handler<FrameCaptureNotificationBusHandler>()
  251. ;
  252. }
  253. }
  254. };
  255. void FrameCaptureSystemComponent::Reflect(AZ::ReflectContext* context)
  256. {
  257. FrameCaptureError::Reflect(context);
  258. FrameCaptureTestError::Reflect(context);
  259. Utils::ImageDiffResult::Reflect(context);
  260. FrameCaptureNotificationBusHandler::Reflect(context);
  261. if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
  262. {
  263. serializeContext->Class<FrameCaptureSystemComponent, AZ::Component>()
  264. ->Version(1)
  265. ;
  266. }
  267. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  268. {
  269. behaviorContext->EBus<FrameCaptureRequestBus>("FrameCaptureRequestBus")
  270. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  271. ->Attribute(AZ::Script::Attributes::Module, "atom")
  272. ->Event("CaptureScreenshot", &FrameCaptureRequestBus::Events::CaptureScreenshot)
  273. ->Event("CaptureScreenshotWithPreview", &FrameCaptureRequestBus::Events::CaptureScreenshotWithPreview)
  274. ->Event("CapturePassAttachment", &FrameCaptureRequestBus::Events::CapturePassAttachment)
  275. ;
  276. behaviorContext->EBus<FrameCaptureTestRequestBus>("FrameCaptureTestRequestBus")
  277. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  278. ->Attribute(AZ::Script::Attributes::Module, "atom")
  279. ->Event("SetScreenshotFolder", &FrameCaptureTestRequestBus::Events::SetScreenshotFolder)
  280. ->Event("SetTestEnvPath", &FrameCaptureTestRequestBus::Events::SetTestEnvPath)
  281. ->Event("SetOfficialBaselineImageFolder", &FrameCaptureTestRequestBus::Events::SetOfficialBaselineImageFolder)
  282. ->Event("SetLocalBaselineImageFolder", &FrameCaptureTestRequestBus::Events::SetLocalBaselineImageFolder)
  283. ->Event("BuildScreenshotFilePath", &FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath)
  284. ->Event("BuildOfficialBaselineFilePath", &FrameCaptureTestRequestBus::Events::BuildOfficialBaselineFilePath)
  285. ->Event("BuildLocalBaselineFilePath", &FrameCaptureTestRequestBus::Events::BuildLocalBaselineFilePath)
  286. ->Event("CompareScreenshots", &FrameCaptureTestRequestBus::Events::CompareScreenshots)
  287. ;
  288. }
  289. }
  290. void FrameCaptureSystemComponent::Activate()
  291. {
  292. FrameCaptureRequestBus::Handler::BusConnect();
  293. FrameCaptureTestRequestBus::Handler::BusConnect();
  294. SystemTickBus::Handler::BusConnect();
  295. }
  296. FrameCaptureSystemComponent::CaptureHandle FrameCaptureSystemComponent::InitCapture()
  297. {
  298. if (m_idleCaptures.size())
  299. {
  300. // Use an existing idle capture state
  301. CaptureHandle captureHandle = m_idleCaptures.front();
  302. m_idleCaptures.pop_front();
  303. if (captureHandle.IsNull())
  304. {
  305. AZ_Assert(false, "FrameCaptureSystemComponent found null capture handle in idle list");
  306. return CaptureHandle::Null();
  307. }
  308. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle); // take shared read lock to ensure vector doesn't move while operating on the ptr
  309. CaptureState* capture = captureHandle.GetCaptureState();
  310. if (!capture) // failed to get the capture state ptr, abort
  311. {
  312. return CaptureHandle::Null();
  313. }
  314. capture->Reset();
  315. return captureHandle;
  316. }
  317. else
  318. {
  319. // Create a new CaptureState
  320. AZStd::lock_guard<AZStd::shared_mutex> lock(m_handleLock); // take exclusive write lock as we may move CaptureState locations in memory
  321. uint32_t captureIndex = aznumeric_cast<uint32_t>(m_allCaptures.size());
  322. m_allCaptures.emplace_back(captureIndex);
  323. return CaptureHandle(this, captureIndex);
  324. }
  325. }
  326. void FrameCaptureSystemComponent::Deactivate()
  327. {
  328. FrameCaptureRequestBus::Handler::BusDisconnect();
  329. FrameCaptureTestRequestBus::Handler::BusDisconnect();
  330. SystemTickBus::Handler::BusDisconnect();
  331. m_idleCaptures.clear();
  332. m_inProgressCaptures.clear();
  333. m_allCaptures.clear();
  334. }
  335. AZStd::string FrameCaptureSystemComponent::ResolvePath(const AZStd::string& filePath)
  336. {
  337. AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetDirectInstance();
  338. char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
  339. fileIO->ResolvePath(filePath.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
  340. return AZStd::string(resolvedPath);
  341. }
  342. bool FrameCaptureSystemComponent::CanCapture() const
  343. {
  344. return !AZ::RHI::IsNullRHI();
  345. }
  346. AZ::Outcome<FrameCaptureSystemComponent::CaptureHandle, FrameCaptureError> FrameCaptureSystemComponent::ScreenshotPreparation(
  347. const AZStd::string& imagePath,
  348. AZ::RPI::AttachmentReadback::CallbackFunction callbackFunction)
  349. {
  350. FrameCaptureError error;
  351. if (!CanCapture())
  352. {
  353. error.m_errorMessage = "Frame capture not availble.";
  354. return AZ::Failure(error);
  355. }
  356. if (imagePath.empty() && callbackFunction == nullptr)
  357. {
  358. error.m_errorMessage = "No callback or image path is set. No result will be generated.";
  359. return AZ::Failure(error);
  360. }
  361. AZ_Warning(
  362. "FrameCaptureSystemComponent",
  363. imagePath.empty() || callbackFunction == nullptr,
  364. "Callback and image path are both set. Image path will be ignored.");
  365. CaptureHandle captureHandle = InitCapture();
  366. if (captureHandle.IsNull())
  367. {
  368. error.m_errorMessage = "Failed to allocate a capture.";
  369. return AZ::Failure(error);
  370. }
  371. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  372. CaptureState* capture = captureHandle.GetCaptureState();
  373. if (!capture) // failed to get the capture state ptr, abort
  374. {
  375. error.m_errorMessage = "Failed to get the captureState.";
  376. m_idleCaptures.push_back(captureHandle);
  377. return AZ::Failure(error);
  378. }
  379. if (!capture->m_readback->IsReady())
  380. {
  381. error.m_errorMessage = "Failed to capture attachment since the readback is not ready.";
  382. m_idleCaptures.push_back(captureHandle);
  383. return AZ::Failure(error);
  384. }
  385. capture->m_readback->SetUserIdentifier(captureHandle.GetCaptureStateIndex());
  386. if (callbackFunction != nullptr)
  387. {
  388. capture->m_readback->SetCallback(callbackFunction);
  389. }
  390. else
  391. {
  392. capture->m_readback->SetCallback(
  393. AZStd::bind(&FrameCaptureSystemComponent::CaptureAttachmentCallback, this, AZStd::placeholders::_1));
  394. AZ_Assert(!imagePath.empty(), "The image path must be provided if the callback is not assigned.");
  395. capture->m_outputFilePath = ResolvePath(imagePath);
  396. }
  397. return AZ::Success(captureHandle);
  398. }
  399. FrameCaptureOutcome FrameCaptureSystemComponent::CaptureScreenshotForWindow(const AZStd::string& filePath, AzFramework::NativeWindowHandle windowHandle)
  400. {
  401. return InternalCaptureScreenshot(filePath, windowHandle);
  402. }
  403. FrameCaptureOutcome FrameCaptureSystemComponent::CaptureScreenshot(const AZStd::string& filePath)
  404. {
  405. FrameCaptureError error;
  406. AzFramework::NativeWindowHandle windowHandle = AZ::RPI::ViewportContextRequests::Get()->GetDefaultViewportContext()->GetWindowHandle();
  407. return InternalCaptureScreenshot(filePath, windowHandle);
  408. }
  409. FrameCaptureOutcome FrameCaptureSystemComponent::CaptureScreenshotWithPreview(const AZStd::string& outputFilePath)
  410. {
  411. FrameCaptureError error;
  412. RPI::PassFilter passFilter = RPI::PassFilter::CreateWithPassClass<RPI::ImageAttachmentPreviewPass>();
  413. AZ::RPI::ImageAttachmentPreviewPass* previewPass = nullptr;
  414. AZ::RPI::PassSystemInterface::Get()->ForEachPass(
  415. passFilter,
  416. [&previewPass](AZ::RPI::Pass* pass) -> AZ::RPI::PassFilterExecutionFlow
  417. {
  418. if (pass->GetParent() != nullptr && pass->IsEnabled())
  419. {
  420. previewPass = azrtti_cast<AZ::RPI::ImageAttachmentPreviewPass*>(pass);
  421. return AZ::RPI::PassFilterExecutionFlow::StopVisitingPasses;
  422. }
  423. return AZ::RPI::PassFilterExecutionFlow::ContinueVisitingPasses;
  424. });
  425. if (!previewPass)
  426. {
  427. error.m_errorMessage = "Failed to find an ImageAttachmentPreviewPass.";
  428. return AZ::Failure(error);
  429. }
  430. auto prepOutcome = ScreenshotPreparation(outputFilePath, nullptr);
  431. if (!prepOutcome.IsSuccess())
  432. {
  433. return AZ::Failure(prepOutcome.TakeError());
  434. }
  435. CaptureHandle captureHandle = prepOutcome.GetValue();
  436. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  437. CaptureState* captureState = captureHandle.GetCaptureState();
  438. if (!previewPass->ReadbackOutput(captureState->m_readback))
  439. {
  440. error.m_errorMessage = "Failed to readback output from the ImageAttachmentPreviewPass";
  441. m_idleCaptures.push_back(captureHandle);
  442. return AZ::Failure(error);
  443. }
  444. m_inProgressCaptures.push_back(captureHandle);
  445. FrameCaptureId frameId = captureHandle.GetCaptureStateIndex();
  446. return AZ::Success(frameId);
  447. }
  448. FrameCaptureOutcome FrameCaptureSystemComponent::InternalCaptureScreenshot(
  449. const AZStd::string& imagePath, AzFramework::NativeWindowHandle windowHandle)
  450. {
  451. FrameCaptureError error;
  452. if (!windowHandle)
  453. {
  454. error.m_errorMessage = "No valid window for the capture.";
  455. return AZ::Failure(error);
  456. }
  457. // Find SwapChainPass for the window handle
  458. RPI::SwapChainPass* pass = AZ::RPI::PassSystemInterface::Get()->FindSwapChainPass(windowHandle);
  459. if (!pass)
  460. {
  461. error.m_errorMessage = "Failed to find SwapChainPass for the window.";
  462. return AZ::Failure(error);
  463. }
  464. auto prepOutcome = ScreenshotPreparation(imagePath, nullptr);
  465. if (!prepOutcome.IsSuccess())
  466. {
  467. return AZ::Failure(prepOutcome.GetError());
  468. }
  469. CaptureHandle captureHandle = prepOutcome.GetValue();
  470. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  471. CaptureState* captureState = captureHandle.GetCaptureState();
  472. AZ_Assert(captureState, "ScreenshotPreparation should have created a ready capture state "
  473. "if the capture handle is valid.");
  474. pass->ReadbackSwapChain(captureState->m_readback);
  475. m_inProgressCaptures.push_back(captureHandle);
  476. FrameCaptureId frameId = captureHandle.GetCaptureStateIndex();
  477. return AZ::Success(frameId);
  478. }
  479. FrameCaptureOutcome FrameCaptureSystemComponent::InternalCapturePassAttachment(
  480. const AZStd::string& outputFilePath,
  481. AZ::RPI::AttachmentReadback::CallbackFunction callbackFunction,
  482. const AZStd::vector<AZStd::string>& passHierarchy,
  483. const AZStd::string& slot,
  484. RPI::PassAttachmentReadbackOption option)
  485. {
  486. FrameCaptureError error;
  487. if (passHierarchy.size() == 0)
  488. {
  489. error.m_errorMessage = "Empty data in passHierarchy.";
  490. return AZ::Failure(error);
  491. }
  492. RPI::PassFilter passFilter = RPI::PassFilter::CreateWithPassHierarchy(passHierarchy);
  493. RPI::Pass* pass = RPI::PassSystemInterface::Get()->FindFirstPass(passFilter);
  494. if (!pass)
  495. {
  496. error.m_errorMessage = AZStd::string::format("Failed to find pass from %s", passHierarchy[0].c_str());
  497. return AZ::Failure(error);
  498. }
  499. auto prepOutcome = ScreenshotPreparation(outputFilePath, callbackFunction);
  500. if (!prepOutcome.IsSuccess())
  501. {
  502. return AZ::Failure(prepOutcome.GetError());
  503. }
  504. CaptureHandle captureHandle = prepOutcome.GetValue();
  505. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  506. CaptureState* captureState = captureHandle.GetCaptureState();
  507. AZ_Assert(captureState, "ScreenshotPreparation should have created a ready capture state "
  508. "if the capture handle is valid.");
  509. if (!pass->ReadbackAttachment(captureState->m_readback, captureHandle.GetCaptureStateIndex(), Name(slot), option))
  510. {
  511. error.m_errorMessage = AZStd::string::format(
  512. "Failed to readback the attachment bound to pass [%s] slot [%s]", pass->GetName().GetCStr(), slot.c_str());
  513. m_idleCaptures.push_back(captureHandle);
  514. return AZ::Failure(error);
  515. }
  516. m_inProgressCaptures.push_back(captureHandle);
  517. FrameCaptureId frameId = captureHandle.GetCaptureStateIndex();
  518. return AZ::Success(frameId);
  519. }
  520. FrameCaptureOutcome FrameCaptureSystemComponent::CapturePassAttachment(
  521. const AZStd::string& imagePath,
  522. const AZStd::vector<AZStd::string>& passHierarchy,
  523. const AZStd::string& slot,
  524. RPI::PassAttachmentReadbackOption option)
  525. {
  526. return InternalCapturePassAttachment(
  527. imagePath,
  528. nullptr,
  529. passHierarchy,
  530. slot,
  531. option);
  532. }
  533. FrameCaptureOutcome FrameCaptureSystemComponent::CapturePassAttachmentWithCallback(
  534. RPI::AttachmentReadback::CallbackFunction callback,
  535. const AZStd::vector<AZStd::string>& passHierarchy,
  536. const AZStd::string& slotName,
  537. RPI::PassAttachmentReadbackOption option)
  538. {
  539. auto captureCallback = [this, callback](const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  540. {
  541. CaptureHandle captureHandle(this, readbackResult.m_userIdentifier);
  542. callback(readbackResult); // call user supplied callback function
  543. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  544. CaptureState* captureState = captureHandle.GetCaptureState();
  545. AZ_Assert(captureState && captureState->m_result == FrameCaptureResult::None, "Unexpected value for m_result");
  546. captureState->m_result = FrameCaptureResult::Success; // just need to mark this capture as complete, callback handles the actual processing
  547. };
  548. return InternalCapturePassAttachment("", captureCallback, passHierarchy, slotName, option);
  549. }
  550. void FrameCaptureSystemComponent::OnSystemTick()
  551. {
  552. // inProgressCaptures is in capture submit order, loop over the captures until we find an unfinished one.
  553. // This ensures that OnCaptureFinished is signalled in submission order
  554. while (m_inProgressCaptures.size())
  555. {
  556. CaptureHandle captureHandle(m_inProgressCaptures.front());
  557. if (captureHandle.IsNull())
  558. {
  559. // if we find a null handle, remove it from the list
  560. m_inProgressCaptures.pop_front();
  561. continue;
  562. }
  563. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  564. CaptureState* capture = captureHandle.GetCaptureState();
  565. if (capture->m_result == FrameCaptureResult::None)
  566. {
  567. break;
  568. }
  569. FrameCaptureNotificationBus::Event(captureHandle.GetCaptureStateIndex(), &FrameCaptureNotificationBus::Events::OnFrameCaptureFinished, capture->m_result, capture->m_latestCaptureInfo.c_str());
  570. m_inProgressCaptures.pop_front();
  571. m_idleCaptures.push_back(captureHandle);
  572. }
  573. }
  574. void FrameCaptureSystemComponent::CaptureAttachmentCallback(const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  575. {
  576. CaptureHandle captureHandle(this, readbackResult.m_userIdentifier);
  577. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  578. CaptureState* capture = captureHandle.GetCaptureState();
  579. AZ_Assert(capture && capture->m_result == FrameCaptureResult::None, "Unexpected value for m_result");
  580. capture->m_latestCaptureInfo = capture->m_outputFilePath;
  581. if (readbackResult.m_state == AZ::RPI::AttachmentReadback::ReadbackState::Success)
  582. {
  583. if (readbackResult.m_attachmentType == AZ::RHI::AttachmentType::Buffer)
  584. {
  585. // write buffer data to the data file
  586. AZ::IO::FileIOStream fileStream(capture->m_outputFilePath.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath);
  587. if (fileStream.IsOpen())
  588. {
  589. fileStream.Write(readbackResult.m_dataBuffer->size(), readbackResult.m_dataBuffer->data());
  590. capture->m_result = FrameCaptureResult::Success;
  591. }
  592. else
  593. {
  594. capture->m_latestCaptureInfo = AZStd::string::format("Failed to open file %s for writing", capture->m_outputFilePath.c_str());
  595. capture->m_result = FrameCaptureResult::FileWriteError;
  596. }
  597. }
  598. else if (readbackResult.m_attachmentType == AZ::RHI::AttachmentType::Image)
  599. {
  600. AZStd::string extension;
  601. AzFramework::StringFunc::Path::GetExtension(capture->m_outputFilePath.c_str(), extension, false);
  602. AZStd::to_lower(extension.begin(), extension.end());
  603. if (extension == "ppm")
  604. {
  605. if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
  606. readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM)
  607. {
  608. const auto ppmFrameCapture = PpmFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  609. capture->m_result = ppmFrameCapture.m_result;
  610. capture->m_latestCaptureInfo = ppmFrameCapture.m_errorMessage.value_or("");
  611. }
  612. else
  613. {
  614. capture->m_latestCaptureInfo = AZStd::string::format(
  615. "Can't save image with format %s to a ppm file", RHI::ToString(readbackResult.m_imageDescriptor.m_format));
  616. capture->m_result = FrameCaptureResult::UnsupportedFormat;
  617. }
  618. }
  619. else if (extension == "dds")
  620. {
  621. const auto ddsFrameCapture = DdsFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  622. capture->m_result = ddsFrameCapture.m_result;
  623. capture->m_latestCaptureInfo = ddsFrameCapture.m_errorMessage.value_or("");
  624. }
  625. else if (extension == "tiff" || extension == "tif")
  626. {
  627. const auto tifFrameCapture = TiffFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  628. capture->m_result = tifFrameCapture.m_result;
  629. capture->m_latestCaptureInfo = tifFrameCapture.m_errorMessage.value_or("");
  630. }
  631. else if (extension == "png")
  632. {
  633. if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
  634. readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM)
  635. {
  636. AZStd::string folderPath;
  637. AzFramework::StringFunc::Path::GetFolderPath(capture->m_outputFilePath.c_str(), folderPath);
  638. AZ::IO::SystemFile::CreateDir(folderPath.c_str());
  639. const auto frameCaptureResult = PngFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  640. capture->m_result = frameCaptureResult.m_result;
  641. capture->m_latestCaptureInfo = frameCaptureResult.m_errorMessage.value_or("");
  642. }
  643. else
  644. {
  645. capture->m_latestCaptureInfo = AZStd::string::format(
  646. "Can't save image with format %s to a png file", RHI::ToString(readbackResult.m_imageDescriptor.m_format));
  647. capture->m_result = FrameCaptureResult::UnsupportedFormat;
  648. }
  649. }
  650. else
  651. {
  652. capture->m_latestCaptureInfo = AZStd::string::format("Only supports saving image to ppm or dds files");
  653. capture->m_result = FrameCaptureResult::InvalidArgument;
  654. }
  655. }
  656. }
  657. else
  658. {
  659. capture->m_latestCaptureInfo = AZStd::string::format("Failed to read back attachment [%s]", readbackResult.m_name.GetCStr());
  660. capture->m_result = FrameCaptureResult::InternalError;
  661. }
  662. if (capture->m_result == FrameCaptureResult::Success)
  663. {
  664. // Normalize the path so the slashes will be in the right direction for the local platform allowing easy copy/paste into file browsers.
  665. AZStd::string normalizedPath = capture->m_outputFilePath;
  666. AzFramework::StringFunc::Path::Normalize(normalizedPath);
  667. AZ_Printf("FrameCaptureSystemComponent", "Attachment [%s] was saved to file %s\n", readbackResult.m_name.GetCStr(), normalizedPath.c_str());
  668. }
  669. else
  670. {
  671. AZ_Warning("FrameCaptureSystemComponent", false, "%s", capture->m_latestCaptureInfo.c_str());
  672. }
  673. }
  674. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  675. // CaptureHandle implementation
  676. FrameCaptureSystemComponent::CaptureHandle::CaptureHandle(FrameCaptureSystemComponent* frameCaptureSystemComponent, uint32_t captureStateIndex)
  677. : m_frameCaptureSystemComponent(frameCaptureSystemComponent)
  678. , m_captureStateIndex(captureStateIndex)
  679. {
  680. }
  681. FrameCaptureSystemComponent::CaptureHandle FrameCaptureSystemComponent::CaptureHandle::Null()
  682. {
  683. return CaptureHandle(nullptr, InvalidCaptureHandle);
  684. }
  685. void FrameCaptureSystemComponent::CaptureHandle::lock()
  686. {
  687. AZ_Assert(IsValid() && m_frameCaptureSystemComponent != nullptr, "FrameCaptureSystemComponent attempting to lock an invalid handle");
  688. m_frameCaptureSystemComponent->m_handleLock.lock_shared();
  689. }
  690. void FrameCaptureSystemComponent::CaptureHandle::unlock()
  691. {
  692. AZ_Assert(IsValid() && m_frameCaptureSystemComponent != nullptr, "FrameCaptureSystemComponent attempting to unlock an invalid handle");
  693. m_frameCaptureSystemComponent->m_handleLock.unlock_shared();
  694. }
  695. FrameCaptureSystemComponent::CaptureState* FrameCaptureSystemComponent::CaptureHandle::GetCaptureState()
  696. {
  697. AZ_Assert(IsValid() && m_frameCaptureSystemComponent != nullptr, "FrameCaptureSystemComponent GetCaptureState called on an invalid handle");
  698. if (IsNull() || m_frameCaptureSystemComponent == nullptr)
  699. {
  700. return nullptr;
  701. }
  702. // Ideally we could check the state of the handle lock here to check that a shared lock is being held.
  703. // Nearest available check is can we try an exclusive lock,
  704. // this will also fail if someone else is holding the exclusive lock though.
  705. if(m_frameCaptureSystemComponent->m_handleLock.try_lock())
  706. {
  707. AZ_Assert(false, "FrameCaptureSystemComponent::CaptureHandle::GetCaptureState called without holding a read lock");
  708. m_frameCaptureSystemComponent->m_handleLock.unlock();
  709. return nullptr;
  710. }
  711. size_t captureIdx = aznumeric_cast<size_t>(m_captureStateIndex);
  712. if (captureIdx < m_frameCaptureSystemComponent->m_allCaptures.size())
  713. {
  714. return &m_frameCaptureSystemComponent->m_allCaptures[captureIdx];
  715. }
  716. return nullptr;
  717. }
  718. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  719. // CaptureState implementation
  720. FrameCaptureSystemComponent::CaptureState::CaptureState(uint32_t captureIndex)
  721. {
  722. AZStd::fixed_string<128> scope_name = AZStd::fixed_string<128>::format("FrameCapture_%d", captureIndex);
  723. m_readback = AZStd::make_shared<AZ::RPI::AttachmentReadback>(AZ::RHI::ScopeId{ scope_name });
  724. AZ_Assert(m_readback, "Failed to allocate an AttachmentReadback for the capture state");
  725. }
  726. FrameCaptureSystemComponent::CaptureState::CaptureState(CaptureState&& other)
  727. : m_readback(AZStd::move(other.m_readback))
  728. , m_outputFilePath(AZStd::move(other.m_outputFilePath))
  729. , m_latestCaptureInfo(AZStd::move(other.m_latestCaptureInfo))
  730. {
  731. // atomic doesn't support move or copy construction, or direct assignment.
  732. // This function is only used during m_allCaptures resize due to CaptureState addition
  733. // and the m_handleLock is exclusively locked during that operation.
  734. // Manually copy the atomic value to work around the other issues.
  735. FrameCaptureResult result = other.m_result;
  736. m_result = result;
  737. }
  738. void FrameCaptureSystemComponent::CaptureState::Reset()
  739. {
  740. //m_readback->Reset();
  741. m_outputFilePath.clear();
  742. m_latestCaptureInfo.clear();
  743. m_result = FrameCaptureResult::None;
  744. }
  745. void FrameCaptureSystemComponent::SetScreenshotFolder(const AZStd::string& screenshotFolder)
  746. {
  747. m_screenshotFolder = ResolvePath(screenshotFolder);
  748. }
  749. void FrameCaptureSystemComponent::SetTestEnvPath(const AZStd::string& envPath)
  750. {
  751. m_testEnvPath = envPath;
  752. }
  753. void FrameCaptureSystemComponent::SetOfficialBaselineImageFolder(const AZStd::string& baselineFolder)
  754. {
  755. m_officialBaselineImageFolder = ResolvePath(baselineFolder);
  756. }
  757. void FrameCaptureSystemComponent::SetLocalBaselineImageFolder(const AZStd::string& baselineFolder)
  758. {
  759. m_localBaselineImageFolder = ResolvePath(baselineFolder);
  760. }
  761. FrameCapturePathOutcome FrameCaptureSystemComponent::BuildScreenshotFilePath(const AZStd::string& imageName, bool useEnvPath)
  762. {
  763. AZStd::string imagePath = useEnvPath
  764. ? ResolvePath(AZStd::string::format("%s/%s/%s", m_screenshotFolder.c_str(), m_testEnvPath.c_str(), imageName.c_str()))
  765. : ResolvePath(AZStd::string::format("%s/%s", m_screenshotFolder.c_str(), imageName.c_str()));
  766. if (imagePath.size())
  767. {
  768. return AZ::Success(imagePath);
  769. }
  770. else
  771. {
  772. FrameCaptureTestError error;
  773. error.m_errorMessage = "Failed to build image path.";
  774. return AZ::Failure(error);
  775. }
  776. }
  777. FrameCapturePathOutcome FrameCaptureSystemComponent::BuildOfficialBaselineFilePath(const AZStd::string& imageName, bool useEnvPath)
  778. {
  779. AZStd::string imagePath = useEnvPath
  780. ? ResolvePath(AZStd::string::format("%s/%s/%s", m_officialBaselineImageFolder.c_str(), m_testEnvPath.c_str(), imageName.c_str()))
  781. : ResolvePath(AZStd::string::format("%s/%s", m_officialBaselineImageFolder.c_str(), imageName.c_str()));
  782. if (imagePath.size())
  783. {
  784. return AZ::Success(imagePath);
  785. }
  786. else
  787. {
  788. FrameCaptureTestError error;
  789. error.m_errorMessage = "Failed to build image path.";
  790. return AZ::Failure(error);
  791. }
  792. }
  793. FrameCapturePathOutcome FrameCaptureSystemComponent::BuildLocalBaselineFilePath(const AZStd::string& imageName, bool useEnvPath)
  794. {
  795. AZStd::string imagePath = useEnvPath
  796. ? ResolvePath(AZStd::string::format("%s/%s/%s", m_localBaselineImageFolder.c_str(), m_testEnvPath.c_str(), imageName.c_str()))
  797. : ResolvePath(AZStd::string::format("%s/%s", m_localBaselineImageFolder.c_str(), imageName.c_str()));
  798. if (imagePath.size())
  799. {
  800. return AZ::Success(imagePath);
  801. }
  802. else
  803. {
  804. FrameCaptureTestError error;
  805. error.m_errorMessage = "Failed to build image path.";
  806. return AZ::Failure(error);
  807. }
  808. }
  809. FrameCaptureComparisonOutcome FrameCaptureSystemComponent::CompareScreenshots(
  810. const AZStd::string& filePathA, const AZStd::string& filePathB, float minDiffFilter)
  811. {
  812. FrameCaptureTestError error;
  813. char resolvedFilePathA[AZ_MAX_PATH_LEN] = { 0 };
  814. char resolvedFilePathB[AZ_MAX_PATH_LEN] = { 0 };
  815. AZ::IO::FileIOBase::GetInstance()->ResolvePath(filePathA.c_str(), resolvedFilePathA, AZ_MAX_PATH_LEN);
  816. AZ::IO::FileIOBase::GetInstance()->ResolvePath(filePathB.c_str(), resolvedFilePathB, AZ_MAX_PATH_LEN);
  817. if (!filePathA.ends_with(".png") || !filePathB.ends_with(".png"))
  818. {
  819. error.m_errorMessage = "Image comparison only supports png files for now.";
  820. return AZ::Failure(error);
  821. }
  822. // Load image A
  823. Utils::PngFile imageA = Utils::PngFile::Load(resolvedFilePathA);
  824. if (!imageA.IsValid())
  825. {
  826. error.m_errorMessage = AZStd::string::format("Failed to load image file: %s.", resolvedFilePathA);
  827. return AZ::Failure(error);
  828. }
  829. else if (imageA.GetBufferFormat() != Utils::PngFile::Format::RGBA)
  830. {
  831. error.m_errorMessage = AZStd::string::format("Image comparison only supports 8-bit RGBA png. %s is not.", resolvedFilePathA);
  832. return AZ::Failure(error);
  833. }
  834. // Load image B
  835. Utils::PngFile imageB = Utils::PngFile::Load(resolvedFilePathB);
  836. if (!imageB.IsValid())
  837. {
  838. error.m_errorMessage = AZStd::string::format("Failed to load image file: %s.", resolvedFilePathB);
  839. return AZ::Failure(error);
  840. }
  841. else if (imageA.GetBufferFormat() != Utils::PngFile::Format::RGBA)
  842. {
  843. error.m_errorMessage = AZStd::string::format("Image comparison only supports 8-bit RGBA png. %s is not.", resolvedFilePathB);
  844. return AZ::Failure(error);
  845. }
  846. // Compare
  847. auto compOutcome = Utils::CalcImageDiffRms(
  848. imageA.GetBuffer(), RHI::Size(imageA.GetWidth(), imageA.GetHeight(), 1), AZ::RHI::Format::R8G8B8A8_UNORM,
  849. imageB.GetBuffer(), RHI::Size(imageB.GetWidth(), imageB.GetHeight(), 1), AZ::RHI::Format::R8G8B8A8_UNORM,
  850. minDiffFilter
  851. );
  852. if (!compOutcome.IsSuccess())
  853. {
  854. error.m_errorMessage = compOutcome.GetError().m_errorMessage;
  855. return AZ::Failure(error);
  856. }
  857. return AZ::Success(compOutcome.TakeValue());
  858. }
  859. }
  860. }