/* * 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 "RenderGraph.h" #include "UiRenderer.h" #include #include #include #ifndef _RELEASE #include #include #endif namespace LyShine { static const char* s_maskIncrProfileMarker = "UI_MASK_STENCIL_INCR"; static const char* s_maskDecrProfileMarker = "UI_MASK_STENCIL_DECR"; enum UiColorOp { ColorOp_Unused = 0, // reusing shader flag value, FixedPipelineEmu shader uses 0 to mean eCO_NOSET ColorOp_Normal = 1, // reusing shader flag value, FixedPipelineEmu shader uses 1 to mean eCO_DISABLE ColorOp_PreMultiplyAlpha = 2 // reusing shader flag value, FixedPipelineEmu shader uses 2 to mean eCO_REPLACE }; enum UiAlphaOp { AlphaOp_Unused = 0, // reusing shader flag value, FixedPipelineEmu shader uses 0 to mean eCO_NOSET AlphaOp_Normal = 1, // reusing shader flag value, FixedPipelineEmu shader uses 1 to mean eCO_DISABLE AlphaOp_ModulateAlpha = 2, // reusing shader flag value, FixedPipelineEmu shader uses 2 to mean eCO_REPLACE AlphaOp_ModulateAlphaAndColor = 3 // reusing shader flag value, FixedPipelineEmu shader uses 3 to mean eCO_DECAL }; //////////////////////////////////////////////////////////////////////////////////////////////////// PrimitiveListRenderNode::PrimitiveListRenderNode(const AZ::Data::Instance& texture, bool isClampTextureMode, bool isTextureSRGB, bool preMultiplyAlpha, int blendModeState) : RenderNode(RenderNodeType::PrimitiveList) , m_numTextures(1) , m_isTextureSRGB(isTextureSRGB) , m_preMultiplyAlpha(preMultiplyAlpha) , m_alphaMaskType(AlphaMaskType::None) , m_blendModeState(blendModeState) , m_totalNumVertices(0) , m_totalNumIndices(0) { m_textures[0].m_texture = texture; m_textures[0].m_isClampTextureMode = isClampTextureMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// PrimitiveListRenderNode::PrimitiveListRenderNode(const AZ::Data::Instance& texture, const AZ::Data::Instance& maskTexture, bool isClampTextureMode, bool isTextureSRGB, bool preMultiplyAlpha, AlphaMaskType alphaMaskType, int blendModeState) : RenderNode(RenderNodeType::PrimitiveList) , m_numTextures(2) , m_isTextureSRGB(isTextureSRGB) , m_preMultiplyAlpha(preMultiplyAlpha) , m_alphaMaskType(alphaMaskType) , m_blendModeState(blendModeState) , m_totalNumVertices(0) , m_totalNumIndices(0) { m_textures[0].m_texture = texture; m_textures[0].m_isClampTextureMode = isClampTextureMode; m_textures[1].m_texture = maskTexture; m_textures[1].m_isClampTextureMode = isClampTextureMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// PrimitiveListRenderNode::~PrimitiveListRenderNode() { m_primitives.clear(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void PrimitiveListRenderNode::Render(UiRenderer* uiRenderer , const AZ::Matrix4x4& modelViewProjMat , AZ::RHI::Ptr dynamicDraw) { if (!uiRenderer->IsReady()) { return; } UiRenderer::BaseState curBaseState = uiRenderer->GetBaseState(); UiRenderer::BaseState prevBaseState = curBaseState; if (m_isTextureSRGB) { curBaseState.m_srgbWrite = false; } if (m_alphaMaskType == AlphaMaskType::ModulateAlpha) { curBaseState.m_modulateAlpha = true; } uiRenderer->SetBaseState(curBaseState); const UiRenderer::UiShaderData& uiShaderData = uiRenderer->GetUiShaderData(); // Set render state dynamicDraw->SetStencilState(uiRenderer->GetBaseState().m_stencilState); dynamicDraw->SetTarget0BlendState(uiRenderer->GetBaseState().m_blendState); dynamicDraw->SetShaderVariant(uiRenderer->GetCurrentShaderVariant()); // Set up per draw SRG AZ::Data::Instance drawSrg = dynamicDraw->NewDrawSrg(); // Set textures uint32_t isClampTextureMode = 0; for (int i = 0; i < m_numTextures; ++i) { const AZ::RHI::ImageView* imageView = m_textures[i].m_texture ? m_textures[i].m_texture->GetImageView() : nullptr; if (!imageView) { // Default to white texture auto image = AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::White); imageView = image->GetImageView(); } if (imageView) { drawSrg->SetImageView(uiShaderData.m_imageInputIndex, imageView, i); if (m_textures[i].m_isClampTextureMode) { isClampTextureMode |= (1 << i); } } } // Set sampler state per texture drawSrg->SetConstant(uiShaderData.m_isClampInputIndex, isClampTextureMode); // Set projection matrix drawSrg->SetConstant(uiShaderData.m_viewProjInputIndex, modelViewProjMat); drawSrg->Compile(); // Add the indexed primitives to the dynamic draw context for drawing // // [LYSHINE_ATOM_TODO][ATOM-15073] - need to combine into a single DrawIndexed call to take advantage of the draw call // optimization done by this RenderGraph. This option will be added to DynamicDrawContext. For // now we could combine the vertices ourselves for (const IRenderer::DynUiPrimitive& primitive : m_primitives) { dynamicDraw->DrawIndexed(primitive.m_vertices, primitive.m_numVertices, primitive.m_indices, primitive.m_numIndices, AZ::RHI::IndexFormat::Uint16, drawSrg); } uiRenderer->SetBaseState(prevBaseState); } //////////////////////////////////////////////////////////////////////////////////////////////////// void PrimitiveListRenderNode::AddPrimitive(IRenderer::DynUiPrimitive* primitive) { // always clear the next pointer before adding to list primitive->m_next = nullptr; m_primitives.push_back(*primitive); m_totalNumVertices += primitive->m_numVertices; m_totalNumIndices += primitive->m_numIndices; } //////////////////////////////////////////////////////////////////////////////////////////////////// IRenderer::DynUiPrimitiveList& PrimitiveListRenderNode::GetPrimitives() const { return const_cast(m_primitives); } //////////////////////////////////////////////////////////////////////////////////////////////////// int PrimitiveListRenderNode::GetOrAddTexture(const AZ::Data::Instance& texture, bool isClampTextureMode) { // Check if node is already using this texture int texUnit = FindTexture(texture, isClampTextureMode); // render node is not already using this texture, if there is space to add a texture do so if (texUnit == -1 && m_numTextures < PrimitiveListRenderNode::MaxTextures) { texUnit = m_numTextures; m_textures[texUnit].m_texture = texture; m_textures[texUnit].m_isClampTextureMode = isClampTextureMode; m_numTextures++; } return texUnit; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool PrimitiveListRenderNode::HasSpaceToAddPrimitive(IRenderer::DynUiPrimitive* primitive) const { return primitive->m_numVertices + m_totalNumVertices < std::numeric_limits::max(); } //////////////////////////////////////////////////////////////////////////////////////////////////// int PrimitiveListRenderNode::FindTexture(const AZ::Data::Instance& texture, bool isClampTextureMode) const { for (int i = 0; i < m_numTextures; ++i) { if (m_textures[i].m_texture == texture && m_textures[i].m_isClampTextureMode == isClampTextureMode) { return i; // texture is already in the list } } return -1; } #ifndef _RELEASE //////////////////////////////////////////////////////////////////////////////////////////////////// void PrimitiveListRenderNode::ValidateNode() { size_t numPrims = m_primitives.size(); size_t primCount = 0; const IRenderer::DynUiPrimitive* lastPrim = nullptr; int highestTexUnit = 0; for (const IRenderer::DynUiPrimitive& primitive : m_primitives) { if (primCount > numPrims) { AZ_Error("UI", false, "There are more primitives in the m_primitives slist than m_primitives.size (%d)", numPrims) } primCount++; lastPrim = &primitive; if (primitive.m_vertices[0].texIndex > highestTexUnit) { highestTexUnit = primitive.m_vertices[0].texIndex; } } if (m_numTextures != highestTexUnit+1) { AZ_Error("UI", false, "m_numTextures (%d) is not highestTexUnit+1 (%d)", m_numTextures, highestTexUnit+1) } if (numPrims > 0 && lastPrim != &*m_primitives.last()) { AZ_Error("UI", false, "lastPrim is not the same as last node") } } #endif //////////////////////////////////////////////////////////////////////////////////////////////////// MaskRenderNode::MaskRenderNode(MaskRenderNode* parentMask, bool isMaskingEnabled, bool useAlphaTest, bool drawBehind, bool drawInFront) : RenderNode(RenderNodeType::Mask) , m_parentMask(parentMask) , m_isMaskingEnabled(isMaskingEnabled) , m_useAlphaTest(useAlphaTest) , m_drawBehind(drawBehind) , m_drawInFront(drawInFront) { } //////////////////////////////////////////////////////////////////////////////////////////////////// MaskRenderNode::~MaskRenderNode() { for (RenderNode* renderNode : m_contentRenderNodes) { delete renderNode; } m_contentRenderNodes.clear(); for (RenderNode* renderNode : m_maskRenderNodes) { AZ_Assert(renderNode->GetType() != RenderNodeType::Mask, "There cannot be mask render nodes in the mask visual"); delete renderNode; } m_maskRenderNodes.clear(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void MaskRenderNode::Render(UiRenderer* uiRenderer , const AZ::Matrix4x4& modelViewProjMat , AZ::RHI::Ptr dynamicDraw) { UiRenderer::BaseState priorBaseState = uiRenderer->GetBaseState(); if (m_isMaskingEnabled || m_drawBehind) { SetupBeforeRenderingMask(uiRenderer, dynamicDraw, true, priorBaseState); for (RenderNode* renderNode : m_maskRenderNodes) { renderNode->Render(uiRenderer, modelViewProjMat, dynamicDraw); } SetupAfterRenderingMask(uiRenderer, dynamicDraw, true, priorBaseState); } for (RenderNode* renderNode : m_contentRenderNodes) { renderNode->Render(uiRenderer, modelViewProjMat, dynamicDraw); } if (m_isMaskingEnabled || m_drawInFront) { SetupBeforeRenderingMask(uiRenderer, dynamicDraw, false, priorBaseState); for (RenderNode* renderNode : m_maskRenderNodes) { renderNode->Render(uiRenderer, modelViewProjMat, dynamicDraw); } SetupAfterRenderingMask(uiRenderer, dynamicDraw, false, priorBaseState); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool MaskRenderNode::IsMaskRedundant() { // if there are no content nodes then there is no point rendering anything for the mask primitives // unless the mask primitives are non-empty and we are visually drawing the mask primitives in front or // behind of the children. if (m_contentRenderNodes.empty() && (m_maskRenderNodes.empty() || (!m_drawBehind && !m_drawInFront))) { return true; } else { return false; } } #ifndef _RELEASE //////////////////////////////////////////////////////////////////////////////////////////////////// void MaskRenderNode::ValidateNode() { for (RenderNode* renderNode : m_maskRenderNodes) { renderNode->ValidateNode(); } for (RenderNode* renderNode : m_contentRenderNodes) { renderNode->ValidateNode(); } } #endif //////////////////////////////////////////////////////////////////////////////////////////////////// void MaskRenderNode::SetupBeforeRenderingMask(UiRenderer* uiRenderer, AZ::RHI::Ptr dynamicDraw, bool firstPass, UiRenderer::BaseState priorBaseState) { UiRenderer::BaseState curBaseState = priorBaseState; // If using alpha test for drawing the renderable components on this element then we turn on // alpha test as a pre-render step curBaseState.m_useAlphaTest = m_useAlphaTest; // if either of the draw flags are checked then we may want to draw the renderable component(s) // on this element, otherwise use the color mask to stop them rendering curBaseState.m_blendState.m_enable = false; curBaseState.m_blendState.m_writeMask = 0x0; if ((m_drawBehind && firstPass) || (m_drawInFront && !firstPass)) { curBaseState.m_blendState.m_enable = true; curBaseState.m_blendState.m_writeMask = 0xF; } if (m_isMaskingEnabled) { AZ::RHI::StencilOpState stencilOpState; stencilOpState.m_func = AZ::RHI::ComparisonFunc::Equal; // masking is enabled so we want to setup to increment (first pass) or decrement (second pass) // the stencil buff when rendering the renderable component(s) on this element if (firstPass) { stencilOpState.m_passOp = AZ::RHI::StencilOp::Increment; } else { stencilOpState.m_passOp = AZ::RHI::StencilOp::Decrement; } curBaseState.m_stencilState.m_frontFace = stencilOpState; curBaseState.m_stencilState.m_backFace = stencilOpState; // set up for stencil write dynamicDraw->SetStencilReference(uiRenderer->GetStencilRef()); curBaseState.m_stencilState.m_enable = true; curBaseState.m_stencilState.m_writeMask = 0xFF; } else { // masking is not enabled curBaseState.m_stencilState.m_enable = false; } uiRenderer->SetBaseState(curBaseState); } //////////////////////////////////////////////////////////////////////////////////////////////////// void MaskRenderNode::SetupAfterRenderingMask(UiRenderer* uiRenderer, AZ::RHI::Ptr dynamicDraw, bool firstPass, UiRenderer::BaseState priorBaseState) { if (m_isMaskingEnabled) { // Masking is enabled so on the first pass we want to increment the stencil ref stored // in the UiRenderer and used by all normal rendering, this is so that it matches the // increments to the stencil buffer that we have just done by rendering the mask. // On the second pass we want to decrement the stencil ref so it is back to what it // was before rendering the normal children of this mask element. if (firstPass) { uiRenderer->IncrementStencilRef(); } else { uiRenderer->DecrementStencilRef(); } dynamicDraw->SetStencilReference(uiRenderer->GetStencilRef()); if (firstPass) { UiRenderer::BaseState curBaseState = priorBaseState; // turn off stencil write and turn on stencil test curBaseState.m_stencilState.m_enable = true; curBaseState.m_stencilState.m_writeMask = 0x00; AZ::RHI::StencilOpState stencilOpState; stencilOpState.m_func = AZ::RHI::ComparisonFunc::Equal; curBaseState.m_stencilState.m_frontFace = stencilOpState; curBaseState.m_stencilState.m_backFace = stencilOpState; uiRenderer->SetBaseState(curBaseState); } else { // second pass, set base state back to what it was before rendering this element uiRenderer->SetBaseState(priorBaseState); } } else { // masking is not enabled // remove any color mask or alpha test that we set in pre-render uiRenderer->SetBaseState(priorBaseState); } } //////////////////////////////////////////////////////////////////////////////////////////////////// RenderTargetRenderNode::RenderTargetRenderNode( RenderTargetRenderNode* parentRenderTarget, AZ::Data::Instance attachmentImage, const AZ::Vector2& viewportTopLeft, const AZ::Vector2& viewportSize, const AZ::Color& clearColor, int nestLevel) : RenderNode(RenderNodeType::RenderTarget) , m_parentRenderTarget(parentRenderTarget) , m_attachmentImage(attachmentImage) , m_viewportX(viewportTopLeft.GetX()) , m_viewportY(viewportTopLeft.GetY()) , m_viewportWidth(viewportSize.GetX()) , m_viewportHeight(viewportSize.GetY()) , m_clearColor(clearColor) , m_nestLevel(nestLevel) { AZ::MakeOrthographicMatrixRH(m_modelViewProjMat, m_viewportX, m_viewportX + m_viewportWidth, m_viewportY + m_viewportHeight, m_viewportY, 0.0f, 1.0f); } //////////////////////////////////////////////////////////////////////////////////////////////////// RenderTargetRenderNode::~RenderTargetRenderNode() { for (RenderNode* renderNode : m_childRenderNodes) { delete renderNode; } m_childRenderNodes.clear(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderTargetRenderNode::Render(UiRenderer* uiRenderer , [[maybe_unused]] const AZ::Matrix4x4& modelViewProjMat , [[maybe_unused]] AZ::RHI::Ptr dynamicDraw) { if (!m_attachmentImage) { return; } ISystem* system = gEnv->pSystem; if (system && !gEnv->IsDedicated()) { // Use a dedicated dynamic draw context for rendering to the texture since it can only have one draw list tag if (!m_dynamicDraw) { m_dynamicDraw = uiRenderer->CreateDynamicDrawContextForRTT(GetRenderTargetName()); } if (m_dynamicDraw) { UiRenderer::BaseState priorBaseState = uiRenderer->GetBaseState(); UiRenderer::BaseState curBaseState = priorBaseState; curBaseState.m_blendState.m_blendAlphaSource = AZ::RHI::BlendFactor::One; curBaseState.m_blendState.m_blendAlphaDest = AZ::RHI::BlendFactor::AlphaSource1Inverse; uiRenderer->SetBaseState(curBaseState); for (RenderNode* renderNode : m_childRenderNodes) { renderNode->Render(uiRenderer, m_modelViewProjMat, m_dynamicDraw); } uiRenderer->SetBaseState(priorBaseState); } else { AZ_WarningOnce("UI", false, "Failed to create a Dynamic Draw Context for UI Element's render target. "\ "Please ensure that the custom LyShinePass has been added to the project's main render pipeline."); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// const char* RenderTargetRenderNode::GetRenderTargetName() const { return m_attachmentImage->GetRHIImage()->GetName().GetCStr(); } //////////////////////////////////////////////////////////////////////////////////////////////////// int RenderTargetRenderNode::GetNestLevel() const { return m_nestLevel; } //////////////////////////////////////////////////////////////////////////////////////////////////// const AZ::Data::Instance RenderTargetRenderNode::GetRenderTarget() const { return m_attachmentImage; } #ifndef _RELEASE //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderTargetRenderNode::ValidateNode() { for (RenderNode* renderNode : m_childRenderNodes) { renderNode->ValidateNode(); } } #endif //////////////////////////////////////////////////////////////////////////////////////////////////// bool RenderTargetRenderNode::CompareNestLevelForSort(RenderTargetRenderNode* a, RenderTargetRenderNode*b) { // elements with higher nest levels should be rendered first so they should be considered "less than" // for the sort return a->m_nestLevel > b->m_nestLevel; } //////////////////////////////////////////////////////////////////////////////////////////////////// RenderGraph::RenderGraph() { // we keep track of the list of render nodes that new nodes should be added to. Initially // it is the main, top-level list of nodes. If we start defining a mask or render to texture // then it becomes the node list for that render node. m_renderNodeListStack.push(&m_renderNodes); } //////////////////////////////////////////////////////////////////////////////////////////////////// RenderGraph::~RenderGraph() { ResetGraph(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::ResetGraph() { // clear and delete the list of render target nodes for (RenderNode* renderNode : m_renderTargetRenderNodes) { delete renderNode; } m_renderTargetRenderNodes.clear(); // clear and delete the list of render nodes for (RenderNode* renderNode : m_renderNodes) { delete renderNode; } m_renderNodes.clear(); // clear and delete the dynamic quads for (DynamicQuad* quad : m_dynamicQuads) { delete quad; } m_dynamicQuads.clear(); m_currentMask = nullptr; m_currentRenderTarget = nullptr; // clear the render node list stack and reset it to be the top level node list while (!m_renderNodeListStack.empty()) { m_renderNodeListStack.pop(); } m_renderNodeListStack.push(&m_renderNodes); m_isDirty = true; m_renderToRenderTargetCount = 0; #ifndef _RELEASE m_wasBuiltThisFrame = true; m_timeGraphLastBuiltMs = AZStd::GetTimeUTCMilliSecond(); #endif } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::BeginMask(bool isMaskingEnabled, bool useAlphaTest, bool drawBehind, bool drawInFront) { // this uses pool allocator MaskRenderNode* maskRenderNode = new MaskRenderNode(m_currentMask, isMaskingEnabled, useAlphaTest, drawBehind, drawInFront); m_currentMask = maskRenderNode; m_renderNodeListStack.push(&maskRenderNode->GetMaskRenderNodeList()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::StartChildrenForMask() { AZ_Assert(m_currentMask, "Calling StartChildrenForMask while not defining a mask"); m_renderNodeListStack.pop(); m_renderNodeListStack.push(&m_currentMask->GetContentRenderNodeList()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::EndMask() { AZ_Assert(m_currentMask, "Calling EndMask while not defining a mask"); if (m_currentMask) { MaskRenderNode* newMaskRenderNode = m_currentMask; m_currentMask = m_currentMask->GetParentMask(); // pop off the mask's content render node list m_renderNodeListStack.pop(); if (newMaskRenderNode->IsMaskRedundant()) { // We don't know the mask is redundant until we have created this node and found that it hasn't got // child nodes. This is not common but does happen sometimes when all the children are currently disabled. delete newMaskRenderNode; } else { m_renderNodeListStack.top()->push_back(newMaskRenderNode); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::BeginRenderToTexture([[maybe_unused]] int renderTargetHandle, [[maybe_unused]] SDepthTexture* renderTargetDepthSurface, [[maybe_unused]] const AZ::Vector2& viewportTopLeft, [[maybe_unused]] const AZ::Vector2& viewportSize, [[maybe_unused]] const AZ::Color& clearColor) { // LYSHINE_ATOM_TODO - this function will be removed when all IRenderer references are gone from UI components } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::BeginRenderToTexture(AZ::Data::Instance attachmentImage, const AZ::Vector2& viewportTopLeft, const AZ::Vector2& viewportSize, const AZ::Color& clearColor) { // this uses pool allocator RenderTargetRenderNode* renderTargetRenderNode = new RenderTargetRenderNode( m_currentRenderTarget, attachmentImage, viewportTopLeft, viewportSize, clearColor, m_renderTargetNestLevel); m_currentRenderTarget = renderTargetRenderNode; m_renderNodeListStack.push(&m_currentRenderTarget->GetChildRenderNodeList()); m_renderTargetNestLevel++; } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::EndRenderToTexture() { AZ_Assert(m_currentRenderTarget, "Calling EndRenderToTexture while not defining a render target node"); if (m_currentRenderTarget) { RenderTargetRenderNode* newRenderTargetRenderNode = m_currentRenderTarget; m_currentRenderTarget = m_currentRenderTarget->GetParentRenderTarget(); // we don't add this node to the normal list of render nodes since it is rendered before // the main render for the render graph m_renderTargetRenderNodes.push_back(newRenderTargetRenderNode); m_renderNodeListStack.pop(); m_renderTargetNestLevel--; } } void RenderGraph::AddPrimitive( IRenderer::DynUiPrimitive* primitive, ITexture* texture, bool isClampTextureMode, bool isTextureSRGB, bool isTexturePremultipliedAlpha, BlendMode blendMode) { // LYSHINE_ATOM_TODO - this function will be removed when all IRenderer references are gone from UI components AZ_UNUSED(primitive); AZ_UNUSED(texture); AZ_UNUSED(isClampTextureMode); AZ_UNUSED(isTextureSRGB); AZ_UNUSED(isTexturePremultipliedAlpha); AZ_UNUSED(blendMode); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::AddPrimitiveAtom(IRenderer::DynUiPrimitive* primitive, const AZ::Data::Instance& texture, bool isClampTextureMode, bool isTextureSRGB, bool isTexturePremultipliedAlpha, BlendMode blendMode) { AZStd::vector* renderNodeList = m_renderNodeListStack.top(); int texUnit = -1; if (renderNodeList) { // we want to pre-multiply alpha if we are rendering to a render target AND we are not rendering from a render target bool isPreMultiplyAlpha = m_renderTargetNestLevel > 0 && !isTexturePremultipliedAlpha; // given the blend mode get the right state, the state depends on whether the shader is outputing premultiplied alpha. // The shader can be outputing premultiplied alpha EITHER if the input texture is premultiplied alpha OR if the // shader is doing the premultiply of the output color bool isShaderOutputPremultAlpha = isPreMultiplyAlpha || isTexturePremultipliedAlpha; int blendModeState = GetBlendModeState(blendMode, isShaderOutputPremultAlpha); PrimitiveListRenderNode* renderNodeToAddTo = nullptr; if (!renderNodeList->empty()) { RenderNode* lastRenderNode = renderNodeList->back(); if (lastRenderNode && lastRenderNode->GetType() == RenderNodeType::PrimitiveList) { PrimitiveListRenderNode* primListRenderNode = static_cast(lastRenderNode); // compare render state if (primListRenderNode->GetIsTextureSRGB() == isTextureSRGB && primListRenderNode->GetBlendModeState() == blendModeState && primListRenderNode->GetIsPremultiplyAlpha() == isPreMultiplyAlpha && primListRenderNode->GetAlphaMaskType() == AlphaMaskType::None && primListRenderNode->HasSpaceToAddPrimitive(primitive)) { // render state is the same - we can add the primitive to this list if the texture is in // the list or there is space for another texture texUnit = primListRenderNode->GetOrAddTexture(texture, isClampTextureMode); if (texUnit != -1) { renderNodeToAddTo = primListRenderNode; } } } } if (!renderNodeToAddTo) { // We can't add this primitive to the existing render node, we need to create a new render node // this uses a pool allocator for fast allocation renderNodeToAddTo = new PrimitiveListRenderNode(texture, isClampTextureMode, isTextureSRGB, isPreMultiplyAlpha, blendModeState); renderNodeList->push_back(renderNodeToAddTo); texUnit = 0; } // Ensure that the vertices are referencing the right texture unit // Because primitive verts are only created when a UI component changes, they have a longer // lifetime than the render graph. So if not much has changed since the render graph was last built // it is quite likely that the verts are already set to use the correct texture unit. if (primitive->m_vertices[0].texIndex != texUnit) { for (int i = 0; i < primitive->m_numVertices; ++i) { primitive->m_vertices[i].texIndex = texUnit; } } // add this primitive to the render node renderNodeToAddTo->AddPrimitive(primitive); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::AddAlphaMaskPrimitive([[maybe_unused]] IRenderer::DynUiPrimitive* primitive, [[maybe_unused]] ITexture* texture, [[maybe_unused]] ITexture* maskTexture, [[maybe_unused]] bool isClampTextureMode, [[maybe_unused]] bool isTextureSRGB, [[maybe_unused]] bool isTexturePremultipliedAlpha, [[maybe_unused]] BlendMode blendMode) { // LYSHINE_ATOM_TODO - this function will be removed when all IRenderer references are gone from UI components } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::AddAlphaMaskPrimitiveAtom(IRenderer::DynUiPrimitive* primitive, AZ::Data::Instance contentAttachmentImage, AZ::Data::Instance maskAttachmentImage, bool isClampTextureMode, bool isTextureSRGB, bool isTexturePremultipliedAlpha, BlendMode blendMode) { AZStd::vector* renderNodeList = m_renderNodeListStack.top(); int texUnit0 = -1; int texUnit1 = -1; if (renderNodeList) { // we want to pre-multiply alpha if we are rendering to a render target AND we are not rendering from a render target bool isPreMultiplyAlpha = m_renderTargetNestLevel > 0 && !isTexturePremultipliedAlpha; // given the blend mode get the right state, the state depends on whether the shader is outputing premultiplied alpha. // The shader can be outputing premultiplied alpha EITHER if the input texture is premultiplied alpha OR if the // shader is doing the premultiply of the output color bool isShaderOutputPremultAlpha = isPreMultiplyAlpha || isTexturePremultipliedAlpha; int blendModeState = GetBlendModeState(blendMode, isShaderOutputPremultAlpha); AlphaMaskType alphaMaskType = isShaderOutputPremultAlpha ? AlphaMaskType::ModulateAlphaAndColor : AlphaMaskType::ModulateAlpha; PrimitiveListRenderNode* renderNodeToAddTo = nullptr; if (!renderNodeList->empty()) { RenderNode* lastRenderNode = renderNodeList->back(); if (lastRenderNode && lastRenderNode->GetType() == RenderNodeType::PrimitiveList) { PrimitiveListRenderNode* primListRenderNode = static_cast(lastRenderNode); // compare render state if (primListRenderNode->GetIsTextureSRGB() == isTextureSRGB && primListRenderNode->GetBlendModeState() == blendModeState && primListRenderNode->GetIsPremultiplyAlpha() == isPreMultiplyAlpha && primListRenderNode->GetAlphaMaskType() == alphaMaskType && primListRenderNode->HasSpaceToAddPrimitive(primitive)) { // render state is the same - we can add the primitive to this list if the texture is in // the list or there is space for another texture texUnit0 = primListRenderNode->GetOrAddTexture(contentAttachmentImage, true); texUnit1 = primListRenderNode->GetOrAddTexture(maskAttachmentImage, true); if (texUnit0 != -1 && texUnit1 != -1) { renderNodeToAddTo = primListRenderNode; } } } } if (!renderNodeToAddTo) { // We can't add this primitive to the existing render node, we need to create a new render node // this uses a pool allocator for fast allocation renderNodeToAddTo = new PrimitiveListRenderNode(contentAttachmentImage, maskAttachmentImage, isClampTextureMode, isTextureSRGB, isPreMultiplyAlpha, alphaMaskType, blendModeState); renderNodeList->push_back(renderNodeToAddTo); texUnit0 = 0; texUnit1 = 1; } // Ensure that the vertices are referencing the right texture unit // Because primitive verts are only created when a UI component changes, they have a longer // lifetime than the render graph. So if not much has changed since the render graph was last built // it is quite likely that the verts are already set to use the correct texture unit. if (primitive->m_vertices[0].texIndex != texUnit0 || primitive->m_vertices[0].texIndex2 != texUnit1) { for (int i = 0; i < primitive->m_numVertices; ++i) { primitive->m_vertices[i].texIndex = texUnit0; primitive->m_vertices[i].texIndex2 = texUnit1; } } // add this primitive to the render node renderNodeToAddTo->AddPrimitive(primitive); } } //////////////////////////////////////////////////////////////////////////////////////////////////// IRenderer::DynUiPrimitive* RenderGraph::GetDynamicQuadPrimitive(const AZ::Vector2* positions, uint32 packedColor) { const int numVertsInQuad = 4; const int numIndicesInQuad = 6; // points are a clockwise quad static const Vec2 uvs[numVertsInQuad] = { {0, 0}, {1, 0}, {1, 1}, {0, 1} }; static uint16 indices[numIndicesInQuad] = { 0, 1, 2, 2, 3, 0 }; DynamicQuad* quad = new DynamicQuad; for (int i = 0; i < numVertsInQuad; ++i) { quad->m_quadVerts[i].xy = Vec2(positions[i].GetX(), positions[i].GetY()); quad->m_quadVerts[i].color.dcolor = packedColor; quad->m_quadVerts[i].st = uvs[i]; quad->m_quadVerts[i].texIndex = 0; quad->m_quadVerts[i].texHasColorChannel = 1; quad->m_quadVerts[i].texIndex2 = 0; quad->m_quadVerts[i].pad = 0; } quad->m_primitive.m_vertices = quad->m_quadVerts; quad->m_primitive.m_numVertices = numVertsInQuad; quad->m_primitive.m_indices = indices; quad->m_primitive.m_numIndices = numIndicesInQuad; m_dynamicQuads.push_back(quad); return &quad->m_primitive; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool RenderGraph::IsRenderingToMask() const { return m_isRenderingToMask; } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::SetIsRenderingToMask(bool isRenderingToMask) { m_isRenderingToMask = isRenderingToMask; } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::PushAlphaFade(float alphaFadeValue) { float currentAlphaFade = GetAlphaFade(); m_alphaFadeStack.push(alphaFadeValue * currentAlphaFade); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::PushOverrideAlphaFade(float alphaFadeValue) { m_alphaFadeStack.push(alphaFadeValue); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::PopAlphaFade() { if (!m_alphaFadeStack.empty()) { m_alphaFadeStack.pop(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// float RenderGraph::GetAlphaFade() const { float alphaFade = 1.0f; // by default nothing is faded if (!m_alphaFadeStack.empty()) { alphaFade = m_alphaFadeStack.top(); } return alphaFade; } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::Render(UiRenderer* uiRenderer, [[maybe_unused]] const AZ::Vector2& viewportSize) { AZ::RHI::Ptr dynamicDraw = uiRenderer->GetDynamicDrawContext(); // Disable stencil and enable blend/color write dynamicDraw->SetStencilState(uiRenderer->GetBaseState().m_stencilState); dynamicDraw->SetTarget0BlendState(uiRenderer->GetBaseState().m_blendState); // First render the render targets, they are sorted so that more deeply nested ones are rendered first. // They only need to be rendered the first time that a render graph is rendered after it has been built. if (m_renderToRenderTargetCount == 0) { // Enable the Rtt passes to draw onto the render targets SetRttPassesEnabled(uiRenderer, true); } // LYSHINE_ATOM_TODO - It is currently necessary to render to the targets twice. Needs investigation constexpr int timesToRenderToRenderTargets = 2; if (m_renderToRenderTargetCount < timesToRenderToRenderTargets) { for (RenderNode* renderNode : m_renderTargetRenderNodes) { renderNode->Render(uiRenderer, uiRenderer->GetModelViewProjectionMatrix(), dynamicDraw); } m_renderToRenderTargetCount++; } else if (m_renderToRenderTargetCount < timesToRenderToRenderTargets + 1) { // Disable the rtt render passes since they don't need to be rendered to until the graph becomes invalidated again. // This is also necessary to prevent the render targets' contents getting cleared on load by the pass. SetRttPassesEnabled(uiRenderer, false); m_renderToRenderTargetCount++; } for (RenderNode* renderNode : m_renderNodes) { renderNode->Render(uiRenderer, uiRenderer->GetModelViewProjectionMatrix(), dynamicDraw); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::SetDirtyFlag(bool isDirty) { if (m_isDirty != isDirty) { if (isDirty) { // when graph first becomes dirty it must be reset since an element may have been deleted // and the graph contains pointers to DynUiPrimitives owned by components on elements. ResetGraph(); } m_isDirty = isDirty; } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool RenderGraph::GetDirtyFlag() { return m_isDirty; } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::FinalizeGraph() { // sort the render targets so that more deeply nested ones are rendered first std::sort(m_renderTargetRenderNodes.begin(), m_renderTargetRenderNodes.end(), RenderTargetRenderNode::CompareNestLevelForSort); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool RenderGraph::IsEmpty() { return m_renderNodes.empty(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::GetRenderTargetsAndDependencies(LyShine::AttachmentImagesAndDependencies& attachmentImagesAndDependencies) { for (RenderNode* renderNode : m_renderTargetRenderNodes) { const RenderTargetRenderNode* renderTargetRenderNode = static_cast(renderNode); if (renderTargetRenderNode->GetNestLevel() == 0) { LyShine::AttachmentImages attachmentImages; const AZStd::vector& childNodeList = renderTargetRenderNode->GetChildRenderNodeList(); for (auto& childNode : childNodeList) { if (childNode->GetType() == RenderNodeType::RenderTarget) { const RenderTargetRenderNode* childRenderTargetRenderNode = static_cast(childNode); attachmentImages.emplace_back(childRenderTargetRenderNode->GetRenderTarget()); } } attachmentImagesAndDependencies.emplace_back(AttachmentImageAndDependentsPair(renderTargetRenderNode->GetRenderTarget(), attachmentImages)); } } } #ifndef _RELEASE //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::ValidateGraph() { for (RenderNode* renderNode : m_renderNodes) { renderNode->ValidateNode(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::GetDebugInfoRenderGraph(LyShineDebug::DebugInfoRenderGraph& info) const { info.m_numPrimitives = 0; info.m_numRenderNodes = 0; info.m_numTriangles = 0; info.m_numUniqueTextures = 0; info.m_numMasks = 0; info.m_numRTs = 0; info.m_numNodesDueToMask = 0; info.m_numNodesDueToRT = 0; info.m_numNodesDueToBlendMode = 0; info.m_numNodesDueToSrgb = 0; info.m_numNodesDueToMaxVerts = 0; info.m_numNodesDueToTextures = 0; info.m_wasBuiltThisFrame = m_wasBuiltThisFrame; info.m_timeGraphLastBuiltMs = m_timeGraphLastBuiltMs; info.m_isReusingRenderTargets = m_renderToRenderTargetCount >= 2 && !m_renderTargetRenderNodes.empty(); m_wasBuiltThisFrame = false; AZStd::set> uniqueTextures; // If we are rendering to the render targets this frame then record the stats for doing that if (m_renderToRenderTargetCount < 2) { for (RenderNode* renderNode : m_renderTargetRenderNodes) { const RenderTargetRenderNode* renderTargetRenderNode = static_cast(renderNode); if (renderTargetRenderNode->GetChildRenderNodeList().size() > 0) { info.m_numNodesDueToRT += 1; // there is an extra draw call because these are inside a render target (so can't be combined with those outside) } ++info.m_numRTs; const AZStd::vector& childNodeList = renderTargetRenderNode->GetChildRenderNodeList(); // walk the rendertarget's graph recursively to add up all of the data GetDebugInfoRenderNodeList(childNodeList, info, uniqueTextures); } } // walk the graph recursively to add up all of the data GetDebugInfoRenderNodeList(m_renderNodes, info, uniqueTextures); info.m_numUniqueTextures = uniqueTextures.size(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::GetDebugInfoRenderNodeList( const AZStd::vector& renderNodeList, LyShineDebug::DebugInfoRenderGraph& info, AZStd::set>& uniqueTextures) const { const PrimitiveListRenderNode* prevPrimListNode = nullptr; bool isFirstNode = true; bool wasLastNodeAMask = false; for (const RenderNode* renderNode : renderNodeList) { ++info.m_numRenderNodes; if (renderNode->GetType() == RenderNodeType::Mask) { const MaskRenderNode* maskRenderNode = static_cast(renderNode); if (maskRenderNode->GetMaskRenderNodeList().size() > 0) { info.m_numNodesDueToMask += 1; // there are always 2 draw calls for a mask so the mask adds one even if it is the first element } if (maskRenderNode->GetContentRenderNodeList().size() > 0) { info.m_numNodesDueToMask += 1; // there is an extra draw call because these are inside a mask (so can't be combined with those outside) } if (!isFirstNode) { info.m_numNodesDueToMask += 1; // caused a break from the previous due to a mask } wasLastNodeAMask = true; ++info.m_numMasks; GetDebugInfoRenderNodeList(maskRenderNode->GetContentRenderNodeList(), info, uniqueTextures); if (maskRenderNode->GetIsMaskingEnabled()) { GetDebugInfoRenderNodeList(maskRenderNode->GetMaskRenderNodeList(), info, uniqueTextures); } prevPrimListNode = nullptr; } else if (renderNode->GetType() == RenderNodeType::PrimitiveList) { if (wasLastNodeAMask) { info.m_numNodesDueToMask += 1; // this could not be combined with the render nodes before the mask wasLastNodeAMask = false; } const PrimitiveListRenderNode* primListRenderNode = static_cast(renderNode); IRenderer::DynUiPrimitiveList& primitives = primListRenderNode->GetPrimitives(); info.m_numPrimitives += primitives.size(); { for (const IRenderer::DynUiPrimitive& primitive : primitives) { info.m_numTriangles += primitive.m_numIndices / 3; } } for (int i = 0; i < primListRenderNode->GetNumTextures(); ++i) { uniqueTextures.insert(primListRenderNode->GetTexture(i)); } if (prevPrimListNode) { if (prevPrimListNode->GetBlendModeState() != primListRenderNode->GetBlendModeState()) { ++info.m_numNodesDueToBlendMode; } else if (prevPrimListNode->GetIsTextureSRGB() != primListRenderNode->GetIsTextureSRGB()) { ++info.m_numNodesDueToSrgb; } else if (!prevPrimListNode->HasSpaceToAddPrimitive(&primListRenderNode->GetPrimitives().front())) { ++info.m_numNodesDueToMaxVerts; } else if (prevPrimListNode->GetNumTextures() == PrimitiveListRenderNode::MaxTextures) { ++info.m_numNodesDueToTextures; } } prevPrimListNode = primListRenderNode; } isFirstNode = false; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::DebugReportDrawCalls(AZ::IO::HandleType fileHandle, LyShineDebug::DebugInfoDrawCallReport& reportInfo, void* context) const { if (m_renderNodes.empty()) { AZStd::string logLine = "Rendergraph is empty\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); } else { // first list the render nodes for creating render targets for (const RenderNode* renderNode : m_renderTargetRenderNodes) { const RenderTargetRenderNode* renderTargetRenderNode = static_cast(renderNode); const char* renderTargetName = renderTargetRenderNode->GetRenderTargetName(); AZ::Color clearColor = renderTargetRenderNode->GetClearColor(); AZStd::string logLine = AZStd::string::format("RenderTarget %s (ClearColor=(%f,%f,%f), ClearAlpha=%f, Viewport=(%f,%f,%f,%f)) :\r\n", renderTargetName, static_cast(clearColor.GetR()), static_cast(clearColor.GetG()), static_cast(clearColor.GetB()), static_cast(clearColor.GetA()), renderTargetRenderNode->GetViewportX(), renderTargetRenderNode->GetViewportY(), renderTargetRenderNode->GetViewportWidth(), renderTargetRenderNode->GetViewportHeight()); AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); const AZStd::vector& childNodeList = renderTargetRenderNode->GetChildRenderNodeList(); AZStd::string indent = " "; DebugReportDrawCallsRenderNodeList(childNodeList, fileHandle, reportInfo, context, indent); // write blank separator line logLine = "\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); } AZStd::string logLine = "Main render target:\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); // Recursively visit all the render nodes AZStd::string indent = " "; DebugReportDrawCallsRenderNodeList(m_renderNodes, fileHandle, reportInfo, context, indent); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::DebugReportDrawCallsRenderNodeList( const AZStd::vector& renderNodeList, AZ::IO::HandleType fileHandle, LyShineDebug::DebugInfoDrawCallReport& reportInfo, void* context, const AZStd::string& indent) const { AZStd::string logLine; bool previousNodeAlreadyCounted = false; const PrimitiveListRenderNode* prevPrimListNode = nullptr; for (const RenderNode* renderNode : renderNodeList) { if (renderNode->GetType() == RenderNodeType::Mask) { const MaskRenderNode* maskRenderNode = static_cast(renderNode); AZStd::string newIndent = indent + " "; logLine = AZStd::string::format("%sMask (MaskEnabled=%d, UseAlphaTest=%d, DrawBehind=%d, DrawInFront=%d) :\r\n", indent.c_str(), static_cast(maskRenderNode->GetIsMaskingEnabled()), static_cast(maskRenderNode->GetUseAlphaTest()), static_cast(maskRenderNode->GetDrawBehind()), static_cast(maskRenderNode->GetDrawInFront())); AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); logLine = AZStd::string::format("%s Mask shape render nodes:\r\n", indent.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); DebugReportDrawCallsRenderNodeList(maskRenderNode->GetMaskRenderNodeList(), fileHandle, reportInfo, context, newIndent); logLine = AZStd::string::format("%s Mask content render nodes:\r\n", indent.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); DebugReportDrawCallsRenderNodeList(maskRenderNode->GetContentRenderNodeList(), fileHandle, reportInfo, context, newIndent); prevPrimListNode = nullptr; } else if (renderNode->GetType() == RenderNodeType::PrimitiveList) { const PrimitiveListRenderNode* primListRenderNode = static_cast(renderNode); bool nodeExistsBecauseOfExceedingMaxTextures = false; if (prevPrimListNode) { if (prevPrimListNode->GetBlendModeState() == primListRenderNode->GetBlendModeState() && prevPrimListNode->GetIsTextureSRGB() == primListRenderNode->GetIsTextureSRGB() && prevPrimListNode->HasSpaceToAddPrimitive(&primListRenderNode->GetPrimitives().front()) && prevPrimListNode->GetNumTextures() == PrimitiveListRenderNode::MaxTextures) { // this node could have been combined with the previous node if less unique textures were used // so this is an opportunity for texture atlases to reduce draw calls nodeExistsBecauseOfExceedingMaxTextures = true; } } // If this render node was created because the previous render node ran out of textures // then we need to record the previous render node's textures as contributing to exceeding // the max textures. if (nodeExistsBecauseOfExceedingMaxTextures) { if (!previousNodeAlreadyCounted) { for (int i = 0; i < prevPrimListNode->GetNumTextures(); ++i) { AZ::Data::Instance texture = prevPrimListNode->GetTexture(i); if (!texture) { texture = AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::White); } bool isClampTextureUsage = prevPrimListNode->GetTextureIsClampMode(i); LyShineDebug::DebugInfoTextureUsage* matchingTextureUsage = nullptr; // The texture should already be in reportInfo because we have already visited the previous // render node. for (LyShineDebug::DebugInfoTextureUsage& reportTextureUsage : reportInfo.m_textures) { if (reportTextureUsage.m_texture == texture && reportTextureUsage.m_isClampTextureUsage == isClampTextureUsage) { matchingTextureUsage = &reportTextureUsage; break; } } if (matchingTextureUsage) { matchingTextureUsage->m_numDrawCallsWhereExceedingMaxTextures++; } } previousNodeAlreadyCounted = true; } } else { previousNodeAlreadyCounted = false; } IRenderer::DynUiPrimitiveList& primitives = primListRenderNode->GetPrimitives(); int numPrimitives = primitives.size(); int numTriangles = 0; for (const IRenderer::DynUiPrimitive& primitive : primitives) { numTriangles += primitive.m_numIndices / 3; } // Write heading to logfile for this render node logLine = AZStd::string::format("%sPrimitive render node (Blend mode=%d, SRGB=%d). NumPrims=%d, NumTris=%d. Using textures:\r\n", indent.c_str(), primListRenderNode->GetBlendModeState(), static_cast(primListRenderNode->GetIsTextureSRGB()), numPrimitives, numTriangles); AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); for (int i = 0; i < primListRenderNode->GetNumTextures(); ++i) { AZ::Data::Instance texture = primListRenderNode->GetTexture(i); if (!texture) { texture = AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::White); } bool isClampTextureUsage = primListRenderNode->GetTextureIsClampMode(i); LyShineDebug::DebugInfoTextureUsage* matchingTextureUsage = nullptr; // Write line to logfile for this texture AZStd::string textureName; AZ::Data::AssetCatalogRequestBus::BroadcastResult(textureName, &AZ::Data::AssetCatalogRequests::GetAssetPathById, texture->GetAssetId()); logLine = AZStd::string::format("%s %s\r\n", indent.c_str(), textureName.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(fileHandle, logLine.c_str(), logLine.size()); // see if texture is in reportInfo for (LyShineDebug::DebugInfoTextureUsage& reportTextureUsage : reportInfo.m_textures) { if (reportTextureUsage.m_texture == texture && reportTextureUsage.m_isClampTextureUsage == isClampTextureUsage) { matchingTextureUsage = &reportTextureUsage; break; } } if (!matchingTextureUsage) { // Texture is not already in reportInfo so add it LyShineDebug::DebugInfoTextureUsage newTextureUsage; newTextureUsage.m_texture = texture; newTextureUsage.m_isClampTextureUsage = isClampTextureUsage; newTextureUsage.m_numCanvasesUsed = 0; newTextureUsage.m_numDrawCallsUsed = 0; newTextureUsage.m_numDrawCallsWhereExceedingMaxTextures = 0; newTextureUsage.m_lastContextUsed = nullptr; reportInfo.m_textures.push_back(newTextureUsage); matchingTextureUsage = &reportInfo.m_textures.back(); } matchingTextureUsage->m_numDrawCallsUsed++; if (nodeExistsBecauseOfExceedingMaxTextures) { matchingTextureUsage->m_numDrawCallsWhereExceedingMaxTextures++; } if (matchingTextureUsage->m_lastContextUsed != context) { matchingTextureUsage->m_numCanvasesUsed++; matchingTextureUsage->m_lastContextUsed = context; } } prevPrimListNode = primListRenderNode; } } } #endif //////////////////////////////////////////////////////////////////////////////////////////////////// int RenderGraph::GetBlendModeState(LyShine::BlendMode blendMode, bool isShaderOutputPremultAlpha) const { // Our blend modes are complicated by the fact we want to be able to render to a render target and then // render from that render target texture to the back buffer and get the same result as if we rendered // directly to the back buffer. This should be true even if the render target texture does not end up // fully opaque. // If the blend mode is LyShine::BlendMode::Normal and we just use GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA // then this doesn't work for render targets that end up with transparency. To make it work the alpha has to be // accumulated as we render it into the alpha channel of the render target. If we use: // GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA it gets used for both the color blend op and the alpha blend op // so we end up with: dstAlpha = srcAlpha * srcAlpha + dstAlpha * (1-srcAlpha). // This does not accumulate properly. // What we actually want is: dstAlpha = srcAlpha + dstAlpha * (1-srcAlpha) // So that would mean for alpha we want GS_BLSRC_ONE | GS_BLDST_ONEMINUSSRCALPHA // If the IRenderer::SetState allowed us to set the alpha and color blend op separately that would be pretty simple. // However, it does not. So we use a work around. We use a variant of the shader that premultiplies the output // color by the output alpha. So using that variant means that: // GS_BLSRC_ONE | GS_BLDST_ONEMINUSSRCALPHA // will give the same *color* result as GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA // while giving us the alpha result that we want. // // For blend modes other than LyShine::BlendMode::Normal we make similar adjustments. This works well for // LyShine::BlendMode::Add. For the other three blend modes we cannot get the same results - but the results // for those blend modes have always been inadequate. Until we get full control over the blend ops // we won't be able to properly support those blend modes by using blend states. Even then to do them // properly might require shader changes also. For the moment using the blend modes Screen, Darken, Lighten // is not encouraged, especially when rendering to a render target. int flags = GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA; // the default switch (blendMode) { case LyShine::BlendMode::Normal: // This is the default mode that does an alpha blend by interpolating based on src alpha if (isShaderOutputPremultAlpha) { flags = GS_BLSRC_ONE | GS_BLDST_ONEMINUSSRCALPHA; } else { flags = GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA; } break; case LyShine::BlendMode::Add: // This works well, the amount of the src color added is controlled by src alpha if (isShaderOutputPremultAlpha) { flags = GS_BLSRC_ONE | GS_BLDST_ONE; } else { flags = GS_BLSRC_SRCALPHA | GS_BLDST_ONE; } break; case LyShine::BlendMode::Screen: // This is a poor approximation of the PhotoShop Screen mode but trying to take some account of src alpha // In Photoshop this would be 1 - ( (1-SrcColor) * (1-DstColor) ) // So we should use a blend op of multiply but the IRenderer interface doesn't support that. We get some multiply // from GS_BLDST_ONEMINUSSRCCOL which multiplies the DstColor by (1-SrcColor) if (isShaderOutputPremultAlpha) { flags = GS_BLSRC_ONE | GS_BLDST_ONEMINUSSRCCOL; } else { flags = GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCCOL; } break; case LyShine::BlendMode::Darken: // This is a poor approximation of the PhotoShop Darken mode but trying to take some account of src alpha // In Photoshop Darken means min(SrcColor, DstColor) if (isShaderOutputPremultAlpha) { flags = GS_BLOP_MIN | GS_BLSRC_ONE | GS_BLDST_ONE | GS_BLALPHA_MAX; } else { flags = GS_BLOP_MIN | GS_BLSRC_ONEMINUSSRCALPHA | GS_BLDST_ONE; } break; case LyShine::BlendMode::Lighten: // This is a pretty good an approximation of the PhotoShop Lighten mode but trying to take some account of src alpha // In PhotoShop Lighten means max(SrcColor, DstColor) if (isShaderOutputPremultAlpha) { flags = GS_BLOP_MAX | GS_BLSRC_ONE| GS_BLDST_ONE; } else { flags = GS_BLOP_MAX | GS_BLSRC_SRCALPHA | GS_BLDST_ONE; } break; } return flags; } void RenderGraph::SetRttPassesEnabled(UiRenderer* uiRenderer, bool enabled) { // Enable or disable the rtt render passes AZ::RPI::SceneId sceneId = uiRenderer->GetViewportContext()->GetRenderScene()->GetId(); for (RenderTargetRenderNode* renderTargetRenderNode : m_renderTargetRenderNodes) { // Find the rtt pass to disable AZ::RPI::RasterPass* rttPass = nullptr; LyShinePassRequestBus::EventResult(rttPass, sceneId, &LyShinePassRequestBus::Events::GetRttPass, renderTargetRenderNode->GetRenderTargetName()); if (rttPass) { rttPass->SetEnabled(enabled); } } } }