//
// Copyright 2019 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// FramebufferMtl.mm:
//    Implements the class methods for FramebufferMtl.
//

#include "libANGLE/angletypes.h"
#include "libANGLE/renderer/metal/ContextMtl.h"

#include <TargetConditionals.h>

#include "common/MemoryBuffer.h"
#include "common/angleutils.h"
#include "common/debug.h"
#include "libANGLE/ErrorStrings.h"
#include "libANGLE/renderer/metal/BufferMtl.h"
#include "libANGLE/renderer/metal/DisplayMtl.h"
#include "libANGLE/renderer/metal/FrameBufferMtl.h"
#include "libANGLE/renderer/metal/SurfaceMtl.h"
#include "libANGLE/renderer/metal/mtl_utils.h"
#include "libANGLE/renderer/renderer_utils.h"

namespace rx
{
namespace
{
// Override clear color based on texture's write mask
void OverrideMTLClearColor(const mtl::TextureRef &texture,
                           const mtl::ClearColorValue &clearColor,
                           MTLClearColor *colorOut)
{
    *colorOut =
        mtl::EmulatedAlphaClearColor(clearColor.toMTLClearColor(), texture->getColorWritableMask());
}

const gl::InternalFormat &GetReadAttachmentInfo(const gl::Context *context,
                                                RenderTargetMtl *renderTarget)
{
    GLenum implFormat;

    if (renderTarget && renderTarget->getFormat())
    {
        implFormat = renderTarget->getFormat()->actualAngleFormat().fboImplementationInternalFormat;
    }
    else
    {
        implFormat = GL_NONE;
    }

    return gl::GetSizedInternalFormatInfo(implFormat);
}

}  // namespace

// FramebufferMtl implementation
FramebufferMtl::FramebufferMtl(const gl::FramebufferState &state, ContextMtl *context, bool flipY)
    : FramebufferImpl(state),
      mColorRenderTargets(context->getNativeCaps().maxColorAttachments, nullptr),
      mBackbuffer(nullptr),
      mFlipY(flipY)
{
    reset();
}

FramebufferMtl::~FramebufferMtl() {}

void FramebufferMtl::reset()
{
    for (auto &rt : mColorRenderTargets)
    {
        rt = nullptr;
    }
    mDepthRenderTarget = mStencilRenderTarget = nullptr;

    mRenderPassFirstColorAttachmentFormat = nullptr;

    mReadPixelBuffer = nullptr;
}

void FramebufferMtl::destroy(const gl::Context *context)
{
    reset();
}

angle::Result FramebufferMtl::discard(const gl::Context *context,
                                      size_t count,
                                      const GLenum *attachments)
{
    return invalidate(context, count, attachments);
}

angle::Result FramebufferMtl::invalidate(const gl::Context *context,
                                         size_t count,
                                         const GLenum *attachments)
{
    return invalidateImpl(context, count, attachments);
}

angle::Result FramebufferMtl::invalidateSub(const gl::Context *context,
                                            size_t count,
                                            const GLenum *attachments,
                                            const gl::Rectangle &area)
{
    if (area.encloses(getCompleteRenderArea()))
    {
        return invalidateImpl(context, count, attachments);
    }
    return angle::Result::Continue;
}

angle::Result FramebufferMtl::clear(const gl::Context *context, GLbitfield mask)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);

    mtl::ClearRectParams clearOpts;

    bool clearColor   = IsMaskFlagSet(mask, static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT));
    bool clearDepth   = IsMaskFlagSet(mask, static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT));
    bool clearStencil = IsMaskFlagSet(mask, static_cast<GLbitfield>(GL_STENCIL_BUFFER_BIT));

    gl::DrawBufferMask clearColorBuffers;
    if (clearColor)
    {
        clearColorBuffers    = mState.getEnabledDrawBuffers();
        clearOpts.clearColor = contextMtl->getClearColorValue();
    }
    if (clearDepth)
    {
        clearOpts.clearDepth = contextMtl->getClearDepthValue();
    }
    if (clearStencil)
    {
        clearOpts.clearStencil = contextMtl->getClearStencilValue();
    }

    return clearImpl(context, clearColorBuffers, &clearOpts);
}

angle::Result FramebufferMtl::clearBufferfv(const gl::Context *context,
                                            GLenum buffer,
                                            GLint drawbuffer,
                                            const GLfloat *values)
{
    mtl::ClearRectParams clearOpts;

    gl::DrawBufferMask clearColorBuffers;
    if (buffer == GL_DEPTH)
    {
        clearOpts.clearDepth = values[0];
    }
    else
    {
        clearColorBuffers.set(drawbuffer);
        clearOpts.clearColor = mtl::ClearColorValue(values[0], values[1], values[2], values[3]);
    }

    return clearImpl(context, clearColorBuffers, &clearOpts);
}
angle::Result FramebufferMtl::clearBufferuiv(const gl::Context *context,
                                             GLenum buffer,
                                             GLint drawbuffer,
                                             const GLuint *values)
{
    gl::DrawBufferMask clearColorBuffers;
    clearColorBuffers.set(drawbuffer);

    mtl::ClearRectParams clearOpts;
    clearOpts.clearColor = mtl::ClearColorValue(values[0], values[1], values[2], values[3]);

    return clearImpl(context, clearColorBuffers, &clearOpts);
}
angle::Result FramebufferMtl::clearBufferiv(const gl::Context *context,
                                            GLenum buffer,
                                            GLint drawbuffer,
                                            const GLint *values)
{
    mtl::ClearRectParams clearOpts;

    gl::DrawBufferMask clearColorBuffers;
    if (buffer == GL_STENCIL)
    {
        clearOpts.clearStencil = values[0] & mtl::kStencilMaskAll;
    }
    else
    {
        clearColorBuffers.set(drawbuffer);
        clearOpts.clearColor = mtl::ClearColorValue(values[0], values[1], values[2], values[3]);
    }

    return clearImpl(context, clearColorBuffers, &clearOpts);
}
angle::Result FramebufferMtl::clearBufferfi(const gl::Context *context,
                                            GLenum buffer,
                                            GLint drawbuffer,
                                            GLfloat depth,
                                            GLint stencil)
{
    mtl::ClearRectParams clearOpts;
    clearOpts.clearDepth   = depth;
    clearOpts.clearStencil = stencil & mtl::kStencilMaskAll;

    return clearImpl(context, gl::DrawBufferMask(), &clearOpts);
}

const gl::InternalFormat &FramebufferMtl::getImplementationColorReadFormat(
    const gl::Context *context) const
{
    return GetReadAttachmentInfo(context, getColorReadRenderTargetNoCache(context));
}

angle::Result FramebufferMtl::readPixels(const gl::Context *context,
                                         const gl::Rectangle &area,
                                         GLenum format,
                                         GLenum type,
                                         const gl::PixelPackState &pack,
                                         gl::Buffer *packBuffer,
                                         void *pixels)
{
    // Clip read area to framebuffer.
    const gl::Extents &fbSize = getState().getReadAttachment()->getSize();
    const gl::Rectangle fbRect(0, 0, fbSize.width, fbSize.height);

    gl::Rectangle clippedArea;
    if (!ClipRectangle(area, fbRect, &clippedArea))
    {
        // nothing to read
        return angle::Result::Continue;
    }
    gl::Rectangle flippedArea = getCorrectFlippedReadArea(context, clippedArea);

    ContextMtl *contextMtl = mtl::GetImpl(context);

    const gl::InternalFormat &sizedFormatInfo = gl::GetInternalFormatInfo(format, type);

    GLuint outputPitch = 0;
    ANGLE_CHECK_GL_MATH(contextMtl,
                        sizedFormatInfo.computeRowPitch(type, area.width, pack.alignment,
                                                        pack.rowLength, &outputPitch));
    GLuint outputSkipBytes = 0;
    ANGLE_CHECK_GL_MATH(contextMtl, sizedFormatInfo.computeSkipBytes(type, outputPitch, 0, pack,
                                                                     false, &outputSkipBytes));

    outputSkipBytes += (clippedArea.x - area.x) * sizedFormatInfo.pixelBytes +
                       (clippedArea.y - area.y) * outputPitch;

    const angle::Format &angleFormat = GetFormatFromFormatType(format, type);

    PackPixelsParams params(flippedArea, angleFormat, outputPitch, pack.reverseRowOrder, packBuffer,
                            0);

    if (params.packBuffer)
    {
        // If PBO is active, pixels is treated as offset.
        params.offset = reinterpret_cast<ptrdiff_t>(pixels) + outputSkipBytes;
    }

    if (mFlipY)
    {
        params.reverseRowOrder = !params.reverseRowOrder;
    }

    ANGLE_TRY(readPixelsImpl(context, flippedArea, params, getColorReadRenderTarget(context),
                             static_cast<uint8_t *>(pixels) + outputSkipBytes));

    return angle::Result::Continue;
}

namespace
{

using FloatRectangle = gl::RectangleImpl<float>;

float clamp0Max(float v, float max)
{
    return std::max(0.0f, std::min(max, v));
}

void ClampToBoundsAndAdjustCorrespondingValue(float a,
                                              float originalASize,
                                              float maxSize,
                                              float b,
                                              float originalBSize,
                                              float *newA,
                                              float *newB)
{
    float clippedA = clamp0Max(a, maxSize);
    float delta    = clippedA - a;
    *newA          = clippedA;
    *newB          = b + delta * originalBSize / originalASize;
}

void ClipRectToBoundsAndAdjustCorrespondingRect(const FloatRectangle &a,
                                                const gl::Rectangle &originalA,
                                                const gl::Rectangle &clipDimensions,
                                                const FloatRectangle &b,
                                                const gl::Rectangle &originalB,
                                                FloatRectangle *newA,
                                                FloatRectangle *newB)
{
    float newAValues[4];
    float newBValues[4];
    ClampToBoundsAndAdjustCorrespondingValue(a.x0(), originalA.width, clipDimensions.width, b.x0(),
                                             originalB.width, &newAValues[0], &newBValues[0]);
    ClampToBoundsAndAdjustCorrespondingValue(a.y0(), originalA.height, clipDimensions.height,
                                             b.y0(), originalB.height, &newAValues[1],
                                             &newBValues[1]);
    ClampToBoundsAndAdjustCorrespondingValue(a.x1(), originalA.width, clipDimensions.width, b.x1(),
                                             originalB.width, &newAValues[2], &newBValues[2]);
    ClampToBoundsAndAdjustCorrespondingValue(a.y1(), originalA.height, clipDimensions.height,
                                             b.y1(), originalB.height, &newAValues[3],
                                             &newBValues[3]);

    *newA = FloatRectangle(newAValues);
    *newB = FloatRectangle(newBValues);
}

void ClipRectsToBoundsAndAdjustCorrespondingRect(const FloatRectangle &a,
                                                 const gl::Rectangle &originalA,
                                                 const gl::Rectangle &aClipDimensions,
                                                 const FloatRectangle &b,
                                                 const gl::Rectangle &originalB,
                                                 const gl::Rectangle &bClipDimensions,
                                                 FloatRectangle *newA,
                                                 FloatRectangle *newB)
{
    FloatRectangle tempA;
    FloatRectangle tempB;
    ClipRectToBoundsAndAdjustCorrespondingRect(a, originalA, aClipDimensions, b, originalB, &tempA,
                                               &tempB);
    ClipRectToBoundsAndAdjustCorrespondingRect(tempB, originalB, bClipDimensions, tempA, originalA,
                                               newB, newA);
}

void RoundValueAndAdjustCorrespondingValue(float a,
                                           float originalASize,
                                           float b,
                                           float originalBSize,
                                           int *newA,
                                           float *newB)
{
    float roundedA = std::round(a);
    float delta    = roundedA - a;
    *newA          = static_cast<int>(roundedA);
    *newB          = b + delta * originalBSize / originalASize;
}

gl::Rectangle RoundRectToPixelsAndAdjustCorrespondingRectToMatch(const FloatRectangle &a,
                                                                 const gl::Rectangle &originalA,
                                                                 const FloatRectangle &b,
                                                                 const gl::Rectangle &originalB,
                                                                 FloatRectangle *newB)
{
    int newAValues[4];
    float newBValues[4];
    RoundValueAndAdjustCorrespondingValue(a.x0(), originalA.width, b.x0(), originalB.width,
                                          &newAValues[0], &newBValues[0]);
    RoundValueAndAdjustCorrespondingValue(a.y0(), originalA.height, b.y0(), originalB.height,
                                          &newAValues[1], &newBValues[1]);
    RoundValueAndAdjustCorrespondingValue(a.x1(), originalA.width, b.x1(), originalB.width,
                                          &newAValues[2], &newBValues[2]);
    RoundValueAndAdjustCorrespondingValue(a.y1(), originalA.height, b.y1(), originalB.height,
                                          &newAValues[3], &newBValues[3]);

    *newB = FloatRectangle(newBValues);
    return gl::Rectangle(newAValues[0], newAValues[1], newAValues[2] - newAValues[0],
                         newAValues[3] - newAValues[1]);
}

}  // namespace

angle::Result FramebufferMtl::blit(const gl::Context *context,
                                   const gl::Rectangle &sourceAreaIn,
                                   const gl::Rectangle &destAreaIn,
                                   GLbitfield mask,
                                   GLenum filter)
{
    bool blitColorBuffer   = (mask & GL_COLOR_BUFFER_BIT) != 0;
    bool blitDepthBuffer   = (mask & GL_DEPTH_BUFFER_BIT) != 0;
    bool blitStencilBuffer = (mask & GL_STENCIL_BUFFER_BIT) != 0;

    const gl::State &glState                = context->getState();
    const gl::Framebuffer *glSrcFramebuffer = glState.getReadFramebuffer();

    FramebufferMtl *srcFrameBuffer = mtl::GetImpl(glSrcFramebuffer);

    blitColorBuffer =
        blitColorBuffer && srcFrameBuffer->getColorReadRenderTarget(context) != nullptr;
    blitDepthBuffer   = blitDepthBuffer && srcFrameBuffer->getDepthRenderTarget() != nullptr;
    blitStencilBuffer = blitStencilBuffer && srcFrameBuffer->getStencilRenderTarget() != nullptr;

    if (!blitColorBuffer && !blitDepthBuffer && !blitStencilBuffer)
    {
        // No-op
        return angle::Result::Continue;
    }

    const gl::Rectangle srcFramebufferDimensions = srcFrameBuffer->getCompleteRenderArea();
    const gl::Rectangle dstFramebufferDimensions = this->getCompleteRenderArea();

    FloatRectangle srcRect(sourceAreaIn);
    FloatRectangle dstRect(destAreaIn);

    FloatRectangle clippedSrcRect;
    FloatRectangle clippedDstRect;
    ClipRectsToBoundsAndAdjustCorrespondingRect(srcRect, sourceAreaIn, srcFramebufferDimensions,
                                                dstRect, destAreaIn, dstFramebufferDimensions,
                                                &clippedSrcRect, &clippedDstRect);

    FloatRectangle adjustedSrcRect;
    gl::Rectangle srcClippedDestArea = RoundRectToPixelsAndAdjustCorrespondingRectToMatch(
        clippedDstRect, destAreaIn, clippedSrcRect, sourceAreaIn, &adjustedSrcRect);

    if (srcFrameBuffer->flipY())
    {
        adjustedSrcRect.y =
            srcFramebufferDimensions.height - adjustedSrcRect.y - adjustedSrcRect.height;
        adjustedSrcRect = adjustedSrcRect.flip(false, true);
    }

    // If the destination is flipped in either direction, we will flip the source instead so that
    // the destination area is always unflipped.
    adjustedSrcRect =
        adjustedSrcRect.flip(srcClippedDestArea.isReversedX(), srcClippedDestArea.isReversedY());
    srcClippedDestArea = srcClippedDestArea.removeReversal();

    // Clip the destination area to the framebuffer size and scissor.
    gl::Rectangle scissoredDestArea;
    if (!gl::ClipRectangle(ClipRectToScissor(glState, dstFramebufferDimensions, false),
                           srcClippedDestArea, &scissoredDestArea))
    {
        return angle::Result::Continue;
    }

    // Use blit with draw
    mtl::BlitParams baseParams;
    baseParams.dstTextureSize =
        gl::Extents(dstFramebufferDimensions.width, dstFramebufferDimensions.height, 1);
    baseParams.dstRect        = srcClippedDestArea;
    baseParams.dstScissorRect = scissoredDestArea;
    baseParams.dstFlipY       = this->flipY();

    baseParams.srcNormalizedCoords =
        mtl::NormalizedCoords(adjustedSrcRect.x, adjustedSrcRect.y, adjustedSrcRect.width,
                              adjustedSrcRect.height, srcFramebufferDimensions);
    // This flag is for auto flipping the rect inside RenderUtils. Since we already flip it using
    // getCorrectFlippedReadArea(). This flag is not needed.
    baseParams.srcYFlipped = false;
    baseParams.unpackFlipX = false;
    baseParams.unpackFlipY = false;

    return blitWithDraw(context, srcFrameBuffer, blitColorBuffer, blitDepthBuffer,
                        blitStencilBuffer, filter, baseParams);
}

angle::Result FramebufferMtl::blitWithDraw(const gl::Context *context,
                                           FramebufferMtl *srcFrameBuffer,
                                           bool blitColorBuffer,
                                           bool blitDepthBuffer,
                                           bool blitStencilBuffer,
                                           GLenum filter,
                                           const mtl::BlitParams &baseParams)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    // Use blit with draw
    mtl::RenderCommandEncoder *renderEncoder = nullptr;

    // Blit Depth & stencil
    if (blitDepthBuffer || blitStencilBuffer)
    {
        mtl::DepthStencilBlitParams dsBlitParams;
        memcpy(&dsBlitParams, &baseParams, sizeof(baseParams));
        RenderTargetMtl *srcDepthRt   = srcFrameBuffer->getDepthRenderTarget();
        RenderTargetMtl *srcStencilRt = srcFrameBuffer->getStencilRenderTarget();

        if (blitDepthBuffer)
        {
            dsBlitParams.src      = srcDepthRt->getTexture();
            dsBlitParams.srcLevel = srcDepthRt->getLevelIndex();
            dsBlitParams.srcLayer = srcDepthRt->getLayerIndex();
        }

        if (blitStencilBuffer && srcStencilRt->getTexture())
        {
            dsBlitParams.srcStencil = srcStencilRt->getTexture()->getStencilView();
            dsBlitParams.srcLevel   = srcStencilRt->getLevelIndex();
            dsBlitParams.srcLayer   = srcStencilRt->getLayerIndex();

            if (!contextMtl->getDisplay()->getFeatures().hasShaderStencilOutput.enabled &&
                mStencilRenderTarget)
            {
                // Directly writing to stencil in shader is not supported, use temporary copy buffer
                // work around. This is a compute pass.
                mtl::StencilBlitViaBufferParams stencilOnlyBlitParams = dsBlitParams;
                stencilOnlyBlitParams.dstStencil      = mStencilRenderTarget->getTexture();
                stencilOnlyBlitParams.dstStencilLayer = mStencilRenderTarget->getLayerIndex();
                stencilOnlyBlitParams.dstStencilLevel = mStencilRenderTarget->getLevelIndex();
                stencilOnlyBlitParams.dstPackedDepthStencilFormat =
                    mStencilRenderTarget->getFormat()->hasDepthAndStencilBits();

                ANGLE_TRY(contextMtl->getDisplay()->getUtils().blitStencilViaCopyBuffer(
                    context, stencilOnlyBlitParams));

                // Prevent the stencil to be blitted with draw again
                dsBlitParams.srcStencil = nullptr;
            }
        }

        // The actual blitting of depth and/or stencil
        renderEncoder = ensureRenderPassStarted(context);
        ANGLE_TRY(contextMtl->getDisplay()->getUtils().blitDepthStencilWithDraw(
            context, renderEncoder, dsBlitParams));
    }  // if (blitDepthBuffer || blitStencilBuffer)
    else
    {
        renderEncoder = ensureRenderPassStarted(context);
    }

    // Blit color
    if (blitColorBuffer)
    {
        mtl::ColorBlitParams colorBlitParams;
        memcpy(&colorBlitParams, &baseParams, sizeof(baseParams));

        RenderTargetMtl *srcColorRt = srcFrameBuffer->getColorReadRenderTarget(context);
        ASSERT(srcColorRt);

        colorBlitParams.src      = srcColorRt->getTexture();
        colorBlitParams.srcLevel = srcColorRt->getLevelIndex();
        colorBlitParams.srcLayer = srcColorRt->getLayerIndex();

        colorBlitParams.enabledBuffers = getState().getEnabledDrawBuffers();
        colorBlitParams.filter         = filter;
        colorBlitParams.dstLuminance   = srcColorRt->getFormat()->actualAngleFormat().isLUMA();

        ANGLE_TRY(contextMtl->getDisplay()->getUtils().blitColorWithDraw(
            context, renderEncoder, srcColorRt->getFormat()->actualAngleFormat(), colorBlitParams));
    }

    return angle::Result::Continue;
}

bool FramebufferMtl::totalBitsUsedIsLessThanOrEqualToMaxBitsSupported(
    const gl::Context *context) const
{
    ContextMtl *contextMtl = mtl::GetImpl(context);

    uint32_t bitsUsed = 0;
    for (const gl::FramebufferAttachment &attachment : mState.getColorAttachments())
    {
        if (attachment.isAttached())
        {
            bitsUsed += attachment.getRedSize() + attachment.getGreenSize() +
                        attachment.getBlueSize() + attachment.getAlphaSize();
        }
    }

    return bitsUsed <= contextMtl->getDisplay()->getMaxColorTargetBits();
}

gl::FramebufferStatus FramebufferMtl::checkStatus(const gl::Context *context) const
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    if (!contextMtl->getDisplay()->getFeatures().allowSeparateDepthStencilBuffers.enabled &&
        mState.hasSeparateDepthAndStencilAttachments())
    {
        return gl::FramebufferStatus::Incomplete(
            GL_FRAMEBUFFER_UNSUPPORTED,
            gl::err::kFramebufferIncompleteUnsupportedSeparateDepthStencilBuffers);
    }

    if (mState.getDepthAttachment() && mState.getDepthAttachment()->getFormat().info->depthBits &&
        mState.getDepthAttachment()->getFormat().info->stencilBits)
    {
        return checkPackedDepthStencilAttachment();
    }

    if (mState.getStencilAttachment() &&
        mState.getStencilAttachment()->getFormat().info->depthBits &&
        mState.getStencilAttachment()->getFormat().info->stencilBits)
    {
        return checkPackedDepthStencilAttachment();
    }

    if (!totalBitsUsedIsLessThanOrEqualToMaxBitsSupported(context))
    {
        return gl::FramebufferStatus::Incomplete(
            GL_FRAMEBUFFER_UNSUPPORTED,
            gl::err::kFramebufferIncompleteColorBitsUsedExceedsMaxColorBitsSupported);
    }

    return gl::FramebufferStatus::Complete();
}

gl::FramebufferStatus FramebufferMtl::checkPackedDepthStencilAttachment() const
{
    if (ANGLE_APPLE_AVAILABLE_XCI(10.14, 13.1, 12.0))
    {
        // If depth/stencil attachment has depth & stencil bits, then depth & stencil must not have
        // separate attachment. i.e. They must be the same texture or one of them has no
        // attachment.
        if (mState.hasSeparateDepthAndStencilAttachments())
        {
            WARN() << "Packed depth stencil texture/buffer must not be mixed with other "
                      "texture/buffer.";
            return gl::FramebufferStatus::Incomplete(
                GL_FRAMEBUFFER_UNSUPPORTED,
                gl::err::kFramebufferIncompleteUnsupportedSeparateDepthStencilBuffers);
        }
    }
    else
    {
        // Metal 2.0 and below doesn't allow packed depth stencil texture to be attached only as
        // depth or stencil buffer. i.e. None of the depth & stencil attachment can be null.
        if (!mState.getDepthStencilAttachment())
        {
            WARN() << "Packed depth stencil texture/buffer must be bound to both depth & stencil "
                      "attachment point.";
            return gl::FramebufferStatus::Incomplete(
                GL_FRAMEBUFFER_UNSUPPORTED,
                gl::err::kFramebufferIncompleteUnsupportedSeparateDepthStencilBuffers);
        }
    }
    return gl::FramebufferStatus::Complete();
}

angle::Result FramebufferMtl::syncState(const gl::Context *context,
                                        GLenum binding,
                                        const gl::Framebuffer::DirtyBits &dirtyBits,
                                        gl::Command command)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    bool mustNotifyContext = false;
    // Cache old mRenderPassDesc before update*RenderTarget() invalidate it.
    mtl::RenderPassDesc oldRenderPassDesc = mRenderPassDesc;

    for (size_t dirtyBit : dirtyBits)
    {
        switch (dirtyBit)
        {
            case gl::Framebuffer::DIRTY_BIT_DEPTH_ATTACHMENT:
                ANGLE_TRY(updateDepthRenderTarget(context));
                break;
            case gl::Framebuffer::DIRTY_BIT_STENCIL_ATTACHMENT:
                ANGLE_TRY(updateStencilRenderTarget(context));
                break;
            case gl::Framebuffer::DIRTY_BIT_DEPTH_BUFFER_CONTENTS:
                // Restore depth attachment load action as its content may have been updated
                // after framebuffer invalidation.
                mRenderPassDesc.depthAttachment.loadAction = MTLLoadActionLoad;
                break;
            case gl::Framebuffer::DIRTY_BIT_STENCIL_BUFFER_CONTENTS:
                // Restore stencil attachment load action as its content may have been updated
                // after framebuffer invalidation.
                mRenderPassDesc.stencilAttachment.loadAction = MTLLoadActionLoad;
                break;
            case gl::Framebuffer::DIRTY_BIT_DRAW_BUFFERS:
                mustNotifyContext = true;
                break;
            case gl::Framebuffer::DIRTY_BIT_READ_BUFFER:
            case gl::Framebuffer::DIRTY_BIT_DEFAULT_WIDTH:
            case gl::Framebuffer::DIRTY_BIT_DEFAULT_HEIGHT:
            case gl::Framebuffer::DIRTY_BIT_DEFAULT_SAMPLES:
            case gl::Framebuffer::DIRTY_BIT_DEFAULT_FIXED_SAMPLE_LOCATIONS:
                break;
            default:
            {
                static_assert(gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0 == 0, "FB dirty bits");
                if (dirtyBit < gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_MAX)
                {
                    size_t colorIndexGL = static_cast<size_t>(
                        dirtyBit - gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0);
                    ANGLE_TRY(updateColorRenderTarget(context, colorIndexGL));
                }
                else
                {
                    ASSERT(dirtyBit >= gl::Framebuffer::DIRTY_BIT_COLOR_BUFFER_CONTENTS_0 &&
                           dirtyBit < gl::Framebuffer::DIRTY_BIT_COLOR_BUFFER_CONTENTS_MAX);
                    // NOTE: might need to notify context.

                    // Restore color attachment load action as its content may have been updated
                    // after framebuffer invalidation.
                    size_t colorIndexGL = static_cast<size_t>(
                        dirtyBit - gl::Framebuffer::DIRTY_BIT_COLOR_BUFFER_CONTENTS_0);
                    mRenderPassDesc.colorAttachments[colorIndexGL].loadAction = MTLLoadActionLoad;
                }
                break;
            }
        }
    }

    ANGLE_TRY(prepareRenderPass(context, &mRenderPassDesc));
    bool renderPassChanged = !oldRenderPassDesc.equalIgnoreLoadStoreOptions(mRenderPassDesc);

    if (mustNotifyContext || renderPassChanged)
    {
        FramebufferMtl *currentDrawFramebuffer =
            mtl::GetImpl(context->getState().getDrawFramebuffer());
        if (currentDrawFramebuffer == this)
        {
            contextMtl->onDrawFrameBufferChangedState(context, this, renderPassChanged);
        }

        // Recreate pixel reading buffer if needed in future.
        mReadPixelBuffer = nullptr;
    }

    return angle::Result::Continue;
}

angle::Result FramebufferMtl::getSamplePosition(const gl::Context *context,
                                                size_t index,
                                                GLfloat *xy) const
{
    UNIMPLEMENTED();
    return angle::Result::Stop;
}

bool FramebufferMtl::prepareForUse(const gl::Context *context) const
{
    if (mBackbuffer)
    {
        // Backbuffer might obtain new drawable, which means it might change the
        // the native texture used as the target of the render pass.
        // We need to call this before creating render encoder.
        if (IsError(mBackbuffer->ensureCurrentDrawableObtained(context)))
        {
            return false;
        }

        if (mBackbuffer->hasRobustResourceInit())
        {
            (void)mBackbuffer->initializeContents(context, GL_BACK, gl::ImageIndex::Make2D(0));
            if (mBackbuffer->hasDepthStencil())
            {
                (void)mBackbuffer->initializeContents(context, GL_DEPTH, gl::ImageIndex::Make2D(0));
            }
        }
    }
    return true;
}

RenderTargetMtl *FramebufferMtl::getColorReadRenderTarget(const gl::Context *context) const
{
    if (mState.getReadIndex() >= mColorRenderTargets.size())
    {
        return nullptr;
    }

    if (!prepareForUse(context))
    {
        return nullptr;
    }

    return mColorRenderTargets[mState.getReadIndex()];
}

RenderTargetMtl *FramebufferMtl::getColorReadRenderTargetNoCache(const gl::Context *context) const
{
    if (mState.getReadIndex() >= mColorRenderTargets.size())
    {
        return nullptr;
    }

    if (mBackbuffer)
    {
        // If we have a backbuffer/window surface, we can take the old path here and return
        // the cached color render target.
        return getColorReadRenderTarget(context);
    }
    // If we have no backbuffer, get the attachment from state color attachments, as it may have
    // changed before syncing.
    const gl::FramebufferAttachment *attachment = mState.getColorAttachment(mState.getReadIndex());
    RenderTargetMtl *currentTarget              = nullptr;
    if (attachment->getRenderTarget(context, attachment->getRenderToTextureSamples(),
                                    &currentTarget) == angle::Result::Stop)
    {
        return nullptr;
    }
    return currentTarget;
}

int FramebufferMtl::getSamples() const
{
    return mRenderPassDesc.sampleCount;
}

gl::Rectangle FramebufferMtl::getCompleteRenderArea() const
{
    return gl::Rectangle(0, 0, mState.getDimensions().width, mState.getDimensions().height);
}

bool FramebufferMtl::renderPassHasStarted(ContextMtl *contextMtl) const
{
    return contextMtl->hasStartedRenderPass(mRenderPassDesc);
}

mtl::RenderCommandEncoder *FramebufferMtl::ensureRenderPassStarted(const gl::Context *context)
{
    return ensureRenderPassStarted(context, mRenderPassDesc);
}

mtl::RenderCommandEncoder *FramebufferMtl::ensureRenderPassStarted(const gl::Context *context,
                                                                   const mtl::RenderPassDesc &desc)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);

    if (!prepareForUse(context))
    {
        return nullptr;
    }

    // Only support ensureRenderPassStarted() with different load & store options only. The
    // texture, level, slice must be the same.
    ASSERT(desc.equalIgnoreLoadStoreOptions(mRenderPassDesc));

    mtl::RenderCommandEncoder *encoder = contextMtl->getRenderPassCommandEncoder(desc);

    if (mRenderPassCleanStart)
    {
        // After a clean start we should reset the loadOp to MTLLoadActionLoad in case this render
        // pass could be interrupted by a conversion compute shader pass then being resumed later.
        mRenderPassCleanStart = false;
        for (mtl::RenderPassColorAttachmentDesc &colorAttachment : mRenderPassDesc.colorAttachments)
        {
            colorAttachment.loadAction = MTLLoadActionLoad;
        }
        mRenderPassDesc.depthAttachment.loadAction   = MTLLoadActionLoad;
        mRenderPassDesc.stencilAttachment.loadAction = MTLLoadActionLoad;
    }

    return encoder;
}

void FramebufferMtl::setLoadStoreActionOnRenderPassFirstStart(
    mtl::RenderPassAttachmentDesc *attachmentOut,
    const bool forceDepthStencilMultisampleLoad)
{
    ASSERT(mRenderPassCleanStart);

    mtl::RenderPassAttachmentDesc &attachment = *attachmentOut;

    if (!forceDepthStencilMultisampleLoad && attachment.storeAction == MTLStoreActionDontCare)
    {
        // If we previously discarded attachment's content, then don't need to load it.
        attachment.loadAction = MTLLoadActionDontCare;
    }
    else
    {
        attachment.loadAction = MTLLoadActionLoad;
    }

    if (attachment.hasImplicitMSTexture())
    {
        attachment.storeAction = MTLStoreActionStoreAndMultisampleResolve;
    }
    else
    {
        attachment.storeAction = MTLStoreActionStore;  // Default action is store
    }
}

void FramebufferMtl::onStartedDrawingToFrameBuffer(const gl::Context *context)
{
    mRenderPassCleanStart = true;

    // If any of the render targets need to load their multisample textures, we should do the same
    // for depth/stencil.
    bool forceDepthStencilMultisampleLoad = false;

    // Compute loadOp based on previous storeOp and reset storeOp flags:
    for (mtl::RenderPassColorAttachmentDesc &colorAttachment : mRenderPassDesc.colorAttachments)
    {
        forceDepthStencilMultisampleLoad |=
            colorAttachment.storeAction == MTLStoreActionStoreAndMultisampleResolve;
        setLoadStoreActionOnRenderPassFirstStart(&colorAttachment, false);
    }
    // Depth load/store
    setLoadStoreActionOnRenderPassFirstStart(&mRenderPassDesc.depthAttachment,
                                             forceDepthStencilMultisampleLoad);

    // Stencil load/store
    setLoadStoreActionOnRenderPassFirstStart(&mRenderPassDesc.stencilAttachment,
                                             forceDepthStencilMultisampleLoad);
}

void FramebufferMtl::onFrameEnd(const gl::Context *context)
{
    if (!mBackbuffer || mBackbuffer->preserveBuffer())
    {
        return;
    }

    ContextMtl *contextMtl = mtl::GetImpl(context);
    // Always discard default FBO's depth stencil & multisample buffers at the end of the frame:
    if (this->renderPassHasStarted(contextMtl))
    {
        mtl::RenderCommandEncoder *encoder = contextMtl->getRenderCommandEncoder();

        constexpr GLenum dsAttachments[] = {GL_DEPTH, GL_STENCIL};
        (void)invalidateImpl(context, 2, dsAttachments);
        if (mBackbuffer->getSamples() > 1)
        {
            encoder->setColorStoreAction(MTLStoreActionMultisampleResolve, 0);
        }

        contextMtl->endEncoding(false);

        // Reset discard flag.
        onStartedDrawingToFrameBuffer(context);
    }
}

angle::Result FramebufferMtl::updateColorRenderTarget(const gl::Context *context,
                                                      size_t colorIndexGL)
{
    ASSERT(colorIndexGL < mColorRenderTargets.size());
    // Reset load store action
    mRenderPassDesc.colorAttachments[colorIndexGL].reset();
    return updateCachedRenderTarget(context, mState.getColorAttachment(colorIndexGL),
                                    &mColorRenderTargets[colorIndexGL]);
}

angle::Result FramebufferMtl::updateDepthRenderTarget(const gl::Context *context)
{
    // Reset load store action
    mRenderPassDesc.depthAttachment.reset();
    return updateCachedRenderTarget(context, mState.getDepthAttachment(), &mDepthRenderTarget);
}

angle::Result FramebufferMtl::updateStencilRenderTarget(const gl::Context *context)
{
    // Reset load store action
    mRenderPassDesc.stencilAttachment.reset();
    return updateCachedRenderTarget(context, mState.getStencilAttachment(), &mStencilRenderTarget);
}

angle::Result FramebufferMtl::updateCachedRenderTarget(const gl::Context *context,
                                                       const gl::FramebufferAttachment *attachment,
                                                       RenderTargetMtl **cachedRenderTarget)
{
    RenderTargetMtl *newRenderTarget = nullptr;
    if (attachment)
    {
        ASSERT(attachment->isAttached());
        ANGLE_TRY(attachment->getRenderTarget(context, attachment->getRenderToTextureSamples(),
                                              &newRenderTarget));
    }
    *cachedRenderTarget = newRenderTarget;
    return angle::Result::Continue;
}

angle::Result FramebufferMtl::prepareRenderPass(const gl::Context *context,
                                                mtl::RenderPassDesc *pDescOut)
{
    const gl::DrawBufferMask enabledDrawBuffers = getState().getEnabledDrawBuffers();

    mtl::RenderPassDesc &desc = *pDescOut;

    mRenderPassFirstColorAttachmentFormat = nullptr;
    mRenderPassAttachmentsSameColorType   = true;
    uint32_t maxColorAttachments = static_cast<uint32_t>(mState.getColorAttachments().size());
    desc.numColorAttachments     = 0;
    desc.sampleCount             = 1;
    for (uint32_t colorIndexGL = 0; colorIndexGL < maxColorAttachments; ++colorIndexGL)
    {
        ASSERT(colorIndexGL < mColorRenderTargets.size());

        mtl::RenderPassColorAttachmentDesc &colorAttachment = desc.colorAttachments[colorIndexGL];
        const RenderTargetMtl *colorRenderTarget            = mColorRenderTargets[colorIndexGL];

        // GL allows data types of fragment shader color outputs to be incompatible with disabled
        // color attachments. To prevent various Metal validation issues, assign textures only to
        // enabled attachments.
        if (colorRenderTarget && enabledDrawBuffers.test(colorIndexGL))
        {
            colorRenderTarget->toRenderPassAttachmentDesc(&colorAttachment);

            desc.numColorAttachments = std::max(desc.numColorAttachments, colorIndexGL + 1);
            desc.sampleCount = std::max(desc.sampleCount, colorRenderTarget->getRenderSamples());

            if (!mRenderPassFirstColorAttachmentFormat)
            {
                mRenderPassFirstColorAttachmentFormat = colorRenderTarget->getFormat();
            }
            else if (colorRenderTarget->getFormat())
            {
                if (mRenderPassFirstColorAttachmentFormat->actualAngleFormat().isSint() !=
                        colorRenderTarget->getFormat()->actualAngleFormat().isSint() ||
                    mRenderPassFirstColorAttachmentFormat->actualAngleFormat().isUint() !=
                        colorRenderTarget->getFormat()->actualAngleFormat().isUint())
                {
                    mRenderPassAttachmentsSameColorType = false;
                }
            }
        }
        else
        {
            colorAttachment.reset();
        }
    }

    if (mDepthRenderTarget)
    {
        mDepthRenderTarget->toRenderPassAttachmentDesc(&desc.depthAttachment);
        desc.sampleCount = std::max(desc.sampleCount, mDepthRenderTarget->getRenderSamples());
    }
    else
    {
        desc.depthAttachment.reset();
    }

    if (mStencilRenderTarget)
    {
        mStencilRenderTarget->toRenderPassAttachmentDesc(&desc.stencilAttachment);
        desc.sampleCount = std::max(desc.sampleCount, mStencilRenderTarget->getRenderSamples());
    }
    else
    {
        desc.stencilAttachment.reset();
    }

    if (desc.numColorAttachments == 0 && mDepthRenderTarget == nullptr &&
        mStencilRenderTarget == nullptr)
    {
        desc.defaultWidth  = mState.getDefaultWidth();
        desc.defaultHeight = mState.getDefaultHeight();
    }

    return angle::Result::Continue;
}

angle::Result FramebufferMtl::clearWithLoadOp(const gl::Context *context,
                                              gl::DrawBufferMask clearColorBuffers,
                                              const mtl::ClearRectParams &clearOpts)
{
    ContextMtl *contextMtl             = mtl::GetImpl(context);
    bool startedRenderPass             = contextMtl->hasStartedRenderPass(mRenderPassDesc);
    mtl::RenderCommandEncoder *encoder = nullptr;

    if (startedRenderPass)
    {
        encoder = ensureRenderPassStarted(context);
        if (encoder->hasDrawCalls())
        {
            // Render pass already has draw calls recorded, it is better to use clear with draw
            // operation.
            return clearWithDraw(context, clearColorBuffers, clearOpts);
        }
        else
        {
            // If render pass has started but there is no draw call yet. It is OK to change the
            // loadOp.
            return clearWithLoadOpRenderPassStarted(context, clearColorBuffers, clearOpts, encoder);
        }
    }
    else
    {
        return clearWithLoadOpRenderPassNotStarted(context, clearColorBuffers, clearOpts);
    }
}

angle::Result FramebufferMtl::clearWithLoadOpRenderPassNotStarted(
    const gl::Context *context,
    gl::DrawBufferMask clearColorBuffers,
    const mtl::ClearRectParams &clearOpts)
{
    mtl::RenderPassDesc tempDesc = mRenderPassDesc;

    for (uint32_t colorIndexGL = 0; colorIndexGL < tempDesc.numColorAttachments; ++colorIndexGL)
    {
        ASSERT(colorIndexGL < tempDesc.colorAttachments.size());

        mtl::RenderPassColorAttachmentDesc &colorAttachment =
            tempDesc.colorAttachments[colorIndexGL];
        const mtl::TextureRef &texture = colorAttachment.texture;

        if (clearColorBuffers.test(colorIndexGL))
        {
            colorAttachment.loadAction = MTLLoadActionClear;
            OverrideMTLClearColor(texture, clearOpts.clearColor.value(),
                                  &colorAttachment.clearColor);
        }
        else
        {
            colorAttachment.loadAction = MTLLoadActionLoad;
        }

        if (colorAttachment.hasImplicitMSTexture())
        {
            colorAttachment.storeAction = MTLStoreActionStoreAndMultisampleResolve;
        }
        else
        {
            colorAttachment.storeAction = MTLStoreActionStore;
        }
    }

    if (clearOpts.clearDepth.valid())
    {
        tempDesc.depthAttachment.loadAction = MTLLoadActionClear;
        tempDesc.depthAttachment.clearDepth = clearOpts.clearDepth.value();
    }
    else
    {
        tempDesc.depthAttachment.loadAction = MTLLoadActionLoad;
    }

    if (tempDesc.depthAttachment.hasImplicitMSTexture())
    {
        tempDesc.depthAttachment.storeAction = MTLStoreActionStoreAndMultisampleResolve;
    }
    else
    {
        tempDesc.depthAttachment.storeAction = MTLStoreActionStore;
    }

    if (clearOpts.clearStencil.valid())
    {
        tempDesc.stencilAttachment.loadAction   = MTLLoadActionClear;
        tempDesc.stencilAttachment.clearStencil = clearOpts.clearStencil.value();
    }
    else
    {
        tempDesc.stencilAttachment.loadAction = MTLLoadActionLoad;
    }

    if (tempDesc.stencilAttachment.hasImplicitMSTexture())
    {
        tempDesc.stencilAttachment.storeAction = MTLStoreActionStoreAndMultisampleResolve;
    }
    else
    {
        tempDesc.stencilAttachment.storeAction = MTLStoreActionStore;
    }

    // Start new render encoder with loadOp=Clear
    ensureRenderPassStarted(context, tempDesc);

    return angle::Result::Continue;
}

angle::Result FramebufferMtl::clearWithLoadOpRenderPassStarted(
    const gl::Context *context,
    gl::DrawBufferMask clearColorBuffers,
    const mtl::ClearRectParams &clearOpts,
    mtl::RenderCommandEncoder *encoder)
{
    ASSERT(!encoder->hasDrawCalls());

    for (uint32_t colorIndexGL = 0; colorIndexGL < mRenderPassDesc.numColorAttachments;
         ++colorIndexGL)
    {
        ASSERT(colorIndexGL < mRenderPassDesc.colorAttachments.size());

        mtl::RenderPassColorAttachmentDesc &colorAttachment =
            mRenderPassDesc.colorAttachments[colorIndexGL];
        const mtl::TextureRef &texture = colorAttachment.texture;

        if (clearColorBuffers.test(colorIndexGL))
        {
            MTLClearColor clearVal;
            OverrideMTLClearColor(texture, clearOpts.clearColor.value(), &clearVal);

            encoder->setColorLoadAction(MTLLoadActionClear, clearVal, colorIndexGL);
        }
    }

    if (clearOpts.clearDepth.valid())
    {
        encoder->setDepthLoadAction(MTLLoadActionClear, clearOpts.clearDepth.value());
    }

    if (clearOpts.clearStencil.valid())
    {
        encoder->setStencilLoadAction(MTLLoadActionClear, clearOpts.clearStencil.value());
    }

    return angle::Result::Continue;
}

angle::Result FramebufferMtl::clearWithDraw(const gl::Context *context,
                                            gl::DrawBufferMask clearColorBuffers,
                                            const mtl::ClearRectParams &clearOpts)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    DisplayMtl *display    = contextMtl->getDisplay();

    if (mRenderPassAttachmentsSameColorType)
    {
        // Start new render encoder if not already.
        mtl::RenderCommandEncoder *encoder = ensureRenderPassStarted(context, mRenderPassDesc);

        return display->getUtils().clearWithDraw(context, encoder, clearOpts);
    }

    // Not all attachments have the same color type.
    mtl::ClearRectParams overrideClearOps = clearOpts;
    overrideClearOps.enabledBuffers.reset();

    // First clear depth/stencil without color attachment
    if (clearOpts.clearDepth.valid() || clearOpts.clearStencil.valid())
    {
        mtl::RenderPassDesc dsOnlyDesc     = mRenderPassDesc;
        dsOnlyDesc.numColorAttachments     = 0;
        mtl::RenderCommandEncoder *encoder = contextMtl->getRenderPassCommandEncoder(dsOnlyDesc);

        ANGLE_TRY(display->getUtils().clearWithDraw(context, encoder, overrideClearOps));
    }

    // Clear the color attachment one by one.
    overrideClearOps.enabledBuffers.set(0);
    for (size_t drawbuffer : clearColorBuffers)
    {
        if (drawbuffer >= mRenderPassDesc.numColorAttachments)
        {
            // Iteration over drawbuffer indices always goes in ascending order
            break;
        }
        RenderTargetMtl *renderTarget = mColorRenderTargets[drawbuffer];
        if (!renderTarget || !renderTarget->getTexture())
        {
            continue;
        }
        const mtl::Format &format     = *renderTarget->getFormat();
        mtl::PixelType clearColorType = overrideClearOps.clearColor.value().getType();
        if ((clearColorType == mtl::PixelType::Int && !format.actualAngleFormat().isSint()) ||
            (clearColorType == mtl::PixelType::UInt && !format.actualAngleFormat().isUint()) ||
            (clearColorType == mtl::PixelType::Float && format.actualAngleFormat().isInt()))
        {
            continue;
        }

        overrideClearOps.clearWriteMaskArray[0] = overrideClearOps.clearWriteMaskArray[drawbuffer];

        mtl::RenderCommandEncoder *encoder =
            contextMtl->getRenderTargetCommandEncoder(*renderTarget);
        ANGLE_TRY(display->getUtils().clearWithDraw(context, encoder, overrideClearOps));
    }

    return angle::Result::Continue;
}

angle::Result FramebufferMtl::clearImpl(const gl::Context *context,
                                        gl::DrawBufferMask clearColorBuffers,
                                        mtl::ClearRectParams *pClearOpts)
{
    auto &clearOpts = *pClearOpts;

    if (!clearOpts.clearColor.valid() && !clearOpts.clearDepth.valid() &&
        !clearOpts.clearStencil.valid())
    {
        // No Op.
        return angle::Result::Continue;
    }

    ContextMtl *contextMtl = mtl::GetImpl(context);
    const gl::Rectangle renderArea(0, 0, mState.getDimensions().width,
                                   mState.getDimensions().height);

    clearOpts.colorFormat    = mRenderPassFirstColorAttachmentFormat;
    clearOpts.dstTextureSize = mState.getExtents();
    clearOpts.clearArea      = ClipRectToScissor(contextMtl->getState(), renderArea, false);
    clearOpts.flipY          = mFlipY;

    // Discard clear altogether if scissor has 0 width or height.
    if (clearOpts.clearArea.width == 0 || clearOpts.clearArea.height == 0)
    {
        return angle::Result::Continue;
    }

    clearOpts.clearWriteMaskArray = contextMtl->getWriteMaskArray();
    uint32_t stencilMask          = contextMtl->getStencilMask();
    if (!contextMtl->getDepthMask())
    {
        // Disable depth clearing, since depth write is disable
        clearOpts.clearDepth.reset();
    }

    // Only clear enabled buffers
    clearOpts.enabledBuffers = clearColorBuffers;

    bool allBuffersUnmasked = true;
    for (size_t enabledBuffer : clearColorBuffers)
    {
        if (clearOpts.clearWriteMaskArray[enabledBuffer] != MTLColorWriteMaskAll)
        {
            allBuffersUnmasked = false;
            break;
        }
    }

    if (clearOpts.clearArea == renderArea &&
        (!clearOpts.clearColor.valid() || allBuffersUnmasked) &&
        (!clearOpts.clearStencil.valid() ||
         (stencilMask & mtl::kStencilMaskAll) == mtl::kStencilMaskAll))
    {
        return clearWithLoadOp(context, clearColorBuffers, clearOpts);
    }

    return clearWithDraw(context, clearColorBuffers, clearOpts);
}

angle::Result FramebufferMtl::invalidateImpl(const gl::Context *context,
                                             size_t count,
                                             const GLenum *attachments)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    gl::DrawBufferMask invalidateColorBuffers;
    bool invalidateDepthBuffer   = false;
    bool invalidateStencilBuffer = false;

    for (size_t i = 0; i < count; ++i)
    {
        const GLenum attachment = attachments[i];

        switch (attachment)
        {
            case GL_DEPTH:
            case GL_DEPTH_ATTACHMENT:
                invalidateDepthBuffer = true;
                break;
            case GL_STENCIL:
            case GL_STENCIL_ATTACHMENT:
                invalidateStencilBuffer = true;
                break;
            case GL_DEPTH_STENCIL_ATTACHMENT:
                invalidateDepthBuffer   = true;
                invalidateStencilBuffer = true;
                break;
            default:
                ASSERT(
                    (attachment >= GL_COLOR_ATTACHMENT0 && attachment <= GL_COLOR_ATTACHMENT15) ||
                    (attachment == GL_COLOR));

                invalidateColorBuffers.set(
                    attachment == GL_COLOR ? 0u : (attachment - GL_COLOR_ATTACHMENT0));
        }
    }

    // Set the appropriate storeOp for attachments.
    // If we already start the render pass, then need to set the store action now.
    bool renderPassStarted = contextMtl->hasStartedRenderPass(mRenderPassDesc);
    mtl::RenderCommandEncoder *encoder =
        renderPassStarted ? contextMtl->getRenderCommandEncoder() : nullptr;

    for (uint32_t i = 0; i < mRenderPassDesc.numColorAttachments; ++i)
    {
        if (invalidateColorBuffers.test(i))
        {
            // Some opaque formats, like RGB8, are emulated as RGBA with alpha channel initialized
            // to 1.0. Invalidating such attachments may lead to random values in their alpha
            // channel, so skip invalidation in this case.
            RenderTargetMtl *renderTarget = mColorRenderTargets[i];
            if (renderTarget && renderTarget->getTexture())
            {
                const mtl::Format &mtlFormat        = *renderTarget->getFormat();
                const angle::Format &intendedFormat = mtlFormat.intendedAngleFormat();
                const angle::Format &actualFormat   = mtlFormat.actualAngleFormat();
                if (intendedFormat.alphaBits == 0 && actualFormat.alphaBits)
                {
                    continue;
                }
            }

            mtl::RenderPassColorAttachmentDesc &colorAttachment =
                mRenderPassDesc.colorAttachments[i];
            colorAttachment.storeAction = MTLStoreActionDontCare;
            if (renderPassStarted)
            {
                encoder->setColorStoreAction(MTLStoreActionDontCare, i);
            }
        }
    }

    if (invalidateDepthBuffer && mDepthRenderTarget)
    {
        mRenderPassDesc.depthAttachment.storeAction = MTLStoreActionDontCare;
        if (renderPassStarted)
        {
            encoder->setDepthStoreAction(MTLStoreActionDontCare);
        }
    }

    if (invalidateStencilBuffer && mStencilRenderTarget)
    {
        mRenderPassDesc.stencilAttachment.storeAction = MTLStoreActionDontCare;
        if (renderPassStarted)
        {
            encoder->setStencilStoreAction(MTLStoreActionDontCare);
        }
    }

    // Do not encode any further commands in this render pass which can affect the
    // framebuffer, or their effects will be lost.
    contextMtl->endEncoding(false);
    // Reset discard flag.
    onStartedDrawingToFrameBuffer(context);

    return angle::Result::Continue;
}

gl::Rectangle FramebufferMtl::getCorrectFlippedReadArea(const gl::Context *context,
                                                        const gl::Rectangle &glArea) const
{
    RenderTargetMtl *readRT = getColorReadRenderTarget(context);
    if (!readRT)
    {
        readRT = mDepthRenderTarget;
    }
    if (!readRT)
    {
        readRT = mStencilRenderTarget;
    }
    ASSERT(readRT);
    gl::Rectangle flippedArea = glArea;
    if (mFlipY)
    {
        flippedArea.y = readRT->getTexture()->height(readRT->getLevelIndex()) - flippedArea.y -
                        flippedArea.height;
    }

    return flippedArea;
}

namespace
{

angle::Result readPixelsCopyImpl(
    const gl::Context *context,
    const gl::Rectangle &area,
    const PackPixelsParams &packPixelsParams,
    const RenderTargetMtl *renderTarget,
    const std::function<angle::Result(const gl::Rectangle &region, const uint8_t *&src)> &getDataFn,
    uint8_t *pixels)
{
    const mtl::Format &readFormat        = *renderTarget->getFormat();
    const angle::Format &readAngleFormat = readFormat.actualAngleFormat();

    auto packPixelsRowParams = packPixelsParams;
    gl::Rectangle srcRowRegion(area.x, area.y, area.width, 1);
    int bufferRowPitch = area.width * readAngleFormat.pixelBytes;

    int rowOffset = packPixelsParams.reverseRowOrder ? -1 : 1;
    int startRow  = packPixelsParams.reverseRowOrder ? (area.y1() - 1) : area.y;

    // Copy pixels row by row
    packPixelsRowParams.area.height     = 1;
    packPixelsRowParams.reverseRowOrder = false;
    for (int r = startRow, i = 0; i < area.height;
         ++i, r += rowOffset, pixels += packPixelsRowParams.outputPitch)
    {
        srcRowRegion.y             = r;
        packPixelsRowParams.area.y = packPixelsParams.area.y + i;

        const uint8_t *src;
        ANGLE_TRY(getDataFn(srcRowRegion, src));
        PackPixels(packPixelsRowParams, readAngleFormat, bufferRowPitch, src, pixels);
    }

    return angle::Result::Continue;
}

}  // namespace

angle::Result FramebufferMtl::readPixelsImpl(const gl::Context *context,
                                             const gl::Rectangle &area,
                                             const PackPixelsParams &packPixelsParams,
                                             const RenderTargetMtl *renderTarget,
                                             uint8_t *pixels) const
{
    ContextMtl *contextMtl             = mtl::GetImpl(context);
    const angle::FeaturesMtl &features = contextMtl->getDisplay()->getFeatures();

    if (!renderTarget)
    {
        return angle::Result::Continue;
    }

    if (packPixelsParams.packBuffer)
    {
        return readPixelsToPBO(context, area, packPixelsParams, renderTarget);
    }

    mtl::TextureRef texture;
    if (mBackbuffer)
    {
        // Backbuffer might have MSAA texture as render target, needs to obtain the
        // resolved texture to be able to read pixels.
        ANGLE_TRY(mBackbuffer->ensureColorTextureReadyForReadPixels(context));
        texture = mBackbuffer->getColorTexture();
    }
    else
    {
        texture = renderTarget->getTexture();
        // For non-default framebuffer, MSAA read pixels is disallowed.
        if (!texture)
        {
            return angle::Result::Stop;
        }
        ANGLE_MTL_CHECK(contextMtl, texture->samples() == 1, GL_INVALID_OPERATION);
    }

    const mtl::Format &readFormat        = *renderTarget->getFormat();
    const angle::Format &readAngleFormat = readFormat.actualAngleFormat();

    if (features.copyIOSurfaceToNonIOSurfaceForReadOptimization.enabled &&
        texture->hasIOSurface() && texture->mipmapLevels() == 1 &&
        texture->textureType() == MTLTextureType2D)
    {
        // Reading a texture may be slow if it's an IOSurface because metal has to lock/unlock the
        // surface, whereas copying the texture to non IOSurface texture and then reading from that
        // may be fast depending on the GPU/driver.
        ANGLE_TRY(contextMtl->copy2DTextureSlice0Level0ToWorkTexture(texture));
        texture = contextMtl->getWorkTexture();
    }

    if (features.copyTextureToBufferForReadOptimization.enabled)
    {
        ANGLE_TRY(contextMtl->copyTextureSliceLevelToWorkBuffer(
            context, texture, renderTarget->getLevelIndex(), renderTarget->getLayerIndex()));

        int bufferRowPitch =
            texture->width(renderTarget->getLevelIndex()) * readAngleFormat.pixelBytes;

        const mtl::BufferRef &buffer = contextMtl->getWorkBuffer();
        buffer->syncContent(contextMtl, contextMtl->getBlitCommandEncoder());
        const uint8_t *bufferData = buffer->mapReadOnly(contextMtl);

        angle::Result result = readPixelsCopyImpl(
            context, area, packPixelsParams, renderTarget,
            [&](const gl::Rectangle &region, const uint8_t *&src) {
                src =
                    bufferData + region.y * bufferRowPitch + region.x * readAngleFormat.pixelBytes;
                return angle::Result::Continue;
            },
            pixels);

        buffer->unmap(contextMtl);

        return result;
    }

    if (texture->isBeingUsedByGPU(contextMtl))
    {
        contextMtl->flushCommandBuffer(mtl::WaitUntilFinished);
    }

    angle::MemoryBuffer readPixelRowBuffer;
    int bufferRowPitch = area.width * readAngleFormat.pixelBytes;
    ANGLE_CHECK_GL_ALLOC(contextMtl, readPixelRowBuffer.resize(bufferRowPitch));
    return readPixelsCopyImpl(
        context, area, packPixelsParams, renderTarget,
        [&](const gl::Rectangle &region, const uint8_t *&src) {
            // Read the pixels data to the row buffer
            ANGLE_TRY(mtl::ReadTexturePerSliceBytes(
                context, texture, bufferRowPitch, region, renderTarget->getLevelIndex(),
                renderTarget->getLayerIndex(), readPixelRowBuffer.data()));
            src = readPixelRowBuffer.data();
            return angle::Result::Continue;
        },
        pixels);
}

angle::Result FramebufferMtl::readPixelsToPBO(const gl::Context *context,
                                              const gl::Rectangle &area,
                                              const PackPixelsParams &packPixelsParams,
                                              const RenderTargetMtl *renderTarget) const
{
    ASSERT(packPixelsParams.packBuffer);
    ASSERT(renderTarget);

    ContextMtl *contextMtl = mtl::GetImpl(context);

    ANGLE_MTL_CHECK(contextMtl, packPixelsParams.offset <= std::numeric_limits<uint32_t>::max(),
                    GL_INVALID_OPERATION);
    uint32_t offset = static_cast<uint32_t>(packPixelsParams.offset);

    BufferMtl *packBufferMtl = mtl::GetImpl(packPixelsParams.packBuffer);
    mtl::BufferRef dstBuffer = packBufferMtl->getCurrentBuffer();

    return readPixelsToBuffer(context, area, renderTarget, packPixelsParams.reverseRowOrder,
                              *packPixelsParams.destFormat, offset, packPixelsParams.outputPitch,
                              &dstBuffer);
}

angle::Result FramebufferMtl::readPixelsToBuffer(const gl::Context *context,
                                                 const gl::Rectangle &area,
                                                 const RenderTargetMtl *renderTarget,
                                                 bool reverseRowOrder,
                                                 const angle::Format &dstAngleFormat,
                                                 uint32_t dstBufferOffset,
                                                 uint32_t dstBufferRowPitch,
                                                 const mtl::BufferRef *pDstBuffer) const
{
    ASSERT(renderTarget);

    ContextMtl *contextMtl = mtl::GetImpl(context);

    const mtl::Format &readFormat        = *renderTarget->getFormat();
    const angle::Format &readAngleFormat = readFormat.actualAngleFormat();

    mtl::TextureRef texture = renderTarget->getTexture();

    const mtl::BufferRef &dstBuffer = *pDstBuffer;

    if (dstAngleFormat.id != readAngleFormat.id || texture->samples() > 1 ||
        (dstBufferOffset % dstAngleFormat.pixelBytes) ||
        (dstBufferOffset % mtl::kTextureToBufferBlittingAlignment))
    {
        const angle::Format *actualDstAngleFormat;

        // SRGB is special case: We need to write sRGB values to buffer, not linear values.
        switch (readAngleFormat.id)
        {
            case angle::FormatID::B8G8R8A8_UNORM_SRGB:
            case angle::FormatID::R8G8B8_UNORM_SRGB:
            case angle::FormatID::R8G8B8A8_UNORM_SRGB:
                if (dstAngleFormat.id != readAngleFormat.id)
                {
                    switch (dstAngleFormat.id)
                    {
                        case angle::FormatID::B8G8R8A8_UNORM:
                            actualDstAngleFormat =
                                &angle::Format::Get(angle::FormatID::B8G8R8A8_UNORM_SRGB);
                            break;
                        case angle::FormatID::R8G8B8A8_UNORM:
                            actualDstAngleFormat =
                                &angle::Format::Get(angle::FormatID::R8G8B8A8_UNORM_SRGB);
                            break;
                        default:
                            // Unsupported format.
                            ANGLE_MTL_CHECK(contextMtl, false, GL_INVALID_ENUM);
                    }
                    break;
                }
                OS_FALLTHROUGH;
            default:
                actualDstAngleFormat = &dstAngleFormat;
        }

        // Use compute shader
        mtl::CopyPixelsToBufferParams params;
        params.buffer            = dstBuffer;
        params.bufferStartOffset = dstBufferOffset;
        params.bufferRowPitch    = dstBufferRowPitch;

        params.texture                = texture;
        params.textureArea            = area;
        params.textureLevel           = renderTarget->getLevelIndex();
        params.textureSliceOrDeph     = renderTarget->getLayerIndex();
        params.reverseTextureRowOrder = reverseRowOrder;

        ANGLE_TRY(contextMtl->getDisplay()->getUtils().packPixelsFromTextureToBuffer(
            contextMtl, *actualDstAngleFormat, params));
    }
    else
    {
        // Use blit command encoder
        if (!reverseRowOrder)
        {
            ANGLE_TRY(mtl::ReadTexturePerSliceBytesToBuffer(
                context, texture, dstBufferRowPitch, area, renderTarget->getLevelIndex(),
                renderTarget->getLayerIndex(), dstBufferOffset, dstBuffer));
        }
        else
        {
            gl::Rectangle srcRowRegion(area.x, area.y, area.width, 1);

            int startRow = area.y1() - 1;

            uint32_t bufferRowOffset = dstBufferOffset;
            // Copy pixels row by row
            for (int r = startRow, copiedRows = 0; copiedRows < area.height;
                 ++copiedRows, --r, bufferRowOffset += dstBufferRowPitch)
            {
                srcRowRegion.y = r;

                // Read the pixels data to the buffer's row
                ANGLE_TRY(mtl::ReadTexturePerSliceBytesToBuffer(
                    context, texture, dstBufferRowPitch, srcRowRegion,
                    renderTarget->getLevelIndex(), renderTarget->getLayerIndex(), bufferRowOffset,
                    dstBuffer));
            }
        }
    }

    return angle::Result::Continue;
}

}  // namespace rx
