//
// 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 <algorithm>
#include <initializer_list>

#include "common/Color.h"
#include "common/MemoryBuffer.h"
#include "common/debug.h"
#include "common/mathutil.h"
#include "common/unsafe_buffers.h"
#include "image_util/imageformats.h"
#include "image_util/loadimage.h"
#include "libANGLE/ErrorStrings.h"
#include "libANGLE/Surface.h"
#include "libANGLE/renderer/Format.h"
#include "libANGLE/renderer/metal/BufferMtl.h"
#include "libANGLE/renderer/metal/ContextMtl.h"
#include "libANGLE/renderer/metal/DisplayMtl.h"
#include "libANGLE/renderer/metal/FrameBufferMtl.h"
#include "libANGLE/renderer/metal/ImageMtl.h"
#include "libANGLE/renderer/metal/SamplerMtl.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 GetCubeOrArraySliceMipIndex(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();
}

// layer is ignored if texture type is not Cube or 2D array or 3D
gl::ImageIndex GetLayerMipIndex(const mtl::TextureRef &image, uint32_t layer, 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) + layer);
            return gl::ImageIndex::MakeCubeMapFace(cubeFace, level);
        }
        case MTLTextureType2DArray:
            return gl::ImageIndex::Make2DArray(level, layer);
        case MTLTextureType2DMultisample:
            return gl::ImageIndex::Make2DMultisample();
        case MTLTextureType3D:
            return gl::ImageIndex::Make3D(level, layer);
        default:
            UNREACHABLE();
            break;
    }

    return gl::ImageIndex();
}

GLuint GetImageLayerIndexFrom(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 GetImageCubeFaceIndexOrZeroFrom(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 CopyTextureData(const MTLSize &regionSize,
                     size_t srcRowPitch,
                     size_t src2DImageSize,
                     const uint8_t *psrc,
                     size_t destRowPitch,
                     size_t dest2DImageSize,
                     uint8_t *pdst)
{
    {
        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 =
                    ANGLE_UNSAFE_TODO(psrc + d * src2DImageSize + r * srcRowPitch);
                uint8_t *pCopyDst =
                    ANGLE_UNSAFE_TODO(pdst + d * dest2DImageSize + r * destRowPitch);
                ANGLE_UNSAFE_TODO(memcpy(pCopyDst, pCopySrc, rowCopySize));
            }
        }
    }
}

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 =
                    ANGLE_UNSAFE_TODO(psrc + d * src2DImageSize + r * srcRowPitch);
                uint8_t *pCopyDst =
                    ANGLE_UNSAFE_TODO(pdst + d * dest2DImageSize + r * destRowPitch);
                ANGLE_UNSAFE_TODO(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 =
                        ANGLE_UNSAFE_TODO(psrc + d * src2DImageSize + r * srcRowPitch +
                                          c * srcAngleFormat.pixelBytes);

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

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

mtl::BlitCommandEncoder *GetBlitCommandEncoderForResources(
    ContextMtl *contextMtl,
    const std::initializer_list<const mtl::Resource *> &resources)
{
    if (std::none_of(resources.begin(), resources.end(), [contextMtl](const mtl::Resource *res) {
            return res->hasPendingRenderWorks(contextMtl);
        }))
    {
        // If no resource has pending render works waiting to be submitted, then it's safe to
        // create a blit encoder without ending current render pass. The blit commands
        // will run before any pending render commands.
        return contextMtl->getBlitCommandEncoderWithoutEndingRenderEncoder();
    }
    return contextMtl->getBlitCommandEncoder();
}

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, &stagingBuffer));

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

    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 CopyTextureContentsToStagingBuffer(ContextMtl *contextMtl,
                                                 const angle::Format &textureAngleFormat,
                                                 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 * textureAngleFormat.pixelBytes;
    size_t stagingBuffer2DImageSize = stagingBufferRowPitch * regionSize.height;
    size_t stagingBufferSize        = stagingBuffer2DImageSize * regionSize.depth;
    mtl::BufferRef stagingBuffer;
    ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, stagingBufferSize, &stagingBuffer));

    uint8_t *pdst = stagingBuffer->map(contextMtl).data();
    CopyTextureData(regionSize, bytesPerRow, bytesPer2DImage, data, stagingBufferRowPitch,
                    stagingBuffer2DImageSize, pdst);

    stagingBuffer->unmap(contextMtl);

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

    return angle::Result::Continue;
}

angle::Result SaturateDepth(ContextMtl *contextMtl,
                            mtl::BufferRef srcBuffer,
                            mtl::BufferRef dstBuffer,
                            uint32_t srcBufferOffset,
                            uint32_t srcPitch,
                            MTLSize size)
{
    static_assert(gl::IMPLEMENTATION_MAX_2D_TEXTURE_SIZE <= UINT_MAX);
    mtl::DepthSaturationParams params;
    params.srcBuffer       = srcBuffer;
    params.dstBuffer       = dstBuffer;
    params.srcBufferOffset = srcBufferOffset;
    params.dstWidth        = static_cast<uint32_t>(size.width);
    params.dstHeight       = static_cast<uint32_t>(size.height);
    params.srcPitch        = srcPitch;
    ANGLE_TRY(contextMtl->getDisplay()->getUtils().saturateDepth(contextMtl, params));

    return angle::Result::Continue;
}

// This will copy a buffer to:
// - the respective level & slice of an original texture if the "dst" texture is a view.
// - the "dst" texture if it is not a view.
// Notes:
// - dstSlice is a slice in the "dst" texture not original texture.
// - dstLevel is a level in the "dst" texture not original texture.
// This function is needed because some GPUs such as the ones having AMD Bronze driver
// have a bug when copying a buffer to a view of a 3D texture.
void CopyBufferToOriginalTextureIfDstIsAView(ContextMtl *contextMtl,
                                             mtl::BlitCommandEncoder *blitEncoder,
                                             const mtl::BufferRef &src,
                                             size_t srcOffset,
                                             size_t srcBytesPerRow,
                                             size_t srcBytesPerImage,
                                             MTLSize srcSize,
                                             const mtl::TextureRef &dst,
                                             const uint32_t dstSlice,
                                             const mtl::MipmapNativeLevel &dstLevel,
                                             MTLOrigin dstOrigin,
                                             MTLBlitOption blitOption)
{
    mtl::TextureRef correctedTexture      = dst;
    mtl::MipmapNativeLevel correctedLevel = dstLevel;
    uint32_t correctedSlice               = dstSlice;
    // TODO(b/343734719): Simulator has bug in parentRelativeSlice() so skip this step
    // on simulator.
    if (!contextMtl->getDisplay()->isSimulator() && correctedTexture->parentTexture())
    {
        correctedLevel = correctedLevel + correctedTexture->parentRelativeLevel().get();
        correctedSlice += correctedTexture->parentRelativeSlice();
        correctedTexture = correctedTexture->parentTexture();
    }

    blitEncoder->copyBufferToTexture(src, srcOffset, srcBytesPerRow, srcBytesPerImage, srcSize,
                                     correctedTexture, correctedSlice, correctedLevel, dstOrigin,
                                     blitOption);
}

angle::Result UploadDepthStencilTextureContentsWithStagingBuffer(
    ContextMtl *contextMtl,
    const angle::Format &textureAngleFormat,
    MTLRegion region,
    const mtl::MipmapNativeLevel &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);

    // Depth and stencil textures cannot be of 3D type;
    // arrays and cube maps must be uploaded per-slice.
    ASSERT(region.size.depth == 1);

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

    if (textureAngleFormat.id == angle::FormatID::D32_FLOAT)
    {
        ANGLE_TRY(SaturateDepth(contextMtl, stagingBuffer, stagingBuffer, 0,
                                static_cast<uint32_t>(region.size.width), region.size));
    }

    // Copy staging buffer to texture.
    mtl::BlitCommandEncoder *encoder =
        GetBlitCommandEncoderForResources(contextMtl, {stagingBuffer.get(), texture.get()});

    CopyBufferToOriginalTextureIfDstIsAView(
        contextMtl, encoder, 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,
    const mtl::MipmapNativeLevel &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);

    // Depth and stencil textures cannot be of 3D type;
    // arrays and cube maps must be uploaded per-slice.
    ASSERT(region.size.depth == 1);

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

    if (angleStagingDepthFormat.id == angle::FormatID::D32_FLOAT)
    {
        ANGLE_TRY(SaturateDepth(contextMtl, stagingDepthBuffer, stagingDepthBuffer, 0,
                                static_cast<uint32_t>(region.size.width), region.size));
    }

    mtl::BlitCommandEncoder *encoder = GetBlitCommandEncoderForResources(
        contextMtl, {stagingDepthBuffer.get(), stagingStencilBuffer.get(), texture.get()});

    CopyBufferToOriginalTextureIfDstIsAView(
        contextMtl, encoder, stagingDepthBuffer, 0, stagingDepthBufferRowPitch,
        stagingDepthBuffer2DImageSize, region.size, texture, slice, mipmapLevel, region.origin,
        MTLBlitOptionDepthFromDepthStencil);
    CopyBufferToOriginalTextureIfDstIsAView(
        contextMtl, encoder, stagingStencilBuffer, 0, stagingStencilBufferRowPitch,
        stagingStencilBuffer2DImageSize, region.size, texture, slice, mipmapLevel, region.origin,
        MTLBlitOptionStencilFromDepthStencil);

    return angle::Result::Continue;
}

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

    angle::FormatID stagingBufferFormatID   = textureAngleFormat.id;
    const angle::Format &angleStagingFormat = angle::Format::Get(stagingBufferFormatID);

    size_t stagingBufferRowPitch;
    size_t stagingBuffer2DImageSize;
    mtl::BufferRef stagingBuffer;
    ANGLE_TRY(CopyTextureContentsToStagingBuffer(
        contextMtl, angleStagingFormat, region.size, data, bytesPerRow, bytesPer2DImage,
        &stagingBufferRowPitch, &stagingBuffer2DImageSize, &stagingBuffer));

    mtl::BlitCommandEncoder *encoder =
        GetBlitCommandEncoderForResources(contextMtl, {stagingBuffer.get(), texture.get()});

    CopyBufferToOriginalTextureIfDstIsAView(
        contextMtl, encoder, stagingBuffer, 0, stagingBufferRowPitch, stagingBuffer2DImageSize,
        region.size, texture, slice, mipmapLevel, region.origin, 0);

    return angle::Result::Continue;
}

angle::Result UploadTextureContents(const gl::Context *context,
                                    const angle::Format &textureAngleFormat,
                                    const MTLRegion &region,
                                    const mtl::MipmapNativeLevel &mipmapLevel,
                                    uint32_t slice,
                                    const uint8_t *data,
                                    size_t bytesPerRow,
                                    size_t bytesPer2DImage,
                                    bool avoidStagingBuffers,
                                    const mtl::TextureRef &texture)

{
    ASSERT(texture && texture->valid());
    ContextMtl *contextMtl       = mtl::GetImpl(context);
    const mtl::Format &mtlFormat = contextMtl->getPixelFormat(textureAngleFormat.id);

    bool preferGPUInitialization =
        !avoidStagingBuffers &&
        PreferStagedTextureUploads(context, texture, mtlFormat, mtl::StagingPurpose::Upload);
    if (texture->isCPUAccessible() && !preferGPUInitialization)
    {
        if (mtlFormat.isPVRTC())
        {
            // Replace Region Validation: rowBytes must be 0
            bytesPerRow = 0;
        }

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

        return angle::Result::Continue;
    }

    // Texture is not CPU accessible or staging is forced due to a workaround
    if (!textureAngleFormat.depthBits && !textureAngleFormat.stencilBits)
    {
        // Upload color data
        ANGLE_TRY(UploadTextureContentsWithStagingBuffer(contextMtl, textureAngleFormat, region,
                                                         mipmapLevel, slice, data, bytesPerRow,
                                                         bytesPer2DImage, texture));
    }
    else if (textureAngleFormat.depthBits && textureAngleFormat.stencilBits)
    {
        // Packed depth-stencil
        ANGLE_TRY(UploadPackedDepthStencilTextureContentsWithStagingBuffer(
            contextMtl, textureAngleFormat, region, mipmapLevel, slice, data, bytesPerRow,
            bytesPer2DImage, texture));
    }
    else
    {
        // Depth or stencil
        ANGLE_TRY(UploadDepthStencilTextureContentsWithStagingBuffer(
            contextMtl, textureAngleFormat, region, mipmapLevel, slice, data, bytesPerRow,
            bytesPer2DImage, texture));
    }

    return angle::Result::Continue;
}

// This might be unused on platform not supporting swizzle.
ANGLE_APPLE_UNUSED
GLenum OverrideSwizzleValue(const gl::Context *context,
                            GLenum swizzle,
                            const mtl::Format &format,
                            const gl::InternalFormat &glInternalFormat)
{
    if (format.actualAngleFormat().hasDepthOrStencilBits())
    {
        ASSERT(!format.swizzled);
        if (context->getState().getClientVersion() >= gl::ES_3_0 && glInternalFormat.sized)
        {
            // ES 3.1 spec: treat depth and stencil textures as red textures during sampling.
            if (swizzle == GL_GREEN || swizzle == GL_BLUE)
            {
                return GL_NONE;
            }
            else if (swizzle == GL_ALPHA)
            {
                return GL_ONE;
            }
        }
        else
        {
            // https://www.khronos.org/registry/OpenGL/extensions/OES/OES_depth_texture.txt
            // Treat depth texture as luminance texture during sampling.
            if (swizzle == GL_GREEN || swizzle == GL_BLUE)
            {
                return GL_RED;
            }
            else if (swizzle == GL_ALPHA)
            {
                return GL_ONE;
            }
        }
    }
    else if (format.swizzled)
    {
        // Combine the swizzles
        switch (swizzle)
        {
            case GL_RED:
                return format.swizzle[0];
            case GL_GREEN:
                return format.swizzle[1];
            case GL_BLUE:
                return format.swizzle[2];
            case GL_ALPHA:
                return format.swizzle[3];
            default:
                break;
        }
    }

    return swizzle;
}

mtl::TextureRef &GetLayerLevelTextureView(
    TextureMtl::LayerLevelTextureViewVector *layerLevelTextureViews,
    uint32_t layer,
    uint32_t level,
    uint32_t layerCount,
    uint32_t levelCount)
{
    // Lazily allocate the full layer and level count to not trigger any std::vector reallocations.
    if (layerLevelTextureViews->empty())
    {
        layerLevelTextureViews->resize(layerCount);
    }
    ASSERT(layerLevelTextureViews->size() > layer);

    TextureMtl::TextureViewVector &levelTextureViews = (*layerLevelTextureViews)[layer];

    if (levelTextureViews.empty())
    {
        levelTextureViews.resize(levelCount);
    }
    ASSERT(levelTextureViews.size() > level);

    return levelTextureViews[level];
}

}  // namespace

// TextureMtl::NativeTextureWrapper implementation.
// This class uses GL level instead of mtl::MipmapNativeLevel.
// It seamlessly translates GL level to native level based on the base GL information passed in the
// constructor. The base GL level is unchanged thoughout the lifetime of this object.
// Note that NativeTextureWrapper's base GL level doesn't necessarily mean it's the same as a GL
// texture's real base level.
// - If NativeTextureWrapper holds a native storage of a non-immutable texture,
// its base GL level is indeed equal to the GL texture's base level.
// - If NativeTextureWrapper holds a native storage of an immutable texture,
// it base GL level is actually 0.
// - If NativeTextureWrapper holds a view from base level to max level of a GL texture,
// then its base GL level is equal to the GL texture's base level.
class TextureMtl::NativeTextureWrapper : angle::NonCopyable
{
  public:
    NativeTextureWrapper(mtl::TextureRef texture, GLuint baseGLLevel, const mtl::Format &format)
        : mNativeTexture(std::move(texture)), mBaseGLLevel(baseGLLevel), mFormat(format)
    {
        ASSERT(mNativeTexture && mNativeTexture->valid());
    }

    operator const mtl::TextureRef &() const { return mNativeTexture; }
    const mtl::TextureRef &getNativeTexture() const { return mNativeTexture; }

    void replaceRegion(ContextMtl *context,
                       const MTLRegion &region,
                       GLuint glLevel,
                       uint32_t slice,
                       const uint8_t *data,
                       size_t bytesPerRow,
                       size_t bytesPer2DImage)
    {
        mNativeTexture->replaceRegion(context, region, getNativeLevel(glLevel), slice, data,
                                      bytesPerRow, bytesPer2DImage);
    }

    void getBytes(ContextMtl *context,
                  size_t bytesPerRow,
                  size_t bytesPer2DImage,
                  const MTLRegion &region,
                  GLuint glLevel,
                  uint32_t slice,
                  angle::Span<uint8_t> dataOut)
    {
        mNativeTexture->getBytes(context, bytesPerRow, bytesPer2DImage, region,
                                 getNativeLevel(glLevel), slice, dataOut);
    }

    const mtl::Format &getFormat() const { return mFormat; }
    GLuint getBaseGLLevel() const { return mBaseGLLevel; }
    // Get max addressable GL level that this texture supports.
    GLuint getMaxSupportedGLLevel() const { return mBaseGLLevel + mipmapLevels() - 1; }
    // Check whether a GL level refers to a valid mip in this texture.
    bool isGLLevelSupported(GLuint glLevel)
    {
        return glLevel >= mBaseGLLevel && glLevel <= getMaxSupportedGLLevel();
    }
    mtl::MipmapNativeLevel getNativeLevel(GLuint glLevel) const
    {
        return mtl::GetNativeMipLevel(glLevel, mBaseGLLevel);
    }
    GLuint getGLLevel(const mtl::MipmapNativeLevel &nativeLevel) const
    {
        return mtl::GetGLMipLevel(nativeLevel, mBaseGLLevel);
    }

    mtl::TextureRef getStencilView() { return mNativeTexture->getStencilView(); }

    MTLTextureType textureType() const { return mNativeTexture->textureType(); }
    MTLPixelFormat pixelFormat() const { return mNativeTexture->pixelFormat(); }

    uint32_t mipmapLevels() const { return mNativeTexture->mipmapLevels(); }
    uint32_t arrayLength() const { return mNativeTexture->arrayLength(); }
    uint32_t cubeFaces() const { return mNativeTexture->cubeFaces(); }
    uint32_t cubeFacesOrArrayLength() const { return mNativeTexture->cubeFacesOrArrayLength(); }

    uint32_t width(GLuint glLevel) const { return mNativeTexture->width(getNativeLevel(glLevel)); }
    uint32_t height(GLuint glLevel) const
    {
        return mNativeTexture->height(getNativeLevel(glLevel));
    }
    uint32_t depth(GLuint glLevel) const { return mNativeTexture->depth(getNativeLevel(glLevel)); }

    gl::Extents size(GLuint glLevel) const { return mNativeTexture->size(getNativeLevel(glLevel)); }

    // Get width, height, depth, size at base level.
    uint32_t widthAt0() const { return width(mBaseGLLevel); }
    uint32_t heightAt0() const { return height(mBaseGLLevel); }
    uint32_t depthAt0() const { return depth(mBaseGLLevel); }
    gl::Extents sizeAt0() const { return size(mBaseGLLevel); }

  protected:
    mtl::TextureRef mNativeTexture;
    const GLuint mBaseGLLevel;
    const mtl::Format mFormat;
};

// This class extends NativeTextureWrapper with support for view creation
class TextureMtl::NativeTextureWrapperWithViewSupport : public NativeTextureWrapper
{
  public:
    NativeTextureWrapperWithViewSupport(mtl::TextureRef texture,
                                        GLuint baseGLLevel,
                                        const mtl::Format &format)
        : NativeTextureWrapper(std::move(texture), baseGLLevel, format)
    {}

    // Create a view of one slice at a level.
    mtl::TextureRef createSliceMipView(uint32_t slice, GLuint glLevel)
    {
        return mNativeTexture->createSliceMipView(slice, getNativeLevel(glLevel));
    }
    // Create a levels range view
    mtl::TextureRef createMipsView(GLuint glLevel, uint32_t levels)
    {
        return mNativeTexture->createMipsView(getNativeLevel(glLevel), levels);
    }
    // Create a view of a level.
    mtl::TextureRef createMipView(GLuint glLevel)
    {
        return mNativeTexture->createMipView(getNativeLevel(glLevel));
    }
    // Create a view for a shader image binding.
    mtl::TextureRef createShaderImageView2D(GLuint glLevel, int layer, MTLPixelFormat format)
    {
        return mNativeTexture->createShaderImageView2D(getNativeLevel(glLevel), layer, format);
    }

    // Create a swizzled view
    mtl::TextureRef createMipsSwizzleView(GLuint glLevel,
                                          uint32_t levels,
                                          MTLPixelFormat format,
                                          const MTLTextureSwizzleChannels &swizzle)
    {
        return mNativeTexture->createMipsSwizzleView(getNativeLevel(glLevel), levels, format,
                                                     swizzle);
    }
};

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

TextureMtl::~TextureMtl() = default;

void TextureMtl::onDestroy(const gl::Context *context)
{
    deallocateNativeStorage(/*keepImages=*/false);
    mBoundSurface = nullptr;
}

void TextureMtl::deallocateNativeStorage(bool keepImages, bool keepSamplerStateAndFormat)
{

    if (!keepImages)
    {
        mTexImageDefs.clear();
        mShaderImageViews.clear();
    }
    else if (mNativeTextureStorage)
    {
        // Release native texture but keep its image definitions.
        retainImageDefinitions();
    }

    mNativeTextureStorage       = nullptr;
    mViewFromBaseToMaxLevel     = nullptr;
    mSwizzleStencilSamplingView = 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 &samplesMapRenderTargets : mRenderTargets)
    {
        for (RenderTargetMtl &perSampleCountRenderTarget : samplesMapRenderTargets.second)
        {
            perSampleCountRenderTarget.reset();
        }
    }

    for (auto &samplesMapMSTextures : mImplicitMSTextures)
    {
        for (mtl::TextureRef &perSampleCountMSTexture : samplesMapMSTextures.second)
        {
            perSampleCountMSTexture.reset();
        }
    }

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

    if (!keepSamplerStateAndFormat)
    {
        mMetalSamplerState = nil;
    }
}

GLuint TextureMtl::getStorageMipLevelCount(ImageMipLevels mipLevels) const
{
    switch (mipLevels)
    {
        // Only the contiguous levels from base to max that have actually been specified, i.e.
        // enabled. This avoids reserving memory for an entire mip pyramid when an application
        // uploads only level 0 and never calls glGenerateMipmap.
        case ImageMipLevels::EnabledLevels:
            return mState.getEnabledLevelCount();
        // The full chain from base to max level, regardless of which levels have been specified
        // (used by glGenerateMipmap).
        case ImageMipLevels::FullMipChainForGenerateMipmap:
            return mState.getMipmapMaxLevel() + 1 - mState.getEffectiveBaseLevel();
        default:
            UNREACHABLE();
            return 0;
    }
}

angle::Result TextureMtl::ensureNativeStorageCreated(const gl::Context *context,
                                                     bool keepImages,
                                                     ImageMipLevels mipLevels)
{
    auto clearImagesAssociatedWithStorage = [&]() {
        ASSERT(mNativeTextureStorage);
        GLuint mips      = mNativeTextureStorage->mipmapLevels();
        int numCubeFaces = static_cast<int>(mNativeTextureStorage->cubeFaces());
        for (int face = 0; face < numCubeFaces; ++face)
        {
            for (mtl::MipmapNativeLevel actualMip = mtl::kZeroNativeMipLevel;
                 actualMip.get() < mips; ++actualMip)
            {
                GLuint imageMipLevel = mNativeTextureStorage->getGLLevel(actualMip);
                mTexImageDefs[face][imageMipLevel].image = nullptr;
            }
        }
    };

    // Number of mip levels to allocate. By default only the levels that have actually been
    // specified are allocated; the rest of the mip chain is deferred until glGenerateMipmap (or a
    // subsequent upload of higher levels) requires it.
    GLuint mips = getStorageMipLevelCount(mipLevels);

    if (mNativeTextureStorage)
    {
        // If more mip levels are needed than are currently allocated (e.g. additional levels have
        // been specified since the storage was created, or glGenerateMipmap now requires the full
        // chain), recreate the storage with the larger level count while preserving the
        // already-populated image data.
        if (!isImmutableOrPBuffer() && mNativeTextureStorage->mipmapLevels() < mips)
        {
            deallocateNativeStorage(/*keepImages=*/true, /*keepSamplerStateAndFormat=*/true);
        }
        else
        {
            // Storage already has enough levels. Deallocate images associated with the storage.
            if (!keepImages)
            {
                clearImagesAssociatedWithStorage();
            }
            return angle::Result::Continue;
        }
    }

    // This should not be called from immutable texture.
    ASSERT(!isImmutableOrPBuffer());
    ASSERT(mState.getType() != gl::TextureType::_2DMultisample);
    ASSERT(mState.getType() != gl::TextureType::_2DMultisampleArray);

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

    // Create actual texture object:
    gl::ImageDesc desc = mState.getBaseLevelDesc();
    ANGLE_CHECK(contextMtl, desc.format.valid(), gl::err::kInternalError, GL_INVALID_OPERATION);
    angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(desc.format.info->sizedInternalFormat);
    mtl::Format format = contextMtl->getPixelFormat(angleFormatId);

    ANGLE_TRY(createNativeStorage(context, mState.getType(), mips, 0, desc.size, format));

    // Transfer data from defined images to actual texture object
    int numCubeFaces = static_cast<int>(mNativeTextureStorage->cubeFaces());
    for (int face = 0; face < numCubeFaces; ++face)
    {
        for (mtl::MipmapNativeLevel actualMip = mtl::kZeroNativeMipLevel; actualMip.get() < mips;
             ++actualMip)
        {
            GLuint imageMipLevel             = mNativeTextureStorage->getGLLevel(actualMip);
            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 = mNativeTextureStorage->size(imageMipLevel);
            if (imageToTransfer && imageToTransfer->sizeAt0() == actualMipSize &&
                imageToTransfer->arrayLength() == mNativeTextureStorage->arrayLength() &&
                imageToTransfer->pixelFormat() == mNativeTextureStorage->pixelFormat())
            {
                mtl::BlitCommandEncoder *encoder = GetBlitCommandEncoderForResources(
                    contextMtl,
                    {imageToTransfer.get(), mNativeTextureStorage->getNativeTexture().get()});

                encoder->copyTexture(imageToTransfer, 0, mtl::kZeroNativeMipLevel,
                                     *mNativeTextureStorage, face, actualMip,
                                     imageToTransfer->arrayLength(), 1);

                // Invalidate texture image definition at this index so that we can make it a
                // view of the native texture at this index later.
                imageToTransfer = nullptr;
            }
        }
    }

    if (!keepImages)
    {
        clearImagesAssociatedWithStorage();
    }

    return angle::Result::Continue;
}

angle::Result TextureMtl::createNativeStorage(const gl::Context *context,
                                              gl::TextureType type,
                                              GLuint mips,
                                              GLuint samples,
                                              const gl::Extents &size,
                                              const mtl::Format &format)
{
    ASSERT(samples == 0 || mips == 0);
    ContextMtl *contextMtl = mtl::GetImpl(context);

    // Create actual texture object:
    mSlices              = 1;
    bool allowFormatView =
        format.hasDepthAndStencilBits() ||
        needsFormatViewForPixelLocalStorage(
            contextMtl->getDisplay()->getNativePixelLocalStorageOptions(), format);
    mtl::TextureRef nativeTextureStorage;
    switch (type)
    {
        case gl::TextureType::_2D:
            ANGLE_TRY(mtl::Texture::Make2DTexture(contextMtl, format, size.width, size.height, mips,
                                                  /** renderTargetOnly */ false, allowFormatView,
                                                  &nativeTextureStorage));
            break;
        case gl::TextureType::CubeMap:
            mSlices = 6;
            ANGLE_TRY(mtl::Texture::MakeCubeTexture(contextMtl, format, size.width, mips,
                                                    /** renderTargetOnly */ false, allowFormatView,
                                                    &nativeTextureStorage));
            break;
        case gl::TextureType::_3D:
            ANGLE_TRY(mtl::Texture::Make3DTexture(
                contextMtl, format, size.width, size.height, size.depth, mips,
                /** renderTargetOnly */ false, allowFormatView, &nativeTextureStorage));
            break;
        case gl::TextureType::_2DArray:
            mSlices = size.depth;
            ANGLE_TRY(mtl::Texture::Make2DArrayTexture(
                contextMtl, format, size.width, size.height, mips, mSlices,
                /** renderTargetOnly */ false, allowFormatView, &nativeTextureStorage));
            break;
        case gl::TextureType::_2DMultisample:
            ANGLE_TRY(mtl::Texture::Make2DMSTexture(
                contextMtl, format, size.width, size.height, samples,
                /** renderTargetOnly */ false, allowFormatView, &nativeTextureStorage));
            break;
        default:
            UNREACHABLE();
    }

    if (mState.getImmutableFormat())
    {
        mNativeTextureStorage = std::make_unique<NativeTextureWrapperWithViewSupport>(
            std::move(nativeTextureStorage), /*baseGLLevel=*/0, format);
    }
    else
    {
        mNativeTextureStorage = std::make_unique<NativeTextureWrapperWithViewSupport>(
            std::move(nativeTextureStorage), /*baseGLLevel=*/mState.getEffectiveBaseLevel(),
            format);
    }
    ANGLE_TRY(initializeNowIfNeeded(context, format, *mNativeTextureStorage));

    ANGLE_TRY(createViewFromBaseToMaxLevel());

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

    ASSERT(mNativeTextureStorage);

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

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

    const mtl::Format &storageFormat = mNativeTextureStorage->getFormat();
    if (storageFormat.actualAngleFormat().depthBits && !storageFormat.getCaps().filterable)
    {
        // On devices not supporting filtering for depth textures, we need to convert to nearest
        // here.
        samplerDesc.setMinFilter(MTLSamplerMinMagFilterNearest);
        samplerDesc.setMagFilter(MTLSamplerMinMagFilterNearest);
        if (samplerDesc.getMipFilter() != MTLSamplerMipFilterNotMipmapped)
        {
            samplerDesc.setMipFilter(MTLSamplerMipFilterNearest);
        }

        samplerDesc.setMaxAnisotropy(1);
    }

    // OpenGL ES 3.x: The rules for texel selection are modified
    // for cube maps so that texture wrap modes are ignored.
    if ((mState.getType() == gl::TextureType::CubeMap ||
         mState.getType() == gl::TextureType::CubeMapArray) &&
        context->getState().getClientVersion() >= gl::ES_3_0)
    {
        samplerDesc.setRAddressMode(MTLSamplerAddressModeClampToEdge);
        samplerDesc.setSAddressMode(MTLSamplerAddressModeClampToEdge);
        samplerDesc.setTAddressMode(MTLSamplerAddressModeClampToEdge);
    }
    mMetalSamplerState = contextMtl->getDisplay()->getStateCache().getSamplerState(
        contextMtl->getMetalDevice(), samplerDesc);

    return angle::Result::Continue;
}

angle::Result TextureMtl::createViewFromBaseToMaxLevel()
{
    ASSERT(mNativeTextureStorage);
    uint32_t baseLevel =
        std::min(mNativeTextureStorage->getMaxSupportedGLLevel(), mState.getEffectiveBaseLevel());
    uint32_t maxLevel =
        std::min(mNativeTextureStorage->getMaxSupportedGLLevel(), mState.getEffectiveMaxLevel());

    // In edge case where base level > max level, clamp up to base level.
    maxLevel = std::max(maxLevel, baseLevel);

    mtl::TextureRef nativeViewFromBaseToMaxLevelRef;
    if (maxLevel == mNativeTextureStorage->getMaxSupportedGLLevel() &&
        baseLevel == mNativeTextureStorage->getBaseGLLevel())
    {
        // If base & max level are the same in mNativeTextureStorage, we don't need
        // a dedicated view. Furthermore, Intel driver has some bugs when sampling a view
        // of a stencil texture.
        nativeViewFromBaseToMaxLevelRef = mNativeTextureStorage->getNativeTexture();
    }
    else
    {
        uint32_t baseToMaxLevels = maxLevel - baseLevel + 1;
        nativeViewFromBaseToMaxLevelRef =
            mNativeTextureStorage->createMipsView(baseLevel, baseToMaxLevels);
    }

    mViewFromBaseToMaxLevel = std::make_unique<NativeTextureWrapper>(
        nativeViewFromBaseToMaxLevelRef, mState.getEffectiveBaseLevel(),
        mNativeTextureStorage->getFormat());

    // Recreate in bindToShader()
    mSwizzleStencilSamplingView = nullptr;
    return angle::Result::Continue;
}

angle::Result TextureMtl::onBaseMaxLevelsChanged(const gl::Context *context)
{
    if (!mNativeTextureStorage)
    {
        return angle::Result::Continue;
    }

    if (isImmutableOrPBuffer())
    {
        // For immutable texture, only recreate base-max view.
        ANGLE_TRY(createViewFromBaseToMaxLevel());
        // Invalidate base-max per level views so that they can be recreated
        // in generateMipmap()
        for (mtl::TextureRef &view : mLevelViewsWithinBaseMax)
        {
            view.reset();
        }
        return angle::Result::Continue;
    }

    // The native storage only needs to span the specified (enabled) levels. Compute the top GL
    // level it should currently cover; if the existing storage already covers at least that range
    // (and the base level matches), there is no need to recreate it. Using '<=' also preserves a
    // larger chain previously allocated by glGenerateMipmap and avoids redundant re-creation when
    // the max level is merely lowered.
    GLuint desiredTopGLLevel =
        mState.getEffectiveBaseLevel() + getStorageMipLevelCount(ImageMipLevels::EnabledLevels) - 1;

    if (mState.getEffectiveBaseLevel() == mNativeTextureStorage->getBaseGLLevel() &&
        desiredTopGLLevel <= mNativeTextureStorage->getMaxSupportedGLLevel())
    {
        ASSERT(mState.getBaseLevelDesc().size == mNativeTextureStorage->sizeAt0());
        // The existing storage already spans the required levels, so don't recreate it.
        // This might feel unnecessary at first since the front-end might prevent redundant base/max
        // level change already. However, there are cases that cause native storage to be created
        // before base/max level dirty bit is passed to Metal backend and lead to unwanted problems.
        // Example:
        // 1. texture with a non-default base/max level state is set.
        // 2. The texture is used first as a framebuffer attachment. This operation does not fully
        //    sync the texture state and therefore does not unset base/max level dirty bits.
        // 3. The same texture is then used for sampling; this operation fully syncs the texture
        //    state. Base/max level dirty bits may lead to recreating the texture storage thus
        //    invalidating native render target references created in step 2.
        // 4. If the framebuffer created in step 2 is used again, its native render target
        //    references will not be updated to point to the new storage because everything is in
        //    sync from the frontend point of view.
        // 5. Note: if the new range is different, it is expected that native render target
        //    references will be updated during draw framebuffer sync.
        //
        // The storage may cover more levels than are currently enabled (e.g. when
        // GL_TEXTURE_MAX_LEVEL is lowered, or a larger chain was previously allocated by
        // glGenerateMipmap). Recreate the base-max view so it spans only the current [base, max]
        // range; otherwise shaders could sample mip levels beyond GL_TEXTURE_MAX_LEVEL.
        ANGLE_TRY(createViewFromBaseToMaxLevel());
        // Invalidate base-max per level views so that they can be recreated in generateMipmap().
        for (mtl::TextureRef &view : mLevelViewsWithinBaseMax)
        {
            view.reset();
        }
        return angle::Result::Continue;
    }

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

    // We need to recreate a new native texture storage with number of levels = max level - base
    // level + 1. This can be achieved by simply deleting the old storage. The storage will be
    // lazily recreated later via ensureNativeStorageCreated().
    // Note: We release the native texture storage but keep old image definitions. So that when the
    // storage is recreated, its levels can be recreated with data from the old image definitions
    // respectively.
    deallocateNativeStorage(/*keepImages=*/true, /*keepSamplerStateAndFormat=*/true);

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

    return angle::Result::Continue;
}

angle::Result TextureMtl::ensureImageCreated(const gl::Context *context,
                                             const gl::ImageIndex &index)
{
    mtl::TextureRef &image = getImage(index);
    if (!image)
    {
        ContextMtl *contextMtl = mtl::GetImpl(context);

        // Image at this level hasn't been defined yet. We need to define it:
        const gl::ImageDesc &desc = mState.getImageDesc(index);
        angle::FormatID angleFormatId =
            angle::Format::InternalFormatToID(desc.format.info->sizedInternalFormat);
        mtl::Format format = contextMtl->getPixelFormat(angleFormatId);

        ANGLE_TRY(redefineImage(context, index, format, desc.size));
    }
    return angle::Result::Continue;
}

angle::Result TextureMtl::ensureLevelViewsWithinBaseMaxCreated()
{
    ASSERT(mViewFromBaseToMaxLevel);
    for (mtl::MipmapNativeLevel mip = mtl::kZeroNativeMipLevel;
         mip.get() < mViewFromBaseToMaxLevel->mipmapLevels(); ++mip)
    {
        if (mLevelViewsWithinBaseMax[mip])
        {
            continue;
        }

        GLuint mipGLLevel = mViewFromBaseToMaxLevel->getGLLevel(mip);

        if (mViewFromBaseToMaxLevel->textureType() != MTLTextureTypeCube &&
            mTexImageDefs[0][mipGLLevel].image)
        {
            // Reuse texture image view.
            mLevelViewsWithinBaseMax[mip] = mTexImageDefs[0][mipGLLevel].image;
        }
        else
        {
            mLevelViewsWithinBaseMax[mip] = mNativeTextureStorage->createMipView(mipGLLevel);
        }
    }
    return angle::Result::Continue;
}

mtl::TextureRef TextureMtl::createImageViewFromTextureStorage(GLuint cubeFaceOrZero, GLuint glLevel)
{
    mtl::TextureRef image;
    if (mNativeTextureStorage->textureType() == MTLTextureTypeCube)
    {
        // Cube texture's image is per face.
        image = mNativeTextureStorage->createSliceMipView(cubeFaceOrZero, glLevel);
    }
    else
    {
        if (mViewFromBaseToMaxLevel->isGLLevelSupported(glLevel))
        {
            mtl::MipmapNativeLevel nativeLevel = mViewFromBaseToMaxLevel->getNativeLevel(glLevel);
            if (mLevelViewsWithinBaseMax[nativeLevel])
            {
                // Reuse the native level view
                image = mLevelViewsWithinBaseMax[nativeLevel];
            }
        }

        if (!image)
        {
            image = mNativeTextureStorage->createMipView(glLevel);
        }
    }

    return image;
}

void TextureMtl::retainImageDefinitions()
{
    if (!mNativeTextureStorage)
    {
        return;
    }
    const GLuint mips = mNativeTextureStorage->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 (mtl::MipmapNativeLevel mip = mtl::kZeroNativeMipLevel; mip.get() < mips; ++mip)
        {
            GLuint imageMipLevel         = mNativeTextureStorage->getGLLevel(mip);
            ImageDefinitionMtl &imageDef = mTexImageDefs[face][imageMipLevel];
            if (imageDef.image)
            {
                continue;
            }
            imageDef.image    = createImageViewFromTextureStorage(face, imageMipLevel);
            imageDef.formatID = mNativeTextureStorage->getFormat().intendedFormatId;
        }
    }
}

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

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

    if (!imageDef.image && mNativeTextureStorage)
    {
        // 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 (!mNativeTextureStorage->isGLLevelSupported(imageIndex.getLevelIndex()))
        {
            // Image outside native texture's mip levels is skipped.
            return imageDef;
        }

        imageDef.image =
            createImageViewFromTextureStorage(cubeFaceOrZero, imageIndex.getLevelIndex());
        imageDef.formatID = mNativeTextureStorage->getFormat().intendedFormatId;
    }

    return imageDef;
}

angle::Result TextureMtl::getRenderTarget(ContextMtl *context,
                                          const gl::ImageIndex &imageIndex,
                                          GLsizei implicitSamples,
                                          RenderTargetMtl **renderTargetOut)
{
    ASSERT(imageIndex.getType() == gl::TextureType::_2D ||
           imageIndex.getType() == gl::TextureType::Rectangle ||
           imageIndex.getType() == gl::TextureType::_2DMultisample || imageIndex.hasLayer());

    const gl::RenderToTextureImageIndex renderToTextureIndex =
        implicitSamples <= 1
            ? gl::RenderToTextureImageIndex::Default
            : static_cast<gl::RenderToTextureImageIndex>(PackSampleCount(implicitSamples));

    ImageDefinitionMtl &imageDef = getImageDefinition(imageIndex);
    mtl::Format format           = context->getPixelFormat(imageDef.formatID);

    GLuint layer         = GetImageLayerIndexFrom(imageIndex);
    RenderTargetMtl &rtt = mRenderTargets[imageIndex][renderToTextureIndex];
    if (!rtt.getTexture())
    {
        // Lazy initialization of render target:
        if (imageDef.image)
        {
            if (imageIndex.getType() == gl::TextureType::CubeMap)
            {
                // Cube map is special, the image is already the view of its layer
                rtt.set(imageDef.image, mtl::kZeroNativeMipLevel, 0, format);
            }
            else
            {
                rtt.set(imageDef.image, mtl::kZeroNativeMipLevel, layer, format);
            }
        }
    }

    if (implicitSamples > 1 && !rtt.getImplicitMSTexture())
    {
        // This format must supports implicit resolve
        ANGLE_CHECK(context, format.getCaps().resolve, gl::err::kInternalError, GL_INVALID_VALUE);
        mtl::TextureRef &msTexture = mImplicitMSTextures[imageIndex][renderToTextureIndex];
        if (!msTexture)
        {
            const gl::ImageDesc &desc = mState.getImageDesc(imageIndex);
            ANGLE_TRY(mtl::Texture::MakeMemoryLess2DMSTexture(
                context, format, desc.size.width, desc.size.height, implicitSamples, &msTexture));
        }
        rtt.setImplicitMSTexture(msTexture);
    }

    *renderTargetOut = &rtt;

    return angle::Result::Continue;
}

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

    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::OwnImageIndex &ownIndex,
                                      const gl::Box &area,
                                      GLenum format,
                                      GLenum type,
                                      const gl::PixelUnpackState &unpack,
                                      gl::Buffer *unpackBuffer,
                                      const uint8_t *pixels)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();

    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::OwnImageIndex &ownIndex,
                                             GLenum internalFormat,
                                             const gl::Extents &size,
                                             const gl::PixelUnpackState &unpack,
                                             size_t imageSize,
                                             const uint8_t *pixels)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();

    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::OwnImageIndex &ownIndex,
                                                const gl::Box &area,
                                                GLenum format,
                                                const gl::PixelUnpackState &unpack,
                                                size_t imageSize,
                                                const uint8_t *pixels)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();

    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::OwnImageIndex &ownIndex,
                                    const gl::Rectangle &sourceArea,
                                    GLenum internalFormat,
                                    gl::Framebuffer *source)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();

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

    FramebufferMtl *srcFramebufferMtl = mtl::GetImpl(source);
    RenderTargetMtl *srcReadRT        = srcFramebufferMtl->getColorReadRenderTarget(context);
    RenderTargetMtl colorReadRT;
    if (srcReadRT)
    {
        // Need to duplicate RenderTargetMtl since the srcReadRT would be invalidated in
        // redefineImage(). This can happen if the source and this texture are the same texture.
        // Duplication ensures the copyImage() will be able to proceed even if the source texture
        // will be redefined.
        colorReadRT.duplicateFrom(*srcReadRT);
    }

    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() || context->isRobustResourceInitEnabled()) &&
        !fbRect.encloses(sourceArea))
    {
        ANGLE_TRY(initializeContents(context, GL_NONE, gl::OwnImageIndex(index)));
    }

    return copySubImageImpl(context, index, gl::Offset(0, 0, 0), sourceArea, internalFormatInfo,
                            srcFramebufferMtl, &colorReadRT);
}

angle::Result TextureMtl::copySubImage(const gl::Context *context,
                                       const gl::OwnImageIndex &ownIndex,
                                       const gl::Offset &destOffset,
                                       const gl::Rectangle &sourceArea,
                                       gl::Framebuffer *source)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();

    const gl::InternalFormat &currentFormat = *mState.getImageDesc(index).format.info;
    FramebufferMtl *srcFramebufferMtl       = mtl::GetImpl(source);
    RenderTargetMtl *colorReadRT            = srcFramebufferMtl->getColorReadRenderTarget(context);
    return copySubImageImpl(context, index, destOffset, sourceArea, currentFormat,
                            srcFramebufferMtl, colorReadRT);
}

angle::Result TextureMtl::copyTexture(const gl::Context *context,
                                      const gl::OwnImageIndex &ownIndex,
                                      GLenum internalFormat,
                                      GLenum type,
                                      gl::OwnLevel ownSourceLevel,
                                      bool unpackFlipY,
                                      bool unpackPremultiplyAlpha,
                                      bool unpackUnmultiplyAlpha,
                                      const gl::Texture *source)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();
    const uint32_t sourceLevel = ownSourceLevel.getUntranslated().get();

    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::OwnImageIndex &ownIndex,
                                         const gl::Offset &destOffset,
                                         gl::OwnLevel ownSourceLevel,
                                         const gl::Box &sourceBox,
                                         bool unpackFlipY,
                                         bool unpackPremultiplyAlpha,
                                         bool unpackUnmultiplyAlpha,
                                         const gl::Texture *source)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();
    const uint32_t sourceLevel = ownSourceLevel.getUntranslated().get();

    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, mState.getImmutableLevels(), 0, 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,
                                                   const void *imageCreateInfoPNext)
{
    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)
{
    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, 0, mState.getLevelZeroDesc().samples, mtlFormat, size);
}

angle::Result TextureMtl::setEGLImageTarget(const gl::Context *context,
                                            gl::TextureType type,
                                            egl::Image *image)
{
    deallocateNativeStorage(/*keepImages=*/false);

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

    ImageMtl *imageMtl = mtl::GetImpl(image);
    if (type != imageMtl->getImageTextureType())
    {
        return angle::Result::Stop;
    }

    const angle::FormatID angleFormatId =
        angle::Format::InternalFormatToID(image->getFormat().info->sizedInternalFormat);
    mtl::Format imageFormat = contextMtl->getPixelFormat(angleFormatId);

    mNativeTextureStorage = std::make_unique<NativeTextureWrapperWithViewSupport>(
        imageMtl->getTexture(), /*baseGLLevel=*/0, imageFormat);

    mSlices = mNativeTextureStorage->cubeFacesOrArrayLength();

    ANGLE_TRY(ensureSamplerStateCreated(context));
    ANGLE_TRY(createViewFromBaseToMaxLevel());

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

    return angle::Result::Continue;
}

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)
{
    // glGenerateMipmap needs the full mip chain allocated, expanding the storage if it was
    // previously allocated with only the specified levels.
    ANGLE_TRY(ensureNativeStorageCreated(context, /*keepImages=*/false,
                                         ImageMipLevels::FullMipChainForGenerateMipmap));

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

    const mtl::Format &format   = mNativeTextureStorage->getFormat();
    const mtl::FormatCaps &caps = format.getCaps();
    bool sRGB                   = format.actualInternalFormat().colorEncoding == GL_SRGB;

    bool avoidGPUPath =
        contextMtl->getDisplay()->getFeatures().forceNonCSBaseMipmapGeneration.enabled &&
        mViewFromBaseToMaxLevel->widthAt0() < 5;

    if (!avoidGPUPath && caps.writable && mState.getType() == gl::TextureType::_3D)
    {
        // http://anglebug.com/42263496.
        // Use compute for 3D mipmap generation.
        ANGLE_TRY(ensureLevelViewsWithinBaseMaxCreated());
        ANGLE_TRY(contextMtl->getDisplay()->getUtils().generateMipmapCS(
            contextMtl, *mViewFromBaseToMaxLevel, sRGB, &mLevelViewsWithinBaseMax));
    }
    else if (!avoidGPUPath && caps.filterable && caps.colorRenderable)
    {
        mtl::BlitCommandEncoder *blitEncoder = GetBlitCommandEncoderForResources(
            contextMtl, {mViewFromBaseToMaxLevel->getNativeTexture().get()});
        blitEncoder->generateMipmapsForTexture(*mViewFromBaseToMaxLevel);
    }
    else
    {
        ANGLE_TRY(generateMipmapCPU(context));
    }

    return angle::Result::Continue;
}

angle::Result TextureMtl::generateMipmapCPU(const gl::Context *context)
{
    ASSERT(mViewFromBaseToMaxLevel);

    ContextMtl *contextMtl           = mtl::GetImpl(context);
    const angle::Format &angleFormat = mViewFromBaseToMaxLevel->getFormat().actualAngleFormat();
    // This format must have mip generation function.
    ANGLE_CHECK(contextMtl, angleFormat.mipGenerationFunction, gl::err::kInternalError,
                GL_INVALID_OPERATION);

    for (uint32_t slice = 0; slice < mSlices; ++slice)
    {
        GLuint baseGLLevel = mViewFromBaseToMaxLevel->getBaseGLLevel();

        uint32_t prevLevelWidth    = mViewFromBaseToMaxLevel->widthAt0();
        uint32_t prevLevelHeight   = mViewFromBaseToMaxLevel->heightAt0();
        uint32_t prevLevelDepth    = mViewFromBaseToMaxLevel->depthAt0();
        size_t prevLevelRowPitch   = angleFormat.pixelBytes * prevLevelWidth;
        size_t prevLevelDepthPitch = prevLevelRowPitch * prevLevelHeight;
        angle::MemoryBuffer prevLevelData;
        ANGLE_CHECK_GL_ALLOC(contextMtl,
                             prevLevelData.resize(prevLevelDepthPitch * prevLevelDepth));
        angle::MemoryBuffer dstLevelData;

        // Download base level data
        mViewFromBaseToMaxLevel->getBytes(
            contextMtl, prevLevelRowPitch, prevLevelDepthPitch,
            MTLRegionMake3D(0, 0, 0, prevLevelWidth, prevLevelHeight, prevLevelDepth), baseGLLevel,
            slice, prevLevelData.span());

        for (GLuint mip = 1; mip < mViewFromBaseToMaxLevel->mipmapLevels(); ++mip)
        {
            GLuint glLevel     = baseGLLevel + mip;
            uint32_t dstWidth  = mViewFromBaseToMaxLevel->width(glLevel);
            uint32_t dstHeight = mViewFromBaseToMaxLevel->height(glLevel);
            uint32_t dstDepth  = mViewFromBaseToMaxLevel->depth(glLevel);

            size_t dstRowPitch   = angleFormat.pixelBytes * dstWidth;
            size_t dstDepthPitch = dstRowPitch * dstHeight;
            size_t dstDataSize   = dstDepthPitch * dstDepth;
            ANGLE_CHECK_GL_ALLOC(contextMtl, dstLevelData.resize(dstDataSize));

            // Generate mip level
            angleFormat.mipGenerationFunction(prevLevelWidth, prevLevelHeight, prevLevelDepth,
                                              prevLevelData.data(), prevLevelRowPitch,
                                              prevLevelDepthPitch, dstLevelData.data(), dstRowPitch,
                                              dstDepthPitch);

            mtl::MipmapNativeLevel nativeLevel = mViewFromBaseToMaxLevel->getNativeLevel(glLevel);

            // Upload to texture
            ANGLE_TRY(UploadTextureContents(context, angleFormat,
                                            MTLRegionMake3D(0, 0, 0, dstWidth, dstHeight, dstDepth),
                                            nativeLevel, slice, dstLevelData.data(), dstRowPitch,
                                            dstDepthPitch, false, *mViewFromBaseToMaxLevel));

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

    }  // For layers

    return angle::Result::Continue;
}

bool TextureMtl::needsFormatViewForPixelLocalStorage(const ShPixelLocalStorageOptions &plsOptions,
                                                     const mtl::Format &format) const
{
    // On iOS devices with GPU family 5 and later, Metal doesn't apply lossless compression to
    // the texture if we set MTLTextureUsagePixelFormatView. This shouldn't be a problem though
    // because iOS devices implement pixel local storage with framebuffer fetch instead of shader
    // images.
    if (plsOptions.type == ShPixelLocalStorageType::ImageLoadStore)
    {
        switch (format.metalFormat)
        {
            case MTLPixelFormatRGBA8Unorm:
            case MTLPixelFormatRGBA8Uint:
            case MTLPixelFormatRGBA8Sint:
                return !plsOptions.supportsNativeRGBA8ImageFormats;
            default:
                break;
        }
    }
    return false;
}

bool TextureMtl::isImmutableOrPBuffer() const
{
    return mState.getImmutableFormat() || mBoundSurface;
}

angle::Result TextureMtl::setBaseLevel(const gl::Context *context, GLuint baseLevel)
{
    return onBaseMaxLevelsChanged(context);
}

angle::Result TextureMtl::bindTexImage(const gl::Context *context, egl::Surface *surface)
{
    deallocateNativeStorage(/*keepImages=*/false);

    mBoundSurface         = surface;
    auto pBuffer          = GetImplAs<OffscreenSurfaceMtl>(surface);
    mtl::Format pbufferFormat = pBuffer->getColorFormat();
    mNativeTextureStorage     = std::make_unique<NativeTextureWrapperWithViewSupport>(
        pBuffer->getColorTexture(), /*baseGLLevel=*/0, pbufferFormat);
    mSlices = mNativeTextureStorage->cubeFacesOrArrayLength();

    ANGLE_TRY(ensureSamplerStateCreated(context));
    ANGLE_TRY(createViewFromBaseToMaxLevel());

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

    return angle::Result::Continue;
}

angle::Result TextureMtl::releaseTexImage(const gl::Context *context)
{
    deallocateNativeStorage(/*keepImages=*/false);
    mBoundSurface = nullptr;
    return angle::Result::Continue;
}

angle::Result TextureMtl::getAttachmentRenderTarget(const gl::Context *context,
                                                    GLenum binding,
                                                    const gl::OwnImageIndex &ownImageIndex,
                                                    GLsizei samples,
                                                    FramebufferAttachmentRenderTarget **rtOut)
{
    const gl::ImageIndex imageIndex = ownImageIndex.getUntranslated();

    ANGLE_TRY(
        ensureNativeStorageCreated(context, /*keepImages=*/true, ImageMipLevels::EnabledLevels));

    ContextMtl *contextMtl = mtl::GetImpl(context);
    ANGLE_CHECK(contextMtl, mNativeTextureStorage, gl::err::kInternalError, GL_INVALID_OPERATION);

    RenderTargetMtl *rtt;
    ANGLE_TRY(getRenderTarget(contextMtl, imageIndex, samples, &rtt));

    *rtOut = rtt;

    return angle::Result::Continue;
}

angle::Result TextureMtl::syncState(const gl::Context *context,
                                    const gl::Texture::DirtyBits &dirtyBits,
                                    gl::Command source)
{
    ContextMtl *contextMtl = mtl::GetImpl(context);
    for (size_t dirtyBit : dirtyBits)
    {
        switch (dirtyBit)
        {
            case gl::Texture::DIRTY_BIT_COMPARE_MODE:
            case gl::Texture::DIRTY_BIT_COMPARE_FUNC:
                // Tell context to rebind textures so that ProgramMtl has a chance to verify
                // depth texture compare mode.
                contextMtl->invalidateCurrentTextures();
                // fall through
                OS_FALLTHROUGH;
            case gl::Texture::DIRTY_BIT_MIN_FILTER:
            case gl::Texture::DIRTY_BIT_MAG_FILTER:
            case gl::Texture::DIRTY_BIT_WRAP_S:
            case gl::Texture::DIRTY_BIT_WRAP_T:
            case gl::Texture::DIRTY_BIT_WRAP_R:
            case gl::Texture::DIRTY_BIT_MAX_ANISOTROPY:
            case gl::Texture::DIRTY_BIT_MIN_LOD:
            case gl::Texture::DIRTY_BIT_MAX_LOD:
            case gl::Texture::DIRTY_BIT_SRGB_DECODE:
            case gl::Texture::DIRTY_BIT_BORDER_COLOR:
                // Recreate sampler state
                mMetalSamplerState = nil;
                break;
            case gl::Texture::DIRTY_BIT_MAX_LEVEL:
            case gl::Texture::DIRTY_BIT_BASE_LEVEL:
                ANGLE_TRY(onBaseMaxLevelsChanged(context));
                break;
            case gl::Texture::DIRTY_BIT_SWIZZLE_RED:
            case gl::Texture::DIRTY_BIT_SWIZZLE_GREEN:
            case gl::Texture::DIRTY_BIT_SWIZZLE_BLUE:
            case gl::Texture::DIRTY_BIT_SWIZZLE_ALPHA:
            case gl::Texture::DIRTY_BIT_DEPTH_STENCIL_TEXTURE_MODE:
            {
                // Recreate swizzle/stencil view.
                mSwizzleStencilSamplingView = nullptr;
            }
            break;
            default:
                break;
        }
    }

    ANGLE_TRY(
        ensureNativeStorageCreated(context, /*keepImages=*/true, ImageMipLevels::EnabledLevels));
    ANGLE_TRY(ensureSamplerStateCreated(context));

    return angle::Result::Continue;
}

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

    float minLodClamp;
    float maxLodClamp;
    id<MTLSamplerState> samplerState;

    if (!mSwizzleStencilSamplingView)
    {
        ContextMtl *contextMtl             = mtl::GetImpl(context);
        const angle::FeaturesMtl &features = contextMtl->getDisplay()->getFeatures();
        const mtl::Format &format          = mNativeTextureStorage->getFormat();

        // Sampling from unused channels of depth and stencil textures is undefined in Metal.
        // ANGLE relies on swizzled views to enforce values required by OpenGL ES specs. Some
        // drivers fail to sample from a swizzled view of a stencil texture so skip this step.
        const bool skipStencilSwizzle = format.actualFormatId == angle::FormatID::S8_UINT &&
                                        features.avoidStencilTextureSwizzle.enabled;
        if (!skipStencilSwizzle &&
            (mState.getSwizzleState().swizzleRequired() ||
             format.actualAngleFormat().hasDepthOrStencilBits() || format.swizzled) &&
            features.hasTextureSwizzle.enabled)
        {
            const gl::InternalFormat &glInternalFormat = *mState.getBaseLevelDesc().format.info;

            MTLTextureSwizzleChannels swizzle = MTLTextureSwizzleChannelsMake(
                mtl::GetTextureSwizzle(OverrideSwizzleValue(
                    context, mState.getSwizzleState().swizzleRed, format, glInternalFormat)),
                mtl::GetTextureSwizzle(OverrideSwizzleValue(
                    context, mState.getSwizzleState().swizzleGreen, format, glInternalFormat)),
                mtl::GetTextureSwizzle(OverrideSwizzleValue(
                    context, mState.getSwizzleState().swizzleBlue, format, glInternalFormat)),
                mtl::GetTextureSwizzle(OverrideSwizzleValue(
                    context, mState.getSwizzleState().swizzleAlpha, format, glInternalFormat)));

            MTLPixelFormat pixelFormat = mViewFromBaseToMaxLevel->pixelFormat();
            if (mState.isStencilMode())
            {
                if (pixelFormat == MTLPixelFormatDepth32Float_Stencil8)
                {
                    pixelFormat = MTLPixelFormatX32_Stencil8;
                }
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
                else if (pixelFormat == MTLPixelFormatDepth24Unorm_Stencil8)
                {
                    pixelFormat = MTLPixelFormatX24_Stencil8;
                }
#endif
            }

            mSwizzleStencilSamplingView = mNativeTextureStorage->createMipsSwizzleView(
                mViewFromBaseToMaxLevel->getBaseGLLevel(), mViewFromBaseToMaxLevel->mipmapLevels(),
                pixelFormat, swizzle);
        }
        else
        {
            mSwizzleStencilSamplingView = mState.isStencilMode()
                                              ? mViewFromBaseToMaxLevel->getStencilView()
                                              : mViewFromBaseToMaxLevel->getNativeTexture();
        }
    }

    if (!sampler)
    {
        samplerState = mMetalSamplerState;
        minLodClamp  = mState.getSamplerState().getMinLod();
        maxLodClamp  = mState.getSamplerState().getMaxLod();
    }
    else
    {
        SamplerMtl *samplerMtl = mtl::GetImpl(sampler);
        samplerState           = samplerMtl->getSampler(mtl::GetImpl(context));
        minLodClamp            = sampler->getSamplerState().getMinLod();
        maxLodClamp            = sampler->getSamplerState().getMaxLod();
    }

    minLodClamp = std::max(minLodClamp, 0.f);

    cmdEncoder->setTexture(shaderType, mSwizzleStencilSamplingView, textureSlotIndex);
    cmdEncoder->setSamplerState(shaderType, samplerState, minLodClamp, maxLodClamp,
                                samplerSlotIndex);

    return angle::Result::Continue;
}

angle::Result TextureMtl::bindToShaderImage(const gl::Context *context,
                                            mtl::RenderCommandEncoder *cmdEncoder,
                                            gl::ShaderType shaderType,
                                            int textureSlotIndex,
                                            int level,
                                            int layer,
                                            GLenum format)
{
    ASSERT(mNativeTextureStorage);
    ASSERT(mState.getImmutableFormat());
    ASSERT(0 <= level && static_cast<uint32_t>(level) < mState.getImmutableLevels());
    ASSERT(0 <= layer && static_cast<uint32_t>(layer) < mSlices);

    ContextMtl *contextMtl        = mtl::GetImpl(context);
    angle::FormatID angleFormatId = angle::Format::InternalFormatToID(format);
    mtl::Format imageAccessFormat = contextMtl->getPixelFormat(angleFormatId);
    LayerLevelTextureViewVector &textureViewVector =
        mShaderImageViews[imageAccessFormat.metalFormat];
    mtl::TextureRef &textureRef = GetLayerLevelTextureView(&textureViewVector, layer, level,
                                                           mSlices, mState.getImmutableLevels());

    if (textureRef == nullptr)
    {
        textureRef = mNativeTextureStorage->createShaderImageView2D(level, layer,
                                                                    imageAccessFormat.metalFormat);
    }

    cmdEncoder->setRWTexture(shaderType, textureRef, textureSlotIndex);
    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 imageWithinNativeStorageLevels = false;
    if (mNativeTextureStorage && mNativeTextureStorage->isGLLevelSupported(index.getLevelIndex()))
    {
        imageWithinNativeStorageLevels = true;
        GLuint glLevel                 = index.getLevelIndex();
        // 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.
        ASSERT(mNativeTextureStorage->textureType() == mtl::GetTextureType(index.getType()));
        if (mNativeTextureStorage->getFormat() != mtlFormat ||
            size != mNativeTextureStorage->size(glLevel))
        {
            // Keep other images
            deallocateNativeStorage(/*keepImages=*/true);
        }
    }

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

    ContextMtl *contextMtl = mtl::GetImpl(context);
    ImageDefinitionMtl &imageDef = getImageDefinition(index);

    // If native texture still exists, it means the size hasn't been changed, no need to create new
    // image
    if (mNativeTextureStorage && imageWithinNativeStorageLevels)
    {
        if (imageDef.image &&
            imageDef.image->textureType() ==
                mtl::GetTextureType(GetTextureImageType(index.getType())) &&
            imageDef.formatID == mNativeTextureStorage->getFormat().intendedFormatId &&
            imageDef.image->sizeAt0() == size)
        {
            // Keep it! (No-op)
        }
        else
        {
            // Recreate the view at this index.
            imageDef.image = nullptr;
            imageDef       = getImageDefinition(index);
        }
    }
    else
    {
        imageDef.formatID    = mtlFormat.intendedFormatId;
        bool allowFormatView =
            mtlFormat.hasDepthAndStencilBits() ||
            needsFormatViewForPixelLocalStorage(
                contextMtl->getDisplay()->getNativePixelLocalStorageOptions(), mtlFormat);
        // 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, &imageDef.image));
                break;
            case gl::TextureType::_3D:
                ANGLE_TRY(mtl::Texture::Make3DTexture(
                    contextMtl, mtlFormat, size.width, size.height, size.depth, 1,
                    /** renderTargetOnly */ false, allowFormatView, &imageDef.image));
                break;
            case gl::TextureType::_2DArray:
                ANGLE_TRY(mtl::Texture::Make2DArrayTexture(
                    contextMtl, mtlFormat, size.width, size.height, 1, size.depth,
                    /** renderTargetOnly */ false, allowFormatView, &imageDef.image));
                break;
            default:
                UNREACHABLE();
        }

        // Make sure emulated channels are properly initialized in this newly allocated texture.
        ANGLE_TRY(initializeNowIfNeeded(context, mtlFormat, imageDef.image));
    }

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

    return angle::Result::Continue;
}

angle::Result TextureMtl::setStorageImpl(const gl::Context *context,
                                         gl::TextureType type,
                                         GLuint mips,
                                         GLuint samples,
                                         const mtl::Format &mtlFormat,
                                         const gl::Extents &size)
{
    // Don't need to hold old images data.
    deallocateNativeStorage(/*keepImages=*/false);

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

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

    ANGLE_TRY(createNativeStorage(context, type, mips, samples, size, mtlFormat));
    ANGLE_TRY(createViewFromBaseToMaxLevel());

    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)
{
    if (!oriPixels && !unpackBuffer)
    {
        return angle::Result::Continue;
    }

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

    ANGLE_TRY(ensureImageCreated(context, index));
    ImageDefinitionMtl &imageDef = getImageDefinition(index);

    GLuint sourceRowPitch   = 0;
    GLuint sourceDepthPitch = 0;
    GLuint sourceSkipBytes  = 0;
    ANGLE_CHECK_GL_MATH(contextMtl, formatInfo.computeRowDepthSkipBytes(
                                        type, area.width, area.height, unpack, index.usesTex3D(),
                                        &sourceRowPitch, &sourceDepthPitch, &sourceSkipBytes));

    // Get corresponding source data's ANGLE format
    angle::FormatID srcAngleFormatId;
    if (formatInfo.sizedInternalFormat == GL_DEPTH_COMPONENT24)
    {
        // GL_DEPTH_COMPONENT24 is special case, its supplied data is 32 bit depth.
        srcAngleFormatId = angle::FormatID::D32_UNORM;
    }
    else
    {
        srcAngleFormatId = angle::Format::InternalFormatToID(formatInfo.sizedInternalFormat);
    }
    const angle::Format &srcAngleFormat = angle::Format::Get(srcAngleFormatId);

    const uint8_t *usablePixels = ANGLE_UNSAFE_TODO(oriPixels + sourceSkipBytes);

    // Upload to texture
    if (index.getType() == gl::TextureType::_2DArray)
    {
        // OpenGL unifies texture array and texture 3d's box area by using z & depth as array start
        // index & length for texture array. However, Metal treats them differently. We need to
        // handle them in separate code.
        MTLRegion mtlRegion = MTLRegionMake3D(area.x, area.y, 0, area.width, area.height, 1);

        for (int slice = 0; slice < area.depth; ++slice)
        {
            int sliceIndex           = slice + area.z;
            const uint8_t *srcPixels = ANGLE_UNSAFE_TODO(usablePixels + slice * sourceDepthPitch);
            ANGLE_TRY(setPerSliceSubImage(context, sliceIndex, mtlRegion, formatInfo, type,
                                          srcAngleFormat, sourceRowPitch, sourceDepthPitch,
                                          unpackBuffer, srcPixels, imageDef));
        }
    }
    else
    {
        MTLRegion 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,
                                      imageDef));
    }

    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 ImageDefinitionMtl &imageDef)
{
    // If source pixels are luminance or RGB8, we need to convert them to RGBA

    ContextMtl *contextMtl  = mtl::GetImpl(context);
    mtl::Format imageFormat = contextMtl->getPixelFormat(imageDef.formatID);

    if (imageFormat.needConversion(pixelsAngleFormat.id))
    {
        return convertAndSetPerSliceSubImage(context, slice, mtlArea, internalFormat, type,
                                             pixelsAngleFormat, pixelsRowPitch, pixelsDepthPitch,
                                             unpackBuffer, pixels, imageDef);
    }

    // No conversion needed.
    if (unpackBuffer)
    {
        uintptr_t offset = reinterpret_cast<uintptr_t>(pixels);
        GLuint minRowPitch;
        ANGLE_CHECK_GL_MATH(contextMtl, internalFormat.computeRowPitch(
                                            type, static_cast<GLint>(mtlArea.size.width),
                                            /** aligment */ 1, /** rowLength */ 0, &minRowPitch));
        if (offset % imageFormat.actualAngleFormat().pixelBytes || pixelsRowPitch < minRowPitch)
        {
            // offset is not divisible by pixelByte or the source row pitch is smaller than minimum
            // row pitch, use convertAndSetPerSliceSubImage() function.
            return convertAndSetPerSliceSubImage(context, slice, mtlArea, internalFormat, type,
                                                 pixelsAngleFormat, pixelsRowPitch,
                                                 pixelsDepthPitch, unpackBuffer, pixels, imageDef);
        }

        BufferMtl *unpackBufferMtl = mtl::GetImpl(unpackBuffer);

        if (imageFormat.hasDepthAndStencilBits())
        {
            // NOTE(hqle): packed depth & stencil texture cannot copy from buffer directly, needs
            // to split its depth & stencil data and copy separately.
            const uint8_t *clientData =
                unpackBufferMtl->getBufferDataReadOnly(contextMtl, offset).data();
            ANGLE_TRY(UploadTextureContents(
                context, imageFormat.actualAngleFormat(), mtlArea, mtl::kZeroNativeMipLevel, slice,
                clientData, pixelsRowPitch, pixelsDepthPitch, false, imageDef.image));
        }
        else
        {
            mtl::BufferRef sourceBuffer = unpackBufferMtl->getCurrentBuffer();
            // PVRTC1 blocks are stored in a reflected Morton order
            // and need to be linearized for buffer uploads in Metal.
            // This step is skipped for textures that have only one block.
            if (imageFormat.isPVRTC() && mtlArea.size.height > 4)
            {
                // PVRTC1 inherent requirement.
                ASSERT(gl::isPow2(mtlArea.size.width) && gl::isPow2(mtlArea.size.height));
                // Metal-specific limitation enforced by ANGLE validation.
                ASSERT(mtlArea.size.width == mtlArea.size.height);
                static_assert(gl::IMPLEMENTATION_MAX_2D_TEXTURE_SIZE <= 262144,
                              "The current kernel can handle up to 65536 blocks per dimension.");

                // Current command buffer implementation does not support 64-bit offsets.
                ANGLE_CHECK_GL_MATH(contextMtl, offset <= std::numeric_limits<uint32_t>::max());

                mtl::BufferRef stagingBuffer;
                ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, pixelsDepthPitch, &stagingBuffer));

                mtl::BlockLinearizationParams params;
                params.srcBuffer       = sourceBuffer;
                params.dstBuffer       = stagingBuffer;
                params.srcBufferOffset = static_cast<uint32_t>(offset);
                params.blocksWide =
                    static_cast<GLuint>(mtlArea.size.width) / internalFormat.compressedBlockWidth;
                params.blocksHigh =
                    static_cast<GLuint>(mtlArea.size.height) / internalFormat.compressedBlockHeight;

                // PVRTC1 textures always have at least 2 blocks in each dimension.
                // Enforce correct block layout for 8x8 textures that use 8x4 blocks.
                params.blocksWide = std::max(params.blocksWide, 2u);

                ANGLE_TRY(contextMtl->getDisplay()->getUtils().linearizeBlocks(contextMtl, params));

                sourceBuffer = stagingBuffer;
                offset       = 0;
            }
            else if (pixelsAngleFormat.id == angle::FormatID::D32_FLOAT)
            {
                // Current command buffer implementation does not support 64-bit offsets.
                ANGLE_CHECK_GL_MATH(contextMtl, offset <= std::numeric_limits<uint32_t>::max());
                size_t imageSize = pixelsRowPitch * mtlArea.size.height;
                mtl::BufferRef stagingBuffer;
                ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, imageSize, &stagingBuffer));

                ASSERT(pixelsAngleFormat.pixelBytes == 4 && offset % 4 == 0);
                ANGLE_TRY(SaturateDepth(contextMtl, sourceBuffer, stagingBuffer,
                                        static_cast<uint32_t>(offset),
                                        static_cast<uint32_t>(pixelsRowPitch) / 4, mtlArea.size));

                sourceBuffer = stagingBuffer;
                offset       = 0;
            }

            size_t srcBytesPerImage = mtlArea.size.depth > 1 ? pixelsDepthPitch : 0;

            // Use blit encoder to copy
            mtl::BlitCommandEncoder *blitEncoder = GetBlitCommandEncoderForResources(
                contextMtl, {sourceBuffer.get(), imageDef.image.get()});
            CopyBufferToOriginalTextureIfDstIsAView(
                contextMtl, blitEncoder, sourceBuffer, offset, pixelsRowPitch, srcBytesPerImage,
                mtlArea.size, imageDef.image, slice, mtl::kZeroNativeMipLevel, mtlArea.origin,
                imageFormat.isPVRTC() ? MTLBlitOptionRowLinearPVRTC : MTLBlitOptionNone);
        }
    }
    else
    {
        ANGLE_TRY(UploadTextureContents(context, imageFormat.actualAngleFormat(), mtlArea,
                                        mtl::kZeroNativeMipLevel, slice, pixels, pixelsRowPitch,
                                        pixelsDepthPitch, false, imageDef.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 ImageDefinitionMtl &imageDef)
{
    ASSERT(imageDef.image && imageDef.image->valid());

    ContextMtl *contextMtl = mtl::GetImpl(context);
    mtl::Format imageFormat = contextMtl->getPixelFormat(imageDef.formatID);

    if (unpackBuffer)
    {
        ANGLE_CHECK_GL_MATH(contextMtl, reinterpret_cast<uintptr_t>(pixels) <=
                                            std::numeric_limits<uint32_t>::max());

        uint32_t offset = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(pixels));

        BufferMtl *unpackBufferMtl = mtl::GetImpl(unpackBuffer);
        if (!imageFormat.getCaps().isRenderable() || imageFormat.hasDepthOrStencilBits() ||
            imageFormat.intendedAngleFormat().isBlock ||
            !contextMtl->getDisplay()->getUtils().isPixelsUnpackSupported(pixelsAngleFormat))
        {
            // Unsupported format, use CPU path.
            const uint8_t *clientData =
                unpackBufferMtl->getBufferDataReadOnly(contextMtl, offset).data();
            ANGLE_TRY(convertAndSetPerSliceSubImage(
                context, slice, mtlArea, internalFormat, type, pixelsAngleFormat, pixelsRowPitch,
                pixelsDepthPitch, nullptr, clientData, imageDef));
        }
        else
        {
            // Use shaders
            mtl::CopyPixelsFromBufferParams params;
            params.buffer            = unpackBufferMtl->getCurrentBuffer();
            params.bufferStartOffset = offset;
            params.bufferRowPitch    = static_cast<uint32_t>(pixelsRowPitch);
            params.bufferDepthPitch  = static_cast<uint32_t>(pixelsDepthPitch);
            params.texture           = imageDef.image;
            params.textureArea       = mtl::MTLRegionToGLRect(mtlArea);

            // If texture is not array, slice must be zero, if texture is array, mtlArea.origin.z
            // must be zero.
            // This is because this function uses Metal convention: where slice is only used for
            // array textures, and z layer of mtlArea.origin is only used for 3D textures.
            ASSERT(slice == 0 || mtlArea.origin.z == 0);

            // For mtl::RenderUtils we convert to OpenGL convention: z layer is used as either array
            // texture's slice or 3D texture's layer index.
            params.textureSliceOrDepth = std::max(slice, static_cast<int>(mtlArea.origin.z));

            for (uint32_t z = 0; z < static_cast<uint32_t>(mtlArea.size.depth); ++z)
            {
                ANGLE_TRY(contextMtl->getDisplay()->getUtils().unpackPixelsWithDraw(
                    context, pixelsAngleFormat, params));

                params.textureSliceOrDepth++;
                params.bufferStartOffset += params.bufferDepthPitch;
            }
        }
    }  // if (unpackBuffer)
    else
    {
        LoadImageFunctionInfo loadFunctionInfo = imageFormat.textureLoadFunctions
                                                     ? imageFormat.textureLoadFunctions(type)
                                                     : LoadImageFunctionInfo();
        const angle::Format &dstFormat         = angle::Format::Get(imageFormat.actualFormatId);
        size_t dstRowPitch;
        size_t dstDepthPitch;
        if (dstFormat.isBlock)
        {
            const gl::InternalFormat &dstFormatInfo =
                gl::GetSizedInternalFormatInfo(dstFormat.glInternalFormat);
            GLuint rowPitch;
            ANGLE_CHECK_GL_MATH(contextMtl,
                                dstFormatInfo.computeCompressedImageRowPitch(
                                    static_cast<GLsizei>(mtlArea.size.width), &rowPitch));
            dstRowPitch = rowPitch;

            GLuint depthPitch;
            ANGLE_CHECK_GL_MATH(contextMtl, dstFormatInfo.computeCompressedImageDepthPitch(
                                                static_cast<GLsizei>(mtlArea.size.height),
                                                static_cast<GLuint>(dstRowPitch), &depthPitch));
            dstDepthPitch = depthPitch;
        }
        else
        {
            dstRowPitch   = dstFormat.pixelBytes * mtlArea.size.width;
            dstDepthPitch = dstRowPitch * mtlArea.size.height;
        }

        // It is very important to avoid allocating a new buffer for each row during these
        // uploads.
        const bool kAvoidStagingBuffers = true;

        // Check if original image data is compressed:
        if (imageFormat.intendedAngleFormat().isBlock)
        {
            if (imageFormat.intendedFormatId != imageFormat.actualFormatId)
            {
                ASSERT(loadFunctionInfo.loadFunction);

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

                // Decompress
                loadFunctionInfo.loadFunction(contextMtl->getImageLoadContext(), 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,
                                                mtl::kZeroNativeMipLevel, slice,
                                                decompressBuf.data(), dstRowPitch, dstDepthPitch,
                                                kAvoidStagingBuffers, imageDef.image));
            }
            else
            {
                // Assert that we're filling the level in it's entierety.
                ASSERT(mtlArea.size.width ==
                       static_cast<unsigned int>(imageDef.image->sizeAt0().width));
                ASSERT(mtlArea.size.height ==
                       static_cast<unsigned int>(imageDef.image->sizeAt0().height));
                ANGLE_TRY(UploadTextureContents(
                    context, dstFormat, mtlArea, mtl::kZeroNativeMipLevel, slice, pixels,
                    dstRowPitch, dstDepthPitch, kAvoidStagingBuffers, imageDef.image));
            }
        }  // if (imageFormat.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 =
                        ANGLE_UNSAFE_TODO(pixels + d * pixelsDepthPitch + r * pixelsRowPitch);
                    mtlRow.origin.y     = mtlArea.origin.y + r;

                    // Convert pixels
                    if (loadFunctionInfo.loadFunction)
                    {
                        loadFunctionInfo.loadFunction(contextMtl->getImageLoadContext(),
                                                      mtlRow.size.width, 1, 1, psrc, pixelsRowPitch,
                                                      0, conversionRow.data(), dstRowPitch, 0);
                    }
                    else if (imageFormat.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,
                                                    mtl::kZeroNativeMipLevel, slice,
                                                    conversionRow.data(), dstRowPitch, 0,
                                                    kAvoidStagingBuffers, imageDef.image));
                }
            }
        }  // if (imageFormat.intendedAngleFormat().isBlock)
    }  // if (unpackBuffer)

    return angle::Result::Continue;
}

angle::Result TextureMtl::initializeNowIfNeeded(const gl::Context *context,
                                                const mtl::Format &mtlFormat,
                                                const mtl::TextureRef &texture)
{
    bool emulatedChannels = mtl::IsFormatEmulated(mtlFormat);
    ContextMtl *contextMtl = mtl::GetImpl(context);
    bool toNonZero = contextMtl->getDisplay()->getFeatures().allocateNonZeroTextures.enabled;

    // For emulated channels that GL texture intends to not have,
    // or if we want to detect uninitialized reads, we need to initialize their content.
    if (emulatedChannels || toNonZero)
    {
        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)
            {
                auto index = mtl::ImageNativeIndex::FromBaseZeroGLIndex(
                    GetCubeOrArraySliceMipIndex(texture, layer, mip));

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

angle::Result TextureMtl::initializeContents(const gl::Context *context,
                                             GLenum binding,
                                             const gl::OwnImageIndex &ownIndex)
{
    const gl::ImageIndex index = ownIndex.getUntranslated();

    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, GL_NONE, gl::OwnImageIndex(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, GL_NONE, gl::OwnImageIndex(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);
    // For Texture's image definition, we always use zero mip level.
    if (format.metalFormat == MTLPixelFormatInvalid)
    {
        return angle::Result::Stop;
    }
    return mtl::InitializeTextureContents(
        context, image, format,
        mtl::ImageNativeIndex::FromBaseZeroGLIndex(
            GetLayerMipIndex(image, GetImageLayerIndexFrom(index), /** level */ 0)));
}

angle::Result TextureMtl::copySubImageImpl(const gl::Context *context,
                                           const gl::ImageIndex &index,
                                           const gl::Offset &destOffset,
                                           const gl::Rectangle &sourceArea,
                                           const gl::InternalFormat &internalFormat,
                                           const FramebufferMtl *source,
                                           const RenderTargetMtl *colorReadRT)
{
    if (!colorReadRT || !colorReadRT->getTexture())
    {
        // Is this an error?
        return angle::Result::Continue;
    }

    gl::Extents fbSize = colorReadRT->getTexture()->size(colorReadRT->getLevelIndex());
    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));

    ContextMtl *contextMtl = mtl::GetImpl(context);
    mtl::Format format     = contextMtl->getPixelFormat(
        angle::Format::InternalFormatToID(internalFormat.sizedInternalFormat));

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

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

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

    RenderTargetMtl *imageRtt;
    ANGLE_TRY(getRenderTarget(contextMtl, index, /*implicitSamples=*/0, &imageRtt));

    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.srcNormalizedCoords = mtl::NormalizedCoords(
        clippedSourceArea, colorReadRT->getTexture()->size(blitParams.srcLevel));
    blitParams.srcYFlipped  = source->getFlipY();
    blitParams.dstLuminance = internalFormat.isLUMA();

    return displayMtl->getUtils().blitColorWithDraw(
        context, cmdEncoder, colorReadRT->getFormat().actualAngleFormat(), 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,
                                          const FramebufferMtl *source,
                                          const RenderTargetMtl *colorReadRT)
{
    mtl::TextureRef &image = getImage(index);
    ASSERT(image && image->valid());

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

    mtl::Format format = contextMtl->getPixelFormat(
        angle::Format::InternalFormatToID(internalFormat.sizedInternalFormat));
    const angle::Format &dstFormat = angle::Format::Get(format.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::_2DArray:
            ASSERT(index.hasLayer());
            dstSlice = index.getLayerIndex();
            break;
        case gl::TextureType::_3D:
            ASSERT(index.hasLayer());
            dstSlice               = 0;
            mtlDstRowArea.origin.z = index.getLayerIndex();
            break;
        default:
            UNREACHABLE();
    }

    // It is very important to avoid allocating a new buffer for each row during these
    // uploads.
    const bool kAvoidStagingBuffers = true;

    // 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 = source->getCorrectFlippedReadArea(context, srcRowArea);
        ANGLE_TRY(source->readPixelsImpl(context, flippedSrcRowArea, packParams, colorReadRT,
                                         conversionRow.data()));

        // Upload to texture
        ANGLE_TRY(UploadTextureContents(context, dstFormat, mtlDstRowArea, mtl::kZeroNativeMipLevel,
                                        dstSlice, conversionRow.data(), dstRowPitch, 0,
                                        kAvoidStagingBuffers, 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();

    mtl::Format format = contextMtl->getPixelFormat(
        angle::Format::InternalFormatToID(internalFormat.sizedInternalFormat));
    if (!format.getCaps().isRenderable())
    {
        return copySubTextureCPU(context, index, destOffset, internalFormat,
                                 mtl::kZeroNativeMipLevel, sourceBox, sourceAngleFormat,
                                 unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha,
                                 sourceImage);
    }
    return copySubTextureWithDraw(
        context, index, destOffset, internalFormat, mtl::kZeroNativeMipLevel, 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,
                                                 const mtl::MipmapNativeLevel &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, mtl::ImageNativeIndex::FromBaseZeroGLIndex(GetZeroLevelIndex(image)));
    mtl::ColorBlitParams blitParams;

    blitParams.dstTextureSize = image->sizeAt0();
    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.srcNormalizedCoords =
        mtl::NormalizedCoords(sourceBox.toRect(), sourceTexture->size(sourceNativeLevel));
    blitParams.srcYFlipped            = false;
    blitParams.dstLuminance           = internalFormat.isLUMA();
    blitParams.unpackFlipY            = unpackFlipY;
    blitParams.unpackPremultiplyAlpha = unpackPremultiplyAlpha;
    blitParams.unpackUnmultiplyAlpha  = unpackUnmultiplyAlpha;
    blitParams.transformLinearToSrgb  = sourceAngleFormat.isSRGB;

    mtl::Format format = contextMtl->getPixelFormat(
        angle::Format::InternalFormatToID(internalFormat.sizedInternalFormat));
    return displayMtl->getUtils().copyTextureWithDraw(context, cmdEncoder, sourceAngleFormat,
                                                      format.actualAngleFormat(), blitParams);
}

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

    mtl::Format format = contextMtl->getPixelFormat(
        angle::Format::InternalFormatToID(internalFormat.sizedInternalFormat));
    const angle::Format &dstAngleFormat = format.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.span());

    // 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, mtl::kZeroNativeMipLevel,
                                    0, conversionDst.data(), convRowPitch, 0, false, image));

    return angle::Result::Continue;
}

}  // namespace rx
