//
// Copyright 2021 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.
//
// EGLMultiContextTest.cpp:
//   Tests relating to multiple non-shared Contexts.

#include <gtest/gtest.h>

#include "test_utils/ANGLETest.h"
#include "test_utils/MultiThreadSteps.h"
#include "test_utils/angle_test_configs.h"
#include "test_utils/gl_raii.h"
#include "util/EGLWindow.h"
#include "util/test_utils.h"

using namespace angle;

namespace
{

EGLBoolean SafeDestroyContext(EGLDisplay display, EGLContext &context)
{
    EGLBoolean result = EGL_TRUE;
    if (context != EGL_NO_CONTEXT)
    {
        result  = eglDestroyContext(display, context);
        context = EGL_NO_CONTEXT;
    }
    return result;
}

class EGLMultiContextTest : public ANGLETest<>
{
  public:
    EGLMultiContextTest() : mContexts{EGL_NO_CONTEXT, EGL_NO_CONTEXT}, mTexture(0) {}

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

        EGLDisplay display = getEGLWindow()->getDisplay();

        if (display != EGL_NO_DISPLAY)
        {
            for (auto &context : mContexts)
            {
                SafeDestroyContext(display, context);
            }
        }

        // Set default test state to not give an error on shutdown.
        getEGLWindow()->makeCurrent();
    }

    bool chooseConfig(EGLDisplay dpy, EGLConfig *config) const
    {
        bool result          = false;
        EGLint count         = 0;
        EGLint clientVersion = EGL_OPENGL_ES3_BIT;
        EGLint attribs[]     = {EGL_RED_SIZE,
                                8,
                                EGL_GREEN_SIZE,
                                8,
                                EGL_BLUE_SIZE,
                                8,
                                EGL_ALPHA_SIZE,
                                8,
                                EGL_RENDERABLE_TYPE,
                                clientVersion,
                                EGL_SURFACE_TYPE,
                                EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
                                EGL_NONE};

        result = eglChooseConfig(dpy, attribs, config, 1, &count);
        EXPECT_EGL_TRUE(result && (count > 0));
        return result;
    }

    bool createContext(EGLDisplay dpy, EGLConfig config, EGLContext *context)
    {
        bool result      = false;
        EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};

        *context = eglCreateContext(dpy, config, nullptr, attribs);
        result   = (*context != EGL_NO_CONTEXT);
        EXPECT_TRUE(result);
        return result;
    }

    bool createPbufferSurface(EGLDisplay dpy,
                              EGLConfig config,
                              EGLint width,
                              EGLint height,
                              EGLSurface *surface)
    {
        bool result      = false;
        EGLint attribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};

        *surface = eglCreatePbufferSurface(dpy, config, attribs);
        result   = (*surface != EGL_NO_SURFACE);
        EXPECT_TRUE(result);
        return result;
    }

    enum class FenceTest
    {
        ClientWait,
        ServerWait,
        GetStatus,
    };
    enum class FlushMethod
    {
        Flush,
        Finish,
    };
    void testFenceWithOpenRenderPass(FenceTest test, FlushMethod flushMethod);

    EGLContext mContexts[2];
    GLuint mTexture;
};

// Test that calling eglDeleteContext on a context that is not current succeeds.
TEST_P(EGLMultiContextTest, TestContextDestroySimple)
{
    EGLWindow *window = getEGLWindow();
    EGLDisplay dpy    = window->getDisplay();

    EGLContext context1 = window->createContext(EGL_NO_CONTEXT, nullptr);
    EGLContext context2 = window->createContext(EGL_NO_CONTEXT, nullptr);

    EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context1));
    EXPECT_EGL_TRUE(eglDestroyContext(dpy, context2));
    EXPECT_EGL_SUCCESS();

    // Cleanup
    EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    EXPECT_EGL_TRUE(eglDestroyContext(dpy, context1));
    EXPECT_EGL_SUCCESS();
}

// Test that an error is generated when using EGL objects after calling eglTerminate.
TEST_P(EGLMultiContextTest, NegativeTestAfterEglTerminate)
{
    EGLWindow *window = getEGLWindow();
    EGLDisplay dpy    = window->getDisplay();

    EGLConfig config = EGL_NO_CONFIG_KHR;
    EXPECT_TRUE(chooseConfig(dpy, &config));

    EGLContext context = EGL_NO_CONTEXT;
    EXPECT_TRUE(createContext(dpy, config, &context));
    ASSERT_EGL_SUCCESS() << "eglCreateContext failed.";

    EGLSurface drawSurface = EGL_NO_SURFACE;
    EXPECT_TRUE(createPbufferSurface(dpy, config, 2560, 1080, &drawSurface));
    ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed.";

    EGLSurface readSurface = EGL_NO_SURFACE;
    EXPECT_TRUE(createPbufferSurface(dpy, config, 2560, 1080, &readSurface));
    ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed.";

    EXPECT_EGL_TRUE(eglMakeCurrent(dpy, drawSurface, readSurface, context));
    EXPECT_EGL_SUCCESS();

    // Terminate the display
    EXPECT_EGL_TRUE(eglTerminate(dpy));
    EXPECT_EGL_SUCCESS();

    // Try to use invalid handles
    EGLint value;
    eglQuerySurface(dpy, drawSurface, EGL_SWAP_BEHAVIOR, &value);
    EXPECT_EGL_ERROR(EGL_BAD_SURFACE);
    eglQuerySurface(dpy, readSurface, EGL_HEIGHT, &value);
    EXPECT_EGL_ERROR(EGL_BAD_SURFACE);

    // Cleanup
    EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    EXPECT_EGL_SUCCESS();
    window->destroyGL();
}

// Test that a compute shader running in one thread will still work when rendering is happening in
// another thread (with non-shared contexts).  The non-shared context will still share a Vulkan
// command buffer.
TEST_P(EGLMultiContextTest, ComputeShaderOkayWithRendering)
{
    ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading());
    ANGLE_SKIP_TEST_IF(!isVulkanRenderer());
    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 || getClientMinorVersion() < 1);

    // Initialize contexts
    EGLWindow *window = getEGLWindow();
    EGLDisplay dpy    = window->getDisplay();
    EGLConfig config  = window->getConfig();

    constexpr size_t kThreadCount    = 2;
    EGLSurface surface[kThreadCount] = {EGL_NO_SURFACE, EGL_NO_SURFACE};
    EGLContext ctx[kThreadCount]     = {EGL_NO_CONTEXT, EGL_NO_CONTEXT};

    EGLint pbufferAttributes[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE, EGL_NONE};

    for (size_t t = 0; t < kThreadCount; ++t)
    {
        surface[t] = eglCreatePbufferSurface(dpy, config, pbufferAttributes);
        EXPECT_EGL_SUCCESS();

        ctx[t] = window->createContext(EGL_NO_CONTEXT, nullptr);
        EXPECT_NE(EGL_NO_CONTEXT, ctx[t]);
    }

    // Synchronization tools to ensure the two threads are interleaved as designed by this test.
    std::mutex mutex;
    std::condition_variable condVar;

    enum class Step
    {
        Thread0Start,
        Thread0DispatchedCompute,
        Thread1Drew,
        Thread0DispatchedComputeAgain,
        Finish,
        Abort,
    };
    Step currentStep = Step::Thread0Start;

    // This first thread dispatches a compute shader.  It immediately starts.
    std::thread deletingThread = std::thread([&]() {
        ThreadSynchronization<Step> threadSynchronization(&currentStep, &mutex, &condVar);

        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0]));
        EXPECT_EGL_SUCCESS();

        // Potentially wait to be signalled to start.
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Start));

        // Wake up and do next step: Create, detach, and dispatch a compute shader program.
        constexpr char kCS[]  = R"(#version 310 es
layout(local_size_x=1) in;
void main()
{
})";
        GLuint computeProgram = glCreateProgram();
        GLuint cs             = CompileShader(GL_COMPUTE_SHADER, kCS);
        EXPECT_NE(0u, cs);

        glAttachShader(computeProgram, cs);
        glDeleteShader(cs);
        glLinkProgram(computeProgram);
        GLint linkStatus;
        glGetProgramiv(computeProgram, GL_LINK_STATUS, &linkStatus);
        EXPECT_GL_TRUE(linkStatus);
        glDetachShader(computeProgram, cs);
        EXPECT_GL_NO_ERROR();
        glUseProgram(computeProgram);

        glDispatchCompute(8, 4, 2);
        EXPECT_GL_NO_ERROR();

        // Signal the second thread and wait for it to draw and flush.
        threadSynchronization.nextStep(Step::Thread0DispatchedCompute);
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Drew));

        // Wake up and do next step: Dispatch the same compute shader again.
        glDispatchCompute(8, 4, 2);

        // Signal the second thread and wait for it to draw and flush again.
        threadSynchronization.nextStep(Step::Thread0DispatchedComputeAgain);
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish));

        // Wake up and do next step: Dispatch the same compute shader again, and force flush the
        // underlying command buffer.
        glDispatchCompute(8, 4, 2);
        glFinish();

        // Clean-up and exit this thread.
        EXPECT_GL_NO_ERROR();
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
        EXPECT_EGL_SUCCESS();
    });

    // This second thread renders.  It starts once the other thread does its first nextStep()
    std::thread continuingThread = std::thread([&]() {
        ThreadSynchronization<Step> threadSynchronization(&currentStep, &mutex, &condVar);

        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[1], surface[1], ctx[1]));
        EXPECT_EGL_SUCCESS();

        // Wait for first thread to create and dispatch a compute shader.
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0DispatchedCompute));

        // Wake up and do next step: Create graphics resources, draw, and force flush the
        // underlying command buffer.
        GLTexture texture;
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

        GLRenderbuffer renderbuffer;
        GLFramebuffer fbo;
        glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
        constexpr int kRenderbufferSize = 4;
        glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kRenderbufferSize, kRenderbufferSize);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
                                  renderbuffer);
        glBindTexture(GL_TEXTURE_2D, texture);

        GLProgram graphicsProgram;
        graphicsProgram.makeRaster(essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
        ASSERT_TRUE(graphicsProgram.valid());

        drawQuad(graphicsProgram.get(), essl1_shaders::PositionAttrib(), 0.5f);
        glFinish();

        // Signal the first thread and wait for it to dispatch a compute shader again.
        threadSynchronization.nextStep(Step::Thread1Drew);
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0DispatchedComputeAgain));

        // Wake up and do next step: Draw and force flush the underlying command buffer again.
        drawQuad(graphicsProgram.get(), essl1_shaders::PositionAttrib(), 0.5f);
        glFinish();

        // Signal the first thread and wait exit this thread.
        threadSynchronization.nextStep(Step::Finish);

        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
        EXPECT_EGL_SUCCESS();
    });

    deletingThread.join();
    continuingThread.join();

    ASSERT_NE(currentStep, Step::Abort);

    // Clean up
    EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    for (size_t t = 0; t < kThreadCount; ++t)
    {
        eglDestroySurface(dpy, surface[t]);
        eglDestroyContext(dpy, ctx[t]);
    }
}

// Test that repeated EGL init + terminate with improper cleanup doesn't cause an OOM crash.
// To reproduce the OOM error -
//     1. Increase the loop count to a large number
//     2. Remove the call to "eglReleaseThread" in the for loop
TEST_P(EGLMultiContextTest, RepeatedEglInitAndTerminate)
{
    ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading());

    // Release all resources in parent thread
    getEGLWindow()->destroyGL();

    EGLDisplay dpy;
    EGLSurface srf;
    EGLContext ctx;
    EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(),
                          EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, GetParam().getDeviceType(),
                          EGL_NONE};

    for (int i = 0; i < 100; i++)
    {
        std::thread thread = std::thread([&]() {
            dpy = eglGetPlatformDisplayEXT(
                EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
            EXPECT_TRUE(dpy != EGL_NO_DISPLAY);
            EXPECT_EGL_TRUE(eglInitialize(dpy, nullptr, nullptr));

            EGLConfig config = EGL_NO_CONFIG_KHR;
            EXPECT_TRUE(chooseConfig(dpy, &config));

            EXPECT_TRUE(createPbufferSurface(dpy, config, 2560, 1080, &srf));
            ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed.";

            EXPECT_TRUE(createContext(dpy, config, &ctx));
            EXPECT_EGL_TRUE(eglMakeCurrent(dpy, srf, srf, ctx));

            // Clear and read back to make sure thread uses context.
            glClearColor(1.0, 0.0, 0.0, 1.0);
            glClear(GL_COLOR_BUFFER_BIT);
            EXPECT_PIXEL_EQ(0, 0, 255, 0, 0, 255);

            eglTerminate(dpy);
            EXPECT_EGL_SUCCESS();
            eglReleaseThread();
            EXPECT_EGL_SUCCESS();
            dpy = EGL_NO_DISPLAY;
            srf = EGL_NO_SURFACE;
            ctx = EGL_NO_CONTEXT;
        });

        thread.join();
    }
}

// Test that thread B can reuse the unterminated display created by thread A
// even after thread A is destroyed.
TEST_P(EGLMultiContextTest, ReuseUnterminatedDisplay)
{
    // Release all resources in parent thread
    getEGLWindow()->destroyGL();

    EGLDisplay dpy;
    EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(),
                          EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, GetParam().getDeviceType(),
                          EGL_NONE};

    std::thread threadA = std::thread([&]() {
        dpy = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE,
                                       reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
        EXPECT_TRUE(dpy != EGL_NO_DISPLAY);
        EXPECT_EGL_TRUE(eglInitialize(dpy, nullptr, nullptr));
    });
    threadA.join();

    std::thread threadB = std::thread([&]() {
        EGLSurface srf;
        EGLContext ctx;
        EGLConfig config = EGL_NO_CONFIG_KHR;
        // If threadA's termination caused "dpy" to be incorrectly terminated all EGL APIs below
        // staring with eglChooseConfig(...) will error out with an EGL_NOT_INITIALIZED error.
        EXPECT_TRUE(chooseConfig(dpy, &config));

        EXPECT_TRUE(createPbufferSurface(dpy, config, 2560, 1080, &srf));
        ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed.";

        EXPECT_TRUE(createContext(dpy, config, &ctx));
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, srf, srf, ctx));

        // Clear and read back to make sure thread uses context.
        glClearColor(1.0, 0.0, 0.0, 1.0);
        glClear(GL_COLOR_BUFFER_BIT);
        EXPECT_PIXEL_EQ(0, 0, 255, 0, 0, 255);

        srf = EGL_NO_SURFACE;
        ctx = EGL_NO_CONTEXT;
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, srf, srf, ctx));
        eglTerminate(dpy);
        EXPECT_EGL_SUCCESS();
        EXPECT_EGL_SUCCESS();
        dpy = EGL_NO_DISPLAY;
    });
    threadB.join();
}

// Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after
// that.  Note that only validatity of the fence operations are tested here.  The test could
// potentially be enhanced with EGL images similarly to how
// MultithreadingTestES3::testFenceWithOpenRenderPass tests correctness of synchronization through
// a shared texture.
void EGLMultiContextTest::testFenceWithOpenRenderPass(FenceTest test, FlushMethod flushMethod)
{
    ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading());

    constexpr uint32_t kWidth  = 100;
    constexpr uint32_t kHeight = 200;

    EGLSyncKHR sync = EGL_NO_SYNC_KHR;

    std::mutex mutex;
    std::condition_variable condVar;

    enum class Step
    {
        Start,
        Thread0CreateFence,
        Thread1WaitFence,
        Finish,
        Abort,
    };
    Step currentStep = Step::Start;

    auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) {
        ThreadSynchronization<Step> threadSynchronization(&currentStep, &mutex, &condVar);

        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start));

        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context));

        // Issue a draw
        ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
        drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f);
        ASSERT_GL_NO_ERROR();

        // Issue a fence.  A render pass is currently open, but it should be closed in the Vulkan
        // backend.
        sync = eglCreateSyncKHR(dpy, EGL_SYNC_FENCE_KHR, nullptr);
        EXPECT_NE(sync, EGL_NO_SYNC_KHR);

        // Wait for thread 1 to wait on it.
        threadSynchronization.nextStep(Step::Thread0CreateFence);
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1WaitFence));

        // Wait a little to give thread 1 time to wait on the sync object before flushing it.
        angle::Sleep(500);
        switch (flushMethod)
        {
            case FlushMethod::Flush:
                glFlush();
                break;
            case FlushMethod::Finish:
                glFinish();
                break;
        }

        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish));

        EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::red);

        // Clean up
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    };

    auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) {
        ThreadSynchronization<Step> threadSynchronization(&currentStep, &mutex, &condVar);

        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context));

        // Wait for thread 0 to create the fence object.
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0CreateFence));

        // Test access to the fence object
        threadSynchronization.nextStep(Step::Thread1WaitFence);

        constexpr GLuint64 kTimeout = 2'000'000'000;  // 2 seconds
        EGLint result               = EGL_CONDITION_SATISFIED_KHR;
        switch (test)
        {
            case FenceTest::ClientWait:
                result = eglClientWaitSyncKHR(dpy, sync, 0, kTimeout);
                break;
            case FenceTest::ServerWait:
                ASSERT_TRUE(eglWaitSyncKHR(dpy, sync, 0));
                break;
            case FenceTest::GetStatus:
            {
                EGLint value;
                EXPECT_EGL_TRUE(eglGetSyncAttribKHR(dpy, sync, EGL_SYNC_STATUS_KHR, &value));
                if (value != EGL_SIGNALED_KHR)
                {
                    result = eglClientWaitSyncKHR(dpy, sync, 0, kTimeout);
                }
                break;
            }
        }
        ASSERT_TRUE(result == EGL_CONDITION_SATISFIED_KHR);

        // Issue a draw
        ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
        drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f);
        ASSERT_GL_NO_ERROR();

        EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::green);

        // Clean up
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));

        threadSynchronization.nextStep(Step::Finish);
    };

    std::array<LockStepThreadFunc, 2> threadFuncs = {
        std::move(thread0),
        std::move(thread1),
    };

    RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data());

    ASSERT_NE(currentStep, Step::Abort);
}

// Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after
// that.
TEST_P(EGLMultiContextTest, ThreadBClientWaitBeforeThreadASyncFlush)
{
    testFenceWithOpenRenderPass(FenceTest::ClientWait, FlushMethod::Flush);
}

// Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after
// that.
TEST_P(EGLMultiContextTest, ThreadBServerWaitBeforeThreadASyncFlush)
{
    testFenceWithOpenRenderPass(FenceTest::ServerWait, FlushMethod::Flush);
}

// Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after
// that.
TEST_P(EGLMultiContextTest, ThreadBGetStatusBeforeThreadASyncFlush)
{
    testFenceWithOpenRenderPass(FenceTest::GetStatus, FlushMethod::Flush);
}

// Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after
// that.
TEST_P(EGLMultiContextTest, ThreadBClientWaitBeforeThreadASyncFinish)
{
    testFenceWithOpenRenderPass(FenceTest::ClientWait, FlushMethod::Finish);
}

// Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after
// that.
TEST_P(EGLMultiContextTest, ThreadBServerWaitBeforeThreadASyncFinish)
{
    testFenceWithOpenRenderPass(FenceTest::ServerWait, FlushMethod::Finish);
}

// Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after
// that.
TEST_P(EGLMultiContextTest, ThreadBGetStatusBeforeThreadASyncFinish)
{
    testFenceWithOpenRenderPass(FenceTest::GetStatus, FlushMethod::Finish);
}

// Test that thread B can submit while three other threads are waiting for GPU to finish.
TEST_P(EGLMultiContextTest, ThreadBCanSubmitWhileThreadAWaiting)
{
    ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading());
    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_buffer_storage"));

    constexpr uint32_t kWidth  = 100;
    constexpr uint32_t kHeight = 200;

    std::mutex mutex;
    std::condition_variable condVar;

    enum class Step
    {
        Start,
        ThreadAMapBufferBufferRange,
        ThreadBSubmit,
        Finish,
        Abort,
    };

    Step currentStep = Step::Start;
    std::atomic<size_t> threadCountMakingMapBufferCall(0);
    std::atomic<size_t> threadCountFinishedMapBufferCall(0);
    constexpr size_t kMaxThreadCountMakingMapBufferCall = 3;
    auto threadA = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) {
        ThreadSynchronization<Step> threadSynchronization(&currentStep, &mutex, &condVar);
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start));

        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context));
        // Issue a draw
        ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
        drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f);
        // Issue a compute shader write to a buffer
        constexpr char kCS[] = R"(#version 310 es
                                layout(local_size_x=1, local_size_y=1, local_size_z=1) in;
                                layout(std140, binding = 0) buffer block {
                                    uvec4 data;
                                } outBlock;
                                void main()
                                {
                                    outBlock.data += uvec4(1);
                                })";
        ANGLE_GL_COMPUTE_PROGRAM(computeProgram, kCS);
        glUseProgram(computeProgram);
        constexpr std::array<uint32_t, 4> kInitData = {};
        GLBuffer coherentBuffer;
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, coherentBuffer);
        glBufferStorageEXT(GL_SHADER_STORAGE_BUFFER, sizeof(kInitData), kInitData.data(),
                           GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT_EXT |
                               GL_MAP_COHERENT_BIT_EXT);
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, coherentBuffer);
        glDispatchCompute(100, 100, 100);
        EXPECT_GL_NO_ERROR();

        // Map the buffers for read. This should trigger driver wait for GPU to finish
        glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, coherentBuffer);
        if (++threadCountMakingMapBufferCall == kMaxThreadCountMakingMapBufferCall)
        {
            threadSynchronization.nextStep(Step::ThreadAMapBufferBufferRange);
        }
        // Wait for thread B to start submit commands.
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::ThreadBSubmit));
        glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, sizeof(kInitData),
                         GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT);
        ASSERT_GL_NO_ERROR();
        ++threadCountFinishedMapBufferCall;

        ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish));
        EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::red);
        // Clean up
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    };

    auto threadB = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) {
        ThreadSynchronization<Step> threadSynchronization(&currentStep, &mutex, &condVar);
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context));

        // Prepare for draw and then wait until threadA is ready to wait
        ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
        glScissor(0, 0, 1, 1);
        ASSERT_TRUE(threadSynchronization.waitForStep(Step::ThreadAMapBufferBufferRange));

        // Test submit in a loop
        threadSynchronization.nextStep(Step::ThreadBSubmit);
        for (int loop = 0;
             loop < 16 && threadCountFinishedMapBufferCall < kMaxThreadCountMakingMapBufferCall;
             loop++)
        {
            drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f);
            glFinish();
            EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::green);
        }

        // Clean up
        ASSERT_GL_NO_ERROR();
        EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
        threadSynchronization.nextStep(Step::Finish);
    };

    std::array<LockStepThreadFunc, kMaxThreadCountMakingMapBufferCall + 1> threadFuncs;
    for (size_t i = 0; i < kMaxThreadCountMakingMapBufferCall; i++)
    {
        threadFuncs[i] = std::move(threadA);
    }
    threadFuncs[kMaxThreadCountMakingMapBufferCall] = std::move(threadB);

    RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data());
    ASSERT_NE(currentStep, Step::Abort);
}

}  // anonymous namespace

GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLMultiContextTest);
ANGLE_INSTANTIATE_TEST_ES31(EGLMultiContextTest);
