ScriptAutomationScriptBindings.cpp 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888
  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 <ScriptAutomationSystemComponent.h>
  9. #include <ScriptAutomationScriptBindings.h>
  10. #include <AzCore/Component/Entity.h>
  11. #include <AzCore/Component/EntityId.h>
  12. #include <AzCore/Console/IConsole.h>
  13. #include <AzCore/Math/MathReflection.h>
  14. #include <AzCore/RTTI/BehaviorContext.h>
  15. #include <AzCore/StringFunc/StringFunc.h>
  16. #include <AzCore/Debug/ProfilerBus.h>
  17. #include <AzCore/Settings/SettingsRegistry.h>
  18. #include <AzCore/Settings/SettingsRegistryScriptUtils.h>
  19. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  20. #include <AzCore/std/string/string.h>
  21. #include <AzCore/std/string/string_view.h>
  22. #include <AzCore/std/optional.h>
  23. #include <AzCore/IO/Path/Path.h>
  24. #include <AzFramework/Components/ConsoleBus.h>
  25. #include <AzFramework/Components/CameraBus.h>
  26. #include <AzFramework/IO/LocalFileIO.h>
  27. #include <AzFramework/Windowing/NativeWindow.h>
  28. #include <Atom/RPI.Public/Pass/AttachmentReadback.h>
  29. #include <Atom/RPI.Public/RPISystemInterface.h>
  30. #include <ScriptAutomation_Traits.h>
  31. AZ_CVAR(AZ::CVarFixedString, sa_image_compare_app_path, AZ_TRAIT_SCRIPTAUTOMATION_DEFAULT_IMAGE_COMPARE_PATH, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Default image compare app path");
  32. AZ_CVAR(AZ::CVarFixedString, sa_image_compare_arguments, AZ_TRAIT_SCRIPTAUTOMATION_DEFAULT_IMAGE_COMPARE_ARGUMENTS, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Default image compare arguments");
  33. AZ_CVAR(bool, sa_launch_image_compare_for_failed_baseline_compare, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Should ScriptAutomation launch an image compare for every failed screenshot baseline compare");
  34. /* sa_launch_image_compare_for_failed_baseline_compare can be set to true for local work by adding a setreg file containing the below json
  35. * {
  36. * "Amazon": {
  37. * "AzCore": {
  38. * "Runtime": {
  39. * "ConsoleCommands": {
  40. * "sa_launch_image_compare_for_failed_baseline_compare": 1
  41. * }
  42. * }
  43. * }
  44. * }
  45. * }
  46. */
  47. namespace AZ::Platform
  48. {
  49. bool LaunchProgram(const AZStd::string& progPath, const AZStd::string& arguments);
  50. }
  51. namespace AZ::ScriptAutomation
  52. {
  53. static constexpr char NewScreenshotPlaceholder[] = "{NewScreenshotPath}";
  54. static constexpr char ExpectedScreenshotPlaceholder[] = "{ExpectedScreenshotPath}";
  55. static constexpr char TestNamePlaceholder[] = "{TestName}";
  56. static constexpr char ImageNamePlaceholder[] = "{ImageName}";
  57. static constexpr char PlaceholderEndChar[] = "}";
  58. namespace Utils
  59. {
  60. void ReplacePlaceholder(AZStd::string& string, const char* placeholderName, const AZStd::string& newValue)
  61. {
  62. for (auto index = string.find(placeholderName); index != AZStd::string::npos; index = string.find(placeholderName))
  63. {
  64. auto endIndex = string.find(PlaceholderEndChar, index);
  65. string.erase(index, endIndex - index + 1);
  66. string.insert(index, newValue);
  67. }
  68. }
  69. void RunImageDiff(
  70. const AZStd::string& newImagePath,
  71. const AZStd::string& compareImagePath,
  72. const AZStd::string& testName,
  73. const AZStd::string& imageName)
  74. {
  75. AZStd::string appPath = static_cast<AZStd::string_view>(static_cast<AZ::CVarFixedString>(sa_image_compare_app_path));
  76. AZStd::string arguments = static_cast<AZStd::string_view>(static_cast<AZ::CVarFixedString>(sa_image_compare_arguments));
  77. ReplacePlaceholder(arguments, NewScreenshotPlaceholder, newImagePath);
  78. ReplacePlaceholder(arguments, ExpectedScreenshotPlaceholder, compareImagePath);
  79. ReplacePlaceholder(arguments, TestNamePlaceholder, testName);
  80. ReplacePlaceholder(arguments, ImageNamePlaceholder, imageName);
  81. if (!Platform::LaunchProgram(appPath, arguments))
  82. {
  83. AZ_Error("ScriptAutomation", false, "Failed to launch image diff - \"%s %s\"", appPath.c_str(), arguments.c_str());
  84. }
  85. }
  86. bool SupportsResizeClientAreaOfDefaultWindow()
  87. {
  88. return AzFramework::NativeWindow::SupportsClientAreaResizeOfDefaultWindow();
  89. }
  90. void ResizeClientArea(uint32_t width, uint32_t height, const AzFramework::WindowPosOptions& options)
  91. {
  92. AzFramework::NativeWindowHandle windowHandle = nullptr;
  93. AzFramework::WindowSystemRequestBus::BroadcastResult(
  94. windowHandle,
  95. &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle);
  96. AzFramework::WindowSize clientAreaSize = {width, height};
  97. AzFramework::WindowRequestBus::Event(windowHandle, &AzFramework::WindowRequestBus::Events::ResizeClientArea, clientAreaSize, options);
  98. AzFramework::WindowSize newWindowSize;
  99. AzFramework::WindowRequestBus::EventResult(newWindowSize, windowHandle, &AzFramework::WindowRequests::GetClientAreaSize);
  100. AZ_Error("ResizeClientArea", newWindowSize.m_width == width && newWindowSize.m_height == height,
  101. "Requested window resize to %ux%u but got %ux%u. This display resolution is too low or desktop scaling is too high.",
  102. width, height, newWindowSize.m_width, newWindowSize.m_height);
  103. }
  104. bool SupportsToggleFullScreenOfDefaultWindow()
  105. {
  106. return AzFramework::NativeWindow::CanToggleFullScreenStateOfDefaultWindow();
  107. }
  108. void ToggleFullScreenOfDefaultWindow()
  109. {
  110. AzFramework::NativeWindow::ToggleFullScreenStateOfDefaultWindow();
  111. }
  112. AZ::IO::FixedMaxPath GetProfilingPath(bool resolvePath)
  113. {
  114. AZ::IO::FixedMaxPath path("@user@");
  115. if (resolvePath)
  116. {
  117. if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
  118. {
  119. path.clear();
  120. settingsRegistry->Get(path.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectUserPath);
  121. }
  122. }
  123. path /= "scriptautomation/profiling";
  124. return path.LexicallyNormal();
  125. }
  126. AZ::IO::FixedMaxPath ResolvePath(const AZ::IO::PathView path)
  127. {
  128. AZStd::optional<AZ::IO::FixedMaxPath> resolvedPath = AZ::IO::FileIOBase::GetInstance()->ResolvePath(path);
  129. return resolvedPath ? resolvedPath.value() : AZ::IO::FixedMaxPath{};
  130. }
  131. } // namespace Utils
  132. namespace Bindings
  133. {
  134. void RunScript(const AZStd::string& scriptFilePath)
  135. {
  136. // Unlike other Script_ callback functions, we process immediately instead of pushing onto the m_scriptOperations queue.
  137. // This function is special because running the script is what adds more commands onto the m_scriptOperations queue.
  138. ScriptAutomationInterface::Get()->ExecuteScript(scriptFilePath.c_str());
  139. }
  140. void Print(const AZStd::string& message [[maybe_unused]])
  141. {
  142. auto func = [message]()
  143. {
  144. AZ_UNUSED(message); // mark explicitly captured variable `message` as used because AZ_Error is a nop in release builds
  145. AZ_Info("ScriptAutomation", "Script: %s\n", message.c_str());
  146. };
  147. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(func));
  148. }
  149. void Warning(const AZStd::string& message [[maybe_unused]])
  150. {
  151. auto func = [message]()
  152. {
  153. AZ_UNUSED(message); // mark explicitly captured variable `message` as used because AZ_Error is a nop in release builds
  154. AZ_Warning("ScriptAutomation", false, "Script: %s", message.c_str());
  155. };
  156. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(func));
  157. }
  158. void Error(const AZStd::string& message [[maybe_unused]])
  159. {
  160. auto func = [message]()
  161. {
  162. AZ_UNUSED(message); // mark explicitly captured variable `message` as used because AZ_Error is a nop in release builds
  163. AZ_Error("ScriptAutomation", false, "Script: %s", message.c_str());
  164. };
  165. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(func));
  166. }
  167. void ExecuteConsoleCommand(const AZStd::string& command)
  168. {
  169. auto func = [command]()
  170. {
  171. AzFramework::ConsoleRequestBus::Broadcast(
  172. &AzFramework::ConsoleRequests::ExecuteConsoleCommand, command.c_str());
  173. };
  174. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(func));
  175. }
  176. void IdleFrames(int numFrames)
  177. {
  178. auto func = [numFrames]()
  179. {
  180. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  181. scriptAutomationInterface->SetIdleFrames(numFrames);
  182. };
  183. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(func));
  184. }
  185. void IdleSeconds(float numSeconds)
  186. {
  187. auto func = [numSeconds]()
  188. {
  189. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  190. scriptAutomationInterface->SetIdleSeconds(numSeconds);
  191. };
  192. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(func));
  193. }
  194. void ResizeViewport(int width, int height)
  195. {
  196. auto operation = [width,height]()
  197. {
  198. if (Utils::SupportsResizeClientAreaOfDefaultWindow())
  199. {
  200. AzFramework::WindowPosOptions options;
  201. options.m_ignoreScreenSizeLimit = true;
  202. Utils::ResizeClientArea(width, height, options);
  203. }
  204. else
  205. {
  206. AZ_Error("ScriptAutomation", false, "ResizeClientArea() is not supported on this platform");
  207. }
  208. };
  209. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  210. }
  211. void SetCamera(const AZStd::string& entityName)
  212. {
  213. auto operation = [entityName]()
  214. {
  215. // Find all Component Entity Cameras
  216. AZ::EBusAggregateResults<AZ::EntityId> cameraComponentEntities;
  217. Camera::CameraBus::BroadcastResult(cameraComponentEntities, &Camera::CameraRequests::GetCameras);
  218. // add names of all found entities with Camera Components
  219. for (int i = 0; i < cameraComponentEntities.values.size(); i++)
  220. {
  221. AZ::Entity* entity = nullptr;
  222. AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, cameraComponentEntities.values[i]);
  223. if (entity)
  224. {
  225. if (entity->GetName() == entityName)
  226. {
  227. Camera::CameraRequestBus::Event(cameraComponentEntities.values[i], &Camera::CameraRequestBus::Events::MakeActiveView);
  228. }
  229. }
  230. }
  231. };
  232. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  233. }
  234. void CapturePassTimestamp(const AZStd::string& outputFilePath)
  235. {
  236. auto operation = [outputFilePath]()
  237. {
  238. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  239. scriptAutomationInterface->StartProfilingCapture();
  240. scriptAutomationInterface->PauseAutomation();
  241. AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CapturePassTimestamp, outputFilePath);
  242. };
  243. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  244. }
  245. void CaptureCpuFrameTime(const AZStd::string& outputFilePath)
  246. {
  247. auto operation = [outputFilePath]()
  248. {
  249. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  250. scriptAutomationInterface->StartProfilingCapture();
  251. scriptAutomationInterface->PauseAutomation();
  252. AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuFrameTime, outputFilePath);
  253. };
  254. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  255. }
  256. void CapturePassPipelineStatistics(const AZStd::string& outputFilePath)
  257. {
  258. auto operation = [outputFilePath]()
  259. {
  260. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  261. scriptAutomationInterface->StartProfilingCapture();
  262. scriptAutomationInterface->PauseAutomation();
  263. AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CapturePassPipelineStatistics, outputFilePath);
  264. };
  265. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  266. }
  267. void CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath)
  268. {
  269. auto operation = [outputFilePath]()
  270. {
  271. if (auto profilerSystem = AZ::Debug::ProfilerSystemInterface::Get(); profilerSystem)
  272. {
  273. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  274. scriptAutomationInterface->StartProfilingCapture();
  275. scriptAutomationInterface->PauseAutomation();
  276. profilerSystem->CaptureFrame(outputFilePath);
  277. }
  278. };
  279. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  280. }
  281. void CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath)
  282. {
  283. auto operation = [benchmarkName, outputFilePath]()
  284. {
  285. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  286. scriptAutomationInterface->StartProfilingCapture();
  287. scriptAutomationInterface->PauseAutomation();
  288. AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureBenchmarkMetadata, benchmarkName, outputFilePath);
  289. };
  290. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  291. }
  292. AZStd::vector<AZStd::string> SplitStringImmediate(const AZStd::string& source, const AZStd::string& delimiter)
  293. {
  294. AZStd::vector<AZStd::string> splitStringList;
  295. auto SplitString = [&splitStringList](AZStd::string_view token)
  296. {
  297. splitStringList.emplace_back(token);
  298. };
  299. AZ::StringFunc::TokenizeVisitor(source, SplitString, delimiter, false, false);
  300. return splitStringList;
  301. }
  302. AZStd::string ResolvePath(const AZStd::string& path)
  303. {
  304. return Utils::ResolvePath(AZ::IO::PathView(path)).String();
  305. }
  306. AZStd::string GetRenderApiName()
  307. {
  308. AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get();
  309. return rpiSystem->GetRenderApiName().GetCStr();
  310. }
  311. AZStd::string GetRenderPipelineName()
  312. {
  313. AZ::IConsole* console = AZ::Interface<AZ::IConsole>::Get();
  314. AZStd::string renderPipelinePath;
  315. console->GetCvarValue("r_default_pipeline_name", renderPipelinePath);
  316. AZ_Assert(renderPipelinePath.size() > 0, "Invalid render pipeline path obtained from r_default_pipeline_name CVAR");
  317. return AZ::IO::PathView(renderPipelinePath).Stem().String();
  318. }
  319. AZStd::string GetPlatformName()
  320. {
  321. return AZ_TRAIT_OS_PLATFORM_CODENAME_LOWER;
  322. }
  323. AZStd::string GetProfilingOutputPath(bool normalized)
  324. {
  325. return Utils::GetProfilingPath(normalized).String();
  326. }
  327. void LoadLevel(const AZStd::string& levelName)
  328. {
  329. auto operation = [levelName]()
  330. {
  331. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  332. scriptAutomationInterface->LoadLevel(levelName.c_str());
  333. };
  334. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  335. }
  336. bool PrepareForScreenCapture(const AZStd::string& imageName)
  337. {
  338. AZ::Render::FrameCapturePathOutcome pathOutcome;
  339. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  340. pathOutcome, &AZ::Render::FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath, imageName, true);
  341. if (!pathOutcome.IsSuccess())
  342. {
  343. return false;
  344. }
  345. AZStd::string fullFilePath = pathOutcome.GetValue();
  346. if (!AZ::IO::PathView(fullFilePath).IsRelativeTo(Utils::ResolvePath("@user@")))
  347. {
  348. // The main reason we require screenshots to be in a specific folder is to ensure we don't delete or replace some other important file.
  349. AZ_Error("ScriptAutomation", false, "Screenshots must be captured under the '%s' folder. Attempted to save screenshot to '%s'.",
  350. Utils::ResolvePath("@user@").c_str(),
  351. fullFilePath.c_str());
  352. return false;
  353. }
  354. // Delete the file if it already exists
  355. if (AZ::IO::LocalFileIO::GetInstance()->Exists(fullFilePath.c_str()) &&
  356. !AZ::IO::LocalFileIO::GetInstance()->Remove(fullFilePath.c_str()))
  357. {
  358. AZ_Error("ScriptAutomation", false, "Failed to delete existing screenshot file '%s'.", fullFilePath.c_str());
  359. return false;
  360. }
  361. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  362. scriptAutomationInterface->PauseAutomation();
  363. return true;
  364. }
  365. void SetScreenshotFolder(const AZStd::string& screenshotFolder)
  366. {
  367. auto operation = [screenshotFolder]()
  368. {
  369. AZ::Render::FrameCaptureTestRequestBus::Broadcast(
  370. &AZ::Render::FrameCaptureTestRequestBus::Events::SetScreenshotFolder, screenshotFolder);
  371. };
  372. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  373. }
  374. void SetTestEnvPath(const AZStd::string& envPath)
  375. {
  376. auto operation = [envPath]()
  377. {
  378. AZ::Render::FrameCaptureTestRequestBus::Broadcast(&AZ::Render::FrameCaptureTestRequestBus::Events::SetTestEnvPath, envPath);
  379. };
  380. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  381. }
  382. void SetOfficialBaselineImageFolder(const AZStd::string& baselineFolder)
  383. {
  384. auto operation = [baselineFolder]()
  385. {
  386. AZ::Render::FrameCaptureTestRequestBus::Broadcast(
  387. &AZ::Render::FrameCaptureTestRequestBus::Events::SetOfficialBaselineImageFolder, baselineFolder);
  388. };
  389. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  390. }
  391. void SetLocalBaselineImageFolder(const AZStd::string& baselineFolder)
  392. {
  393. auto operation = [baselineFolder]()
  394. {
  395. AZ::Render::FrameCaptureTestRequestBus::Broadcast(
  396. &AZ::Render::FrameCaptureTestRequestBus::Events::SetLocalBaselineImageFolder, baselineFolder);
  397. };
  398. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  399. }
  400. void CaptureScreenshot(const AZStd::string& imageName)
  401. {
  402. auto operation = [imageName]()
  403. {
  404. // Note this will pause the script until the capture is complete
  405. if (PrepareForScreenCapture(imageName))
  406. {
  407. AZ::Render::FrameCapturePathOutcome pathOutcome;
  408. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  409. pathOutcome, &AZ::Render::FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath, imageName, true);
  410. AZ_Assert(pathOutcome.IsSuccess(), "Path check should already be done in PrepareForScreenCapture().");
  411. AZStd::string screenshotFilePath = pathOutcome.GetValue();
  412. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  413. AZ::Render::FrameCaptureOutcome captureOutcome;
  414. AZ::Render::FrameCaptureRequestBus::BroadcastResult(captureOutcome, &AZ::Render::FrameCaptureRequestBus::Events::CaptureScreenshot, screenshotFilePath);
  415. AZ_Error("ScriptAutomation", captureOutcome.IsSuccess(),
  416. "Frame capture initialization failed. %s", captureOutcome.GetError().m_errorMessage.c_str());
  417. if (captureOutcome.IsSuccess())
  418. {
  419. scriptAutomationInterface->SetFrameCaptureId(captureOutcome.GetValue());
  420. }
  421. else
  422. {
  423. AZ_Error("ScriptAutomation", false, "Script: failed to trigger screenshot capture");
  424. scriptAutomationInterface->ResumeAutomation();
  425. }
  426. }
  427. };
  428. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  429. }
  430. void CaptureScreenshotWithPreview(const AZStd::string& imageName)
  431. {
  432. auto operation = [imageName]()
  433. {
  434. // Note this will pause the script until the capture is complete
  435. if (PrepareForScreenCapture(imageName))
  436. {
  437. AZ::Render::FrameCapturePathOutcome pathOutcome;
  438. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  439. pathOutcome, &AZ::Render::FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath, imageName, true);
  440. AZ_Assert(pathOutcome.IsSuccess(), "Path check should already be done in PrepareForScreenCapture().");
  441. AZStd::string screenshotFilePath = pathOutcome.GetValue();
  442. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  443. AZ::Render::FrameCaptureOutcome captureOutcome;
  444. AZ::Render::FrameCaptureRequestBus::BroadcastResult(captureOutcome, &AZ::Render::FrameCaptureRequestBus::Events::CaptureScreenshotWithPreview, screenshotFilePath);
  445. AZ_Error("ScriptAutomation", captureOutcome.IsSuccess(),
  446. "Frame capture initialization failed. %s", captureOutcome.GetError().m_errorMessage.c_str());
  447. if (captureOutcome.IsSuccess())
  448. {
  449. scriptAutomationInterface->SetFrameCaptureId(captureOutcome.GetValue());
  450. }
  451. else
  452. {
  453. AZ_Error("ScriptAutomation", false, "Script: failed to trigger screenshot capture with preview");
  454. scriptAutomationInterface->ResumeAutomation();
  455. }
  456. }
  457. };
  458. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  459. }
  460. void CapturePassAttachment(AZ::ScriptDataContext& dc)
  461. {
  462. // manually parse args as this takes a lua table as an arg
  463. if (dc.GetNumArguments() != 3 && dc.GetNumArguments() != 4)
  464. {
  465. AZ_Error("ScriptAutomation", false, "Script: CapturePassAttachment needs three or four arguments");
  466. return;
  467. }
  468. if (!dc.IsTable(0))
  469. {
  470. AZ_Error("ScriptAutomation", false, "Script: CapturePassAttachment's first argument must be a table of strings");
  471. return;
  472. }
  473. if (!dc.IsString(1) || !dc.IsString(2))
  474. {
  475. AZ_Error("ScriptAutomation", false, "Script: CapturePassAttachment's second and third argument must be strings");
  476. return;
  477. }
  478. if (dc.GetNumArguments() == 4 && !dc.IsString(3))
  479. {
  480. AZ_Error("ScriptAutomation", false, "Script: CapturePassAttachment's forth argument must be a string 'Input' or 'Output'");
  481. return;
  482. }
  483. const char* stringValue = nullptr;
  484. AZStd::vector<AZStd::string> passHierarchy;
  485. AZStd::string slot;
  486. AZStd::string imageName;
  487. // read slot name and output file path
  488. dc.ReadArg(1, stringValue);
  489. slot = AZStd::string(stringValue);
  490. dc.ReadArg(2, stringValue);
  491. imageName = AZStd::string(stringValue);
  492. AZ::RPI::PassAttachmentReadbackOption readbackOption = AZ::RPI::PassAttachmentReadbackOption::Output;
  493. if (dc.GetNumArguments() == 4)
  494. {
  495. AZStd::string option;
  496. dc.ReadArg(3, option);
  497. if (option == "Input")
  498. {
  499. readbackOption = AZ::RPI::PassAttachmentReadbackOption::Input;
  500. }
  501. }
  502. // read pass hierarchy
  503. AZ::ScriptDataContext stringtable;
  504. dc.InspectTable(0, stringtable);
  505. const char* fieldName;
  506. int fieldIndex;
  507. int elementIndex;
  508. while (stringtable.InspectNextElement(elementIndex, fieldName, fieldIndex))
  509. {
  510. if (fieldIndex != -1)
  511. {
  512. if (!stringtable.IsString(elementIndex))
  513. {
  514. AZ_Error("ScriptAutomation", false, "Script: CapturePassAttachment's first argument must contain only strings");
  515. return;
  516. }
  517. const char* stringTableValue = nullptr;
  518. if (stringtable.ReadValue(elementIndex, stringTableValue))
  519. {
  520. passHierarchy.push_back(stringTableValue);
  521. }
  522. }
  523. }
  524. auto operation = [passHierarchy, slot, imageName, readbackOption]()
  525. {
  526. // Note this will pause the script until the capture is complete
  527. if (PrepareForScreenCapture(imageName))
  528. {
  529. AZ::Render::FrameCapturePathOutcome pathOutcome;
  530. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  531. pathOutcome, &AZ::Render::FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath, imageName, true);
  532. AZ_Assert(pathOutcome.IsSuccess(), "Path check should already be done in PrepareForScreenCapture().");
  533. AZStd::string screenshotFilePath = pathOutcome.GetValue();
  534. auto scriptAutomationInterface = ScriptAutomationInterface::Get();
  535. AZ::Render::FrameCaptureOutcome captureOutcome;
  536. AZ::Render::FrameCaptureRequestBus::BroadcastResult(
  537. captureOutcome,
  538. &AZ::Render::FrameCaptureRequestBus::Events::CapturePassAttachment,
  539. screenshotFilePath,
  540. passHierarchy,
  541. slot,
  542. readbackOption);
  543. AZ_Error("ScriptAutomation", captureOutcome.IsSuccess(),
  544. "Frame capture initialization failed. %s", captureOutcome.GetError().m_errorMessage.c_str());
  545. if (captureOutcome.IsSuccess())
  546. {
  547. scriptAutomationInterface->SetFrameCaptureId(captureOutcome.GetValue());
  548. }
  549. else
  550. {
  551. AZ_Error("ScriptAutomation", false, "Script: failed to trigger screenshot capture of pass");
  552. scriptAutomationInterface->ResumeAutomation();
  553. }
  554. }
  555. };
  556. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  557. }
  558. void CompareScreenshots(const AZStd::string& compareName, const AZStd::string& comparisonLevel, const AZStd::string& filePathA, const AZStd::string& filePathB, float minDiffFilter)
  559. {
  560. // capture strings by copy or risk them being deleted before we access them.
  561. auto operation = [=]()
  562. {
  563. const ImageComparisonToleranceLevel* toleranceLevel = ScriptAutomationInterface::Get()->FindToleranceLevel(comparisonLevel);
  564. if (!toleranceLevel)
  565. {
  566. AZ_Error("ScriptAutomation", false, "Failed to find image comparison level named %s", comparisonLevel.c_str());
  567. return;
  568. }
  569. AZStd::string resolvedPathA = ResolvePath(filePathA);
  570. AZStd::string resolvedPathB = ResolvePath(filePathB);
  571. AZ::Render::FrameCaptureComparisonOutcome compareOutcome;
  572. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  573. compareOutcome,
  574. &AZ::Render::FrameCaptureTestRequestBus::Events::CompareScreenshots,
  575. resolvedPathA,
  576. resolvedPathB,
  577. minDiffFilter);
  578. AZ_Error(
  579. "ScriptAutomation",
  580. compareOutcome.IsSuccess(),
  581. "%s screenshot compare error. Error \"%s\"",
  582. compareName.c_str(),
  583. compareOutcome.GetError().m_errorMessage.c_str()
  584. );
  585. if (compareOutcome.IsSuccess())
  586. {
  587. float diffScore = toleranceLevel->m_filterImperceptibleDiffs
  588. ? compareOutcome.GetValue().m_diffScore
  589. : compareOutcome.GetValue().m_filteredDiffScore;
  590. if (diffScore > toleranceLevel->m_threshold)
  591. {
  592. AZ_Error(
  593. "ScriptAutomation",
  594. false,
  595. "%s screenshot compare failed. Diff score %.5f exceeds threshold of %.5f ('%s').",
  596. compareName.c_str(),
  597. diffScore,
  598. toleranceLevel->m_threshold,
  599. toleranceLevel->m_name.c_str()
  600. );
  601. // TODO: open image compare app if CVAR is set
  602. }
  603. else
  604. {
  605. AZ_Printf(
  606. "ScriptAutomation",
  607. "%s screenshot compare passed. Diff score is %.5f, threshold of %.5f ('%s').",
  608. compareName.c_str(),
  609. diffScore,
  610. toleranceLevel->m_threshold,
  611. toleranceLevel->m_name.c_str()
  612. );
  613. }
  614. }
  615. };
  616. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  617. }
  618. void CompareScreenshotToBaseline(const AZStd::string& compareName, const AZStd::string& comparisonLevel, const AZStd::string& imageName, float minDiffFilter)
  619. {
  620. // capture strings by copy or risk them being deleted before we access them.
  621. auto operation = [=]()
  622. {
  623. const ImageComparisonToleranceLevel* toleranceLevel = ScriptAutomationInterface::Get()->FindToleranceLevel(comparisonLevel);
  624. if (!toleranceLevel)
  625. {
  626. AZ_Error("ScriptAutomation", false, "Failed to find image comparison level named %s", comparisonLevel.c_str());
  627. return;
  628. }
  629. // build test image filepath
  630. AZ::Render::FrameCapturePathOutcome pathOutcome;
  631. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  632. pathOutcome, &AZ::Render::FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath, imageName, true);
  633. if (!pathOutcome.IsSuccess())
  634. {
  635. AZ_Error(
  636. "ScriptAutomation",
  637. false,
  638. "%s screenshot compare error. Failed to build screenshot file path for image name %s",
  639. compareName.c_str(),
  640. imageName.c_str());
  641. }
  642. AZStd::string screenshotFilePath = pathOutcome.GetValue();
  643. // build official comparison image filepath
  644. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  645. pathOutcome, &AZ::Render::FrameCaptureTestRequestBus::Events::BuildOfficialBaselineFilePath, imageName, true);
  646. if (!pathOutcome.IsSuccess())
  647. {
  648. AZ_Error(
  649. "ScriptAutomation",
  650. false,
  651. "%s screenshot compare error. Failed to build official baseline file path for image name %s",
  652. compareName.c_str(),
  653. imageName.c_str());
  654. }
  655. AZStd::string baselineFilePath = pathOutcome.GetValue();
  656. // compare test image against the official baseline
  657. AZ::Render::FrameCaptureComparisonOutcome compareOutcome;
  658. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  659. compareOutcome,
  660. &AZ::Render::FrameCaptureTestRequestBus::Events::CompareScreenshots,
  661. screenshotFilePath,
  662. baselineFilePath,
  663. minDiffFilter);
  664. AZ_Error(
  665. "ScriptAutomation",
  666. compareOutcome.IsSuccess(),
  667. "%s screenshot compare error. Error \"%s\"",
  668. compareName.c_str(),
  669. compareOutcome.GetError().m_errorMessage.c_str()
  670. );
  671. if (compareOutcome.IsSuccess())
  672. {
  673. float diffScore = toleranceLevel->m_filterImperceptibleDiffs
  674. ? compareOutcome.GetValue().m_diffScore
  675. : compareOutcome.GetValue().m_filteredDiffScore;
  676. if (diffScore > toleranceLevel->m_threshold)
  677. {
  678. AZ_Error(
  679. "ScriptAutomation",
  680. false,
  681. "%s screenshot compare failed. Diff score %.5f exceeds threshold of %.5f ('%s').",
  682. compareName.c_str(),
  683. diffScore,
  684. toleranceLevel->m_threshold,
  685. toleranceLevel->m_name.c_str()
  686. );
  687. if (sa_launch_image_compare_for_failed_baseline_compare)
  688. {
  689. Utils::RunImageDiff(screenshotFilePath, baselineFilePath, compareName, imageName);
  690. }
  691. }
  692. else
  693. {
  694. AZ_Printf(
  695. "ScriptAutomation",
  696. "%s screenshot compare passed. Diff score is %.5f, threshold of %.5f ('%s').\n",
  697. compareName.c_str(),
  698. diffScore,
  699. toleranceLevel->m_threshold,
  700. toleranceLevel->m_name.c_str()
  701. );
  702. }
  703. }
  704. };
  705. ScriptAutomationInterface::Get()->QueueScriptOperation(AZStd::move(operation));
  706. }
  707. } // namespace Bindings
  708. void ReflectScriptBindings(AZ::BehaviorContext* behaviorContext)
  709. {
  710. AZ::MathReflect(behaviorContext);
  711. AZ::SettingsRegistryScriptUtils::ReflectSettingsRegistryToBehaviorContext(*behaviorContext);
  712. behaviorContext->Method("RunScript", &Bindings::RunScript);
  713. behaviorContext->Method("Print", &Bindings::Print);
  714. behaviorContext->Method("Warning", &Bindings::Warning);
  715. behaviorContext->Method("Error", &Bindings::Error);
  716. behaviorContext->Method("ExecuteConsoleCommand", &Bindings::ExecuteConsoleCommand);
  717. behaviorContext->Method("IdleFrames", &Bindings::IdleFrames);
  718. behaviorContext->Method("IdleSeconds", &Bindings::IdleSeconds);
  719. behaviorContext->Method("ResizeViewport", &Bindings::ResizeViewport);
  720. behaviorContext->Method("SetCamera", &Bindings::SetCamera);
  721. behaviorContext->Method("SplitString", &Bindings::SplitStringImmediate);
  722. behaviorContext->Method("ResolvePath", &Bindings::ResolvePath);
  723. behaviorContext->Method("NormalizePath", [](AZStd::string_view path) -> AZStd::string { return AZ::IO::PathView(path).LexicallyNormal().String(); });
  724. behaviorContext->Method("DegToRad", &AZ::DegToRad);
  725. behaviorContext->Method("GetRenderApiName", &Bindings::GetRenderApiName);
  726. behaviorContext->Method("GetRenderPipelineName", &Bindings::GetRenderPipelineName);
  727. behaviorContext->Method("GetPlatformName", &Bindings::GetPlatformName);
  728. behaviorContext->Method("GetProfilingOutputPath", &Bindings::GetProfilingOutputPath);
  729. behaviorContext->Method("LoadLevel", &Bindings::LoadLevel);
  730. // Screenshots...
  731. behaviorContext->Method("SetScreenshotFolder", &Bindings::SetScreenshotFolder);
  732. behaviorContext->Method("SetTestEnvPath", &Bindings::SetTestEnvPath);
  733. behaviorContext->Method("SetOfficialBaselineImageFolder", &Bindings::SetOfficialBaselineImageFolder);
  734. behaviorContext->Method("SetLocalBaselineImageFolder", &Bindings::SetLocalBaselineImageFolder);
  735. behaviorContext->Method("CaptureScreenshot", &Bindings::CaptureScreenshot);
  736. behaviorContext->Method("CaptureScreenshotWithPreview", &Bindings::CaptureScreenshotWithPreview);
  737. behaviorContext->Method("CapturePassAttachment", &Bindings::CapturePassAttachment);
  738. behaviorContext->Method("CompareScreenshots", &Bindings::CompareScreenshots);
  739. behaviorContext->Method("CompareScreenshotToBaseline", &Bindings::CompareScreenshotToBaseline);
  740. // Profiling data...
  741. behaviorContext->Method("CapturePassTimestamp", &Bindings::CapturePassTimestamp);
  742. behaviorContext->Method("CaptureCpuFrameTime", &Bindings::CaptureCpuFrameTime);
  743. behaviorContext->Method("CapturePassPipelineStatistics", &Bindings::CapturePassPipelineStatistics);
  744. behaviorContext->Method("CaptureCpuProfilingStatistics", &Bindings::CaptureCpuProfilingStatistics);
  745. behaviorContext->Method("CaptureBenchmarkMetadata", &Bindings::CaptureBenchmarkMetadata);
  746. }
  747. } // namespace AZ::ScriptAutomation