// // Copyright 2016 The ANGLE Project Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // EGLContextSharingTest.cpp: // Tests relating to shared Contexts. #include #include "common/tls.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/OSWindow.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 EGLContextSharingTest : public ANGLETest<> { public: EGLContextSharingTest() : 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(); } EGLContext mContexts[2] = {EGL_NO_CONTEXT, EGL_NO_CONTEXT}; GLuint mTexture; }; class EGLContextSharingTestNoFixture : public EGLContextSharingTest { public: EGLContextSharingTestNoFixture() : EGLContextSharingTest() {} void testSetUp() override { mOsWindow = OSWindow::New(); mMajorVersion = GetParam().majorVersion; } void testTearDown() override { if (mDisplay != EGL_NO_DISPLAY) { eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (mSurface != EGL_NO_SURFACE) { eglDestroySurface(mDisplay, mSurface); ASSERT_EGL_SUCCESS(); mSurface = EGL_NO_SURFACE; } for (auto &context : mContexts) { SafeDestroyContext(mDisplay, context); } eglTerminate(mDisplay); mDisplay = EGL_NO_DISPLAY; ASSERT_EGL_SUCCESS(); eglReleaseThread(); ASSERT_EGL_SUCCESS(); } mOsWindow->destroy(); OSWindow::Delete(&mOsWindow); ASSERT_EGL_SUCCESS() << "Error during test TearDown"; } bool chooseConfig(EGLConfig *config) const { bool result = false; EGLint count = 0; EGLint clientVersion = mMajorVersion == 3 ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_ES2_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(mDisplay, attribs, config, 1, &count); EXPECT_EGL_TRUE(result && (count > 0)); return result; } bool createContext(EGLConfig config, EGLContext *context, EGLContext share_context = EGL_NO_CONTEXT) { bool result = false; EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, mMajorVersion, EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE, mVirtualizationGroup++, EGL_NONE}; if (!IsEGLDisplayExtensionEnabled(mDisplay, "EGL_ANGLE_context_virtualization")) { attribs[2] = EGL_NONE; } *context = eglCreateContext(mDisplay, config, share_context, attribs); result = (*context != EGL_NO_CONTEXT); EXPECT_TRUE(result); return result; } bool createWindowSurface(EGLConfig config, EGLNativeWindowType win, EGLSurface *surface) { bool result = false; EGLint attribs[] = {EGL_NONE}; *surface = eglCreateWindowSurface(mDisplay, config, win, attribs); result = (*surface != EGL_NO_SURFACE); 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; } OSWindow *mOsWindow; EGLDisplay mDisplay = EGL_NO_DISPLAY; EGLSurface mSurface = EGL_NO_SURFACE; const EGLint kWidth = 64; const EGLint kHeight = 64; EGLint mMajorVersion = 0; std::atomic mVirtualizationGroup; }; // Tests that creating resources works after freeing the share context. TEST_P(EGLContextSharingTest, BindTextureAfterShareContextFree) { EGLDisplay display = getEGLWindow()->getDisplay(); EGLConfig config = getEGLWindow()->getConfig(); EGLSurface surface = getEGLWindow()->getSurface(); const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, getEGLWindow()->getClientMajorVersion(), EGL_NONE}; mContexts[0] = eglCreateContext(display, config, nullptr, contextAttribs); ASSERT_EGL_SUCCESS(); ASSERT_TRUE(mContexts[0] != EGL_NO_CONTEXT); mContexts[1] = eglCreateContext(display, config, mContexts[1], contextAttribs); ASSERT_EGL_SUCCESS(); ASSERT_TRUE(mContexts[1] != EGL_NO_CONTEXT); ASSERT_EGL_TRUE(SafeDestroyContext(display, mContexts[0])); ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[1])); ASSERT_EGL_SUCCESS(); glGenTextures(1, &mTexture); glBindTexture(GL_TEXTURE_2D, mTexture); ASSERT_GL_NO_ERROR(); } // Tests the creation of contexts using EGL_ANGLE_display_texture_share_group TEST_P(EGLContextSharingTest, DisplayShareGroupContextCreation) { EGLDisplay display = getEGLWindow()->getDisplay(); EGLConfig config = getEGLWindow()->getConfig(); const EGLint inShareGroupContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE}; // Check whether extension's supported to avoid clearing the EGL error state // after failed context creation. bool extensionEnabled = IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group"); // Test creating two contexts in the global share group mContexts[0] = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); mContexts[1] = eglCreateContext(display, config, mContexts[1], inShareGroupContextAttribs); if (!extensionEnabled) { // Make sure an error is generated and early-exit ASSERT_EGL_ERROR(EGL_BAD_ATTRIBUTE); ASSERT_EQ(EGL_NO_CONTEXT, mContexts[0]); return; } ASSERT_EGL_SUCCESS(); ASSERT_NE(EGL_NO_CONTEXT, mContexts[0]); ASSERT_NE(EGL_NO_CONTEXT, mContexts[1]); eglDestroyContext(display, mContexts[0]); mContexts[0] = EGL_NO_CONTEXT; // Try creating a context that is not in the global share group but tries to share with a // context that is const EGLint notInShareGroupContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_FALSE, EGL_NONE}; mContexts[0] = eglCreateContext(display, config, mContexts[1], notInShareGroupContextAttribs); ASSERT_EGL_ERROR(EGL_BAD_ATTRIBUTE); ASSERT_TRUE(mContexts[0] == EGL_NO_CONTEXT); } // Tests the sharing of textures using EGL_ANGLE_display_texture_share_group TEST_P(EGLContextSharingTest, DisplayShareGroupObjectSharing) { EGLDisplay display = getEGLWindow()->getDisplay(); ANGLE_SKIP_TEST_IF( !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")); EGLConfig config = getEGLWindow()->getConfig(); EGLSurface surface = getEGLWindow()->getSurface(); const EGLint inShareGroupContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE}; // Create two contexts in the global share group but not in the same context share group mContexts[0] = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); mContexts[1] = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); ASSERT_EGL_SUCCESS(); ASSERT_NE(EGL_NO_CONTEXT, mContexts[0]); ASSERT_NE(EGL_NO_CONTEXT, mContexts[1]); ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); ASSERT_EGL_SUCCESS(); // Create a texture and buffer in ctx 0 GLTexture textureFromCtx0; glBindTexture(GL_TEXTURE_2D, textureFromCtx0); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glBindTexture(GL_TEXTURE_2D, 0); ASSERT_GL_TRUE(glIsTexture(textureFromCtx0)); GLBuffer bufferFromCtx0; glBindBuffer(GL_ARRAY_BUFFER, bufferFromCtx0); glBufferData(GL_ARRAY_BUFFER, 1, nullptr, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); ASSERT_GL_TRUE(glIsBuffer(bufferFromCtx0)); ASSERT_GL_NO_ERROR(); // Switch to context 1 and verify that the texture is accessible but the buffer is not ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[1])); ASSERT_EGL_SUCCESS(); ASSERT_GL_TRUE(glIsTexture(textureFromCtx0)); ASSERT_GL_FALSE(glIsBuffer(bufferFromCtx0)); ASSERT_GL_NO_ERROR(); // Call readpixels on the texture to verify that the backend has proper support GLFramebuffer fbo; glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureFromCtx0, 0); GLubyte pixel[4]; glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); ASSERT_GL_NO_ERROR(); // Switch back to context 0 and delete the buffer ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); ASSERT_EGL_SUCCESS(); } // Tests that shared textures using EGL_ANGLE_display_texture_share_group are released when the last // context is destroyed TEST_P(EGLContextSharingTest, DisplayShareGroupReleasedWithLastContext) { EGLDisplay display = getEGLWindow()->getDisplay(); ANGLE_SKIP_TEST_IF( !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")); EGLConfig config = getEGLWindow()->getConfig(); EGLSurface surface = getEGLWindow()->getSurface(); const EGLint inShareGroupContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE}; // Create two contexts in the global share group but not in the same context share group mContexts[0] = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); mContexts[1] = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); // Create a texture and buffer in ctx 0 ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); GLTexture textureFromCtx0; glBindTexture(GL_TEXTURE_2D, textureFromCtx0); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glBindTexture(GL_TEXTURE_2D, 0); ASSERT_GL_TRUE(glIsTexture(textureFromCtx0)); // Switch to context 1 and verify that the texture is accessible ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[1])); ASSERT_GL_TRUE(glIsTexture(textureFromCtx0)); // Destroy both contexts, the texture should be cleaned up automatically ASSERT_EGL_TRUE(eglDestroyContext(display, mContexts[0])); mContexts[0] = EGL_NO_CONTEXT; ASSERT_EGL_TRUE(eglDestroyContext(display, mContexts[1])); mContexts[1] = EGL_NO_CONTEXT; // Unmake current, so the context can be released. ASSERT_EGL_TRUE(eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); // Create a new context and verify it cannot access the texture previously created mContexts[0] = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); ASSERT_GL_FALSE(glIsTexture(textureFromCtx0)); } // Tests that after creating a texture using EGL_ANGLE_display_texture_share_group, // and deleting the Context and the egl::ShareGroup who own a texture staged updates, // the texture staged updates are flushed, and the Context and egl::ShareGroup can be destroyed // successfully, and the texture can still be accessed from the global display texture share group TEST_P(EGLContextSharingTest, DisplayShareGroupReleaseShareGroupThatOwnsStagedUpdates) { EGLDisplay display = getEGLWindow()->getDisplay(); ANGLE_SKIP_TEST_IF( !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")); EGLConfig config = getEGLWindow()->getConfig(); EGLSurface surface = getEGLWindow()->getSurface(); const EGLint inShareGroupContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE}; // Create two contexts in the global share group, but not in the same context share group EGLContext context1 = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); EGLContext context2 = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); // Create a texture in context1 and stage a texture update ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, context1)); constexpr GLsizei kTexSize = 2; const GLColor kBlueData[kTexSize * kTexSize] = {GLColor::blue, GLColor::blue, GLColor::blue, GLColor::blue}; GLTexture textureFromCtx0; glBindTexture(GL_TEXTURE_2D, textureFromCtx0); // This will stage a texture update in context1's SharedGroup::BufferPool glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kBlueData); // Destroy context 1, this also destroys context1's SharedGroup and BufferPool // The texture staged update in context1's SharedGroup BufferPool will be flushed SafeDestroyContext(display, context1); // Switch to context2 and verify that the texture is accessible ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, context2)); ASSERT_GL_TRUE(glIsTexture(textureFromCtx0)); // Sample from textureFromCtx0 and check it works properly glBindTexture(GL_TEXTURE_2D, textureFromCtx0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); ANGLE_GL_PROGRAM(program1, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); glUseProgram(program1); drawQuad(program1, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue); // Destroy context2 eglDestroyContext(display, context2); } // Tests that after creating a texture using EGL_ANGLE_display_texture_share_group, // and use it for sampling, and then deleting the Context (which destroys shareGroup) works. If // anything cached in ShareGroup, it should be handled nicely if texture can outlive ShareGroup (for // example, bugs like angleproject:7466). TEST_P(EGLContextSharingTest, DisplayShareGroupReleaseShareGroupThenDestroyTexture) { EGLDisplay display = getEGLWindow()->getDisplay(); ANGLE_SKIP_TEST_IF( !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")); EGLConfig config = getEGLWindow()->getConfig(); EGLSurface surface = getEGLWindow()->getSurface(); const EGLint inShareGroupContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE}; // Create two contexts in the global share group, but not in the same context share group EGLContext context1 = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); EGLContext context2 = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); // Create a texture in context1 and stage a texture update ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, context1)); constexpr GLsizei kTexSize = 2; const GLColor kBlueData[kTexSize * kTexSize] = {GLColor::blue, GLColor::blue, GLColor::blue, GLColor::blue}; { GLTexture textureFromCtx1; glBindTexture(GL_TEXTURE_2D, textureFromCtx1); // This will stage a texture update in context1's SharedGroup::BufferPool glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kBlueData); // Sample from textureFromCtx1 and check it works properly glBindTexture(GL_TEXTURE_2D, textureFromCtx1); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); ANGLE_GL_PROGRAM(program1, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); glUseProgram(program1); drawQuad(program1, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue); // Destroy context 1, this also destroys context1's SharedGroup and BufferPool // The texture staged update in context1's SharedGroup BufferPool will be flushed SafeDestroyContext(display, context1); // Switch to context2 and verify that the texture is accessible ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, context2)); ASSERT_GL_TRUE(glIsTexture(textureFromCtx1)); // textureFromCtx1 should be destroyed when leaving this scope } // Destroy context2 eglDestroyContext(display, context2); } // Tests that deleting an object on one Context doesn't destroy it ahead-of-time. Mostly focused // on the Vulkan back-end where we manage object lifetime manually. TEST_P(EGLContextSharingTest, TextureLifetime) { EGLWindow *eglWindow = getEGLWindow(); EGLConfig config = getEGLWindow()->getConfig(); EGLDisplay display = getEGLWindow()->getDisplay(); // Create a pbuffer surface for use with a shared context. EGLSurface surface = eglWindow->getSurface(); EGLContext mainContext = eglWindow->getContext(); // Initialize a shared context. mContexts[0] = eglCreateContext(display, config, mainContext, nullptr); ASSERT_NE(mContexts[0], EGL_NO_CONTEXT); // Create a Texture on the shared context. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); constexpr GLsizei kTexSize = 2; const GLColor kTexData[kTexSize * kTexSize] = {GLColor::red, GLColor::green, GLColor::blue, GLColor::yellow}; GLTexture tex; glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, kTexData); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Make the main Context current and draw with the texture. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mainContext)); glBindTexture(GL_TEXTURE_2D, tex); ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); glUseProgram(program); // No uniform update because the update seems to hide the error on Vulkan. // Enqueue the draw call. drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); // Delete the texture in the main context to orphan it. // Do not read back the data to keep the commands in the graph. tex.reset(); // Bind and delete the test context. This should trigger texture garbage collection. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); SafeDestroyContext(display, mContexts[0]); // Bind the main context to clean up the test. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mainContext)); } // Tests that deleting an object on one Context doesn't destroy it ahead-of-time. Mostly focused // on the Vulkan back-end where we manage object lifetime manually. TEST_P(EGLContextSharingTest, SamplerLifetime) { EGLWindow *eglWindow = getEGLWindow(); EGLConfig config = getEGLWindow()->getConfig(); EGLDisplay display = getEGLWindow()->getDisplay(); ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3); ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(display, "EGL_KHR_create_context")); // Create a pbuffer surface for use with a shared context. EGLSurface surface = eglWindow->getSurface(); EGLContext mainContext = eglWindow->getContext(); std::vector contextAttributes; contextAttributes.push_back(EGL_CONTEXT_MAJOR_VERSION_KHR); contextAttributes.push_back(getClientMajorVersion()); contextAttributes.push_back(EGL_NONE); // Initialize a shared context. mContexts[0] = eglCreateContext(display, config, mainContext, contextAttributes.data()); ASSERT_NE(mContexts[0], EGL_NO_CONTEXT); // Create a Texture on the shared context. Also create a Sampler object. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); constexpr GLsizei kTexSize = 2; const GLColor kTexData[kTexSize * kTexSize] = {GLColor::red, GLColor::green, GLColor::blue, GLColor::yellow}; GLTexture tex; glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, kTexData); GLSampler sampler; glBindSampler(0, sampler); glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Make the main Context current and draw with the texture and sampler. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mainContext)); glBindTexture(GL_TEXTURE_2D, tex); glBindSampler(0, sampler); ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); glUseProgram(program); // No uniform update because the update seems to hide the error on Vulkan. // Enqueue the draw call. drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); // Delete the texture and sampler in the main context to orphan them. // Do not read back the data to keep the commands in the graph. tex.reset(); sampler.reset(); // Bind and delete the test context. This should trigger texture garbage collection. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mContexts[0])); SafeDestroyContext(display, mContexts[0]); // Bind the main context to clean up the test. ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, mainContext)); } // Test that deleting an object reading from a shared object in one context doesn't cause the other // context to crash. Mostly focused on the Vulkan back-end where we track resource dependencies in // a graph. TEST_P(EGLContextSharingTest, DeleteReaderOfSharedTexture) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); // GL Fences require GLES 3.0+ ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3); // 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(t == 0 ? EGL_NO_CONTEXT : ctx[0], nullptr); EXPECT_NE(EGL_NO_CONTEXT, ctx[t]); } // Initialize test resources. They are done outside the threads to reduce the sources of // errors and thus deadlock-free teardown. ASSERT_EGL_TRUE(eglMakeCurrent(dpy, surface[1], surface[1], ctx[1])); // Shared texture to read from. constexpr GLsizei kTexSize = 1; const GLColor kTexData = GLColor::red; GLTexture sharedTex; glBindTexture(GL_TEXTURE_2D, sharedTex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, &kTexData); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Resources for each context. GLRenderbuffer renderbuffer[kThreadCount]; GLFramebuffer fbo[kThreadCount]; GLProgram program[kThreadCount]; for (size_t t = 0; t < kThreadCount; ++t) { ASSERT_EGL_TRUE(eglMakeCurrent(dpy, surface[t], surface[t], ctx[t])); glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer[t]); constexpr int kRenderbufferSize = 4; glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kRenderbufferSize, kRenderbufferSize); glBindFramebuffer(GL_FRAMEBUFFER, fbo[t]); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer[t]); glBindTexture(GL_TEXTURE_2D, sharedTex); program[t].makeRaster(essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); ASSERT_TRUE(program[t].valid()); } EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); // Synchronization tools to ensure the two threads are interleaved as designed by this test. std::mutex mutex; std::condition_variable condVar; std::atomic deletingThreadSyncObj; std::atomic continuingThreadSyncObj; enum class Step { Start, Thread0Draw, Thread1Draw, Thread0Delete, Finish, Abort, }; Step currentStep = Step::Start; std::thread deletingThread = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0])); EXPECT_EGL_SUCCESS(); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); // Draw using the shared texture. drawQuad(program[0].get(), essl1_shaders::PositionAttrib(), 0.5f); deletingThreadSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); ASSERT_GL_NO_ERROR(); // Force the fence to be created glFlush(); // Wait for the other thread to also draw using the shared texture. threadSynchronization.nextStep(Step::Thread0Draw); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Draw)); ASSERT_TRUE(continuingThreadSyncObj != nullptr); glWaitSync(continuingThreadSyncObj, 0, GL_TIMEOUT_IGNORED); ASSERT_GL_NO_ERROR(); glDeleteSync(continuingThreadSyncObj); ASSERT_GL_NO_ERROR(); continuingThreadSyncObj = nullptr; // Delete this thread's framebuffer (reader of the shared texture). fbo[0].reset(); // Wait for the other thread to use the shared texture again before unbinding the // context (so no implicit flush happens). threadSynchronization.nextStep(Step::Thread0Delete); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); EXPECT_GL_NO_ERROR(); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EGL_SUCCESS(); }); std::thread continuingThread = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[1], surface[1], ctx[1])); EXPECT_EGL_SUCCESS(); // Wait for first thread to draw using the shared texture. ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Draw)); ASSERT_TRUE(deletingThreadSyncObj != nullptr); glWaitSync(deletingThreadSyncObj, 0, GL_TIMEOUT_IGNORED); ASSERT_GL_NO_ERROR(); glDeleteSync(deletingThreadSyncObj); ASSERT_GL_NO_ERROR(); deletingThreadSyncObj = nullptr; // Draw using the shared texture. drawQuad(program[0].get(), essl1_shaders::PositionAttrib(), 0.5f); continuingThreadSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); ASSERT_GL_NO_ERROR(); // Force the fence to be created glFlush(); // Wait for the other thread to delete its framebuffer. threadSynchronization.nextStep(Step::Thread1Draw); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Delete)); // Write to the shared texture differently, so a dependency is created from the previous // readers of the shared texture (the two framebuffers of the two threads) to the new // commands being recorded for the shared texture. // // If the backend attempts to create a dependency from nodes associated with the // previous readers of the texture to the new node that will contain the following // commands, there will be a use-after-free error. const GLColor kTexData2 = GLColor::green; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, &kTexData2); drawQuad(program[0].get(), essl1_shaders::PositionAttrib(), 0.5f); 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 for (size_t t = 0; t < kThreadCount; ++t) { eglDestroySurface(dpy, surface[t]); eglDestroyContext(dpy, ctx[t]); } } // Tests that Context will be destroyed in thread cleanup callback and it is safe to call GLES APIs. TEST_P(EGLContextSharingTest, ThreadCleanupCallback) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); ANGLE_SKIP_TEST_IF(!IsAndroid()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLConfig config = window->getConfig(); EGLContext context = window->createContext(EGL_NO_CONTEXT, nullptr); EXPECT_NE(context, EGL_NO_CONTEXT); EGLint pbufferAttributes[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; EGLSurface surface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); EXPECT_NE(surface, EGL_NO_SURFACE); auto registerThreadCleanupCallback = []() { #if defined(ANGLE_PLATFORM_ANDROID) static pthread_once_t once = PTHREAD_ONCE_INIT; static TLSIndex tlsIndex = TLS_INVALID_INDEX; auto createThreadCleanupTLSIndex = []() { auto threadCleanupCallback = [](void *) { // From the point of view of this test Context is still current. glFinish(); // Test expects that Context will be destroyed here. eglReleaseThread(); }; tlsIndex = CreateTLSIndex(threadCleanupCallback); }; pthread_once(&once, createThreadCleanupTLSIndex); ASSERT(tlsIndex != TLS_INVALID_INDEX); // Set any non nullptr value. SetTLSValue(tlsIndex, &once); #endif }; std::thread thread([&]() { registerThreadCleanupCallback(); eglMakeCurrent(dpy, surface, surface, context); ASSERT_EGL_SUCCESS(); // 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); // Destroy context and surface while still current. eglDestroyContext(dpy, context); ASSERT_EGL_SUCCESS(); eglDestroySurface(dpy, surface); ASSERT_EGL_SUCCESS(); }); thread.join(); // Check if context was actually destroyed. EGLint val; EXPECT_EGL_FALSE(eglQueryContext(dpy, context, EGL_CONTEXT_CLIENT_TYPE, &val)); EXPECT_EQ(eglGetError(), EGL_BAD_CONTEXT); } // Tests that Context will be automatically unmade from current on thread exit. TEST_P(EGLContextSharingTest, UnmakeFromCurrentOnThreadExit) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); ANGLE_SKIP_TEST_IF(!IsAndroid()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLConfig config = window->getConfig(); EGLContext context = window->createContext(EGL_NO_CONTEXT, nullptr); EXPECT_NE(context, EGL_NO_CONTEXT); EGLint pbufferAttributes[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; EGLSurface surface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); EXPECT_NE(surface, EGL_NO_SURFACE); std::mutex mutex; std::condition_variable condVar; enum class Step { Start, ThreadMakeCurrent, Finish, Abort, }; Step currentStep = Step::Start; ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); std::thread thread([&]() { eglMakeCurrent(dpy, surface, surface, context); ASSERT_EGL_SUCCESS(); threadSynchronization.nextStep(Step::ThreadMakeCurrent); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); // 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); }); // Wait while Context is made current in the thread. ASSERT_TRUE(threadSynchronization.waitForStep(Step::ThreadMakeCurrent)); // This should fail because context is already current in other thread. eglMakeCurrent(dpy, surface, surface, context); EXPECT_EQ(eglGetError(), EGL_BAD_ACCESS); // Finish the thread. threadSynchronization.nextStep(Step::Finish); thread.join(); // This should succeed, because thread is finished so context is no longer current. eglMakeCurrent(dpy, surface, surface, context); EXPECT_EGL_SUCCESS(); eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); ASSERT_EGL_SUCCESS(); // Destroy context and surface. eglDestroyContext(dpy, context); ASSERT_EGL_SUCCESS(); eglDestroySurface(dpy, surface); ASSERT_EGL_SUCCESS(); } // Test that an inactive but alive thread doesn't prevent memory cleanup. TEST_P(EGLContextSharingTestNoFixture, InactiveThreadDoesntPreventCleanup) { EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, GetParam().getDeviceType(), EGL_NONE}; // Synchronization tools to ensure the two threads are interleaved as designed by this test. std::mutex mutex; std::condition_variable condVar; enum class Step { Start, Thread0Initialize, Thread1MakeCurrent, Thread0MakeCurrent, Thread1Render, Thread0Terminate, Finish, Abort, }; Step currentStep = Step::Start; std::thread thread0 = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); mDisplay = eglGetPlatformDisplayEXT( EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); threadSynchronization.nextStep(Step::Thread0Initialize); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1MakeCurrent)); EGLContext ctx; EGLSurface srf; EGLConfig config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); EXPECT_TRUE(createContext(config, &ctx)); EXPECT_TRUE(createPbufferSurface(mDisplay, config, 1280, 720, &srf)); ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed."; EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, srf, srf, ctx)); threadSynchronization.nextStep(Step::Thread0MakeCurrent); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Render)); eglTerminate(mDisplay); EXPECT_EGL_SUCCESS(); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); threadSynchronization.nextStep(Step::Thread0Terminate); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); // Wait a little to simulate an inactive but alive thread. angle::Sleep(100); }); std::thread thread1 = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Initialize)); EGLContext ctx; EGLSurface srf; EGLConfig config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); EXPECT_TRUE(createContext(config, &ctx)); EXPECT_TRUE(createPbufferSurface(mDisplay, config, 1280, 720, &srf)); ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed."; EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, srf, srf, ctx)); threadSynchronization.nextStep(Step::Thread1MakeCurrent); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0MakeCurrent)); // Clear and read back to make sure thread uses context and surface. glClearColor(1.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); EXPECT_PIXEL_EQ(0, 0, 255, 0, 0, 255); threadSynchronization.nextStep(Step::Thread1Render); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Terminate)); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); threadSynchronization.nextStep(Step::Finish); }); thread1.join(); thread0.join(); ASSERT_NE(currentStep, Step::Abort); } // Test that eglTerminate() with a thread doesn't cause other threads to crash. TEST_P(EGLContextSharingTestNoFixture, EglTerminateMultiThreaded) { // http://anglebug.com/42264731 // The following EGL calls led to a crash in eglMakeCurrent(): // // Thread A: eglMakeCurrent(context A) // Thread B: eglDestroyContext(context A) // B: eglTerminate() <<--- this release context A // Thread A: eglMakeCurrent(context B) EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE}; mDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); EGLConfig config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight); EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface)); ASSERT_EGL_SUCCESS() << "eglCreateWindowSurface failed."; EXPECT_TRUE(createContext(config, &mContexts[0])); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[0])); // Must be after the eglMakeCurrent() so renderer string is initialized. ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); // TODO(http://anglebug.com/42264822): Fails with OpenGL ES backend. ANGLE_SKIP_TEST_IF(IsOpenGLES()); EXPECT_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); // Synchronization tools to ensure the two threads are interleaved as designed by this test. std::mutex mutex; std::condition_variable condVar; enum class Step { Start, Thread0Clear, Thread1Terminate, Thread0MakeCurrentContext1, Finish, Abort, }; Step currentStep = Step::Start; std::thread thread0 = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[0])); // Clear and read back to make sure thread 0 uses context 0. glClearColor(1.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); EXPECT_PIXEL_EQ(0, 0, 255, 0, 0, 255); // Wait for thread 1 to clear. threadSynchronization.nextStep(Step::Thread0Clear); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Terminate)); // First Display was terminated, so we need to create a new one to create a new Context. mDisplay = eglGetPlatformDisplayEXT( EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); EXPECT_TRUE(createContext(config, &mContexts[1])); // Thread1's terminate call will make mSurface an invalid handle, recreate a new surface EXPECT_TRUE(createPbufferSurface(mDisplay, config, 1280, 720, &mSurface)); ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed."; EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[1])); // Clear and read back to make sure thread 0 uses context 1. glClearColor(1.0, 1.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); EXPECT_PIXEL_EQ(0, 0, 255, 255, 0, 255); // Cleanup EXPECT_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_TRUE(SafeDestroyContext(mDisplay, mContexts[1])); eglDestroySurface(mDisplay, mSurface); mSurface = EGL_NO_SURFACE; eglTerminate(mDisplay); mDisplay = EGL_NO_DISPLAY; EXPECT_EGL_SUCCESS(); threadSynchronization.nextStep(Step::Thread0MakeCurrentContext1); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); }); std::thread thread1 = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); // Wait for thread 0 to clear. ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Clear)); // Destroy context 0 while thread1 has it current. EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_TRUE(SafeDestroyContext(mDisplay, mContexts[0])); EXPECT_EGL_SUCCESS(); eglTerminate(mDisplay); mDisplay = EGL_NO_DISPLAY; EXPECT_EGL_SUCCESS(); // Wait for the thread 0 to make a new context and glClear(). threadSynchronization.nextStep(Step::Thread1Terminate); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0MakeCurrentContext1)); threadSynchronization.nextStep(Step::Finish); }); thread0.join(); thread1.join(); ASSERT_NE(currentStep, Step::Abort); } // Test that eglDestoryContext() can be called multiple times on the same Context without causing // errors. TEST_P(EGLContextSharingTestNoFixture, EglDestoryContextManyTimesSameContext) { EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE}; mDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); EGLConfig config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight); EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface)); ASSERT_EGL_SUCCESS() << "eglCreateWindowSurface failed."; EXPECT_TRUE(createContext(config, &mContexts[0])); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[0])); // Must be after the eglMakeCurrent() so renderer string is initialized. ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); // TODO(http://anglebug.com/42264822): Fails with OpenGL ES backend. ANGLE_SKIP_TEST_IF(IsOpenGLES()); EXPECT_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); // Synchronization tools to ensure the two threads are interleaved as designed by this test. std::mutex mutex; std::condition_variable condVar; enum class Step { Start, Thread0Clear, Thread1Terminate, Thread0MakeCurrentContext1, Finish, Abort, }; Step currentStep = Step::Start; std::thread thread0 = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[0])); // Clear and read back to make sure thread 0 uses context 0. glClearColor(1.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); EXPECT_PIXEL_EQ(0, 0, 255, 0, 0, 255); // Wait for thread 1 to clear. threadSynchronization.nextStep(Step::Thread0Clear); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Terminate)); // First Display was terminated, so we need to create a new one to create a new Context. mDisplay = eglGetPlatformDisplayEXT( EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); EXPECT_TRUE(createContext(config, &mContexts[1])); // Thread1's terminate call will make mSurface an invalid handle, recreate a new surface EXPECT_TRUE(createPbufferSurface(mDisplay, config, 1280, 720, &mSurface)); ASSERT_EGL_SUCCESS() << "eglCreatePbufferSurface failed."; EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[1])); EXPECT_EGL_SUCCESS(); // Clear and read back to make sure thread 0 uses context 1. glClearColor(1.0, 1.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); EXPECT_PIXEL_EQ(0, 0, 255, 255, 0, 255); // Cleanup EXPECT_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_TRUE(SafeDestroyContext(mDisplay, mContexts[1])); eglDestroySurface(mDisplay, mSurface); mSurface = EGL_NO_SURFACE; eglTerminate(mDisplay); mDisplay = EGL_NO_DISPLAY; EXPECT_EGL_SUCCESS(); threadSynchronization.nextStep(Step::Thread0MakeCurrentContext1); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); }); std::thread thread1 = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); // Wait for thread 0 to clear. ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Clear)); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); // Destroy context 0 5 times while thread1 has it current. EXPECT_EGL_TRUE(eglDestroyContext(mDisplay, mContexts[0])); EXPECT_EGL_TRUE(eglDestroyContext(mDisplay, mContexts[0])); EXPECT_EGL_TRUE(eglDestroyContext(mDisplay, mContexts[0])); EXPECT_EGL_TRUE(eglDestroyContext(mDisplay, mContexts[0])); EXPECT_EGL_TRUE(eglDestroyContext(mDisplay, mContexts[0])); mContexts[0] = EGL_NO_CONTEXT; eglDestroySurface(mDisplay, mSurface); mSurface = EGL_NO_SURFACE; eglTerminate(mDisplay); mDisplay = EGL_NO_DISPLAY; EXPECT_EGL_SUCCESS(); // Wait for the thread 0 to make a new context and glClear(). threadSynchronization.nextStep(Step::Thread1Terminate); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0MakeCurrentContext1)); threadSynchronization.nextStep(Step::Finish); }); thread0.join(); thread1.join(); ASSERT_NE(currentStep, Step::Abort); } // Test that eglTerminate() can be called multiple times on the same Display while Contexts are // still current without causing errors. TEST_P(EGLContextSharingTestNoFixture, EglTerminateMultipleTimes) { // https://bugs.chromium.org/p/skia/issues/detail?id=12413#c4 // The following sequence caused a crash with the D3D backend in the Skia infra: // eglDestroyContext(ctx0) // eglDestroySurface(srf0) // eglTerminate(shared-display) // eglDestroyContext(ctx1) // completes the cleanup from the above terminate // eglDestroySurface(srf1) // eglTerminate(shared-display) EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE}; mDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); EGLConfig config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight); EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface)); EXPECT_TRUE(mSurface != EGL_NO_SURFACE); ASSERT_EGL_SUCCESS() << "eglCreateWindowSurface failed."; EXPECT_TRUE(createContext(config, &mContexts[0])); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[0])); EXPECT_TRUE(createContext(config, &mContexts[1])); EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[1])); // Must be after the eglMakeCurrent() so renderer string is initialized. // TODO(http://anglebug.com/42264822): Fails with Mac + OpenGL backend. ANGLE_SKIP_TEST_IF(IsMac() && IsOpenGL()); EXPECT_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); eglDestroySurface(mDisplay, mSurface); mSurface = EGL_NO_SURFACE; EXPECT_EGL_TRUE(eglDestroyContext(mDisplay, mContexts[0])); mContexts[0] = EGL_NO_CONTEXT; eglTerminate(mDisplay); EXPECT_EGL_SUCCESS(); eglDestroyContext(mDisplay, mContexts[1]); mContexts[1] = EGL_NO_CONTEXT; ASSERT_EGL_ERROR(EGL_NOT_INITIALIZED); eglTerminate(mDisplay); EXPECT_EGL_SUCCESS(); mDisplay = EGL_NO_DISPLAY; } // Test that we can eglSwapBuffers in one thread while another thread renders to a texture. TEST_P(EGLContextSharingTestNoFixture, SwapBuffersShared) { EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE}; mDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); EGLConfig config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight); EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface)); ASSERT_EGL_SUCCESS() << "eglCreateWindowSurface failed."; EGLSurface pbufferSurface; EXPECT_TRUE(createPbufferSurface(mDisplay, config, kWidth, kHeight, &pbufferSurface)); // Create the two contextss EXPECT_TRUE(createContext(config, &mContexts[0])); EXPECT_TRUE(createContext(config, &mContexts[1], mContexts[0])); eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[0]); ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); // Synchronization tools to ensure the two threads are interleaved as designed by this test. std::mutex mutex; std::condition_variable condVar; enum class Step { Start, TextureInitialized, Finish, Abort, }; Step currentStep = Step::Start; // Sample a texture in the swap thread. std::thread swapThread = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); eglMakeCurrent(mDisplay, mSurface, mSurface, mContexts[0]); glGenTextures(1, &mTexture); glBindTexture(GL_TEXTURE_2D, mTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); threadSynchronization.nextStep(Step::TextureInitialized); ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); glUseProgram(program); for (int i = 0; i < 100; ++i) { glClear(GL_COLOR_BUFFER_BIT); drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); eglSwapBuffers(mDisplay, mSurface); } eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglReleaseThread(); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); }); // Render to the texture in the render thread. std::thread renderThread = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_EGL_TRUE(eglMakeCurrent(mDisplay, pbufferSurface, pbufferSurface, mContexts[1])); GLFramebuffer fbo; glBindFramebuffer(GL_FRAMEBUFFER, fbo); ANGLE_GL_PROGRAM(redProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); ANGLE_GL_PROGRAM(greenProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); ANGLE_GL_PROGRAM(blueProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Blue()); // The render thread will draw to the texture. ASSERT_TRUE(threadSynchronization.waitForStep(Step::TextureInitialized)); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0); glDisable(GL_DEPTH_TEST); for (int i = 0; i < 400; ++i) { glClear(GL_COLOR_BUFFER_BIT); glUseProgram(redProgram); drawQuad(redProgram, essl1_shaders::PositionAttrib(), 0.5f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(greenProgram); drawQuad(greenProgram, essl1_shaders::PositionAttrib(), 0.5f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(blueProgram); drawQuad(blueProgram, essl1_shaders::PositionAttrib(), 0.5f); } eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglReleaseThread(); threadSynchronization.nextStep(Step::Finish); }); swapThread.join(); renderThread.join(); eglDestroySurface(mDisplay, pbufferSurface); ASSERT_EGL_SUCCESS(); } class EGLContextSharingTestNoSyncTextureUploads : public EGLContextSharingTest {}; // Test that an application that does not synchronize when using textures across shared contexts can // still see texture updates. This behavior is not required by the GLES specification, but is // exhibited by some applications. That application will malfunction if our implementation does not // handle this in the way it expects. Only the vulkan backend has the workaround needed for this // usecase. TEST_P(EGLContextSharingTestNoSyncTextureUploads, NoSync) { EGLDisplay display = getEGLWindow()->getDisplay(); EGLConfig config = getEGLWindow()->getConfig(); const EGLint inShareGroupContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; const EGLint pbufferAttributes[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; constexpr size_t kThreadCount = 2; EGLSurface surface[kThreadCount] = {EGL_NO_SURFACE, EGL_NO_SURFACE}; for (size_t t = 0; t < kThreadCount; ++t) { mContexts[t] = eglCreateContext(display, config, t == 0 ? EGL_NO_CONTEXT : mContexts[0], inShareGroupContextAttribs); ASSERT_EGL_SUCCESS(); ASSERT_NE(EGL_NO_CONTEXT, mContexts[t]); surface[t] = eglCreatePbufferSurface(display, config, pbufferAttributes); EXPECT_EGL_SUCCESS(); ASSERT_NE(EGL_NO_SURFACE, surface[t]); } GLTexture textureFromCtx0; constexpr size_t kTextureCount = 10; GLTexture textures[kTextureCount]; // Synchronization tools to ensure the two threads are interleaved as designed by this test. std::mutex mutex; std::condition_variable condVar; enum class Step { Start, Ctx0Current, Ctx1Current, TexturesDone, Finish, Abort, }; Step currentStep = Step::Start; std::thread creatingThread = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); ASSERT_EGL_TRUE(eglMakeCurrent(display, surface[0], surface[0], mContexts[0])); ASSERT_EGL_SUCCESS(); threadSynchronization.nextStep(Step::Ctx0Current); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Ctx1Current)); // Create the shared textures that will be accessed by the other context glBindTexture(GL_TEXTURE_2D, textureFromCtx0); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); ASSERT_GL_TRUE(glIsTexture(textureFromCtx0)); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::red); glFinish(); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::blue); // Do not glFinish // We set 6 to be the threshold to flush texture updates. // We create redundant textures here to ensure that we trigger that threshold. for (size_t i = 0; i < kTextureCount; i++) { glBindTexture(GL_TEXTURE_2D, textures[i]); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); ASSERT_GL_TRUE(glIsTexture(textures[i])); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::blue); } ASSERT_GL_NO_ERROR(); threadSynchronization.nextStep(Step::TexturesDone); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); ASSERT_EGL_TRUE(eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); ASSERT_EGL_SUCCESS(); }); std::thread samplingThread = std::thread([&]() { ThreadSynchronization threadSynchronization(¤tStep, &mutex, &condVar); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Ctx0Current)); ASSERT_EGL_TRUE(eglMakeCurrent(display, surface[1], surface[1], mContexts[1])); ASSERT_EGL_SUCCESS(); threadSynchronization.nextStep(Step::Ctx1Current); ASSERT_TRUE(threadSynchronization.waitForStep(Step::TexturesDone)); ASSERT_GL_TRUE(glIsTexture(textureFromCtx0)); ASSERT_GL_NO_ERROR(); // Draw using ctx0 texture as sampler GLTexture ctx1tex; glBindTexture(GL_TEXTURE_2D, ctx1tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, GLColor::black.data()); ASSERT_GL_NO_ERROR(); GLFramebuffer fbo; glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ctx1tex, 0); ASSERT_GL_NO_ERROR(); GLuint sampler; glGenSamplers(1, &sampler); ASSERT_GL_NO_ERROR(); ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); glUseProgram(program); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureFromCtx0); glBindSampler(0, sampler); glUniform1i(glGetUniformLocation(program, essl1_shaders::PositionAttrib()), 0); ASSERT_GL_NO_ERROR(); drawQuad(program, essl1_shaders::PositionAttrib(), 0.5); ASSERT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue); threadSynchronization.nextStep(Step::Finish); ASSERT_EGL_TRUE(eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); ASSERT_EGL_SUCCESS(); }); creatingThread.join(); samplingThread.join(); ASSERT_NE(currentStep, Step::Abort); ASSERT_EGL_SUCCESS(); for (size_t t = 0; t < kThreadCount; ++t) { ASSERT_EGL_TRUE(eglDestroySurface(display, surface[t])); ASSERT_EGL_SUCCESS(); } } // Tests that creating a context and immediately destroying it works when no surface has been // created. TEST_P(EGLContextSharingTestNoFixture, ImmediateContextDestroyAfterCreation) { EGLAttrib dispattrs[3] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE}; mDisplay = eglGetPlatformDisplay(EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast(EGL_DEFAULT_DISPLAY), dispattrs); EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY); EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr)); EGLConfig config = EGL_NO_CONFIG_KHR; EXPECT_TRUE(chooseConfig(&config)); // Create a context and immediately destroy it. Note that no window surface should be created // for this test. Regression test for platforms that expose multiple queue families in Vulkan, // and ANGLE defers creation of the device until a surface is created. In this case, the // context is being destroyed before a queue is ever created. EXPECT_TRUE(createContext(config, &mContexts[0])); EXPECT_TRUE(SafeDestroyContext(mDisplay, mContexts[0])); ASSERT_EGL_SUCCESS(); } } // anonymous namespace GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLContextSharingTest); ANGLE_INSTANTIATE_TEST(EGLContextSharingTest, ES2_D3D9(), ES2_D3D11(), ES3_D3D11(), ES2_METAL(), ES3_METAL(), ES2_OPENGL(), ES3_OPENGL(), ES2_VULKAN(), ES3_VULKAN()); ANGLE_INSTANTIATE_TEST(EGLContextSharingTestNoFixture, WithNoFixture(ES2_METAL()), WithNoFixture(ES3_METAL()), WithNoFixture(ES2_OPENGLES()), WithNoFixture(ES3_OPENGLES()), WithNoFixture(ES2_OPENGL()), WithNoFixture(ES3_OPENGL()), WithNoFixture(ES2_VULKAN()), WithNoFixture(ES3_VULKAN())); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLContextSharingTestNoSyncTextureUploads); ANGLE_INSTANTIATE_TEST(EGLContextSharingTestNoSyncTextureUploads, ES2_VULKAN().enable(Feature::ForceSubmitImmutableTextureUpdates), ES3_VULKAN().enable(Feature::ForceSubmitImmutableTextureUpdates));