// // Copyright 2018 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. // // MulithreadingTest.cpp : Tests of multithreaded rendering #include "platform/FeaturesVk.h" #include "test_utils/ANGLETest.h" #include "test_utils/MultiThreadSteps.h" #include "test_utils/gl_raii.h" #include "util/EGLWindow.h" #include "util/test_utils.h" #include #include #include namespace angle { class MultithreadingTest : public ANGLETest { public: static constexpr uint32_t kSize = 512; protected: MultithreadingTest() { setWindowWidth(kSize); setWindowHeight(kSize); setConfigRedBits(8); setConfigGreenBits(8); setConfigBlueBits(8); setConfigAlphaBits(8); } bool hasFenceSyncExtension() const { return IsEGLDisplayExtensionEnabled(getEGLWindow()->getDisplay(), "EGL_KHR_fence_sync"); } bool hasGLSyncExtension() const { return IsGLExtensionEnabled("GL_OES_EGL_sync"); } EGLContext createMultithreadedContext(EGLWindow *window, EGLContext shareCtx) { EGLint attribs[] = {EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE, mVirtualizationGroup++, EGL_NONE}; if (!IsEGLDisplayExtensionEnabled(getEGLWindow()->getDisplay(), "EGL_ANGLE_context_virtualization")) { attribs[0] = EGL_NONE; } return window->createContext(shareCtx, attribs); } void runMultithreadedGLTest( std::function testBody, size_t threadCount) { std::mutex mutex; EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLConfig config = window->getConfig(); constexpr EGLint kPBufferSize = 256; std::vector threads(threadCount); for (size_t threadIdx = 0; threadIdx < threadCount; threadIdx++) { threads[threadIdx] = std::thread([&, threadIdx]() { EGLSurface surface = EGL_NO_SURFACE; EGLContext ctx = EGL_NO_CONTEXT; { std::lock_guard lock(mutex); // Initialize the pbuffer and context EGLint pbufferAttributes[] = { EGL_WIDTH, kPBufferSize, EGL_HEIGHT, kPBufferSize, EGL_NONE, EGL_NONE, }; surface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); EXPECT_EGL_SUCCESS(); ctx = createMultithreadedContext(window, EGL_NO_CONTEXT); EXPECT_NE(EGL_NO_CONTEXT, ctx); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); EXPECT_EGL_SUCCESS(); } testBody(surface, threadIdx); { std::lock_guard lock(mutex); // Clean up EXPECT_EGL_TRUE( eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EGL_SUCCESS(); eglDestroySurface(dpy, surface); eglDestroyContext(dpy, ctx); } }); } for (std::thread &thread : threads) { thread.join(); } } std::atomic mVirtualizationGroup; }; class MultithreadingTestES3 : public MultithreadingTest { public: void textureThreadFunction(bool useDraw); void mainThreadDraw(bool useDraw); protected: MultithreadingTestES3() : mTexture2D(0), mExitThread(false), mMainThreadSyncObj(NULL), mSecondThreadSyncObj(NULL) { setWindowWidth(kSize); setWindowHeight(kSize); setConfigRedBits(8); setConfigGreenBits(8); setConfigBlueBits(8); setConfigAlphaBits(8); } GLuint create2DTexture() { GLuint texture2D; glGenTextures(1, &texture2D); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture2D); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); EXPECT_GL_NO_ERROR(); return texture2D; } void testSetUp() override { mTexture2D = create2DTexture(); } void testTearDown() override { if (mTexture2D) { glDeleteTextures(1, &mTexture2D); } } std::mutex mutex; GLuint mTexture2D; std::atomic mExitThread; std::atomic mDrawGreen; // Toggle drawing green or red std::atomic mMainThreadSyncObj; std::atomic mSecondThreadSyncObj; }; // Test that it's possible to make one context current on different threads TEST_P(MultithreadingTest, MakeCurrentSingleContext) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); std::mutex mutex; EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLContext ctx = window->getContext(); EGLSurface surface = window->getSurface(); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EGL_SUCCESS(); constexpr size_t kThreadCount = 16; std::array threads; for (std::thread &thread : threads) { thread = std::thread([&]() { std::lock_guard lock(mutex); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); EXPECT_EGL_SUCCESS(); EXPECT_EGL_TRUE(eglSwapBuffers(dpy, surface)); EXPECT_EGL_SUCCESS(); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EGL_SUCCESS(); }); } for (std::thread &thread : threads) { thread.join(); } EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); EXPECT_EGL_SUCCESS(); } // Test that multiple threads can clear and readback pixels successfully at the same time TEST_P(MultithreadingTest, MultiContextClear) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); auto testBody = [](EGLSurface surface, size_t thread) { constexpr size_t kIterationsPerThread = 32; for (size_t iteration = 0; iteration < kIterationsPerThread; iteration++) { // Base the clear color on the thread and iteration indexes so every clear color is // unique const GLColor color(static_cast(thread % 255), static_cast(iteration % 255), 0, 255); const angle::Vector4 floatColor = color.toNormalizedVector(); glClearColor(floatColor[0], floatColor[1], floatColor[2], floatColor[3]); EXPECT_GL_NO_ERROR(); glClear(GL_COLOR_BUFFER_BIT); EXPECT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, color); } }; runMultithreadedGLTest(testBody, 72); } // Verify that threads can interleave eglDestroyContext and draw calls without // any crashes. TEST_P(MultithreadingTest, MultiContextDeleteDraw) { // Skip this test on non-D3D11 backends, as it has the potential to time-out // and this test was originally intended to catch a crash on the D3D11 backend. ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); ANGLE_SKIP_TEST_IF(!IsD3D11()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLConfig config = window->getConfig(); std::thread t1 = std::thread([&]() { // 5000 is chosen here as it reliably reproduces the former crash. for (int i = 0; i < 5000; i++) { EGLContext ctx1 = createMultithreadedContext(window, EGL_NO_CONTEXT); EGLContext ctx2 = createMultithreadedContext(window, EGL_NO_CONTEXT); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx2)); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx1)); EXPECT_EGL_TRUE(eglDestroyContext(dpy, ctx2)); EXPECT_EGL_TRUE(eglDestroyContext(dpy, ctx1)); } }); std::thread t2 = std::thread([&]() { EGLint pbufferAttributes[] = { EGL_WIDTH, 256, EGL_HEIGHT, 256, EGL_NONE, EGL_NONE, }; EGLSurface surface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); EXPECT_EGL_SUCCESS(); auto ctx = createMultithreadedContext(window, EGL_NO_CONTEXT); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); constexpr size_t kIterationsPerThread = 512; constexpr size_t kDrawsPerIteration = 512; ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); glUseProgram(program); GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); auto quadVertices = GetQuadVertices(); GLBuffer vertexBuffer; glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * 6, quadVertices.data(), GL_STATIC_DRAW); GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib()); glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); for (size_t iteration = 0; iteration < kIterationsPerThread; iteration++) { const GLColor color(static_cast(15151 % 255), static_cast(iteration % 255), 0, 255); const angle::Vector4 floatColor = color.toNormalizedVector(); glUniform4fv(colorLocation, 1, floatColor.data()); for (size_t draw = 0; draw < kDrawsPerIteration; draw++) { EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); glDrawArrays(GL_TRIANGLES, 0, 6); } } }); t1.join(); t2.join(); } // Test that multiple threads can draw and readback pixels successfully at the same time TEST_P(MultithreadingTest, MultiContextDraw) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); ANGLE_SKIP_TEST_IF(isSwiftshader()); auto testBody = [](EGLSurface surface, size_t thread) { constexpr size_t kIterationsPerThread = 32; constexpr size_t kDrawsPerIteration = 500; ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); glUseProgram(program); GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); auto quadVertices = GetQuadVertices(); GLBuffer vertexBuffer; glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * 6, quadVertices.data(), GL_STATIC_DRAW); GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib()); glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); for (size_t iteration = 0; iteration < kIterationsPerThread; iteration++) { // Base the clear color on the thread and iteration indexes so every clear color is // unique const GLColor color(static_cast(thread % 255), static_cast(iteration % 255), 0, 255); const angle::Vector4 floatColor = color.toNormalizedVector(); glUniform4fv(colorLocation, 1, floatColor.data()); for (size_t draw = 0; draw < kDrawsPerIteration; draw++) { glDrawArrays(GL_TRIANGLES, 0, 6); } EXPECT_PIXEL_COLOR_EQ(0, 0, color); } }; runMultithreadedGLTest(testBody, 4); } // Test that multiple threads can draw and readback pixels successfully at the same time while EGL // images are bound between them TEST_P(MultithreadingTest, MultiContextDrawWithEGLImage) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); ANGLE_SKIP_TEST_IF(isSwiftshader()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); ANGLE_SKIP_TEST_IF(IsGLExtensionEnabled("GL_OES_EGL_image")); ANGLE_SKIP_TEST_IF(IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_gl_texture_2D_image")); // Create a source 2D texture GLTexture sourceTexture; glBindTexture(GL_TEXTURE_2D, sourceTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::red); // Disable mipmapping glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); ASSERT_GL_NO_ERROR(); // Create an image from the source texture constexpr EGLint attribs[] = { EGL_IMAGE_PRESERVED, EGL_TRUE, EGL_NONE, }; EGLImageKHR image = eglCreateImageKHR( dpy, window->getContext(), EGL_GL_TEXTURE_2D_KHR, reinterpret_cast(static_cast(sourceTexture.get())), attribs); auto testBody = [image](EGLSurface surface, size_t thread) { constexpr size_t kIterationsPerThread = 32; constexpr size_t kDrawsPerIteration = 500; // Just bind the EGL image to this context and leave it bound while drawing GLTexture destTexture; glBindTexture(GL_TEXTURE_EXTERNAL_OES, destTexture); glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, image); ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); glUseProgram(program); GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); auto quadVertices = GetQuadVertices(); GLBuffer vertexBuffer; glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * 6, quadVertices.data(), GL_STATIC_DRAW); GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib()); glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); for (size_t iteration = 0; iteration < kIterationsPerThread; iteration++) { // Base the clear color on the thread and iteration indexes so every clear color is // unique const GLColor color(static_cast(thread % 255), static_cast(iteration % 255), 0, 255); const angle::Vector4 floatColor = color.toNormalizedVector(); glUniform4fv(colorLocation, 1, floatColor.data()); for (size_t draw = 0; draw < kDrawsPerIteration; draw++) { glDrawArrays(GL_TRIANGLES, 0, 6); } EXPECT_PIXEL_COLOR_EQ(0, 0, color); } }; runMultithreadedGLTest(testBody, 4); eglDestroyImage(dpy, image); } // Test that multiple threads can draw and read back pixels correctly. // Using eglSwapBuffers stresses race conditions around use of QueueSerials. TEST_P(MultithreadingTest, MultiContextDrawWithSwapBuffers) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); // http://anglebug.com/5099 ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); auto testBody = [dpy](EGLSurface surface, size_t thread) { constexpr size_t kIterationsPerThread = 100; constexpr size_t kDrawsPerIteration = 10; ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); glUseProgram(program); GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); auto quadVertices = GetQuadVertices(); GLBuffer vertexBuffer; glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * 6, quadVertices.data(), GL_STATIC_DRAW); GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib()); glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); for (size_t iteration = 0; iteration < kIterationsPerThread; iteration++) { // Base the clear color on the thread and iteration indexes so every clear color is // unique const GLColor color(static_cast(thread % 255), static_cast(iteration % 255), 0, 255); const angle::Vector4 floatColor = color.toNormalizedVector(); glUniform4fv(colorLocation, 1, floatColor.data()); for (size_t draw = 0; draw < kDrawsPerIteration; draw++) { glDrawArrays(GL_TRIANGLES, 0, 6); } EXPECT_EGL_TRUE(eglSwapBuffers(dpy, surface)); EXPECT_EGL_SUCCESS(); EXPECT_PIXEL_COLOR_EQ(0, 0, color); } }; runMultithreadedGLTest(testBody, 32); } // Test that ANGLE handles multiple threads creating and destroying resources (vertex buffer in this // case). Disable defer_flush_until_endrenderpass so that glFlush will issue work to GPU in order to // maximize the chance we resources can be destroyed at the wrong time. TEST_P(MultithreadingTest, MultiContextCreateAndDeleteResources) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); auto testBody = [dpy](EGLSurface surface, size_t thread) { constexpr size_t kIterationsPerThread = 32; constexpr size_t kDrawsPerIteration = 1; ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); glUseProgram(program); GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); auto quadVertices = GetQuadVertices(); for (size_t iteration = 0; iteration < kIterationsPerThread; iteration++) { GLBuffer vertexBuffer; glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * 6, quadVertices.data(), GL_STATIC_DRAW); GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib()); glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); // Base the clear color on the thread and iteration indexes so every clear color is // unique const GLColor color(static_cast(thread % 255), static_cast(iteration % 255), 0, 255); const angle::Vector4 floatColor = color.toNormalizedVector(); glUniform4fv(colorLocation, 1, floatColor.data()); for (size_t draw = 0; draw < kDrawsPerIteration; draw++) { glDrawArrays(GL_TRIANGLES, 0, 6); } EXPECT_EGL_TRUE(eglSwapBuffers(dpy, surface)); EXPECT_EGL_SUCCESS(); EXPECT_PIXEL_COLOR_EQ(0, 0, color); } glFinish(); }; runMultithreadedGLTest(testBody, 32); } TEST_P(MultithreadingTest, MultiCreateContext) { // Supported by CGL, GLX, and WGL (https://anglebug.com/4725) // Not supported on Ozone (https://crbug.com/1103009) ANGLE_SKIP_TEST_IF(!(IsWindows() || IsLinux() || IsOSX()) || IsOzone()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLContext ctx = window->getContext(); EGLSurface surface = window->getSurface(); // Un-makeCurrent the test window's context EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EGL_SUCCESS(); constexpr size_t kThreadCount = 16; std::atomic barrier(0); std::vector threads(kThreadCount); std::vector contexts(kThreadCount); for (size_t threadIdx = 0; threadIdx < kThreadCount; threadIdx++) { threads[threadIdx] = std::thread([&, threadIdx]() { contexts[threadIdx] = EGL_NO_CONTEXT; { contexts[threadIdx] = createMultithreadedContext(window, EGL_NO_CONTEXT); EXPECT_NE(EGL_NO_CONTEXT, contexts[threadIdx]); barrier++; } while (barrier < kThreadCount) { } { EXPECT_TRUE(eglDestroyContext(dpy, contexts[threadIdx])); } }); } for (std::thread &thread : threads) { thread.join(); } // Re-make current the test window's context for teardown. EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); EXPECT_EGL_SUCCESS(); } void MultithreadingTestES3::textureThreadFunction(bool useDraw) { EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLConfig config = window->getConfig(); EGLSurface surface = EGL_NO_SURFACE; EGLContext ctx = EGL_NO_CONTEXT; // Initialize the pbuffer and context EGLint pbufferAttributes[] = { EGL_WIDTH, kSize, EGL_HEIGHT, kSize, EGL_NONE, EGL_NONE, }; surface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); EXPECT_EGL_SUCCESS(); EXPECT_NE(EGL_NO_SURFACE, surface); ctx = createMultithreadedContext(window, window->getContext()); EXPECT_NE(EGL_NO_CONTEXT, ctx); EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); EXPECT_EGL_SUCCESS(); std::vector greenColor(kSize * kSize, GLColor::green); std::vector redColor(kSize * kSize, GLColor::red); ANGLE_GL_PROGRAM(greenProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); ANGLE_GL_PROGRAM(redProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); glBindTexture(GL_TEXTURE_2D, mTexture2D); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); ASSERT_GL_NO_ERROR(); GLFramebuffer fbo; glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture2D, 0); ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); mSecondThreadSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); ASSERT_GL_NO_ERROR(); // Force the fence to be created glFlush(); // Draw something while (!mExitThread) { std::lock_guard lock(mutex); if (mMainThreadSyncObj != nullptr) { glWaitSync(mMainThreadSyncObj, 0, GL_TIMEOUT_IGNORED); ASSERT_GL_NO_ERROR(); glDeleteSync(mSecondThreadSyncObj); ASSERT_GL_NO_ERROR(); mMainThreadSyncObj = nullptr; } else { continue; } glBindTexture(GL_TEXTURE_2D, mTexture2D); ASSERT_GL_NO_ERROR(); if (mDrawGreen) { if (useDraw) { glBindFramebuffer(GL_FRAMEBUFFER, fbo); drawQuad(greenProgram.get(), std::string(essl1_shaders::PositionAttrib()), 0.0f); } else { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, greenColor.data()); } ASSERT_GL_NO_ERROR(); } else { if (useDraw) { glBindFramebuffer(GL_FRAMEBUFFER, fbo); drawQuad(redProgram.get(), std::string(essl1_shaders::PositionAttrib()), 0.0f); } else { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, redColor.data()); } ASSERT_GL_NO_ERROR(); } ASSERT_EQ(mSecondThreadSyncObj.load(), nullptr); mSecondThreadSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); ASSERT_GL_NO_ERROR(); // Force the fence to be created glFlush(); mDrawGreen = !mDrawGreen; } // Clean up EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EGL_SUCCESS(); eglDestroySurface(dpy, surface); eglDestroyContext(dpy, ctx); } // Test fence sync with multiple threads drawing void MultithreadingTestES3::mainThreadDraw(bool useDraw) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLContext ctx = window->getContext(); EGLSurface surface = window->getSurface(); // Use odd numbers so we bounce between red and green in the final image constexpr int kNumIterations = 5; constexpr int kNumDraws = 5; mDrawGreen = false; std::thread textureThread(&MultithreadingTestES3::textureThreadFunction, this, true); ANGLE_GL_PROGRAM(texProgram, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); for (int iterations = 0; iterations < kNumIterations; ++iterations) { for (int draws = 0; draws < kNumDraws;) { std::lock_guard lock(mutex); if (mSecondThreadSyncObj != nullptr) { glWaitSync(mSecondThreadSyncObj, 0, GL_TIMEOUT_IGNORED); ASSERT_GL_NO_ERROR(); glDeleteSync(mSecondThreadSyncObj); ASSERT_GL_NO_ERROR(); mSecondThreadSyncObj = nullptr; } else { continue; } glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, mTexture2D); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glUseProgram(texProgram); drawQuad(texProgram.get(), std::string(essl1_shaders::PositionAttrib()), 0.0f); ASSERT_EQ(mMainThreadSyncObj.load(), nullptr); mMainThreadSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); ASSERT_GL_NO_ERROR(); // Force the fence to be created glFlush(); ++draws; } ASSERT_GL_NO_ERROR(); swapBuffers(); } mExitThread = true; textureThread.join(); ASSERT_GL_NO_ERROR(); GLColor color; if (mDrawGreen) { color = GLColor::green; } else { color = GLColor::red; } EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, color); // Re-make current the test window's context for teardown. EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); EXPECT_EGL_SUCCESS(); } // Test that glFenceSync/glWaitSync works correctly with multithreading. // Main thread: Samples from the shared texture to draw to the default FBO. // Secondary (Texture) thread: Draws to the shared texture, which the Main thread samples from. // The overall execution flow is: // Main Thread: // 1. Wait for the mSecondThreadSyncObj fence object to be created. // - This fence object is used by synchronize access to the shared texture by indicating that the // Secondary thread's draws to the texture have all completed and it's now safe to sample from // it. // 2. Once the fence is created, add a glWaitSync(mSecondThreadSyncObj) to the command stream and // then delete it. // 3. Draw, sampling from the shared texture. // 4. Create a new mMainThreadSyncObj. // - This fence object is used to synchronize access to the shared texture by indicating that the // Main thread's draws are no longer sampling from the texture, so it's now safe for the // Secondary thread to draw to it again with a new color. // Secondary (Texture) Thread: // 1. Wait for the mMainThreadSyncObj fence object to be created. // 2. Once the fence is created, add a glWaitSync(mMainThreadSyncObj) to the command stream and then // delete it. // 3. Draw/Fill the texture. // 4. Create a new mSecondThreadSyncObj. // // These threads loop for the specified number of iterations, drawing/sampling the shared texture // with the necessary glFlush()s and occasional eglSwapBuffers() to mimic a real multithreaded GLES // application. TEST_P(MultithreadingTestES3, MultithreadFenceDraw) { // http://anglebug.com/5418 ANGLE_SKIP_TEST_IF(IsLinux() && IsVulkan() && (IsIntel() || isSwiftshader())); // Have the secondary thread use glDrawArrays() mainThreadDraw(true); } // Same as MultithreadFenceDraw, but with the secondary thread using glTexImage2D rather than // glDrawArrays. TEST_P(MultithreadingTestES3, MultithreadFenceTexImage) { // http://anglebug.com/5418 ANGLE_SKIP_TEST_IF(IsLinux() && IsIntel() && IsVulkan()); // http://anglebug.com/5439 ANGLE_SKIP_TEST_IF(IsLinux() && isSwiftshader()); // Have the secondary thread use glTexImage2D() mainThreadDraw(false); } // Test that waiting on a sync object that hasn't been flushed and without a current context returns // TIMEOUT_EXPIRED or CONDITION_SATISFIED, but doesn't generate an error or crash. TEST_P(MultithreadingTest, NoFlushNoContextReturnsTimeout) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension() || !hasGLSyncExtension()); std::mutex mutex; EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); glClearColor(1.0f, 0.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); EGLSyncKHR sync = eglCreateSyncKHR(dpy, EGL_SYNC_FENCE_KHR, nullptr); EXPECT_NE(sync, EGL_NO_SYNC_KHR); std::thread thread = std::thread([&]() { std::lock_guard lock(mutex); // Make sure there is no active context on this thread. EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EGL_SUCCESS(); // Don't wait forever to make sure the test terminates constexpr GLuint64 kTimeout = 1'000'000'000; // 1 second int result = eglClientWaitSyncKHR(dpy, sync, 0, kTimeout); // We typically expect to get back TIMEOUT_EXPIRED since the sync object was never flushed. // However, the OpenGL ES backend returns CONDITION_SATISFIED, which is also a passing // result. ASSERT_TRUE(result == EGL_TIMEOUT_EXPIRED_KHR || result == EGL_CONDITION_SATISFIED_KHR); }); thread.join(); EXPECT_EGL_TRUE(eglDestroySyncKHR(dpy, sync)); } // Test that waiting on sync object that hasn't been flushed yet, but is later flushed by another // thread, correctly returns when the fence is signalled without a timeout. TEST_P(MultithreadingTest, CreateFenceThreadAClientWaitSyncThreadBDelayedFlush) { ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension() || !hasGLSyncExtension()); // TODO: Fails on Pixel 4 with OpenGLES backend. ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES()); EGLWindow *window = getEGLWindow(); EGLDisplay dpy = window->getDisplay(); EGLConfig config = window->getConfig(); EGLSurface surface; EGLContext context; constexpr EGLint kPBufferSize = 256; // Initialize the pbuffer and context EGLint pbufferAttributes[] = { EGL_WIDTH, kPBufferSize, EGL_HEIGHT, kPBufferSize, EGL_NONE, EGL_NONE, }; // Create 2 surfaces, one for each thread surface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); EXPECT_EGL_SUCCESS(); // Create 2 shared contexts, one for each thread context = window->createContext(EGL_NO_CONTEXT, nullptr); EXPECT_NE(EGL_NO_CONTEXT, context); // Sync object EGLSyncKHR sync = EGL_NO_SYNC_KHR; // 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, Thread1CreateFence, Thread0ClientWaitSync, Thread1Flush, 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(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); // Do work. glClearColor(1.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); // Wait for thread 1 to clear. threadSynchronization.nextStep(Step::Thread0Clear); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1CreateFence)); // Wait on the sync object, but do *not* flush it, since the other thread will flush. constexpr GLuint64 kTimeout = 2'000'000'000; // 1 second threadSynchronization.nextStep(Step::Thread0ClientWaitSync); ASSERT_EQ(EGL_CONDITION_SATISFIED_KHR, eglClientWaitSyncKHR(dpy, sync, 0, kTimeout)); 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(dpy, surface, surface, context)); // Do work. glClearColor(0.0, 1.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); sync = eglCreateSyncKHR(dpy, EGL_SYNC_FENCE_KHR, nullptr); EXPECT_NE(sync, EGL_NO_SYNC_KHR); // Wait for the thread 0 to eglClientWaitSyncKHR(). threadSynchronization.nextStep(Step::Thread1CreateFence); ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0ClientWaitSync)); // Wait a little to give thread 1 time to wait on the sync object before flushing it. angle::Sleep(500); glFlush(); // Clean up EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); threadSynchronization.nextStep(Step::Finish); }); thread0.join(); thread1.join(); // Clean up if (surface != EGL_NO_SURFACE) { eglDestroySurface(dpy, surface); } if (context != EGL_NO_CONTEXT) { eglDestroyContext(dpy, context); } ASSERT_NE(currentStep, Step::Abort); } // TODO(geofflang): Test sharing a program between multiple shared contexts on multiple threads ANGLE_INSTANTIATE_TEST(MultithreadingTest, ES2_OPENGL(), ES3_OPENGL(), ES2_OPENGLES(), ES3_OPENGLES(), ES3_VULKAN(), ES3_VULKAN_SWIFTSHADER(), ES2_D3D11(), ES3_D3D11()); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MultithreadingTestES3); ANGLE_INSTANTIATE_TEST(MultithreadingTestES3, ES3_OPENGL(), ES3_OPENGLES(), ES3_VULKAN(), ES3_VULKAN_SWIFTSHADER(), ES3_D3D11()); } // namespace angle