Jelajahi Sumber

Add Soak Test To Validate reload notifications of Shader related assets (#260)

* Add Soak Test To Validate reload notifications of Shader related assets

Added new Example called "RPI/ShaderReloadTest".
As an example it provides an ImGUI UI to change the
shader used for the FullscreenTrianglePass which renders
a solid Red, Green or Blue color.

A button to check the expected color is vaialble.

A lau script is available: ShaderReloadSoakTest.bv.lua, which
mimics pressing the buttons and testing for the expected color
when the shader asset changes.

Because this is a new Soak Test it should NOT be added
to any of the automated test suites.

Signed-off-by: galibzon <[email protected]>
galibzon 3 tahun lalu
induk
melakukan
196e8ab3c7

+ 2 - 0
Gem/Code/Source/AtomSampleViewerModule.cpp

@@ -44,6 +44,7 @@
 #include <TransparencyExampleComponent.h>
 #include <DiffuseGIExampleComponent.h>
 #include <SSRExampleComponent.h>
+#include <ShaderReloadTestComponent.h>
 
 #include <RHI/AlphaToCoverageExampleComponent.h>
 #include <RHI/AsyncComputeExampleComponent.h>
@@ -156,6 +157,7 @@ namespace AtomSampleViewer
                 ParallaxMappingExampleComponent::CreateDescriptor(),
                 DiffuseGIExampleComponent::CreateDescriptor(),
                 SSRExampleComponent::CreateDescriptor(),
+                ShaderReloadTestComponent::CreateDescriptor(),
                 });
         }
 

+ 2 - 0
Gem/Code/Source/SampleComponentManager.cpp

@@ -99,6 +99,7 @@
 #include <TransparencyExampleComponent.h>
 #include <DiffuseGIExampleComponent.h>
 #include <SSRExampleComponent.h>
+#include <ShaderReloadTestComponent.h>
 
 #include <Atom/Bootstrap/DefaultWindowBus.h>
 
@@ -282,6 +283,7 @@ namespace AtomSampleViewer
         SampleComponentManager::RegisterSampleComponent(SampleEntry::NewRPISample( "RPI/SceneReloadSoakTest", azrtti_typeid<SceneReloadSoakTestComponent>() ));
         SampleComponentManager::RegisterSampleComponent(SampleEntry::NewRPISample( "RPI/Shading", azrtti_typeid<ShadingExampleComponent>() ));
         SampleComponentManager::RegisterSampleComponent(SampleEntry::NewRPISample( "RPI/StreamingImage", azrtti_typeid<StreamingImageExampleComponent>() ));
+        SampleComponentManager::RegisterSampleComponent(SampleEntry::NewRPISample( "RPI/ShaderReloadTest", azrtti_typeid<ShaderReloadTestComponent>() ));
         SampleComponentManager::RegisterSampleComponent(SampleEntry::NewRPISample( "Features/AreaLight", azrtti_typeid<AreaLightExampleComponent>() ));
         SampleComponentManager::RegisterSampleComponent(SampleEntry::NewRPISample( "Features/Bloom", azrtti_typeid<BloomExampleComponent>() ));
         SampleComponentManager::RegisterSampleComponent(SampleEntry::NewRPISample( "Features/Checkerboard", azrtti_typeid<CheckerboardExampleComponent>(), []() {return (Utils::GetRHIDevice()->GetPhysicalDevice().GetDescriptor().m_vendorId != RHI::VendorId::ARM && Utils::GetRHIDevice()->GetPhysicalDevice().GetDescriptor().m_vendorId != RHI::VendorId::Qualcomm); } ));

+ 391 - 0
Gem/Code/Source/ShaderReloadTestComponent.cpp

@@ -0,0 +1,391 @@
+/*
+ * 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/IO/Path/Path.h>
+#include <AzCore/Utils/Utils.h>
+#include <AzFramework/IO/LocalFileIO.h>
+
+#include <Atom/RPI.Public/View.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+
+#include <Automation/AssetStatusTracker.h>
+
+#include "ShaderReloadTestComponent.h"
+
+namespace AtomSampleViewer
+{
+    void ShaderReloadTestComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class < ShaderReloadTestComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    ShaderReloadTestComponent::ShaderReloadTestComponent()
+    {
+    }
+
+    void ShaderReloadTestComponent::InitTestDataFolders()
+    {
+        m_relativeTestDataFolder = "Shaders/ShaderReloadTest/TestData";
+        m_relativeTempSourceFolder =  "Shaders/ShaderReloadTest/Temp";
+
+        auto io = AZ::IO::LocalFileIO::GetInstance();
+
+        auto projectPath = AZ::Utils::GetProjectPath();
+        AzFramework::StringFunc::Path::Join(projectPath.c_str(), m_relativeTestDataFolder.c_str(), m_absoluteTestDataFolder);
+        if (!io->Exists(m_absoluteTestDataFolder.c_str()))
+        {
+            AZ_Error("ShaderReloadTestComponent", false, "Could not find source folder '%s'. This sample can only be used on dev platforms.", m_absoluteTestDataFolder.c_str());
+            m_absoluteTestDataFolder.clear();
+            return;
+        }
+
+        AzFramework::StringFunc::Path::Join(projectPath.c_str(), m_relativeTempSourceFolder.c_str(), m_absoluteTempSourceFolder);
+        if (!io->CreatePath(m_absoluteTempSourceFolder.c_str()))
+        {
+            AZ_Error("ShaderReloadTestComponent", false, "Could not create temp folder '%s'.", m_absoluteTempSourceFolder.c_str());
+            m_absoluteTempSourceFolder.clear();
+        }
+    }
+
+    void ShaderReloadTestComponent::CopyTestFile(const char * originalName, const char * newName, bool replaceIfExists)
+    {
+        auto io = AZ::IO::LocalFileIO::GetInstance();
+
+        AZStd::string newFilePath;
+        AzFramework::StringFunc::Path::Join(m_absoluteTempSourceFolder.c_str(), newName, newFilePath);
+        if (io->Exists(newFilePath.c_str()))
+        {
+            if (!replaceIfExists)
+            {
+                return;
+            }
+        }
+
+        AZStd::string originalFilePath;
+        AzFramework::StringFunc::Path::Join(m_absoluteTestDataFolder.c_str(), originalName, originalFilePath);
+        if (!io->Exists(originalFilePath.c_str()))
+        {
+            AZ_Error("ShaderReloadTestComponent", false, "Could not find source file '%s'. This sample can only be used on dev platforms.", originalFilePath.c_str());
+            return;
+        }
+
+        // Instead of copying the file using AZ::IO::LocalFileIO, we load the file and write out a new file over top
+        // the destination. This is necessary to make the AP reliably detect the changes (if we just copy the file,
+        // sometimes it recognizes the OS level copy as an updated file and sometimes not).
+
+        AZ::IO::Path copyFrom = AZ::IO::Path(originalFilePath);
+        AZ::IO::Path copyTo = AZ::IO::Path(newFilePath);
+
+        m_fileIoErrorHandler.BusConnect();
+
+        auto readResult = AZ::Utils::ReadFile(copyFrom.c_str());
+        if (!readResult.IsSuccess())
+        {
+            m_fileIoErrorHandler.ReportLatestIOError(readResult.GetError());
+            return;
+        }
+
+        auto writeResult = AZ::Utils::WriteFile(readResult.GetValue(), copyTo.c_str());
+        if (!writeResult.IsSuccess())
+        {
+            m_fileIoErrorHandler.ReportLatestIOError(writeResult.GetError());
+            return;
+        }
+
+        m_fileIoErrorHandler.BusDisconnect();
+    }
+
+    void ShaderReloadTestComponent::DeleteTestFile(const char* tempSourceFile)
+    {
+        AZ::IO::Path deletePath = AZ::IO::Path(m_absoluteTempSourceFolder).Append(tempSourceFile);
+
+        if (AZ::IO::LocalFileIO::GetInstance()->Exists(deletePath.c_str()))
+        {
+            m_fileIoErrorHandler.BusConnect();
+
+            if (!AZ::IO::LocalFileIO::GetInstance()->Remove(deletePath.c_str()))
+            {
+                m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to delete '%s'.", deletePath.c_str()));
+            }
+
+            m_fileIoErrorHandler.BusDisconnect();
+        }
+    }
+
+    bool ShaderReloadTestComponent::ReadInConfig(const AZ::ComponentConfig*)
+    {
+        return true;
+    }
+
+    void ShaderReloadTestComponent::Activate()
+    {
+        InitTestDataFolders();
+
+        AZ::TickBus::Handler::BusConnect();
+        // connect to the bus before creating new pipeline
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusConnect();
+
+        PreloadFullscreenShader();
+
+
+        //AssetStatusTracker assetStatusTracker;
+        //assetStatusTracker.StartTracking();
+        //
+        //AZStd::string shaderSourcePath;
+        //AzFramework::StringFunc::Path::Join(m_relativeTempSourceFolder.c_str(), "Fullscreen.shader", shaderSourcePath);
+        //assetStatusTracker.ExpectAsset(shaderSourcePath, 1);
+        //
+        //// A short wait is necessary as the pipeline creation will fatally fail
+        //// if the "Fullscreen.azshader" doesn't exist.
+        //const uint32_t MaxWaitTimeMillis = 10000;
+        //const uint32_t MilliWaits = 50;
+        //uint32_t timeWaitMillis = 0;
+        //while (!assetStatusTracker.DidExpectedAssetsFinish())
+        //{
+        //    AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(MilliWaits));
+        //    timeWaitMillis += MilliWaits;
+        //    if (timeWaitMillis >= MaxWaitTimeMillis)
+        //    {
+        //        AZ_Error(LogName, false, "Failed to activate this test because exceeded wait time of %u milliseconds", MaxWaitTimeMillis);
+        //        return;
+        //    }
+        //}
+        //assetStatusTracker.StopTracking();
+
+        // The above wait time is just for shader recompilation, it is important to wait a little bit more
+        // for the shader asset to be discoverable when the FullscreenTriangle pass is loaded and tries
+        // to reference the shader. 1 second is plenty and generous.
+        //AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(1000));
+
+    }
+
+    void ShaderReloadTestComponent::PreloadFullscreenShader()
+    {
+        m_initialized = false;
+        CopyTestFile(RedShaderFile, "Fullscreen.azsl");
+        CopyTestFile("Fullscreen.shader.txt", "Fullscreen.shader");
+        m_expectedPixelColor = RED_COLOR;
+
+        AZStd::string shaderAssetPath;
+        AzFramework::StringFunc::Path::Join(m_relativeTempSourceFolder.c_str(), "Fullscreen.azshader", shaderAssetPath);
+
+        AZStd::vector<AZ::AssetCollectionAsyncLoader::AssetToLoadInfo> assetList = {
+            {shaderAssetPath, azrtti_typeid<AZ::RPI::ShaderAsset>()},
+        };
+
+        m_assetLoadManager.LoadAssetsAsync(assetList, [&](AZStd::string_view assetName, [[maybe_unused]] bool success, size_t pendingAssetCount)
+            {
+                AZ_Error(LogName, success, "Error loading asset %s, a crash will occur when OnAllAssetsReadyActivate() is called!", assetName.data());
+                AZ_TracePrintf(LogName, "Asset %s loaded %s. Wait for %zu more assets before full activation\n", assetName.data(), success ? "successfully" : "UNSUCCESSFULLY", pendingAssetCount);
+                if (!pendingAssetCount && !m_initialized)
+                {
+                    OnAllAssetsReadyActivate();
+                }
+            });
+    }
+
+    void ShaderReloadTestComponent::OnAllAssetsReadyActivate()
+    {
+        ActivateFullscreenTrianglePipeline();
+
+        // Create an ImGuiActiveContextScope to ensure the ImGui context on the new pipeline's ImGui pass is activated.
+        m_imguiScope = AZ::Render::ImGuiActiveContextScope::FromPass({ "FullscreenPipeline", "ImGuiPass" });
+        m_imguiSidebar.Activate();
+
+        m_initialized = true;
+    }
+
+    void ShaderReloadTestComponent::Deactivate()
+    {
+        if (m_initialized)
+        {
+            m_imguiSidebar.Deactivate();
+            m_imguiScope = {}; // restores previous ImGui context.
+            DeactivateFullscreenTrianglePipeline();
+            m_initialized = false;
+        }
+
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusDisconnect();
+
+        AZ::TickBus::Handler::BusDisconnect();
+    }
+
+    void ShaderReloadTestComponent::DefaultWindowCreated()
+    {
+        AZ::Render::Bootstrap::DefaultWindowBus::BroadcastResult(m_windowContext,
+            &AZ::Render::Bootstrap::DefaultWindowBus::Events::GetDefaultWindowContext);
+    }
+
+    void ShaderReloadTestComponent::ActivateFullscreenTrianglePipeline()
+    {
+        // save original render pipeline first and remove it from the scene
+        AZ::RPI::ScenePtr defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        m_originalPipeline = defaultScene->GetDefaultRenderPipeline();
+        defaultScene->RemoveRenderPipeline(m_originalPipeline->GetId());
+
+        // add the checker board pipeline
+        const AZStd::string pipelineName("Fullscreen");
+        AZ::RPI::RenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.m_mainViewTagName = "MainCamera";
+        pipelineDesc.m_name = pipelineName;
+        pipelineDesc.m_rootPassTemplate = "FullscreenPipeline";
+        m_cbPipeline = AZ::RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext);
+        defaultScene->AddRenderPipeline(m_cbPipeline);
+        m_cbPipeline->SetDefaultView(m_originalPipeline->GetDefaultView());
+        m_passHierarchy.push_back(pipelineName);
+        m_passHierarchy.push_back("CopyToSwapChain");
+    }
+
+    void ShaderReloadTestComponent::DeactivateFullscreenTrianglePipeline()
+    {
+        // remove cb pipeline before adding original pipeline.
+        if (!m_cbPipeline)
+        {
+            return;
+        }
+
+        AZ::RPI::ScenePtr defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        defaultScene->RemoveRenderPipeline(m_cbPipeline->GetId());
+
+        defaultScene->AddRenderPipeline(m_originalPipeline);
+
+        m_cbPipeline = nullptr;
+        m_passHierarchy.clear();
+    }
+
+    void ShaderReloadTestComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        if (!m_initialized)
+        {
+            return;
+        }
+
+        DrawSidebar();
+    }
+
+    void ShaderReloadTestComponent::DrawSidebar()
+    {
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        ImGui::Text("ShaderReloadTest");
+        if (ScriptableImGui::Button("Red shader"))
+        {
+            m_capturedColorAsString.clear();
+            m_expectedPixelColor = RED_COLOR;
+            CopyTestFile(RedShaderFile, "Fullscreen.azsl");
+        }
+        if (ScriptableImGui::Button("Green shader"))
+        {
+            m_capturedColorAsString.clear();
+            m_expectedPixelColor = GREEN_COLOR;
+            CopyTestFile(GreenShaderFile, "Fullscreen.azsl");
+        }
+        if (ScriptableImGui::Button("Blue shader"))
+        {
+            m_capturedColorAsString.clear();
+            m_expectedPixelColor = BLUE_COLOR;
+            CopyTestFile(BlueShaderFile, "Fullscreen.azsl");
+        }
+
+        ImGui::Spacing();
+
+        ImGui::Text("Expected Color:");
+        ImGui::Text("0x%08X", m_expectedPixelColor);
+
+        ImGui::Spacing();
+
+        if (ScriptableImGui::Button("Check color"))
+        {
+            if (!m_isCapturingRenderOutput)
+            {
+                m_isCapturingRenderOutput = StartRenderOutputCapture();
+            }
+        }
+
+        ImGui::Spacing();
+
+        ImGui::Text("Captured Color:");
+        ImGui::Text(m_capturedColorAsString.c_str());
+
+        m_imguiSidebar.End();
+    }
+
+    bool ShaderReloadTestComponent::StartRenderOutputCapture()
+    {
+        auto captureCallback = [&](const AZ::RPI::AttachmentReadback::ReadbackResult& result)
+        {
+            if (result.m_dataBuffer)
+            {
+                const auto width = result.m_imageDescriptor.m_size.m_width;
+                const auto height = result.m_imageDescriptor.m_size.m_height;
+                uint32_t pixelColor = ReadPixel(result.m_dataBuffer.get()->data(), result.m_imageDescriptor, width/8, height/8);
+                ValidatePixelColor(pixelColor);
+            }
+            else
+            {
+                AZ_Error(LogName, false, "Failed to capture render output attachment");
+                ValidatePixelColor(0);
+                m_capturedColorAsString = "CAPTURE ERROR!";
+            }
+        };
+
+        m_capturedColorAsString.clear();
+        bool startedCapture = false;
+        AZ::Render::FrameCaptureRequestBus::BroadcastResult(
+            startedCapture, &AZ::Render::FrameCaptureRequestBus::Events::CapturePassAttachmentWithCallback, m_passHierarchy,
+            AZStd::string("Output"), captureCallback, AZ::RPI::PassAttachmentReadbackOption::Output);
+        AZ_Error(LogName, startedCapture, "Failed to start CapturePassAttachmentWithCallback");
+        return startedCapture;
+    }
+
+    uint32_t ShaderReloadTestComponent::ReadPixel(const uint8_t* rawRGBAPixelData, const AZ::RHI::ImageDescriptor& imageDescriptor, uint32_t x, uint32_t y) const
+    {
+        const auto width = imageDescriptor.m_size.m_width;
+        const auto height = imageDescriptor.m_size.m_height;
+        AZ_Assert((x < width) && (y < height), "Invalid read pixel location (x, y)=(%u, %u) for width=%u, height=%u", x, y, width, height);
+        auto tmp = reinterpret_cast<const uint32_t *>(rawRGBAPixelData);
+        const uint32_t pixelColor = tmp[ (width * y) + x];
+        if (imageDescriptor.m_format == AZ::RHI::Format::R8G8B8A8_UNORM)
+        {
+            return pixelColor;
+        }
+        else if (imageDescriptor.m_format == AZ::RHI::Format::B8G8R8A8_UNORM)
+        {
+            auto getColorComponent = +[](uint32_t color, int bitPosition) {
+                return (color >> bitPosition) & 0xFF;
+            };
+            const uint32_t blueValue =   getColorComponent(pixelColor, 0);
+            const uint32_t greenValue = getColorComponent(pixelColor, 8);
+            const uint32_t redValue =  getColorComponent(pixelColor, 16);
+            const uint32_t alphaValue =  getColorComponent(pixelColor, 24);
+            return (alphaValue << 24) | (blueValue << 16) | (greenValue << 8) | redValue;
+        }
+        AZ_Error(LogName, false, "Invalid pixel format=%u", aznumeric_cast<uint32_t>(imageDescriptor.m_format));
+        return pixelColor;
+    }
+
+    void ShaderReloadTestComponent::ValidatePixelColor(uint32_t color)
+    {
+        AZ_TracePrintf(LogName, "INFO: Got pixel color=0x%08X, expecting pixel color=0x%08X", color, m_expectedPixelColor);
+        AZ_Error(LogName, m_expectedPixelColor == color, "Invalid pixel color. Got 0x%08X, was expecting 0x%08X", color, m_expectedPixelColor);
+        m_capturedColorAsString = AZStd::string::format("0x%08X", color);
+        m_isCapturingRenderOutput = false;
+    }
+}

+ 117 - 0
Gem/Code/Source/ShaderReloadTestComponent.h

@@ -0,0 +1,117 @@
+/*
+ * 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <Atom/Bootstrap/DefaultWindowBus.h>
+#include <Atom/Feature/ImGui/ImGuiUtils.h>
+
+#include <AzCore/Component/Component.h>
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <AzFramework/Entity/EntityContextBus.h>
+
+#include <Atom/Utils/AssetCollectionAsyncLoader.h>
+
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/Utils.h>
+#include <Utils/FileIOErrorHandler.h>
+
+namespace AtomSampleViewer
+{
+    // This example component updates (upon user, or script input) the shader that is being used
+    // to render a FullscreenTrianglePass, with the purpose on validating that the
+    // shader reload notification events work properly.
+    class ShaderReloadTestComponent final
+        : public AZ::Component
+        , public AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(ShaderReloadTestComponent, "{47540623-2BB6-4A56-A013-760D5CDAD748}");
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        ShaderReloadTestComponent();
+        ~ShaderReloadTestComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        static constexpr char LogName[] = "ShaderReloadTest";
+        static constexpr char RedShaderFile[] = "Fullscreen_RED.azsl";
+        static constexpr char GreenShaderFile[] = "Fullscreen_GREEN.azsl";
+        static constexpr char BlueShaderFile[] = "Fullscreen_BLUE.azsl";
+        // The following colors are specified in the format R8G8B8A8_UNORM (DX12).
+        // Vulkan produces pixels in the format B8G8R8A8_UNORM
+        static constexpr uint32_t RED_COLOR =   0xFF0000FF;
+        static constexpr uint32_t GREEN_COLOR = 0xFF00FF00;
+        static constexpr uint32_t BLUE_COLOR =  0xFFFF0000;
+
+        void InitTestDataFolders();
+        void CopyTestFile(const char * originalName, const char * newName, bool replaceIfExists = true);
+        void DeleteTestFile(const char* tempSourceFile);
+
+        // AZ::Component overrides...
+        bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override;
+
+        // DefaultWindowNotificationBus::Handler overrides...
+        void DefaultWindowCreated() override;
+        
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        void PreloadFullscreenShader();
+        void OnAllAssetsReadyActivate();
+        void ActivateFullscreenTrianglePipeline();
+        void DeactivateFullscreenTrianglePipeline();
+
+        // draw debug menu
+        void DrawSidebar();
+
+        // Starts the process of capturing the render output attachment.
+        // Returns true if the request was successfully enqueued.
+        bool StartRenderOutputCapture();
+
+        // Reads an 0xAABBGGRR pixel from the input buffer.
+        uint32_t ReadPixel(const uint8_t* rawRGBAPixelData, const AZ::RHI::ImageDescriptor& imageDescriptor, uint32_t x, uint32_t y) const;
+
+        // Validates if the given color is the expected color, and prepares for another
+        // render output capture and validation.
+        void ValidatePixelColor(uint32_t color);
+
+        FileIOErrorHandler m_fileIoErrorHandler;
+
+        //! Async asset load. Used to guarantee that "Fullscreen.azshader" exists before
+        //! instantiating the FullscreenTriangle.pass.
+        AZ::AssetCollectionAsyncLoader m_assetLoadManager;
+
+        bool m_initialized = false;
+        AZStd::string m_relativeTestDataFolder;   //< Stores several txt files with contents to be copied over various source asset files.
+        AZStd::string m_relativeTempSourceFolder; //< Folder for temp source asset files. These are what the sample edits and reloads.
+        AZStd::string m_absoluteTestDataFolder;   //< Stores several txt files with contents to be copied over various source asset files.
+        AZStd::string m_absoluteTempSourceFolder; //< Folder for temp source asset files. These are what the sample edits and reloads.
+        bool m_isCapturingRenderOutput = false;
+        uint32_t m_expectedPixelColor;
+        AZStd::string m_capturedColorAsString;
+
+
+        // for checkerboard render pipeline
+        AZ::RPI::RenderPipelinePtr m_cbPipeline;
+        AZ::RPI::RenderPipelinePtr m_originalPipeline;
+        AZStd::shared_ptr<AZ::RPI::WindowContext> m_windowContext;
+        AZStd::vector<AZStd::string> m_passHierarchy; // Used to capture the CopyToSwapChain pass output.
+
+        // debug menu
+        ImGuiSidebar m_imguiSidebar;
+        AZ::Render::ImGuiActiveContextScope m_imguiScope;
+    };
+} // namespace AtomSampleViewer

+ 2 - 0
Gem/Code/atomsampleviewergem_private_files.cmake

@@ -167,6 +167,8 @@ set(FILES
     Source/TonemappingExampleComponent.h
     Source/TransparencyExampleComponent.cpp
     Source/TransparencyExampleComponent.h
+    Source/ShaderReloadTestComponent.cpp
+    Source/ShaderReloadTestComponent.h
     Source/Utils/FileIOErrorHandler.cpp
     Source/Utils/FileIOErrorHandler.h
     Source/Utils/ImGuiAssetBrowser.cpp

+ 8 - 0
Passes/ASV/PassTemplates.azasset

@@ -75,6 +75,14 @@
             {
                 "Name": "SelectorPassTemplate",
                 "Path": "Passes/SelectorPass.pass"
+            },
+            {
+                "Name": "FullscreenPassTemplate",
+                "Path": "Passes/Fullscreen.pass"
+            },
+            {
+                "Name": "FullscreenPipeline",
+                "Path": "Passes/FullscreenPipeline.pass"
             }
         ]
     }

+ 53 - 0
Passes/Fullscreen.pass

@@ -0,0 +1,53 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "PassAsset",
+    "ClassData": {
+        "PassTemplate": {
+            "Name": "FullscreenPassTemplate",
+            "PassClass": "FullScreenTriangle",
+            "Slots": [
+                {
+                    "Name": "Output",
+                    "SlotType": "Output",
+                    "ScopeAttachmentUsage": "RenderTarget",
+                    "LoadStoreAction": {
+                        "ClearValue": {
+                            "Value": [
+                                0.0,
+                                0.0,
+                                0.0,
+                                0.0
+                            ]
+                        },
+                        "LoadAction": "Clear"
+                    }
+                }
+            ],
+            "ImageAttachments": [
+                {
+                    "Name": "OutputAttachment",
+                    "SizeSource": {
+                        "Source": {
+                            "Pass": "Parent",
+                            "Attachment": "SwapChainOutput"
+                        }
+                    },
+                    "FormatSource": {
+                        "Pass": "Parent",
+                        "Attachment": "SwapChainOutput"
+                    }
+                }
+            ],
+            "Connections": [
+                {
+                    "LocalSlot": "Output",
+                    "AttachmentRef": {
+                        "Pass": "This",
+                        "Attachment": "OutputAttachment"
+                    }
+                }
+            ]
+        }
+    }
+}

+ 70 - 0
Passes/FullscreenPipeline.pass

@@ -0,0 +1,70 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "PassAsset",
+    "ClassData": {
+        "PassTemplate": {
+            "Name": "FullscreenPipeline",
+            "PassClass": "ParentPass",
+            "Slots": [
+                {
+                    "Name": "SwapChainOutput",
+                    "SlotType": "InputOutput",
+                    "ScopeAttachmentUsage": "RenderTarget"
+                }
+            ],
+            "PassRequests": [
+                {
+                    "Name": "FullscreenPass",
+                    "TemplateName": "FullscreenPassTemplate",
+                    "PassData": {
+                        "$type": "FullscreenTrianglePassData",
+                        "ShaderAsset": {
+                            "FilePath": "Shaders/ShaderReloadTest/Temp/Fullscreen.shader"
+                        },
+                        "StencilRef": 1,
+                        "PipelineViewTag": "MainCamera"
+                    }
+                },
+                {
+                    "Name": "ImGuiPass",
+                    "TemplateName": "ImGuiPassTemplate",
+                    "Enabled": true,
+                    "Connections": [
+                        {
+                            "LocalSlot": "InputOutput",
+                            "AttachmentRef": {
+                                "Pass": "FullscreenPass",
+                                "Attachment": "Output"
+                            }
+                        }
+                    ],
+                    "PassData": {
+                        "$type": "ImGuiPassData",
+                        "IsDefaultImGui": true
+                    }
+                },
+                {
+                    "Name": "CopyToSwapChain",
+                    "TemplateName": "FullscreenCopyTemplate",
+                    "Connections": [
+                        {
+                            "LocalSlot": "Input",
+                            "AttachmentRef": {
+                                "Pass": "ImGuiPass",
+                                "Attachment": "InputOutput"
+                            }
+                        },
+                        {
+                            "LocalSlot": "Output",
+                            "AttachmentRef": {
+                                "Pass": "Parent",
+                                "Attachment": "SwapChainOutput"
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    }
+}

+ 47 - 0
Scripts/ShaderReloadSoakTest.bv.lua

@@ -0,0 +1,47 @@
+----------------------------------------------------------------------------------------------------
+--
+-- 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.
+--
+-- SPDX-License-Identifier: Apache-2.0 OR MIT
+--
+--
+--
+----------------------------------------------------------------------------------------------------
+
+-- WARNING: This is a soak test, do not add this test to the fully automated
+-- test suites.
+
+OpenSample('RPI/ShaderReloadTest')
+ResizeViewport(500, 500)
+
+
+function TestButton(buttonName)
+    AssetTracking_Start()
+    AssetTracking_ExpectAsset("shaders/shaderreloadtest/temp/fullscreen.shader")
+    SetImguiValue(buttonName, true)
+    AssetTracking_IdleUntilExpectedAssetsFinish(10)
+    -- Even though the shader has been recompiled, it takes a few frames
+    -- for the notification to bubble up and get the screen refreshed.
+    IdleSeconds(0.25)
+
+    SetShowImGui(false)
+    SetImguiValue('Check color', true)
+
+    -- Need a few frames to capture the screen and compare expected color.
+    IdleSeconds(0.25)
+    
+    SetShowImGui(true)
+end
+
+
+-- Fixme. As a Soak Test, this should run for a long time and exit
+-- on the first failure, or upon user request.
+for i=1,5 do
+    -- Always start with "Green shader" or "Blue shader" because when RPI/ShaderReloadTest
+    -- activates it starts with the "Red shader", so updating the source asset
+    --- to the exact same content won't trigger asset recompilation.
+    TestButton("Green shader")
+    TestButton("Blue shader")
+    TestButton("Red shader")
+end

+ 1 - 0
Shaders/ShaderReloadTest/.gitignore

@@ -0,0 +1 @@
+Temp

+ 25 - 0
Shaders/ShaderReloadTest/Temp/Fullscreen.azsl

@@ -0,0 +1,25 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Atom/Features/SrgSemantics.azsli>
+#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
+#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
+
+// Simply displays the texture attached to channel0.
+PSOutput MainPS(VSOutput IN)
+{
+    PSOutput OUT;
+
+    OUT.m_color = float4(1, 0, 0, 1);
+
+    return OUT;
+}

+ 30 - 0
Shaders/ShaderReloadTest/Temp/Fullscreen.shader

@@ -0,0 +1,30 @@
+{ 
+    "Source" : "Fullscreen.azsl",
+
+    "DepthStencilState" : 
+    {
+        "Depth" : 
+        { 
+            "Enable" : false 
+        },
+        "Stencil" :
+        {
+            "Enable" : false
+        }
+    },
+
+    "ProgramSettings":
+    {
+      "EntryPoints":
+      [
+        {
+          "name": "MainVS",
+          "type": "Vertex"
+        },
+        {
+          "name": "MainPS",
+          "type": "Fragment"
+        }
+      ]
+    }   
+}

+ 30 - 0
Shaders/ShaderReloadTest/TestData/Fullscreen.shader.txt

@@ -0,0 +1,30 @@
+{ 
+    "Source" : "Fullscreen.azsl",
+
+    "DepthStencilState" : 
+    {
+        "Depth" : 
+        { 
+            "Enable" : false 
+        },
+        "Stencil" :
+        {
+            "Enable" : false
+        }
+    },
+
+    "ProgramSettings":
+    {
+      "EntryPoints":
+      [
+        {
+          "name": "MainVS",
+          "type": "Vertex"
+        },
+        {
+          "name": "MainPS",
+          "type": "Fragment"
+        }
+      ]
+    }   
+}

+ 25 - 0
Shaders/ShaderReloadTest/TestData/Fullscreen_BLUE.azsl

@@ -0,0 +1,25 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Atom/Features/SrgSemantics.azsli>
+#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
+#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
+
+// Simply displays the texture attached to channel0.
+PSOutput MainPS(VSOutput IN)
+{
+    PSOutput OUT;
+
+    OUT.m_color = float4(0, 0, 1, 1);
+
+    return OUT;
+}

+ 25 - 0
Shaders/ShaderReloadTest/TestData/Fullscreen_GREEN.azsl

@@ -0,0 +1,25 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Atom/Features/SrgSemantics.azsli>
+#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
+#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
+
+// Simply displays the texture attached to channel0.
+PSOutput MainPS(VSOutput IN)
+{
+    PSOutput OUT;
+
+    OUT.m_color = float4(0, 1, 0, 1);
+
+    return OUT;
+}

+ 25 - 0
Shaders/ShaderReloadTest/TestData/Fullscreen_RED.azsl

@@ -0,0 +1,25 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Atom/Features/SrgSemantics.azsli>
+#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
+#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
+
+// Simply displays the texture attached to channel0.
+PSOutput MainPS(VSOutput IN)
+{
+    PSOutput OUT;
+
+    OUT.m_color = float4(1, 0, 0, 1);
+
+    return OUT;
+}