//
// Copyright 2016 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.
//
// TimerQueriesTest.cpp
//   Various tests for EXT_disjoint_timer_query functionality and validation
//

#include "system_utils.h"
#include "test_utils/ANGLETest.h"
#include "random_utils.h"

using namespace angle;

class TimerQueriesTest : public ANGLETest
{
  protected:
    TimerQueriesTest() : mProgram(0), mProgramCostly(0)
    {
        setWindowWidth(128);
        setWindowHeight(128);
        setConfigRedBits(8);
        setConfigGreenBits(8);
        setConfigBlueBits(8);
        setConfigAlphaBits(8);
        setConfigDepthBits(24);
    }

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

        const std::string passthroughVS =
            "attribute highp vec4 position; void main(void)\n"
            "{\n"
            "    gl_Position = position;\n"
            "}\n";

        const std::string passthroughPS =
            "precision highp float; void main(void)\n"
            "{\n"
            "    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n"
            "}\n";

        const std::string costlyVS =
            "attribute highp vec4 position; varying highp vec4 testPos; void main(void)\n"
            "{\n"
            "    testPos     = position;\n"
            "    gl_Position = position;\n"
            "}\n";

        const std::string costlyPS =
            "precision highp float; varying highp vec4 testPos; void main(void)\n"
            "{\n"
            "    vec4 test = testPos;\n"
            "    for (int i = 0; i < 500; i++)\n"
            "    {\n"
            "        test = sqrt(test);\n"
            "    }\n"
            "    gl_FragColor = test;\n"
            "}\n";

        mProgram = CompileProgram(passthroughVS, passthroughPS);
        ASSERT_NE(0u, mProgram) << "shader compilation failed.";

        mProgramCostly = CompileProgram(costlyVS, costlyPS);
        ASSERT_NE(0u, mProgramCostly) << "shader compilation failed.";
    }

    virtual void TearDown()
    {
        glDeleteProgram(mProgram);
        glDeleteProgram(mProgramCostly);
        ANGLETest::TearDown();
    }

    GLuint mProgram;
    GLuint mProgramCostly;
};

// Test that all proc addresses are loadable
TEST_P(TimerQueriesTest, ProcAddresses)
{
    if (!extensionEnabled("GL_EXT_disjoint_timer_query"))
    {
        std::cout << "Test skipped because GL_EXT_disjoint_timer_query is not available."
                  << std::endl;
        return;
    }

    ASSERT_NE(nullptr, eglGetProcAddress("glGenQueriesEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glDeleteQueriesEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glIsQueryEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glBeginQueryEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glEndQueryEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glQueryCounterEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glGetQueryivEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glGetQueryObjectivEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glGetQueryObjectuivEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glGetQueryObjecti64vEXT"));
    ASSERT_NE(nullptr, eglGetProcAddress("glGetQueryObjectui64vEXT"));
}

// Tests the time elapsed query
TEST_P(TimerQueriesTest, TimeElapsed)
{
    if (!extensionEnabled("GL_EXT_disjoint_timer_query"))
    {
        std::cout << "Test skipped because GL_EXT_disjoint_timer_query is not available."
                  << std::endl;
        return;
    }

    GLint queryTimeElapsedBits = 0;
    glGetQueryivEXT(GL_TIME_ELAPSED_EXT, GL_QUERY_COUNTER_BITS_EXT, &queryTimeElapsedBits);
    ASSERT_GL_NO_ERROR();

    std::cout << "Time elapsed counter bits: " << queryTimeElapsedBits << std::endl;

    // Skip test if the number of bits is 0
    if (queryTimeElapsedBits == 0)
    {
        std::cout << "Test skipped because of 0 counter bits" << std::endl;
        return;
    }

    glDepthMask(GL_TRUE);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    GLuint query1 = 0;
    GLuint query2 = 0;
    glGenQueriesEXT(1, &query1);
    glGenQueriesEXT(1, &query2);

    // Test time elapsed for a single quad
    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, query1);
    drawQuad(mProgram, "position", 0.8f);
    glEndQueryEXT(GL_TIME_ELAPSED_EXT);
    ASSERT_GL_NO_ERROR();

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    // Test time elapsed for costly quad
    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, query2);
    drawQuad(mProgramCostly, "position", 0.8f);
    glEndQueryEXT(GL_TIME_ELAPSED_EXT);
    ASSERT_GL_NO_ERROR();

    swapBuffers();

    int timeout  = 200000;
    GLuint ready = GL_FALSE;
    while (ready == GL_FALSE && timeout > 0)
    {
        angle::Sleep(0);
        glGetQueryObjectuivEXT(query1, GL_QUERY_RESULT_AVAILABLE_EXT, &ready);
        timeout--;
    }
    ready = GL_FALSE;
    while (ready == GL_FALSE && timeout > 0)
    {
        angle::Sleep(0);
        glGetQueryObjectuivEXT(query2, GL_QUERY_RESULT_AVAILABLE_EXT, &ready);
        timeout--;
    }
    ASSERT_LT(0, timeout) << "Query result available timed out" << std::endl;

    GLuint64 result1 = 0;
    GLuint64 result2 = 0;
    glGetQueryObjectui64vEXT(query1, GL_QUERY_RESULT_EXT, &result1);
    glGetQueryObjectui64vEXT(query2, GL_QUERY_RESULT_EXT, &result2);
    ASSERT_GL_NO_ERROR();

    glDeleteQueriesEXT(1, &query1);
    glDeleteQueriesEXT(1, &query2);
    ASSERT_GL_NO_ERROR();

    std::cout << "Elapsed time: " << result1 << " cheap quad" << std::endl;
    std::cout << "Elapsed time: " << result2 << " costly quad" << std::endl;

    // The time elapsed should be nonzero
    EXPECT_LT(0ul, result1);
    EXPECT_LT(0ul, result2);

    // TODO(geofflang): Re-enable this check when it is non-flaky
    // The costly quad should take longer than the cheap quad
    // EXPECT_LT(result1, result2);
}

// Tests time elapsed for a non draw call (texture upload)
TEST_P(TimerQueriesTest, TimeElapsedTextureTest)
{
    // OSX drivers don't seem to properly time non-draw calls so we skip the test on Mac
    if (IsOSX())
    {
        std::cout << "Test skipped on OSX" << std::endl;
        return;
    }

    if (!extensionEnabled("GL_EXT_disjoint_timer_query"))
    {
        std::cout << "Test skipped because GL_EXT_disjoint_timer_query is not available."
                  << std::endl;
        return;
    }

    GLint queryTimeElapsedBits = 0;
    glGetQueryivEXT(GL_TIME_ELAPSED_EXT, GL_QUERY_COUNTER_BITS_EXT, &queryTimeElapsedBits);
    ASSERT_GL_NO_ERROR();

    std::cout << "Time elapsed counter bits: " << queryTimeElapsedBits << std::endl;

    // Skip test if the number of bits is 0
    if (queryTimeElapsedBits == 0)
    {
        std::cout << "Test skipped because of 0 counter bits" << std::endl;
        return;
    }

    GLubyte pixels[] = {0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0};

    // Query and texture initialization
    GLuint texture;
    GLuint query = 0;
    glGenQueriesEXT(1, &query);
    glGenTextures(1, &texture);

    // Upload a texture inside the query
    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, query);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels);
    glGenerateMipmap(GL_TEXTURE_2D);
    glFinish();
    glEndQueryEXT(GL_TIME_ELAPSED_EXT);
    ASSERT_GL_NO_ERROR();

    int timeout  = 200000;
    GLuint ready = GL_FALSE;
    while (ready == GL_FALSE && timeout > 0)
    {
        angle::Sleep(0);
        glGetQueryObjectuivEXT(query, GL_QUERY_RESULT_AVAILABLE_EXT, &ready);
        timeout--;
    }
    ASSERT_LT(0, timeout) << "Query result available timed out" << std::endl;

    GLuint64 result = 0;
    glGetQueryObjectui64vEXT(query, GL_QUERY_RESULT_EXT, &result);
    ASSERT_GL_NO_ERROR();

    glDeleteTextures(1, &texture);
    glDeleteQueriesEXT(1, &query);

    std::cout << "Elapsed time: " << result << std::endl;
    EXPECT_LT(0ul, result);
}

// Tests validation of query functions with respect to elapsed time query
TEST_P(TimerQueriesTest, TimeElapsedValidationTest)
{
    if (!extensionEnabled("GL_EXT_disjoint_timer_query"))
    {
        std::cout << "Test skipped because GL_EXT_disjoint_timer_query is not available."
                  << std::endl;
        return;
    }

    GLint queryTimeElapsedBits = 0;
    glGetQueryivEXT(GL_TIME_ELAPSED_EXT, GL_QUERY_COUNTER_BITS_EXT, &queryTimeElapsedBits);
    ASSERT_GL_NO_ERROR();

    std::cout << "Time elapsed counter bits: " << queryTimeElapsedBits << std::endl;

    // Skip test if the number of bits is 0
    if (queryTimeElapsedBits == 0)
    {
        std::cout << "Test skipped because of 0 counter bits" << std::endl;
        return;
    }

    GLuint query = 0;
    glGenQueriesEXT(-1, &query);
    EXPECT_GL_ERROR(GL_INVALID_VALUE);

    glGenQueriesEXT(1, &query);
    EXPECT_GL_NO_ERROR();

    glBeginQueryEXT(GL_TIMESTAMP_EXT, query);
    EXPECT_GL_ERROR(GL_INVALID_ENUM);

    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, 0);
    EXPECT_GL_ERROR(GL_INVALID_OPERATION);

    glEndQueryEXT(GL_TIME_ELAPSED_EXT);
    EXPECT_GL_ERROR(GL_INVALID_OPERATION);

    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, query);
    EXPECT_GL_NO_ERROR();

    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, query);
    EXPECT_GL_ERROR(GL_INVALID_OPERATION);

    glEndQueryEXT(GL_TIME_ELAPSED_EXT);
    EXPECT_GL_NO_ERROR();

    glEndQueryEXT(GL_TIME_ELAPSED_EXT);
    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
}

// Tests timer queries operating under multiple EGL contexts with mid-query switching
TEST_P(TimerQueriesTest, TimeElapsedMulticontextTest)
{
    if (IsAMD() && IsOpenGL())
    {
        // TODO(jmadill): Figure out why this test is flaky on AMD/OpenGL.
        // http://anglebug.com/1541
        std::cout << "Test skipped on AMD OpenGL." << std::endl;
        return;
    }

    if (!extensionEnabled("GL_EXT_disjoint_timer_query"))
    {
        std::cout << "Test skipped because GL_EXT_disjoint_timer_query is not available."
                  << std::endl;
        return;
    }

    GLint queryTimeElapsedBits = 0;
    glGetQueryivEXT(GL_TIME_ELAPSED_EXT, GL_QUERY_COUNTER_BITS_EXT, &queryTimeElapsedBits);
    ASSERT_GL_NO_ERROR();

    std::cout << "Time elapsed counter bits: " << queryTimeElapsedBits << std::endl;

    // Skip test if the number of bits is 0
    if (queryTimeElapsedBits == 0)
    {
        std::cout << "Test skipped because of 0 counter bits" << std::endl;
        return;
    }

    // Without a glClear, the first draw call on GL takes a huge amount of time when run after the
    // D3D test on certain NVIDIA drivers
    glDepthMask(GL_TRUE);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    EGLint contextAttributes[] = {
        EGL_CONTEXT_MAJOR_VERSION_KHR,
        GetParam().majorVersion,
        EGL_CONTEXT_MINOR_VERSION_KHR,
        GetParam().minorVersion,
        EGL_NONE,
    };

    EGLWindow *window = getEGLWindow();

    EGLDisplay display = window->getDisplay();
    EGLConfig config   = window->getConfig();
    EGLSurface surface = window->getSurface();

    struct ContextInfo
    {
        EGLContext context;
        GLuint program;
        GLuint query;
        EGLDisplay display;

        ContextInfo() : context(EGL_NO_CONTEXT), program(0), query(0), display(EGL_NO_DISPLAY) {}

        ~ContextInfo()
        {
            if (context != EGL_NO_CONTEXT && display != EGL_NO_DISPLAY)
            {
                eglDestroyContext(display, context);
            }
        }
    };
    ContextInfo contexts[2];

    // Shaders
    const std::string cheapVS =
        "attribute highp vec4 position; void main(void)\n"
        "{\n"
        "    gl_Position = position;\n"
        "}\n";

    const std::string cheapPS =
        "precision highp float; void main(void)\n"
        "{\n"
        "    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n"
        "}\n";

    const std::string costlyVS =
        "attribute highp vec4 position; varying highp vec4 testPos; void main(void)\n"
        "{\n"
        "    testPos     = position;\n"
        "    gl_Position = position;\n"
        "}\n";

    const std::string costlyPS =
        "precision highp float; varying highp vec4 testPos; void main(void)\n"
        "{\n"
        "    vec4 test = testPos;\n"
        "    for (int i = 0; i < 500; i++)\n"
        "    {\n"
        "        test = sqrt(test);\n"
        "    }\n"
        "    gl_FragColor = test;\n"
        "}\n";

    // Setup the first context with a cheap shader
    contexts[0].context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttributes);
    contexts[0].display = display;
    ASSERT_NE(contexts[0].context, EGL_NO_CONTEXT);
    eglMakeCurrent(display, surface, surface, contexts[0].context);
    contexts[0].program = CompileProgram(cheapVS, cheapPS);
    glGenQueriesEXT(1, &contexts[0].query);
    ASSERT_GL_NO_ERROR();

    // Setup the second context with an expensive shader
    contexts[1].context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttributes);
    contexts[1].display = display;
    ASSERT_NE(contexts[1].context, EGL_NO_CONTEXT);
    eglMakeCurrent(display, surface, surface, contexts[1].context);
    contexts[1].program = CompileProgram(costlyVS, costlyPS);
    glGenQueriesEXT(1, &contexts[1].query);
    ASSERT_GL_NO_ERROR();

    // Start the query and draw a quad on the first context without ending the query
    eglMakeCurrent(display, surface, surface, contexts[0].context);
    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, contexts[0].query);
    drawQuad(contexts[0].program, "position", 0.8f);
    ASSERT_GL_NO_ERROR();

    // Switch contexts, draw the expensive quad and end its query
    eglMakeCurrent(display, surface, surface, contexts[1].context);
    glBeginQueryEXT(GL_TIME_ELAPSED_EXT, contexts[1].query);
    drawQuad(contexts[1].program, "position", 0.8f);
    glEndQueryEXT(GL_TIME_ELAPSED_EXT);
    ASSERT_GL_NO_ERROR();

    // Go back to the first context, end its query, and get the result
    eglMakeCurrent(display, surface, surface, contexts[0].context);
    glEndQueryEXT(GL_TIME_ELAPSED_EXT);

    GLuint64 result1 = 0;
    GLuint64 result2 = 0;
    glGetQueryObjectui64vEXT(contexts[0].query, GL_QUERY_RESULT_EXT, &result1);
    glDeleteQueriesEXT(1, &contexts[0].query);
    glDeleteProgram(contexts[0].program);
    ASSERT_GL_NO_ERROR();

    // Get the 2nd context's results
    eglMakeCurrent(display, surface, surface, contexts[1].context);
    glGetQueryObjectui64vEXT(contexts[1].query, GL_QUERY_RESULT_EXT, &result2);
    glDeleteQueriesEXT(1, &contexts[1].query);
    glDeleteProgram(contexts[1].program);
    ASSERT_GL_NO_ERROR();

    // Switch back to main context
    eglMakeCurrent(display, surface, surface, window->getContext());

    // Compare the results. The cheap quad should be smaller than the expensive one if
    // virtualization is working correctly
    std::cout << "Elapsed time: " << result1 << " cheap quad" << std::endl;
    std::cout << "Elapsed time: " << result2 << " costly quad" << std::endl;
    EXPECT_LT(0ul, result1);
    EXPECT_LT(0ul, result2);
    EXPECT_LT(result1, result2);
}

// Tests GPU timestamp functionality
TEST_P(TimerQueriesTest, Timestamp)
{
    if (!extensionEnabled("GL_EXT_disjoint_timer_query"))
    {
        std::cout << "Test skipped because GL_EXT_disjoint_timer_query is not available."
                  << std::endl;
        return;
    }

    GLint queryTimestampBits = 0;
    glGetQueryivEXT(GL_TIMESTAMP_EXT, GL_QUERY_COUNTER_BITS_EXT, &queryTimestampBits);
    ASSERT_GL_NO_ERROR();

    std::cout << "Timestamp counter bits: " << queryTimestampBits << std::endl;

    // Macs for some reason return 0 bits so skip the test for now if either are 0
    if (queryTimestampBits == 0)
    {
        std::cout << "Test skipped because of 0 counter bits" << std::endl;
        return;
    }

    glDepthMask(GL_TRUE);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    GLuint query1 = 0;
    GLuint query2 = 0;
    glGenQueriesEXT(1, &query1);
    glGenQueriesEXT(1, &query2);
    glQueryCounterEXT(query1, GL_TIMESTAMP_EXT);
    drawQuad(mProgram, "position", 0.8f);
    glQueryCounterEXT(query2, GL_TIMESTAMP_EXT);

    ASSERT_GL_NO_ERROR();

    swapBuffers();

    int timeout  = 200000;
    GLuint ready = GL_FALSE;
    while (ready == GL_FALSE && timeout > 0)
    {
        angle::Sleep(0);
        glGetQueryObjectuivEXT(query1, GL_QUERY_RESULT_AVAILABLE_EXT, &ready);
        timeout--;
    }
    ready = GL_FALSE;
    while (ready == GL_FALSE && timeout > 0)
    {
        angle::Sleep(0);
        glGetQueryObjectuivEXT(query2, GL_QUERY_RESULT_AVAILABLE_EXT, &ready);
        timeout--;
    }
    ASSERT_LT(0, timeout) << "Query result available timed out" << std::endl;

    GLuint64 result1 = 0;
    GLuint64 result2 = 0;
    glGetQueryObjectui64vEXT(query1, GL_QUERY_RESULT_EXT, &result1);
    glGetQueryObjectui64vEXT(query2, GL_QUERY_RESULT_EXT, &result2);

    ASSERT_GL_NO_ERROR();

    glDeleteQueriesEXT(1, &query1);
    glDeleteQueriesEXT(1, &query2);

    std::cout << "Timestamps: " << result1 << " " << result2 << std::endl;
    EXPECT_LT(0ul, result1);
    EXPECT_LT(0ul, result2);
    EXPECT_LT(result1, result2);
}

class TimerQueriesTestES3 : public TimerQueriesTest
{
};

// Tests getting timestamps via glGetInteger64v
TEST_P(TimerQueriesTestES3, TimestampGetInteger64)
{
    if (!extensionEnabled("GL_EXT_disjoint_timer_query"))
    {
        std::cout << "Test skipped because GL_EXT_disjoint_timer_query is not available."
                  << std::endl;
        return;
    }

    GLint queryTimestampBits = 0;
    glGetQueryivEXT(GL_TIMESTAMP_EXT, GL_QUERY_COUNTER_BITS_EXT, &queryTimestampBits);
    ASSERT_GL_NO_ERROR();

    std::cout << "Timestamp counter bits: " << queryTimestampBits << std::endl;

    if (queryTimestampBits == 0)
    {
        std::cout << "Test skipped because of 0 counter bits" << std::endl;
        return;
    }

    glDepthMask(GL_TRUE);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    GLint64 result1 = 0;
    GLint64 result2 = 0;
    glGetInteger64v(GL_TIMESTAMP_EXT, &result1);
    drawQuad(mProgram, "position", 0.8f);
    glGetInteger64v(GL_TIMESTAMP_EXT, &result2);
    ASSERT_GL_NO_ERROR();
    std::cout << "Timestamps (getInteger64v): " << result1 << " " << result2 << std::endl;
    EXPECT_LT(0l, result1);
    EXPECT_LT(0l, result2);
    EXPECT_LT(result1, result2);
}

ANGLE_INSTANTIATE_TEST(TimerQueriesTest,
                       ES2_D3D9(),
                       ES2_D3D11(),
                       ES3_D3D11(),
                       ES2_OPENGL(),
                       ES3_OPENGL());

ANGLE_INSTANTIATE_TEST(TimerQueriesTestES3, ES3_D3D11(), ES3_OPENGL());
