//
// Copyright 2015 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.
//

#include "test_utils/ANGLETest.h"

#include "test_utils/gl_raii.h"

using namespace angle;

namespace
{

void TexImageCubeMapFaces(GLint level,
                          GLenum internalformat,
                          GLsizei width,
                          GLenum format,
                          GLenum type,
                          void *pixels)
{
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, level, internalformat, width, width, 0, format,
                 type, pixels);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, level, internalformat, width, width, 0, format,
                 type, pixels);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, level, internalformat, width, width, 0, format,
                 type, pixels);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, level, internalformat, width, width, 0, format,
                 type, pixels);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, level, internalformat, width, width, 0, format,
                 type, pixels);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, level, internalformat, width, width, 0, format,
                 type, pixels);
}

class BaseMipmapTest : public ANGLETest
{
  protected:
    void clearAndDrawQuad(GLuint program, GLsizei viewportWidth, GLsizei viewportHeight)
    {
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        glViewport(0, 0, viewportWidth, viewportHeight);
        ASSERT_GL_NO_ERROR();

        drawQuad(program, "position", 0.0f);
    }
};

}  // namespace

class MipmapTest : public BaseMipmapTest
{
  protected:
    MipmapTest()
        : m2DProgram(0),
          mCubeProgram(0),
          mTexture2D(0),
          mTextureCube(0),
          mLevelZeroBlueInitData(nullptr),
          mLevelZeroWhiteInitData(nullptr),
          mLevelOneInitData(nullptr),
          mLevelTwoInitData(nullptr),
          mOffscreenFramebuffer(0)
    {
        setWindowWidth(128);
        setWindowHeight(128);
        setConfigRedBits(8);
        setConfigGreenBits(8);
        setConfigBlueBits(8);
        setConfigAlphaBits(8);
    }

    void setUp2DProgram()
    {
        // Vertex Shader source
        const std::string vs =
            R"(attribute vec4 position;
            varying vec2 vTexCoord;

            void main()
            {
                gl_Position = position;
                vTexCoord   = (position.xy * 0.5) + 0.5;
            })";

        // Fragment Shader source
        const std::string fs =
            R"(precision mediump float;

            uniform sampler2D uTexture;
            varying vec2 vTexCoord;

            void main()
            {
                gl_FragColor = texture2D(uTexture, vTexCoord);
            })";

        m2DProgram = CompileProgram(vs, fs);
        ASSERT_NE(0u, m2DProgram);
    }

    void setUpCubeProgram()
    {
        // A simple vertex shader for the texture cube
        const std::string cubeVS =
            R"(attribute vec4 position;
            varying vec4 vPosition;
            void main()
            {
                gl_Position = position;
                vPosition = position;
            })";

        // A very simple fragment shader to sample from the negative-Y face of a texture cube.
        const std::string cubeFS =
            R"(precision mediump float;
            uniform samplerCube uTexture;
            varying vec4 vPosition;

            void main()
            {
                gl_FragColor = textureCube(uTexture, vec3(vPosition.x, -1, vPosition.y));
            })";

        mCubeProgram = CompileProgram(cubeVS, cubeFS);
        ASSERT_NE(0u, mCubeProgram);
    }

    void SetUp() override
    {
        ANGLETest::SetUp();

        setUp2DProgram();

        setUpCubeProgram();

        mLevelZeroBlueInitData = createRGBInitData(getWindowWidth(), getWindowHeight(), 0, 0, 255); // Blue
        mLevelZeroWhiteInitData = createRGBInitData(getWindowWidth(), getWindowHeight(), 255, 255, 255); // White
        mLevelOneInitData = createRGBInitData((getWindowWidth() / 2), (getWindowHeight() / 2), 0, 255, 0);   // Green
        mLevelTwoInitData = createRGBInitData((getWindowWidth() / 4), (getWindowHeight() / 4), 255, 0, 0);   // Red

        glGenFramebuffers(1, &mOffscreenFramebuffer);
        glGenTextures(1, &mTexture2D);

        // Initialize the texture2D to be empty, and don't use mips.
        glBindTexture(GL_TEXTURE_2D, mTexture2D);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, getWindowWidth(), getWindowHeight(), 0, GL_RGB,
                     GL_UNSIGNED_BYTE, nullptr);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

        ASSERT_EQ(getWindowWidth(), getWindowHeight());

        // Create a non-mipped texture cube. Set the negative-Y face to be blue.
        glGenTextures(1, &mTextureCube);
        glBindTexture(GL_TEXTURE_CUBE_MAP, mTextureCube);
        TexImageCubeMapFaces(0, GL_RGB, getWindowWidth(), GL_RGB, GL_UNSIGNED_BYTE, nullptr);
        glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGB, getWindowWidth(), getWindowWidth(),
                     0, GL_RGB, GL_UNSIGNED_BYTE, mLevelZeroBlueInitData);

        // Complete the texture cube without mipmaps to start with.
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

        ASSERT_GL_NO_ERROR();
    }

    void TearDown() override
    {
        glDeleteProgram(m2DProgram);
        glDeleteProgram(mCubeProgram);
        glDeleteFramebuffers(1, &mOffscreenFramebuffer);
        glDeleteTextures(1, &mTexture2D);
        glDeleteTextures(1, &mTextureCube);

        SafeDeleteArray(mLevelZeroBlueInitData);
        SafeDeleteArray(mLevelZeroWhiteInitData);
        SafeDeleteArray(mLevelOneInitData);
        SafeDeleteArray(mLevelTwoInitData);

        ANGLETest::TearDown();
    }

    GLubyte *createRGBInitData(GLint width, GLint height, GLint r, GLint g, GLint b)
    {
        GLubyte *data = new GLubyte[3 * width * height];

        for (int i = 0; i < width * height; i+=1)
        {
            data[3 * i + 0] = static_cast<GLubyte>(r);
            data[3 * i + 1] = static_cast<GLubyte>(g);
            data[3 * i + 2] = static_cast<GLubyte>(b);
        }

        return data;
    }

    void clearTextureLevel0(GLenum textarget,
                            GLuint texture,
                            GLfloat red,
                            GLfloat green,
                            GLfloat blue,
                            GLfloat alpha)
    {
        glBindFramebuffer(GL_FRAMEBUFFER, mOffscreenFramebuffer);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, textarget, texture, 0);
        ASSERT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_FRAMEBUFFER));
        glClearColor(red, green, blue, alpha);
        glClear(GL_COLOR_BUFFER_BIT);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }

    GLuint m2DProgram;
    GLuint mCubeProgram;
    GLuint mTexture2D;
    GLuint mTextureCube;

    GLubyte* mLevelZeroBlueInitData;
    GLubyte* mLevelZeroWhiteInitData;
    GLubyte* mLevelOneInitData;
    GLubyte* mLevelTwoInitData;

  private:
    GLuint mOffscreenFramebuffer;
};

class MipmapTestES3 : public BaseMipmapTest
{
  protected:
    MipmapTestES3()
        : mTexture(0),
          mArrayProgram(0),
          mTextureArraySliceUniformLocation(-1),
          m3DProgram(0),
          mTexture3DSliceUniformLocation(-1),
          mTexture3DLODUniformLocation(-1),
          m2DProgram(0)

    {
        setWindowWidth(128);
        setWindowHeight(128);
        setConfigRedBits(8);
        setConfigGreenBits(8);
        setConfigBlueBits(8);
        setConfigAlphaBits(8);
    }

    std::string vertexShaderSource()
    {
        // Don't put "#version ..." on its own line. See [cpp]p1:
        // "If there are sequences of preprocessing tokens within the list of arguments that
        //  would otherwise act as preprocessing directives, the behavior is undefined"
        return
            R"(#version 300 es
            precision highp float;
            in vec4 position;
            out vec2 texcoord;

            void main()
            {
                gl_Position = vec4(position.xy, 0.0, 1.0);
                texcoord = (position.xy * 0.5) + 0.5;
            })";
    }

    void setUpArrayProgram()
    {
        const std::string fragmentShaderSourceArray =
            R"(#version 300 es
            precision highp float;
            uniform highp sampler2DArray tex;
            uniform int slice;
            in vec2 texcoord;
            out vec4 out_FragColor;

            void main()
            {
                out_FragColor = texture(tex, vec3(texcoord, float(slice)));
            })";

        mArrayProgram = CompileProgram(vertexShaderSource(), fragmentShaderSourceArray);
        if (mArrayProgram == 0)
        {
            FAIL() << "shader compilation failed.";
        }

        mTextureArraySliceUniformLocation = glGetUniformLocation(mArrayProgram, "slice");
        ASSERT_NE(-1, mTextureArraySliceUniformLocation);

        glUseProgram(mArrayProgram);
        glUseProgram(0);
        ASSERT_GL_NO_ERROR();
    }

    void setUp3DProgram()
    {
        const std::string fragmentShaderSource3D =
            R"(#version 300 es
            precision highp float;
            uniform highp sampler3D tex;
            uniform float slice;
            uniform float lod;
            in vec2 texcoord;
            out vec4 out_FragColor;

            void main()
            {
                out_FragColor = textureLod(tex, vec3(texcoord, slice), lod);
            })";

        m3DProgram = CompileProgram(vertexShaderSource(), fragmentShaderSource3D);
        if (m3DProgram == 0)
        {
            FAIL() << "shader compilation failed.";
        }

        mTexture3DSliceUniformLocation = glGetUniformLocation(m3DProgram, "slice");
        ASSERT_NE(-1, mTexture3DSliceUniformLocation);

        mTexture3DLODUniformLocation = glGetUniformLocation(m3DProgram, "lod");
        ASSERT_NE(-1, mTexture3DLODUniformLocation);

        glUseProgram(m3DProgram);
        glUniform1f(mTexture3DLODUniformLocation, 0);
        glUseProgram(0);
        ASSERT_GL_NO_ERROR();
    }

    void setUp2DProgram()
    {
        const std::string fragmentShaderSource2D =
            R"(#version 300 es
            precision highp float;
            uniform highp sampler2D tex;
            in vec2 texcoord;
            out vec4 out_FragColor;

            void main()
            {
                out_FragColor = texture(tex, texcoord);
            })";

        m2DProgram = CompileProgram(vertexShaderSource(), fragmentShaderSource2D);
        ASSERT_NE(0u, m2DProgram);

        ASSERT_GL_NO_ERROR();
    }

    void setUpCubeProgram()
    {
        // A very simple fragment shader to sample from the negative-Y face of a texture cube.
        const std::string cubeFS =
            R"(#version 300 es
            precision mediump float;
            uniform samplerCube uTexture;
            in vec2 texcoord;
            out vec4 out_FragColor;

            void main()
            {
                out_FragColor = texture(uTexture, vec3(texcoord.x, -1, texcoord.y));
            })";

        mCubeProgram = CompileProgram(vertexShaderSource(), cubeFS);
        ASSERT_NE(0u, mCubeProgram);

        ASSERT_GL_NO_ERROR();
    }

    void SetUp() override
    {
        ANGLETest::SetUp();

        glGenTextures(1, &mTexture);
        ASSERT_GL_NO_ERROR();

        setUpArrayProgram();
        setUp3DProgram();
        setUp2DProgram();
        setUpCubeProgram();
    }

    void TearDown() override
    {
        glDeleteTextures(1, &mTexture);

        glDeleteProgram(mArrayProgram);
        glDeleteProgram(m3DProgram);
        glDeleteProgram(m2DProgram);
        glDeleteProgram(mCubeProgram);

        ANGLETest::TearDown();
    }

    GLuint mTexture;

    GLuint mArrayProgram;
    GLint mTextureArraySliceUniformLocation;

    GLuint m3DProgram;
    GLint mTexture3DSliceUniformLocation;
    GLint mTexture3DLODUniformLocation;

    GLuint m2DProgram;

    GLuint mCubeProgram;
};

// This test uses init data for the first three levels of the texture. It passes the level 0 data in, then renders, then level 1, then renders, etc.
// This ensures that renderers using the zero LOD workaround (e.g. D3D11 FL9_3) correctly pass init data to the mipmapped texture,
// even if the the zero-LOD texture is currently in use.
TEST_P(MipmapTest, DISABLED_ThreeLevelsInitData)
{
    // Pass in level zero init data.
    glBindTexture(GL_TEXTURE_2D, mTexture2D);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, getWindowWidth(), getWindowHeight(), 0, GL_RGB, GL_UNSIGNED_BYTE, mLevelZeroBlueInitData);
    ASSERT_GL_NO_ERROR();

    // Disable mips.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    // Draw a full-sized quad, and check it's blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Draw a half-sized quad, and check it's blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::blue);

    // Draw a quarter-sized quad, and check it's blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::blue);

    // Complete the texture by initializing the remaining levels.
    int n = 1;
    while (getWindowWidth() / (1U << n) >= 1)
    {
        glTexImage2D(GL_TEXTURE_2D, n, GL_RGB, getWindowWidth() / (1U << n),
                     getWindowWidth() / (1U << n), 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
        ASSERT_GL_NO_ERROR();
        n+=1;
    }

    // Pass in level one init data.
    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGB, getWindowWidth() / 2, getWindowHeight() / 2, 0, GL_RGB, GL_UNSIGNED_BYTE, mLevelOneInitData);
    ASSERT_GL_NO_ERROR();

    // Draw a full-sized quad, and check it's blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Draw a half-sized quad, and check it's blue. We've not enabled mipmaps yet, so our init data for level one shouldn't be used.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::blue);

    // Enable mipmaps.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);

    // Draw a half-sized quad, and check it's green.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::green);

    // Draw a quarter-sized quad, and check it's black, since we've not passed any init data for level two.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::black);

    // Pass in level two init data.
    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGB, getWindowWidth() / 4, getWindowHeight() / 4, 0, GL_RGB, GL_UNSIGNED_BYTE, mLevelTwoInitData);
    ASSERT_GL_NO_ERROR();

    // Draw a full-sized quad, and check it's blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Draw a half-sized quad, and check it's green.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::green);

    // Draw a quarter-sized quad, and check it's red.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::red);

    // Now disable mipmaps again, and render multiple sized quads. They should all be blue, since level 0 is blue.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::blue);
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::blue);

    // Now reset level 0 to white, keeping mipmaps disabled. Then, render various sized quads. They should be white.
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, getWindowWidth(), getWindowHeight(), 0, GL_RGB, GL_UNSIGNED_BYTE, mLevelZeroWhiteInitData);
    ASSERT_GL_NO_ERROR();

    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::white);
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::white);
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::white);

    // Then enable mipmaps again. The quads should be white, green, red respectively.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);

    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::white);
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::green);
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::red);
}

// This test generates (and uses) mipmaps on a texture using init data. D3D11 will use a non-renderable TextureStorage for this.
// The test then disables mips, renders to level zero of the texture, and reenables mips before using the texture again.
// To do this, D3D11 has to convert the TextureStorage into a renderable one.
// This test ensures that the conversion works correctly.
// In particular, on D3D11 Feature Level 9_3 it ensures that both the zero LOD workaround texture AND the 'normal' texture are copied during conversion.
TEST_P(MipmapTest, GenerateMipmapFromInitDataThenRender)
{
    // Pass in initial data so the texture is blue.
    glBindTexture(GL_TEXTURE_2D, mTexture2D);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, getWindowWidth(), getWindowHeight(), 0, GL_RGB, GL_UNSIGNED_BYTE, mLevelZeroBlueInitData);

    // Then generate the mips.
    glGenerateMipmap(GL_TEXTURE_2D);
    ASSERT_GL_NO_ERROR();

    // Enable mipmaps.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);

    // Now draw the texture to various different sized areas.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Use mip level 1
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::blue);

    // Use mip level 2
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::blue);

    ASSERT_GL_NO_ERROR();

    // Disable mips. Render a quad using the texture and ensure it's blue.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Clear level 0 of the texture to red.
    clearTextureLevel0(GL_TEXTURE_2D, mTexture2D, 1.0f, 0.0f, 0.0f, 1.0f);

    // Reenable mips, and try rendering different-sized quads.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);

    // Level 0 is now red, so this should render red.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::red);

    // Use mip level 1, blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::blue);

    // Use mip level 2, blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::blue);
}

// This test ensures that mips are correctly generated from a rendered image.
// In particular, on D3D11 Feature Level 9_3, the clear call will be performed on the zero-level texture, rather than the mipped one.
// The test ensures that the zero-level texture is correctly copied into the mipped texture before the mipmaps are generated.
TEST_P(MipmapTest, GenerateMipmapFromRenderedImage)
{
    glBindTexture(GL_TEXTURE_2D, mTexture2D);
    // Clear the texture to blue.
    clearTextureLevel0(GL_TEXTURE_2D, mTexture2D, 0.0f, 0.0f, 1.0f, 1.0f);

    // Then generate the mips
    glGenerateMipmap(GL_TEXTURE_2D);
    ASSERT_GL_NO_ERROR();

    // Enable mips.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);

    // Now draw the texture to various different sized areas.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Use mip level 1
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::blue);

    // Use mip level 2
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::blue);
}

// Test to ensure that rendering to a mipmapped texture works, regardless of whether mipmaps are enabled or not.
// TODO: This test hits a texture rebind bug in the D3D11 renderer. Fix this.
TEST_P(MipmapTest, RenderOntoLevelZeroAfterGenerateMipmap)
{
    // TODO(geofflang): Figure out why this is broken on AMD OpenGL
    if ((IsAMD() || IsIntel()) && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
    {
        std::cout << "Test skipped on Intel/AMD OpenGL." << std::endl;
        return;
    }

    glBindTexture(GL_TEXTURE_2D, mTexture2D);

    // Clear the texture to blue.
    clearTextureLevel0(GL_TEXTURE_2D, mTexture2D, 0.0f, 0.0f, 1.0f, 1.0f);

    // Now, draw the texture to a quad that's the same size as the texture. This draws to the default framebuffer.
    // The quad should be blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Now go back to the texture, and generate mips on it.
    glGenerateMipmap(GL_TEXTURE_2D);
    ASSERT_GL_NO_ERROR();

    // Now try rendering the textured quad again. Note: we've not told GL to use the generated mips.
    // The quad should be blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Now tell GL to use the generated mips.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    EXPECT_GL_NO_ERROR();

    // Now render the textured quad again. It should be still be blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);

    // Now render the textured quad to an area smaller than the texture (i.e. to force minification). This should be blue.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::blue);

    // Now clear the texture to green. This just clears the top level. The lower mips should remain blue.
    clearTextureLevel0(GL_TEXTURE_2D, mTexture2D, 0.0f, 1.0f, 0.0f, 1.0f);

    // Render a textured quad equal in size to the texture. This should be green, since we just cleared level 0.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::green);

    // Render a small textured quad. This forces minification, so should render blue (the color of levels 1+).
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::blue);

    // Disable mipmaps again
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    ASSERT_GL_NO_ERROR();

    // Render a textured quad equal in size to the texture. This should be green, the color of level 0 in the texture.
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::green);

    // Render a small textured quad. This would force minification if mips were enabled, but they're not. Therefore, this should be green.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::green);
}

// Regression test for a bug that cause mipmaps to only generate using the top left corner as input.
TEST_P(MipmapTest, MipMapGenerationD3D9Bug)
{
    if (!extensionEnabled("GL_EXT_texture_storage") || !extensionEnabled("GL_OES_rgb8_rgba8") ||
        !extensionEnabled("GL_ANGLE_texture_usage"))
    {
        std::cout << "Test skipped due to missing extensions." << std::endl;
        return;
    }

    const GLColor mip0Color[4] = {
        GLColor::red, GLColor::green, GLColor::red, GLColor::green,
    };
    const GLColor mip1Color = GLColor(127, 127, 0, 255);

    GLTexture texture;
    glBindTexture(GL_TEXTURE_2D, texture.get());
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_USAGE_ANGLE, GL_FRAMEBUFFER_ATTACHMENT_ANGLE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    glTexStorage2DEXT(GL_TEXTURE_2D, 2, GL_RGBA8_OES, 2, 2);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, mip0Color);
    glGenerateMipmap(GL_TEXTURE_2D);

    // Only draw to a 1 pixel viewport so the lower mip is used
    clearAndDrawQuad(m2DProgram, 1, 1);
    EXPECT_PIXEL_COLOR_NEAR(0, 0, mip1Color, 1.0);
}

// This test ensures that the level-zero workaround for TextureCubes (on D3D11 Feature Level 9_3)
// works as expected. It tests enabling/disabling mipmaps, generating mipmaps, and rendering to level zero.
TEST_P(MipmapTest, TextureCubeGeneralLevelZero)
{
    glBindTexture(GL_TEXTURE_CUBE_MAP, mTextureCube);

    // Draw. Since the negative-Y face's is blue, this should be blue.
    clearAndDrawQuad(mCubeProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);

    // Generate mipmaps, and render. This should be blue.
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    clearAndDrawQuad(mCubeProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);

    // Draw using a smaller viewport (to force a lower LOD of the texture). This should still be blue.
    clearAndDrawQuad(mCubeProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);

    // Now clear the negative-Y face of the cube to red.
    clearTextureLevel0(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, mTextureCube, 1.0f, 0.0f, 0.0f, 1.0f);

    // Draw using a full-size viewport. This should be red.
    clearAndDrawQuad(mCubeProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);

    // Draw using a quarter-size viewport, to force a lower LOD. This should be *BLUE*, since we only cleared level zero
    // of the negative-Y face to red, and left its mipmaps blue.
    clearAndDrawQuad(mCubeProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);

    // Disable mipmaps again, and draw a to a quarter-size viewport.
    // Since this should use level zero of the texture, this should be *RED*.
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    clearAndDrawQuad(mCubeProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
}

// This test ensures that rendering to level-zero of a TextureCube works as expected.
TEST_P(MipmapTest, TextureCubeRenderToLevelZero)
{
    glBindTexture(GL_TEXTURE_CUBE_MAP, mTextureCube);

    // Draw. Since the negative-Y face's is blue, this should be blue.
    clearAndDrawQuad(mCubeProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);

    // Now clear the negative-Y face of the cube to red.
    clearTextureLevel0(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, mTextureCube, 1.0f, 0.0f, 0.0f, 1.0f);

    // Draw using a full-size viewport. This should be red.
    clearAndDrawQuad(mCubeProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);

    // Draw a to a quarter-size viewport. This should also be red.
    clearAndDrawQuad(mCubeProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
}

// Creates a mipmapped 2D array texture with three layers, and calls ANGLE's GenerateMipmap.
// Then tests if the mipmaps are rendered correctly for all three layers.
TEST_P(MipmapTestES3, MipmapsForTextureArray)
{
    int px = getWindowWidth() / 2;
    int py = getWindowHeight() / 2;

    glBindTexture(GL_TEXTURE_2D_ARRAY, mTexture);

    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 5, GL_RGBA8, 16, 16, 3);

    // Fill the first layer with red
    std::vector<GLColor> pixelsRed(16 * 16, GLColor::red);
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 16, 16, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                    pixelsRed.data());

    // Fill the second layer with green
    std::vector<GLColor> pixelsGreen(16 * 16, GLColor::green);
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, 16, 16, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                    pixelsGreen.data());

    // Fill the third layer with blue
    std::vector<GLColor> pixelsBlue(16 * 16, GLColor::blue);
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, 16, 16, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                    pixelsBlue.data());

    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    EXPECT_GL_NO_ERROR();

    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);

    EXPECT_GL_NO_ERROR();

    glUseProgram(mArrayProgram);

    EXPECT_GL_NO_ERROR();

    // Draw the first slice
    glUniform1i(mTextureArraySliceUniformLocation, 0);
    drawQuad(mArrayProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::red);

    // Draw the second slice
    glUniform1i(mTextureArraySliceUniformLocation, 1);
    drawQuad(mArrayProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::green);

    // Draw the third slice
    glUniform1i(mTextureArraySliceUniformLocation, 2);
    drawQuad(mArrayProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::blue);
}

// Create a mipmapped 2D array texture with more layers than width / height, and call
// GenerateMipmap.
TEST_P(MipmapTestES3, MipmapForDeepTextureArray)
{
    int px = getWindowWidth() / 2;
    int py = getWindowHeight() / 2;

    glBindTexture(GL_TEXTURE_2D_ARRAY, mTexture);

    // Fill the whole texture with red.
    std::vector<GLColor> pixelsRed(2 * 2 * 4, GLColor::red);
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 2, 2, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE,
                 pixelsRed.data());

    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    EXPECT_GL_NO_ERROR();

    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);

    EXPECT_GL_NO_ERROR();

    glUseProgram(mArrayProgram);

    EXPECT_GL_NO_ERROR();

    // Draw the first slice
    glUniform1i(mTextureArraySliceUniformLocation, 0);
    drawQuad(mArrayProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::red);

    // Draw the fourth slice
    glUniform1i(mTextureArraySliceUniformLocation, 3);
    drawQuad(mArrayProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::red);
}

// Creates a mipmapped 3D texture with two layers, and calls ANGLE's GenerateMipmap.
// Then tests if the mipmaps are rendered correctly for all two layers.
TEST_P(MipmapTestES3, MipmapsForTexture3D)
{
    int px = getWindowWidth() / 2;
    int py = getWindowHeight() / 2;

    glBindTexture(GL_TEXTURE_3D, mTexture);

    glTexStorage3D(GL_TEXTURE_3D, 5, GL_RGBA8, 16, 16, 2);

    // Fill the first layer with red
    std::vector<GLColor> pixelsRed(16 * 16, GLColor::red);
    glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 16, 16, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                    pixelsRed.data());

    // Fill the second layer with green
    std::vector<GLColor> pixelsGreen(16 * 16, GLColor::green);
    glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 1, 16, 16, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                    pixelsGreen.data());

    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    EXPECT_GL_NO_ERROR();

    glGenerateMipmap(GL_TEXTURE_3D);

    EXPECT_GL_NO_ERROR();

    glUseProgram(m3DProgram);

    EXPECT_GL_NO_ERROR();

    // Mipmap level 0
    // Draw the first slice
    glUniform1f(mTexture3DLODUniformLocation, 0.);
    glUniform1f(mTexture3DSliceUniformLocation, 0.25f);
    drawQuad(m3DProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::red);

    // Draw the second slice
    glUniform1f(mTexture3DSliceUniformLocation, 0.75f);
    drawQuad(m3DProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::green);

    // Mipmap level 1
    // The second mipmap should only have one slice.
    glUniform1f(mTexture3DLODUniformLocation, 1.);
    drawQuad(m3DProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_NEAR(px, py, 127, 127, 0, 255, 1.0);

    glUniform1f(mTexture3DSliceUniformLocation, 0.75f);
    drawQuad(m3DProgram, "position", 0.5f);
    EXPECT_GL_NO_ERROR();
    EXPECT_PIXEL_NEAR(px, py, 127, 127, 0, 255, 1.0);
}

// Create a 2D texture with levels 0-2, call GenerateMipmap with base level 1 so that level 0 stays
// the same, and then sample levels 0 and 2.
// GLES 3.0.4 section 3.8.10:
// "Mipmap generation replaces texel array levels levelbase + 1 through q with arrays derived from
// the levelbase array, regardless of their previous contents. All other mipmap arrays, including
// the levelbase array, are left unchanged by this computation."
TEST_P(MipmapTestES3, GenerateMipmapBaseLevel)
{
    if (IsAMD() && IsDesktopOpenGL())
    {
        // Observed incorrect rendering on AMD, sampling level 2 returns black.
        std::cout << "Test skipped on AMD OpenGL." << std::endl;
        return;
    }

    glBindTexture(GL_TEXTURE_2D, mTexture);

    ASSERT_EQ(getWindowWidth(), getWindowHeight());

    // Fill level 0 with blue
    std::vector<GLColor> pixelsBlue(getWindowWidth() * getWindowHeight(), GLColor::blue);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, getWindowWidth(), getWindowHeight(), 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, pixelsBlue.data());

    // Fill level 1 with red
    std::vector<GLColor> pixelsRed(getWindowWidth() * getWindowHeight() / 4, GLColor::red);
    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, getWindowWidth() / 2, getWindowHeight() / 2, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, pixelsRed.data());

    // Fill level 2 with green
    std::vector<GLColor> pixelsGreen(getWindowWidth() * getWindowHeight() / 16, GLColor::green);
    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, getWindowWidth() / 4, getWindowHeight() / 4, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, pixelsGreen.data());

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);

    EXPECT_GL_NO_ERROR();

    // The blue level 0 should be untouched by this since base level is 1.
    glGenerateMipmap(GL_TEXTURE_2D);

    EXPECT_GL_NO_ERROR();

    // Draw using level 2. It should be set to red by GenerateMipmap.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::red);

    // Draw using level 0. It should still be blue.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);
}

// Create a cube map with levels 0-2, call GenerateMipmap with base level 1 so that level 0 stays
// the same, and then sample levels 0 and 2.
// GLES 3.0.4 section 3.8.10:
// "Mipmap generation replaces texel array levels levelbase + 1 through q with arrays derived from
// the levelbase array, regardless of their previous contents. All other mipmap arrays, including
// the levelbase array, are left unchanged by this computation."
TEST_P(MipmapTestES3, GenerateMipmapCubeBaseLevel)
{
    if (IsAMD() && IsDesktopOpenGL())
    {
        // Observed incorrect rendering on AMD, sampling level 2 returns black.
        std::cout << "Test skipped on AMD OpenGL." << std::endl;
        return;
    }

    ASSERT_EQ(getWindowWidth(), getWindowHeight());

    glBindTexture(GL_TEXTURE_CUBE_MAP, mTexture);
    std::vector<GLColor> pixelsBlue(getWindowWidth() * getWindowWidth(), GLColor::blue);
    TexImageCubeMapFaces(0, GL_RGBA8, getWindowWidth(), GL_RGBA, GL_UNSIGNED_BYTE,
                         pixelsBlue.data());

    // Fill level 1 with red
    std::vector<GLColor> pixelsRed(getWindowWidth() * getWindowWidth() / 4, GLColor::red);
    TexImageCubeMapFaces(1, GL_RGBA8, getWindowWidth() / 2, GL_RGBA, GL_UNSIGNED_BYTE,
                         pixelsRed.data());

    // Fill level 2 with green
    std::vector<GLColor> pixelsGreen(getWindowWidth() * getWindowWidth() / 16, GLColor::green);
    TexImageCubeMapFaces(2, GL_RGBA8, getWindowWidth() / 4, GL_RGBA, GL_UNSIGNED_BYTE,
                         pixelsGreen.data());

    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 1);

    EXPECT_GL_NO_ERROR();

    // The blue level 0 should be untouched by this since base level is 1.
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

    EXPECT_GL_NO_ERROR();

    // Draw using level 2. It should be set to red by GenerateMipmap.
    clearAndDrawQuad(mCubeProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::red);

    if (IsNVIDIA() && IsOpenGL())
    {
        // Observed incorrect rendering on NVIDIA, level zero seems to be incorrectly affected by
        // GenerateMipmap.
        std::cout << "Test partially skipped on NVIDIA OpenGL." << std::endl;
        return;
    }

    // Draw using level 0. It should still be blue.
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 0);
    clearAndDrawQuad(mCubeProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);
}

// Create a texture with levels 0-2, call GenerateMipmap with max level 1 so that level 2 stays the
// same, and then sample levels 1 and 2.
// GLES 3.0.4 section 3.8.10:
// "Mipmap generation replaces texel array levels levelbase + 1 through q with arrays derived from
// the levelbase array, regardless of their previous contents. All other mipmap arrays, including
// the levelbase array, are left unchanged by this computation."
TEST_P(MipmapTestES3, GenerateMipmapMaxLevel)
{
    glBindTexture(GL_TEXTURE_2D, mTexture);

    // Fill level 0 with blue
    std::vector<GLColor> pixelsBlue(getWindowWidth() * getWindowHeight(), GLColor::blue);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, getWindowWidth(), getWindowHeight(), 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, pixelsBlue.data());

    // Fill level 1 with red
    std::vector<GLColor> pixelsRed(getWindowWidth() * getWindowHeight() / 4, GLColor::red);
    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, getWindowWidth() / 2, getWindowHeight() / 2, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, pixelsRed.data());

    // Fill level 2 with green
    std::vector<GLColor> pixelsGreen(getWindowWidth() * getWindowHeight() / 16, GLColor::green);
    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, getWindowWidth() / 4, getWindowHeight() / 4, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, pixelsGreen.data());

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);

    EXPECT_GL_NO_ERROR();

    // The green level 2 should be untouched by this since max level is 1.
    glGenerateMipmap(GL_TEXTURE_2D);

    EXPECT_GL_NO_ERROR();

    // Draw using level 1. It should be set to blue by GenerateMipmap.
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 2, getWindowHeight() / 2);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 4, getWindowHeight() / 4, GLColor::blue);

    // Draw using level 2. It should still be green.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
    clearAndDrawQuad(m2DProgram, getWindowWidth() / 4, getWindowHeight() / 4);
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 8, getWindowHeight() / 8, GLColor::green);
}

// Call GenerateMipmap with out-of-range base level. The spec is interpreted so that an out-of-range
// base level does not have a color-renderable/texture-filterable internal format, so the
// GenerateMipmap call generates INVALID_OPERATION. GLES 3.0.4 section 3.8.10:
// "If the levelbase array was not specified with an unsized internal format from table 3.3 or a
// sized internal format that is both color-renderable and texture-filterable according to table
// 3.13, an INVALID_OPERATION error is generated."
TEST_P(MipmapTestES3, GenerateMipmapBaseLevelOutOfRange)
{
    glBindTexture(GL_TEXTURE_2D, mTexture);

    // Fill level 0 with blue
    std::vector<GLColor> pixelsBlue(getWindowWidth() * getWindowHeight(), GLColor::blue);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, getWindowWidth(), getWindowHeight(), 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, pixelsBlue.data());

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1000);

    EXPECT_GL_NO_ERROR();

    // Expecting the out-of-range base level to be treated as not color-renderable and
    // texture-filterable.
    glGenerateMipmap(GL_TEXTURE_2D);
    EXPECT_GL_ERROR(GL_INVALID_OPERATION);

    // Draw using level 0. It should still be blue.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::blue);
}

// Call GenerateMipmap with out-of-range base level on an immutable texture. The base level should
// be clamped, so the call doesn't generate an error.
TEST_P(MipmapTestES3, GenerateMipmapBaseLevelOutOfRangeImmutableTexture)
{
    glBindTexture(GL_TEXTURE_2D, mTexture);

    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::green);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1000);

    EXPECT_GL_NO_ERROR();

    // This is essentially a no-op, since the texture only has one level.
    glGenerateMipmap(GL_TEXTURE_2D);

    EXPECT_GL_NO_ERROR();

    // The only level of the texture should still be green.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    clearAndDrawQuad(m2DProgram, getWindowWidth(), getWindowHeight());
    EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2, getWindowHeight() / 2, GLColor::green);
}

// A native version of the WebGL2 test tex-base-level-bug.html
TEST_P(MipmapTestES3, BaseLevelTextureBug)
{
    if (IsOpenGL() && IsAMD())
    {
        std::cout << "Test skipped on Windows AMD OpenGL." << std::endl;
        return;
    }

#if defined(ANGLE_PLATFORM_APPLE)
    // Regression in 10.12.4 needing workaround -- crbug.com/705865.
    // Seems to be passing on AMD GPUs. Definitely not NVIDIA.
    // Probably not Intel.
    if (IsNVIDIA() || IsIntel())
    {
        std::cout << "Test skipped on macOS with NVIDIA and Intel GPUs." << std::endl;
        return;
    }
#endif

    glBindTexture(GL_TEXTURE_2D, mTexture);
    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::red);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    ASSERT_GL_NO_ERROR();

    drawQuad(m2DProgram, "position", 0.5f);
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    ASSERT_GL_NO_ERROR();

    drawQuad(m2DProgram, "position", 0.5f);
    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
}

// Use this to select which configurations (e.g. which renderer, which GLES major version) these tests should be run against.
// Note: we run these tests against 9_3 on WARP due to hardware driver issues on Win7
ANGLE_INSTANTIATE_TEST(MipmapTest,
                       ES2_D3D9(),
                       ES2_D3D11(EGL_EXPERIMENTAL_PRESENT_PATH_COPY_ANGLE),
                       ES2_D3D11(EGL_EXPERIMENTAL_PRESENT_PATH_FAST_ANGLE),
                       ES2_D3D11_FL9_3_WARP(),
                       ES2_OPENGL(),
                       ES3_OPENGL(),
                       ES2_OPENGLES(),
                       ES3_OPENGLES());
ANGLE_INSTANTIATE_TEST(MipmapTestES3, ES3_D3D11(), ES3_OPENGL(), ES3_OPENGLES());
