| // |
| // 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(¤tStep, &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(¤tStep, &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. Run the test without the rest of the code in change 3329273 |
| TEST_P(EGLMultiContextTest, RepeatedEglInitAndTerminate) |
| { |
| // GL and GLES drivers don't seem to perform appropriate cleanup |
| // SwiftShader fails with "Extension not supported" error on the bots |
| ANGLE_SKIP_TEST_IF(!IsVulkan() || isSwiftshader()); |
| |
| // Release all resources in parent thread |
| getEGLWindow()->destroyGL(); |
| |
| EGLDisplay dpy; |
| EGLSurface srf; |
| EGLContext ctx; |
| EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), 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 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(¤tStep, &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(¤tStep, &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); |
| } |
| |
| } // anonymous namespace |
| |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLMultiContextTest); |
| ANGLE_INSTANTIATE_TEST_ES31(EGLMultiContextTest); |