//
// Copyright (c) 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.
//
// MultiviewPerfTest:
//   Performance tests for multiview rendering.
//   - MultiviewCPUBoundBenchmark issues many draw calls and state changes to stress the CPU.
//   - MultiviewGPUBoundBenchmark draws half a million quads with multiple attributes per vertex in
//   order to stress the GPU's memory system.
//

#include "ANGLEPerfTest.h"
#include "common/vector_utils.h"
#include "platform/WorkaroundsD3D.h"
#include "shader_utils.h"
#include "test_utils/gl_raii.h"

#include <string.h>

using namespace angle;

namespace
{

std::string GetShaderExtensionHeader(bool usesMultiview, int numViews, GLenum shaderType)
{
    if (!usesMultiview)
    {
        return "";
    }

    if (shaderType == GL_VERTEX_SHADER)
    {
        return "#extension GL_OVR_multiview : require\nlayout(num_views = " + ToString(numViews) +
               ") in;\n";
        ;
    }
    ASSERT(shaderType == GL_FRAGMENT_SHADER);
    return "#extension GL_OVR_multiview : require\n";
}

struct Vertex
{
    Vector4 position;
    Vector4 colorAttributeData[6];
};

enum class MultiviewOption
{
    NoAcceleration,
    InstancedMultiviewVertexShader,
    InstancedMultiviewGeometryShader,

    Unspecified
};

using MultiviewPerfWorkload = std::pair<int, int>;

struct MultiviewPerfParams final : public RenderTestParams
{
    MultiviewPerfParams(const EGLPlatformParameters &platformParametersIn,
                        const MultiviewPerfWorkload &workloadIn,
                        MultiviewOption multiviewOptionIn)
    {
        iterationsPerStep = 1;
        majorVersion      = 3;
        minorVersion      = 0;
        eglParameters     = platformParametersIn;
        windowWidth       = workloadIn.first;
        windowHeight      = workloadIn.second;
        multiviewOption   = multiviewOptionIn;
        numViews          = 2;
    }

    std::string suffix() const override
    {
        std::string name = RenderTestParams::suffix();
        switch (multiviewOption)
        {
            case MultiviewOption::NoAcceleration:
                name += "_no_acc";
                break;
            case MultiviewOption::InstancedMultiviewVertexShader:
                name += "_instanced_multiview_vertex_shader";
                break;
            case MultiviewOption::InstancedMultiviewGeometryShader:
                name += "_instanced_multiview_geometry_shader";
                break;
            default:
                UNREACHABLE();
        }
        name += "_" + ToString(numViews) + "_views";
        return name;
    }

    MultiviewOption multiviewOption;
    int numViews;
};

std::ostream &operator<<(std::ostream &os, const MultiviewPerfParams &params)
{
    os << params.suffix().substr(1);
    return os;
}

class MultiviewBenchmark : public ANGLERenderTest,
                           public ::testing::WithParamInterface<MultiviewPerfParams>
{
  public:
    MultiviewBenchmark(const std::string &testName)
        : ANGLERenderTest(testName, GetParam()), mProgram(0)
    {
        addExtensionPrerequisite("GL_ANGLE_multiview");
    }

    virtual ~MultiviewBenchmark()
    {
        if (mProgram != 0)
        {
            glDeleteProgram(mProgram);
        }
    }

    void initializeBenchmark() override;
    void drawBenchmark() final;

    void overrideWorkaroundsD3D(WorkaroundsD3D *workarounds) override
    {
        workarounds->selectViewInGeometryShader =
            (GetParam().multiviewOption == MultiviewOption::InstancedMultiviewGeometryShader);
    }

  protected:
    virtual void renderScene() = 0;

    void createProgram(const char *vs, const char *fs)
    {
        mProgram = CompileProgram(vs, fs);
        if (mProgram == 0)
        {
            FAIL() << "shader compilation failed.";
        }
        glUseProgram(mProgram);
        ASSERT_GL_NO_ERROR();
    }

    GLuint mProgram;
    GLVertexArray mVAO;
    GLBuffer mVBO;

  private:
    GLFramebuffer mFramebuffer;
    GLTexture mColorTexture;
    GLTexture mDepthTexture;
};

class MultiviewCPUBoundBenchmark : public MultiviewBenchmark
{
  public:
    MultiviewCPUBoundBenchmark() : MultiviewBenchmark("MultiviewCPUBoundBenchmark") {}

    void initializeBenchmark() override;

  protected:
    void renderScene() override;
};

class MultiviewGPUBoundBenchmark : public MultiviewBenchmark
{
  public:
    MultiviewGPUBoundBenchmark() : MultiviewBenchmark("MultiviewGPUBoundBenchmark") {}

    void initializeBenchmark() override;

  protected:
    void renderScene() override;
};

void MultiviewBenchmark::initializeBenchmark()
{
    const MultiviewPerfParams *params = static_cast<const MultiviewPerfParams *>(&mTestParams);
    ASSERT(params->windowWidth % params->numViews == 0);

    glBindTexture(GL_TEXTURE_2D, mColorTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, params->windowWidth, params->windowHeight, 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, nullptr);

    glBindTexture(GL_TEXTURE_2D, mDepthTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, params->windowWidth, params->windowHeight,
                 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);

    glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);

    switch (params->multiviewOption)
    {
        case MultiviewOption::NoAcceleration:
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                                   mColorTexture, 0);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
                                   mDepthTexture, 0);
            break;
        case MultiviewOption::InstancedMultiviewVertexShader:
        case MultiviewOption::InstancedMultiviewGeometryShader:
        {
            const int widthPerView = params->windowWidth / params->numViews;
            std::vector<GLint> viewportOffsets(2 * params->numViews);
            for (int i = 0u; i < params->numViews; ++i)
            {
                viewportOffsets[i * 2]     = i * widthPerView;
                viewportOffsets[i * 2 + 1] = 0;
            }
            glFramebufferTextureMultiviewSideBySideANGLE(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                                         mColorTexture, 0, params->numViews,
                                                         viewportOffsets.data());
            glFramebufferTextureMultiviewSideBySideANGLE(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                                                         mDepthTexture, 0, params->numViews,
                                                         viewportOffsets.data());
            break;
        }
        default:
            UNREACHABLE();
    }

    GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0};
    glDrawBuffers(1, DrawBuffers);

    ASSERT_GL_NO_ERROR();
}

void MultiviewBenchmark::drawBenchmark()
{
    const MultiviewPerfParams *params = static_cast<const MultiviewPerfParams *>(&mTestParams);
    const int viewWidth               = params->windowWidth / params->numViews;
    const int viewHeight              = params->windowHeight;

    switch (params->multiviewOption)
    {
        case MultiviewOption::NoAcceleration:
            glEnable(GL_SCISSOR_TEST);
            // Iterate over each view and render the scene.
            for (int i = 0; i < params->numViews; ++i)
            {
                glViewport(viewWidth * i, 0, viewWidth, viewHeight);
                glScissor(viewWidth * i, 0, viewWidth, viewHeight);
                renderScene();
            }
            break;
        case MultiviewOption::InstancedMultiviewVertexShader:
        case MultiviewOption::InstancedMultiviewGeometryShader:
            glViewport(0, 0, viewWidth, viewHeight);
            glScissor(0, 0, viewWidth, viewHeight);
            renderScene();
            break;
        default:
            UNREACHABLE();
    }

    ASSERT_GL_NO_ERROR();
}

void MultiviewCPUBoundBenchmark::initializeBenchmark()
{
    MultiviewBenchmark::initializeBenchmark();

    const MultiviewPerfParams *params = static_cast<const MultiviewPerfParams *>(&mTestParams);
    const bool usesMultiview = (params->multiviewOption != MultiviewOption::NoAcceleration);

    const std::string vs =
        "#version 300 es\n" +
        GetShaderExtensionHeader(usesMultiview, params->numViews, GL_VERTEX_SHADER) +
        "layout(location=0) in vec4 vPosition;\n"
        "uniform vec2 uOffset;\n"
        "void main()\n"
        "{\n"
        "   vec4 v = vPosition;\n"
        "   v.xy += uOffset;\n"
        "	gl_Position = v;\n"
        "}\n";

    const std::string fs =
        "#version 300 es\n" +
        GetShaderExtensionHeader(usesMultiview, params->numViews, GL_FRAGMENT_SHADER) +
        "precision mediump float;\n"
        "out vec4 col;\n"
        "uniform float uColor;\n"
        "void main()\n"
        "{\n"
        "    col = vec4(1.);\n"
        "}\n";

    createProgram(vs.c_str(), fs.c_str());

    const float viewWidth  = static_cast<float>(params->windowWidth / params->numViews);
    const float viewHeight = static_cast<float>(params->windowHeight);
    const float quadWidth  = 2.f / viewWidth;
    const float quadHeight = 2.f / viewHeight;
    Vector4 vertices[6]    = {Vector4(.0f, .0f, .0f, 1.f),
                           Vector4(quadWidth, .0f, .0f, 1.f),
                           Vector4(quadWidth, quadHeight, 0.f, 1.f),
                           Vector4(.0f, .0f, 0.f, 1.f),
                           Vector4(quadWidth, quadHeight, .0f, 1.f),
                           Vector4(.0f, quadHeight, .0f, 1.f)};

    glBindVertexArray(mVAO);

    glBindBuffer(GL_ARRAY_BUFFER, mVBO);
    glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(Vector4), vertices, GL_STATIC_DRAW);

    const GLint posLoc = glGetAttribLocation(mProgram, "vPosition");
    glVertexAttribPointer(posLoc, 4, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(posLoc);

    // Render once to guarantee that the program is compiled and linked.
    drawBenchmark();

    ASSERT_GL_NO_ERROR();
}

void MultiviewCPUBoundBenchmark::renderScene()
{
    glClearColor(0, 0, 0, 0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glUseProgram(mProgram);

    glBindVertexArray(mVAO);

    const MultiviewPerfParams *params = static_cast<const MultiviewPerfParams *>(&mTestParams);
    const int viewWidth               = params->windowWidth / params->numViews;
    const int viewHeight              = params->windowHeight;

    for (int w = 0; w < viewWidth; ++w)
    {
        for (int h = 0; h < viewHeight; ++h)
        {
            const float wf = static_cast<float>(w) / viewWidth;
            const float wh = static_cast<float>(h) / viewHeight;
            glUniform2f(glGetUniformLocation(mProgram, "uOffset"), 2.f * wf - 1.f, 2.f * wh - 1.f);
            glUniform1f(glGetUniformLocation(mProgram, "uColor"), wf);
            glDrawArrays(GL_TRIANGLES, 0, 6);
        }
    }
}

void MultiviewGPUBoundBenchmark::initializeBenchmark()
{
    MultiviewBenchmark::initializeBenchmark();

    const MultiviewPerfParams *params = static_cast<const MultiviewPerfParams *>(&mTestParams);
    const bool usesMultiview = (params->multiviewOption != MultiviewOption::NoAcceleration);

    const std::string &vs =
        "#version 300 es\n" +
        GetShaderExtensionHeader(usesMultiview, params->numViews, GL_VERTEX_SHADER) +
        "layout(location=0) in vec4 vPosition;\n"
        "layout(location=1) in vec4 vert_Col0;\n"
        "layout(location=2) in vec4 vert_Col1;\n"
        "layout(location=3) in vec4 vert_Col2;\n"
        "layout(location=4) in vec4 vert_Col3;\n"
        "layout(location=5) in vec4 vert_Col4;\n"
        "layout(location=6) in vec4 vert_Col5;\n"
        "out vec4 frag_Col0;\n"
        "out vec4 frag_Col1;\n"
        "out vec4 frag_Col2;\n"
        "out vec4 frag_Col3;\n"
        "out vec4 frag_Col4;\n"
        "out vec4 frag_Col5;\n"
        "void main()\n"
        "{\n"
        "   frag_Col0 = vert_Col0;\n"
        "   frag_Col1 = vert_Col1;\n"
        "   frag_Col2 = vert_Col2;\n"
        "   frag_Col3 = vert_Col3;\n"
        "   frag_Col4 = vert_Col4;\n"
        "   frag_Col5 = vert_Col5;\n"
        "	gl_Position = vPosition;\n"
        "}\n";

    const std::string &fs =
        "#version 300 es\n" +
        GetShaderExtensionHeader(usesMultiview, params->numViews, GL_FRAGMENT_SHADER) +
        "precision mediump float;\n"
        "in vec4 frag_Col0;\n"
        "in vec4 frag_Col1;\n"
        "in vec4 frag_Col2;\n"
        "in vec4 frag_Col3;\n"
        "in vec4 frag_Col4;\n"
        "in vec4 frag_Col5;\n"
        "out vec4 col;\n"
        "void main()\n"
        "{\n"
        "    col += frag_Col0;\n"
        "    col += frag_Col1;\n"
        "    col += frag_Col2;\n"
        "    col += frag_Col3;\n"
        "    col += frag_Col4;\n"
        "    col += frag_Col5;\n"
        "}\n";

    createProgram(vs.c_str(), fs.c_str());
    ASSERT_GL_NO_ERROR();

    // Generate a vertex buffer of triangulated quads so that we have one quad per pixel.
    const int viewWidth           = params->windowWidth / params->numViews;
    const int viewHeight          = params->windowHeight;
    const float quadWidth         = 2.f / static_cast<float>(viewWidth);
    const float quadHeight        = 2.f / static_cast<float>(viewHeight);
    const int kNumQuads           = viewWidth * viewHeight;
    const int kNumVerticesPerQuad = 6;
    std::vector<Vertex> vertexData(kNumQuads * kNumVerticesPerQuad);
    for (int h = 0; h < viewHeight; ++h)
    {
        for (int w = 0; w < viewWidth; ++w)
        {
            float wf = static_cast<float>(w) / viewWidth;
            float hf = static_cast<float>(h) / viewHeight;

            size_t index = static_cast<size_t>(h * viewWidth + w) * 6u;

            auto &v0    = vertexData[index];
            v0.position = Vector4(2.f * wf - 1.f, 2.f * hf - 1.f, .0f, 1.f);
            memset(v0.colorAttributeData, 0, sizeof(v0.colorAttributeData));

            auto &v1    = vertexData[index + 1];
            v1.position = Vector4(v0.position.x() + quadWidth, v0.position.y(), .0f, 1.f);
            memset(v1.colorAttributeData, 0, sizeof(v1.colorAttributeData));

            auto &v2    = vertexData[index + 2];
            v2.position = Vector4(v1.position.x(), v1.position.y() + quadHeight, .0f, 1.f);
            memset(v2.colorAttributeData, 0, sizeof(v2.colorAttributeData));

            auto &v3    = vertexData[index + 3];
            v3.position = v0.position;
            memset(v3.colorAttributeData, 0, sizeof(v3.colorAttributeData));

            auto &v4    = vertexData[index + 4];
            v4.position = v2.position;
            memset(v4.colorAttributeData, 0, sizeof(v4.colorAttributeData));

            auto &v5    = vertexData[index + 5];
            v5.position = Vector4(v0.position.x(), v0.position.y() + quadHeight, .0f, 1.f);
            memset(v5.colorAttributeData, 0, sizeof(v5.colorAttributeData));
        }
    }

    glBindVertexArray(mVAO);

    glBindBuffer(GL_ARRAY_BUFFER, mVBO);
    glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(Vertex), vertexData.data(),
                 GL_STATIC_DRAW);

    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
    glEnableVertexAttribArray(0);

    for (unsigned int i = 0u; i < 6u; ++i)
    {
        size_t offset = sizeof(Vector4) * (i + 1u);
        glVertexAttribPointer(i + 1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),
                              reinterpret_cast<const void *>(offset));
        glEnableVertexAttribArray(i + 1);
    }

    // Render once to guarantee that the program is compiled and linked.
    drawBenchmark();
}

void MultiviewGPUBoundBenchmark::renderScene()
{
    glClearColor(0, 0, 0, 0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glUseProgram(mProgram);

    glBindVertexArray(mVAO);

    const MultiviewPerfParams *params = static_cast<const MultiviewPerfParams *>(&mTestParams);
    const int viewWidth               = params->windowWidth / params->numViews;
    const int viewHeight              = params->windowHeight;
    glDrawArrays(GL_TRIANGLES, 0, viewWidth * viewHeight * 6);
}

namespace
{
MultiviewPerfWorkload SmallWorkload()
{
    return MultiviewPerfWorkload(64, 64);
}

MultiviewPerfWorkload BigWorkload()
{
    return MultiviewPerfWorkload(1024, 768);
}

MultiviewPerfParams NoAcceleration(const EGLPlatformParameters &eglParameters,
                                   const MultiviewPerfWorkload &workload)
{
    return MultiviewPerfParams(eglParameters, workload, MultiviewOption::NoAcceleration);
}

MultiviewPerfParams SelectViewInGeometryShader(const MultiviewPerfWorkload &workload)
{
    return MultiviewPerfParams(egl_platform::D3D11(), workload,
                               MultiviewOption::InstancedMultiviewGeometryShader);
}

MultiviewPerfParams SelectViewInVertexShader(const EGLPlatformParameters &eglParameters,
                                             const MultiviewPerfWorkload &workload)
{
    return MultiviewPerfParams(eglParameters, workload,
                               MultiviewOption::InstancedMultiviewVertexShader);
}
}  // namespace

TEST_P(MultiviewCPUBoundBenchmark, Run)
{
    run();
}

ANGLE_INSTANTIATE_TEST(MultiviewCPUBoundBenchmark,
                       NoAcceleration(egl_platform::OPENGL_OR_GLES(false), SmallWorkload()),
                       NoAcceleration(egl_platform::D3D11(), SmallWorkload()),
                       SelectViewInGeometryShader(SmallWorkload()),
                       SelectViewInVertexShader(egl_platform::OPENGL_OR_GLES(false),
                                                SmallWorkload()),
                       SelectViewInVertexShader(egl_platform::D3D11(), SmallWorkload()));

TEST_P(MultiviewGPUBoundBenchmark, Run)
{
    run();
}

ANGLE_INSTANTIATE_TEST(MultiviewGPUBoundBenchmark,
                       NoAcceleration(egl_platform::OPENGL_OR_GLES(false), BigWorkload()),
                       NoAcceleration(egl_platform::D3D11(), BigWorkload()),
                       SelectViewInGeometryShader(BigWorkload()),
                       SelectViewInVertexShader(egl_platform::OPENGL_OR_GLES(false), BigWorkload()),
                       SelectViewInVertexShader(egl_platform::D3D11(), BigWorkload()));

}  // anonymous namespace
