Reenable LyShine mask support now using Atom (#1218)

* Add option to set stencil ref in Dynamic Draw Context

* Add depth/stencil attachment slot to UI pass

* Rework mask rendering to use Atom

* Add missing circle mask image
main
michabr 5 years ago committed by GitHub
parent 0c7605c9b6
commit 6584e1290b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -322,7 +322,14 @@
"Pass": "AuxGeomPass",
"Attachment": "ColorInputOutput"
}
}
},
{
"LocalSlot": "DepthInputOutput",
"AttachmentRef": {
"Pass": "DepthPrePass",
"Attachment": "Depth"
}
}
]
},
{

@ -427,6 +427,13 @@
"Pass": "DebugOverlayPass",
"Attachment": "InputOutput"
}
},
{
"LocalSlot": "DepthInputOutput",
"AttachmentRef": {
"Pass": "DepthPrePass",
"Attachment": "Depth"
}
}
]
},

@ -7,6 +7,23 @@
"Name": "UIPassTemplate",
"PassClass": "RasterPass",
"Slots": [
{
"Name": "DepthInputOutput",
"SlotType": "InputOutput",
"ScopeAttachmentUsage": "DepthStencil",
"LoadStoreAction": {
"ClearValue": {
"Type": "DepthStencil",
"Value": [
0.0,
0.0,
0.0,
0.0
]
},
"LoadActionStencil": "Clear"
}
},
{
"Name": "InputOutput",
"SlotType": "InputOutput",

@ -10,6 +10,11 @@
{
"Name": "InputOutput",
"SlotType": "InputOutput"
},
{
"Name": "DepthInputOutput",
"SlotType": "InputOutput",
"ScopeAttachmentUsage": "DepthStencil"
}
],
"PassRequests": [
@ -24,6 +29,13 @@
"Pass": "Parent",
"Attachment": "InputOutput"
}
},
{
"LocalSlot": "DepthInputOutput",
"AttachmentRef": {
"Pass": "Parent",
"Attachment": "DepthInputOutput"
}
}
],
"PassData": {

@ -138,6 +138,12 @@ namespace AZ
//! Without per draw viewport, the viewport setup in pass is usually used.
void UnsetViewport();
//! Set stencil reference for following draws which are added to this DynamicDrawContext
void SetStencilReference(uint8_t stencilRef);
//! Get the current stencil reference.
uint8_t GetStencilReference() const;
//! Draw Indexed primitives with vertex and index data and per draw srg
//! The per draw srg need to be provided if it's required by shader.
void DrawIndexed(void* vertexData, uint32_t vertexCount, void* indexData, uint32_t indexCount, RHI::IndexFormat indexFormat, Data::Instance < ShaderResourceGroup> drawSrg = nullptr);
@ -204,10 +210,13 @@ namespace AZ
bool m_useScissor = false;
RHI::Scissor m_scissor;
// current scissor
// current viewport
bool m_useViewport = false;
RHI::Viewport m_viewport;
// Current stencil reference value
uint8_t m_stencilRef = 0;
// Cached RHI pipeline states for different combination of render states
AZStd::unordered_map<HashValue64, const RHI::PipelineState*> m_cachedRhiPipelineStates;

@ -382,6 +382,16 @@ namespace AZ
m_useViewport = false;
}
void DynamicDrawContext::SetStencilReference(uint8_t stencilRef)
{
m_stencilRef = stencilRef;
}
uint8_t DynamicDrawContext::GetStencilReference() const
{
return m_stencilRef;
}
void DynamicDrawContext::SetShaderVariant(ShaderVariantId shaderVariantId)
{
AZ_Assert( m_initialized && m_supportShaderVariants, "DynamicDrawContext is not initialized or unable to support shader variants. "
@ -475,6 +485,9 @@ namespace AZ
drawItem.m_viewports = &m_viewport;
}
// Set stencil reference. Used when stencil is enabled.
drawItem.m_stencilRef = m_stencilRef;
drawItemInfo.m_sortKey = m_sortKey++;
m_cachedDrawItems.emplace_back(drawItemInfo);
}

@ -4,7 +4,7 @@
{
"StableId": 1,
"Options": {
"o_preMultiplyAlpha": "true",
"o_preMultiplyAlpha": "false",
"o_alphaTest": "false",
"o_srgbWrite": "true",
"o_modulate": "Modulate::None"
@ -14,7 +14,7 @@
"StableId": 2,
"Options": {
"o_preMultiplyAlpha": "false",
"o_alphaTest": "false",
"o_alphaTest": "true",
"o_srgbWrite": "true",
"o_modulate": "Modulate::None"
}

@ -137,7 +137,11 @@ namespace LyShine
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> dynamicDraw = uiRenderer->GetDynamicDrawContext();
const UiRenderer::UiShaderData& uiShaderData = uiRenderer->GetUiShaderData();
dynamicDraw->SetShaderVariant(uiShaderData.m_shaderVariantDefault);
// 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<AZ::RPI::ShaderResourceGroup> drawSrg = dynamicDraw->NewDrawSrg();
@ -307,7 +311,7 @@ namespace LyShine
////////////////////////////////////////////////////////////////////////////////////////////////////
void MaskRenderNode::Render(UiRenderer* uiRenderer)
{
int priorBaseState = uiRenderer->GetBaseState();
UiRenderer::BaseState priorBaseState = uiRenderer->GetBaseState();
if (m_isMaskingEnabled || m_drawBehind)
{
@ -369,68 +373,61 @@ namespace LyShine
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////
void MaskRenderNode::SetupBeforeRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState)
void MaskRenderNode::SetupBeforeRenderingMask(UiRenderer* uiRenderer, 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
int alphaTest = 0;
if (m_useAlphaTest)
{
alphaTest = GS_ALPHATEST_GREATER;
}
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
int colorMask = GS_COLMASK_NONE;
curBaseState.m_blendState.m_enable = false;
curBaseState.m_blendState.m_writeMask = 0x0;
if ((m_drawBehind && firstPass) ||
(m_drawInFront && !firstPass))
{
colorMask = 0; // mask everything, don't write color or alpha, we just write to stencil buffer
curBaseState.m_blendState.m_enable = true;
curBaseState.m_blendState.m_writeMask = 0xF;
}
if (m_isMaskingEnabled)
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
int passOp = 0;
if (firstPass)
{
passOp = STENCOP_PASS(FSS_STENCOP_INCR);
gEnv->pRenderer->PushProfileMarker(s_maskIncrProfileMarker);
stencilOpState.m_passOp = AZ::RHI::StencilOp::Increment;
}
else
{
passOp = STENCOP_PASS(FSS_STENCOP_DECR);
gEnv->pRenderer->PushProfileMarker(s_maskDecrProfileMarker);
stencilOpState.m_passOp = AZ::RHI::StencilOp::Decrement;
}
curBaseState.m_stencilState.m_frontFace = stencilOpState;
curBaseState.m_stencilState.m_backFace = stencilOpState;
// set up for stencil write
const uint32 stencilRef = uiRenderer->GetStencilRef();
const uint32 stencilMask = 0xFF;
const uint32 stencilWriteMask = 0xFF;
const int32 stencilState = STENC_FUNC(FSS_STENCFUNC_EQUAL)
| STENCOP_FAIL(FSS_STENCOP_KEEP) | STENCOP_ZFAIL(FSS_STENCOP_KEEP) | passOp;
gEnv->pRenderer->SetStencilState(stencilState, stencilRef, stencilMask, stencilWriteMask);
// Set the base state that should be used when rendering the renderable component(s) on this
// element
uiRenderer->SetBaseState(priorBaseState | GS_STENCIL | alphaTest | colorMask);
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> dynamicDraw = uiRenderer->GetDynamicDrawContext();
dynamicDraw->SetStencilReference(uiRenderer->GetStencilRef());
curBaseState.m_stencilState.m_enable = true;
curBaseState.m_stencilState.m_writeMask = 0xFF;
}
else
{
// masking is not enabled
// Even if not masking we still use alpha test (if checked). This is primarily to help the user to
// visualize what their alpha tested mask looks like.
if (colorMask || alphaTest)
{
uiRenderer->SetBaseState(priorBaseState | colorMask | alphaTest);
}
curBaseState.m_stencilState.m_enable = false;
}
uiRenderer->SetBaseState(curBaseState);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void MaskRenderNode::SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState)
void MaskRenderNode::SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, UiRenderer::BaseState priorBaseState)
{
if (m_isMaskingEnabled)
{
@ -442,26 +439,29 @@ namespace LyShine
if (firstPass)
{
uiRenderer->IncrementStencilRef();
gEnv->pRenderer->PopProfileMarker(s_maskIncrProfileMarker);
}
else
{
uiRenderer->DecrementStencilRef();
gEnv->pRenderer->PopProfileMarker(s_maskDecrProfileMarker);
}
// turn off stencil write and turn on stencil test
const uint32 stencilRef = uiRenderer->GetStencilRef();
const uint32 stencilMask = 0xFF;
const uint32 stencilWriteMask = 0x00;
const int32 stencilState = STENC_FUNC(FSS_STENCFUNC_EQUAL)
| STENCOP_FAIL(FSS_STENCOP_KEEP) | STENCOP_ZFAIL(FSS_STENCOP_KEEP) | STENCOP_PASS(FSS_STENCOP_KEEP);
gEnv->pRenderer->SetStencilState(stencilState, stencilRef, stencilMask, stencilWriteMask);
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> dynamicDraw = uiRenderer->GetDynamicDrawContext();
dynamicDraw->SetStencilReference(uiRenderer->GetStencilRef());
if (firstPass)
{
// first pass, turn on stencil test for drawing children
uiRenderer->SetBaseState(priorBaseState | GS_STENCIL);
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
{
@ -475,7 +475,6 @@ namespace LyShine
// remove any color mask or alpha test that we set in pre-render
uiRenderer->SetBaseState(priorBaseState);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
@ -637,35 +636,24 @@ namespace LyShine
////////////////////////////////////////////////////////////////////////////////////////////////////
void RenderGraph::BeginMask(bool isMaskingEnabled, bool useAlphaTest, bool drawBehind, bool drawInFront)
{
#ifdef LYSHINE_ATOM_TODO // keeping this code for future phase (masks and render targets)
// this uses pool allocator
MaskRenderNode* maskRenderNode = new MaskRenderNode(m_currentMask, isMaskingEnabled, useAlphaTest, drawBehind, drawInFront);
m_currentMask = maskRenderNode;
m_renderNodeListStack.push(&maskRenderNode->GetMaskRenderNodeList());
#else
AZ_UNUSED(drawInFront);
AZ_UNUSED(drawBehind);
AZ_UNUSED(useAlphaTest);
AZ_UNUSED(isMaskingEnabled);
#endif
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void RenderGraph::StartChildrenForMask()
{
#ifdef LYSHINE_ATOM_TODO // keeping this code for future phase (masks and render targets)
AZ_Assert(m_currentMask, "Calling StartChildrenForMask while not defining a mask");
m_renderNodeListStack.pop();
m_renderNodeListStack.push(&m_currentMask->GetContentRenderNodeList());
#endif
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void RenderGraph::EndMask()
{
#ifdef LYSHINE_ATOM_TODO // keeping this code for future phase (masks and render targets)
AZ_Assert(m_currentMask, "Calling EndMask while not defining a mask");
if (m_currentMask)
{
@ -686,7 +674,6 @@ namespace LyShine
m_renderNodeListStack.top()->push_back(newMaskRenderNode);
}
}
#endif
}
////////////////////////////////////////////////////////////////////////////////////////////////////
@ -996,6 +983,12 @@ namespace LyShine
// LYSHINE_ATOM_TODO - will probably need to support this when converting UI Editor to use Atom
AZ_UNUSED(viewportSize);
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> 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.
#ifdef LYSHINE_ATOM_TODO // keeping this code for reference for future phase (render targets)

@ -22,12 +22,11 @@
#include <Atom/RPI.Reflect/Image/Image.h>
#include <AtomCore/Instance/Instance.h>
#include "UiRenderer.h"
#ifndef _RELEASE
#include "LyShineDebug.h"
#endif
class UiRenderer;
namespace LyShine
{
enum RenderNodeType
@ -157,8 +156,8 @@ namespace LyShine
#endif
private: // functions
void SetupBeforeRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState);
void SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState);
void SetupBeforeRenderingMask(UiRenderer* uiRenderer, bool firstPass, UiRenderer::BaseState priorBaseState);
void SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, UiRenderer::BaseState priorBaseState);
private: // data
AZStd::vector<RenderNode*> m_maskRenderNodes; //!< The render nodes used to render the mask shape

@ -20,7 +20,6 @@
#include <Atom/Bootstrap/DefaultWindowBus.h>
#include <Atom/RPI.Public/ViewportContextBus.h>
#include <Atom/RPI.Public/RenderPipeline.h>
#include <IRenderer.h> // LYSHINE_ATOM_TODO - remove when GS_DEPTHFUNC_LEQUAL reference is removed with LyShine render target Atom conversion
#include <AzCore/Math/MatrixUtils.h>
#include <AzCore/Debug/Trace.h>
@ -33,9 +32,7 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
UiRenderer::UiRenderer(AZ::RPI::ViewportContextPtr viewportContext)
: m_baseState(GS_DEPTHFUNC_LEQUAL)
, m_stencilRef(0)
, m_viewportContext(viewportContext)
: m_viewportContext(viewportContext)
{
// Use bootstrap scene event to indicate when the RPI has fully
// initialized with all assets loaded and is ready to be used
@ -127,6 +124,8 @@ void UiRenderer::CreateDynamicDrawContext(AZ::RPI::ScenePtr scene, AZ::Data::Ins
{ "TEXCOORD", AZ::RHI::Format::R32G32_FLOAT },
{ "BLENDINDICES", AZ::RHI::Format::R16G16_UINT } }
);
m_dynamicDraw->AddDrawStateOptions(AZ::RPI::DynamicDrawContext::DrawStateOptions::StencilState
| AZ::RPI::DynamicDrawContext::DrawStateOptions::BlendMode);
m_dynamicDraw->EndInit();
}
@ -170,25 +169,24 @@ void UiRenderer::CacheShaderData(const AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext>
shaderOptionsDefault.push_back(AZ::RPI::ShaderOption(AZ::Name("o_srgbWrite"), AZ::Name("true")));
shaderOptionsDefault.push_back(AZ::RPI::ShaderOption(AZ::Name("o_modulate"), AZ::Name("Modulate::None")));
m_uiShaderData.m_shaderVariantDefault = dynamicDraw->UseShaderVariant(shaderOptionsDefault);
AZ::RPI::ShaderOptionList shaderOptionsAlphaTest;
shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_preMultiplyAlpha"), AZ::Name("false")));
shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_alphaTest"), AZ::Name("true")));
shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_srgbWrite"), AZ::Name("true")));
shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_modulate"), AZ::Name("Modulate::None")));
m_uiShaderData.m_shaderVariantAlphaTest = dynamicDraw->UseShaderVariant(shaderOptionsAlphaTest);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiRenderer::BeginUiFrameRender()
{
#ifdef LYSHINE_ATOM_TODO
m_renderer = gEnv->pRenderer;
// we are rendering at the end of the frame, after tone mapping, so we should be writing sRGB values
m_renderer->SetSrgbWrite(true);
#ifndef _RELEASE
if (m_debugTextureDataRecordLevel > 0)
{
m_texturesUsedInFrame.clear();
}
#endif
#endif
// Various platform drivers expect all texture slots used in the shader to be bound
BindNullTexture();
}
@ -204,18 +202,10 @@ void UiRenderer::EndUiFrameRender()
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiRenderer::BeginCanvasRender()
{
#ifdef LYSHINE_ATOM_TODO
m_baseState = GS_NODEPTHTEST;
m_stencilRef = 0;
// Set default starting state
IRenderer* renderer = gEnv->pRenderer;
renderer->SetCullMode(R_CULL_DISABLE);
renderer->SetColorOp(eCO_MODULATE, eCO_MODULATE, DEF_TEXARG0, DEF_TEXARG0);
renderer->SetState(GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA | GS_NODEPTHTEST);
#endif
// Set base state
m_baseState.ResetToDefault();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
@ -229,11 +219,13 @@ AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> UiRenderer::GetDynamicDrawContext()
return m_dynamicDraw;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
const UiRenderer::UiShaderData& UiRenderer::GetUiShaderData()
{
return m_uiShaderData;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::Matrix4x4 UiRenderer::GetModelViewProjectionMatrix()
{
auto viewportContext = GetViewportContext();
@ -253,6 +245,7 @@ AZ::Matrix4x4 UiRenderer::GetModelViewProjectionMatrix()
return modelViewProjMat;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::Vector2 UiRenderer::GetViewportSize()
{
auto viewportContext = GetViewportContext();
@ -267,17 +260,30 @@ AZ::Vector2 UiRenderer::GetViewportSize()
}
////////////////////////////////////////////////////////////////////////////////////////////////////
int UiRenderer::GetBaseState()
UiRenderer::BaseState UiRenderer::GetBaseState()
{
return m_baseState;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiRenderer::SetBaseState(int state)
void UiRenderer::SetBaseState(BaseState state)
{
m_baseState = state;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::RPI::ShaderVariantId UiRenderer::GetCurrentShaderVariant()
{
AZ::RPI::ShaderVariantId variantId = m_uiShaderData.m_shaderVariantDefault;
if (m_baseState.m_useAlphaTest)
{
variantId = m_uiShaderData.m_shaderVariantAlphaTest;
}
return variantId;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
uint32 UiRenderer::GetStencilRef()
{
@ -354,6 +360,7 @@ void UiRenderer::DebugDisplayTextureData(int recordingOption)
{
if (recordingOption > 0)
{
#ifdef LYSHINE_ATOM_TODO // Convert debug to use Atom images
// compute the total area of all the textures, also create a vector that we can sort by area
AZStd::vector<ITexture*> textures;
int totalArea = 0;
@ -431,6 +438,7 @@ void UiRenderer::DebugDisplayTextureData(int recordingOption)
texture->GetWidth(), texture->GetHeight(), texture->GetDataSize(), texture->GetFormatName(), texture->GetName());
WriteLine(buffer, white);
}
#endif
}
}

@ -14,6 +14,7 @@
#include <Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h>
#include <Atom/RPI.Public/WindowContext.h>
#include <Atom/RPI.Public/ViewportContext.h>
#include <Atom/RHI.Reflect/RenderStates.h>
#include <Atom/Bootstrap/BootstrapNotificationBus.h>
#ifndef _RELEASE
@ -38,6 +39,36 @@ public: // types
AZ::RHI::ShaderInputConstantIndex m_isClampInputIndex;
AZ::RPI::ShaderVariantId m_shaderVariantDefault;
AZ::RPI::ShaderVariantId m_shaderVariantAlphaTest;
};
// Base state
struct BaseState
{
BaseState()
{
ResetToDefault();
}
void ResetToDefault()
{
// Enable blend/color write
m_blendState.m_enable = true;
m_blendState.m_writeMask = 0xF;
m_blendState.m_blendSource = AZ::RHI::BlendFactor::AlphaSource;
m_blendState.m_blendDest = AZ::RHI::BlendFactor::AlphaSourceInverse;
m_blendState.m_blendOp = AZ::RHI::BlendOp::Add;
// Disable stencil
m_stencilState = AZ::RHI::StencilState();
m_stencilState.m_enable = 0;
m_useAlphaTest = false;
}
AZ::RHI::TargetBlendState m_blendState;
AZ::RHI::StencilState m_stencilState;
bool m_useAlphaTest = false;
};
public: // member functions
@ -74,10 +105,13 @@ public: // member functions
AZ::Vector2 GetViewportSize();
//! Get the current base state
int GetBaseState();
BaseState GetBaseState();
//! Set the base state
void SetBaseState(int state);
void SetBaseState(BaseState state);
//! Get the shader variant based on current render properties
AZ::RPI::ShaderVariantId GetCurrentShaderVariant();
//! Get the current stencil test reference value
uint32 GetStencilRef();
@ -126,8 +160,8 @@ protected: // attributes
static constexpr char LogName[] = "UiRenderer";
int m_baseState;
uint32 m_stencilRef;
BaseState m_baseState;
uint32 m_stencilRef = 0;
UiShaderData m_uiShaderData;
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> m_dynamicDraw;

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8474b897fe02f70ed8d0e5c47cae8c816c832a7a5739b8c32317737fd275774f
size 1069752
Loading…
Cancel
Save