FrameCaptureSystemComponent.cpp 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  3. *
  4. * SPDX-License-Identifier: Apache-2.0 OR MIT
  5. *
  6. */
  7. #include "FrameCaptureSystemComponent.h"
  8. #include <Atom/RPI.Public/Pass/PassSystemInterface.h>
  9. #include <Atom/RPI.Public/Pass/PassFilter.h>
  10. #include <Atom/RPI.Public/Pass/RenderPass.h>
  11. #include <Atom/RPI.Public/Pass/Specific/SwapChainPass.h>
  12. #include <Atom/RPI.Public/ViewportContextManager.h>
  13. #include <Atom/Utils/DdsFile.h>
  14. #include <Atom/Utils/PpmFile.h>
  15. #include <AtomCore/Serialization/Json/JsonUtils.h>
  16. #include <AzCore/Jobs/JobFunction.h>
  17. #include <AzCore/Jobs/JobCompletion.h>
  18. #include <AzCore/IO/SystemFile.h>
  19. #include <AzCore/RTTI/BehaviorContext.h>
  20. #include <AzCore/Script/ScriptContextAttributes.h>
  21. #include <AzCore/Serialization/SerializeContext.h>
  22. #include <AzFramework/IO/LocalFileIO.h>
  23. #include <AzFramework/StringFunc/StringFunc.h>
  24. #include <AzCore/Preprocessor/EnumReflectUtils.h>
  25. #include <AzCore/Console/Console.h>
  26. #if defined(OPEN_IMAGE_IO_ENABLED)
  27. #include <OpenImageIO/imageio.h>
  28. #endif
  29. namespace AZ
  30. {
  31. namespace Render
  32. {
  33. AZ_ENUM_DEFINE_REFLECT_UTILITIES(FrameCaptureResult);
  34. #if defined(OPEN_IMAGE_IO_ENABLED)
  35. AZ_CVAR(unsigned int,
  36. r_pngCompressionLevel,
  37. 3, // A compression level of 3 seems like the best default in terms of file size and saving speeds
  38. nullptr,
  39. ConsoleFunctorFlags::Null,
  40. "Sets the compression level for saving png screenshots. Valid values are from 0 to 8"
  41. );
  42. FrameCaptureOutputResult PngFrameCaptureOutput(
  43. const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  44. {
  45. AZStd::shared_ptr<AZStd::vector<uint8_t>> buffer = readbackResult.m_dataBuffer;
  46. // convert bgra to rgba by swapping channels
  47. const int numChannels = AZ::RHI::GetFormatComponentCount(readbackResult.m_imageDescriptor.m_format);
  48. if (readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM)
  49. {
  50. buffer = AZStd::make_shared<AZStd::vector<uint8_t>>(readbackResult.m_dataBuffer->size());
  51. AZStd::copy(readbackResult.m_dataBuffer->begin(), readbackResult.m_dataBuffer->end(), buffer->begin());
  52. AZ::JobCompletion jobCompletion;
  53. const int numThreads = 8;
  54. const int numPixelsPerThread = buffer->size() / numChannels / numThreads;
  55. for (int i = 0; i < numThreads; ++i)
  56. {
  57. int startPixel = i * numPixelsPerThread;
  58. AZ::Job* job = AZ::CreateJobFunction(
  59. [&, startPixel, numPixelsPerThread]()
  60. {
  61. for (int pixelOffset = 0; pixelOffset < numPixelsPerThread; ++pixelOffset)
  62. {
  63. if (startPixel * numChannels + numChannels < buffer->size())
  64. {
  65. AZStd::swap(
  66. buffer->data()[(startPixel + pixelOffset) * numChannels],
  67. buffer->data()[(startPixel + pixelOffset) * numChannels + 2]
  68. );
  69. }
  70. }
  71. }, true, nullptr);
  72. job->SetDependent(&jobCompletion);
  73. job->Start();
  74. }
  75. jobCompletion.StartAndWaitForCompletion();
  76. }
  77. using namespace OIIO;
  78. AZStd::unique_ptr<ImageOutput> out = ImageOutput::create(outputFilePath.c_str());
  79. if (out)
  80. {
  81. ImageSpec spec(
  82. readbackResult.m_imageDescriptor.m_size.m_width,
  83. readbackResult.m_imageDescriptor.m_size.m_height,
  84. numChannels
  85. );
  86. spec.attribute("png:compressionLevel", r_pngCompressionLevel);
  87. if (out->open(outputFilePath.c_str(), spec))
  88. {
  89. out->write_image(TypeDesc::UINT8, buffer->data());
  90. out->close();
  91. return FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt};
  92. }
  93. }
  94. return FrameCaptureOutputResult{FrameCaptureResult::InternalError, "Unable to save frame capture output to " + outputFilePath};
  95. }
  96. #endif
  97. FrameCaptureOutputResult DdsFrameCaptureOutput(
  98. const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  99. {
  100. // write the read back result of the image attachment to a dds file
  101. const auto outcome = AZ::DdsFile::WriteFile(
  102. outputFilePath,
  103. {readbackResult.m_imageDescriptor.m_size, readbackResult.m_imageDescriptor.m_format, readbackResult.m_dataBuffer.get()});
  104. return outcome.IsSuccess() ? FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt}
  105. : FrameCaptureOutputResult{FrameCaptureResult::InternalError, outcome.GetError().m_message};
  106. }
  107. FrameCaptureOutputResult PpmFrameCaptureOutput(
  108. const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  109. {
  110. // write the read back result of the image attachment to a buffer
  111. const AZStd::vector<uint8_t> outBuffer = Utils::PpmFile::CreatePpmFromImageBuffer(
  112. *readbackResult.m_dataBuffer.get(), readbackResult.m_imageDescriptor.m_size, readbackResult.m_imageDescriptor.m_format);
  113. // write the buffer to a ppm file
  114. if (IO::FileIOStream fileStream(outputFilePath.c_str(), IO::OpenMode::ModeWrite | IO::OpenMode::ModeCreatePath);
  115. fileStream.IsOpen())
  116. {
  117. fileStream.Write(outBuffer.size(), outBuffer.data());
  118. fileStream.Close();
  119. return FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt};
  120. }
  121. return FrameCaptureOutputResult{
  122. FrameCaptureResult::FileWriteError,
  123. AZStd::string::format("Failed to open file %s for writing", outputFilePath.c_str())};
  124. }
  125. class FrameCaptureNotificationBusHandler final
  126. : public FrameCaptureNotificationBus::Handler
  127. , public AZ::BehaviorEBusHandler
  128. {
  129. public:
  130. AZ_EBUS_BEHAVIOR_BINDER(FrameCaptureNotificationBusHandler, "{68D1D94C-7055-4D32-8E22-BEEEBA0940C4}", AZ::SystemAllocator, OnCaptureFinished);
  131. void OnCaptureFinished(FrameCaptureResult result, const AZStd::string& info) override
  132. {
  133. Call(FN_OnCaptureFinished, result, info);
  134. }
  135. static void Reflect(AZ::ReflectContext* context)
  136. {
  137. if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
  138. {
  139. FrameCaptureResultReflect(*serializeContext);
  140. }
  141. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  142. {
  143. //[GFX_TODO][ATOM-13424] Replace this with a utility in AZ_ENUM_DEFINE_REFLECT_UTILITIES
  144. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::None)>("FrameCaptureResult_None")
  145. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  146. ->Attribute(AZ::Script::Attributes::Module, "atom");
  147. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::Success)>("FrameCaptureResult_Success")
  148. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  149. ->Attribute(AZ::Script::Attributes::Module, "atom");
  150. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::FileWriteError)>("FrameCaptureResult_FileWriteError")
  151. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  152. ->Attribute(AZ::Script::Attributes::Module, "atom");
  153. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::InvalidArgument)>("FrameCaptureResult_InvalidArgument")
  154. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  155. ->Attribute(AZ::Script::Attributes::Module, "atom");
  156. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::UnsupportedFormat)>("FrameCaptureResult_UnsupportedFormat")
  157. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  158. ->Attribute(AZ::Script::Attributes::Module, "atom");
  159. behaviorContext->EnumProperty<static_cast<int>(FrameCaptureResult::InternalError)>("FrameCaptureResult_InternalError")
  160. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  161. ->Attribute(AZ::Script::Attributes::Module, "atom");
  162. behaviorContext->EBus<FrameCaptureNotificationBus>("FrameCaptureNotificationBus")
  163. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  164. ->Attribute(AZ::Script::Attributes::Module, "atom")
  165. ->Handler<FrameCaptureNotificationBusHandler>()
  166. ;
  167. }
  168. }
  169. };
  170. void FrameCaptureSystemComponent::Reflect(AZ::ReflectContext* context)
  171. {
  172. if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
  173. {
  174. serializeContext->Class<FrameCaptureSystemComponent, AZ::Component>()
  175. ->Version(1)
  176. ;
  177. }
  178. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  179. {
  180. behaviorContext->EBus<FrameCaptureRequestBus>("FrameCaptureRequestBus")
  181. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  182. ->Attribute(AZ::Script::Attributes::Module, "atom")
  183. ->Event("CaptureScreenshot", &FrameCaptureRequestBus::Events::CaptureScreenshot)
  184. ->Event("CaptureScreenshotWithPreview", &FrameCaptureRequestBus::Events::CaptureScreenshotWithPreview)
  185. ->Event("CapturePassAttachment", &FrameCaptureRequestBus::Events::CapturePassAttachment)
  186. ;
  187. FrameCaptureNotificationBusHandler::Reflect(context);
  188. }
  189. }
  190. void FrameCaptureSystemComponent::Activate()
  191. {
  192. FrameCaptureRequestBus::Handler::BusConnect();
  193. }
  194. void FrameCaptureSystemComponent::InitReadback()
  195. {
  196. if (!m_readback)
  197. {
  198. m_readback = AZStd::make_shared<AZ::RPI::AttachmentReadback>(AZ::RHI::ScopeId{ "FrameCapture" });
  199. }
  200. m_readback->SetCallback(AZStd::bind(&FrameCaptureSystemComponent::CaptureAttachmentCallback, this, AZStd::placeholders::_1));
  201. }
  202. void FrameCaptureSystemComponent::Deactivate()
  203. {
  204. m_readback = nullptr;
  205. FrameCaptureRequestBus::Handler::BusDisconnect();
  206. }
  207. AZStd::string FrameCaptureSystemComponent::ResolvePath(const AZStd::string& filePath)
  208. {
  209. AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetDirectInstance();
  210. char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
  211. fileIO->ResolvePath(filePath.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
  212. return AZStd::string(resolvedPath);
  213. }
  214. bool FrameCaptureSystemComponent::CaptureScreenshotForWindow(const AZStd::string& filePath, AzFramework::NativeWindowHandle windowHandle)
  215. {
  216. InitReadback();
  217. if (m_state != State::Idle)
  218. {
  219. AZ_Warning("FrameCaptureSystemComponent", false, "Another capture has not finished yet");
  220. return false;
  221. }
  222. // Find SwapChainPass for the window handle
  223. RPI::SwapChainPass* pass = AZ::RPI::PassSystemInterface::Get()->FindSwapChainPass(windowHandle);
  224. if (!pass)
  225. {
  226. AZ_Warning("FrameCaptureSystemComponent", false, "Failed to find SwapChainPass for the window");
  227. return false;
  228. }
  229. if (!m_readback->IsReady())
  230. {
  231. AZ_Assert(false, "Failed to capture attachment since the readback is not ready");
  232. return false;
  233. }
  234. m_outputFilePath = ResolvePath(filePath);
  235. m_latestCaptureInfo.clear();
  236. m_state = State::Pending;
  237. m_result = FrameCaptureResult::None;
  238. SystemTickBus::Handler::BusConnect();
  239. pass->ReadbackSwapChain(m_readback);
  240. return true;
  241. }
  242. bool FrameCaptureSystemComponent::CaptureScreenshot(const AZStd::string& filePath)
  243. {
  244. AzFramework::NativeWindowHandle windowHandle = AZ::RPI::ViewportContextRequests::Get()->GetDefaultViewportContext()->GetWindowHandle();
  245. if (windowHandle)
  246. {
  247. return CaptureScreenshotForWindow(filePath, windowHandle);
  248. }
  249. return false;
  250. }
  251. bool FrameCaptureSystemComponent::CaptureScreenshotWithPreview(const AZStd::string& outputFilePath)
  252. {
  253. InitReadback();
  254. if (m_state != State::Idle)
  255. {
  256. AZ_Warning("FrameCaptureSystemComponent", false, "Another capture has not finished yet");
  257. return false;
  258. }
  259. if (!m_readback->IsReady())
  260. {
  261. AZ_Assert(false, "Failed to capture attachment since the readback is not ready");
  262. return false;
  263. }
  264. m_outputFilePath.clear();
  265. if (!outputFilePath.empty())
  266. {
  267. m_outputFilePath = ResolvePath(outputFilePath);
  268. }
  269. m_latestCaptureInfo.clear();
  270. // Find the pass first
  271. RPI::PassClassFilter<RPI::ImageAttachmentPreviewPass> passFilter;
  272. AZStd::vector<AZ::RPI::Pass*> foundPasses = AZ::RPI::PassSystemInterface::Get()->FindPasses(passFilter);
  273. if (foundPasses.size() == 0)
  274. {
  275. AZ_Warning("FrameCaptureSystemComponent", false, "Failed to find an ImageAttachmentPreviewPass pass ");
  276. return false;
  277. }
  278. AZ::RPI::ImageAttachmentPreviewPass* previewPass = azrtti_cast<AZ::RPI::ImageAttachmentPreviewPass*>(foundPasses[0]);
  279. bool result = previewPass->ReadbackOutput(m_readback);
  280. if (result)
  281. {
  282. m_state = State::Pending;
  283. m_result = FrameCaptureResult::None;
  284. SystemTickBus::Handler::BusConnect();
  285. }
  286. else
  287. {
  288. AZ_Warning("FrameCaptureSystemComponent", false, "CaptureScreenshotWithPreview. Failed to readback output from the ImageAttachmentPreviewPass");;
  289. }
  290. return result;
  291. }
  292. bool FrameCaptureSystemComponent::CapturePassAttachment(const AZStd::vector<AZStd::string>& passHierarchy, const AZStd::string& slot,
  293. const AZStd::string& outputFilePath, RPI::PassAttachmentReadbackOption option)
  294. {
  295. InitReadback();
  296. if (m_state != State::Idle)
  297. {
  298. AZ_Warning("FrameCaptureSystemComponent", false, "Another capture has not finished yet");
  299. return false;
  300. }
  301. if (!m_readback->IsReady())
  302. {
  303. AZ_Assert(false, "Failed to capture attachment since the readback is not ready");
  304. return false;
  305. }
  306. m_outputFilePath.clear();
  307. if (!outputFilePath.empty())
  308. {
  309. m_outputFilePath = ResolvePath(outputFilePath);
  310. }
  311. m_latestCaptureInfo.clear();
  312. // Find the pass first
  313. AZ::RPI::PassHierarchyFilter passFilter(passHierarchy);
  314. AZStd::vector<AZ::RPI::Pass*> foundPasses = AZ::RPI::PassSystemInterface::Get()->FindPasses(passFilter);
  315. if (foundPasses.size() == 0)
  316. {
  317. AZ_Warning("FrameCaptureSystemComponent", false, "Failed to find pass from %s", passFilter.ToString().c_str());
  318. return false;
  319. }
  320. AZ::RPI::Pass* pass = foundPasses[0];
  321. if (pass->ReadbackAttachment(m_readback, Name(slot), option))
  322. {
  323. m_state = State::Pending;
  324. m_result = FrameCaptureResult::None;
  325. SystemTickBus::Handler::BusConnect();
  326. return true;
  327. }
  328. AZ_Warning("FrameCaptureSystemComponent", false, "Failed to readback the attachment bound to pass [%s] slot [%s]", pass->GetName().GetCStr(), slot.c_str());
  329. return false;
  330. }
  331. bool FrameCaptureSystemComponent::CapturePassAttachmentWithCallback(const AZStd::vector<AZStd::string>& passHierarchy, const AZStd::string& slotName
  332. , RPI::AttachmentReadback::CallbackFunction callback, RPI::PassAttachmentReadbackOption option)
  333. {
  334. bool result = CapturePassAttachment(passHierarchy, slotName, "", option);
  335. // Append state change to user provided call back
  336. AZ::RPI::AttachmentReadback::CallbackFunction callbackSetState = [&, callback](const AZ::RPI::AttachmentReadback::ReadbackResult& result)
  337. {
  338. callback(result);
  339. m_state = (result.m_state == AZ::RPI::AttachmentReadback::ReadbackState::Success) ? State::WasSuccess : State::WasFailure;
  340. };
  341. m_readback->SetCallback(callbackSetState);
  342. return result;
  343. }
  344. void FrameCaptureSystemComponent::OnSystemTick()
  345. {
  346. if (m_state == State::WasSuccess || m_state == State::WasFailure)
  347. {
  348. FrameCaptureNotificationBus::Broadcast(&FrameCaptureNotificationBus::Events::OnCaptureFinished, m_result, m_latestCaptureInfo.c_str());
  349. m_state = State::Idle;
  350. m_result = FrameCaptureResult::None;
  351. SystemTickBus::Handler::BusDisconnect();
  352. }
  353. else if (m_state != State::Pending)
  354. {
  355. AZ_Assert(false, "TickBus should not be connected when a readback is not Pending. Something is out of sync");
  356. }
  357. }
  358. void FrameCaptureSystemComponent::CaptureAttachmentCallback(const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
  359. {
  360. AZ_Assert(m_state == State::Pending, "Unexpected value for m_state");
  361. AZ_Assert(m_result == FrameCaptureResult::None, "Unexpected value for m_result");
  362. m_latestCaptureInfo = m_outputFilePath;
  363. if (readbackResult.m_state == AZ::RPI::AttachmentReadback::ReadbackState::Success)
  364. {
  365. if (readbackResult.m_attachmentType == AZ::RHI::AttachmentType::Buffer)
  366. {
  367. // write buffer data to the data file
  368. AZ::IO::FileIOStream fileStream(m_outputFilePath.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath);
  369. if (fileStream.IsOpen())
  370. {
  371. fileStream.Write(readbackResult.m_dataBuffer->size(), readbackResult.m_dataBuffer->data());
  372. m_result = FrameCaptureResult::Success;
  373. }
  374. else
  375. {
  376. m_latestCaptureInfo = AZStd::string::format("Failed to open file %s for writing", m_outputFilePath.c_str());
  377. m_result = FrameCaptureResult::FileWriteError;
  378. }
  379. }
  380. else if (readbackResult.m_attachmentType == AZ::RHI::AttachmentType::Image)
  381. {
  382. AZStd::string extension;
  383. AzFramework::StringFunc::Path::GetExtension(m_outputFilePath.c_str(), extension, false);
  384. AZStd::to_lower(extension.begin(), extension.end());
  385. if (extension == "ppm")
  386. {
  387. if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
  388. readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM)
  389. {
  390. const auto ppmFrameCapture = PpmFrameCaptureOutput(m_outputFilePath, readbackResult);
  391. m_result = ppmFrameCapture.m_result;
  392. m_latestCaptureInfo = ppmFrameCapture.m_errorMessage.value_or("");
  393. }
  394. else
  395. {
  396. m_latestCaptureInfo = AZStd::string::format(
  397. "Can't save image with format %s to a ppm file", RHI::ToString(readbackResult.m_imageDescriptor.m_format));
  398. m_result = FrameCaptureResult::UnsupportedFormat;
  399. }
  400. }
  401. else if (extension == "dds")
  402. {
  403. const auto ddsFrameCapture = DdsFrameCaptureOutput(m_outputFilePath, readbackResult);
  404. m_result = ddsFrameCapture.m_result;
  405. m_latestCaptureInfo = ddsFrameCapture.m_errorMessage.value_or("");
  406. }
  407. #if defined(OPEN_IMAGE_IO_ENABLED)
  408. else if (extension == "png")
  409. {
  410. if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
  411. readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM)
  412. {
  413. AZStd::string folderPath;
  414. AzFramework::StringFunc::Path::GetFolderPath(m_outputFilePath.c_str(), folderPath);
  415. AZ::IO::SystemFile::CreateDir(folderPath.c_str());
  416. const auto frameCaptureResult = PngFrameCaptureOutput(m_outputFilePath, readbackResult);
  417. m_result = frameCaptureResult.m_result;
  418. m_latestCaptureInfo = frameCaptureResult.m_errorMessage.value_or("");
  419. }
  420. else
  421. {
  422. m_latestCaptureInfo = AZStd::string::format(
  423. "Can't save image with format %s to a png file", RHI::ToString(readbackResult.m_imageDescriptor.m_format));
  424. m_result = FrameCaptureResult::UnsupportedFormat;
  425. }
  426. }
  427. #endif
  428. else
  429. {
  430. m_latestCaptureInfo = AZStd::string::format("Only supports saving image to ppm or dds files");
  431. m_result = FrameCaptureResult::InvalidArgument;
  432. }
  433. }
  434. }
  435. else
  436. {
  437. m_latestCaptureInfo = AZStd::string::format("Failed to read back attachment [%s]", readbackResult.m_name.GetCStr());
  438. m_result = FrameCaptureResult::InternalError;
  439. }
  440. if (m_result == FrameCaptureResult::Success)
  441. {
  442. m_state = State::WasSuccess;
  443. // Normalize the path so the slashes will be in the right direction for the local platform allowing easy copy/paste into file browsers.
  444. AZStd::string normalizedPath = m_outputFilePath;
  445. AzFramework::StringFunc::Path::Normalize(normalizedPath);
  446. AZ_Printf("FrameCaptureSystemComponent", "Attachment [%s] was saved to file %s\n", readbackResult.m_name.GetCStr(), normalizedPath.c_str());
  447. }
  448. else
  449. {
  450. m_state = State::WasFailure;
  451. AZ_Warning("FrameCaptureSystemComponent", false, "%s", m_latestCaptureInfo.c_str());
  452. }
  453. }
  454. }
  455. }