//
// 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.
//
// mtl_resources.mm:
//    Implements wrapper classes for Metal's MTLTexture and MTLBuffer.
//

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

#include <TargetConditionals.h>

#include <algorithm>

#include "common/debug.h"
#include "libANGLE/renderer/metal/ContextMtl.h"
#include "libANGLE/renderer/metal/DisplayMtl.h"
#include "libANGLE/renderer/metal/mtl_command_buffer.h"
#include "libANGLE/renderer/metal/mtl_format_utils.h"
#include "libANGLE/renderer/metal/mtl_utils.h"

namespace rx
{
namespace mtl
{
namespace
{
inline NSUInteger GetMipSize(NSUInteger baseSize, NSUInteger level)
{
    return std::max<NSUInteger>(1, baseSize >> level);
}

void SetTextureSwizzle(ContextMtl *context,
                       const Format &format,
                       MTLTextureDescriptor *textureDescOut)
{
// Texture swizzle functions's declarations are only available if macos 10.15 sdk is present
#if defined(__IPHONE_13_0) || defined(__MAC_10_15)
    if (context->getDisplay()->getFeatures().hasTextureSwizzle.enabled && format.swizzled)
    {
        textureDescOut.swizzle = MTLTextureSwizzleChannelsMake(
            GetTextureSwizzle(format.swizzle[0]), GetTextureSwizzle(format.swizzle[1]),
            GetTextureSwizzle(format.swizzle[2]), GetTextureSwizzle(format.swizzle[3]));
    }
#endif
}

// Asynchronously synchronize the content of a resource between GPU memory and its CPU cache.
// NOTE: This operation doesn't finish immediately upon function's return.
template <class T>
void InvokeCPUMemSync(ContextMtl *context, mtl::BlitCommandEncoder *blitEncoder, T *resource)
{
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
    if (blitEncoder)
    {
        blitEncoder->synchronizeResource(resource);
        resource->resetCPUReadMemNeedSync();
    }
#endif
}
// Ensure that a resource's CPU cache will be synchronized after GPU finishes its modifications on
// the resource.
template <class T>
void EnsureCPUMemWillBeSynced(ContextMtl *context, T *resource)
{
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
    // Make sure GPU & CPU contents are synchronized.
    // NOTE: Only MacOS has separated storage for resource on CPU and GPU and needs explicit
    // synchronization
    if (resource->get().storageMode == MTLStorageModeManaged && resource->isCPUReadMemNeedSync())
    {
        mtl::BlitCommandEncoder *blitEncoder = context->getBlitCommandEncoder();
        InvokeCPUMemSync(context, blitEncoder, resource);
    }
#endif
}

}  // namespace
// Resource implementation
Resource::Resource() : mUsageRef(std::make_shared<UsageRef>()) {}

// Share the GPU usage ref with other resource
Resource::Resource(Resource *other) : mUsageRef(other->mUsageRef)
{
    ASSERT(mUsageRef);
}

void Resource::reset()
{
    mUsageRef->cmdBufferQueueSerial = 0;
    resetCPUReadMemDirty();
    resetCPUReadMemNeedSync();
}

bool Resource::isBeingUsedByGPU(Context *context) const
{
    return context->cmdQueue().isResourceBeingUsedByGPU(this);
}

bool Resource::hasPendingWorks(Context *context) const
{
    return context->cmdQueue().resourceHasPendingWorks(this);
}

void Resource::setUsedByCommandBufferWithQueueSerial(uint64_t serial, bool writing)
{
    if (writing)
    {
        mUsageRef->cpuReadMemNeedSync = true;
        mUsageRef->cpuReadMemDirty    = true;
    }

    mUsageRef->cmdBufferQueueSerial = std::max(mUsageRef->cmdBufferQueueSerial, serial);
}

// Texture implemenetation
/** static */
angle::Result Texture::Make2DTexture(ContextMtl *context,
                                     const Format &format,
                                     uint32_t width,
                                     uint32_t height,
                                     uint32_t mips,
                                     bool renderTargetOnly,
                                     bool allowFormatView,
                                     TextureRef *refOut)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        MTLTextureDescriptor *desc =
            [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format.metalFormat
                                                               width:width
                                                              height:height
                                                           mipmapped:mips == 0 || mips > 1];

        return MakeTexture(context, format, desc, mips, renderTargetOnly, allowFormatView, refOut);
    }  // ANGLE_MTL_OBJC_SCOPE
}

/** static */
angle::Result Texture::MakeCubeTexture(ContextMtl *context,
                                       const Format &format,
                                       uint32_t size,
                                       uint32_t mips,
                                       bool renderTargetOnly,
                                       bool allowFormatView,
                                       TextureRef *refOut)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        MTLTextureDescriptor *desc =
            [MTLTextureDescriptor textureCubeDescriptorWithPixelFormat:format.metalFormat
                                                                  size:size
                                                             mipmapped:mips == 0 || mips > 1];

        return MakeTexture(context, format, desc, mips, renderTargetOnly, allowFormatView, refOut);
    }  // ANGLE_MTL_OBJC_SCOPE
}

/** static */
angle::Result Texture::Make2DMSTexture(ContextMtl *context,
                                       const Format &format,
                                       uint32_t width,
                                       uint32_t height,
                                       uint32_t samples,
                                       bool renderTargetOnly,
                                       bool allowFormatView,
                                       TextureRef *refOut)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        MTLTextureDescriptor *desc = [[MTLTextureDescriptor new] ANGLE_MTL_AUTORELEASE];
        desc.textureType           = MTLTextureType2DMultisample;
        desc.pixelFormat           = format.metalFormat;
        desc.width                 = width;
        desc.height                = height;
        desc.mipmapLevelCount      = 1;
        desc.sampleCount           = samples;

        return MakeTexture(context, format, desc, 1, renderTargetOnly, allowFormatView, refOut);
    }  // ANGLE_MTL_OBJC_SCOPE
}

/** static */
angle::Result Texture::Make3DTexture(ContextMtl *context,
                                     const Format &format,
                                     uint32_t width,
                                     uint32_t height,
                                     uint32_t depth,
                                     uint32_t mips,
                                     bool renderTargetOnly,
                                     bool allowFormatView,
                                     TextureRef *refOut)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        // Use texture2DDescriptorWithPixelFormat to calculate full range mipmap range:
        uint32_t maxDimen = std::max(width, height);
        maxDimen          = std::max(maxDimen, depth);
        MTLTextureDescriptor *desc =
            [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format.metalFormat
                                                               width:maxDimen
                                                              height:maxDimen
                                                           mipmapped:mips == 0 || mips > 1];

        desc.textureType = MTLTextureType3D;
        desc.width       = width;
        desc.height      = height;
        desc.depth       = depth;

        return MakeTexture(context, format, desc, mips, renderTargetOnly, allowFormatView, refOut);
    }  // ANGLE_MTL_OBJC_SCOPE
}

/** static */
angle::Result Texture::MakeTexture(ContextMtl *context,
                                   const Format &mtlFormat,
                                   MTLTextureDescriptor *desc,
                                   uint32_t mips,
                                   bool renderTargetOnly,
                                   bool allowFormatView,
                                   TextureRef *refOut)
{
    SetTextureSwizzle(context, mtlFormat, desc);

    refOut->reset(new Texture(context, desc, mips, renderTargetOnly, allowFormatView));

    if (!(*refOut) || !(*refOut)->get())
    {
        ANGLE_MTL_CHECK(context, false, GL_OUT_OF_MEMORY);
    }
    if (!mtlFormat.hasDepthAndStencilBits())
    {
        refOut->get()->setColorWritableMask(GetEmulatedColorWriteMask(mtlFormat));
    }

    return angle::Result::Continue;
}

/** static */
TextureRef Texture::MakeFromMetal(id<MTLTexture> metalTexture)
{
    ANGLE_MTL_OBJC_SCOPE { return TextureRef(new Texture(metalTexture)); }
}

Texture::Texture(id<MTLTexture> metalTexture)
    : mColorWritableMask(std::make_shared<MTLColorWriteMask>(MTLColorWriteMaskAll))
{
    set(metalTexture);
}

Texture::Texture(ContextMtl *context,
                 MTLTextureDescriptor *desc,
                 uint32_t mips,
                 bool renderTargetOnly,
                 bool allowFormatView)
    : mColorWritableMask(std::make_shared<MTLColorWriteMask>(MTLColorWriteMaskAll))
{
    ANGLE_MTL_OBJC_SCOPE
    {
        id<MTLDevice> metalDevice = context->getMetalDevice();

        if (mips > 1 && mips < desc.mipmapLevelCount)
        {
            desc.mipmapLevelCount = mips;
        }

        // Every texture will support being rendered for now
        desc.usage = 0;

        if (context->getNativeFormatCaps(desc.pixelFormat).isRenderable())
        {
            desc.usage |= MTLTextureUsageRenderTarget;
        }

        if (context->getNativeFormatCaps(desc.pixelFormat).depthRenderable ||
            desc.textureType == MTLTextureType2DMultisample)
        {
            // Metal doesn't support host access to depth stencil texture's data
            desc.resourceOptions = MTLResourceStorageModePrivate;
        }

        if (!renderTargetOnly)
        {
            desc.usage = desc.usage | MTLTextureUsageShaderRead;
            if (context->getNativeFormatCaps(desc.pixelFormat).writable)
            {
                desc.usage = desc.usage | MTLTextureUsageShaderWrite;
            }
        }

        if (allowFormatView)
        {
            desc.usage = desc.usage | MTLTextureUsagePixelFormatView;
        }

        set([[metalDevice newTextureWithDescriptor:desc] ANGLE_MTL_AUTORELEASE]);

        mCreationDesc.retainAssign(desc);
    }
}

Texture::Texture(Texture *original, MTLPixelFormat format)
    : Resource(original),
      mColorWritableMask(original->mColorWritableMask)  // Share color write mask property
{
    ANGLE_MTL_OBJC_SCOPE
    {
        auto view = [original->get() newTextureViewWithPixelFormat:format];

        set([view ANGLE_MTL_AUTORELEASE]);
    }
}

Texture::Texture(Texture *original, MTLTextureType type, NSRange mipmapLevelRange, NSRange slices)
    : Resource(original),
      mColorWritableMask(original->mColorWritableMask)  // Share color write mask property
{
    ANGLE_MTL_OBJC_SCOPE
    {
        auto view = [original->get() newTextureViewWithPixelFormat:original->pixelFormat()
                                                       textureType:type
                                                            levels:mipmapLevelRange
                                                            slices:slices];

        set([view ANGLE_MTL_AUTORELEASE]);
    }
}

void Texture::syncContent(ContextMtl *context, mtl::BlitCommandEncoder *blitEncoder)
{
    InvokeCPUMemSync(context, blitEncoder, this);
}

void Texture::syncContentIfNeeded(ContextMtl *context)
{
    EnsureCPUMemWillBeSynced(context, this);
}

bool Texture::isCPUAccessible() const
{
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
    if (get().storageMode == MTLStorageModeManaged)
    {
        return true;
    }
#endif
    return get().storageMode == MTLStorageModeShared;
}

bool Texture::supportFormatView() const
{
    return get().usage & MTLTextureUsagePixelFormatView;
}

void Texture::replace2DRegion(ContextMtl *context,
                              const MTLRegion &region,
                              uint32_t mipmapLevel,
                              uint32_t slice,
                              const uint8_t *data,
                              size_t bytesPerRow)
{
    ASSERT(region.size.depth == 1);
    replaceRegion(context, region, mipmapLevel, slice, data, bytesPerRow, 0);
}

void Texture::replaceRegion(ContextMtl *context,
                            const MTLRegion &region,
                            uint32_t mipmapLevel,
                            uint32_t slice,
                            const uint8_t *data,
                            size_t bytesPerRow,
                            size_t bytesPer2DImage)
{
    if (mipmapLevel >= this->mipmapLevels())
    {
        return;
    }

    ASSERT(isCPUAccessible());

    CommandQueue &cmdQueue = context->cmdQueue();

    syncContentIfNeeded(context);

    // NOTE(hqle): what if multiple contexts on multiple threads are using this texture?
    if (this->isBeingUsedByGPU(context))
    {
        context->flushCommandBufer();
    }

    cmdQueue.ensureResourceReadyForCPU(this);

    if (textureType() != MTLTextureType3D)
    {
        bytesPer2DImage = 0;
    }

    [get() replaceRegion:region
             mipmapLevel:mipmapLevel
                   slice:slice
               withBytes:data
             bytesPerRow:bytesPerRow
           bytesPerImage:bytesPer2DImage];
}

void Texture::getBytes(ContextMtl *context,
                       size_t bytesPerRow,
                       size_t bytesPer2DInage,
                       const MTLRegion &region,
                       uint32_t mipmapLevel,
                       uint32_t slice,
                       uint8_t *dataOut)
{
    ASSERT(isCPUAccessible());

    CommandQueue &cmdQueue = context->cmdQueue();

    syncContentIfNeeded(context);

    // NOTE(hqle): what if multiple contexts on multiple threads are using this texture?
    if (this->isBeingUsedByGPU(context))
    {
        context->flushCommandBufer();
    }

    cmdQueue.ensureResourceReadyForCPU(this);

    [get() getBytes:dataOut
          bytesPerRow:bytesPerRow
        bytesPerImage:bytesPer2DInage
           fromRegion:region
          mipmapLevel:mipmapLevel
                slice:slice];
}

TextureRef Texture::createCubeFaceView(uint32_t face)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        switch (textureType())
        {
            case MTLTextureTypeCube:
                return TextureRef(new Texture(
                    this, MTLTextureType2D, NSMakeRange(0, mipmapLevels()), NSMakeRange(face, 1)));
            default:
                UNREACHABLE();
                return nullptr;
        }
    }
}

TextureRef Texture::createSliceMipView(uint32_t slice, uint32_t level)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        switch (textureType())
        {
            case MTLTextureTypeCube:
            case MTLTextureType2D:
            case MTLTextureType2DArray:
                return TextureRef(new Texture(this, MTLTextureType2D, NSMakeRange(level, 1),
                                              NSMakeRange(slice, 1)));
            default:
                UNREACHABLE();
                return nullptr;
        }
    }
}

TextureRef Texture::createMipView(uint32_t level)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        NSUInteger slices = cubeFacesOrArrayLength();
        return TextureRef(
            new Texture(this, textureType(), NSMakeRange(level, 1), NSMakeRange(0, slices)));
    }
}

TextureRef Texture::createViewWithDifferentFormat(MTLPixelFormat format)
{
    ASSERT(supportFormatView());
    return TextureRef(new Texture(this, format));
}
TextureRef Texture::createViewWithCompatibleFormat(MTLPixelFormat format)
{
    // No need for ASSERT(supportFormatView());
    return TextureRef(new Texture(this, format));
}

MTLPixelFormat Texture::pixelFormat() const
{
    return get().pixelFormat;
}

MTLTextureType Texture::textureType() const
{
    return get().textureType;
}

uint32_t Texture::mipmapLevels() const
{
    return static_cast<uint32_t>(get().mipmapLevelCount);
}

uint32_t Texture::arrayLength() const
{
    return static_cast<uint32_t>(get().arrayLength);
}

uint32_t Texture::cubeFacesOrArrayLength() const
{
    if (textureType() == MTLTextureTypeCube)
    {
        return 6;
    }
    return arrayLength();
}

uint32_t Texture::width(uint32_t level) const
{
    return static_cast<uint32_t>(GetMipSize(get().width, level));
}

uint32_t Texture::height(uint32_t level) const
{
    return static_cast<uint32_t>(GetMipSize(get().height, level));
}

uint32_t Texture::depth(uint32_t level) const
{
    return static_cast<uint32_t>(GetMipSize(get().depth, level));
}

gl::Extents Texture::size(uint32_t level) const
{
    gl::Extents re;

    re.width  = width(level);
    re.height = height(level);
    re.depth  = depth(level);

    return re;
}

gl::Extents Texture::size(const gl::ImageIndex &index) const
{
    gl::Extents extents = size(index.getLevelIndex());

    if (index.hasLayer())
    {
        extents.depth = 1;
    }

    return extents;
}

uint32_t Texture::samples() const
{
    return static_cast<uint32_t>(get().sampleCount);
}

angle::Result Texture::resize(ContextMtl *context, uint32_t width, uint32_t height)
{
    // Resizing texture view is not supported.
    ASSERT(mCreationDesc);

    ANGLE_MTL_OBJC_SCOPE
    {
        MTLTextureDescriptor *newDesc = [[mCreationDesc.get() copy] ANGLE_MTL_AUTORELEASE];
        newDesc.width                 = width;
        newDesc.height                = height;
        id<MTLTexture> newTexture =
            [[get().device newTextureWithDescriptor:newDesc] ANGLE_MTL_AUTORELEASE];

        ANGLE_CHECK_GL_ALLOC(context, newTexture);

        mCreationDesc.retainAssign(newDesc);

        set(newTexture);

        // Reset reference counter
        Resource::reset();
    }

    return angle::Result::Continue;
}

TextureRef Texture::getLinearColorView()
{
    if (mLinearColorView)
    {
        return mLinearColorView;
    }

    switch (pixelFormat())
    {
        case MTLPixelFormatRGBA8Unorm_sRGB:
            mLinearColorView = createViewWithCompatibleFormat(MTLPixelFormatRGBA8Unorm);
            break;
        case MTLPixelFormatBGRA8Unorm_sRGB:
            mLinearColorView = createViewWithCompatibleFormat(MTLPixelFormatBGRA8Unorm);
            break;
        default:
            // NOTE(hqle): Not all sRGB formats are supported yet.
            UNREACHABLE();
    }

    return mLinearColorView;
}

TextureRef Texture::getStencilView()
{
    if (mStencilView)
    {
        return mStencilView;
    }

    switch (pixelFormat())
    {
        case MTLPixelFormatStencil8:
        case MTLPixelFormatX32_Stencil8:
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
        case MTLPixelFormatX24_Stencil8:
#endif
            return mStencilView = shared_from_this();
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
        case MTLPixelFormatDepth24Unorm_Stencil8:
            mStencilView = createViewWithDifferentFormat(MTLPixelFormatX24_Stencil8);
            break;
#endif
        case MTLPixelFormatDepth32Float_Stencil8:
            mStencilView = createViewWithDifferentFormat(MTLPixelFormatX32_Stencil8);
            break;
        default:
            UNREACHABLE();
    }

    return mStencilView;
}

void Texture::set(id<MTLTexture> metalTexture)
{
    ParentClass::set(metalTexture);
    // Reset stencil view
    mStencilView     = nullptr;
    mLinearColorView = nullptr;
}

// Buffer implementation
angle::Result Buffer::MakeBuffer(ContextMtl *context,
                                 size_t size,
                                 const uint8_t *data,
                                 BufferRef *bufferOut)
{

    return MakeBufferWithSharedMemOpt(context, false, size, data, bufferOut);
}

angle::Result Buffer::MakeBufferWithSharedMemOpt(ContextMtl *context,
                                                 bool forceUseSharedMem,
                                                 size_t size,
                                                 const uint8_t *data,
                                                 BufferRef *bufferOut)
{
    bufferOut->reset(new Buffer(context, forceUseSharedMem, size, data));

    if (!(*bufferOut) || !(*bufferOut)->get())
    {
        ANGLE_MTL_CHECK(context, false, GL_OUT_OF_MEMORY);
    }

    return angle::Result::Continue;
}

angle::Result Buffer::MakeBufferWithResOpt(ContextMtl *context,
                                           MTLResourceOptions options,
                                           size_t size,
                                           const uint8_t *data,
                                           BufferRef *bufferOut)
{
    bufferOut->reset(new Buffer(context, options, size, data));

    if (!(*bufferOut) || !(*bufferOut)->get())
    {
        ANGLE_MTL_CHECK(context, false, GL_OUT_OF_MEMORY);
    }

    return angle::Result::Continue;
}

Buffer::Buffer(ContextMtl *context, bool forceUseSharedMem, size_t size, const uint8_t *data)
{
    (void)resetWithSharedMemOpt(context, forceUseSharedMem, size, data);
}

Buffer::Buffer(ContextMtl *context, MTLResourceOptions options, size_t size, const uint8_t *data)
{
    (void)resetWithResOpt(context, options, size, data);
}

angle::Result Buffer::reset(ContextMtl *context, size_t size, const uint8_t *data)
{
    return resetWithSharedMemOpt(context, false, size, data);
}

angle::Result Buffer::resetWithSharedMemOpt(ContextMtl *context,
                                            bool forceUseSharedMem,
                                            size_t size,
                                            const uint8_t *data)
{
    MTLResourceOptions options;

    options = 0;
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
    if (!forceUseSharedMem)
    {
        options |= MTLResourceStorageModeManaged;
    }
    else
#endif
    {
        options |= MTLResourceStorageModeShared;
    }

    return resetWithResOpt(context, options, size, data);
}

angle::Result Buffer::resetWithResOpt(ContextMtl *context,
                                      MTLResourceOptions options,
                                      size_t size,
                                      const uint8_t *data)
{
    ANGLE_MTL_OBJC_SCOPE
    {
        id<MTLBuffer> newBuffer;
        id<MTLDevice> metalDevice = context->getMetalDevice();

        if (data)
        {
            newBuffer = [metalDevice newBufferWithBytes:data length:size options:options];
        }
        else
        {
            newBuffer = [metalDevice newBufferWithLength:size options:options];
        }

        set([newBuffer ANGLE_MTL_AUTORELEASE]);

        // Reset command buffer's reference serial
        Resource::reset();

        return angle::Result::Continue;
    }
}

void Buffer::syncContent(ContextMtl *context, mtl::BlitCommandEncoder *blitEncoder)
{
    InvokeCPUMemSync(context, blitEncoder, this);
}

const uint8_t *Buffer::mapReadOnly(ContextMtl *context)
{
    return mapWithOpt(context, true, false);
}

uint8_t *Buffer::map(ContextMtl *context)
{
    return mapWithOpt(context, false, false);
}

uint8_t *Buffer::mapWithOpt(ContextMtl *context, bool readonly, bool noSync)
{
    mMapReadOnly = readonly;

    if (!noSync)
    {
        CommandQueue &cmdQueue = context->cmdQueue();

        EnsureCPUMemWillBeSynced(context, this);

        if (this->isBeingUsedByGPU(context))
        {
            context->flushCommandBufer();
        }

        cmdQueue.ensureResourceReadyForCPU(this);
    }

    return reinterpret_cast<uint8_t *>([get() contents]);
}

void Buffer::unmap(ContextMtl *context)
{
    flush(context, 0, size());

    // Reset read only flag
    mMapReadOnly = true;
}

void Buffer::unmapNoFlush(ContextMtl *context)
{
    mMapReadOnly = true;
}

void Buffer::unmapAndFlushSubset(ContextMtl *context, size_t offsetWritten, size_t sizeWritten)
{
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
    flush(context, offsetWritten, sizeWritten);
#endif
    mMapReadOnly = true;
}

void Buffer::flush(ContextMtl *context, size_t offsetWritten, size_t sizeWritten)
{
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
    if (!mMapReadOnly)
    {
        if (get().storageMode == MTLStorageModeManaged)
        {
            [get() didModifyRange:NSMakeRange(offsetWritten, sizeWritten)];
        }
    }
#endif
}

size_t Buffer::size() const
{
    return get().length;
}

bool Buffer::useSharedMem() const
{
    return get().storageMode == MTLStorageModeShared;
}
}
}
