Răsfoiți Sursa

Adding readback sample (#363)

* Adding readback sample

This commit introduce the readback sample. It is designed to render a
pattern into a texture that is then readback to host memory and
reuploaded to the GPU to be rendered.
This is only the shell of the proper test that will be updated as soon
as the changes supporting subresource readback are added.

Signed-off-by: Bindless-Chicken <[email protected]>

* Review fixes

Signed-off-by: Bindless-Chicken <[email protected]>

* Switch resource upload method

Signed-off-by: Bindless-Chicken <[email protected]>
Thomas Poulet 3 ani în urmă
părinte
comite
779a7c68d5

+ 342 - 0
Gem/Code/Source/ReadbackExampleComponent.cpp

@@ -0,0 +1,342 @@
+/*
+ * 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 <ReadbackExampleComponent.h>
+
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Public/RenderPipeline.h>
+#include <Atom/RPI.Public/Pass/FullscreenTrianglePass.h>
+#include <Atom/RPI.Public/Image/ImageSystemInterface.h>
+#include <Atom/RPI.Public/Image/AttachmentImagePool.h>
+#include <Atom/RPI.Public/RPIUtils.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Pass/FullscreenTrianglePassData.h>
+
+#include <Automation/ScriptableImGui.h>
+
+namespace AtomSampleViewer
+{
+    static const char* s_readbackPipelineTemplate = "ReadbackPipelineTemplate";
+    static const char* s_fillerPassTemplate = "ReadbackFillerPassTemplate";
+    static const char* s_previewPassTemplate = "ReadbackPreviewPassTemplate";
+
+    static const char* s_fillerShaderPath = "Shaders/Readback/Filler.azshader";
+    static const char* s_previewShaderPath = "Shaders/Readback/Preview.azshader";
+
+    static const char* s_readbackImageName = "ReadbackImage";
+    static const char* s_previewImageName = "PreviewImage";
+
+    void ReadbackExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<ReadbackExampleComponent, AZ::Component>()->Version(0);
+        }
+    }
+
+    ReadbackExampleComponent::ReadbackExampleComponent()
+    {
+    }
+
+    void ReadbackExampleComponent::Activate()
+    {
+        AZ::TickBus::Handler::BusConnect();
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusConnect();
+
+        ActivatePipeline();
+        CreatePasses();
+
+        m_imguiSidebar.Activate();
+    }
+
+    void ReadbackExampleComponent::Deactivate()
+    {
+        m_imguiSidebar.Deactivate();
+
+        DestroyPasses();
+        DeactivatePipeline();
+
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusDisconnect();
+        AZ::TickBus::Handler::BusDisconnect();
+    }
+
+    void ReadbackExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint scriptTime)
+    {
+        // Readback was completed, we need to update the preview image
+        if (m_textureNeedsUpdate)
+        {
+            UploadReadbackResult();
+
+            AZ_Error("ReadbackExample", m_resourceWidth == m_readbackStat.m_descriptor.m_size.m_width, "Incorrect resource width read back.");
+            AZ_Error("ReadbackExample", m_resourceHeight == m_readbackStat.m_descriptor.m_size.m_height, "Incorrect resource height read back.");
+
+            m_textureNeedsUpdate = false;
+        }
+
+        DrawSidebar();
+    }
+
+    void ReadbackExampleComponent::DefaultWindowCreated()
+    {
+        AZ::Render::Bootstrap::DefaultWindowBus::BroadcastResult(m_windowContext, &AZ::Render::Bootstrap::DefaultWindowBus::Events::GetDefaultWindowContext);
+    }
+
+    void ReadbackExampleComponent::CreatePipeline()
+    {
+        // Create the pipeline shell
+        AZ::RPI::RenderPipelineDescriptor readbackPipelineDesc;
+        readbackPipelineDesc.m_mainViewTagName = "MainCamera";
+        readbackPipelineDesc.m_name = "ReadbackPipeline";
+        readbackPipelineDesc.m_rootPassTemplate = s_readbackPipelineTemplate;
+
+        m_readbackPipeline = AZ::RPI::RenderPipeline::CreateRenderPipelineForWindow(readbackPipelineDesc, *m_windowContext);
+    }
+
+    void ReadbackExampleComponent::ActivatePipeline()
+    {
+        // Create the pipeline
+        CreatePipeline();
+
+        // Setup the pipeline
+        m_originalPipeline = m_scene->GetDefaultRenderPipeline();
+        m_scene->AddRenderPipeline(m_readbackPipeline);
+        m_scene->RemoveRenderPipeline(m_originalPipeline->GetId());
+        m_scene->SetDefaultRenderPipeline(m_readbackPipeline->GetId());
+
+        // Create an ImGuiActiveContextScope to ensure the ImGui context on the new pipeline's ImGui pass is activated.
+        m_imguiScope = AZ::Render::ImGuiActiveContextScope::FromPass({ m_readbackPipeline->GetId().GetCStr(), "ImGuiPass" });
+    }
+
+    void ReadbackExampleComponent::DeactivatePipeline()
+    {
+        m_imguiScope = {}; // restores previous ImGui context.
+
+        m_scene->AddRenderPipeline(m_originalPipeline);
+        m_scene->RemoveRenderPipeline(m_readbackPipeline->GetId());
+
+        m_readbackPipeline = nullptr;
+    }
+
+    void ReadbackExampleComponent::CreatePasses()
+    {
+        DestroyPasses();
+
+        CreateResources();
+
+        CreateFillerPass();
+        CreatePreviewPass();
+
+        // Add the filler and preview passes
+        AZ::RPI::Ptr<AZ::RPI::ParentPass> rootPass = m_readbackPipeline->GetRootPass();
+        rootPass->InsertChild(m_fillerPass, AZ::RPI::ParentPass::ChildPassIndex(0));
+        rootPass->InsertChild(m_previewPass, AZ::RPI::ParentPass::ChildPassIndex(1));
+    }
+
+    void ReadbackExampleComponent::DestroyPasses()
+    {
+        if (!m_fillerPass)
+        {
+            return;
+        }
+
+        m_fillerPass->QueueForRemoval();
+        m_fillerPass = nullptr;
+
+        m_previewPass->QueueForRemoval();
+        m_previewPass = nullptr;
+    }
+
+    void ReadbackExampleComponent::PassesChanged()
+    {
+        DestroyPasses();
+        CreatePasses();
+    }
+
+    void ReadbackExampleComponent::CreateFillerPass()
+    {
+        // Load the shader
+        AZ::Data::AssetId shaderAssetId;
+        AZ::Data::AssetCatalogRequestBus::BroadcastResult(
+            shaderAssetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
+            s_fillerShaderPath, azrtti_typeid<AZ::RPI::ShaderAsset>(), false);
+        if (!shaderAssetId.IsValid())
+        {
+            AZ_Assert(false, "[DisplayMapperPass] Unable to obtain asset id for %s.", s_fillerShaderPath);
+        }
+
+        // Create the compute filler pass
+        AZ::RPI::PassRequest createPassRequest;
+        createPassRequest.m_templateName = AZ::Name(s_fillerPassTemplate);
+        createPassRequest.m_passName = AZ::Name("RenderTargetPass");
+
+        // Fill the pass data
+        AZStd::shared_ptr<AZ::RPI::FullscreenTrianglePassData> passData = AZStd::make_shared<AZ::RPI::FullscreenTrianglePassData>();
+        passData->m_shaderAsset.m_assetId = shaderAssetId;
+        passData->m_shaderAsset.m_filePath = s_fillerShaderPath;
+        createPassRequest.m_passData = AZStd::move(passData);
+
+        // Create the connection for the output slot
+        AZ::RPI::PassConnection connection = { AZ::Name("Output"), {AZ::Name("This"), AZ::Name(s_readbackImageName)} };
+        createPassRequest.m_connections.push_back(connection);
+
+        // Register the imported attachment
+        AZ::RPI::PassImageAttachmentDesc imageAttachment;
+        imageAttachment.m_name = s_readbackImageName;
+        imageAttachment.m_lifetime = AZ::RHI::AttachmentLifetimeType::Imported;
+        imageAttachment.m_assetRef.m_assetId = m_readbackImage->GetAssetId();
+        createPassRequest.m_imageAttachmentOverrides.push_back(imageAttachment);
+
+        // Create the pass
+        m_fillerPass = AZ::RPI::PassSystemInterface::Get()->CreatePassFromRequest(&createPassRequest);
+    }
+
+    void ReadbackExampleComponent::CreatePreviewPass()
+    {
+        // Load the shader
+        AZ::Data::AssetId shaderAssetId;
+        AZ::Data::AssetCatalogRequestBus::BroadcastResult(
+            shaderAssetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
+            s_previewShaderPath, azrtti_typeid<AZ::RPI::ShaderAsset>(), false);
+        if (!shaderAssetId.IsValid())
+        {
+            AZ_Assert(false, "[DisplayMapperPass] Unable to obtain asset id for %s.", s_previewShaderPath);
+        }
+
+        // Create the compute filler pass
+        AZ::RPI::PassRequest createPassRequest;
+        createPassRequest.m_templateName = AZ::Name(s_previewPassTemplate);
+        createPassRequest.m_passName = AZ::Name("PreviewPass");
+
+        AZStd::shared_ptr<AZ::RPI::FullscreenTrianglePassData> passData = AZStd::make_shared<AZ::RPI::FullscreenTrianglePassData>();
+        passData->m_shaderAsset.m_assetId = shaderAssetId;
+        passData->m_shaderAsset.m_filePath = s_previewShaderPath;
+        createPassRequest.m_passData = AZStd::move(passData);
+
+        // Create the connection for the output slot
+        AZ::RPI::PassConnection outputConnection = { AZ::Name("Output"), {AZ::Name("Parent"), AZ::Name("SwapChainOutput")} };
+        createPassRequest.m_connections.push_back(outputConnection);
+        AZ::RPI::PassConnection inputConnection = { AZ::Name("Input"), {AZ::Name("This"), AZ::Name(s_previewImageName)} };
+        createPassRequest.m_connections.push_back(inputConnection);
+
+        // Register the imported attachment
+        AZ::RPI::PassImageAttachmentDesc imageAttachment;
+        imageAttachment.m_name = s_previewImageName;
+        imageAttachment.m_lifetime = AZ::RHI::AttachmentLifetimeType::Imported;
+        imageAttachment.m_assetRef.m_assetId = m_previewImage->GetAssetId();
+        createPassRequest.m_imageAttachmentOverrides.push_back(imageAttachment);
+
+        m_previewPass = AZ::RPI::PassSystemInterface::Get()->CreatePassFromRequest(&createPassRequest);
+    }
+
+    void ReadbackExampleComponent::CreateResources()
+    {
+        AZ::Data::Instance<AZ::RPI::AttachmentImagePool> pool = AZ::RPI::ImageSystemInterface::Get()->GetSystemAttachmentPool();
+
+        // Create the readback target
+        {
+            AZ::RPI::CreateAttachmentImageRequest createRequest;
+            createRequest.m_imageName = AZ::Name(s_readbackImageName);
+            createRequest.m_isUniqueName = false;
+            createRequest.m_imagePool = pool.get();
+            createRequest.m_imageDescriptor = AZ::RHI::ImageDescriptor::Create2D(AZ::RHI::ImageBindFlags::Color | AZ::RHI::ImageBindFlags::ShaderWrite | AZ::RHI::ImageBindFlags::CopyRead | AZ::RHI::ImageBindFlags::CopyWrite, m_resourceWidth, m_resourceHeight, AZ::RHI::Format::R8G8B8A8_UNORM);
+
+            m_readbackImage = AZ::RPI::AttachmentImage::Create(createRequest);
+        }
+
+        // Create the preview image
+        {
+            AZ::RPI::CreateAttachmentImageRequest createRequest;
+            createRequest.m_imageName = AZ::Name(s_previewImageName);
+            createRequest.m_isUniqueName = false;
+            createRequest.m_imagePool = pool.get();
+            createRequest.m_imageDescriptor = AZ::RHI::ImageDescriptor::Create2D(AZ::RHI::ImageBindFlags::ShaderRead | AZ::RHI::ImageBindFlags::CopyRead | AZ::RHI::ImageBindFlags::CopyWrite, m_resourceWidth, m_resourceHeight, AZ::RHI::Format::R8G8B8A8_UNORM);
+
+            m_previewImage = AZ::RPI::AttachmentImage::Create(createRequest);
+        }
+    }
+
+    void ReadbackExampleComponent::PerformReadback()
+    {
+        AZ_Assert(m_fillerPass, "Render target pass is null.");
+
+        if (!m_readback)
+        {
+            m_readback = AZStd::make_shared<AZ::RPI::AttachmentReadback>(AZ::RHI::ScopeId{ "RenderTargetCapture" });
+            m_readback->SetCallback(AZStd::bind(&ReadbackExampleComponent::ReadbackCallback, this, AZStd::placeholders::_1));
+        }
+
+        m_fillerPass->ReadbackAttachment(m_readback, AZ::Name("Output"));
+    }
+
+    void ReadbackExampleComponent::ReadbackCallback(const AZ::RPI::AttachmentReadback::ReadbackResult& result)
+    {
+        const AZ::RHI::ImageSubresourceRange range(0, 0, 0, 0);
+        AZ::RHI::ImageSubresourceLayoutPlaced layout;
+        m_previewImage->GetRHIImage()->GetSubresourceLayouts(range, &layout, nullptr);
+
+        m_textureNeedsUpdate = true;
+        m_resultData = result.m_dataBuffer;
+
+        // Fill the readback stats
+        m_readbackStat.m_name = result.m_name;
+        m_readbackStat.m_bytesRead = result.m_dataBuffer->size();
+        m_readbackStat.m_descriptor = result.m_imageDescriptor;
+    }
+
+    void ReadbackExampleComponent::UploadReadbackResult() const
+    {
+        const AZ::RHI::ImageSubresourceRange range(0, 0, 0, 0);
+        AZ::RHI::ImageSubresourceLayoutPlaced layout;
+        m_previewImage->GetRHIImage()->GetSubresourceLayouts(range, &layout, nullptr);
+        AZ::RHI::ImageUpdateRequest updateRequest;
+        updateRequest.m_image = m_previewImage->GetRHIImage();
+        updateRequest.m_sourceSubresourceLayout = layout;
+        updateRequest.m_sourceData = m_resultData->begin();
+        updateRequest.m_imageSubresourcePixelOffset = AZ::RHI::Origin(0, 0, 0);
+        m_previewImage->UpdateImageContents(updateRequest);
+    }
+
+    void ReadbackExampleComponent::DrawSidebar()
+    {
+        if (m_imguiSidebar.Begin())
+        {
+            ImGui::Text("Readback resource dimensions:");
+            if (ScriptableImGui::SliderInt("Width", reinterpret_cast<int*>(&m_resourceWidth), 1, 2048))
+            {
+                PassesChanged();
+            }
+            if (ScriptableImGui::SliderInt("Height", reinterpret_cast<int*>(&m_resourceHeight), 1, 2048))
+            {
+                PassesChanged();
+            }
+
+            ImGui::Separator();
+            ImGui::NewLine();
+
+            if (ScriptableImGui::Button("Readback")) {
+                PerformReadback();
+            }
+
+            ImGui::NewLine();
+            if (m_resultData)
+            {
+                ImGui::Separator();
+                ImGui::Text("Readback statistics");
+                ImGui::NewLine();
+                ImGui::Text("Name: %s", m_readbackStat.m_name.GetCStr());
+                ImGui::Text("Bytes read: %i", m_readbackStat.m_bytesRead);
+                ImGui::Text("[%i; %i; %i]", m_readbackStat.m_descriptor.m_size.m_width, m_readbackStat.m_descriptor.m_size.m_height, m_readbackStat.m_descriptor.m_size.m_depth);
+                ImGui::Text(AZ::RHI::ToString(m_readbackStat.m_descriptor.m_format));
+
+            }
+
+            m_imguiSidebar.End();
+        }
+    }
+}

+ 109 - 0
Gem/Code/Source/ReadbackExampleComponent.h

@@ -0,0 +1,109 @@
+/*
+ * 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 <CommonSampleComponentBase.h>
+
+#include <AzCore/Component/TickBus.h>
+#include <Atom/Bootstrap/DefaultWindowBus.h>
+
+#include <Atom/RPI.Public/Pass/AttachmentReadback.h>
+
+#include <Utils/ImGuiSidebar.h>
+#include <Atom/Feature/ImGui/ImGuiUtils.h>
+
+namespace AtomSampleViewer
+{
+    //! --- Readback Test ---
+    //! 
+    //! This test is designed to test the readback capabilities of ATOM
+    //! It is built around two custom passes working in tandem. The first
+    //! one generate and fill a texture with a pattern. Using the RPI::Pass
+    //! readback capabilities (ReadbackAttachment) it then reads that result
+    //! back to host memory. Once read back the result is uploaded to device
+    //! memory to be used as a texture input in the second pass that will
+    //! display it for operator verification.
+
+    class ReadbackExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(ReadbackExampleComponent, "{B4221426-D22B-4C06-AAFD-4EA277B44CC8}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        ReadbackExampleComponent();
+        ~ReadbackExampleComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        AZ_DISABLE_COPY_MOVE(ReadbackExampleComponent);
+
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint scriptTime) override;
+
+        // AZ::Render::Bootstrap::DefaultWindowNotificationBus overrides ...
+        void DefaultWindowCreated() override;
+
+        void CreatePipeline();
+        void ActivatePipeline();
+        void DeactivatePipeline();
+
+        void CreatePasses();
+        void DestroyPasses();
+        void PassesChanged();
+
+        void CreateFillerPass();
+        void CreatePreviewPass();
+
+        void CreateResources();
+
+        void PerformReadback();
+        void ReadbackCallback(const AZ::RPI::AttachmentReadback::ReadbackResult& result);
+        void UploadReadbackResult() const;
+
+        void DrawSidebar();
+
+        // Pass used to render the pattern and support the readback operation
+        AZ::RHI::Ptr<AZ::RPI::Pass> m_fillerPass;
+        // Pass used to display the readback result back on the screen
+        AZ::RHI::Ptr<AZ::RPI::Pass> m_previewPass;
+
+        // Image used as the readback source
+        AZ::Data::Instance<AZ::RPI::AttachmentImage> m_readbackImage;
+        // Image used as the readback result destination
+        AZ::Data::Instance<AZ::RPI::AttachmentImage> m_previewImage;
+
+        // Custom pipeline
+        AZStd::shared_ptr<AZ::RPI::WindowContext> m_windowContext;
+        AZ::RPI::RenderPipelinePtr m_readbackPipeline;
+        AZ::RPI::RenderPipelinePtr m_originalPipeline;
+        AZ::Render::ImGuiActiveContextScope m_imguiScope;
+
+        // Readback
+        AZStd::shared_ptr<AZ::RPI::AttachmentReadback> m_readback;
+        // Holder for the host available copy of the readback data
+        AZStd::shared_ptr<AZStd::vector<uint8_t>> m_resultData;
+        struct {
+            AZ::Name m_name;
+            size_t m_bytesRead;
+            AZ::RHI::ImageDescriptor m_descriptor;
+        } m_readbackStat;
+        bool m_textureNeedsUpdate = false;
+
+        ImGuiSidebar m_imguiSidebar;
+
+        uint32_t m_resourceWidth = 512;
+        uint32_t m_resourceHeight = 512;
+    };
+} // namespace AtomSampleViewer

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

@@ -103,6 +103,7 @@
 #include <DiffuseGIExampleComponent.h>
 #include <SSRExampleComponent.h>
 #include <ShaderReloadTestComponent.h>
+#include <ReadbackExampleComponent.h>
 
 #include <Atom/Bootstrap/DefaultWindowBus.h>
 
@@ -286,6 +287,7 @@ namespace AtomSampleViewer
             NewRPISample<MultiRenderPipelineExampleComponent>("MultiRenderPipeline"),
             NewRPISample<MultiSceneExampleComponent>("MultiScene"),
             NewRPISample<MultiViewSingleSceneAuxGeomExampleComponent>("MultiViewSingleSceneAuxGeom"),
+            NewRPISample<ReadbackExampleComponent>("Readback"),
             NewRPISample<RenderTargetTextureExampleComponent>("RenderTargetTexture"),
             NewRPISample<RootConstantsExampleComponent>("RootConstants"),
             NewRPISample<SceneReloadSoakTestComponent>("SceneReloadSoakTest"),

+ 2 - 0
Gem/Code/atomsampleviewergem_private_files.cmake

@@ -150,6 +150,8 @@ set(FILES
     Source/ProceduralSkinnedMesh.h
     Source/ProceduralSkinnedMeshUtils.cpp
     Source/ProceduralSkinnedMeshUtils.h
+    Source/ReadbackExampleComponent.cpp
+    Source/ReadbackExampleComponent.h
     Source/RenderTargetTextureExampleComponent.cpp
     Source/RenderTargetTextureExampleComponent.h
     Source/RootConstantsExampleComponent.h

+ 12 - 0
Passes/ASV/PassTemplates.azasset

@@ -79,6 +79,18 @@
             {
                 "Name": "FullscreenPipeline",
                 "Path": "Passes/FullscreenPipeline.pass"
+            },
+            {
+                "Name": "ReadbackFillerPassTemplate",
+                "Path": "Passes/ReadbackFiller.pass"
+            },
+            {
+                "Name": "ReadbackPreviewPassTemplate",
+                "Path": "Passes/ReadbackPreview.pass"
+            },
+            {
+                "Name": "ReadbackPipelineTemplate",
+                "Path": "Passes/ReadbackPipeline.pass"
             }
         ]
     }

+ 35 - 0
Passes/ReadbackFiller.pass

@@ -0,0 +1,35 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "PassAsset",
+    "ClassData": {
+        "PassTemplate": {
+            "Name": "ReadbackFillerPassTemplate",
+            "PassClass": "FullScreenTriangle",
+            "Slots": [
+                {
+                    "Name": "Output",
+                    "SlotType": "Output",
+                    "ScopeAttachmentUsage": "RenderTarget",
+                    "LoadStoreAction": {
+                        "ClearValue": {
+                            "Value": [
+                                0.0,
+                                0.0,
+                                0.0,
+                                0.0
+                            ]
+                        },
+                        "LoadAction": "Clear"
+                    }
+                }
+            ],
+            "ImageAttachments": [
+                {
+                    "Name": "ReadbackImage",
+                    "Lifetime": "Imported"
+                }
+            ]
+        }
+    }
+}

+ 46 - 0
Passes/ReadbackPipeline.pass

@@ -0,0 +1,46 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "PassAsset",
+    "ClassData": {
+        "PassTemplate": {
+            "Name": "ReadbackPipelineTemplate",
+            "PassClass": "ParentPass",
+            "Slots": [
+                {
+                    "Name": "SwapChainOutput",
+                    "SlotType": "InputOutput"
+                }
+            ],
+            "PassData": {
+                "$type": "PassData",
+                "PipelineGlobalConnections": [
+                    {
+                        "GlobalName": "SwapChainOutput",
+                        "Slot": "SwapChainOutput"
+                    }
+                ]
+            },
+            "PassRequests": [
+                {
+                    "Name": "ImGuiPass",
+                    "TemplateName": "ImGuiPassTemplate",
+                    "Enabled": true,
+                    "Connections": [
+                        {
+                            "LocalSlot": "InputOutput",
+                            "AttachmentRef": {
+                                "Pass": "Parent",
+                                "Attachment": "SwapChainOutput"
+                            }
+                        }
+                    ],
+                    "PassData": {
+                        "$type": "ImGuiPassData",
+                        "IsDefaultImGui": true
+                    }
+                }
+            ]
+        }
+    }
+}

+ 41 - 0
Passes/ReadbackPreview.pass

@@ -0,0 +1,41 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "PassAsset",
+    "ClassData": {
+        "PassTemplate": {
+            "Name": "ReadbackPreviewPassTemplate",
+            "PassClass": "FullScreenTriangle",
+            "Slots": [
+                {
+                    "Name": "Output",
+                    "SlotType": "InputOutput",
+                    "ScopeAttachmentUsage": "RenderTarget",
+                    "LoadStoreAction": {
+                        "ClearValue": {
+                            "Value": [
+                                0.0,
+                                0.0,
+                                0.0,
+                                0.0
+                            ]
+                        },
+                        "LoadAction": "Clear"
+                    }
+                },
+                {
+                    "Name": "Input",
+                    "SlotType": "Input",
+                    "ScopeAttachmentUsage": "Shader",
+                    "ShaderInputName": "m_texture"
+                }
+            ],
+            "ImageAttachments": [
+                {
+                    "Name": "PreviewImage",
+                    "Lifetime": "Imported"
+                }
+            ]
+        }
+    }
+}

+ 3 - 0
Scripts/ExpectedScreenshots/Readback/screenshot_1.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:59a8ac8cd849f561cd0ac2f54f48e40a064da3e1586ac2805a49f0e69607ed2f
+size 5710

+ 3 - 0
Scripts/ExpectedScreenshots/Readback/screenshot_2.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:57f8c128273a069d83682acdf285f1deb550ad2cc2654b32387fc4de90da2afa
+size 21673

+ 43 - 0
Scripts/ReadbackTest.bv.lua

@@ -0,0 +1,43 @@
+----------------------------------------------------------------------------------------------------
+--
+-- 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
+--
+--
+--
+----------------------------------------------------------------------------------------------------
+
+g_screenshotOutputFolder = ResolvePath('@user@/Scripts/Screenshots/Readback')
+Print('Saving screenshots to ' .. NormalizePath(g_screenshotOutputFolder))
+
+OpenSample('RPI/Readback')
+
+SelectImageComparisonToleranceLevel("Level H")
+SetShowImGui(false)
+
+-- First capture at 512x512
+ResizeViewport(512, 512)
+SetImguiValue('Width', 512)
+SetImguiValue('Height', 512)
+IdleFrames(1)
+SetImguiValue('Readback', true)
+IdleFrames(5)
+CaptureScreenshot(g_screenshotOutputFolder .. '/screenshot_1.png')
+IdleFrames(1)
+
+-- Then at 1024x1024
+ResizeViewport(1024, 1024)
+SetImguiValue('Width', 1024)
+SetImguiValue('Height', 1024)
+IdleFrames(1)
+SetImguiValue('Readback', true)
+IdleFrames(5)
+CaptureScreenshot(g_screenshotOutputFolder .. '/screenshot_2.png')
+IdleFrames(1)
+
+
+SetShowImGui(true)
+OpenSample(nil)
+

+ 1 - 0
Scripts/_FullTestSuite_.bv.lua

@@ -64,6 +64,7 @@ tests= {
     RunScriptWrapper('scripts/shadowedsponzatest.bv.luac'),
     RunScriptWrapper('scripts/RenderTargetTexture.bv.luac'),
     RunScriptWrapper('scripts/PassTree.bv.luac'),
+    RunScriptWrapper('scripts/ReadbackTest.bv.luac'),
 
     --Fast checking for the samples which don't have a test. Samples should be removed from this list once they have their own tests
 

+ 17 - 0
Shaders/Readback/Filler.azsl

@@ -0,0 +1,17 @@
+/*
+ * 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 <Atom/Features/PostProcessing/FullscreenVertex.azsli>
+#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
+
+PSOutput MainPS(VSOutput IN)
+{
+    PSOutput OUT;
+    OUT.m_color = float4(IN.m_texCoord.xy,0,1);
+    return OUT;
+}; 

+ 34 - 0
Shaders/Readback/Filler.shader

@@ -0,0 +1,34 @@
+{
+    "Source" : "Filler.azsl",
+
+    "DepthStencilState" : { 
+        "Depth" : { 
+            "Enable" : false
+        }
+    },
+
+    "RasterState" : {
+        "DepthClipEnable" : false
+    },
+
+    "BlendState" : {
+        "Enable" : false
+    },
+
+    "DrawList" : "auxgeom",
+
+    "ProgramSettings":
+    {
+        "EntryPoints":
+        [
+            {
+                "name": "MainVS",
+                "type": "Vertex"
+            },
+            {
+                "name": "MainPS",
+                "type": "Fragment"
+            }
+        ]
+    }
+}

+ 31 - 0
Shaders/Readback/Preview.azsl

@@ -0,0 +1,31 @@
+/*
+ * 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 <Atom/Features/SrgSemantics.azsli>
+
+#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
+#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
+
+ShaderResourceGroup RenderImageSrg : SRG_PerPass
+{
+    Texture2D m_texture;
+    Sampler m_sampler
+    {
+        MaxAnisotropy = 16;
+        AddressU = Wrap;
+        AddressV = Wrap;
+        AddressW = Wrap;
+    };
+}
+
+PSOutput MainPS(VSOutput IN)
+{
+    PSOutput OUT;
+    OUT.m_color = RenderImageSrg::m_texture.SampleLevel(RenderImageSrg::m_sampler, IN.m_texCoord.xy, 0).rgba;
+    return OUT;
+}; 

+ 34 - 0
Shaders/Readback/Preview.shader

@@ -0,0 +1,34 @@
+{
+    "Source" : "Preview.azsl",
+
+    "DepthStencilState" : { 
+        "Depth" : { 
+            "Enable" : false
+        }
+    },
+
+    "RasterState" : {
+        "DepthClipEnable" : false
+    },
+
+    "BlendState" : {
+        "Enable" : false
+    },
+
+    "DrawList" : "auxgeom",
+
+    "ProgramSettings":
+    {
+        "EntryPoints":
+        [
+            {
+                "name": "MainVS",
+                "type": "Vertex"
+            },
+            {
+                "name": "MainPS",
+                "type": "Fragment"
+            }
+        ]
+    }
+}