Ver Fonte

Multi-GPU RHI Sample

Signed-off-by: Martin Winter <[email protected]>
Martin Winter há 1 ano atrás
pai
commit
cb70dd8792

+ 711 - 0
Gem/Code/Source/RHI/MultiGPUExampleComponent.cpp

@@ -0,0 +1,711 @@
+/*
+ * 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 <RHI/MultiGPUExampleComponent.h>
+#include <Utils/Utils.h>
+
+#include <SampleComponentManager.h>
+
+#include <Atom/RHI/CommandList.h>
+#include <Atom/RHI.Reflect/InputStreamLayoutBuilder.h>
+#include <Atom/RHI.Reflect/RenderAttachmentLayoutBuilder.h>
+#include <Atom/RPI.Public/Shader/Shader.h>
+#include <Atom/RHI.Reflect/ImageScopeAttachmentDescriptor.h>
+#include <Atom/RPI.Reflect/Shader/ShaderAsset.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <Atom/RHI/MultiDeviceDrawItem.h>
+#include <Atom/RHI/MultiDeviceCopyItem.h>
+#include <Atom/RHI.Reflect/BufferDescriptor.h>
+
+using namespace AZ;
+
+namespace AtomSampleViewer
+{
+    void MultiGPUExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MultiGPUExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    void MultiGPUExampleComponent::OnFramePrepare(AZ::RHI::FrameGraphBuilder& frameGraphBuilder)
+    {
+        static float time = 0.0f;
+        time += 0.005f;
+
+        // Move the triangle around.
+        AZ::Vector3 translation(
+            sinf(time) * 0.25f,
+            cosf(time) * 0.25f,
+            0.0f);
+
+        if (m_shaderResourceGroupShared)
+        {
+            [[maybe_unused]] bool success =
+                m_shaderResourceGroupShared->SetConstant(m_objectMatrixConstantIndex, AZ::Matrix4x4::CreateTranslation(translation));
+            AZ_Warning("MultiGPUExampleComponent", success, "Failed to set SRG Constant m_objectMatrix");
+            m_shaderResourceGroupShared->Compile();
+        }
+
+        BasicRHIComponent::OnFramePrepare(frameGraphBuilder);
+    }
+
+    void MultiGPUExampleComponent::FrameBeginInternal(AZ::RHI::FrameGraphBuilder& frameGraphBuilder)
+    {
+        if (m_outputWidth != m_imageWidth || m_outputHeight != m_imageHeight)
+        {
+            // MultiDeviceImage used as color attachment
+            {
+                m_image = aznew RHI::MultiDeviceImage;
+                RHI::MultiDeviceImageInitRequest initImageRequest;
+                initImageRequest.m_image = m_image.get();
+                initImageRequest.m_descriptor = RHI::ImageDescriptor::Create2D(
+                    RHI::ImageBindFlags::Color | RHI::ImageBindFlags::ShaderReadWrite | RHI::ImageBindFlags::CopyRead, m_outputWidth, m_outputHeight, m_outputFormat);
+                m_imagePool->InitImage(initImageRequest);
+            }
+
+            // MultiDeviceImage holds rendered texture from GPU1 (on GPU0)
+            {
+                m_transferImage = aznew RHI::MultiDeviceImage;
+                RHI::MultiDeviceImageInitRequest initImageRequest;
+                initImageRequest.m_image = m_transferImage.get();
+                initImageRequest.m_descriptor = RHI::ImageDescriptor::Create2D(
+                    RHI::ImageBindFlags::Color | RHI::ImageBindFlags::ShaderRead | RHI::ImageBindFlags::CopyWrite, m_outputWidth, m_outputHeight,
+                    m_outputFormat);
+                m_transferImagePool->InitImage(initImageRequest);
+            }
+
+            RHI::BufferBindFlags stagingBufferBindFlags{ RHI::BufferBindFlags::CopyWrite | RHI::BufferBindFlags::CopyRead };
+
+            {
+                m_stagingBufferToGPU = aznew RHI::MultiDeviceBuffer;
+                AZStd::vector<unsigned int> initialData(m_outputWidth * m_outputHeight, 0xFFFF00FFu);
+
+                RHI::MultiDeviceBufferInitRequest request;
+                request.m_buffer = m_stagingBufferToGPU.get();
+                request.m_descriptor = RHI::BufferDescriptor{stagingBufferBindFlags, initialData.size() * sizeof(unsigned int)};
+                //? Check BindFlags
+                request.m_initialData = initialData.data();
+                if (m_stagingBufferPoolToGPU->InitBuffer(request) != RHI::ResultCode::Success)
+                {
+                    AZ_Error("MultiGPUExampleComponent", false, "StagingBufferToGPU was not created");
+                }
+            }
+
+            {
+                m_stagingBufferToCPU = aznew RHI::MultiDeviceBuffer;
+                RHI::MultiDeviceBufferInitRequest request;
+                request.m_buffer = m_stagingBufferToCPU.get();
+                request.m_descriptor =
+                    RHI::BufferDescriptor{ stagingBufferBindFlags, m_outputWidth * m_outputHeight * sizeof(unsigned int) }; //? Check BindFlags
+                if (m_stagingBufferPoolToCPU->InitBuffer(request) != RHI::ResultCode::Success)
+                {
+                    AZ_Error("MultiGPUExampleComponent", false, "StagingBufferToCPU was not created");
+                }
+            }
+
+            m_scissors[0].m_maxX = m_outputWidth / 2 + 1;
+            m_scissors[1].m_minX = m_outputWidth / 2;
+            m_scissors[1].m_maxX = m_outputWidth;
+
+            m_imageWidth = m_outputWidth;
+            m_imageHeight = m_outputHeight;
+        }
+
+        frameGraphBuilder.GetAttachmentDatabase().ImportImage(
+            m_imageAttachmentIds[0], m_image);
+
+        frameGraphBuilder.GetAttachmentDatabase().ImportImage(
+            m_imageAttachmentIds[1], m_transferImage);
+
+        frameGraphBuilder.GetAttachmentDatabase().ImportBuffer(
+            m_bufferAttachmentIds[0], m_stagingBufferToGPU);
+
+        frameGraphBuilder.GetAttachmentDatabase().ImportBuffer(
+            m_bufferAttachmentIds[1], m_stagingBufferToCPU);
+
+        RHI::SingleDeviceBufferMapRequest request{};
+        request.m_buffer = m_stagingBufferToCPU->GetDeviceBuffer(1).get();
+        request.m_byteCount = m_imageWidth * m_imageHeight * sizeof(uint32_t);
+
+        RHI::SingleDeviceBufferMapResponse response{};
+
+        m_stagingBufferPoolToCPU->GetDeviceBufferPool(1)->MapBuffer(request, response);
+
+        [[maybe_unused]] uint32_t* source = reinterpret_cast<uint32_t*>(response.m_data);
+
+        request.m_buffer = m_stagingBufferToGPU->GetDeviceBuffer(0).get();
+
+        m_stagingBufferPoolToGPU->GetDeviceBufferPool(0)->MapBuffer(request, response);
+
+        uint32_t* destination = reinterpret_cast<uint32_t*>(response.m_data);
+
+        //memset(destination, 0x80, request.m_byteCount);
+
+        memcpy(destination, source, request.m_byteCount);
+
+        /*for (auto i= 0 ; i < 1920 * 8; i++)
+            destination[i + 1080/2*1920] = 0xFFFFFFFF;*/
+
+        m_stagingBufferPoolToCPU->GetDeviceBufferPool(1)->UnmapBuffer(*m_stagingBufferToCPU->GetDeviceBuffer(1));
+        m_stagingBufferPoolToGPU->GetDeviceBufferPool(0)->UnmapBuffer(*m_stagingBufferToGPU->GetDeviceBuffer(0));
+    }
+
+    MultiGPUExampleComponent::MultiGPUExampleComponent()
+    {
+        m_supportRHISamplePipeline = true;
+    }
+
+    void MultiGPUExampleComponent::Activate()
+    {
+        AZ_Error("MultiGPUExampleComponent", RHI::RHISystemInterface::Get()->GetDeviceCount() >= 2, "At least 2 devices required to run this sample");
+
+        m_device_1 = RHI::RHISystemInterface::Get()->GetDevice(0);
+        m_device_2 = RHI::RHISystemInterface::Get()->GetDevice(1);
+
+        m_deviceMask_1 = RHI::MultiDevice::DeviceMask{ 1u << 0 };
+        m_deviceMask_2 = RHI::MultiDevice::DeviceMask{ 1u << 1 };
+        m_deviceMask = m_deviceMask_1 | m_deviceMask_2;
+
+        // Create multi-device resources
+
+        // MultiDeviceImagePool for the render target texture
+        {
+            m_imagePool = aznew RHI::MultiDeviceImagePool;
+            m_imagePool->SetName(Name("RenderTexturePool"));
+
+            RHI::ImagePoolDescriptor imagePoolDescriptor{};
+            imagePoolDescriptor.m_bindFlags =
+                RHI::ImageBindFlags::Color | RHI::ImageBindFlags::ShaderReadWrite | RHI::ImageBindFlags::CopyRead | RHI::ImageBindFlags::CopyWrite;
+
+            if (m_imagePool->Init(m_deviceMask, imagePoolDescriptor) != RHI::ResultCode::Success)
+            {
+                AZ_Error("MultiGPUExampleComponent", false, "Failed to initialize render texture image pool.");
+                return;
+            }
+        }
+
+        // MultiDeviceImagePool used to transfer the rendered texture from GPU 1 -> GPU 0
+        {
+            m_transferImagePool = aznew RHI::MultiDeviceImagePool;
+            m_transferImagePool->SetName(Name("TransferImagePool"));
+
+            RHI::ImagePoolDescriptor imagePoolDescriptor{};
+            imagePoolDescriptor.m_bindFlags =
+                RHI::ImageBindFlags::ShaderReadWrite | RHI::ImageBindFlags::CopyRead | RHI::ImageBindFlags::CopyWrite;
+
+            if (m_transferImagePool->Init(m_deviceMask_1, imagePoolDescriptor) != RHI::ResultCode::Success)
+            {
+                AZ_Error("MultiGPUExampleComponent", false, "Failed to initialize transfer image pool.");
+                return;
+            }
+        }
+
+        RHI::BufferBindFlags stagingBufferBindFlags{ RHI::BufferBindFlags::CopyWrite | RHI::BufferBindFlags::CopyRead };
+
+        // Create staging buffer pool for buffer copy to the GPU
+        {
+            m_stagingBufferPoolToGPU = aznew RHI::MultiDeviceBufferPool;
+
+            RHI::BufferPoolDescriptor bufferPoolDesc;
+            bufferPoolDesc.m_bindFlags = stagingBufferBindFlags;
+            bufferPoolDesc.m_heapMemoryLevel = RHI::HeapMemoryLevel::Host;
+            bufferPoolDesc.m_hostMemoryAccess = RHI::HostMemoryAccess::Write;
+            if (m_stagingBufferPoolToGPU->Init(m_deviceMask_1, bufferPoolDesc) != RHI::ResultCode::Success)
+            {
+                AZ_Error("MultiGPUExampleComponent", false, "StagingBufferPoolToGPU was not initialized");
+            }
+        }
+
+        // Create staging buffer pools for buffer copy to the CPU
+        {
+            m_stagingBufferPoolToCPU = aznew RHI::MultiDeviceBufferPool;
+
+            RHI::BufferPoolDescriptor bufferPoolDesc;
+            bufferPoolDesc.m_bindFlags = stagingBufferBindFlags;
+            bufferPoolDesc.m_heapMemoryLevel = RHI::HeapMemoryLevel::Host;
+            bufferPoolDesc.m_hostMemoryAccess = RHI::HostMemoryAccess::Read;
+            if (m_stagingBufferPoolToCPU->Init(m_deviceMask_2, bufferPoolDesc) != RHI::ResultCode::Success)
+            {
+                AZ_Error("MultiGPUExampleComponent", false, "StagingBufferPoolToCPU was not created");
+            }
+        }
+
+        // Setup main and secondary pipeline
+        CreateRenderScopeProducer();
+        CreateCopyToCPUScopeProducer();
+        CreateCopyToGPUScopeProducer();
+        CreateCompositeScopeProducer();
+
+        RHI::RHISystemNotificationBus::Handler::BusConnect();
+    }
+
+    void MultiGPUExampleComponent::Deactivate()
+    {
+        m_inputAssemblyBuffer = nullptr;
+        m_inputAssemblyBufferPool = nullptr;
+        m_pipelineState = nullptr;
+        m_shaderResourceGroupShared = nullptr;
+
+        m_stagingBufferPoolToGPU = nullptr;
+        m_stagingBufferToGPU = nullptr;
+        m_inputAssemblyBufferPoolComposite = nullptr;
+        m_inputAssemblyBufferComposite = nullptr;
+        m_pipelineStateComposite = nullptr;
+        m_shaderResourceGroupComposite = nullptr;
+        m_shaderResourceGroupDataComposite = RHI::MultiDeviceShaderResourceGroupData{};
+        m_shaderResourceGroupPoolComposite = nullptr;
+
+        m_stagingBufferPoolToCPU = nullptr;
+        m_stagingBufferToCPU = nullptr;
+
+        RHI::RHISystemNotificationBus::Handler::BusDisconnect();
+        m_windowContext = nullptr;
+        m_scopeProducers.clear();
+        m_secondaryScopeProducers.clear();
+    }
+
+    void MultiGPUExampleComponent::CreateRenderScopeProducer()
+    {
+        RHI::PipelineStateDescriptorForDraw pipelineStateDescriptor;
+
+        {
+            m_inputAssemblyBufferPool = aznew RHI::MultiDeviceBufferPool;
+
+            RHI::BufferPoolDescriptor bufferPoolDesc;
+            bufferPoolDesc.m_bindFlags = RHI::BufferBindFlags::InputAssembly;
+            bufferPoolDesc.m_heapMemoryLevel = RHI::HeapMemoryLevel::Device;
+            m_inputAssemblyBufferPool->Init(m_deviceMask, bufferPoolDesc);
+
+            BufferDataTrianglePass bufferData;
+
+            SetVertexPosition(bufferData.m_positions.data(), 0,  0.0,  0.5, 0.0);
+            SetVertexPosition(bufferData.m_positions.data(), 1, -0.5, -0.5, 0.0);
+            SetVertexPosition(bufferData.m_positions.data(), 2,  0.5, -0.5, 0.0);
+
+            SetVertexColor(bufferData.m_colors.data(), 0, 1.0, 0.0, 0.0, 1.0);
+            SetVertexColor(bufferData.m_colors.data(), 1, 0.0, 1.0, 0.0, 1.0);
+            SetVertexColor(bufferData.m_colors.data(), 2, 0.0, 0.0, 1.0, 1.0);
+
+            SetVertexIndexIncreasing(bufferData.m_indices.data(), bufferData.m_indices.size());
+
+            m_inputAssemblyBuffer = aznew RHI::MultiDeviceBuffer;
+
+            RHI::MultiDeviceBufferInitRequest request;
+            request.m_buffer = m_inputAssemblyBuffer.get();
+            request.m_descriptor = RHI::BufferDescriptor{ RHI::BufferBindFlags::InputAssembly, sizeof(bufferData) };
+            request.m_initialData = &bufferData;
+            m_inputAssemblyBufferPool->InitBuffer(request);
+
+            m_streamBufferViews[0] = { *m_inputAssemblyBuffer,
+                                       offsetof(BufferDataTrianglePass, m_positions), sizeof(BufferDataTrianglePass::m_positions),
+                                       sizeof(VertexPosition) };
+
+            m_streamBufferViews[1] = { *m_inputAssemblyBuffer,
+                                       offsetof(BufferDataTrianglePass, m_colors), sizeof(BufferDataTrianglePass::m_colors),
+                                       sizeof(VertexColor) };
+
+            RHI::InputStreamLayoutBuilder layoutBuilder;
+            layoutBuilder.AddBuffer()->Channel("POSITION", RHI::Format::R32G32B32_FLOAT);
+            layoutBuilder.AddBuffer()->Channel("COLOR", RHI::Format::R32G32B32A32_FLOAT);
+            pipelineStateDescriptor.m_inputStreamLayout = layoutBuilder.End();
+
+            RHI::ValidateStreamBufferViews(pipelineStateDescriptor.m_inputStreamLayout, m_streamBufferViews);
+        }
+
+        {
+            const char* triangleShaderFilePath = "Shaders/RHI/triangle.azshader";
+            const char* sampleName = "MultiGPUExample";
+
+            auto shader = LoadShader(triangleShaderFilePath, sampleName);
+            if (shader == nullptr)
+                return;
+
+            auto shaderOptionGroup = shader->CreateShaderOptionGroup();
+            shaderOptionGroup.SetUnspecifiedToDefaultValues();
+
+            // This is an example of how to set different shader options when searching for the shader variant you want to display
+            // Searching by id is simple, but suboptimal. Here it's only used to demonstrate the principle,
+            // but in practice the ShaderOptionIndex and the ShaderOptionValue should be cached for better performance
+            // You can also try DrawMode::Green, DrawMode::Blue or DrawMode::White. The specified color will appear on top of the triangle.
+            shaderOptionGroup.SetValue(AZ::Name("o_drawMode"),  AZ::Name("DrawMode::Red"));
+
+            auto shaderVariant = shader->GetVariant(shaderOptionGroup.GetShaderVariantId());
+
+            shaderVariant.ConfigurePipelineState(pipelineStateDescriptor);
+
+            RHI::RenderAttachmentLayoutBuilder attachmentsBuilder;
+            attachmentsBuilder.AddSubpass()
+                ->RenderTargetAttachment(m_outputFormat);
+            [[maybe_unused]] RHI::ResultCode result = attachmentsBuilder.End(pipelineStateDescriptor.m_renderAttachmentConfiguration.m_renderAttachmentLayout);
+            AZ_Assert(result == RHI::ResultCode::Success, "Failed to create render attachment layout");
+
+            m_pipelineState = shader->AcquirePipelineState(pipelineStateDescriptor);
+            if (!m_pipelineState)
+            {
+                AZ_Error(sampleName, false, "Failed to acquire default pipeline state for shader '%s'", triangleShaderFilePath);
+                return;
+            }
+
+            m_shaderResourceGroupShared = CreateShaderResourceGroup(shader, "TriangleInstanceSrg", sampleName);
+
+            const Name objectMatrixConstantId{ "m_objectMatrix" };
+            FindShaderInputIndex(&m_objectMatrixConstantIndex, m_shaderResourceGroupShared, objectMatrixConstantId, sampleName);
+
+            // In practice m_shaderResourceGroupShared should be one of the cached SRGs owned by the DrawItem
+            if (!shaderVariant.IsFullyBaked() && m_shaderResourceGroupShared->HasShaderVariantKeyFallbackEntry())
+            {
+                // Normally if the requested variant isn't an exact match we have to set it by SetShaderVariantKeyFallbackValue
+                // In most cases this should be the preferred behavior:
+                m_shaderResourceGroupShared->SetShaderVariantKeyFallbackValue(shaderOptionGroup.GetShaderVariantKeyFallbackValue());
+                AZ_Warning(
+                    sampleName, false, "Check the Triangle.shader file - some program variants haven't been baked ('%s')",
+                    triangleShaderFilePath);
+            }
+        }
+
+        // Creates a scope for rendering the triangle.
+        {
+            struct ScopeData
+            {
+                bool second{false};
+            };
+
+            const auto prepareFunction = [this](RHI::FrameGraphInterface frameGraph, [[maybe_unused]] ScopeData& scopeData)
+            {
+                // Binds the swap chain as a color attachment. Clears it to white.
+                RHI::ImageScopeAttachmentDescriptor descriptor;
+                descriptor.m_attachmentId = m_imageAttachmentIds[0];
+                descriptor.m_loadStoreAction.m_loadAction = RHI::AttachmentLoadAction::Clear;
+                descriptor.m_loadStoreAction.m_storeAction = RHI::AttachmentStoreAction::Store;
+                descriptor.m_loadStoreAction.m_clearValue.m_vector4Uint = {0, 0, 0, 0};
+                frameGraph.UseColorAttachment(descriptor);
+
+                // We will submit a single draw item.
+                frameGraph.SetEstimatedItemCount(1);
+            };
+
+            RHI::EmptyCompileFunction<ScopeData> compileFunction;
+
+            const auto executeFunction = [this](const RHI::FrameGraphExecuteContext& context, [[maybe_unused]] const ScopeData& scopeData)
+            {
+                RHI::CommandList* commandList = context.GetCommandList();
+
+                // Set persistent viewport and scissor state.
+                commandList->SetViewports(&m_viewport, 1);
+                commandList->SetScissors(&m_scissors[int(scopeData.second)], 1);
+
+                const RHI::SingleDeviceIndexBufferView indexBufferView = {
+                    *m_inputAssemblyBuffer->GetDeviceBuffer(context.GetDeviceIndex()),
+                    offsetof(BufferDataTrianglePass, m_indices), sizeof(BufferDataTrianglePass::m_indices), RHI::IndexFormat::Uint16
+                };
+
+                RHI::DrawIndexed drawIndexed;
+                drawIndexed.m_indexCount = 3;
+                drawIndexed.m_instanceCount = 1;
+
+                const RHI::SingleDeviceShaderResourceGroup* shaderResourceGroups[] = { m_shaderResourceGroupShared->GetRHIShaderResourceGroup()->GetDeviceShaderResourceGroup(context.GetDeviceIndex()).get() };
+
+                RHI::SingleDeviceDrawItem drawItem;
+                drawItem.m_arguments = drawIndexed;
+                drawItem.m_pipelineState = m_pipelineState->GetDevicePipelineState(context.GetDeviceIndex()).get();
+                drawItem.m_indexBufferView = &indexBufferView;
+                drawItem.m_shaderResourceGroupCount = static_cast<uint8_t>(RHI::ArraySize(shaderResourceGroups));
+                drawItem.m_shaderResourceGroups = shaderResourceGroups;
+                drawItem.m_streamBufferViewCount = static_cast<uint8_t>(m_streamBufferViews.size());
+                AZStd::array<RHI::SingleDeviceStreamBufferView, 2> deviceStreamBufferViews{
+                    m_streamBufferViews[0].GetDeviceStreamBufferView(context.GetDeviceIndex()),
+                    m_streamBufferViews[1].GetDeviceStreamBufferView(context.GetDeviceIndex())
+                };
+                drawItem.m_streamBufferViews = deviceStreamBufferViews.data();
+
+                // Submit the triangle draw item.
+                commandList->Submit(drawItem);
+            };
+
+            m_scopeProducers.emplace_back(
+                aznew
+                    RHI::ScopeProducerFunction<ScopeData, decltype(prepareFunction), decltype(compileFunction), decltype(executeFunction)>(
+                        RHI::ScopeId{ "MultiGPUTriangle0" }, ScopeData{}, prepareFunction, compileFunction, executeFunction, 0));
+
+            m_scopeProducers.emplace_back(
+                aznew
+                    RHI::ScopeProducerFunction<ScopeData, decltype(prepareFunction), decltype(compileFunction), decltype(executeFunction)>(
+                        RHI::ScopeId{ "MultiGPUTriangle1" }, ScopeData{true}, prepareFunction, compileFunction, executeFunction, 1));
+        }
+    }
+
+    void MultiGPUExampleComponent::CreateCompositeScopeProducer()
+    {
+        BufferDataCompositePass bufferData;
+        RHI::PipelineStateDescriptorForDraw pipelineStateDescriptor;
+
+        // Setup input assembly for fullscreen pass
+        {
+            m_inputAssemblyBufferPoolComposite = aznew RHI::MultiDeviceBufferPool();
+
+            RHI::BufferPoolDescriptor bufferPoolDesc;
+            bufferPoolDesc.m_bindFlags = RHI::BufferBindFlags::InputAssembly;
+            bufferPoolDesc.m_heapMemoryLevel = RHI::HeapMemoryLevel::Device;
+            m_inputAssemblyBufferPoolComposite->Init(m_deviceMask_1, bufferPoolDesc);
+
+            SetFullScreenRect(bufferData.m_positions.data(), bufferData.m_uvs.data(), bufferData.m_indices.data());
+
+            m_inputAssemblyBufferComposite = aznew RHI::MultiDeviceBuffer;
+
+            RHI::MultiDeviceBufferInitRequest request;
+            request.m_buffer = m_inputAssemblyBufferComposite.get();
+            request.m_descriptor = RHI::BufferDescriptor{ RHI::BufferBindFlags::InputAssembly, sizeof(bufferData) };
+            request.m_initialData = &bufferData;
+            m_inputAssemblyBufferPoolComposite->InitBuffer(request);
+
+            m_streamBufferViewsComposite[0] = { *m_inputAssemblyBufferComposite,
+                                                offsetof(BufferDataCompositePass, m_positions),
+                                                sizeof(BufferDataCompositePass::m_positions), sizeof(VertexPosition) };
+
+            m_streamBufferViewsComposite[1] = { *m_inputAssemblyBufferComposite,
+                                                offsetof(BufferDataCompositePass, m_uvs), sizeof(BufferDataCompositePass::m_uvs),
+                                                sizeof(VertexUV) };
+
+            RHI::InputStreamLayoutBuilder layoutBuilder;
+            layoutBuilder.AddBuffer()->Channel("POSITION", RHI::Format::R32G32B32_FLOAT);
+            layoutBuilder.AddBuffer()->Channel("UV", RHI::Format::R32G32_FLOAT);
+            pipelineStateDescriptor.m_inputStreamLayout = layoutBuilder.End();
+
+            RHI::ValidateStreamBufferViews(pipelineStateDescriptor.m_inputStreamLayout, m_streamBufferViewsComposite);
+        }
+
+        // Load shader and connect inputs
+        {
+            const char* compositeShaderFilePath = "Shaders/RHI/multigpucomposite.azshader";
+            const char* sampleName = "MultiGPUExample";
+
+            auto shader = LoadShader(compositeShaderFilePath, sampleName);
+            if (shader == nullptr)
+            {
+                AZ_Error("MultiGPUExampleComponent", false, "Could not load shader");
+                return;
+            }
+
+            auto shaderVariant = shader->GetVariant(RPI::ShaderAsset::RootShaderVariantStableId);
+            shaderVariant.ConfigurePipelineState(pipelineStateDescriptor);
+
+            RHI::RenderAttachmentLayoutBuilder attachmentsBuilder;
+            attachmentsBuilder.AddSubpass()->RenderTargetAttachment(m_outputFormat);
+            [[maybe_unused]] RHI::ResultCode result =
+                attachmentsBuilder.End(pipelineStateDescriptor.m_renderAttachmentConfiguration.m_renderAttachmentLayout);
+            AZ_Assert(result == RHI::ResultCode::Success, "Failed to create render attachment layout");
+
+            m_pipelineStateComposite = shader->AcquirePipelineState(pipelineStateDescriptor);
+            if (!m_pipelineStateComposite)
+            {
+                AZ_Error(sampleName, false, "Failed to acquire default pipeline state for shader '%s'", compositeShaderFilePath);
+                return;
+            }
+
+            RHI::ShaderResourceGroupPoolDescriptor srgPoolDescriptor{};
+            srgPoolDescriptor.m_layout = shader->GetAsset()->FindShaderResourceGroupLayout(AZ::Name { "CompositeSrg" }, shader->GetSupervariantIndex()).get();
+
+            m_shaderResourceGroupPoolComposite = aznew RHI::MultiDeviceShaderResourceGroupPool;
+            m_shaderResourceGroupPoolComposite->Init(m_deviceMask_1, srgPoolDescriptor);
+
+            m_shaderResourceGroupComposite = aznew RHI::MultiDeviceShaderResourceGroup;
+            m_shaderResourceGroupPoolComposite->InitGroup(*m_shaderResourceGroupComposite);
+
+            m_shaderResourceGroupDataComposite = RHI::MultiDeviceShaderResourceGroupData{*m_shaderResourceGroupPoolComposite};
+
+            {
+                const AZ::Name inputTextureShaderInput{ "m_inputTextureLeft" };
+                m_textureInputIndices[0] = srgPoolDescriptor.m_layout->FindShaderInputImageIndex(inputTextureShaderInput);
+            }
+            {
+                const AZ::Name inputTextureShaderInput{ "m_inputTextureRight" };
+                m_textureInputIndices[1] = srgPoolDescriptor.m_layout->FindShaderInputImageIndex(inputTextureShaderInput);
+            }
+        }
+
+        // Setup ScopeProducer
+        {
+            struct ScopeData
+            {
+            };
+
+            const auto prepareFunction = [this](RHI::FrameGraphInterface frameGraph, [[maybe_unused]] ScopeData& scopeData)
+            {
+                {
+                    RHI::ImageScopeAttachmentDescriptor descriptor{};
+                    descriptor.m_attachmentId = m_imageAttachmentIds[0];
+                    descriptor.m_loadStoreAction.m_loadAction = RHI::AttachmentLoadAction::Load;
+                    descriptor.m_loadStoreAction.m_storeAction = RHI::AttachmentStoreAction::DontCare;
+                    frameGraph.UseShaderAttachment(descriptor, RHI::ScopeAttachmentAccess::Read);
+                }
+
+                {
+                    RHI::ImageScopeAttachmentDescriptor descriptor{};
+                    descriptor.m_attachmentId = m_imageAttachmentIds[1];
+                    descriptor.m_loadStoreAction.m_loadAction = RHI::AttachmentLoadAction::Load;
+                    descriptor.m_loadStoreAction.m_storeAction = RHI::AttachmentStoreAction::DontCare;
+                    frameGraph.UseShaderAttachment(descriptor, RHI::ScopeAttachmentAccess::Read);
+                }
+
+                {
+                    RHI::ImageScopeAttachmentDescriptor desc{};
+                    desc.m_attachmentId = m_outputAttachmentId;
+                    frameGraph.UseColorAttachment(desc);
+                }
+
+                frameGraph.SetEstimatedItemCount(1);
+
+                frameGraph.ExecuteAfter(RHI::ScopeId{ "MultiGPUTriangle0" });
+                frameGraph.ExecuteAfter(RHI::ScopeId{ "MultiGPUCopyToGPU" });
+            };
+
+            const auto compileFunction = [this](const RHI::FrameGraphCompileContext& context, [[maybe_unused]] const ScopeData& scopeData)
+            {
+                m_shaderResourceGroupDataComposite.SetImageView(m_textureInputIndices[0], context.GetImageView(m_imageAttachmentIds[0]));
+                m_shaderResourceGroupDataComposite.SetImageView(m_textureInputIndices[1], context.GetImageView(m_imageAttachmentIds[1]));
+
+                m_shaderResourceGroupComposite->Compile(m_shaderResourceGroupDataComposite);
+            };
+
+            const auto executeFunction = [=](const RHI::FrameGraphExecuteContext& context, [[maybe_unused]] const ScopeData& scopeData)
+            {
+                RHI::CommandList* commandList = context.GetCommandList();
+
+                commandList->SetViewports(&m_viewport, 1);
+                commandList->SetScissors(&m_scissor, 1);
+
+                const RHI::SingleDeviceIndexBufferView indexBufferView = {
+                    *m_inputAssemblyBufferComposite->GetDeviceBuffer(context.GetDeviceIndex()),
+                    offsetof(BufferDataCompositePass, m_indices), sizeof(BufferDataCompositePass::m_indices), RHI::IndexFormat::Uint16
+                };
+
+                RHI::DrawIndexed drawIndexed;
+                drawIndexed.m_indexCount = 6;
+                drawIndexed.m_instanceCount = 1;
+
+                const RHI::SingleDeviceShaderResourceGroup* shaderResourceGroups[] = { m_shaderResourceGroupComposite->GetDeviceShaderResourceGroup(context.GetDeviceIndex()).get() };
+
+                RHI::SingleDeviceDrawItem drawItem;
+                drawItem.m_arguments = drawIndexed;
+                drawItem.m_pipelineState = m_pipelineStateComposite->GetDevicePipelineState(context.GetDeviceIndex()).get();
+                drawItem.m_indexBufferView = &indexBufferView;
+                drawItem.m_shaderResourceGroupCount = static_cast<uint8_t>(RHI::ArraySize(shaderResourceGroups));
+                drawItem.m_shaderResourceGroups = shaderResourceGroups;
+                drawItem.m_streamBufferViewCount = static_cast<uint8_t>(m_streamBufferViewsComposite.size());
+                AZStd::array<RHI::SingleDeviceStreamBufferView, 2> deviceStreamBufferViews{
+                    m_streamBufferViewsComposite[0].GetDeviceStreamBufferView(context.GetDeviceIndex()),
+                    m_streamBufferViewsComposite[1].GetDeviceStreamBufferView(context.GetDeviceIndex())
+                };
+                drawItem.m_streamBufferViews = deviceStreamBufferViews.data();
+
+                commandList->Submit(drawItem);
+            };
+
+            m_scopeProducers.emplace_back(
+                aznew
+                    RHI::ScopeProducerFunction<ScopeData, decltype(prepareFunction), decltype(compileFunction), decltype(executeFunction)>(
+                        RHI::ScopeId{ "MultiGPUComposite" }, ScopeData{}, prepareFunction, compileFunction, executeFunction));
+        }
+    }
+
+    void MultiGPUExampleComponent::CreateCopyToGPUScopeProducer()
+    {
+        struct ScopeData
+        {
+        };
+
+        const auto prepareFunction = [this]([[maybe_unused]] RHI::FrameGraphInterface frameGraph, [[maybe_unused]] ScopeData& scopeData)
+        {
+            {
+                RHI::ImageScopeAttachmentDescriptor descriptor{};
+                descriptor.m_attachmentId = m_imageAttachmentIds[1];
+                descriptor.m_loadStoreAction.m_loadAction = RHI::AttachmentLoadAction::DontCare;
+                descriptor.m_loadStoreAction.m_storeAction = RHI::AttachmentStoreAction::Store;
+                frameGraph.UseCopyAttachment(descriptor, RHI::ScopeAttachmentAccess::Write);
+            }
+        };
+
+        const auto compileFunction = []([[maybe_unused]] const RHI::FrameGraphCompileContext& context, [[maybe_unused]] const ScopeData& scopeData)
+        {
+        };
+
+        const auto executeFunction = [this](const RHI::FrameGraphExecuteContext& context, [[maybe_unused]] const ScopeData& scopeData)
+        {
+            RHI::SingleDeviceCopyBufferToImageDescriptor copyDescriptor{};
+            copyDescriptor.m_sourceBuffer = m_stagingBufferToGPU->GetDeviceBuffer(context.GetDeviceIndex()).get();
+            copyDescriptor.m_sourceOffset = 0;
+            copyDescriptor.m_sourceBytesPerRow = m_imageWidth * sizeof(uint32_t);
+            copyDescriptor.m_sourceBytesPerImage = static_cast<uint32_t>(m_stagingBufferToGPU->GetDescriptor().m_byteCount);
+            copyDescriptor.m_sourceSize = RHI::Size{ m_imageWidth, m_imageHeight, 1 };
+            copyDescriptor.m_destinationImage = m_transferImage->GetDeviceImage(context.GetDeviceIndex()).get();
+
+            RHI::SingleDeviceCopyItem copyItem(copyDescriptor);
+            context.GetCommandList()->Submit(copyItem);
+        };
+
+        m_scopeProducers.emplace_back(
+            aznew RHI::ScopeProducerFunction<ScopeData, decltype(prepareFunction), decltype(compileFunction), decltype(executeFunction)>(
+                RHI::ScopeId{ "MultiGPUCopyToGPU" }, ScopeData{}, prepareFunction, compileFunction, executeFunction));
+    }
+
+    void MultiGPUExampleComponent::CreateCopyToCPUScopeProducer()
+    {
+        struct ScopeData
+        {
+        };
+
+        const auto prepareFunction = [this]([[maybe_unused]] RHI::FrameGraphInterface frameGraph, [[maybe_unused]] ScopeData& scopeData)
+        {
+            {
+                RHI::BufferScopeAttachmentDescriptor descriptor{};
+                descriptor.m_attachmentId = m_bufferAttachmentIds[1];
+                descriptor.m_bufferViewDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, m_stagingBufferToCPU->GetDescriptor().m_byteCount);
+                descriptor.m_loadStoreAction.m_loadAction = RHI::AttachmentLoadAction::DontCare;
+                descriptor.m_loadStoreAction.m_storeAction = RHI::AttachmentStoreAction::Store;
+                frameGraph.UseCopyAttachment(descriptor, RHI::ScopeAttachmentAccess::Write);
+            }
+
+            {
+                RHI::ImageScopeAttachmentDescriptor descriptor{};
+                descriptor.m_attachmentId = m_imageAttachmentIds[0];
+                descriptor.m_loadStoreAction.m_loadAction = RHI::AttachmentLoadAction::Load;
+                descriptor.m_loadStoreAction.m_storeAction = RHI::AttachmentStoreAction::DontCare;
+                frameGraph.UseCopyAttachment(descriptor, RHI::ScopeAttachmentAccess::Read);
+            }
+
+            frameGraph.ExecuteAfter(RHI::ScopeId{ "MultiGPUTriangle1" });
+        };
+
+        const auto compileFunction = []([[maybe_unused]] const RHI::FrameGraphCompileContext& context, [[maybe_unused]] const ScopeData& scopeData)
+        {
+        };
+
+        const auto executeFunction = [this](const RHI::FrameGraphExecuteContext& context, [[maybe_unused]] const ScopeData& scopeData)
+        {
+            RHI::SingleDeviceCopyImageToBufferDescriptor copyDescriptor{};
+            copyDescriptor.m_sourceImage = m_image->GetDeviceImage(context.GetDeviceIndex()).get();
+            copyDescriptor.m_sourceSize = RHI::Size{ m_imageWidth, m_imageHeight, 1 };
+            copyDescriptor.m_destinationBuffer = m_stagingBufferToCPU->GetDeviceBuffer(context.GetDeviceIndex()).get();
+            copyDescriptor.m_destinationOffset = 0;
+            copyDescriptor.m_destinationBytesPerRow = m_imageWidth * sizeof(uint32_t);
+            copyDescriptor.m_destinationBytesPerImage = static_cast<uint32_t>(m_stagingBufferToCPU->GetDescriptor().m_byteCount);
+            copyDescriptor.m_destinationFormat = m_outputFormat;
+
+            RHI::SingleDeviceCopyItem copyItem(copyDescriptor);
+            context.GetCommandList()->Submit(copyItem);
+        };
+
+        m_scopeProducers.emplace_back(
+            aznew RHI::ScopeProducerFunction<ScopeData, decltype(prepareFunction), decltype(compileFunction), decltype(executeFunction)>(
+                RHI::ScopeId{ "MultiGPUCopyToCPU" }, ScopeData{}, prepareFunction, compileFunction, executeFunction, 1));
+    }
+} // namespace AtomSampleViewer

+ 126 - 0
Gem/Code/Source/RHI/MultiGPUExampleComponent.h

@@ -0,0 +1,126 @@
+/*
+ * 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 <AzCore/Component/Component.h>
+
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+
+#include <Atom/RHI/FrameScheduler.h>
+#include <Atom/RHI/MultiDeviceDrawItem.h>
+#include <Atom/RHI/Device.h>
+#include <Atom/RHI/Factory.h>
+#include <Atom/RHI/MultiDevicePipelineState.h>
+#include <Atom/RHI/MultiDeviceBufferPool.h>
+#include <Atom/RHI/MultiDeviceImagePool.h>
+#include <Atom/RHI/MultiDeviceImage.h>
+#include <Atom/RHI/MultiDeviceShaderResourceGroupPool.h>
+#include <Atom/RHI/MultiDeviceCopyItem.h>
+
+#include <AzCore/Math/Matrix4x4.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    class MultiGPUExampleComponent final
+        : public BasicRHIComponent
+    {
+    public:
+        AZ_COMPONENT(MultiGPUExampleComponent, "{BBA75A38-F111-4F52-AD5E-334B6DD58827}", AZ::Component);
+        AZ_DISABLE_COPY(MultiGPUExampleComponent);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        MultiGPUExampleComponent();
+        ~MultiGPUExampleComponent() override = default;
+
+    protected:
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+        void FrameBeginInternal(AZ::RHI::FrameGraphBuilder& frameGraphBuilder) override;
+
+        // RHISystemNotificationBus::Handler
+        void OnFramePrepare(AZ::RHI::FrameGraphBuilder& frameGraphBuilder) override;
+
+    private:
+        /////////////////////////////////////////////////////////////////////////
+        //! Shared Resources
+
+        void CreateRenderScopeProducer();
+
+        AZ::RHI::MultiDevice::DeviceMask m_deviceMask;
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBufferPool> m_inputAssemblyBufferPool;
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBuffer> m_inputAssemblyBuffer;
+
+        AZ::RHI::ConstPtr<AZ::RHI::MultiDevicePipelineState> m_pipelineState;
+        AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> m_shaderResourceGroupShared;
+        AZ::RHI::ShaderInputConstantIndex m_objectMatrixConstantIndex;
+
+        struct BufferDataTrianglePass
+        {
+            AZStd::array<VertexPosition, 3> m_positions;
+            AZStd::array<VertexColor, 3> m_colors;
+            AZStd::array<uint16_t, 3> m_indices;
+        };
+
+        AZStd::array<AZ::RHI::MultiDeviceStreamBufferView, 2> m_streamBufferViews;
+
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceImagePool> m_imagePool{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceImage> m_image{};
+        AZStd::array<AZ::RHI::AttachmentId, 2> m_imageAttachmentIds = { { AZ::RHI::AttachmentId("MultiGPURenderTexture1"),
+                                                                          AZ::RHI::AttachmentId("MultiGPURenderTexture2") } };
+        AZStd::array<AZ::RHI::AttachmentId, 2> m_bufferAttachmentIds = { { AZ::RHI::AttachmentId("MultiGPUBufferToGPU"),
+                                                                          AZ::RHI::AttachmentId("MultiGPUBufferToCPU") } };
+        uint32_t m_imageWidth{0};
+        uint32_t m_imageHeight{0};
+
+        /////////////////////////////////////////////////////////////////////////
+        //! First device methods and members
+
+        void CreateCopyToGPUScopeProducer();
+        void CreateCopyToCPUScopeProducer();
+        void CreateCompositeScopeProducer();
+
+        struct BufferDataCompositePass
+        {
+            AZStd::array<VertexPosition, 4> m_positions;
+            AZStd::array<VertexUV, 4> m_uvs;
+            AZStd::array<uint16_t, 6> m_indices;
+        };
+
+        AZStd::array<AZ::RHI::ShaderInputImageIndex, 2> m_textureInputIndices;
+
+        AZ::RHI::Ptr<AZ::RHI::Device> m_device_1{};
+        AZ::RHI::MultiDevice::DeviceMask m_deviceMask_1{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBufferPool> m_stagingBufferPoolToGPU{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBuffer> m_stagingBufferToGPU{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceImagePool> m_transferImagePool{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceImage> m_transferImage{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBufferPool> m_inputAssemblyBufferPoolComposite{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBuffer> m_inputAssemblyBufferComposite{};
+        AZStd::array<AZ::RHI::MultiDeviceStreamBufferView, 2> m_streamBufferViewsComposite;
+        AZ::RHI::ConstPtr<AZ::RHI::MultiDevicePipelineState> m_pipelineStateComposite;
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceShaderResourceGroupPool> m_shaderResourceGroupPoolComposite;
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceShaderResourceGroup> m_shaderResourceGroupComposite;
+        AZ::RHI::MultiDeviceShaderResourceGroupData m_shaderResourceGroupDataComposite;
+        AZStd::array<AZ::RHI::Scissor, 2> m_scissors{};
+
+        /////////////////////////////////////////////////////////////////////////
+        //! Second device methods and members
+
+        AZ::RHI::Ptr<AZ::RHI::Device> m_device_2{};
+        AZ::RHI::MultiDevice::DeviceMask m_deviceMask_2{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBufferPool> m_stagingBufferPoolToCPU{};
+        AZ::RHI::Ptr<AZ::RHI::MultiDeviceBuffer> m_stagingBufferToCPU{};
+        AZStd::vector<AZStd::shared_ptr<AZ::RHI::ScopeProducer>> m_secondaryScopeProducers;
+    };
+} // namespace AtomSampleViewer

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

@@ -52,6 +52,7 @@
 #include <RHI/MRTExampleComponent.h>
 #include <RHI/MSAAExampleComponent.h>
 #include <RHI/MultiThreadComponent.h>
+#include <RHI/MultiGPUExampleComponent.h>
 #include <RHI/MultiViewportSwapchainComponent.h>
 #include <RHI/MultipleViewsComponent.h>
 #include <RHI/QueryExampleComponent.h>
@@ -280,6 +281,7 @@ namespace AtomSampleViewer
             NewRHISample<MultipleViewsComponent>("MultipleViews"),
             NewRHISample<MRTExampleComponent>("MultiRenderTarget"),
             NewRHISample<MultiThreadComponent>("MultiThread"),
+            NewRHISample<MultiGPUExampleComponent>("MultiGPU", []() { return AZ::RHI::RHISystemInterface::Get()->GetDeviceCount() >= 2; }),
             NewRHISample<MultiViewportSwapchainComponent>("MultiViewportSwapchainComponent", [] { return IsMultiViewportSwapchainSampleSupported(); }),
             NewRHISample<QueryExampleComponent>("Queries"),
             NewRHISample<RayTracingExampleComponent>("RayTracing", []() {return Utils::GetRHIDevice()->GetFeatures().m_rayTracing; }),

+ 2 - 0
Gem/Code/atomsampleviewergem_private_files.cmake

@@ -53,6 +53,8 @@ set(FILES
     Source/RHI/MRTExampleComponent.cpp
     Source/RHI/MSAAExampleComponent.h
     Source/RHI/MSAAExampleComponent.cpp
+    Source/RHI/MultiGPUExampleComponent.cpp
+    Source/RHI/MultiGPUExampleComponent.h
     Source/RHI/MultiThreadComponent.cpp
     Source/RHI/MultiThreadComponent.h
     Source/RHI/MultipleViewsComponent.cpp

+ 64 - 0
Shaders/RHI/MultiGPUComposite.azsl

@@ -0,0 +1,64 @@
+/*
+ * 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>
+
+ShaderResourceGroup CompositeSrg : SRG_PerObject
+{
+    Texture2D m_inputTextureLeft;
+    Texture2D m_inputTextureRight;
+
+    Sampler m_sampler
+    {
+        MinFilter = Linear;
+        MagFilter = Linear;
+        MipFilter = Linear;
+        AddressU = Clamp;
+        AddressV = Clamp;
+        AddressW = Clamp;
+    };
+}
+
+struct VSInput 
+{
+    float3 m_position : POSITION;
+    float2 m_uv : UV0;
+};
+
+struct VSOutput
+{
+    float4 m_position : SV_Position;
+    float2 m_uv : UV0;
+};
+
+VSOutput MainVS(VSInput vsInput)
+{
+    VSOutput OUT;
+    OUT.m_position = float4(vsInput.m_position, 1.0);
+    OUT.m_uv = vsInput.m_uv;
+    return OUT;
+}
+
+struct PSOutput
+{
+    float4 m_color : SV_Target0;
+};
+
+PSOutput MainPS(VSOutput psInput)
+{
+    PSOutput OUT;
+    if(psInput.m_uv.x <= 0.5)
+    {
+        OUT.m_color = CompositeSrg::m_inputTextureLeft.Sample(CompositeSrg::m_sampler, psInput.m_uv);
+    }
+    else
+    {
+        OUT.m_color = CompositeSrg::m_inputTextureRight.Sample(CompositeSrg::m_sampler, psInput.m_uv);
+    }
+	return OUT;
+}

+ 22 - 0
Shaders/RHI/MultiGPUComposite.shader

@@ -0,0 +1,22 @@
+{
+    "Source": "MultiGPUComposite.azsl",
+    "DepthStencilState": {
+        "Depth": {
+            "Enable": false,
+            "CompareFunc": "GreaterEqual"
+        }
+    },
+    "DrawList": "forward",
+    "ProgramSettings": {
+        "EntryPoints": [
+            {
+                "name": "MainVS",
+                "type": "Vertex"
+            },
+            {
+                "name": "MainPS",
+                "type": "Fragment"
+            }
+        ]
+    }
+}

+ 2 - 0
atomsampleviewer_asset_files.cmake

@@ -117,6 +117,8 @@ set(FILES
     Shaders/RHI/MRTTarget.shader
     Shaders/RHI/MSAAResolve.azsl
     Shaders/RHI/MSAAResolve.shader
+    Shaders/RHI/MultiGPUComposite.azsl
+    Shaders/RHI/MultiGPUComposite.shader
     Shaders/RHI/MultipleViewsDepth.azsl
     Shaders/RHI/MultipleViewsDepth.shader
     Shaders/RHI/MultipleViewsShadow.azsl