// // 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 "EGLMultiThreadSteps.h" #include "test_utils/ANGLETest.h" #include "test_utils/angle_test_configs.h" #include "test_utils/gl_raii.h" #include "util/EGLWindow.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]; GLuint mTexture; }; // 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}; // 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 (!IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")) { // 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]); // 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[1] = eglCreateContext(display, config, mContexts[1], notInShareGroupContextAttribs); ASSERT_EGL_ERROR(EGL_BAD_ATTRIBUTE); ASSERT_TRUE(mContexts[1] == EGL_NO_CONTEXT); } // Tests the sharing of textures using EGL_ANGLE_display_texture_share_group TEST_P(EGLContextSharingTest, DisplayShareGroupObjectSharing) { EGLDisplay display = getEGLWindow()->getDisplay(); if (!IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")) { std::cout << "Test skipped because EGL_ANGLE_display_texture_share_group is not present." << std::endl; return; } 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(); if (!IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")) { std::cout << "Test skipped because EGL_ANGLE_display_texture_share_group is not present." << std::endl; return; } 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 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]); 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]); } } } // anonymous namespace GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLContextSharingTest); ANGLE_INSTANTIATE_TEST(EGLContextSharingTest, ES2_D3D9(), ES2_D3D11(), ES3_D3D11(), ES2_METAL(), ES2_OPENGL(), ES3_OPENGL(), ES2_VULKAN(), ES3_VULKAN());