
//
// 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.
//
// TextureMtl.mm:
//    Implements the class methods for TextureMtl.
//

#include "libANGLE/renderer/metal/TextureMtl.h"

#include "common/Color.h"
#include "common/MemoryBuffer.h"
#include "common/debug.h"
#include "common/mathutil.h"
#include "libANGLE/Surface.h"
#include "libANGLE/renderer/metal/ContextMtl.h"
#include "libANGLE/renderer/metal/DisplayMtl.h"
#include "libANGLE/renderer/metal/FrameBufferMtl.h"
#include "libANGLE/renderer/metal/SurfaceMtl.h"
#include "libANGLE/renderer/metal/mtl_common.h"
#include "libANGLE/renderer/metal/mtl_format_utils.h"
#include "libANGLE/renderer/metal/mtl_utils.h"
#include "libANGLE/renderer/renderer_utils.h"

namespace rx
{

namespace
{

gl::ImageIndex GetImageBaseLevelIndex(const mtl::TextureRef &image)
{
    gl::ImageIndex imageBaseIndex;
    switch (image->textureType())
    {
        case MTLTextureType2D:
            imageBaseIndex = gl::ImageIndex::Make2D(0);
            break;
        default:
            UNREACHABLE();
            break;
    }

    return imageBaseIndex;
}

GLuint GetImageLayerIndex(const gl::ImageIndex &index)
{
    switch (index.getType())
    {
        case gl::TextureType::_2D:
            return 0;
        case gl::TextureType::CubeMap:
            return index.cubeMapFaceIndex();
        default:
            UNREACHABLE();
    }

    return 0;
}

// Given texture type, get texture type of one image at a slice and a level.
// For example, for texture 2d, one image is also texture 2d.
// for texture cube, one image is texture 2d.
// For texture 2d array, one image is texture 2d.
gl::TextureType GetTextureImageType(gl::TextureType texType)
{
    switch (texType)
    {
        case gl::TextureType::_2D:
        case gl::TextureType::CubeMap:
            return gl::TextureType::_2D;
        default:
            UNREACHABLE();
            return gl::TextureType::InvalidEnum;
    }
}

angle::Result CopyTextureContentsToStagingBuffer(ContextMtl *contextMtl,
                                                 const angle::Format &textureAngleFormat,
                                                 const angle::Format &stagingAngleFormat,
                                                 const MTLSize &regionSize,
                                                 const uint8_t *data,
                                                 size_t bytesPerRow,
                                                 size_t *bufferRowPitchOut,
                                                 size_t *buffer2DImageSizeOut,
                                                 mtl::BufferRef *bufferOut)
{
    // NOTE(hqle): 3D textures not supported yet.
    ASSERT(regionSize.depth == 1);

    size_t stagingBufferRowPitch    = regionSize.width * stagingAngleFormat.pixelBytes;
    size_t stagingBuffer2DImageSize = stagingBufferRowPitch * regionSize.height;
    size_t stagingBufferSize        = stagingBuffer2DImageSize * regionSize.depth;
    mtl::BufferRef stagingBuffer;
    ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, stagingBufferSize, nullptr, &stagingBuffer));

    uint8_t *pdst = stagingBuffer->map(contextMtl);

    if (textureAngleFormat.id == stagingAngleFormat.id)
    {
        for (NSUInteger r = 0; r < regionSize.height; ++r)
        {
            const uint8_t *pCopySrc = data + r * bytesPerRow;
            uint8_t *pCopyDst       = pdst + r * stagingBufferRowPitch;
            memcpy(pCopyDst, pCopySrc, stagingBufferRowPitch);
        }
    }
    else
    {
        // This is only for depth & stencil case.
        ASSERT(textureAngleFormat.depthBits || textureAngleFormat.stencilBits);
        ASSERT(textureAngleFormat.pixelReadFunction && stagingAngleFormat.pixelWriteFunction);

        // cache to store read result of source pixel
        angle::DepthStencil depthStencilData;
        auto sourcePixelReadData = reinterpret_cast<uint8_t *>(&depthStencilData);
        ASSERT(textureAngleFormat.pixelBytes <= sizeof(depthStencilData));

        for (NSUInteger r = 0; r < regionSize.height; ++r)
        {
            for (NSUInteger c = 0; c < regionSize.width; ++c)
            {
                const uint8_t *sourcePixelData =
                    data + r * bytesPerRow + c * textureAngleFormat.pixelBytes;

                uint8_t *destPixelData =
                    pdst + r * stagingBufferRowPitch + c * stagingAngleFormat.pixelBytes;

                textureAngleFormat.pixelReadFunction(sourcePixelData, sourcePixelReadData);
                stagingAngleFormat.pixelWriteFunction(sourcePixelReadData, destPixelData);
            }
        }
    }

    stagingBuffer->unmap(contextMtl);

    *bufferOut            = stagingBuffer;
    *bufferRowPitchOut    = stagingBufferRowPitch;
    *buffer2DImageSizeOut = stagingBuffer2DImageSize;

    return angle::Result::Continue;
}

angle::Result UploadTextureContentsWithStagingBuffer(ContextMtl *contextMtl,
                                                     const mtl::TextureRef &texture,
                                                     const angle::Format &textureAngleFormat,
                                                     MTLRegion region,
                                                     uint32_t mipmapLevel,
                                                     uint32_t slice,
                                                     const uint8_t *data,
                                                     size_t bytesPerRow)
{
    ASSERT(texture && texture->valid());

    ASSERT(!texture->isCPUAccessible());

    ASSERT(!textureAngleFormat.depthBits || !textureAngleFormat.stencilBits);

    // Compressed texture is not supporte atm
    ASSERT(!textureAngleFormat.isBlock);

    // Copy data to staging buffer
    size_t stagingBufferRowPitch;
    size_t stagingBuffer2DImageSize;
    mtl::BufferRef stagingBuffer;
    ANGLE_TRY(CopyTextureContentsToStagingBuffer(
        contextMtl, textureAngleFormat, textureAngleFormat, region.size, data, bytesPerRow,
        &stagingBufferRowPitch, &stagingBuffer2DImageSize, &stagingBuffer));

    // Copy staging buffer to texture.
    mtl::BlitCommandEncoder *encoder = contextMtl->getBlitCommandEncoder();
    encoder->copyBufferToTexture(stagingBuffer, 0, stagingBufferRowPitch, stagingBuffer2DImageSize,
                                 region.size, texture, slice, mipmapLevel, region.origin,
                                 MTLBlitOptionNone);

    return angle::Result::Continue;
}

// Packed depth stencil upload using staging buffer
angle::Result UploadPackedDepthStencilTextureContentsWithStagingBuffer(
    ContextMtl *contextMtl,
    const mtl::TextureRef &texture,
    const angle::Format &textureAngleFormat,
    MTLRegion region,
    uint32_t mipmapLevel,
    uint32_t slice,
    const uint8_t *data,
    size_t bytesPerRow)
{
    ASSERT(texture && texture->valid());

    ASSERT(!texture->isCPUAccessible());

    ASSERT(textureAngleFormat.depthBits && textureAngleFormat.stencilBits);

    // We have to split the depth & stencil data into 2 buffers.
    angle::FormatID stagingDepthBufferFormatId;
    angle::FormatID stagingStencilBufferFormatId;

    switch (textureAngleFormat.id)
    {
        case angle::FormatID::D24_UNORM_S8_UINT:
            stagingDepthBufferFormatId   = angle::FormatID::D24_UNORM_X8_UINT;
            stagingStencilBufferFormatId = angle::FormatID::S8_UINT;
            break;
        case angle::FormatID::D32_FLOAT_S8X24_UINT:
            stagingDepthBufferFormatId   = angle::FormatID::D32_FLOAT;
            stagingStencilBufferFormatId = angle::FormatID::S8_UINT;
            break;
        default:
            ANGLE_MTL_UNREACHABLE(contextMtl);
    }

    const angle::Format &angleStagingDepthFormat = angle::Format::Get(stagingDepthBufferFormatId);
    const angle::Format &angleStagingStencilFormat =
        angle::Format::Get(stagingStencilBufferFormatId);

    size_t stagingDepthBufferRowPitch, stagingStencilBufferRowPitch;
    size_t stagingDepthBuffer2DImageSize, stagingStencilBuffer2DImageSize;
    mtl::BufferRef stagingDepthbuffer, stagingStencilBuffer;

    ANGLE_TRY(CopyTextureContentsToStagingBuffer(
        contextMtl, textureAngleFormat, angleStagingDepthFormat, region.size, data, bytesPerRow,
        &stagingDepthBufferRowPitch, &stagingDepthBuffer2DImageSize, &stagingDepthbuffer));

    ANGLE_TRY(CopyTextureContentsToStagingBuffer(
        contextMtl, textureAngleFormat, angleStagingStencilFormat, region.size, data, bytesPerRow,
        &stagingStencilBufferRowPitch, &stagingStencilBuffer2DImageSize, &stagingStencilBuffer));

    mtl::BlitCommandEncoder *encoder = contextMtl->getBlitCommandEncoder();

    encoder->copyBufferToTexture(stagingDepthbuffer, 0, stagingDepthBufferRowPitch,
                                 stagingDepthBuffer2DImageSize, region.size, texture, slice,
                                 mipmapLevel, region.origin, MTLBlitOptionDepthFromDepthStencil);
    encoder->copyBufferToTexture(stagingStencilBuffer, 0, stagingStencilBufferRowPitch,
                                 stagingStencilBuffer2DImageSize, region.size, texture, slice,
                                 mipmapLevel, region.origin, MTLBlitOptionStencilFromDepthStencil);

    return angle::Result::Continue;
}

angle::Result UploadTextureContents(const gl::Context *context,
                                    const mtl::TextureRef &texture,
                                    const angle::Format &textureAngleFormat,
                                    const MTLRegion &region,
                                    uint32_t mipmapLevel,
                                    uint32_t slice,
                                    const uint8_t *data,
                                    size_t bytesPerRow)
{
    ASSERT(texture && texture->valid());
    ContextMtl *contextMtl = mtl::GetImpl(context);

    if (texture->isCPUAccessible())
    {
        // If texture is CPU accessible, just call replaceRegion() directly.
        texture->replaceRegion(contextMtl, region, mipmapLevel, slice, data, bytesPerRow);

        return angle::Result::Continue;
    }

    // Texture is not CPU accessible, we need to use staging buffer
    if (textureAngleFormat.depthBits && textureAngleFormat.stencilBits)
    {
        ANGLE_TRY(UploadPackedDepthStencilTextureContentsWithStagingBuffer(
            contextMtl, texture, textureAngleFormat, region, mipmapLevel, slice, data,
            bytesPerRow));
    }
    else
    {
        ANGLE_TRY(UploadTextureContentsWithStagingBuffer(contextMtl, texture, textureAngleFormat,
                                                         region, mipmapLevel, slice, data,
                                                         bytesPerRow));
    }

    return angle::Result::Continue;
}

}  // namespace

// TextureMtl implementation
TextureMtl::TextureMtl(const gl::TextureState &state) : TextureImpl(state) {}

TextureMtl::~TextureMtl() = default;

void TextureMtl::onDestroy(const gl::Context *context)
{
    releaseTexture(true);
}

void TextureMtl::releaseTexture(bool releaseImages)
{
    mFormat = mtl::Format();

    mNativeTexture     = nullptr;
    mMetalSamplerState = nil;

    for (RenderTargetMtl &rt : mLayeredRenderTargets)
    {
        rt.reset();
    }

    if (releaseImages)
    {
        mTexImages.clear();
    }

    mLayeredTextureViews.clear();

    mIsPow2 = false;
}

angle::Result TextureMtl::ensureTextureCreated(const gl::Context *context)
{
    if (mNativeTexture)
    {
        return angle::Result::Continue;
    }

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

    // Create actual texture object:
    int layers                = 0;
    const GLuint mips         = mState.getMipmapMaxLevel() + 1;
    const gl::ImageDesc &desc = mState.getBaseLevelDesc();

    mIsPow2 = gl::isPow2(desc.size.width) && gl::isPow2(desc.size.height);

    switch (mState.getType())
    {
        case gl::TextureType::_2D:
            layers = 1;
            ANGLE_TRY(mtl::Texture::Make2DTexture(contextMtl, mFormat, desc.size.width,
                                                  desc.size.height, mips, false, true,
                                                  &mNativeTexture));
            mLayeredRenderTargets.resize(1);
            mLayeredRenderTargets[0].set(mNativeTexture, 0, 0, mFormat);
            mLayeredTextureViews.resize(1);
            mLayeredTextureViews[0] = mNativeTexture;
            break;
        case gl::TextureType::CubeMap:
            layers = 6;
            ANGLE_TRY(mtl::Texture::MakeCubeTexture(contextMtl, mFormat, desc.size.width, mips,
                                                    false, true, &mNativeTexture));
            mLayeredRenderTargets.resize(gl::kCubeFaceCount);
            mLayeredTextureViews.resize(gl::kCubeFaceCount);
            for (uint32_t f = 0; f < gl::kCubeFaceCount; ++f)
            {
                mLayeredTextureViews[f] = mNativeTexture->createCubeFaceView(f);
                mLayeredRenderTargets[f].set(mLayeredTextureViews[f], 0, 0, mFormat);
            }
            break;
        default:
            UNREACHABLE();
    }

    ANGLE_TRY(checkForEmulatedChannels(context, mFormat, mNativeTexture));

    // Transfer data from images to actual texture object
    mtl::BlitCommandEncoder *encoder = nullptr;
    for (int layer = 0; layer < layers; ++layer)
    {
        for (GLuint mip = 0; mip < mips; ++mip)
        {
            mtl::TextureRef &imageToTransfer = mTexImages[layer][mip];

            // Only transfer if this mip & slice image has been defined and in correct size &
            // format.
            gl::Extents actualMipSize = mNativeTexture->size(mip);
            if (imageToTransfer && imageToTransfer->size() == actualMipSize &&
                imageToTransfer->pixelFormat() == mNativeTexture->pixelFormat())
            {
                MTLSize mtlSize =
                    MTLSizeMake(actualMipSize.width, actualMipSize.height, actualMipSize.depth);
                MTLOrigin mtlOrigin = MTLOriginMake(0, 0, 0);

                if (!encoder)
                {
                    encoder = contextMtl->getBlitCommandEncoder();
                }
                encoder->copyTexture(imageToTransfer, 0, 0, mtlOrigin, mtlSize, mNativeTexture,
                                     layer, mip, mtlOrigin);
            }

            imageToTransfer = nullptr;
            // Make this image the actual texture object's view at this mip and slice.
            // So that in future, glTexSubImage* will update the actual texture
            // directly.
            mTexImages[layer][mip] = mNativeTexture->createSliceMipView(layer, mip);
        }
    }

    // Create sampler state
    ANGLE_TRY(ensureSamplerStateCreated(context));

    return angle::Result::Continue;
}

angle::Result TextureMtl::ensureSamplerStateCreated(const gl::Context *context)
{
    if (mMetalSamplerState)
    {
        return angle::Result::Continue;
    }

    ContextMtl *contextMtl = mtl::GetImpl(context);
    DisplayMtl *displayMtl = contextMtl->getDisplay();

    mtl::SamplerDesc samplerDesc(mState.getSamplerState());

    if (mFormat.actualAngleFormat().depthBits &&
        !displayMtl->getFeatures().hasDepthTextureFiltering.enabled)
    {
        // On devices not supporting filtering for depth textures, we need to convert to nearest
        // here.
        samplerDesc.minFilter = MTLSamplerMinMagFilterNearest;
        samplerDesc.magFilter = MTLSamplerMinMagFilterNearest;
        if (samplerDesc.mipFilter != MTLSamplerMipFilterNotMipmapped)
        {
            samplerDesc.mipFilter = MTLSamplerMipFilterNearest;
        }

        samplerDesc.maxAnisotropy = 1;
    }

    mMetalSamplerState =
        displayMtl->getStateCache().getSamplerState(displayMtl->getMetalDevice(), samplerDesc);

    return angle::Result::Continue;
}

angle::Result TextureMtl::ensureImageCreated(const gl::Context *context,
                                             const gl::ImageIndex &index)
{
    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];
    if (!image)
    {
        // Image at this level hasn't been defined yet. We need to define it:
        const gl::ImageDesc &desc = mState.getImageDesc(index);
        ANGLE_TRY(redefineImage(context, index, mFormat, desc.size));
    }
    return angle::Result::Continue;
}

angle::Result TextureMtl::setImage(const gl::Context *context,
                                   const gl::ImageIndex &index,
                                   GLenum internalFormat,
                                   const gl::Extents &size,
                                   GLenum format,
                                   GLenum type,
                                   const gl::PixelUnpackState &unpack,
                                   gl::Buffer *unpackBuffer,
                                   const uint8_t *pixels)
{
    const gl::InternalFormat &formatInfo = gl::GetInternalFormatInfo(internalFormat, type);

    return setImageImpl(context, index, formatInfo, size, type, unpack, pixels);
}

angle::Result TextureMtl::setSubImage(const gl::Context *context,
                                      const gl::ImageIndex &index,
                                      const gl::Box &area,
                                      GLenum format,
                                      GLenum type,
                                      const gl::PixelUnpackState &unpack,
                                      gl::Buffer *unpackBuffer,
                                      const uint8_t *pixels)
{
    const gl::InternalFormat &formatInfo = gl::GetInternalFormatInfo(format, type);

    return setSubImageImpl(context, index, area, formatInfo, type, unpack, pixels);
}

angle::Result TextureMtl::setCompressedImage(const gl::Context *context,
                                             const gl::ImageIndex &index,
                                             GLenum internalFormat,
                                             const gl::Extents &size,
                                             const gl::PixelUnpackState &unpack,
                                             size_t imageSize,
                                             const uint8_t *pixels)
{
    const gl::InternalFormat &formatInfo = gl::GetSizedInternalFormatInfo(internalFormat);

    return setImageImpl(context, index, formatInfo, size, GL_UNSIGNED_BYTE, unpack, pixels);
}

angle::Result TextureMtl::setCompressedSubImage(const gl::Context *context,
                                                const gl::ImageIndex &index,
                                                const gl::Box &area,
                                                GLenum format,
                                                const gl::PixelUnpackState &unpack,
                                                size_t imageSize,
                                                const uint8_t *pixels)
{

    const gl::InternalFormat &formatInfo = gl::GetInternalFormatInfo(format, GL_UNSIGNED_BYTE);

    return setSubImageImpl(context, index, area, formatInfo, GL_UNSIGNED_BYTE, unpack, pixels);
}

angle::Result TextureMtl::copyImage(const gl::Context *context,
                                    const gl::ImageIndex &index,
                                    const gl::Rectangle &sourceArea,
                                    GLenum internalFormat,
                                    gl::Framebuffer *source)
{
    gl::Extents newImageSize(sourceArea.width, sourceArea.height, 1);
    const gl::InternalFormat &internalFormatInfo =
        gl::GetInternalFormatInfo(internalFormat, GL_UNSIGNED_BYTE);

    ContextMtl *contextMtl = mtl::GetImpl(context);
    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(internalFormatInfo.sizedInternalFormat);
    const mtl::Format &mtlFormat = contextMtl->getPixelFormat(angleFormatId);

    ANGLE_TRY(redefineImage(context, index, mtlFormat, newImageSize));

    if (context->isWebGL())
    {
        ANGLE_TRY(initializeContents(context, index));
    }

    return copySubImageImpl(context, index, gl::Offset(0, 0, 0), sourceArea, internalFormatInfo,
                            source);
}

angle::Result TextureMtl::copySubImage(const gl::Context *context,
                                       const gl::ImageIndex &index,
                                       const gl::Offset &destOffset,
                                       const gl::Rectangle &sourceArea,
                                       gl::Framebuffer *source)
{
    const gl::InternalFormat &currentFormat = *mState.getImageDesc(index).format.info;
    return copySubImageImpl(context, index, destOffset, sourceArea, currentFormat, source);
}

angle::Result TextureMtl::copyTexture(const gl::Context *context,
                                      const gl::ImageIndex &index,
                                      GLenum internalFormat,
                                      GLenum type,
                                      size_t sourceLevel,
                                      bool unpackFlipY,
                                      bool unpackPremultiplyAlpha,
                                      bool unpackUnmultiplyAlpha,
                                      const gl::Texture *source)
{
    const gl::ImageDesc &sourceImageDesc = source->getTextureState().getImageDesc(
        NonCubeTextureTypeToTarget(source->getType()), sourceLevel);
    const gl::InternalFormat &internalFormatInfo = gl::GetInternalFormatInfo(internalFormat, type);

    // Only 2D textures are supported.
    ASSERT(sourceImageDesc.size.depth == 1);

    ContextMtl *contextMtl = mtl::GetImpl(context);
    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(internalFormatInfo.sizedInternalFormat);
    const mtl::Format &mtlFormat = contextMtl->getPixelFormat(angleFormatId);

    ANGLE_TRY(redefineImage(context, index, mtlFormat, sourceImageDesc.size));

    return copySubTextureImpl(
        context, index, gl::Offset(0, 0, 0), internalFormatInfo, sourceLevel,
        gl::Box(0, 0, 0, sourceImageDesc.size.width, sourceImageDesc.size.height, 1), unpackFlipY,
        unpackPremultiplyAlpha, unpackUnmultiplyAlpha, source);
}

angle::Result TextureMtl::copySubTexture(const gl::Context *context,
                                         const gl::ImageIndex &index,
                                         const gl::Offset &destOffset,
                                         size_t sourceLevel,
                                         const gl::Box &sourceBox,
                                         bool unpackFlipY,
                                         bool unpackPremultiplyAlpha,
                                         bool unpackUnmultiplyAlpha,
                                         const gl::Texture *source)
{
    const gl::InternalFormat &currentFormat = *mState.getImageDesc(index).format.info;

    return copySubTextureImpl(context, index, destOffset, currentFormat, sourceLevel, sourceBox,
                              unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha, source);
}

angle::Result TextureMtl::copyCompressedTexture(const gl::Context *context,
                                                const gl::Texture *source)
{
    UNIMPLEMENTED();

    return angle::Result::Stop;
}

angle::Result TextureMtl::setStorage(const gl::Context *context,
                                     gl::TextureType type,
                                     size_t mipmaps,
                                     GLenum internalFormat,
                                     const gl::Extents &size)
{
    ContextMtl *contextMtl               = mtl::GetImpl(context);
    const gl::InternalFormat &formatInfo = gl::GetSizedInternalFormatInfo(internalFormat);
    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(formatInfo.sizedInternalFormat);
    const mtl::Format &mtlFormat = contextMtl->getPixelFormat(angleFormatId);

    return setStorageImpl(context, type, mipmaps, mtlFormat, size);
}

angle::Result TextureMtl::setStorageExternalMemory(const gl::Context *context,
                                                   gl::TextureType type,
                                                   size_t levels,
                                                   GLenum internalFormat,
                                                   const gl::Extents &size,
                                                   gl::MemoryObject *memoryObject,
                                                   GLuint64 offset,
                                                   GLbitfield createFlags,
                                                   GLbitfield usageFlags)
{
    UNIMPLEMENTED();

    return angle::Result::Stop;
}

angle::Result TextureMtl::setStorageMultisample(const gl::Context *context,
                                                gl::TextureType type,
                                                GLsizei samples,
                                                GLint internalformat,
                                                const gl::Extents &size,
                                                bool fixedSampleLocations)
{
    UNIMPLEMENTED();

    return angle::Result::Stop;
}

angle::Result TextureMtl::setEGLImageTarget(const gl::Context *context,
                                            gl::TextureType type,
                                            egl::Image *image)
{
    UNIMPLEMENTED();

    return angle::Result::Stop;
}

angle::Result TextureMtl::setImageExternal(const gl::Context *context,
                                           gl::TextureType type,
                                           egl::Stream *stream,
                                           const egl::Stream::GLTextureDescription &desc)
{
    UNIMPLEMENTED();
    return angle::Result::Stop;
}

angle::Result TextureMtl::generateMipmap(const gl::Context *context)
{
    ANGLE_TRY(ensureTextureCreated(context));

    ContextMtl *contextMtl = mtl::GetImpl(context);
    if (!mNativeTexture)
    {
        return angle::Result::Continue;
    }

    const gl::TextureCapsMap &textureCapsMap = contextMtl->getNativeTextureCaps();
    const gl::TextureCaps &textureCaps       = textureCapsMap.get(mFormat.intendedFormatId);

    if (textureCaps.filterable && textureCaps.renderbuffer)
    {
        mtl::BlitCommandEncoder *blitEncoder = contextMtl->getBlitCommandEncoder();
        blitEncoder->generateMipmapsForTexture(mNativeTexture);
    }
    else
    {
        ANGLE_TRY(generateMipmapCPU(context));
    }

    return angle::Result::Continue;
}

angle::Result TextureMtl::generateMipmapCPU(const gl::Context *context)
{
    ASSERT(mNativeTexture && mNativeTexture->valid());
    ASSERT(mLayeredTextureViews.size() <= std::numeric_limits<uint32_t>::max());
    uint32_t layers = static_cast<uint32_t>(mLayeredTextureViews.size());

    ContextMtl *contextMtl           = mtl::GetImpl(context);
    const angle::Format &angleFormat = mFormat.actualAngleFormat();
    // This format must have mip generation function.
    ANGLE_MTL_TRY(contextMtl, angleFormat.mipGenerationFunction);

    // NOTE(hqle): Support base level of ES 3.0.
    for (uint32_t layer = 0; layer < layers; ++layer)
    {
        int maxMipLevel = static_cast<int>(mNativeTexture->mipmapLevels()) - 1;
        int firstLevel  = 0;

        uint32_t prevLevelWidth  = mNativeTexture->width();
        uint32_t prevLevelHeight = mNativeTexture->height();
        size_t prevLevelRowPitch = angleFormat.pixelBytes * prevLevelWidth;
        std::unique_ptr<uint8_t[]> prevLevelData(new (std::nothrow)
                                                     uint8_t[prevLevelRowPitch * prevLevelHeight]);
        ANGLE_CHECK_GL_ALLOC(contextMtl, prevLevelData);
        std::unique_ptr<uint8_t[]> dstLevelData;

        // Download base level data
        mLayeredTextureViews[layer]->getBytes(
            contextMtl, prevLevelRowPitch, MTLRegionMake2D(0, 0, prevLevelWidth, prevLevelHeight),
            firstLevel, prevLevelData.get());

        for (int mip = firstLevel + 1; mip <= maxMipLevel; ++mip)
        {
            uint32_t dstWidth  = mNativeTexture->width(mip);
            uint32_t dstHeight = mNativeTexture->height(mip);

            size_t dstRowPitch = angleFormat.pixelBytes * dstWidth;
            size_t dstDataSize = dstRowPitch * dstHeight;
            if (!dstLevelData)
            {
                // Allocate once and reuse the buffer
                dstLevelData.reset(new (std::nothrow) uint8_t[dstDataSize]);
                ANGLE_CHECK_GL_ALLOC(contextMtl, dstLevelData);
            }

            // Generate mip level
            angleFormat.mipGenerationFunction(prevLevelWidth, prevLevelHeight, 1,
                                              prevLevelData.get(), prevLevelRowPitch, 0,
                                              dstLevelData.get(), dstRowPitch, 0);

            // Upload to texture
            ANGLE_TRY(UploadTextureContents(context, mNativeTexture, angleFormat,
                                            MTLRegionMake2D(0, 0, dstWidth, dstHeight), mip, layer,
                                            dstLevelData.get(), dstRowPitch));

            prevLevelWidth    = dstWidth;
            prevLevelHeight   = dstHeight;
            prevLevelRowPitch = dstRowPitch;
            std::swap(prevLevelData, dstLevelData);
        }  // for mip level

    }  // For layers

    return angle::Result::Continue;
}

angle::Result TextureMtl::setBaseLevel(const gl::Context *context, GLuint baseLevel)
{
    // NOTE(hqle): ES 3.0
    UNIMPLEMENTED();

    return angle::Result::Stop;
}

angle::Result TextureMtl::bindTexImage(const gl::Context *context, egl::Surface *surface)
{
    releaseTexture(true);

    auto pBuffer     = GetImplAs<OffscreenSurfaceMtl>(surface);
    mNativeTexture   = pBuffer->getColorTexture();
    mFormat          = pBuffer->getColorFormat();
    gl::Extents size = mNativeTexture->size();
    mIsPow2          = gl::isPow2(size.width) && gl::isPow2(size.height) && gl::isPow2(size.depth);
    ANGLE_TRY(ensureSamplerStateCreated(context));

    ASSERT(mState.getType() == gl::TextureType::_2D);
    mLayeredRenderTargets.resize(1);
    mLayeredRenderTargets[0].set(mNativeTexture, 0, 0, mFormat);
    mLayeredTextureViews.resize(1);
    mLayeredTextureViews[0] = mNativeTexture;

    // Create image view for glTexSubImage* and glCopySubTexture*
    ASSERT(mNativeTexture->mipmapLevels() == 1);
    mTexImages[0][0] = mNativeTexture;

    // Tell context to rebind textures
    ContextMtl *contextMtl = mtl::GetImpl(context);
    contextMtl->invalidateCurrentTextures();

    return angle::Result::Continue;
}

angle::Result TextureMtl::releaseTexImage(const gl::Context *context)
{
    releaseTexture(true);
    return angle::Result::Continue;
}

angle::Result TextureMtl::getAttachmentRenderTarget(const gl::Context *context,
                                                    GLenum binding,
                                                    const gl::ImageIndex &imageIndex,
                                                    GLsizei samples,
                                                    FramebufferAttachmentRenderTarget **rtOut)
{
    ANGLE_TRY(ensureTextureCreated(context));
    // NOTE(hqle): Support MSAA.
    // Non-zero mip level attachments are an ES 3.0 feature.
    ASSERT(imageIndex.getLevelIndex() == 0);

    ContextMtl *contextMtl = mtl::GetImpl(context);
    ANGLE_MTL_TRY(contextMtl, mNativeTexture);

    switch (imageIndex.getType())
    {
        case gl::TextureType::_2D:
            *rtOut = &mLayeredRenderTargets[0];
            break;
        case gl::TextureType::CubeMap:
            *rtOut = &mLayeredRenderTargets[imageIndex.cubeMapFaceIndex()];
            break;
        default:
            UNREACHABLE();
    }

    return angle::Result::Continue;
}

angle::Result TextureMtl::syncState(const gl::Context *context,
                                    const gl::Texture::DirtyBits &dirtyBits,
                                    gl::Command source)
{
    if (dirtyBits.any())
    {
        // Invalidate sampler state
        mMetalSamplerState = nil;
    }

    ANGLE_TRY(ensureTextureCreated(context));
    ANGLE_TRY(ensureSamplerStateCreated(context));

    return angle::Result::Continue;
}

angle::Result TextureMtl::bindToShader(const gl::Context *context,
                                       mtl::RenderCommandEncoder *cmdEncoder,
                                       gl::ShaderType shaderType,
                                       int textureSlotIndex,
                                       int samplerSlotIndex)
{
    ASSERT(mNativeTexture);

    float minLodClamp = std::max(0.f, mState.getSamplerState().getMinLod());
    float maxLodClamp = mState.getSamplerState().getMaxLod();

    cmdEncoder->setTexture(shaderType, mNativeTexture, textureSlotIndex);
    cmdEncoder->setSamplerState(shaderType, mMetalSamplerState, minLodClamp, maxLodClamp,
                                samplerSlotIndex);

    return angle::Result::Continue;
}

angle::Result TextureMtl::redefineImage(const gl::Context *context,
                                        const gl::ImageIndex &index,
                                        const mtl::Format &mtlFormat,
                                        const gl::Extents &size)
{
    if (mNativeTexture)
    {
        if (mNativeTexture->valid())
        {
            // Calculate the expected size for the index we are defining. If the size is different
            // from the given size, or the format is different, we are redefining the image so we
            // must release it.
            bool typeChanged =
                mNativeTexture->textureType() != mtl::GetTextureType(index.getType());
            if (mFormat != mtlFormat || size != mNativeTexture->size(index) || typeChanged)
            {
                // Keep other images data if texture type hasn't been changed.
                releaseTexture(typeChanged);
            }
        }
    }

    // Early-out on empty textures, don't create a zero-sized storage.
    if (size.empty())
    {
        return angle::Result::Continue;
    }

    ContextMtl *contextMtl = mtl::GetImpl(context);
    // Cache last defined image format:
    mFormat                = mtlFormat;
    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];

    // If actual texture exists, it means the size hasn't been changed, no need to create new image
    if (mNativeTexture && image)
    {
        ASSERT(image->textureType() == mtl::GetTextureType(GetTextureImageType(index.getType())) &&
               image->pixelFormat() == mFormat.metalFormat && image->size() == size);
    }
    else
    {
        // Create image to hold texture's data at this level & slice:
        switch (index.getType())
        {
            case gl::TextureType::_2D:
            case gl::TextureType::CubeMap:
                ANGLE_TRY(mtl::Texture::Make2DTexture(contextMtl, mtlFormat, size.width,
                                                      size.height, 1, false, false, &image));
                break;
            default:
                UNREACHABLE();
        }
    }

    // Make sure emulated channels are properly initialized
    ANGLE_TRY(checkForEmulatedChannels(context, mtlFormat, image));

    // Tell context to rebind textures
    contextMtl->invalidateCurrentTextures();

    return angle::Result::Continue;
}

// If mipmaps = 0, this function will create full mipmaps texture.
angle::Result TextureMtl::setStorageImpl(const gl::Context *context,
                                         gl::TextureType type,
                                         size_t mipmaps,
                                         const mtl::Format &mtlFormat,
                                         const gl::Extents &size)
{
    if (mNativeTexture)
    {
        // Don't need to hold old images data.
        releaseTexture(true);
    }

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

    // Tell context to rebind textures
    contextMtl->invalidateCurrentTextures();

    mFormat = mtlFormat;

    // Texture will be created later in ensureTextureCreated()

    return angle::Result::Continue;
}

angle::Result TextureMtl::setImageImpl(const gl::Context *context,
                                       const gl::ImageIndex &index,
                                       const gl::InternalFormat &formatInfo,
                                       const gl::Extents &size,
                                       GLenum type,
                                       const gl::PixelUnpackState &unpack,
                                       const uint8_t *pixels)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(formatInfo.sizedInternalFormat);
    const mtl::Format &mtlFormat = contextMtl->getPixelFormat(angleFormatId);

    ANGLE_TRY(redefineImage(context, index, mtlFormat, size));

    // Early-out on empty textures, don't create a zero-sized storage.
    if (size.empty())
    {
        return angle::Result::Continue;
    }

    return setSubImageImpl(context, index, gl::Box(0, 0, 0, size.width, size.height, size.depth),
                           formatInfo, type, unpack, pixels);
}

angle::Result TextureMtl::setSubImageImpl(const gl::Context *context,
                                          const gl::ImageIndex &index,
                                          const gl::Box &area,
                                          const gl::InternalFormat &formatInfo,
                                          GLenum type,
                                          const gl::PixelUnpackState &unpack,
                                          const uint8_t *pixels)
{
    if (!pixels)
    {
        return angle::Result::Continue;
    }

    ASSERT(unpack.skipRows == 0 && unpack.skipPixels == 0 && unpack.skipImages == 0);

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

    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(formatInfo.sizedInternalFormat);
    const mtl::Format &mtlSrcFormat = contextMtl->getPixelFormat(angleFormatId);

    if (mFormat.metalFormat != mtlSrcFormat.metalFormat)
    {
        ANGLE_MTL_CHECK(contextMtl, false, GL_INVALID_OPERATION);
    }

    ANGLE_TRY(ensureImageCreated(context, index));
    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];

    GLuint sourceRowPitch = 0;
    ANGLE_CHECK_GL_MATH(contextMtl, formatInfo.computeRowPitch(type, area.width, unpack.alignment,
                                                               unpack.rowLength, &sourceRowPitch));
    // Check if partial image update is supported for this format
    if (!formatInfo.supportSubImage())
    {
        // area must be the whole mip level
        sourceRowPitch   = 0;
        gl::Extents size = image->size(index);
        if (area.x != 0 || area.y != 0 || area.width != size.width || area.height != size.height)
        {
            ANGLE_MTL_CHECK(contextMtl, false, GL_INVALID_OPERATION);
        }
    }

    // Only 2D/cube texture is supported atm
    auto mtlRegion = MTLRegionMake2D(area.x, area.y, area.width, area.height);

    const angle::Format &srcAngleFormat =
        angle::Format::Get(angle::Format::InternalFormatToID(formatInfo.sizedInternalFormat));

    // If source pixels are luminance or RGB8, we need to convert them to RGBA
    if (mFormat.needConversion(srcAngleFormat.id))
    {
        return convertAndSetSubImage(context, index, mtlRegion, formatInfo, type, srcAngleFormat,
                                     sourceRowPitch, pixels);
    }

    // Upload to texture
    ANGLE_TRY(UploadTextureContents(context, image, mFormat.actualAngleFormat(), mtlRegion, 0, 0,
                                    pixels, sourceRowPitch));

    return angle::Result::Continue;
}

angle::Result TextureMtl::convertAndSetSubImage(const gl::Context *context,
                                                const gl::ImageIndex &index,
                                                const MTLRegion &mtlArea,
                                                const gl::InternalFormat &internalFormat,
                                                GLenum type,
                                                const angle::Format &pixelsFormat,
                                                size_t pixelsRowPitch,
                                                const uint8_t *pixels)
{
    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];
    ASSERT(image && image->valid());
    ASSERT(image->textureType() == MTLTextureType2D);

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

    LoadImageFunctionInfo loadFunctionInfo =
        mFormat.textureLoadFunctions ? mFormat.textureLoadFunctions(type) : LoadImageFunctionInfo();

    const angle::Format &dstFormat = angle::Format::Get(mFormat.actualFormatId);
    const size_t dstRowPitch       = dstFormat.pixelBytes * mtlArea.size.width;

    // Check if original image data is compressed:
    if (mFormat.intendedAngleFormat().isBlock)
    {
        ASSERT(loadFunctionInfo.loadFunction);

        // Need to create a buffer to hold entire decompressed image.
        const size_t dstDepthPitch = dstRowPitch * mtlArea.size.height;
        angle::MemoryBuffer decompressBuf;
        ANGLE_CHECK_GL_ALLOC(contextMtl, decompressBuf.resize(dstDepthPitch * mtlArea.size.depth));

        // Decompress
        loadFunctionInfo.loadFunction(mtlArea.size.width, mtlArea.size.height, mtlArea.size.depth,
                                      pixels, pixelsRowPitch, 0, decompressBuf.data(), dstRowPitch,
                                      dstDepthPitch);

        // Upload to texture
        ANGLE_TRY(UploadTextureContents(context, image, dstFormat, mtlArea, 0, 0,
                                        decompressBuf.data(), dstRowPitch));
    }  // if (mFormat.intendedAngleFormat().isBlock)
    else
    {
        // Create scratch buffer
        angle::MemoryBuffer conversionRow;
        ANGLE_CHECK_GL_ALLOC(contextMtl, conversionRow.resize(dstRowPitch));

        MTLRegion mtlRow    = mtlArea;
        mtlRow.size.height  = 1;
        const uint8_t *psrc = pixels;
        for (NSUInteger r = 0; r < mtlArea.size.height; ++r, psrc += pixelsRowPitch)
        {
            mtlRow.origin.y = mtlArea.origin.y + r;

            // Convert pixels
            if (loadFunctionInfo.loadFunction)
            {
                loadFunctionInfo.loadFunction(mtlRow.size.width, 1, 1, psrc, pixelsRowPitch, 0,
                                              conversionRow.data(), dstRowPitch, 0);
            }
            else
            {
                CopyImageCHROMIUM(psrc, pixelsRowPitch, pixelsFormat.pixelBytes, 0,
                                  pixelsFormat.pixelReadFunction, conversionRow.data(), dstRowPitch,
                                  dstFormat.pixelBytes, 0, dstFormat.pixelWriteFunction,
                                  internalFormat.format, dstFormat.componentType, mtlRow.size.width,
                                  1, 1, false, false, false);
            }

            // Upload to texture
            ANGLE_TRY(UploadTextureContents(context, image, dstFormat, mtlRow, 0, 0,
                                            conversionRow.data(), dstRowPitch));
        }
    }

    return angle::Result::Continue;
}

angle::Result TextureMtl::checkForEmulatedChannels(const gl::Context *context,
                                                   const mtl::Format &mtlFormat,
                                                   const mtl::TextureRef &texture)
{
    bool emulatedChannels = mtl::IsFormatEmulated(mtlFormat);

    // For emulated channels that GL texture intends to not have,
    // we need to initialize their content.
    if (emulatedChannels)
    {
        uint32_t mipmaps = texture->mipmapLevels();

        int layers = texture->textureType() == MTLTextureTypeCube ? 6 : 1;
        for (int layer = 0; layer < layers; ++layer)
        {
            auto cubeFace = static_cast<gl::TextureTarget>(
                static_cast<int>(gl::TextureTarget::CubeMapPositiveX) + layer);
            for (uint32_t mip = 0; mip < mipmaps; ++mip)
            {
                gl::ImageIndex index;
                if (layers > 1)
                {
                    index = gl::ImageIndex::MakeCubeMapFace(cubeFace, mip);
                }
                else
                {
                    index = gl::ImageIndex::Make2D(mip);
                }

                ANGLE_TRY(mtl::InitializeTextureContents(context, texture, mFormat, index));
            }
        }
    }
    return angle::Result::Continue;
}

angle::Result TextureMtl::initializeContents(const gl::Context *context,
                                             const gl::ImageIndex &index)
{
    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];
    if (!image)
    {
        ANGLE_TRY(ensureTextureCreated(context));
    }
    return mtl::InitializeTextureContents(context, image, mFormat, GetImageBaseLevelIndex(image));
}

angle::Result TextureMtl::copySubImageImpl(const gl::Context *context,
                                           const gl::ImageIndex &index,
                                           const gl::Offset &destOffset,
                                           const gl::Rectangle &sourceArea,
                                           const gl::InternalFormat &internalFormat,
                                           gl::Framebuffer *source)
{
    gl::Extents fbSize = source->getReadColorAttachment()->getSize();
    gl::Rectangle clippedSourceArea;
    if (!ClipRectangle(sourceArea, gl::Rectangle(0, 0, fbSize.width, fbSize.height),
                       &clippedSourceArea))
    {
        return angle::Result::Continue;
    }

    // If negative offsets are given, clippedSourceArea ensures we don't read from those offsets.
    // However, that changes the sourceOffset->destOffset mapping.  Here, destOffset is shifted by
    // the same amount as clipped to correct the error.
    const gl::Offset modifiedDestOffset(destOffset.x + clippedSourceArea.x - sourceArea.x,
                                        destOffset.y + clippedSourceArea.y - sourceArea.y, 0);

    ANGLE_TRY(ensureImageCreated(context, index));

    if (!mFormat.getCaps().isRenderable())
    {
        return copySubImageCPU(context, index, modifiedDestOffset, clippedSourceArea,
                               internalFormat, source);
    }

    // NOTE(hqle): Use compute shader.
    return copySubImageWithDraw(context, index, modifiedDestOffset, clippedSourceArea,
                                internalFormat, source);
}

angle::Result TextureMtl::copySubImageWithDraw(const gl::Context *context,
                                               const gl::ImageIndex &index,
                                               const gl::Offset &modifiedDestOffset,
                                               const gl::Rectangle &clippedSourceArea,
                                               const gl::InternalFormat &internalFormat,
                                               gl::Framebuffer *source)
{
    ContextMtl *contextMtl         = mtl::GetImpl(context);
    DisplayMtl *displayMtl         = contextMtl->getDisplay();
    FramebufferMtl *framebufferMtl = mtl::GetImpl(source);

    RenderTargetMtl *colorReadRT = framebufferMtl->getColorReadRenderTarget(context);

    if (!colorReadRT || !colorReadRT->getTexture())
    {
        // Is this an error?
        return angle::Result::Continue;
    }

    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];
    ASSERT(image && image->valid());

    mtl::RenderCommandEncoder *cmdEncoder =
        contextMtl->getRenderCommandEncoder(image, GetImageBaseLevelIndex(image));
    mtl::BlitParams blitParams;

    blitParams.dstOffset    = modifiedDestOffset;
    blitParams.dstColorMask = image->getColorWritableMask();

    blitParams.src          = colorReadRT->getTexture();
    blitParams.srcLevel     = static_cast<uint32_t>(colorReadRT->getLevelIndex());
    blitParams.srcRect      = clippedSourceArea;
    blitParams.srcYFlipped  = framebufferMtl->flipY();
    blitParams.dstLuminance = internalFormat.isLUMA();

    return displayMtl->getUtils().blitWithDraw(context, cmdEncoder, blitParams);
}

angle::Result TextureMtl::copySubImageCPU(const gl::Context *context,
                                          const gl::ImageIndex &index,
                                          const gl::Offset &modifiedDestOffset,
                                          const gl::Rectangle &clippedSourceArea,
                                          const gl::InternalFormat &internalFormat,
                                          gl::Framebuffer *source)
{
    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];
    ASSERT(image && image->valid());

    ContextMtl *contextMtl         = mtl::GetImpl(context);
    FramebufferMtl *framebufferMtl = mtl::GetImpl(source);
    RenderTargetMtl *colorReadRT   = framebufferMtl->getColorReadRenderTarget(context);

    if (!colorReadRT || !colorReadRT->getTexture())
    {
        // Is this an error?
        return angle::Result::Continue;
    }

    const angle::Format &dstFormat = angle::Format::Get(mFormat.actualFormatId);
    const int dstRowPitch          = dstFormat.pixelBytes * clippedSourceArea.width;
    angle::MemoryBuffer conversionRow;
    ANGLE_CHECK_GL_ALLOC(contextMtl, conversionRow.resize(dstRowPitch));

    MTLRegion mtlDstRowArea  = MTLRegionMake2D(modifiedDestOffset.x, 0, clippedSourceArea.width, 1);
    gl::Rectangle srcRowArea = gl::Rectangle(clippedSourceArea.x, 0, clippedSourceArea.width, 1);

    for (int r = 0; r < clippedSourceArea.height; ++r)
    {
        mtlDstRowArea.origin.y = modifiedDestOffset.y + r;
        srcRowArea.y           = clippedSourceArea.y + r;

        PackPixelsParams packParams(srcRowArea, dstFormat, dstRowPitch, false, nullptr, 0);

        // Read pixels from framebuffer to memory:
        gl::Rectangle flippedSrcRowArea =
            framebufferMtl->getCorrectFlippedReadArea(context, srcRowArea);
        ANGLE_TRY(framebufferMtl->readPixelsImpl(context, flippedSrcRowArea, packParams,
                                                 colorReadRT, conversionRow.data()));

        // Upload to texture
        ANGLE_TRY(UploadTextureContents(context, image, dstFormat, mtlDstRowArea, 0, 0,
                                        conversionRow.data(), dstRowPitch));
    }

    return angle::Result::Continue;
}

angle::Result TextureMtl::copySubTextureImpl(const gl::Context *context,
                                             const gl::ImageIndex &index,
                                             const gl::Offset &destOffset,
                                             const gl::InternalFormat &internalFormat,
                                             size_t sourceLevel,
                                             const gl::Box &sourceBox,
                                             bool unpackFlipY,
                                             bool unpackPremultiplyAlpha,
                                             bool unpackUnmultiplyAlpha,
                                             const gl::Texture *source)
{
    // Only 2D textures are supported.
    ASSERT(sourceBox.depth == 1);
    ASSERT(source->getType() == gl::TextureType::_2D);
    gl::ImageIndex sourceIndex = gl::ImageIndex::Make2D(static_cast<GLint>(sourceLevel));

    ContextMtl *contextMtl = mtl::GetImpl(context);
    TextureMtl *sourceMtl  = mtl::GetImpl(source);

    ANGLE_TRY(ensureImageCreated(context, index));

    ANGLE_TRY(sourceMtl->ensureImageCreated(context, sourceIndex));

    const gl::ImageDesc &srcDesc    = source->getState().getImageDesc(sourceIndex);
    const mtl::TextureRef &srcImage = sourceMtl->mTexImages[0][sourceLevel];
    const mtl::Format &srcFormat    = contextMtl->getPixelFormat(
        angle::Format::InternalFormatToID(srcDesc.format.info->sizedInternalFormat));
    const angle::Format &srcAngleFormat = srcFormat.actualAngleFormat();

    const mtl::Format &dstFormat = contextMtl->getPixelFormat(
        angle::Format::InternalFormatToID(internalFormat.sizedInternalFormat));

    if (!dstFormat.getCaps().isRenderable())
    {
        return copySubTextureCPU(context, index, destOffset, internalFormat, 0, sourceBox,
                                 srcAngleFormat, unpackFlipY, unpackPremultiplyAlpha,
                                 unpackUnmultiplyAlpha, srcImage);
    }
    return copySubTextureWithDraw(context, index, destOffset, internalFormat, 0, sourceBox,
                                  srcAngleFormat, unpackFlipY, unpackPremultiplyAlpha,
                                  unpackUnmultiplyAlpha, srcImage);
}

angle::Result TextureMtl::copySubTextureWithDraw(const gl::Context *context,
                                                 const gl::ImageIndex &index,
                                                 const gl::Offset &destOffset,
                                                 const gl::InternalFormat &internalFormat,
                                                 uint32_t sourceNativeLevel,
                                                 const gl::Box &sourceBox,
                                                 const angle::Format &sourceAngleFormat,
                                                 bool unpackFlipY,
                                                 bool unpackPremultiplyAlpha,
                                                 bool unpackUnmultiplyAlpha,
                                                 const mtl::TextureRef &sourceTexture)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    DisplayMtl *displayMtl = contextMtl->getDisplay();

    mtl::TextureRef image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];
    ASSERT(image && image->valid());

    if (internalFormat.colorEncoding == GL_SRGB)
    {
        image = image->getLinearColorView();
    }

    mtl::RenderCommandEncoder *cmdEncoder =
        contextMtl->getRenderCommandEncoder(image, GetImageBaseLevelIndex(image));
    mtl::BlitParams blitParams;

    blitParams.dstOffset    = destOffset;
    blitParams.dstColorMask = image->getColorWritableMask();

    blitParams.src      = sourceTexture;
    blitParams.srcLevel = sourceNativeLevel;
    blitParams.srcRect = gl::Rectangle(sourceBox.x, sourceBox.y, sourceBox.width, sourceBox.height);
    blitParams.srcYFlipped            = false;
    blitParams.dstLuminance           = internalFormat.isLUMA();
    blitParams.unpackFlipY            = unpackFlipY;
    blitParams.unpackPremultiplyAlpha = unpackPremultiplyAlpha;
    blitParams.unpackUnmultiplyAlpha  = unpackUnmultiplyAlpha;

    return displayMtl->getUtils().blitWithDraw(context, cmdEncoder, blitParams);
}

angle::Result TextureMtl::copySubTextureCPU(const gl::Context *context,
                                            const gl::ImageIndex &index,
                                            const gl::Offset &destOffset,
                                            const gl::InternalFormat &internalFormat,
                                            uint32_t sourceNativeLevel,
                                            const gl::Box &sourceBox,
                                            const angle::Format &sourceAngleFormat,
                                            bool unpackFlipY,
                                            bool unpackPremultiplyAlpha,
                                            bool unpackUnmultiplyAlpha,
                                            const mtl::TextureRef &sourceTexture)
{
    mtl::TextureRef &image = mTexImages[GetImageLayerIndex(index)][index.getLevelIndex()];
    ASSERT(image && image->valid());

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

    const angle::Format &dstAngleFormat = mFormat.actualAngleFormat();
    const int srcRowPitch               = sourceAngleFormat.pixelBytes * sourceBox.width;
    const int srcImageSize              = srcRowPitch * sourceBox.height;
    const int convRowPitch              = dstAngleFormat.pixelBytes * sourceBox.width;
    const int convImageSize             = convRowPitch * sourceBox.height;
    angle::MemoryBuffer conversionSrc, conversionDst;
    ANGLE_CHECK_GL_ALLOC(contextMtl, conversionSrc.resize(srcImageSize));
    ANGLE_CHECK_GL_ALLOC(contextMtl, conversionDst.resize(convImageSize));

    MTLRegion mtlSrcArea =
        MTLRegionMake2D(sourceBox.x, sourceBox.y, sourceBox.width, sourceBox.height);
    MTLRegion mtlDstArea =
        MTLRegionMake2D(destOffset.x, destOffset.y, sourceBox.width, sourceBox.height);

    // Read pixels from source to memory:
    sourceTexture->getBytes(contextMtl, srcRowPitch, mtlSrcArea, sourceNativeLevel,
                            conversionSrc.data());

    // Convert to destination format
    CopyImageCHROMIUM(conversionSrc.data(), srcRowPitch, sourceAngleFormat.pixelBytes, 0,
                      sourceAngleFormat.pixelReadFunction, conversionDst.data(), convRowPitch,
                      dstAngleFormat.pixelBytes, 0, dstAngleFormat.pixelWriteFunction,
                      internalFormat.format, internalFormat.componentType, sourceBox.width,
                      sourceBox.height, 1, unpackFlipY, unpackPremultiplyAlpha,
                      unpackUnmultiplyAlpha);

    // Upload to texture
    ANGLE_TRY(UploadTextureContents(context, image, dstAngleFormat, mtlDstArea, 0, 0,
                                    conversionDst.data(), convRowPitch));

    return angle::Result::Continue;
}

}
