| // | |
| // 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 "common/unsafe_buffers.h" | |
| #include <array> | |
| #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) | |
| { | |
| setPbuffer(true); | |
| } | |
| 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, | |
| }; | |
| bool hasFenceSyncExtension() const | |
| { | |
| return IsEGLDisplayExtensionEnabled(getEGLWindow()->getDisplay(), "EGL_KHR_fence_sync"); | |
| } | |
| void testFenceWithOpenRenderPass(FenceTest test, FlushMethod flushMethod); | |
| EGLContext mContexts[2]; | |
| GLuint mTexture; | |
| }; | |
| // Test that a failed eglMakeCurrent to a new context does not leave that context bound to the | |
| // thread, and that subsequently destroying multiple contexts does not touch stale state. | |
| // https://crbug.com/536645892 | |
| TEST_P(EGLMultiContextTest, ThreadStateResetAfterMakeCurrentFailure) | |
| { | |
| EGLWindow *window = getEGLWindow(); | |
| EGLDisplay dpy = window->getDisplay(); | |
| ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_lock_surface3")); | |
| const EGLint clientVersion = EGL_OPENGL_ES3_BIT; | |
| const EGLint configAttribs[] = {EGL_RED_SIZE, | |
| 8, | |
| EGL_GREEN_SIZE, | |
| 8, | |
| EGL_BLUE_SIZE, | |
| 8, | |
| EGL_ALPHA_SIZE, | |
| 8, | |
| EGL_RENDERABLE_TYPE, | |
| clientVersion, | |
| EGL_SURFACE_TYPE, | |
| (EGL_PBUFFER_BIT | EGL_LOCK_SURFACE_BIT_KHR), | |
| EGL_NONE}; | |
| EGLint count = 0; | |
| EGLConfig config = EGL_NO_CONFIG_KHR; | |
| EXPECT_EGL_TRUE(eglChooseConfig(dpy, configAttribs, &config, 1, &count)); | |
| ANGLE_SKIP_TEST_IF(config == EGL_NO_CONFIG_KHR); | |
| const EGLint pbufferAttribs[] = {EGL_WIDTH, 4, EGL_HEIGHT, 4, EGL_NONE}; | |
| EGLSurface pbufferSurface = eglCreatePbufferSurface(dpy, config, pbufferAttribs); | |
| ASSERT_NE(pbufferSurface, EGL_NO_SURFACE); | |
| const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE}; | |
| mContexts[0] = eglCreateContext(dpy, config, nullptr, ctxAttribs); | |
| ASSERT_NE(mContexts[0], EGL_NO_CONTEXT); | |
| mContexts[1] = eglCreateContext(dpy, config, nullptr, ctxAttribs); | |
| ASSERT_NE(mContexts[1], EGL_NO_CONTEXT); | |
| // Ensure nothing is current on this thread. | |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); | |
| ASSERT_EQ(eglGetCurrentContext(), EGL_NO_CONTEXT); | |
| // Lock the surface so that eglMakeCurrent fails with EGL_BAD_ACCESS. | |
| const EGLint lockAttribs[] = {EGL_LOCK_USAGE_HINT_KHR, EGL_WRITE_SURFACE_BIT_KHR, EGL_NONE}; | |
| ASSERT_EGL_TRUE(eglLockSurfaceKHR(dpy, pbufferSurface, lockAttribs)); | |
| // eglMakeCurrent is expected to fail because the surface is locked. | |
| EXPECT_EGL_FALSE(eglMakeCurrent(dpy, pbufferSurface, pbufferSurface, mContexts[0])); | |
| EXPECT_EGL_ERROR(EGL_BAD_ACCESS); | |
| // The failed eglMakeCurrent must not have left the new context bound to the thread. | |
| EXPECT_EQ(eglGetCurrentContext(), EGL_NO_CONTEXT); | |
| EXPECT_EGL_TRUE(eglUnlockSurfaceKHR(dpy, pbufferSurface)); | |
| // Destroying the two contexts must not touch stale state left over from the failed | |
| // eglMakeCurrent above. | |
| EXPECT_EGL_TRUE(SafeDestroyContext(dpy, mContexts[0])); | |
| EXPECT_EGL_SUCCESS(); | |
| EXPECT_EGL_TRUE(SafeDestroyContext(dpy, mContexts[1])); | |
| EXPECT_EGL_SUCCESS(); | |
| EXPECT_EQ(eglGetCurrentContext(), EGL_NO_CONTEXT); | |
| EXPECT_EGL_TRUE(eglDestroySurface(dpy, pbufferSurface)); | |
| } | |
| // Test that a failed eglMakeCurrent with separate draw and read surfaces (where read surface | |
| // is locked) correctly fails validation and leaves no stale surface/thread state. | |
| // https://crbug.com/536645892 | |
| TEST_P(EGLMultiContextTest, SeparateReadSurfaceFailureCleansUp) | |
| { | |
| EGLWindow *window = getEGLWindow(); | |
| EGLDisplay dpy = window->getDisplay(); | |
| ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_lock_surface3")); | |
| const EGLint clientVersion = EGL_OPENGL_ES3_BIT; | |
| const EGLint configAttribs[] = {EGL_RED_SIZE, | |
| 8, | |
| EGL_GREEN_SIZE, | |
| 8, | |
| EGL_BLUE_SIZE, | |
| 8, | |
| EGL_ALPHA_SIZE, | |
| 8, | |
| EGL_RENDERABLE_TYPE, | |
| clientVersion, | |
| EGL_SURFACE_TYPE, | |
| (EGL_PBUFFER_BIT | EGL_LOCK_SURFACE_BIT_KHR), | |
| EGL_NONE}; | |
| EGLint count = 0; | |
| EGLConfig config = EGL_NO_CONFIG_KHR; | |
| EXPECT_EGL_TRUE(eglChooseConfig(dpy, configAttribs, &config, 1, &count)); | |
| ANGLE_SKIP_TEST_IF(config == EGL_NO_CONFIG_KHR); | |
| const EGLint pbufferAttribs[] = {EGL_WIDTH, 4, EGL_HEIGHT, 4, EGL_NONE}; | |
| EGLSurface drawSurface = eglCreatePbufferSurface(dpy, config, pbufferAttribs); | |
| ASSERT_NE(drawSurface, EGL_NO_SURFACE); | |
| EGLSurface readSurface = eglCreatePbufferSurface(dpy, config, pbufferAttribs); | |
| ASSERT_NE(readSurface, EGL_NO_SURFACE); | |
| const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE}; | |
| mContexts[0] = eglCreateContext(dpy, config, nullptr, ctxAttribs); | |
| ASSERT_NE(mContexts[0], EGL_NO_CONTEXT); | |
| // Ensure nothing is current on this thread. | |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); | |
| ASSERT_EQ(eglGetCurrentContext(), EGL_NO_CONTEXT); | |
| // Lock ONLY the read surface so that eglMakeCurrent fails. | |
| const EGLint lockAttribs[] = {EGL_LOCK_USAGE_HINT_KHR, EGL_WRITE_SURFACE_BIT_KHR, EGL_NONE}; | |
| ASSERT_EGL_TRUE(eglLockSurfaceKHR(dpy, readSurface, lockAttribs)); | |
| // eglMakeCurrent is expected to fail because the read surface is locked. | |
| EXPECT_EGL_FALSE(eglMakeCurrent(dpy, drawSurface, readSurface, mContexts[0])); | |
| EXPECT_EGL_ERROR(EGL_BAD_ACCESS); | |
| // Thread state must be clean. | |
| EXPECT_EQ(eglGetCurrentContext(), EGL_NO_CONTEXT); | |
| EXPECT_EQ(eglGetCurrentSurface(EGL_DRAW), EGL_NO_SURFACE); | |
| EXPECT_EQ(eglGetCurrentSurface(EGL_READ), EGL_NO_SURFACE); | |
| EXPECT_EGL_TRUE(eglUnlockSurfaceKHR(dpy, readSurface)); | |
| // Context and surfaces can now be safely destroyed without assertions or UAF. | |
| EXPECT_EGL_TRUE(SafeDestroyContext(dpy, mContexts[0])); | |
| EXPECT_EGL_SUCCESS(); | |
| EXPECT_EGL_TRUE(eglDestroySurface(dpy, drawSurface)); | |
| EXPECT_EGL_TRUE(eglDestroySurface(dpy, readSurface)); | |
| EXPECT_EGL_SUCCESS(); | |
| } | |
| // 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; | |
| std::array<EGLSurface, kThreadCount> surface = {EGL_NO_SURFACE, EGL_NO_SURFACE}; | |
| std::array<EGLContext, kThreadCount> ctx = {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. 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; | |
| EGLAttrib dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, | |
| GetParam().getRenderer(), | |
| EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, | |
| GetParam().getDeviceType(), | |
| EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, | |
| static_cast<EGLAttrib>(GetPbufferOnlyDefaultPlatformType()), | |
| EGL_NONE}; | |
| for (int i = 0; i < 50; i++) // Note: this test is fairly slow b/303089709 | |
| { | |
| std::thread thread = std::thread([&]() { | |
| dpy = eglGetPlatformDisplay(GetEglPlatform(), | |
| 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; | |
| EGLAttrib dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, | |
| GetParam().getRenderer(), | |
| EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, | |
| GetParam().getDeviceType(), | |
| EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, | |
| static_cast<EGLAttrib>(GetPbufferOnlyDefaultPlatformType()), | |
| EGL_NONE}; | |
| std::thread threadA = std::thread([&]() { | |
| dpy = eglGetPlatformDisplay(GetEglPlatform(), 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()); | |
| ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension()); | |
| 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); | |
| } | |
| // 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(¤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); | |
| // 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(¤tStep, &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); | |
| } | |
| // Test that if there are any placeholder objects when the programs don't use any resources | |
| // (such as textures), they can correctly be used in non-shared contexts (without causing | |
| // double-free). | |
| TEST_P(EGLMultiContextTest, NonSharedContextsReuseDescritorSetLayoutHandle) | |
| { | |
| EGLWindow *window = getEGLWindow(); | |
| EGLDisplay dpy = window->getDisplay(); | |
| EGLSurface surface = window->getSurface(); | |
| EGLContext context1 = window->createContext(EGL_NO_CONTEXT, nullptr); | |
| EGLContext context2 = window->createContext(EGL_NO_CONTEXT, nullptr); | |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context1)); | |
| EXPECT_EGL_SUCCESS(); | |
| ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); | |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); | |
| ASSERT_GL_NO_ERROR(); | |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context2)); | |
| EXPECT_EGL_SUCCESS(); | |
| ANGLE_GL_PROGRAM(program1, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); | |
| drawQuad(program1, essl1_shaders::PositionAttrib(), 0.5f); | |
| ASSERT_GL_NO_ERROR(); | |
| // Cleanup | |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); | |
| EXPECT_EGL_TRUE(eglDestroyContext(dpy, context1)); | |
| EXPECT_EGL_TRUE(eglDestroyContext(dpy, context2)); | |
| EXPECT_EGL_SUCCESS(); | |
| } | |
| } // anonymous namespace | |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLMultiContextTest); | |
| ANGLE_INSTANTIATE_TEST_ES31_AND( | |
| EGLMultiContextTest, | |
| ES31_VULKAN_SWIFTSHADER().enable(Feature::SupportsLockSurfaceExtension)); |