//
// Copyright 2017 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.
//
// EGLIOSurfaceClientBufferTest.cpp: tests for the EGL_ANGLE_iosurface_client_buffer extension.
//

#include "test_utils/ANGLETest.h"

#include "common/mathutil.h"
#include "common/unsafe_buffers.h"
#include "test_utils/gl_raii.h"
#include "util/EGLWindow.h"

#include <CoreFoundation/CoreFoundation.h>
#if TARGET_OS_OSX
#    include <IOSurface/IOSurface.h>
#else
#    include <IOSurface/IOSurfaceRef.h>
#endif
using namespace angle;

namespace
{

constexpr char kIOSurfaceExt[] = "EGL_ANGLE_iosurface_client_buffer";

void AddIntegerValue(CFMutableDictionaryRef dictionary, const CFStringRef key, int32_t value)
{
    CFNumberRef number = CFNumberCreate(nullptr, kCFNumberSInt32Type, &value);
    CFDictionaryAddValue(dictionary, key, number);
    CFRelease(number);
}

class [[nodiscard]] ScopedIOSurfaceRef : angle::NonCopyable
{
  public:
    explicit ScopedIOSurfaceRef(IOSurfaceRef surface) : mSurface(surface) {}

    ~ScopedIOSurfaceRef()
    {
        if (mSurface != nullptr)
        {
            CFRelease(mSurface);
            mSurface = nullptr;
        }
    }

    IOSurfaceRef get() const { return mSurface; }

    ScopedIOSurfaceRef(ScopedIOSurfaceRef &&other)
    {
        if (mSurface != nullptr)
        {
            CFRelease(mSurface);
        }
        mSurface       = other.mSurface;
        other.mSurface = nullptr;
    }

    ScopedIOSurfaceRef &operator=(ScopedIOSurfaceRef &&other)
    {
        if (mSurface != nullptr)
        {
            CFRelease(mSurface);
        }
        mSurface       = other.mSurface;
        other.mSurface = nullptr;

        return *this;
    }

  private:
    IOSurfaceRef mSurface = nullptr;
};

struct IOSurfacePlaneInfo
{
    int width;
    int height;
    int bytesPerElement;
};

ScopedIOSurfaceRef CreateIOSurface(int32_t format, const std::vector<IOSurfacePlaneInfo> &planes)
{
    EXPECT_GT(planes.size(), 0u);

    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
        kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    AddIntegerValue(dict, kIOSurfaceWidth, planes[0].width);
    AddIntegerValue(dict, kIOSurfaceHeight, planes[0].height);
    AddIntegerValue(dict, kIOSurfacePixelFormat, format);

    if (planes.size() > 1)
    {
        CFMutableArrayRef planesInfo =
            CFArrayCreateMutable(kCFAllocatorDefault, planes.size(), &kCFTypeArrayCallBacks);
        for (const IOSurfacePlaneInfo &plane : planes)
        {
            CFMutableDictionaryRef planeInfo =
                CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
                                          &kCFTypeDictionaryValueCallBacks);
            AddIntegerValue(planeInfo, kIOSurfacePlaneWidth, plane.width);
            AddIntegerValue(planeInfo, kIOSurfacePlaneHeight, plane.height);
            AddIntegerValue(planeInfo, kIOSurfacePlaneBytesPerElement, plane.bytesPerElement);

            CFArrayAppendValue(planesInfo, planeInfo);
            CFRelease(planeInfo);
        }

        CFDictionaryAddValue(dict, kIOSurfacePlaneInfo, planesInfo);
        CFRelease(planesInfo);
    }
    else
    {
        AddIntegerValue(dict, kIOSurfaceBytesPerElement, planes[0].bytesPerElement);
    }

    IOSurfaceRef ioSurface = IOSurfaceCreate(dict);
    EXPECT_NE(nullptr, ioSurface);
    CFRelease(dict);

    return ScopedIOSurfaceRef(ioSurface);
}

ScopedIOSurfaceRef CreateSinglePlaneIOSurface(int width,
                                              int height,
                                              int32_t format,
                                              int bytesPerElement)
{
    std::vector<IOSurfacePlaneInfo> planes{{width, height, bytesPerElement}};
    return CreateIOSurface(format, planes);
}

}  // anonymous namespace

class IOSurfaceClientBufferTest : public ANGLETest<>
{
  protected:
    EGLint getTextureTarget() const
    {
        EGLint target = 0;
        eglGetConfigAttrib(mDisplay, mConfig, EGL_BIND_TO_TEXTURE_TARGET_ANGLE, &target);
        return target;
    }

    GLint getGLTextureTarget() const
    {
        EGLint targetEGL = getTextureTarget();
        GLenum targetGL  = 0;
        switch (targetEGL)
        {
            case EGL_TEXTURE_2D:
                targetGL = GL_TEXTURE_2D;
                break;
            case EGL_TEXTURE_RECTANGLE_ANGLE:
                targetGL = GL_TEXTURE_RECTANGLE_ANGLE;
                break;
            default:
                break;
        }
        return targetGL;
    }

    IOSurfaceClientBufferTest() : mConfig(0), mDisplay(nullptr) {}

    void testSetUp() override
    {
        mConfig  = getEGLWindow()->getConfig();
        mDisplay = getEGLWindow()->getDisplay();
    }

    void createIOSurfacePbuffer(const ScopedIOSurfaceRef &ioSurface,
                                EGLint width,
                                EGLint height,
                                EGLint plane,
                                GLenum internalFormat,
                                GLenum type,
                                EGLSurface *pbuffer) const
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         width,
            EGL_HEIGHT,                        height,
            EGL_IOSURFACE_PLANE_ANGLE,         plane,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE,
                static_cast<EGLint>(internalFormat),
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            static_cast<EGLint>(type),
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        *pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE, ioSurface.get(),
                                                    mConfig, attribs);
        EXPECT_NE(EGL_NO_SURFACE, *pbuffer);
    }

    void bindIOSurfaceToTexture(const ScopedIOSurfaceRef &ioSurface,
                                EGLint width,
                                EGLint height,
                                EGLint plane,
                                GLenum internalFormat,
                                GLenum type,
                                EGLSurface *pbuffer,
                                GLTexture *texture) const
    {
        createIOSurfacePbuffer(ioSurface, width, height, plane, internalFormat, type, pbuffer);

        // Bind the pbuffer
        glBindTexture(getGLTextureTarget(), *texture);
        EGLBoolean result = eglBindTexImage(mDisplay, *pbuffer, EGL_BACK_BUFFER);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();
    }

    void doClearTest(const ScopedIOSurfaceRef &ioSurface,
                     EGLint width,
                     EGLint height,
                     EGLint plane,
                     GLenum internalFormat,
                     GLenum type,
                     const GLColor &data)
    {
        std::array<uint8_t, 4> dataArray{data.R, data.G, data.B, data.A};
        doClearTest(ioSurface, width, height, plane, internalFormat, type, dataArray);
    }

    template <typename T, size_t dataSize>
    void doClearTest(const ScopedIOSurfaceRef &ioSurface,
                     EGLint width,
                     EGLint height,
                     EGLint plane,
                     GLenum internalFormat,
                     GLenum type,
                     const std::array<T, dataSize> &data)
    {
        // Bind the IOSurface to a texture and clear it.
        EGLSurface pbuffer;
        GLTexture texture;
        bindIOSurfaceToTexture(ioSurface, width, height, plane, internalFormat, type, &pbuffer,
                               &texture);

        GLFramebuffer fbo;
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        EXPECT_GL_NO_ERROR();
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getGLTextureTarget(), texture,
                               0);
        EXPECT_GL_NO_ERROR();
        ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
        EXPECT_GL_NO_ERROR();

        glClearColor(1.0f / 255.0f, 2.0f / 255.0f, 3.0f / 255.0f, 4.0f / 255.0f);
        EXPECT_GL_NO_ERROR();
        glClear(GL_COLOR_BUFFER_BIT);
        EXPECT_GL_NO_ERROR();

        // Unbind pbuffer and check content.
        EGLBoolean result = eglReleaseTexImage(mDisplay, pbuffer, EGL_BACK_BUFFER);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();

        // IOSurface client buffer's rendering doesn't automatically finish after
        // eglReleaseTexImage(). Need to explicitly call glFinish().
        glFinish();

        IOSurfaceLock(ioSurface.get(), kIOSurfaceLockReadOnly, nullptr);
        std::array<T, dataSize> iosurfaceData;
        ANGLE_UNSAFE_TODO(memcpy(iosurfaceData.data(),
                                 IOSurfaceGetBaseAddressOfPlane(ioSurface.get(), plane),
                                 sizeof(T) * data.size()));
        IOSurfaceUnlock(ioSurface.get(), kIOSurfaceLockReadOnly, nullptr);

        if (internalFormat == GL_RGB && IsMac() && IsOpenGL())
        {
            // Ignore alpha component for BGRX, the alpha value is undefined
            for (int i = 0; i < 3; i++)
            {
                ASSERT_EQ(data[i], iosurfaceData[i]);
            }
        }
        else
        {
            ASSERT_EQ(data, iosurfaceData);
        }

        result = eglDestroySurface(mDisplay, pbuffer);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();
    }

    enum ColorMask
    {
        R = 1,
        G = 2,
        B = 4,
        A = 8,
    };
    void doSampleTestWithExtraSteps(const ScopedIOSurfaceRef &ioSurface,
                                    EGLint width,
                                    EGLint height,
                                    EGLint plane,
                                    GLenum internalFormat,
                                    GLenum type,
                                    void *data,
                                    size_t dataSize,
                                    int mask,
                                    const std::function<void()> &extraStepsBeforeSample)
    {
        // Write the data to the IOSurface
        IOSurfaceLock(ioSurface.get(), 0, nullptr);
        ANGLE_UNSAFE_TODO(
            memcpy(IOSurfaceGetBaseAddressOfPlane(ioSurface.get(), plane), data, dataSize));
        IOSurfaceUnlock(ioSurface.get(), 0, nullptr);

        GLTexture texture;
        glBindTexture(getGLTextureTarget(), texture);
        glTexParameteri(getGLTextureTarget(), GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(getGLTextureTarget(), GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        // Bind the IOSurface to a texture and clear it.
        EGLSurface pbuffer;
        bindIOSurfaceToTexture(ioSurface, width, height, plane, internalFormat, type, &pbuffer,
                               &texture);

        if (extraStepsBeforeSample)
        {
            extraStepsBeforeSample();
        }

        doSampleTestWithTexture(texture, mask);

        EGLBoolean result = eglDestroySurface(mDisplay, pbuffer);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();
    }

    void doSampleTest(const ScopedIOSurfaceRef &ioSurface,
                      EGLint width,
                      EGLint height,
                      EGLint plane,
                      GLenum internalFormat,
                      GLenum type,
                      void *data,
                      size_t dataSize,
                      int mask)
    {
        doSampleTestWithExtraSteps(ioSurface, width, height, plane, internalFormat, type, data,
                                   dataSize, mask, nullptr);
    }

    void doSampleTestWithTexture(const GLTexture &texture, int mask)
    {
        constexpr char kVS[] =
            "attribute vec4 position;\n"
            "void main()\n"
            "{\n"
            "    gl_Position = vec4(position.xy, 0.0, 1.0);\n"
            "}\n";
        constexpr char kFS_rect[] =
            "#extension GL_ARB_texture_rectangle : require\n"
            "precision mediump float;\n"
            "uniform sampler2DRect tex;\n"
            "void main()\n"
            "{\n"
            "    gl_FragColor = texture2DRect(tex, vec2(0, 0));\n"
            "}\n";
        constexpr char kFS_2D[] =
            "precision mediump float;\n"
            "uniform sampler2D tex;\n"
            "void main()\n"
            "{\n"
            "    gl_FragColor = texture2D(tex, vec2(0, 0));\n"
            "}\n";

        ANGLE_GL_PROGRAM(program, kVS,
                         (getTextureTarget() == EGL_TEXTURE_RECTANGLE_ANGLE ? kFS_rect : kFS_2D));
        glUseProgram(program);

        GLint location = glGetUniformLocation(program, "tex");
        ASSERT_NE(-1, location);
        glUniform1i(location, 0);

        glClearColor(0.0, 0.0, 0.0, 0.0);
        glClear(GL_COLOR_BUFFER_BIT);
        drawQuad(program, "position", 0.5f, 1.0f, false);

        GLColor expectedColor((mask & R) ? 1 : 0, (mask & G) ? 2 : 0, (mask & B) ? 3 : 0,
                              (mask & A) ? 4 : 255);
        EXPECT_PIXEL_COLOR_EQ(0, 0, expectedColor);
        ASSERT_GL_NO_ERROR();
    }

    void doBlitTest(bool ioSurfaceIsSource, int width, int height)
    {
        if (!hasBlitExt())
        {
            return;
        }

        // Create IOSurface and bind it to a texture.
        ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(width, height, 'BGRA', 4);
        EGLSurface pbuffer;
        GLTexture texture;
        bindIOSurfaceToTexture(ioSurface, width, height, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, &pbuffer,
                               &texture);

        GLFramebuffer iosurfaceFbo;
        glBindFramebuffer(GL_FRAMEBUFFER, iosurfaceFbo);
        EXPECT_GL_NO_ERROR();
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getGLTextureTarget(), texture,
                               0);
        EXPECT_GL_NO_ERROR();
        ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
        EXPECT_GL_NO_ERROR();

        // Create another framebuffer with a regular renderbuffer.
        GLFramebuffer fbo;
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        EXPECT_GL_NO_ERROR();
        GLRenderbuffer rbo;
        glBindRenderbuffer(GL_RENDERBUFFER, rbo);
        EXPECT_GL_NO_ERROR();
        glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
        EXPECT_GL_NO_ERROR();
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
        EXPECT_GL_NO_ERROR();
        ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
        EXPECT_GL_NO_ERROR();

        glBindRenderbuffer(GL_RENDERBUFFER, 0);
        EXPECT_GL_NO_ERROR();
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        EXPECT_GL_NO_ERROR();

        // Choose which is going to be the source and destination.
        GLFramebuffer &src = ioSurfaceIsSource ? iosurfaceFbo : fbo;
        GLFramebuffer &dst = ioSurfaceIsSource ? fbo : iosurfaceFbo;

        // Clear source to known color.
        glBindFramebuffer(GL_FRAMEBUFFER, src);
        glClearColor(1.0f / 255.0f, 2.0f / 255.0f, 3.0f / 255.0f, 4.0f / 255.0f);
        EXPECT_GL_NO_ERROR();
        glClear(GL_COLOR_BUFFER_BIT);
        EXPECT_GL_NO_ERROR();

        // Blit to destination.
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER_ANGLE, dst);
        glBlitFramebufferANGLE(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT,
                               GL_NEAREST);

        // Read back from destination.
        glBindFramebuffer(GL_FRAMEBUFFER, dst);
        GLColor expectedColor(1, 2, 3, 4);
        EXPECT_PIXEL_COLOR_EQ(0, 0, expectedColor);

        // Unbind pbuffer and check content.
        EGLBoolean result = eglReleaseTexImage(mDisplay, pbuffer, EGL_BACK_BUFFER);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();

        result = eglDestroySurface(mDisplay, pbuffer);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();
    }

    bool hasIOSurfaceExt() const { return IsEGLDisplayExtensionEnabled(mDisplay, kIOSurfaceExt); }
    bool hasBlitExt() const
    {
        return IsEGLDisplayExtensionEnabled(mDisplay, "ANGLE_framebuffer_blit");
    }

    EGLConfig mConfig;
    EGLDisplay mDisplay;
};

// Test using RGBA8888 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToRGBA8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    // DesktopOpenGL doesn't support RGBA IOSurface.
    ANGLE_SKIP_TEST_IF(IsDesktopOpenGL());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'RGBA', 4);

    GLColor color(1, 2, 3, 4);
    doClearTest(ioSurface, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, color);
}

// Test reading from RGBA8888 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromRGBA8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    // DesktopOpenGL doesn't support RGBA IOSurface.
    ANGLE_SKIP_TEST_IF(IsDesktopOpenGL());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'RGBA', 4);

    GLColor color(1, 2, 3, 4);
    doSampleTest(ioSurface, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &color, sizeof(color),
                 R | G | B | A);
}

// Test using BGRA8888 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToBGRA8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLColor color(3, 2, 1, 4);
    doClearTest(ioSurface, 1, 1, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, color);
}

// Test reading from BGRA8888 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromBGRA8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLColor color(3, 2, 1, 4);
    doSampleTest(ioSurface, 1, 1, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, &color, sizeof(color),
                 R | G | B | A);
}

// Test using RGBX8888 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToRGBX8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    // DesktopOpenGL doesn't support RGBA IOSurface.
    ANGLE_SKIP_TEST_IF(IsDesktopOpenGL());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'RGBA', 4);

    GLColor color(1, 2, 3, 255);
    doClearTest(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, color);
}

// Test reading from RGBX8888 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromRGBX8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    // DesktopOpenGL doesn't support RGBA IOSurface.
    ANGLE_SKIP_TEST_IF(IsDesktopOpenGL());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'RGBA', 4);

    GLColor color(1, 2, 3, 255);
    doSampleTest(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, &color, sizeof(color), R | G | B);
}

// Test using BGRX8888 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToBGRX8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLColor color(3, 2, 1, 255);
    doClearTest(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, color);
}

// Test reading from BGRX8888 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromBGRX8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLColor color(3, 2, 1, 4);
    doSampleTest(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, &color, sizeof(color), R | G | B);
}

// Test using RG88 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToRG88IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, '2C08', 2);

    std::array<uint8_t, 2> color{1, 2};
    doClearTest(ioSurface, 1, 1, 0, GL_RG, GL_UNSIGNED_BYTE, color);
}

// Test reading from RG88 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromRG88IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, '2C08', 2);

    uint8_t color[2] = {1, 2};
    doSampleTest(ioSurface, 1, 1, 0, GL_RG, GL_UNSIGNED_BYTE, &color, sizeof(color), R | G);
}

// Test using R8 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToR8IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'L008', 1);

    std::array<uint8_t, 1> color{1};
    doClearTest(ioSurface, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, color);
}

// Test reading from R8 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromR8IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'L008', 1);

    uint8_t color = 1;
    doSampleTest(ioSurface, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, &color, sizeof(color), R);
}

// Test using RG1616 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToRG1616IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_norm16"));

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, '2C16', 4);

    std::array<uint16_t, 2> color{257, 514};
    doClearTest(ioSurface, 1, 1, 0, GL_RG, GL_UNSIGNED_SHORT, color);
}

// Test reading from RG1616 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromRG1616IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_norm16"));

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, '2C16', 4);

    uint16_t color[2] = {257, 514};
    doSampleTest(ioSurface, 1, 1, 0, GL_RG, GL_UNSIGNED_SHORT, &color, sizeof(color), R | G);
}

// Test using R16 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToR16IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_norm16"));

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'L016', 2);

    std::array<uint16_t, 1> color{257};
    doClearTest(ioSurface, 1, 1, 0, GL_RED, GL_UNSIGNED_SHORT, color);
}

// Test reading from R16 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromR16IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_norm16"));

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'L016', 2);

    uint16_t color = 257;
    doSampleTest(ioSurface, 1, 1, 0, GL_RED, GL_UNSIGNED_SHORT, &color, sizeof(color), R);
}

// Test using BGRA_1010102 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToBGRA1010102IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'l10r', 4);

    std::array<uint32_t, 1> color{(0 << 30) | (1 << 22) | (2 << 12) | (3 << 2)};
    doClearTest(ioSurface, 1, 1, 0, GL_RGB10_A2, GL_UNSIGNED_INT_2_10_10_10_REV, color);
}

// Test reading from BGRA_1010102 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromBGRA1010102IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'l10r', 4);

    uint32_t color = (3 << 30) | (1 << 22) | (2 << 12) | (3 << 2);
    doSampleTest(ioSurface, 1, 1, 0, GL_RGB10_A2, GL_UNSIGNED_INT_2_10_10_10_REV, &color,
                 sizeof(color),
                 R | G | B);  // Don't test alpha, unorm '4' can't be represented with 2 bits.
}

// Test using RGBA_16F IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToRGBA16FIOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'RGhA', 8);

    std::array<GLushort, 4> color{
        gl::float32ToFloat16(1.0f / 255.0f), gl::float32ToFloat16(2.0f / 255.0f),
        gl::float32ToFloat16(3.0f / 255.0f), gl::float32ToFloat16(4.0f / 255.0f)};
    doClearTest(ioSurface, 1, 1, 0, GL_RGBA, GL_HALF_FLOAT, color);
}

// Test reading from RGBA_16F IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromToRGBA16FIOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'RGhA', 8);

    std::array<GLushort, 4> color{
        gl::float32ToFloat16(1.0f / 255.0f), gl::float32ToFloat16(2.0f / 255.0f),
        gl::float32ToFloat16(3.0f / 255.0f), gl::float32ToFloat16(4.0f / 255.0f)};
    doSampleTest(ioSurface, 1, 1, 0, GL_RGBA, GL_HALF_FLOAT, color.data(), sizeof(GLushort) * 4,
                 R | G | B | A);
}

// Test using YUV420 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToYUV420IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    std::vector<IOSurfacePlaneInfo> planes{{2, 2, 1}, {1, 1, 2}};
    ScopedIOSurfaceRef ioSurface = CreateIOSurface('420v', planes);

    {
        std::array<GLubyte, 1> colors{1};
        doClearTest(ioSurface, planes[0].width, planes[0].height, 0, GL_RED, GL_UNSIGNED_BYTE,
                    colors);
    }

    {
        std::array<GLubyte, 2> colors{1, 2};
        doClearTest(ioSurface, planes[1].width, planes[1].height, 1, GL_RG, GL_UNSIGNED_BYTE,
                    colors);
    }
}

// Test reading from YUV420 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromToYUV420IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    std::vector<IOSurfacePlaneInfo> planes{{2, 2, 1}, {1, 1, 2}};
    ScopedIOSurfaceRef ioSurface = CreateIOSurface('420v', planes);

    {
        std::array<GLubyte, 1> colors{1};
        doSampleTest(ioSurface, planes[0].width, planes[0].height, 0, GL_RED, GL_UNSIGNED_BYTE,
                     colors.data(), sizeof(GLubyte) * colors.size(), R);
    }

    {
        std::array<GLubyte, 2> colors{1, 2};
        doSampleTest(ioSurface, planes[1].width, planes[1].height, 1, GL_RG, GL_UNSIGNED_BYTE,
                     colors.data(), sizeof(GLubyte) * colors.size(), R | G);
    }
}

// Test using P010 IOSurfaces for rendering
TEST_P(IOSurfaceClientBufferTest, RenderToP010IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    std::vector<IOSurfacePlaneInfo> planes{{2, 2, 2}, {1, 1, 4}};
    ScopedIOSurfaceRef ioSurface = CreateIOSurface('x420', planes);

    {
        std::array<GLushort, 1> colors{257};
        doClearTest(ioSurface, planes[0].width, planes[0].height, 0, GL_RED, GL_UNSIGNED_SHORT,
                    colors);
    }

    {
        std::array<GLushort, 2> colors{257, 514};
        doClearTest(ioSurface, planes[1].width, planes[1].height, 1, GL_RG, GL_UNSIGNED_SHORT,
                    colors);
    }
}

// Test reading from P010 IOSurfaces
TEST_P(IOSurfaceClientBufferTest, ReadFromToP010IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    std::vector<IOSurfacePlaneInfo> planes{{2, 2, 2}, {1, 1, 4}};
    ScopedIOSurfaceRef ioSurface = CreateIOSurface('x420', planes);

    {
        std::array<GLushort, 1> colors{257};
        doSampleTest(ioSurface, planes[0].width, planes[0].height, 0, GL_RED, GL_UNSIGNED_SHORT,
                     colors.data(), sizeof(GLushort) * colors.size(), R);
    }

    {
        std::array<GLushort, 2> colors{257, 514};
        doSampleTest(ioSurface, planes[1].width, planes[1].height, 1, GL_RG, GL_UNSIGNED_SHORT,
                     colors.data(), sizeof(GLushort) * colors.size(), R | G);
    }
}

// Test blitting from IOSurface
TEST_P(IOSurfaceClientBufferTest, BlitFromIOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    doBlitTest(true, 2, 2);
}

// Test blitting to IOSurface
TEST_P(IOSurfaceClientBufferTest, BlitToIOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    doBlitTest(false, 2, 2);
}

// Test using glCopyTexSubImage to copy to BGRX8888 IOSurfaces works.
TEST_P(IOSurfaceClientBufferTest, CopySubImageToBGRX8888IOSurface)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLTexture texture;
    glBindTexture(getGLTextureTarget(), texture);
    glTexParameteri(getGLTextureTarget(), GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(getGLTextureTarget(), GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // Bind the IOSurface to a texture and clear it.
    EGLSurface pbuffer;
    bindIOSurfaceToTexture(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, &pbuffer, &texture);

    // 1. Clear default framebuffer with desired color.
    GLColor color(1, 2, 3, 4);
    glClearColor(color.R / 255.f, color.G / 255.f, color.B / 255.f, color.A / 255.f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 2. Copy color from default framebuffer to iosurface's bound texture.
    glCopyTexSubImage2D(getGLTextureTarget(), 0, 0, 0, 0, 0, 1, 1);
    EXPECT_GL_NO_ERROR();

    // 3. Do texture sampling verification.
    doSampleTestWithTexture(texture, R | G | B);
}

// Test the validation errors for missing attributes for eglCreatePbufferFromClientBuffer with
// IOSurface
TEST_P(IOSurfaceClientBufferTest, NegativeValidationMissingAttributes)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(10, 10, 'BGRA', 4);

    // Success case
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format off

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE, ioSurface.get(), mConfig, attribs);
        EXPECT_NE(EGL_NO_SURFACE, pbuffer);

        EGLBoolean result = eglDestroySurface(mDisplay, pbuffer);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();
    }

    // Missing EGL_WIDTH
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
    }

    // Missing EGL_HEIGHT
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
    }

    // Missing EGL_IOSURFACE_PLANE_ANGLE
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
    }

    // Missing EGL_TEXTURE_TARGET - EGL_BAD_MATCH from the base spec of
    // eglCreatePbufferFromClientBuffer
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_MATCH);
    }

    // Missing EGL_TEXTURE_INTERNAL_FORMAT_ANGLE
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
    }

    // Missing EGL_TEXTURE_FORMAT - EGL_BAD_MATCH from the base spec of
    // eglCreatePbufferFromClientBuffer
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_MATCH);
    }

    // Missing EGL_TEXTURE_TYPE_ANGLE
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
    }
}

// Test the validation errors for bad parameters for eglCreatePbufferFromClientBuffer with IOSurface
TEST_P(IOSurfaceClientBufferTest, NegativeValidationBadAttributes)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(10, 10, 'BGRA', 4);

    // Success case
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format off

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE, ioSurface.get(), mConfig, attribs);
        EXPECT_NE(EGL_NO_SURFACE, pbuffer);

        EGLBoolean result = eglDestroySurface(mDisplay, pbuffer);
        EXPECT_EGL_TRUE(result);
        EXPECT_EGL_SUCCESS();
    }

    // EGL_TEXTURE_FORMAT must be EGL_TEXTURE_RGBA
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGB,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // EGL_WIDTH must be at least 1
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         0,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // EGL_WIDTH must be at most the width of the IOSurface
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         11,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // EGL_HEIGHT must be at least 1
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        0,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // EGL_HEIGHT must be at most the height of the IOSurface
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        11,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // EGL_TEXTURE_FORMAT must be equal to the config's texture target
    {
        EGLint target      = getTextureTarget();
        EGLint wrongTarget = 0;
        switch (target)
        {
            case EGL_TEXTURE_RECTANGLE_ANGLE:
                wrongTarget = EGL_TEXTURE_2D;
                break;
            case EGL_TEXTURE_2D:
                wrongTarget = EGL_TEXTURE_RECTANGLE_ANGLE;
                break;
            default:
                break;
        }
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                wrongTarget,
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // EGL_IOSURFACE_PLANE_ANGLE must be at least 0
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         -1,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // EGL_IOSURFACE_PLANE_ANGLE must less than the number of planes of the IOSurface
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         1,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_BGRA_EXT,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_UNSIGNED_BYTE,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }

    // The internal format / type most be listed in the table
    {
        // clang-format off
        const EGLint attribs[] = {
            EGL_WIDTH,                         10,
            EGL_HEIGHT,                        10,
            EGL_IOSURFACE_PLANE_ANGLE,         0,
            EGL_TEXTURE_TARGET,                getTextureTarget(),
            EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_RGBA,
            EGL_TEXTURE_FORMAT,                EGL_TEXTURE_RGBA,
            EGL_TEXTURE_TYPE_ANGLE,            GL_FLOAT,
            EGL_NONE,                          EGL_NONE,
        };
        // clang-format on

        EGLSurface pbuffer = eglCreatePbufferFromClientBuffer(mDisplay, EGL_IOSURFACE_ANGLE,
                                                              ioSurface.get(), mConfig, attribs);
        EXPECT_EQ(EGL_NO_SURFACE, pbuffer);
        EXPECT_EGL_ERROR(EGL_BAD_ATTRIBUTE);
    }
}

// Test IOSurface pbuffers can be made current
TEST_P(IOSurfaceClientBufferTest, MakeCurrent)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(10, 10, 'BGRA', 4);

    EGLSurface pbuffer;
    createIOSurfacePbuffer(ioSurface, 10, 10, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, &pbuffer);

    EGLContext context = getEGLWindow()->getContext();
    EGLBoolean result  = eglMakeCurrent(mDisplay, pbuffer, pbuffer, context);
    EXPECT_EGL_TRUE(result);
    EXPECT_EGL_SUCCESS();
    // The test harness expects the EGL state to be restored before the test exits.
    result = eglMakeCurrent(mDisplay, getEGLWindow()->getSurface(), getEGLWindow()->getSurface(),
                            context);
    EXPECT_EGL_TRUE(result);
    EXPECT_EGL_SUCCESS();
}

// Test reading from BGRX8888 IOSurfaces with bound texture's base/max level set to zero.
// This to verify that changing base/level shouldn't delete the binding.
// bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1337324
TEST_P(IOSurfaceClientBufferTest, ReadFromBGRX8888IOSurfaceWithTexBaseMaxLevelSetToZero)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLColor color(3, 2, 1, 4);
    doSampleTestWithExtraSteps(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, &color, sizeof(color),
                               R | G | B, /* extra steps */ [this] {
                                   glTexParameteri(getGLTextureTarget(), GL_TEXTURE_BASE_LEVEL, 0);
                                   glTexParameteri(getGLTextureTarget(), GL_TEXTURE_MAX_LEVEL, 0);
                               });
}

// Test that the following scenario works:
// - change IOSurface bound texture's max level to 0.
// - attach IOSurface bound texture to a FBO 1.
// - bind FBO 1
// - clear FBO 1 -> this should trigger render targets initialization in backends.
// - bind FBO 0.
// - draw IOSurface bound texture to FBO 0.
//   -> In the past, this could trigger the texture's render targets invalidation in metal backend.
//   See https://issues.chromium.org/issues/335353385
// - bind FBO 1
// - blit FBO 0 to FBO 1.
//   -> this will reconstruct render pass descriptor in metal backend.
// - flush to restart render encoder with new render pass descriptor.
// - draw.
TEST_P(IOSurfaceClientBufferTest, SetMaxLevelWouldInvalidateRenderTargetBug)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);

    ANGLE_GL_PROGRAM(blueProgram, angle::essl1_shaders::vs::Simple(),
                     angle::essl1_shaders::fs::Blue());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLTexture texture;
    glBindTexture(getGLTextureTarget(), texture);

    // Bind the IOSurface to a texture.
    EGLSurface pbuffer;
    bindIOSurfaceToTexture(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, &pbuffer, &texture);

    // 1. Change the texture's max level to 0.
    glTexParameteri(getGLTextureTarget(), GL_TEXTURE_MAX_LEVEL, 0);
    EXPECT_GL_NO_ERROR();

    // 2. Attach IOSurface bound texture to a FBO and clear it.
    GLFramebuffer fbo;
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    EXPECT_GL_NO_ERROR();
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getGLTextureTarget(), texture, 0);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);
    glClearColor(1.0f / 255.0f, 2.0f / 255.0f, 3.0f / 255.0f, 4.0f / 255.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 3. Draw IOSurface bound texture to default FBO.
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    doSampleTestWithTexture(texture, R | G | B);

    // 3. Draw to custom FBO again
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
    glClear(GL_COLOR_BUFFER_BIT);
    // Using a blit is important: it will trigger render pass reconstruction in
    // metal backend due to DIRTY_BIT_COLOR_BUFFER_CONTENTS_0 dirty bit.
    glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST);
    glFlush();  // flush so that render encoder will be recreated again in metal backend.
    glUseProgram(blueProgram);
    drawQuad(blueProgram, angle::essl1_shaders::PositionAttrib(), 0.5f);
    glFlush();

    // Expect the final color to be accumulated color
    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor(1, 2, 255, 255));
}

// Test using GL_EXT_multisampled_render_to_texture to render to BGRX IOSurface.
TEST_P(IOSurfaceClientBufferTest, MultisampledRenderToTextureBGRX)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));

    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());

    ANGLE_GL_PROGRAM(colorProgram, angle::essl1_shaders::vs::Simple(),
                     angle::essl1_shaders::fs::UniformColor());

    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    GLTexture texture;
    glBindTexture(getGLTextureTarget(), texture);

    // Bind the IOSurface to a texture.
    EGLSurface pbuffer;
    bindIOSurfaceToTexture(ioSurface, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, &pbuffer, &texture);

    // Attach IOSurface bound texture to a single sampled FBO and clear it.
    GLFramebuffer singleSampledFbo;
    glBindFramebuffer(GL_FRAMEBUFFER, singleSampledFbo);
    EXPECT_GL_NO_ERROR();
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getGLTextureTarget(), texture, 0);
    glClearColor(1.0f / 255.0f, 2.0f / 255.0f, 3.0f / 255.0f, 4.0f / 255.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // Attach IOSurface to a multisampled FBO and draw translucent blue color
    GLFramebuffer multisampledSampledFbo;
    glBindFramebuffer(GL_FRAMEBUFFER, multisampledSampledFbo);
    EXPECT_GL_NO_ERROR();
    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getGLTextureTarget(),
                                         texture, 0, 4);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);
    glUseProgram(colorProgram);
    const GLint colorUniformLocation =
        glGetUniformLocation(colorProgram, angle::essl1_shaders::ColorUniform());
    ASSERT_NE(colorUniformLocation, -1);
    glUniform4f(colorUniformLocation, 0, 0, 1, 0.5f);
    drawQuad(colorProgram, angle::essl1_shaders::PositionAttrib(), 0.5f);

    // Expect the final color to be accumulated color
    glBindFramebuffer(GL_FRAMEBUFFER, singleSampledFbo);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor(1, 2, 255, 255));
}

// TODO(cwallez@chromium.org): Test setting width and height to less than the IOSurface's work as
// expected.

// Test that binding an IOSurface and setting base level to 3 does not cause an error.
TEST_P(IOSurfaceClientBufferTest, SetNonZeroBaseLevel)
{
    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
    ANGLE_SKIP_TEST_IF(getGLTextureTarget() != GL_TEXTURE_2D);
    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);

    // Create a 1x1 IOSurface
    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);

    // Bind it to a texture
    EGLSurface pbuffer;
    GLTexture texture;
    bindIOSurfaceToTexture(ioSurface, 1, 1, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, &pbuffer, &texture);

    // Set base level to 3
    glTexParameteri(getGLTextureTarget(), GL_TEXTURE_BASE_LEVEL, 3);
    EXPECT_GL_NO_ERROR();

    // Clean up
    EGLBoolean result = eglReleaseTexImage(mDisplay, pbuffer, EGL_BACK_BUFFER);
    EXPECT_EGL_TRUE(result);
    result = eglDestroySurface(mDisplay, pbuffer);
    EXPECT_EGL_TRUE(result);
}

ANGLE_INSTANTIATE_TEST(IOSurfaceClientBufferTest,
                       ES2_VULKAN_SWIFTSHADER(),
                       ES3_VULKAN_SWIFTSHADER(),
                       ES2_METAL(),
                       ES3_METAL(),
                       ES3_METAL()
                           .enable(Feature::EnableMultisampledRenderToTextureOnNonTilers)
                           .enable(Feature::EmulateDontCareLoadWithRandomClear));
