
//
// 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 "image_util/imageformats.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 GetZeroLevelIndex(const mtl::TextureRef &image)
{
    switch (image->textureType())
    {
        case MTLTextureType2D:
            return gl::ImageIndex::Make2D(0);
        case MTLTextureTypeCube:
            return gl::ImageIndex::MakeFromType(gl::TextureType::CubeMap, 0);
        case MTLTextureType2DArray:
            return gl::ImageIndex::Make2DArray(0 /** entire layers */);
        case MTLTextureType2DMultisample:
            return gl::ImageIndex::Make2DMultisample();
        case MTLTextureType3D:
            return gl::ImageIndex::Make3D(0 /** entire layers */);
        default:
            UNREACHABLE();
            break;
    }

    return gl::ImageIndex();
}

// Slice is ignored if texture type is not Cube or 2D array
gl::ImageIndex GetSliceMipIndex(const mtl::TextureRef &image, uint32_t slice, uint32_t level)
{
    switch (image->textureType())
    {
        case MTLTextureType2D:
            return gl::ImageIndex::Make2D(level);
        case MTLTextureTypeCube:
        {
            auto cubeFace = static_cast<gl::TextureTarget>(
                static_cast<int>(gl::TextureTarget::CubeMapPositiveX) + slice);
            return gl::ImageIndex::MakeCubeMapFace(cubeFace, level);
        }
        case MTLTextureType2DArray:
            return gl::ImageIndex::Make2DArray(level, slice);
        case MTLTextureType2DMultisample:
            return gl::ImageIndex::Make2DMultisample();
        case MTLTextureType3D:
            return gl::ImageIndex::Make3D(level);
        default:
            UNREACHABLE();
            break;
    }

    return gl::ImageIndex();
}

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

    return 0;
}

GLuint GetImageCubeFaceIndexOrZero(const gl::ImageIndex &index)
{
    switch (index.getType())
    {
        case gl::TextureType::CubeMap:
            return index.cubeMapFaceIndex();
        default:
            break;
    }

    return 0;
}

// Given texture type, get texture type of one image for a glTexImage call.
// For example, for texture 2d, one image is also texture 2d.
// for texture cube, one image is texture 2d.
gl::TextureType GetTextureImageType(gl::TextureType texType)
{
    switch (texType)
    {
        case gl::TextureType::CubeMap:
            return gl::TextureType::_2D;
        case gl::TextureType::_2D:
        case gl::TextureType::_2DArray:
        case gl::TextureType::_2DMultisample:
        case gl::TextureType::_3D:
        case gl::TextureType::Rectangle:
            return texType;
        default:
            UNREACHABLE();
            return gl::TextureType::InvalidEnum;
    }
}

// D24X8 by default writes depth data to high 24 bits of 32 bit integers. However, Metal separate
// depth stencil blitting expects depth data to be in low 24 bits of the data.
void WriteDepthStencilToDepth24(const uint8_t *srcPtr, uint8_t *dstPtr)
{
    auto src = reinterpret_cast<const angle::DepthStencil *>(srcPtr);
    auto dst = reinterpret_cast<uint32_t *>(dstPtr);
    *dst     = gl::floatToNormalized<24, uint32_t>(static_cast<float>(src->depth));
}

void ConvertDepthStencilData(const MTLSize &regionSize,
                             const angle::Format &srcAngleFormat,
                             size_t srcRowPitch,
                             size_t src2DImageSize,
                             const uint8_t *psrc,
                             const angle::Format &dstAngleFormat,
                             rx::PixelWriteFunction pixelWriteFunctionOverride,
                             size_t destRowPitch,
                             size_t dest2DImageSize,
                             uint8_t *pdst)
{
    if (srcAngleFormat.id == dstAngleFormat.id)
    {
        size_t rowCopySize = std::min(srcRowPitch, destRowPitch);
        for (NSUInteger d = 0; d < regionSize.depth; ++d)
        {
            for (NSUInteger r = 0; r < regionSize.height; ++r)
            {
                const uint8_t *pCopySrc = psrc + d * src2DImageSize + r * srcRowPitch;
                uint8_t *pCopyDst       = pdst + d * dest2DImageSize + r * destRowPitch;
                memcpy(pCopyDst, pCopySrc, rowCopySize);
            }
        }
    }
    else
    {
        rx::PixelWriteFunction pixelWriteFunction = pixelWriteFunctionOverride
                                                        ? pixelWriteFunctionOverride
                                                        : dstAngleFormat.pixelWriteFunction;
        // This is only for depth & stencil case.
        ASSERT(srcAngleFormat.depthBits || srcAngleFormat.stencilBits);
        ASSERT(srcAngleFormat.pixelReadFunction && pixelWriteFunction);

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

        for (NSUInteger d = 0; d < regionSize.depth; ++d)
        {
            for (NSUInteger r = 0; r < regionSize.height; ++r)
            {
                for (NSUInteger c = 0; c < regionSize.width; ++c)
                {
                    const uint8_t *sourcePixelData =
                        psrc + d * src2DImageSize + r * srcRowPitch + c * srcAngleFormat.pixelBytes;

                    uint8_t *destPixelData = pdst + d * dest2DImageSize + r * destRowPitch +
                                             c * dstAngleFormat.pixelBytes;

                    srcAngleFormat.pixelReadFunction(sourcePixelData, sourcePixelReadData);
                    pixelWriteFunction(sourcePixelReadData, destPixelData);
                }
            }
        }
    }
}

angle::Result CopyDepthStencilTextureContentsToStagingBuffer(
    ContextMtl *contextMtl,
    const angle::Format &textureAngleFormat,
    const angle::Format &stagingAngleFormat,
    rx::PixelWriteFunction pixelWriteFunctionOverride,
    const MTLSize &regionSize,
    const uint8_t *data,
    size_t bytesPerRow,
    size_t bytesPer2DImage,
    size_t *bufferRowPitchOut,
    size_t *buffer2DImageSizeOut,
    mtl::BufferRef *bufferOut)
{
    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);

    ConvertDepthStencilData(regionSize, textureAngleFormat, bytesPerRow, bytesPer2DImage, data,
                            stagingAngleFormat, pixelWriteFunctionOverride, stagingBufferRowPitch,
                            stagingBuffer2DImageSize, pdst);

    stagingBuffer->unmap(contextMtl);

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

    return angle::Result::Continue;
}

angle::Result UploadDepthStencilTextureContentsWithStagingBuffer(
    ContextMtl *contextMtl,
    const angle::Format &textureAngleFormat,
    MTLRegion region,
    uint32_t mipmapLevel,
    uint32_t slice,
    const uint8_t *data,
    size_t bytesPerRow,
    size_t bytesPer2DImage,
    const mtl::TextureRef &texture)
{
    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(CopyDepthStencilTextureContentsToStagingBuffer(
        contextMtl, textureAngleFormat, textureAngleFormat, textureAngleFormat.pixelWriteFunction,
        region.size, data, bytesPerRow, bytesPer2DImage, &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 angle::Format &textureAngleFormat,
    MTLRegion region,
    uint32_t mipmapLevel,
    uint32_t slice,
    const uint8_t *data,
    size_t bytesPerRow,
    size_t bytesPer2DImage,
    const mtl::TextureRef &texture)
{
    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;
    // Custom depth write function. We cannot use those in imageformats.cpp since Metal has some
    // special cases.
    rx::PixelWriteFunction stagingDepthBufferWriteFunctionOverride = nullptr;

    switch (textureAngleFormat.id)
    {
        case angle::FormatID::D24_UNORM_S8_UINT:
            // D24_UNORM_X8_UINT writes depth data to high 24 bits. But Metal expects depth data to
            // be in low 24 bits.
            stagingDepthBufferFormatId              = angle::FormatID::D24_UNORM_X8_UINT;
            stagingDepthBufferWriteFunctionOverride = WriteDepthStencilToDepth24;
            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;

    // Copy depth data to staging depth buffer
    ANGLE_TRY(CopyDepthStencilTextureContentsToStagingBuffer(
        contextMtl, textureAngleFormat, angleStagingDepthFormat,
        stagingDepthBufferWriteFunctionOverride, region.size, data, bytesPerRow, bytesPer2DImage,
        &stagingDepthBufferRowPitch, &stagingDepthBuffer2DImageSize, &stagingDepthbuffer));

    // Copy stencil data to staging stencil buffer
    ANGLE_TRY(CopyDepthStencilTextureContentsToStagingBuffer(
        contextMtl, textureAngleFormat, angleStagingStencilFormat, nullptr, region.size, data,
        bytesPerRow, bytesPer2DImage, &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 angle::Format &textureAngleFormat,
                                    const MTLRegion &region,
                                    uint32_t mipmapLevel,
                                    uint32_t slice,
                                    const uint8_t *data,
                                    size_t bytesPerRow,
                                    size_t bytesPer2DImage,
                                    const mtl::TextureRef &texture)
{
    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,
                               bytesPer2DImage);

        return angle::Result::Continue;
    }

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

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

    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)
{
    releaseTexture(releaseImages, false);
}

void TextureMtl::releaseTexture(bool releaseImages, bool releaseTextureObjectsOnly)
{

    if (releaseImages)
    {
        mTexImageDefs.clear();
    }
    else if (mNativeTexture)
    {
        // Release native texture but keep its old per face per mipmap level image views.
        retainImageDefinitions();
    }

    mNativeTexture = nullptr;

    // Clear render target cache for each texture's image. We don't erase them because they
    // might still be referenced by a framebuffer.
    for (auto &sliceRenderTargets : mPerLayerRenderTargets)
    {
        for (RenderTargetMtl &mipRenderTarget : sliceRenderTargets.second)
        {
            mipRenderTarget.reset();
        }
    }

    for (mtl::TextureRef &view : mNativeLevelViews)
    {
        view.reset();
    }

    if (!releaseTextureObjectsOnly)
    {
        mMetalSamplerState = nil;
        mFormat            = mtl::Format();
        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:
    const GLuint mips  = mState.getMipmapMaxLevel() - mState.getEffectiveBaseLevel() + 1;
    gl::ImageDesc desc = mState.getBaseLevelDesc();
    ANGLE_MTL_CHECK(contextMtl, desc.format.valid(), GL_INVALID_OPERATION);
    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(desc.format.info->sizedInternalFormat);
    mFormat = contextMtl->getPixelFormat(angleFormatId);

    return createNativeTexture(context, mState.getType(), mips, desc.size);
}

angle::Result TextureMtl::createNativeTexture(const gl::Context *context,
                                              gl::TextureType type,
                                              GLuint mips,
                                              const gl::Extents &size)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);

    // Create actual texture object:
    mIsPow2          = gl::isPow2(size.width) && gl::isPow2(size.height) && gl::isPow2(size.depth);
    mSlices          = 1;
    int numCubeFaces = 1;
    switch (type)
    {
        case gl::TextureType::_2D:
            ANGLE_TRY(mtl::Texture::Make2DTexture(
                contextMtl, mFormat, size.width, size.height, mips,
                /** renderTargetOnly */ false,
                /** allowFormatView */ mFormat.hasDepthAndStencilBits(), &mNativeTexture));
            break;
        case gl::TextureType::CubeMap:
            mSlices = numCubeFaces = 6;
            ANGLE_TRY(mtl::Texture::MakeCubeTexture(
                contextMtl, mFormat, size.width, mips,
                /** renderTargetOnly */ false,
                /** allowFormatView */ mFormat.hasDepthAndStencilBits(), &mNativeTexture));
            break;
        case gl::TextureType::_3D:
            ANGLE_TRY(mtl::Texture::Make3DTexture(
                contextMtl, mFormat, size.width, size.height, size.depth, mips,
                /** renderTargetOnly */ false,
                /** allowFormatView */ mFormat.hasDepthAndStencilBits(), &mNativeTexture));
            break;
        default:
            UNREACHABLE();
    }

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

    // Transfer data from images to actual texture object
    mtl::BlitCommandEncoder *encoder = nullptr;
    for (int face = 0; face < numCubeFaces; ++face)
    {
        for (GLuint mip = 0; mip < mips; ++mip)
        {
            GLuint imageMipLevel             = mip + mState.getEffectiveBaseLevel();
            mtl::TextureRef &imageToTransfer = mTexImageDefs[face][imageMipLevel].image;

            // 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())
            {
                if (!encoder)
                {
                    encoder = contextMtl->getBlitCommandEncoder();
                }
                encoder->copyTexture(imageToTransfer, 0, 0, mNativeTexture, face, mip,
                                     imageToTransfer->arrayLength(), 1);
            }

            imageToTransfer = nullptr;
        }
    }

    // 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 = getImage(index);
    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::ensureNativeLevelViewsCreated()
{
    ASSERT(mNativeTexture);
    const GLuint baseLevel = mState.getEffectiveBaseLevel();
    for (uint32_t mip = 0; mip < mNativeTexture->mipmapLevels(); ++mip)
    {
        if (mNativeLevelViews[mip])
        {
            continue;
        }

        if (mNativeTexture->textureType() != MTLTextureTypeCube &&
            mTexImageDefs[0][mip + baseLevel].image)
        {
            // Reuse texture image view.
            mNativeLevelViews[mip] = mTexImageDefs[0][mip + baseLevel].image;
        }
        else
        {
            mNativeLevelViews[mip] = mNativeTexture->createMipView(mip);
        }
    }
    return angle::Result::Continue;
}

mtl::TextureRef TextureMtl::createImageViewFromNativeTexture(GLuint cubeFaceOrZero,
                                                             GLuint nativeLevel)
{
    mtl::TextureRef image;
    if (mNativeTexture->textureType() == MTLTextureTypeCube)
    {
        // Cube texture's image is per face.
        image = mNativeTexture->createSliceMipView(cubeFaceOrZero, nativeLevel);
    }
    else
    {
        if (mNativeLevelViews[nativeLevel])
        {
            // Reuse the native level view
            image = mNativeLevelViews[nativeLevel];
        }
        else
        {
            image = mNativeTexture->createMipView(nativeLevel);
        }
    }

    return image;
}

void TextureMtl::retainImageDefinitions()
{
    if (!mNativeTexture)
    {
        return;
    }
    const GLuint mips = mNativeTexture->mipmapLevels();

    int numCubeFaces = 1;
    switch (mState.getType())
    {
        case gl::TextureType::CubeMap:
            numCubeFaces = 6;
            break;
        default:
            break;
    }

    // Create image view per cube face, per mip level
    for (int face = 0; face < numCubeFaces; ++face)
    {
        for (GLuint mip = 0; mip < mips; ++mip)
        {
            GLuint imageMipLevel         = mip + mState.getEffectiveBaseLevel();
            ImageDefinitionMtl &imageDef = mTexImageDefs[face][imageMipLevel];
            if (imageDef.image)
            {
                continue;
            }
            imageDef.image    = createImageViewFromNativeTexture(face, mip);
            imageDef.formatID = mFormat.intendedFormatId;
        }
    }
}

bool TextureMtl::isIndexWithinMinMaxLevels(const gl::ImageIndex &imageIndex) const
{
    return imageIndex.getLevelIndex() >= static_cast<GLint>(mState.getEffectiveBaseLevel()) &&
           imageIndex.getLevelIndex() <= static_cast<GLint>(mState.getEffectiveMaxLevel());
}

int TextureMtl::getNativeLevel(const gl::ImageIndex &imageIndex) const
{
    int baseLevel     = mState.getEffectiveBaseLevel();
    int adjustedLevel = imageIndex.getLevelIndex() - baseLevel;
    return adjustedLevel;
}

mtl::TextureRef &TextureMtl::getImage(const gl::ImageIndex &imageIndex)
{
    return getImageDefinition(imageIndex).image;
}

ImageDefinitionMtl &TextureMtl::getImageDefinition(const gl::ImageIndex &imageIndex)
{
    GLuint cubeFaceOrZero        = GetImageCubeFaceIndexOrZero(imageIndex);
    ImageDefinitionMtl &imageDef = mTexImageDefs[cubeFaceOrZero][imageIndex.getLevelIndex()];

    if (!imageDef.image && mNativeTexture)
    {
        // If native texture is already created, and the image at this index is not available,
        // then create a view of native texture at this index, so that modifications of the image
        // are reflected back to native texture's respective index.
        if (!isIndexWithinMinMaxLevels(imageIndex))
        {
            // Image below base level is skipped.
            return imageDef;
        }

        uint32_t nativeLevel = getNativeLevel(imageIndex);
        if (nativeLevel >= mNativeTexture->mipmapLevels())
        {
            // Image outside native texture's mip levels is skipped.
            return imageDef;
        }

        imageDef.image    = createImageViewFromNativeTexture(cubeFaceOrZero, nativeLevel);
        imageDef.formatID = mFormat.intendedFormatId;
    }

    return imageDef;
}
RenderTargetMtl &TextureMtl::getRenderTarget(const gl::ImageIndex &imageIndex)
{
    ASSERT(imageIndex.getType() == gl::TextureType::_2D ||
           imageIndex.getType() == gl::TextureType::Rectangle ||
           imageIndex.getType() == gl::TextureType::_2DMultisample || imageIndex.hasLayer());
    GLuint layer         = GetImageLayerIndex(imageIndex);
    RenderTargetMtl &rtt = mPerLayerRenderTargets[layer][imageIndex.getLevelIndex()];
    if (!rtt.getTexture())
    {
        // Lazy initialization of render target:
        mtl::TextureRef &image = getImage(imageIndex);
        if (image)
        {
            if (imageIndex.getType() == gl::TextureType::CubeMap)
            {
                // Cube map is special, the image is already the view of its layer
                rtt.set(image, 0, 0, mFormat);
            }
            else
            {
                rtt.set(image, 0, layer, mFormat);
            }
        }
    }
    return rtt;
}

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 &dstFormatInfo = gl::GetInternalFormatInfo(internalFormat, type);

    return setImageImpl(context, index, dstFormatInfo, size, format, type, unpack, unpackBuffer,
                        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, unpackBuffer, 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);
    const gl::State &glState             = context->getState();
    gl::Buffer *unpackBuffer             = glState.getTargetBuffer(gl::BufferBinding::PixelUnpack);

    return setImageImpl(context, index, formatInfo, size, internalFormat, GL_UNSIGNED_BYTE, unpack,
                        unpackBuffer, 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);

    const gl::State &glState = context->getState();
    gl::Buffer *unpackBuffer = glState.getTargetBuffer(gl::BufferBinding::PixelUnpack);

    return setSubImageImpl(context, index, area, formatInfo, GL_UNSIGNED_BYTE, unpack, unpackBuffer,
                           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));

    gl::Extents fbSize = source->getReadColorAttachment()->getSize();
    gl::Rectangle fbRect(0, 0, fbSize.width, fbSize.height);
    if (context->isWebGL() && !fbRect.encloses(sourceArea))
    {
        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,
                                      GLint 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,
                                         GLint 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 mtl::FormatCaps &caps = mFormat.getCaps();

    if (caps.writable && mState.getType() == gl::TextureType::_3D)
    {
        // http://anglebug.com/4921.
        // Use compute for 3D mipmap generation.
        ANGLE_TRY(ensureNativeLevelViewsCreated());
        bool sRGB = mFormat.metalFormat == MTLPixelFormatRGBA8Unorm_sRGB ||
                    mFormat.metalFormat == MTLPixelFormatBGRA8Unorm_sRGB;
        ANGLE_TRY(contextMtl->getDisplay()->getUtils().generateMipmapCS(contextMtl, mNativeTexture,
                                                                        sRGB, &mNativeLevelViews));
    }
    else if (caps.filterable && caps.colorRenderable)
    {
        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());

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

    for (uint32_t slice = 0; slice < mSlices; ++slice)
    {
        int maxMipLevel = static_cast<int>(mNativeTexture->mipmapLevels()) - 1;
        int firstLevel  = 0;

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

        // Download base level data
        mNativeTexture->getBytes(
            contextMtl, prevLevelRowPitch, prevLevelDepthPitch,
            MTLRegionMake3D(0, 0, 0, prevLevelWidth, prevLevelHeight, prevLevelDepth), firstLevel,
            slice, prevLevelData.get());

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

            size_t dstRowPitch   = angleFormat.pixelBytes * dstWidth;
            size_t dstDepthPitch = dstRowPitch * dstHeight;
            size_t dstDataSize   = dstDepthPitch * dstDepth;
            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,
                prevLevelDepthPitch, dstLevelData.get(), dstRowPitch, dstDepthPitch);

            // Upload to texture
            ANGLE_TRY(UploadTextureContents(
                context, angleFormat, MTLRegionMake3D(0, 0, 0, dstWidth, dstHeight, dstDepth), mip,
                slice, dstLevelData.get(), dstRowPitch, dstDepthPitch, mNativeTexture));

            prevLevelWidth      = dstWidth;
            prevLevelHeight     = dstHeight;
            prevLevelDepth      = dstDepth;
            prevLevelRowPitch   = dstRowPitch;
            prevLevelDepthPitch = dstDepthPitch;
            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));

    // 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));

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

    *rtOut = &getRenderTarget(imageIndex);

    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)
{
    bool imageWithinLevelRange = false;

    if (isIndexWithinMinMaxLevels(index) && mNativeTexture && mNativeTexture->valid())
    {
        imageWithinLevelRange = true;
        int nativeLevel       = getNativeLevel(index);
        // 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(nativeLevel) || 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;
    ImageDefinitionMtl &imageDef = getImageDefinition(index);

    // If native texture still exists, it means the size hasn't been changed, no need to create new
    // image
    if (mNativeTexture && imageDef.image && imageWithinLevelRange)
    {
        ASSERT(imageDef.image->textureType() ==
                   mtl::GetTextureType(GetTextureImageType(index.getType())) &&
               imageDef.formatID == mFormat.intendedFormatId && imageDef.image->size() == size);
    }
    else
    {
        imageDef.formatID = mtlFormat.intendedFormatId;
        // 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,
                    /** renderTargetOnly */ false,
                    /** allowFormatView */ mFormat.hasDepthAndStencilBits(), &imageDef.image));
                break;
            case gl::TextureType::_3D:
                ANGLE_TRY(mtl::Texture::Make3DTexture(
                    contextMtl, mtlFormat, size.width, size.height, size.depth, 1,
                    /** renderTargetOnly */ false,
                    /** allowFormatView */ mFormat.hasDepthAndStencilBits(), &imageDef.image));
                break;
            default:
                UNREACHABLE();
        }
    }

    // Make sure emulated channels are properly initialized
    ANGLE_TRY(checkForEmulatedChannels(context, mtlFormat, imageDef.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)
{
    // 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 &dstFormatInfo,
                                       const gl::Extents &size,
                                       GLenum srcFormat,
                                       GLenum srcType,
                                       const gl::PixelUnpackState &unpack,
                                       gl::Buffer *unpackBuffer,
                                       const uint8_t *pixels)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(dstFormatInfo.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;
    }

    // Format of the supplied pixels.
    const gl::InternalFormat *srcFormatInfo;
    if (srcFormat != dstFormatInfo.format || srcType != dstFormatInfo.type)
    {
        srcFormatInfo = &gl::GetInternalFormatInfo(srcFormat, srcType);
    }
    else
    {
        srcFormatInfo = &dstFormatInfo;
    }
    return setSubImageImpl(context, index, gl::Box(0, 0, 0, size.width, size.height, size.depth),
                           *srcFormatInfo, srcType, unpack, unpackBuffer, 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,
                                          gl::Buffer *unpackBuffer,
                                          const uint8_t *oriPixels)
{
    // NOTE(hqle): Support PBO
    ASSERT(!unpackBuffer);

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

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

    ANGLE_TRY(ensureImageCreated(context, index));
    mtl::TextureRef &image = getImage(index);

    GLuint sourceRowPitch   = 0;
    GLuint sourceDepthPitch = 0;
    GLuint sourceSkipBytes  = 0;
    ANGLE_CHECK_GL_MATH(contextMtl, formatInfo.computeRowPitch(type, area.width, unpack.alignment,
                                                               unpack.rowLength, &sourceRowPitch));
    ANGLE_CHECK_GL_MATH(
        contextMtl, formatInfo.computeDepthPitch(area.height, unpack.imageHeight, sourceRowPitch,
                                                 &sourceDepthPitch));
    ANGLE_CHECK_GL_MATH(contextMtl,
                        formatInfo.computeSkipBytes(type, sourceRowPitch, sourceDepthPitch, unpack,
                                                    index.usesTex3D(), &sourceSkipBytes));

    // 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();
        if (area.x != 0 || area.y != 0 || area.width != size.width || area.height != size.height)
        {
            ANGLE_MTL_CHECK(contextMtl, false, GL_INVALID_OPERATION);
        }
    }

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

    const uint8_t *usablePixels = oriPixels + sourceSkipBytes;

    auto mtlRegion = MTLRegionMake3D(area.x, area.y, area.z, area.width, area.height, area.depth);

    ANGLE_TRY(setPerSliceSubImage(context, 0, mtlRegion, formatInfo, type, srcAngleFormat,
                                  sourceRowPitch, sourceDepthPitch, unpackBuffer, usablePixels,
                                  image));

    return angle::Result::Continue;
}

angle::Result TextureMtl::setPerSliceSubImage(const gl::Context *context,
                                              int slice,
                                              const MTLRegion &mtlArea,
                                              const gl::InternalFormat &internalFormat,
                                              GLenum type,
                                              const angle::Format &pixelsAngleFormat,
                                              size_t pixelsRowPitch,
                                              size_t pixelsDepthPitch,
                                              gl::Buffer *unpackBuffer,
                                              const uint8_t *pixels,
                                              const mtl::TextureRef &image)
{
    // If source pixels are luminance or RGB8, we need to convert them to RGBA
    if (mFormat.needConversion(pixelsAngleFormat.id))
    {
        return convertAndSetPerSliceSubImage(context, slice, mtlArea, internalFormat, type,
                                             pixelsAngleFormat, pixelsRowPitch, pixelsDepthPitch,
                                             unpackBuffer, pixels, image);
    }

    // NOTE(hqle): Support PBO
    ASSERT(!unpackBuffer);

    // Upload texture data directly
    ANGLE_TRY(UploadTextureContents(context, mFormat.actualAngleFormat(), mtlArea, 0, slice, pixels,
                                    pixelsRowPitch, pixelsDepthPitch, image));

    return angle::Result::Continue;
}

angle::Result TextureMtl::convertAndSetPerSliceSubImage(const gl::Context *context,
                                                        int slice,
                                                        const MTLRegion &mtlArea,
                                                        const gl::InternalFormat &internalFormat,
                                                        GLenum type,
                                                        const angle::Format &pixelsAngleFormat,
                                                        size_t pixelsRowPitch,
                                                        size_t pixelsDepthPitch,
                                                        gl::Buffer *unpackBuffer,
                                                        const uint8_t *pixels,
                                                        const mtl::TextureRef &image)
{
    ASSERT(image && image->valid());

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

    // NOTE(hqle): Support PBO
    ASSERT(!unpackBuffer);

    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, pixelsDepthPitch,
                                      decompressBuf.data(), dstRowPitch, dstDepthPitch);

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

        // Convert row by row:
        MTLRegion mtlRow   = mtlArea;
        mtlRow.size.height = mtlRow.size.depth = 1;
        for (NSUInteger d = 0; d < mtlArea.size.depth; ++d)
        {
            mtlRow.origin.z = mtlArea.origin.z + d;
            for (NSUInteger r = 0; r < mtlArea.size.height; ++r)
            {
                const uint8_t *psrc = pixels + d * pixelsDepthPitch + r * 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 if (mFormat.hasDepthOrStencilBits())
                {
                    ConvertDepthStencilData(mtlRow.size, pixelsAngleFormat, pixelsRowPitch, 0, psrc,
                                            dstFormat, nullptr, dstRowPitch, 0,
                                            conversionRow.data());
                }
                else
                {
                    CopyImageCHROMIUM(psrc, pixelsRowPitch, pixelsAngleFormat.pixelBytes, 0,
                                      pixelsAngleFormat.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, dstFormat, mtlRow, 0, slice,
                                                conversionRow.data(), dstRowPitch, 0, image));
            }
        }
    }  // if (mFormat.intendedAngleFormat().isBlock)

    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();

        uint32_t layers = texture->cubeFacesOrArrayLength();
        for (uint32_t layer = 0; layer < layers; ++layer)
        {
            for (uint32_t mip = 0; mip < mipmaps; ++mip)
            {
                gl::ImageIndex index = GetSliceMipIndex(texture, layer, mip);

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

angle::Result TextureMtl::initializeContents(const gl::Context *context,
                                             const gl::ImageIndex &index)
{
    if (index.isLayered())
    {
        // InitializeTextureContents is only able to initialize one layer at a time.
        const gl::ImageDesc &desc = mState.getImageDesc(index);
        uint32_t layerCount;
        if (index.isEntireLevelCubeMap())
        {
            layerCount = 6;
        }
        else
        {
            layerCount = desc.size.depth;
        }

        gl::ImageIndexIterator ite = index.getLayerIterator(layerCount);
        while (ite.hasNext())
        {
            gl::ImageIndex layerIndex = ite.next();
            ANGLE_TRY(initializeContents(context, layerIndex));
        }
        return angle::Result::Continue;
    }
    else if (index.getLayerCount() > 1)
    {
        for (int layer = 0; layer < index.getLayerCount(); ++layer)
        {
            int layerIdx = layer + index.getLayerIndex();
            gl::ImageIndex layerIndex =
                gl::ImageIndex::MakeFromType(index.getType(), index.getLevelIndex(), layerIdx);
            ANGLE_TRY(initializeContents(context, layerIndex));
        }
        return angle::Result::Continue;
    }

    ASSERT(index.getLayerCount() == 1 && !index.isLayered());
    ANGLE_TRY(ensureImageCreated(context, index));
    ContextMtl *contextMtl       = mtl::GetImpl(context);
    ImageDefinitionMtl &imageDef = getImageDefinition(index);
    const mtl::TextureRef &image = imageDef.image;
    const mtl::Format &format    = contextMtl->getPixelFormat(imageDef.formatID);
    return mtl::InitializeTextureContents(context, image, format, GetZeroLevelIndex(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;
    }

    const RenderTargetMtl &imageRtt = getRenderTarget(index);

    mtl::RenderCommandEncoder *cmdEncoder = contextMtl->getRenderTargetCommandEncoder(imageRtt);
    mtl::ColorBlitParams blitParams;

    blitParams.dstTextureSize = imageRtt.getTexture()->size(imageRtt.getLevelIndex());
    blitParams.dstRect        = gl::Rectangle(modifiedDestOffset.x, modifiedDestOffset.y,
                                       clippedSourceArea.width, clippedSourceArea.height);
    blitParams.dstScissorRect = blitParams.dstRect;

    blitParams.enabledBuffers.set(0);

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

    return displayMtl->getUtils().blitColorWithDraw(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 = getImage(index);
    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));

    gl::Rectangle srcRowArea = gl::Rectangle(clippedSourceArea.x, 0, clippedSourceArea.width, 1);
    MTLRegion mtlDstRowArea  = MTLRegionMake2D(modifiedDestOffset.x, 0, clippedSourceArea.width, 1);
    uint32_t dstSlice        = 0;
    switch (index.getType())
    {
        case gl::TextureType::_2D:
        case gl::TextureType::CubeMap:
            dstSlice = 0;
            break;
        case gl::TextureType::_3D:
            ASSERT(index.hasLayer());
            dstSlice               = 0;
            mtlDstRowArea.origin.z = index.getLayerIndex();
            break;
        default:
            UNREACHABLE();
    }

    // Copy row by row:
    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,
                                                 framebufferMtl->getColorReadRenderTarget(context),
                                                 conversionRow.data()));

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

    return angle::Result::Continue;
}

angle::Result TextureMtl::copySubTextureImpl(const gl::Context *context,
                                             const gl::ImageIndex &index,
                                             const gl::Offset &destOffset,
                                             const gl::InternalFormat &internalFormat,
                                             GLint 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(sourceLevel);

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

    ANGLE_TRY(ensureImageCreated(context, index));

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

    const ImageDefinitionMtl &srcImageDef  = sourceMtl->getImageDefinition(sourceIndex);
    const mtl::TextureRef &sourceImage     = srcImageDef.image;
    const mtl::Format &sourceFormat        = contextMtl->getPixelFormat(srcImageDef.formatID);
    const angle::Format &sourceAngleFormat = sourceFormat.actualAngleFormat();

    if (!mFormat.getCaps().isRenderable())
    {
        return copySubTextureCPU(context, index, destOffset, internalFormat, 0, sourceBox,
                                 sourceAngleFormat, unpackFlipY, unpackPremultiplyAlpha,
                                 unpackUnmultiplyAlpha, sourceImage);
    }
    return copySubTextureWithDraw(context, index, destOffset, internalFormat, 0, sourceBox,
                                  sourceAngleFormat, unpackFlipY, unpackPremultiplyAlpha,
                                  unpackUnmultiplyAlpha, sourceImage);
}

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 = getImage(index);
    ASSERT(image && image->valid());

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

    mtl::RenderCommandEncoder *cmdEncoder =
        contextMtl->getTextureRenderCommandEncoder(image, GetZeroLevelIndex(image));
    mtl::ColorBlitParams blitParams;

    blitParams.dstTextureSize = image->size();
    blitParams.dstRect =
        gl::Rectangle(destOffset.x, destOffset.y, sourceBox.width, sourceBox.height);
    blitParams.dstScissorRect = blitParams.dstRect;

    blitParams.enabledBuffers.set(0);

    blitParams.src      = sourceTexture;
    blitParams.srcLevel = sourceNativeLevel;
    blitParams.srcLayer = 0;
    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().blitColorWithDraw(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 = getImage(index);
    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, 0, mtlSrcArea, sourceNativeLevel, 0,
                            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, dstAngleFormat, mtlDstArea, 0, 0, conversionDst.data(),
                                    convRowPitch, 0, image));

    return angle::Result::Continue;
}

}
