3
0

FrameCaptureSystemComponent.cpp 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  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. FrameCaptureOutputResult DdsFrameCaptureOutput(
  211. const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  212. {
  213. // write the read back result of the image attachment to a dds file
  214. const auto outcome = AZ::DdsFile::WriteFile(
  215. outputFilePath,
  216. {readbackResult.m_imageDescriptor.m_size, readbackResult.m_imageDescriptor.m_format, readbackResult.m_dataBuffer.get()});
  217. return outcome.IsSuccess() ? FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt}
  218. : FrameCaptureOutputResult{FrameCaptureResult::InternalError, outcome.GetError().m_message};
  219. }
  220. FrameCaptureOutputResult PpmFrameCaptureOutput(
  221. const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  222. {
  223. // write the read back result of the image attachment to a buffer
  224. const AZStd::vector<uint8_t> outBuffer = Utils::PpmFile::CreatePpmFromImageBuffer(
  225. *readbackResult.m_dataBuffer.get(), readbackResult.m_imageDescriptor.m_size, readbackResult.m_imageDescriptor.m_format);
  226. // write the buffer to a ppm file
  227. if (IO::FileIOStream fileStream(outputFilePath.c_str(), IO::OpenMode::ModeWrite | IO::OpenMode::ModeCreatePath);
  228. fileStream.IsOpen())
  229. {
  230. fileStream.Write(outBuffer.size(), outBuffer.data());
  231. fileStream.Close();
  232. return FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt};
  233. }
  234. return FrameCaptureOutputResult{
  235. FrameCaptureResult::FileWriteError,
  236. AZStd::string::format("Failed to open file %s for writing", outputFilePath.c_str())};
  237. }
  238. class FrameCaptureNotificationBusHandler final
  239. : public FrameCaptureNotificationBus::MultiHandler // Use multi handler as it has to handle all use cases
  240. , public AZ::BehaviorEBusHandler
  241. {
  242. public:
  243. AZ_EBUS_BEHAVIOR_BINDER(FrameCaptureNotificationBusHandler, "{68D1D94C-7055-4D32-8E22-BEEEBA0940C4}", AZ::SystemAllocator, OnFrameCaptureFinished);
  244. void OnFrameCaptureFinished(FrameCaptureResult result, const AZStd::string& info) override
  245. {
  246. Call(FN_OnFrameCaptureFinished, result, info);
  247. }
  248. static void Reflect(AZ::ReflectContext* context)
  249. {
  250. if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
  251. {
  252. FrameCaptureResultReflect(*serializeContext);
  253. }
  254. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  255. {
  256. //[GFX_TODO][ATOM-13424] Replace this with a utility in AZ_ENUM_DEFINE_REFLECT_UTILITIES
  257. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::None)>("FrameCaptureResult_None")
  258. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  259. ->Attribute(AZ::Script::Attributes::Module, "atom");
  260. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::Success)>("FrameCaptureResult_Success")
  261. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  262. ->Attribute(AZ::Script::Attributes::Module, "atom");
  263. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::FileWriteError)>("FrameCaptureResult_FileWriteError")
  264. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  265. ->Attribute(AZ::Script::Attributes::Module, "atom");
  266. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::InvalidArgument)>("FrameCaptureResult_InvalidArgument")
  267. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  268. ->Attribute(AZ::Script::Attributes::Module, "atom");
  269. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::UnsupportedFormat)>("FrameCaptureResult_UnsupportedFormat")
  270. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  271. ->Attribute(AZ::Script::Attributes::Module, "atom");
  272. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::InternalError)>("FrameCaptureResult_InternalError")
  273. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  274. ->Attribute(AZ::Script::Attributes::Module, "atom");
  275. behaviorContext->EBus<FrameCaptureNotificationBus>("FrameCaptureNotificationBus")
  276. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  277. ->Attribute(AZ::Script::Attributes::Module, "atom")
  278. ->Handler<FrameCaptureNotificationBusHandler>()
  279. ;
  280. }
  281. }
  282. };
  283. void FrameCaptureSystemComponent::Reflect(AZ::ReflectContext* context)
  284. {
  285. FrameCaptureError::Reflect(context);
  286. FrameCaptureTestError::Reflect(context);
  287. Utils::ImageDiffResult::Reflect(context);
  288. FrameCaptureNotificationBusHandler::Reflect(context);
  289. if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
  290. {
  291. serializeContext->Class<FrameCaptureSystemComponent, AZ::Component>()
  292. ->Version(1)
  293. ;
  294. }
  295. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  296. {
  297. behaviorContext->EBus<FrameCaptureRequestBus>("FrameCaptureRequestBus")
  298. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  299. ->Attribute(AZ::Script::Attributes::Module, "atom")
  300. ->Event("CaptureScreenshot", &FrameCaptureRequestBus::Events::CaptureScreenshot)
  301. ->Event("CaptureScreenshotWithPreview", &FrameCaptureRequestBus::Events::CaptureScreenshotWithPreview)
  302. ->Event("CapturePassAttachment", &FrameCaptureRequestBus::Events::CapturePassAttachment)
  303. ;
  304. behaviorContext->EBus<FrameCaptureTestRequestBus>("FrameCaptureTestRequestBus")
  305. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  306. ->Attribute(AZ::Script::Attributes::Module, "atom")
  307. ->Event("SetScreenshotFolder", &FrameCaptureTestRequestBus::Events::SetScreenshotFolder)
  308. ->Event("SetTestEnvPath", &FrameCaptureTestRequestBus::Events::SetTestEnvPath)
  309. ->Event("SetOfficialBaselineImageFolder", &FrameCaptureTestRequestBus::Events::SetOfficialBaselineImageFolder)
  310. ->Event("SetLocalBaselineImageFolder", &FrameCaptureTestRequestBus::Events::SetLocalBaselineImageFolder)
  311. ->Event("BuildScreenshotFilePath", &FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath)
  312. ->Event("BuildOfficialBaselineFilePath", &FrameCaptureTestRequestBus::Events::BuildOfficialBaselineFilePath)
  313. ->Event("BuildLocalBaselineFilePath", &FrameCaptureTestRequestBus::Events::BuildLocalBaselineFilePath)
  314. ->Event("CompareScreenshots", &FrameCaptureTestRequestBus::Events::CompareScreenshots)
  315. ;
  316. }
  317. }
  318. void FrameCaptureSystemComponent::Activate()
  319. {
  320. FrameCaptureRequestBus::Handler::BusConnect();
  321. FrameCaptureTestRequestBus::Handler::BusConnect();
  322. SystemTickBus::Handler::BusConnect();
  323. }
  324. FrameCaptureSystemComponent::CaptureHandle FrameCaptureSystemComponent::InitCapture()
  325. {
  326. if (m_idleCaptures.size())
  327. {
  328. // Use an existing idle capture state
  329. CaptureHandle captureHandle = m_idleCaptures.front();
  330. m_idleCaptures.pop_front();
  331. if (captureHandle.IsNull())
  332. {
  333. AZ_Assert(false, "FrameCaptureSystemComponent found null capture handle in idle list");
  334. return CaptureHandle::Null();
  335. }
  336. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle); // take shared read lock to ensure vector doesn't move while operating on the ptr
  337. CaptureState* capture = captureHandle.GetCaptureState();
  338. if (!capture) // failed to get the capture state ptr, abort
  339. {
  340. return CaptureHandle::Null();
  341. }
  342. capture->Reset();
  343. return captureHandle;
  344. }
  345. else
  346. {
  347. // Create a new CaptureState
  348. AZStd::lock_guard<AZStd::shared_mutex> lock(m_handleLock); // take exclusive write lock as we may move CaptureState locations in memory
  349. uint32_t captureIndex = aznumeric_cast<uint32_t>(m_allCaptures.size());
  350. m_allCaptures.emplace_back(captureIndex);
  351. return CaptureHandle(this, captureIndex);
  352. }
  353. }
  354. void FrameCaptureSystemComponent::Deactivate()
  355. {
  356. FrameCaptureRequestBus::Handler::BusDisconnect();
  357. FrameCaptureTestRequestBus::Handler::BusDisconnect();
  358. SystemTickBus::Handler::BusDisconnect();
  359. m_idleCaptures.clear();
  360. m_inProgressCaptures.clear();
  361. m_allCaptures.clear();
  362. }
  363. AZStd::string FrameCaptureSystemComponent::ResolvePath(const AZStd::string& filePath)
  364. {
  365. AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetDirectInstance();
  366. char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
  367. fileIO->ResolvePath(filePath.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
  368. return AZStd::string(resolvedPath);
  369. }
  370. bool FrameCaptureSystemComponent::CanCapture() const
  371. {
  372. return !AZ::RHI::IsNullRHI();
  373. }
  374. AZ::Outcome<FrameCaptureSystemComponent::CaptureHandle, FrameCaptureError> FrameCaptureSystemComponent::ScreenshotPreparation(
  375. const AZStd::string& imagePath,
  376. AZ::RPI::AttachmentReadback::CallbackFunction callbackFunction)
  377. {
  378. FrameCaptureError error;
  379. if (!CanCapture())
  380. {
  381. error.m_errorMessage = "Frame capture not availble.";
  382. return AZ::Failure(error);
  383. }
  384. if (imagePath.empty() && callbackFunction == nullptr)
  385. {
  386. error.m_errorMessage = "No callback or image path is set. No result will be generated.";
  387. return AZ::Failure(error);
  388. }
  389. AZ_Warning(
  390. "FrameCaptureSystemComponent",
  391. imagePath.empty() || callbackFunction == nullptr,
  392. "Callback and image path are both set. Image path will be ignored.");
  393. CaptureHandle captureHandle = InitCapture();
  394. if (captureHandle.IsNull())
  395. {
  396. error.m_errorMessage = "Failed to allocate a capture.";
  397. return AZ::Failure(error);
  398. }
  399. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  400. CaptureState* capture = captureHandle.GetCaptureState();
  401. if (!capture) // failed to get the capture state ptr, abort
  402. {
  403. error.m_errorMessage = "Failed to get the captureState.";
  404. m_idleCaptures.push_back(captureHandle);
  405. return AZ::Failure(error);
  406. }
  407. if (!capture->m_readback->IsReady())
  408. {
  409. error.m_errorMessage = "Failed to capture attachment since the readback is not ready.";
  410. m_idleCaptures.push_back(captureHandle);
  411. return AZ::Failure(error);
  412. }
  413. capture->m_readback->SetUserIdentifier(captureHandle.GetCaptureStateIndex());
  414. if (callbackFunction != nullptr)
  415. {
  416. capture->m_readback->SetCallback(callbackFunction);
  417. }
  418. else
  419. {
  420. capture->m_readback->SetCallback(
  421. AZStd::bind(&FrameCaptureSystemComponent::CaptureAttachmentCallback, this, AZStd::placeholders::_1));
  422. AZ_Assert(!imagePath.empty(), "The image path must be provided if the callback is not assigned.");
  423. capture->m_outputFilePath = ResolvePath(imagePath);
  424. }
  425. return AZ::Success(captureHandle);
  426. }
  427. FrameCaptureOutcome FrameCaptureSystemComponent::CaptureScreenshotForWindow(const AZStd::string& filePath, AzFramework::NativeWindowHandle windowHandle)
  428. {
  429. return InternalCaptureScreenshot(filePath, windowHandle);
  430. }
  431. FrameCaptureOutcome FrameCaptureSystemComponent::CaptureScreenshot(const AZStd::string& filePath)
  432. {
  433. FrameCaptureError error;
  434. AzFramework::NativeWindowHandle windowHandle = AZ::RPI::ViewportContextRequests::Get()->GetDefaultViewportContext()->GetWindowHandle();
  435. return InternalCaptureScreenshot(filePath, windowHandle);
  436. }
  437. FrameCaptureOutcome FrameCaptureSystemComponent::CaptureScreenshotWithPreview(const AZStd::string& outputFilePath)
  438. {
  439. FrameCaptureError error;
  440. RPI::PassFilter passFilter = RPI::PassFilter::CreateWithPassClass<RPI::ImageAttachmentPreviewPass>();
  441. AZ::RPI::ImageAttachmentPreviewPass* previewPass = nullptr;
  442. AZ::RPI::PassSystemInterface::Get()->ForEachPass(
  443. passFilter,
  444. [&previewPass](AZ::RPI::Pass* pass) -> AZ::RPI::PassFilterExecutionFlow
  445. {
  446. if (pass->GetParent() != nullptr && pass->IsEnabled())
  447. {
  448. previewPass = azrtti_cast<AZ::RPI::ImageAttachmentPreviewPass*>(pass);
  449. return AZ::RPI::PassFilterExecutionFlow::StopVisitingPasses;
  450. }
  451. return AZ::RPI::PassFilterExecutionFlow::ContinueVisitingPasses;
  452. });
  453. if (!previewPass)
  454. {
  455. error.m_errorMessage = "Failed to find an ImageAttachmentPreviewPass.";
  456. return AZ::Failure(error);
  457. }
  458. auto prepOutcome = ScreenshotPreparation(outputFilePath, nullptr);
  459. if (!prepOutcome.IsSuccess())
  460. {
  461. return AZ::Failure(prepOutcome.TakeError());
  462. }
  463. CaptureHandle captureHandle = prepOutcome.GetValue();
  464. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  465. CaptureState* captureState = captureHandle.GetCaptureState();
  466. if (!previewPass->ReadbackOutput(captureState->m_readback))
  467. {
  468. error.m_errorMessage = "Failed to readback output from the ImageAttachmentPreviewPass";
  469. m_idleCaptures.push_back(captureHandle);
  470. return AZ::Failure(error);
  471. }
  472. m_inProgressCaptures.push_back(captureHandle);
  473. FrameCaptureId frameId = captureHandle.GetCaptureStateIndex();
  474. return AZ::Success(frameId);
  475. }
  476. FrameCaptureOutcome FrameCaptureSystemComponent::InternalCaptureScreenshot(
  477. const AZStd::string& imagePath, AzFramework::NativeWindowHandle windowHandle)
  478. {
  479. FrameCaptureError error;
  480. if (!windowHandle)
  481. {
  482. error.m_errorMessage = "No valid window for the capture.";
  483. return AZ::Failure(error);
  484. }
  485. // Find SwapChainPass for the window handle
  486. RPI::SwapChainPass* pass = AZ::RPI::PassSystemInterface::Get()->FindSwapChainPass(windowHandle);
  487. if (!pass)
  488. {
  489. error.m_errorMessage = "Failed to find SwapChainPass for the window.";
  490. return AZ::Failure(error);
  491. }
  492. auto prepOutcome = ScreenshotPreparation(imagePath, nullptr);
  493. if (!prepOutcome.IsSuccess())
  494. {
  495. return AZ::Failure(prepOutcome.GetError());
  496. }
  497. CaptureHandle captureHandle = prepOutcome.GetValue();
  498. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  499. CaptureState* captureState = captureHandle.GetCaptureState();
  500. AZ_Assert(captureState, "ScreenshotPreparation should have created a ready capture state "
  501. "if the capture handle is valid.");
  502. pass->ReadbackSwapChain(captureState->m_readback);
  503. m_inProgressCaptures.push_back(captureHandle);
  504. FrameCaptureId frameId = captureHandle.GetCaptureStateIndex();
  505. return AZ::Success(frameId);
  506. }
  507. FrameCaptureOutcome FrameCaptureSystemComponent::InternalCapturePassAttachment(
  508. const AZStd::string& outputFilePath,
  509. AZ::RPI::AttachmentReadback::CallbackFunction callbackFunction,
  510. const AZStd::vector<AZStd::string>& passHierarchy,
  511. const AZStd::string& slot,
  512. RPI::PassAttachmentReadbackOption option)
  513. {
  514. FrameCaptureError error;
  515. if (passHierarchy.size() == 0)
  516. {
  517. error.m_errorMessage = "Empty data in passHierarchy.";
  518. return AZ::Failure(error);
  519. }
  520. RPI::PassFilter passFilter = RPI::PassFilter::CreateWithPassHierarchy(passHierarchy);
  521. RPI::Pass* pass = RPI::PassSystemInterface::Get()->FindFirstPass(passFilter);
  522. if (!pass)
  523. {
  524. error.m_errorMessage = AZStd::string::format("Failed to find pass from %s", passHierarchy[0].c_str());
  525. return AZ::Failure(error);
  526. }
  527. auto prepOutcome = ScreenshotPreparation(outputFilePath, callbackFunction);
  528. if (!prepOutcome.IsSuccess())
  529. {
  530. return AZ::Failure(prepOutcome.GetError());
  531. }
  532. CaptureHandle captureHandle = prepOutcome.GetValue();
  533. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  534. CaptureState* captureState = captureHandle.GetCaptureState();
  535. AZ_Assert(captureState, "ScreenshotPreparation should have created a ready capture state "
  536. "if the capture handle is valid.");
  537. if (!pass->ReadbackAttachment(captureState->m_readback, captureHandle.GetCaptureStateIndex(), Name(slot), option))
  538. {
  539. error.m_errorMessage = AZStd::string::format(
  540. "Failed to readback the attachment bound to pass [%s] slot [%s]", pass->GetName().GetCStr(), slot.c_str());
  541. m_idleCaptures.push_back(captureHandle);
  542. return AZ::Failure(error);
  543. }
  544. m_inProgressCaptures.push_back(captureHandle);
  545. FrameCaptureId frameId = captureHandle.GetCaptureStateIndex();
  546. return AZ::Success(frameId);
  547. }
  548. FrameCaptureOutcome FrameCaptureSystemComponent::CapturePassAttachment(
  549. const AZStd::string& imagePath,
  550. const AZStd::vector<AZStd::string>& passHierarchy,
  551. const AZStd::string& slot,
  552. RPI::PassAttachmentReadbackOption option)
  553. {
  554. return InternalCapturePassAttachment(
  555. imagePath,
  556. nullptr,
  557. passHierarchy,
  558. slot,
  559. option);
  560. }
  561. FrameCaptureOutcome FrameCaptureSystemComponent::CapturePassAttachmentWithCallback(
  562. RPI::AttachmentReadback::CallbackFunction callback,
  563. const AZStd::vector<AZStd::string>& passHierarchy,
  564. const AZStd::string& slotName,
  565. RPI::PassAttachmentReadbackOption option)
  566. {
  567. auto captureCallback = [this, callback](const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  568. {
  569. CaptureHandle captureHandle(this, readbackResult.m_userIdentifier);
  570. callback(readbackResult); // call user supplied callback function
  571. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  572. CaptureState* captureState = captureHandle.GetCaptureState();
  573. AZ_Assert(captureState && captureState->m_result == FrameCaptureResult::None, "Unexpected value for m_result");
  574. captureState->m_result = FrameCaptureResult::Success; // just need to mark this capture as complete, callback handles the actual processing
  575. };
  576. return InternalCapturePassAttachment("", captureCallback, passHierarchy, slotName, option);
  577. }
  578. void FrameCaptureSystemComponent::OnSystemTick()
  579. {
  580. // inProgressCaptures is in capture submit order, loop over the captures until we find an unfinished one.
  581. // This ensures that OnCaptureFinished is signalled in submission order
  582. while (m_inProgressCaptures.size())
  583. {
  584. CaptureHandle captureHandle(m_inProgressCaptures.front());
  585. if (captureHandle.IsNull())
  586. {
  587. // if we find a null handle, remove it from the list
  588. m_inProgressCaptures.pop_front();
  589. continue;
  590. }
  591. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  592. CaptureState* capture = captureHandle.GetCaptureState();
  593. if (capture->m_result == FrameCaptureResult::None)
  594. {
  595. break;
  596. }
  597. FrameCaptureNotificationBus::Event(captureHandle.GetCaptureStateIndex(), &FrameCaptureNotificationBus::Events::OnFrameCaptureFinished, capture->m_result, capture->m_latestCaptureInfo.c_str());
  598. m_inProgressCaptures.pop_front();
  599. m_idleCaptures.push_back(captureHandle);
  600. }
  601. }
  602. void FrameCaptureSystemComponent::CaptureAttachmentCallback(const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  603. {
  604. CaptureHandle captureHandle(this, readbackResult.m_userIdentifier);
  605. AZStd::scoped_lock<CaptureHandle> scope_lock(captureHandle);
  606. CaptureState* capture = captureHandle.GetCaptureState();
  607. AZ_Assert(capture && capture->m_result == FrameCaptureResult::None, "Unexpected value for m_result");
  608. capture->m_latestCaptureInfo = capture->m_outputFilePath;
  609. if (readbackResult.m_state == AZ::RPI::AttachmentReadback::ReadbackState::Success)
  610. {
  611. if (readbackResult.m_attachmentType == AZ::RHI::AttachmentType::Buffer)
  612. {
  613. // write buffer data to the data file
  614. AZ::IO::FileIOStream fileStream(capture->m_outputFilePath.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath);
  615. if (fileStream.IsOpen())
  616. {
  617. fileStream.Write(readbackResult.m_dataBuffer->size(), readbackResult.m_dataBuffer->data());
  618. capture->m_result = FrameCaptureResult::Success;
  619. }
  620. else
  621. {
  622. capture->m_latestCaptureInfo = AZStd::string::format("Failed to open file %s for writing", capture->m_outputFilePath.c_str());
  623. capture->m_result = FrameCaptureResult::FileWriteError;
  624. }
  625. }
  626. else if (readbackResult.m_attachmentType == AZ::RHI::AttachmentType::Image)
  627. {
  628. AZStd::string extension;
  629. AzFramework::StringFunc::Path::GetExtension(capture->m_outputFilePath.c_str(), extension, false);
  630. AZStd::to_lower(extension.begin(), extension.end());
  631. if (extension == "ppm")
  632. {
  633. if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
  634. readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM)
  635. {
  636. const auto ppmFrameCapture = PpmFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  637. capture->m_result = ppmFrameCapture.m_result;
  638. capture->m_latestCaptureInfo = ppmFrameCapture.m_errorMessage.value_or("");
  639. }
  640. else
  641. {
  642. capture->m_latestCaptureInfo = AZStd::string::format(
  643. "Can't save image with format %s to a ppm file", RHI::ToString(readbackResult.m_imageDescriptor.m_format));
  644. capture->m_result = FrameCaptureResult::UnsupportedFormat;
  645. }
  646. }
  647. else if (extension == "dds")
  648. {
  649. const auto ddsFrameCapture = DdsFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  650. capture->m_result = ddsFrameCapture.m_result;
  651. capture->m_latestCaptureInfo = ddsFrameCapture.m_errorMessage.value_or("");
  652. }
  653. else if (extension == "tiff" || extension == "tif")
  654. {
  655. const auto tifFrameCapture = TiffFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  656. capture->m_result = tifFrameCapture.m_result;
  657. capture->m_latestCaptureInfo = tifFrameCapture.m_errorMessage.value_or("");
  658. }
  659. else if (extension == "png")
  660. {
  661. if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
  662. readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM)
  663. {
  664. AZStd::string folderPath;
  665. AzFramework::StringFunc::Path::GetFolderPath(capture->m_outputFilePath.c_str(), folderPath);
  666. AZ::IO::SystemFile::CreateDir(folderPath.c_str());
  667. const auto frameCaptureResult = PngFrameCaptureOutput(capture->m_outputFilePath, readbackResult);
  668. capture->m_result = frameCaptureResult.m_result;
  669. capture->m_latestCaptureInfo = frameCaptureResult.m_errorMessage.value_or("");
  670. }
  671. else
  672. {
  673. capture->m_latestCaptureInfo = AZStd::string::format(
  674. "Can't save image with format %s to a png file", RHI::ToString(readbackResult.m_imageDescriptor.m_format));
  675. capture->m_result = FrameCaptureResult::UnsupportedFormat;
  676. }
  677. }
  678. else
  679. {
  680. capture->m_latestCaptureInfo = AZStd::string::format("Only supports saving image to ppm or dds files");
  681. capture->m_result = FrameCaptureResult::InvalidArgument;
  682. }
  683. }
  684. }
  685. else
  686. {
  687. capture->m_latestCaptureInfo = AZStd::string::format("Failed to read back attachment [%s]", readbackResult.m_name.GetCStr());
  688. capture->m_result = FrameCaptureResult::InternalError;
  689. }
  690. if (capture->m_result == FrameCaptureResult::Success)
  691. {
  692. // Normalize the path so the slashes will be in the right direction for the local platform allowing easy copy/paste into file browsers.
  693. AZStd::string normalizedPath = capture->m_outputFilePath;
  694. AzFramework::StringFunc::Path::Normalize(normalizedPath);
  695. AZ_Printf("FrameCaptureSystemComponent", "Attachment [%s] was saved to file %s\n", readbackResult.m_name.GetCStr(), normalizedPath.c_str());
  696. }
  697. else
  698. {
  699. AZ_Warning("FrameCaptureSystemComponent", false, "%s", capture->m_latestCaptureInfo.c_str());
  700. }
  701. }
  702. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  703. // CaptureHandle implementation
  704. FrameCaptureSystemComponent::CaptureHandle::CaptureHandle(FrameCaptureSystemComponent* frameCaptureSystemComponent, uint32_t captureStateIndex)
  705. : m_frameCaptureSystemComponent(frameCaptureSystemComponent)
  706. , m_captureStateIndex(captureStateIndex)
  707. {
  708. }
  709. FrameCaptureSystemComponent::CaptureHandle FrameCaptureSystemComponent::CaptureHandle::Null()
  710. {
  711. return CaptureHandle(nullptr, InvalidCaptureHandle);
  712. }
  713. void FrameCaptureSystemComponent::CaptureHandle::lock()
  714. {
  715. AZ_Assert(IsValid() && m_frameCaptureSystemComponent != nullptr, "FrameCaptureSystemComponent attempting to lock an invalid handle");
  716. m_frameCaptureSystemComponent->m_handleLock.lock_shared();
  717. }
  718. void FrameCaptureSystemComponent::CaptureHandle::unlock()
  719. {
  720. AZ_Assert(IsValid() && m_frameCaptureSystemComponent != nullptr, "FrameCaptureSystemComponent attempting to unlock an invalid handle");
  721. m_frameCaptureSystemComponent->m_handleLock.unlock_shared();
  722. }
  723. FrameCaptureSystemComponent::CaptureState* FrameCaptureSystemComponent::CaptureHandle::GetCaptureState()
  724. {
  725. AZ_Assert(IsValid() && m_frameCaptureSystemComponent != nullptr, "FrameCaptureSystemComponent GetCaptureState called on an invalid handle");
  726. if (IsNull() || m_frameCaptureSystemComponent == nullptr)
  727. {
  728. return nullptr;
  729. }
  730. // Ideally we could check the state of the handle lock here to check that a shared lock is being held.
  731. // Nearest available check is can we try an exclusive lock,
  732. // this will also fail if someone else is holding the exclusive lock though.
  733. if(m_frameCaptureSystemComponent->m_handleLock.try_lock())
  734. {
  735. AZ_Assert(false, "FrameCaptureSystemComponent::CaptureHandle::GetCaptureState called without holding a read lock");
  736. m_frameCaptureSystemComponent->m_handleLock.unlock();
  737. return nullptr;
  738. }
  739. size_t captureIdx = aznumeric_cast<size_t>(m_captureStateIndex);
  740. if (captureIdx < m_frameCaptureSystemComponent->m_allCaptures.size())
  741. {
  742. return &m_frameCaptureSystemComponent->m_allCaptures[captureIdx];
  743. }
  744. return nullptr;
  745. }
  746. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  747. // CaptureState implementation
  748. FrameCaptureSystemComponent::CaptureState::CaptureState(uint32_t captureIndex)
  749. {
  750. AZStd::fixed_string<128> scope_name = AZStd::fixed_string<128>::format("FrameCapture_%d", captureIndex);
  751. m_readback = AZStd::make_shared<AZ::RPI::AttachmentReadback>(AZ::RHI::ScopeId{ scope_name });
  752. AZ_Assert(m_readback, "Failed to allocate an AttachmentReadback for the capture state");
  753. }
  754. FrameCaptureSystemComponent::CaptureState::CaptureState(CaptureState&& other)
  755. : m_readback(AZStd::move(other.m_readback))
  756. , m_outputFilePath(AZStd::move(other.m_outputFilePath))
  757. , m_latestCaptureInfo(AZStd::move(other.m_latestCaptureInfo))
  758. {
  759. // atomic doesn't support move or copy construction, or direct assignment.
  760. // This function is only used during m_allCaptures resize due to CaptureState addition
  761. // and the m_handleLock is exclusively locked during that operation.
  762. // Manually copy the atomic value to work around the other issues.
  763. FrameCaptureResult result = other.m_result;
  764. m_result = result;
  765. }
  766. void FrameCaptureSystemComponent::CaptureState::Reset()
  767. {
  768. //m_readback->Reset();
  769. m_outputFilePath.clear();
  770. m_latestCaptureInfo.clear();
  771. m_result = FrameCaptureResult::None;
  772. }
  773. void FrameCaptureSystemComponent::SetScreenshotFolder(const AZStd::string& screenshotFolder)
  774. {
  775. m_screenshotFolder = ResolvePath(screenshotFolder);
  776. }
  777. void FrameCaptureSystemComponent::SetTestEnvPath(const AZStd::string& envPath)
  778. {
  779. m_testEnvPath = envPath;
  780. }
  781. void FrameCaptureSystemComponent::SetOfficialBaselineImageFolder(const AZStd::string& baselineFolder)
  782. {
  783. m_officialBaselineImageFolder = ResolvePath(baselineFolder);
  784. }
  785. void FrameCaptureSystemComponent::SetLocalBaselineImageFolder(const AZStd::string& baselineFolder)
  786. {
  787. m_localBaselineImageFolder = ResolvePath(baselineFolder);
  788. }
  789. FrameCapturePathOutcome FrameCaptureSystemComponent::BuildScreenshotFilePath(const AZStd::string& imageName, bool useEnvPath)
  790. {
  791. AZStd::string imagePath = useEnvPath
  792. ? ResolvePath(AZStd::string::format("%s/%s/%s", m_screenshotFolder.c_str(), m_testEnvPath.c_str(), imageName.c_str()))
  793. : ResolvePath(AZStd::string::format("%s/%s", m_screenshotFolder.c_str(), imageName.c_str()));
  794. if (imagePath.size())
  795. {
  796. return AZ::Success(imagePath);
  797. }
  798. else
  799. {
  800. FrameCaptureTestError error;
  801. error.m_errorMessage = "Failed to build image path.";
  802. return AZ::Failure(error);
  803. }
  804. }
  805. FrameCapturePathOutcome FrameCaptureSystemComponent::BuildOfficialBaselineFilePath(const AZStd::string& imageName, bool useEnvPath)
  806. {
  807. AZStd::string imagePath = useEnvPath
  808. ? ResolvePath(AZStd::string::format("%s/%s/%s", m_officialBaselineImageFolder.c_str(), m_testEnvPath.c_str(), imageName.c_str()))
  809. : ResolvePath(AZStd::string::format("%s/%s", m_officialBaselineImageFolder.c_str(), imageName.c_str()));
  810. if (imagePath.size())
  811. {
  812. return AZ::Success(imagePath);
  813. }
  814. else
  815. {
  816. FrameCaptureTestError error;
  817. error.m_errorMessage = "Failed to build image path.";
  818. return AZ::Failure(error);
  819. }
  820. }
  821. FrameCapturePathOutcome FrameCaptureSystemComponent::BuildLocalBaselineFilePath(const AZStd::string& imageName, bool useEnvPath)
  822. {
  823. AZStd::string imagePath = useEnvPath
  824. ? ResolvePath(AZStd::string::format("%s/%s/%s", m_localBaselineImageFolder.c_str(), m_testEnvPath.c_str(), imageName.c_str()))
  825. : ResolvePath(AZStd::string::format("%s/%s", m_localBaselineImageFolder.c_str(), imageName.c_str()));
  826. if (imagePath.size())
  827. {
  828. return AZ::Success(imagePath);
  829. }
  830. else
  831. {
  832. FrameCaptureTestError error;
  833. error.m_errorMessage = "Failed to build image path.";
  834. return AZ::Failure(error);
  835. }
  836. }
  837. FrameCaptureComparisonOutcome FrameCaptureSystemComponent::CompareScreenshots(
  838. const AZStd::string& filePathA, const AZStd::string& filePathB, float minDiffFilter)
  839. {
  840. FrameCaptureTestError error;
  841. char resolvedFilePathA[AZ_MAX_PATH_LEN] = { 0 };
  842. char resolvedFilePathB[AZ_MAX_PATH_LEN] = { 0 };
  843. AZ::IO::FileIOBase::GetInstance()->ResolvePath(filePathA.c_str(), resolvedFilePathA, AZ_MAX_PATH_LEN);
  844. AZ::IO::FileIOBase::GetInstance()->ResolvePath(filePathB.c_str(), resolvedFilePathB, AZ_MAX_PATH_LEN);
  845. if (!filePathA.ends_with(".png") || !filePathB.ends_with(".png"))
  846. {
  847. error.m_errorMessage = "Image comparison only supports png files for now.";
  848. return AZ::Failure(error);
  849. }
  850. // Load image A
  851. Utils::PngFile imageA = Utils::PngFile::Load(resolvedFilePathA);
  852. if (!imageA.IsValid())
  853. {
  854. error.m_errorMessage = AZStd::string::format("Failed to load image file: %s.", resolvedFilePathA);
  855. return AZ::Failure(error);
  856. }
  857. else if (imageA.GetBufferFormat() != Utils::PngFile::Format::RGBA)
  858. {
  859. error.m_errorMessage = AZStd::string::format("Image comparison only supports 8-bit RGBA png. %s is not.", resolvedFilePathA);
  860. return AZ::Failure(error);
  861. }
  862. // Load image B
  863. Utils::PngFile imageB = Utils::PngFile::Load(resolvedFilePathB);
  864. if (!imageB.IsValid())
  865. {
  866. error.m_errorMessage = AZStd::string::format("Failed to load image file: %s.", resolvedFilePathB);
  867. return AZ::Failure(error);
  868. }
  869. else if (imageA.GetBufferFormat() != Utils::PngFile::Format::RGBA)
  870. {
  871. error.m_errorMessage = AZStd::string::format("Image comparison only supports 8-bit RGBA png. %s is not.", resolvedFilePathB);
  872. return AZ::Failure(error);
  873. }
  874. // Compare
  875. auto compOutcome = Utils::CalcImageDiffRms(
  876. imageA.GetBuffer(), RHI::Size(imageA.GetWidth(), imageA.GetHeight(), 1), AZ::RHI::Format::R8G8B8A8_UNORM,
  877. imageB.GetBuffer(), RHI::Size(imageB.GetWidth(), imageB.GetHeight(), 1), AZ::RHI::Format::R8G8B8A8_UNORM,
  878. minDiffFilter
  879. );
  880. if (!compOutcome.IsSuccess())
  881. {
  882. error.m_errorMessage = compOutcome.GetError().m_errorMessage;
  883. return AZ::Failure(error);
  884. }
  885. return AZ::Success(compOutcome.TakeValue());
  886. }
  887. }
  888. }