| // |
| // 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 "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 <atomic> |
| #include <mutex> |
| #include <thread> |
| |
| 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 hasWaitSyncExtension() const |
| { |
| return hasFenceSyncExtension() && |
| IsEGLDisplayExtensionEnabled(getEGLWindow()->getDisplay(), "EGL_KHR_wait_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<void(EGLSurface surface, size_t threadIndex)> testBody, |
| size_t threadCount) |
| { |
| std::mutex mutex; |
| |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLConfig config = window->getConfig(); |
| |
| constexpr EGLint kPBufferSize = 256; |
| |
| std::vector<std::thread> 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<decltype(mutex)> 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<decltype(mutex)> 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<EGLint> 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); |
| } |
| } |
| |
| enum class FenceTest |
| { |
| ClientWait, |
| ServerWait, |
| GetStatus, |
| }; |
| enum class FlushMethod |
| { |
| Flush, |
| Finish, |
| }; |
| void testFenceWithOpenRenderPass(FenceTest test, FlushMethod flushMethod); |
| |
| enum class DrawOrder |
| { |
| Before, |
| After, |
| }; |
| void testFramebufferFetch(DrawOrder drawOrder); |
| |
| std::mutex mMutex; |
| GLuint mTexture2D; |
| std::atomic<bool> mExitThread; |
| std::atomic<bool> mDrawGreen; // Toggle drawing green or red |
| std::atomic<GLsync> mMainThreadSyncObj; |
| std::atomic<GLsync> 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<std::thread, kThreadCount> threads; |
| for (std::thread &thread : threads) |
| { |
| thread = std::thread([&]() { |
| std::lock_guard<decltype(mutex)> 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<GLubyte>(thread % 255), |
| static_cast<GLubyte>(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<GLubyte>(15151 % 255), |
| static_cast<GLubyte>(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<GLubyte>(thread % 255), |
| static_cast<GLubyte>(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 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/42263666 |
| 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<GLubyte>(thread % 255), |
| static_cast<GLubyte>(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). |
| 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<GLubyte>(thread % 255), |
| static_cast<GLubyte>(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/42263324) |
| // Not supported on Ozone (https://crbug.com/1103009) |
| ANGLE_SKIP_TEST_IF(!(IsWindows() || IsLinux() || IsMac()) || 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<uint32_t> barrier(0); |
| std::vector<std::thread> threads(kThreadCount); |
| std::vector<EGLContext> 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(); |
| } |
| |
| // Create multiple shared context and draw with shared vertex buffer simutanously |
| TEST_P(MultithreadingTest, CreateMultiSharedContextAndDraw) |
| { |
| // Supported by CGL, GLX, and WGL (https://anglebug.com/42263324) |
| // Not supported on Ozone (https://crbug.com/1103009) |
| ANGLE_SKIP_TEST_IF(!(IsWindows() || IsLinux() || IsMac()) || IsOzone()); |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLConfig config = window->getConfig(); |
| constexpr EGLint kPBufferSize = 256; |
| |
| // Initialize the pbuffer and context |
| EGLint pbufferAttributes[] = { |
| EGL_WIDTH, kPBufferSize, EGL_HEIGHT, kPBufferSize, EGL_NONE, EGL_NONE, |
| }; |
| EGLSurface sharedSurface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); |
| EXPECT_EGL_SUCCESS(); |
| EGLContext sharedCtx = createMultithreadedContext(window, EGL_NO_CONTEXT); |
| EXPECT_NE(EGL_NO_CONTEXT, sharedCtx); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, sharedSurface, sharedSurface, sharedCtx)); |
| EXPECT_EGL_SUCCESS(); |
| |
| // Create a shared vertextBuffer |
| auto quadVertices = GetQuadVertices(); |
| GLBuffer sharedVertexBuffer; |
| glBindBuffer(GL_ARRAY_BUFFER, sharedVertexBuffer); |
| glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * 6, quadVertices.data(), GL_STATIC_DRAW); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Now draw with the buffer and verify |
| { |
| ANGLE_GL_PROGRAM(sharedProgram, essl1_shaders::vs::Simple(), |
| essl1_shaders::fs::UniformColor()); |
| glUseProgram(sharedProgram); |
| GLint colorLocation = glGetUniformLocation(sharedProgram, essl1_shaders::ColorUniform()); |
| GLint positionLocation = |
| glGetAttribLocation(sharedProgram, essl1_shaders::PositionAttrib()); |
| glEnableVertexAttribArray(positionLocation); |
| glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); |
| const GLColor color(0, 0, 0, 255); |
| const angle::Vector4 floatColor = color.toNormalizedVector(); |
| glUniform4fv(colorLocation, 1, floatColor.data()); |
| glDrawArrays(GL_TRIANGLES, 0, 6); |
| EXPECT_PIXEL_COLOR_EQ(0, 0, color); |
| } |
| |
| // Create shared context in their own threads and draw with the shared vertex buffer at the same |
| // time. |
| size_t threadCount = 16; |
| constexpr size_t kIterationsPerThread = 3; |
| constexpr size_t kDrawsPerIteration = 50; |
| std::vector<std::thread> threads(threadCount); |
| std::atomic<uint32_t> numOfContextsCreated(0); |
| std::mutex mutex; |
| 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<decltype(mutex)> lock(mutex); |
| // Initialize the pbuffer and context |
| surface = eglCreatePbufferSurface(dpy, config, pbufferAttributes); |
| EXPECT_EGL_SUCCESS(); |
| ctx = createMultithreadedContext(window, /*EGL_NO_CONTEXT*/ sharedCtx); |
| EXPECT_NE(EGL_NO_CONTEXT, ctx); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, ctx)); |
| EXPECT_EGL_SUCCESS(); |
| numOfContextsCreated++; |
| } |
| |
| // Wait for all contexts created. |
| while (numOfContextsCreated < threadCount) |
| { |
| } |
| |
| // Now draw with shared vertex buffer |
| { |
| ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), |
| essl1_shaders::fs::UniformColor()); |
| glUseProgram(program); |
| |
| GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); |
| GLint positionLocation = |
| glGetAttribLocation(program, essl1_shaders::PositionAttrib()); |
| |
| // Use sharedVertexBuffer |
| glBindBuffer(GL_ARRAY_BUFFER, sharedVertexBuffer); |
| 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<GLubyte>(threadIdx % 255), |
| static_cast<GLubyte>(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); |
| } |
| } |
| |
| // tear down shared context |
| { |
| std::lock_guard<decltype(mutex)> lock(mutex); |
| 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(); |
| } |
| |
| eglDestroySurface(dpy, sharedSurface); |
| eglDestroyContext(dpy, sharedCtx); |
| |
| // Re-make current the test window's context for teardown. |
| EXPECT_EGL_TRUE( |
| eglMakeCurrent(dpy, window->getSurface(), window->getSurface(), window->getContext())); |
| EXPECT_EGL_SUCCESS(); |
| } |
| |
| // Producer/Consumer test using EGLImages and EGLSyncs |
| TEST_P(MultithreadingTest, EGLImageProduceConsume) |
| { |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLContext rootCtx = window->getContext(); |
| |
| ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_image")); |
| ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_gl_texture_2D_image")); |
| ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_fence_sync")); |
| |
| struct sharedImage |
| { |
| EGLImage image; |
| EGLSync sync; |
| GLuint rootTexture; |
| }; |
| |
| std::mutex mutex; |
| std::vector<sharedImage> waitingForProduce; |
| std::vector<sharedImage> waitingForConsume; |
| |
| constexpr size_t kNumImages = 10; |
| for (size_t i = 0; i < kNumImages; i++) |
| { |
| GLuint texture; |
| glGenTextures(1, &texture); |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); |
| |
| sharedImage img; |
| img.image = eglCreateImageKHR(dpy, rootCtx, EGL_GL_TEXTURE_2D_KHR, |
| reinterpret_cast<EGLClientBuffer>(texture), nullptr); |
| img.sync = eglCreateSyncKHR(dpy, EGL_SYNC_FENCE_KHR, nullptr); |
| img.rootTexture = texture; |
| |
| waitingForProduce.push_back(std::move(img)); |
| } |
| |
| constexpr size_t kIterations = 10000; |
| |
| std::thread producerThread([&]() { |
| EGLContext ctx = createMultithreadedContext(window, EGL_NO_CONTEXT); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx)); |
| |
| { |
| ANGLE_GL_PROGRAM(drawColor, essl1_shaders::vs::Simple(), |
| essl1_shaders::fs::UniformColor()); |
| glUseProgram(drawColor); |
| GLint colorUniformLocation = |
| glGetUniformLocation(drawColor, angle::essl1_shaders::ColorUniform()); |
| ASSERT_NE(colorUniformLocation, -1); |
| |
| size_t iteration = 0; |
| while (iteration < kIterations) |
| { |
| sharedImage img; |
| { |
| std::lock_guard<decltype(mutex)> lock(mutex); |
| if (waitingForProduce.empty()) |
| { |
| continue; |
| } |
| img = std::move(waitingForProduce.back()); |
| waitingForProduce.pop_back(); |
| } |
| |
| eglWaitSync(dpy, img.sync, 0); |
| EXPECT_EGL_SUCCESS(); |
| |
| eglDestroySync(dpy, img.sync); |
| |
| GLTexture texture; |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, img.image); |
| EXPECT_GL_NO_ERROR(); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, |
| 0); |
| |
| glClearColor(0.0f, 1.0f, 0.0f, 1.0f); |
| glClear(GL_COLOR_BUFFER_BIT); |
| |
| glUniform4f(colorUniformLocation, float(iteration) / kIterations, 0.0f, 0.0f, 1.0f); |
| drawQuad(drawColor, essl1_shaders::PositionAttrib(), 0); |
| |
| glBindTexture(GL_TEXTURE_2D, 0); |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); |
| |
| img.sync = eglCreateSyncKHR(dpy, EGL_SYNC_FENCE_KHR, nullptr); |
| EXPECT_EGL_SUCCESS(); |
| glFlush(); |
| |
| { |
| std::lock_guard<decltype(mutex)> lock(mutex); |
| waitingForConsume.insert(waitingForConsume.begin(), std::move(img)); |
| } |
| |
| iteration++; |
| } |
| } |
| |
| eglDestroyContext(dpy, ctx); |
| }); |
| |
| std::thread consumerThread([&]() { |
| EGLContext ctx = createMultithreadedContext(window, EGL_NO_CONTEXT); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx)); |
| |
| { |
| ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| glUseProgram(drawTexture); |
| GLint textureUniformLocation = |
| glGetUniformLocation(drawTexture, angle::essl1_shaders::Texture2DUniform()); |
| ASSERT_NE(textureUniformLocation, -1); |
| glUniform1i(textureUniformLocation, 0); |
| glActiveTexture(GL_TEXTURE0); |
| |
| GLTexture backbufferTexture; |
| glBindTexture(GL_TEXTURE_2D, backbufferTexture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| nullptr); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, |
| backbufferTexture, 0); |
| |
| size_t iteration = 0; |
| while (iteration < kIterations) |
| { |
| sharedImage img; |
| { |
| std::lock_guard<decltype(mutex)> lock(mutex); |
| if (waitingForConsume.empty()) |
| { |
| continue; |
| } |
| img = std::move(waitingForConsume.back()); |
| waitingForConsume.pop_back(); |
| } |
| |
| eglWaitSync(dpy, img.sync, 0); |
| EXPECT_EGL_SUCCESS(); |
| eglDestroySync(dpy, img.sync); |
| |
| GLTexture texture; |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, img.image); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| |
| drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0); |
| EXPECT_GL_NO_ERROR(); |
| |
| glBindTexture(GL_TEXTURE_2D, 0); |
| |
| img.sync = eglCreateSyncKHR(dpy, EGL_SYNC_FENCE_KHR, nullptr); |
| EXPECT_EGL_SUCCESS(); |
| glFlush(); |
| |
| { |
| std::lock_guard<decltype(mutex)> lock(mutex); |
| waitingForProduce.insert(waitingForProduce.begin(), std::move(img)); |
| } |
| |
| iteration++; |
| } |
| } |
| eglDestroyContext(dpy, ctx); |
| }); |
| |
| producerThread.join(); |
| consumerThread.join(); |
| |
| // Clean up |
| { |
| for (auto &img : waitingForProduce) |
| { |
| eglDestroyImageKHR(dpy, img.image); |
| eglDestroySync(dpy, img.sync); |
| glDeleteTextures(1, &img.rootTexture); |
| } |
| for (auto &img : waitingForConsume) |
| { |
| eglDestroyImageKHR(dpy, img.image); |
| eglDestroySync(dpy, img.sync); |
| glDeleteTextures(1, &img.rootTexture); |
| } |
| } |
| } |
| |
| 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<GLColor> greenColor(kSize * kSize, GLColor::green); |
| std::vector<GLColor> 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(); |
| |
| // Draw something |
| while (!mExitThread) |
| { |
| if (mMainThreadSyncObj == nullptr) |
| { |
| angle::Sleep(0); |
| } |
| |
| std::lock_guard<decltype(mMutex)> lock(mMutex); |
| |
| if (mMainThreadSyncObj != nullptr) |
| { |
| glWaitSync(mMainThreadSyncObj, 0, GL_TIMEOUT_IGNORED); |
| ASSERT_GL_NO_ERROR(); |
| glDeleteSync(mMainThreadSyncObj); |
| ASSERT_GL_NO_ERROR(); |
| mMainThreadSyncObj = nullptr; |
| } |
| else |
| { |
| continue; |
| } |
| |
| mDrawGreen = !mDrawGreen; |
| |
| glBindTexture(GL_TEXTURE_2D, mTexture2D); |
| ASSERT_GL_NO_ERROR(); |
| |
| if (mDrawGreen) |
| { |
| if (useDraw) |
| { |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| drawQuad(greenProgram, 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, 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(); |
| } |
| |
| // 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) |
| { |
| 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) |
| { |
| GLColor expectedDrawColor; |
| |
| for (int draws = 0; draws < kNumDraws;) |
| { |
| if (mSecondThreadSyncObj == nullptr) |
| { |
| angle::Sleep(0); |
| } |
| |
| std::lock_guard<decltype(mMutex)> lock(mMutex); |
| |
| 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); |
| glUseProgram(texProgram); |
| drawQuad(texProgram, essl1_shaders::PositionAttrib(), 0.0f); |
| |
| // mDrawGreen will be changed by the background thread past mMainThreadSyncObj |
| // as it will start drawing the next color to fbo. This shouldn't affect |
| // pixels of the current frame so save the expected color before unblocking the thread |
| expectedDrawColor = mDrawGreen ? GLColor::green : GLColor::red; |
| |
| ASSERT_EQ(mMainThreadSyncObj.load(), nullptr); |
| mMainThreadSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_GL_NO_ERROR(); |
| |
| ++draws; |
| } |
| |
| ASSERT_GL_NO_ERROR(); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, expectedDrawColor); |
| |
| swapBuffers(); |
| } |
| |
| mExitThread = true; |
| textureThread.join(); |
| |
| // 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) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| // 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) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| // 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<decltype(mutex)> 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()); |
| |
| EGLSyncKHR sync = EGL_NO_SYNC_KHR; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0Clear, |
| Thread1CreateFence, |
| Thread0ClientWaitSync, |
| Thread1Flush, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // 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; // 2 seconds |
| threadSynchronization.nextStep(Step::Thread0ClientWaitSync); |
| ASSERT_EQ(EGL_CONDITION_SATISFIED_KHR, eglClientWaitSyncKHR(dpy, sync, 0, kTimeout)); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> 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); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after |
| // that. |
| void MultithreadingTestES3::testFenceWithOpenRenderPass(FenceTest test, FlushMethod flushMethod) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension() || !hasGLSyncExtension()); |
| |
| constexpr uint32_t kWidth = 100; |
| constexpr uint32_t kHeight = 200; |
| |
| GLsync sync = 0; |
| GLuint texture = 0; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0CreateFence, |
| Thread1WaitFence, |
| Thread0Finish, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create a shared texture to test synchronization |
| GLTexture color; |
| texture = color; |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| // Draw to shared texture. |
| ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Issue a fence. A render pass is currently open, so the fence is not actually submitted |
| // in the Vulkan backend. |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_NE(sync, nullptr); |
| |
| // Wait for thread 1 to wait on it. |
| threadSynchronization.nextStep(Step::Thread0CreateFence); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1WaitFence)); |
| |
| // Wait a little to give thread 1 time to wait on the sync object before flushing it. |
| angle::Sleep(500); |
| switch (flushMethod) |
| { |
| case FlushMethod::Flush: |
| glFlush(); |
| break; |
| case FlushMethod::Finish: |
| glFinish(); |
| break; |
| } |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| threadSynchronization.nextStep(Step::Thread0Finish); |
| threadSynchronization.waitForStep(Step::Finish); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 0 to create the fence object. |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0CreateFence)); |
| |
| // Test access to the fence object |
| threadSynchronization.nextStep(Step::Thread1WaitFence); |
| |
| constexpr GLuint64 kTimeout = 2'000'000'000; // 2 seconds |
| GLenum result = GL_CONDITION_SATISFIED; |
| switch (test) |
| { |
| case FenceTest::ClientWait: |
| result = glClientWaitSync(sync, 0, kTimeout); |
| break; |
| case FenceTest::ServerWait: |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| break; |
| case FenceTest::GetStatus: |
| { |
| GLint value; |
| glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value); |
| if (value != GL_SIGNALED) |
| { |
| result = glClientWaitSync(sync, 0, kTimeout); |
| } |
| break; |
| } |
| } |
| ASSERT_TRUE(result == GL_CONDITION_SATISFIED || result == GL_ALREADY_SIGNALED); |
| |
| // Verify the shared texture is drawn to. |
| glBindTexture(GL_TEXTURE_2D, texture); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::red); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Finish)); |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after |
| // that. |
| TEST_P(MultithreadingTestES3, ThreadBClientWaitBeforeThreadASyncFlush) |
| { |
| testFenceWithOpenRenderPass(FenceTest::ClientWait, FlushMethod::Flush); |
| } |
| |
| // Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after |
| // that. |
| TEST_P(MultithreadingTestES3, ThreadBServerWaitBeforeThreadASyncFlush) |
| { |
| testFenceWithOpenRenderPass(FenceTest::ServerWait, FlushMethod::Flush); |
| } |
| |
| // Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after |
| // that. |
| TEST_P(MultithreadingTestES3, ThreadBGetStatusBeforeThreadASyncFlush) |
| { |
| testFenceWithOpenRenderPass(FenceTest::GetStatus, FlushMethod::Flush); |
| } |
| |
| // Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after |
| // that. |
| TEST_P(MultithreadingTestES3, ThreadBClientWaitBeforeThreadASyncFinish) |
| { |
| testFenceWithOpenRenderPass(FenceTest::ClientWait, FlushMethod::Finish); |
| } |
| |
| // Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after |
| // that. |
| TEST_P(MultithreadingTestES3, ThreadBServerWaitBeforeThreadASyncFinish) |
| { |
| testFenceWithOpenRenderPass(FenceTest::ServerWait, FlushMethod::Finish); |
| } |
| |
| // Test that thread B can wait on thread A's sync before thread A flushes it, and wakes up after |
| // that. |
| TEST_P(MultithreadingTestES3, ThreadBGetStatusBeforeThreadASyncFinish) |
| { |
| testFenceWithOpenRenderPass(FenceTest::GetStatus, FlushMethod::Finish); |
| } |
| |
| // Test the following scenario: |
| // |
| // - Thread A opens a render pass, and flushes it. In the Vulkan backend, this may make the flush |
| // deferred. |
| // - Thread B opens a render pass and creates a fence. In the Vulkan backend, this also defers the |
| // flush. |
| // - Thread C waits on fence |
| // |
| // In the Vulkan backend, submission of the fence is implied by thread C's wait, and thread A may |
| // also be flushed as collateral. If the fence's serial is updated based on thread A's submission, |
| // synchronization between B and C would be broken. |
| TEST_P(MultithreadingTestES3, ThreadCWaitBeforeThreadBSyncFinish) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension() || !hasGLSyncExtension()); |
| |
| constexpr uint32_t kWidth = 100; |
| constexpr uint32_t kHeight = 200; |
| |
| GLsync sync = 0; |
| GLuint texture = 0; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0DrawAndFlush, |
| Thread1CreateFence, |
| Thread2WaitFence, |
| Thread2Finished, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Open a render pass and flush it. |
| ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f); |
| glFlush(); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Thread0DrawAndFlush); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 0 to set up |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0DrawAndFlush)); |
| |
| // Create a shared texture to test synchronization |
| GLTexture color; |
| texture = color; |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| // Draw to shared texture. |
| ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Issue a fence. A render pass is currently open, so the fence is not actually submitted |
| // in the Vulkan backend. |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_NE(sync, nullptr); |
| |
| // Wait for thread 1 to wait on it. |
| threadSynchronization.nextStep(Step::Thread1CreateFence); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread2WaitFence)); |
| |
| // 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::Thread2Finished); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| auto thread2 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 0 to create the fence object. |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1CreateFence)); |
| |
| // Test access to the fence object |
| threadSynchronization.nextStep(Step::Thread2WaitFence); |
| |
| constexpr GLuint64 kTimeout = 2'000'000'000; // 2 seconds |
| GLenum result = glClientWaitSync(sync, 0, kTimeout); |
| ASSERT_TRUE(result == GL_CONDITION_SATISFIED || result == GL_ALREADY_SIGNALED); |
| |
| // Verify the shared texture is drawn to. |
| glBindTexture(GL_TEXTURE_2D, texture); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::red); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread2Finished)); |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| std::array<LockStepThreadFunc, 3> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| std::move(thread2), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that having commands recorded but not submitted on one thread using a texture, does not |
| // interfere with similar commands on another thread using the same texture. Regression test for a |
| // bug in the Vulkan backend where the first thread would batch updates to a descriptor set not |
| // visible to the other thread, while the other thread picks up the (unupdated) descriptor set from |
| // a shared cache. |
| TEST_P(MultithreadingTestES3, UnsynchronizedTextureReads) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension() || !hasGLSyncExtension()); |
| |
| GLsync sync = 0; |
| GLuint texture = 0; |
| |
| constexpr GLubyte kInitialData[4] = {127, 63, 191, 255}; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0CreateTextureAndDraw, |
| Thread1DrawAndFlush, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create a texture, and record a command that draws into it. |
| GLTexture color; |
| texture = color; |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, kInitialData); |
| |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_NE(sync, nullptr); |
| |
| ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.0f); |
| |
| // Don't flush yet; this leaves the descriptor set updates to the texture pending in the |
| // Vulkan backend. |
| threadSynchronization.nextStep(Step::Thread0CreateTextureAndDraw); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1DrawAndFlush)); |
| |
| // Flush after thread 1 |
| EXPECT_PIXEL_COLOR_NEAR( |
| 0, 0, GLColor(kInitialData[0], kInitialData[1], kInitialData[2], kInitialData[3]), 1); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 0 to set up |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0CreateTextureAndDraw)); |
| |
| // Synchronize with the texture upload (but not the concurrent read) |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| |
| // Draw with the same texture, in the same way as thread 0. This ensures that the |
| // descriptor sets used in the Vulkan backend are identical. |
| glBindTexture(GL_TEXTURE_2D, texture); |
| ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.0f); |
| |
| // Flush |
| EXPECT_PIXEL_COLOR_NEAR( |
| 0, 0, GLColor(kInitialData[0], kInitialData[1], kInitialData[2], kInitialData[3]), 1); |
| |
| threadSynchronization.nextStep(Step::Thread1DrawAndFlush); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Similar to UnsynchronizedTextureReads, but the texture update is done through framebuffer write. |
| TEST_P(MultithreadingTestES3, UnsynchronizedTextureReads2) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension() || !hasGLSyncExtension()); |
| |
| GLsync sync = 0; |
| GLuint texture = 0; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0CreateTextureAndDraw, |
| Thread1DrawAndFlush, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create a texture, and record a command that draws into it. |
| GLTexture color; |
| texture = color; |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| glClearColor(1, 0, 0, 1); |
| glClear(GL_COLOR_BUFFER_BIT); |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); |
| |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_NE(sync, nullptr); |
| |
| ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.0f); |
| |
| // Don't flush yet; this leaves the descriptor set updates to the texture pending in the |
| // Vulkan backend. |
| threadSynchronization.nextStep(Step::Thread0CreateTextureAndDraw); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1DrawAndFlush)); |
| |
| // Flush after thread 1 |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 0 to set up |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0CreateTextureAndDraw)); |
| |
| // Synchronize with the texture update (but not the concurrent read) |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| |
| // Draw with the same texture, in the same way as thread 0. This ensures that the |
| // descriptor sets used in the Vulkan backend are identical. |
| glBindTexture(GL_TEXTURE_2D, texture); |
| ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.0f); |
| |
| // Flush |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); |
| |
| threadSynchronization.nextStep(Step::Thread1DrawAndFlush); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Similar to UnsynchronizedTextureReads, but the texture is used once. This is because |
| // UnsynchronizedTextureRead hits a different bug than it intends to test. This test makes sure the |
| // image is put in the right layout, by using it together with another texture (i.e. a different |
| // descriptor set). |
| TEST_P(MultithreadingTestES3, UnsynchronizedTextureReads3) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| constexpr GLubyte kInitialData[4] = {127, 63, 191, 255}; |
| |
| GLuint texture = 0; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0CreateTextureAndDraw, |
| Thread1DrawAndFlush, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create a texture, and record a command that draws into it. |
| GLTexture color; |
| texture = color; |
| |
| glActiveTexture(GL_TEXTURE0); |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, kInitialData); |
| |
| glActiveTexture(GL_TEXTURE1); |
| GLTexture color2; |
| glBindTexture(GL_TEXTURE_2D, color2); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, kInitialData); |
| |
| ANGLE_GL_PROGRAM(setupTexture, essl1_shaders::vs::Texture2D(), |
| R"(precision mediump float; |
| uniform sampler2D tex2D; |
| uniform sampler2D tex2D2; |
| varying vec2 v_texCoord; |
| |
| void main() |
| { |
| gl_FragColor = texture2D(tex2D, v_texCoord) + texture2D(tex2D2, v_texCoord); |
| })"); |
| drawQuad(setupTexture, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| |
| glFinish(); |
| |
| ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Don't flush yet; this leaves the descriptor set updates to the texture pending in the |
| // Vulkan backend. |
| threadSynchronization.nextStep(Step::Thread0CreateTextureAndDraw); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1DrawAndFlush)); |
| |
| // Flush after thread 1 |
| EXPECT_PIXEL_COLOR_NEAR( |
| 0, 0, GLColor(kInitialData[0], kInitialData[1], kInitialData[2], kInitialData[3]), 1); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 0 to set up |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0CreateTextureAndDraw)); |
| |
| // Draw with the same texture, in the same way as thread 0. This ensures that the |
| // descriptor sets used in the Vulkan backend are identical. |
| glBindTexture(GL_TEXTURE_2D, texture); |
| ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Flush |
| EXPECT_PIXEL_COLOR_NEAR( |
| 0, 0, GLColor(kInitialData[0], kInitialData[1], kInitialData[2], kInitialData[3]), 1); |
| |
| threadSynchronization.nextStep(Step::Thread1DrawAndFlush); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test framebuffer fetch program used between share groups. |
| void MultithreadingTestES3::testFramebufferFetch(DrawOrder drawOrder) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_shader_framebuffer_fetch_non_coherent")); |
| |
| GLProgram framebufferFetchProgram; |
| |
| constexpr char kFS[] = R"(#version 300 es |
| #extension GL_EXT_shader_framebuffer_fetch_non_coherent : require |
| layout(noncoherent, location = 0) inout highp vec4 o_color; |
| |
| uniform highp vec4 u_color; |
| void main (void) |
| { |
| o_color += u_color; |
| })"; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0PreCreateProgram, |
| Thread1CreateProgram, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Open a render pass, if requested. |
| if (drawOrder == DrawOrder::Before) |
| { |
| ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| threadSynchronization.nextStep(Step::Thread0PreCreateProgram); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1CreateProgram)); |
| |
| // Render using the framebuffer fetch program |
| if (drawOrder == DrawOrder::After) |
| { |
| ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| glFramebufferFetchBarrierEXT(); |
| |
| glUseProgram(framebufferFetchProgram); |
| GLint colorLocation = glGetUniformLocation(framebufferFetchProgram, "u_color"); |
| glUniform4f(colorLocation, 1, 0, 0, 0); |
| drawQuad(framebufferFetchProgram, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::yellow); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 0 to set up |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0PreCreateProgram)); |
| |
| // Create the framebuffer fetch program |
| framebufferFetchProgram.makeRaster(essl3_shaders::vs::Simple(), kFS); |
| glUseProgram(framebufferFetchProgram); |
| |
| // Notify the other thread to use it |
| threadSynchronization.nextStep(Step::Thread1CreateProgram); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| |
| glClearColor(0, 0, 0, 1); |
| glClear(GL_COLOR_BUFFER_BIT); |
| |
| glFramebufferFetchBarrierEXT(); |
| |
| glUseProgram(framebufferFetchProgram); |
| GLint colorLocation = glGetUniformLocation(framebufferFetchProgram, "u_color"); |
| glUniform4f(colorLocation, 0, 0, 1, 0); |
| drawQuad(framebufferFetchProgram, essl1_shaders::PositionAttrib(), 0.0f); |
| ASSERT_GL_NO_ERROR(); |
| |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Thread 1 creates the framebuffer fetch program. Thread 0 proceeds to use it. |
| TEST_P(MultithreadingTestES3, CreateFramebufferFetchBeforeRenderPass) |
| { |
| testFramebufferFetch(DrawOrder::After); |
| } |
| |
| // Thread 1 creates the framebuffer fetch program while thread 0 is mid render pass. Thread 0 |
| // proceeds to use the framebuffer fetch program in the rest of its render pass. |
| TEST_P(MultithreadingTestES3, CreateFramebufferFetchMidRenderPass) |
| { |
| testFramebufferFetch(DrawOrder::Before); |
| } |
| |
| // Test async monolithic pipeline creation in the Vulkan backend vs shared programs. This test |
| // makes one context/thread create a set of programs, then has another context/thread use them a few |
| // times, and then the original context destroys them. |
| TEST_P(MultithreadingTestES3, ProgramUseAndDestroyInTwoContexts) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| GLProgram programs[6]; |
| |
| GLsync sync = 0; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0CreatePrograms, |
| Thread1UsePrograms, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create the programs |
| programs[0].makeRaster(essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); |
| programs[1].makeRaster(essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); |
| programs[2].makeRaster(essl1_shaders::vs::Simple(), essl1_shaders::fs::Blue()); |
| programs[3].makeRaster(essl1_shaders::vs::Passthrough(), essl1_shaders::fs::Checkered()); |
| programs[4].makeRaster(essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); |
| programs[5].makeRaster(essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); |
| |
| EXPECT_TRUE(programs[0].valid()); |
| EXPECT_TRUE(programs[1].valid()); |
| EXPECT_TRUE(programs[2].valid()); |
| EXPECT_TRUE(programs[3].valid()); |
| EXPECT_TRUE(programs[4].valid()); |
| EXPECT_TRUE(programs[5].valid()); |
| |
| threadSynchronization.nextStep(Step::Thread0CreatePrograms); |
| // Wait for the other thread to use the programs |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1UsePrograms)); |
| |
| // Destroy them |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| programs[0].reset(); |
| programs[1].reset(); |
| programs[2].reset(); |
| programs[3].reset(); |
| programs[4].reset(); |
| programs[5].reset(); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| // Wait for thread 0 to create the programs |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0CreatePrograms)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Use them a few times. |
| drawQuad(programs[0], essl1_shaders::PositionAttrib(), 0.0f); |
| drawQuad(programs[1], essl1_shaders::PositionAttrib(), 0.0f); |
| drawQuad(programs[2], essl1_shaders::PositionAttrib(), 0.0f); |
| drawQuad(programs[3], essl1_shaders::PositionAttrib(), 0.0f); |
| drawQuad(programs[4], essl1_shaders::PositionAttrib(), 0.0f); |
| |
| drawQuad(programs[0], essl1_shaders::PositionAttrib(), 0.0f); |
| drawQuad(programs[1], essl1_shaders::PositionAttrib(), 0.0f); |
| drawQuad(programs[2], essl1_shaders::PositionAttrib(), 0.0f); |
| |
| drawQuad(programs[0], essl1_shaders::PositionAttrib(), 0.0f); |
| |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_NE(sync, nullptr); |
| |
| // Notify the other thread to destroy the programs. |
| threadSynchronization.nextStep(Step::Thread1UsePrograms); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); |
| |
| // Clean up |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Tests that Context with High Priority will correctly sample Texture rendered by Share Context |
| // with Low Priority. |
| TEST_P(MultithreadingTestES3, RenderThenSampleDifferentContextPriority) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| constexpr size_t kIterationCountMax = 10; |
| |
| const bool reduceLoad = isSwiftshader(); |
| const size_t iterationCount = reduceLoad ? 3 : kIterationCountMax; |
| const size_t heavyDrawCount = reduceLoad ? 25 : 100; |
| |
| // Initialize contexts |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLConfig config = window->getConfig(); |
| |
| // Large enough texture to catch timing problems. |
| constexpr GLsizei kTexSize = 1024; |
| constexpr size_t kThreadCount = 2; |
| EGLSurface surface[kThreadCount] = {EGL_NO_SURFACE, EGL_NO_SURFACE}; |
| EGLContext ctx[kThreadCount] = {EGL_NO_CONTEXT, EGL_NO_CONTEXT}; |
| |
| EGLint priorities[kThreadCount] = {EGL_CONTEXT_PRIORITY_LOW_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG}; |
| |
| EGLint pbufferAttributes[kThreadCount][6] = { |
| {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE, EGL_NONE}, |
| {EGL_WIDTH, kTexSize, EGL_HEIGHT, kTexSize, EGL_NONE, EGL_NONE}}; |
| |
| EGLint attributes[] = {EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_NONE, |
| EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE, EGL_NONE, EGL_NONE}; |
| EGLint *extraAttributes = attributes; |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_ANGLE_context_virtualization")) |
| { |
| attributes[2] = EGL_NONE; |
| } |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_IMG_context_priority")) |
| { |
| // Run tests with single priority anyway. |
| extraAttributes += 2; |
| } |
| |
| for (size_t t = 0; t < kThreadCount; ++t) |
| { |
| surface[t] = eglCreatePbufferSurface(dpy, config, pbufferAttributes[t]); |
| EXPECT_EGL_SUCCESS(); |
| |
| attributes[1] = priorities[t]; |
| attributes[3] = mVirtualizationGroup++; |
| |
| ctx[t] = window->createContext(t == 0 ? EGL_NO_CONTEXT : ctx[0], extraAttributes); |
| EXPECT_NE(EGL_NO_CONTEXT, ctx[t]); |
| } |
| |
| // Synchronization tools to ensure the two threads are interleaved as designed by this test. |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0Init, |
| Thread1Init, |
| Thread0Draw, |
| Thread1Draw, |
| Finish = Thread0Draw + kIterationCountMax * 2, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| GLTexture texture; |
| GLsync thread0DrawSyncObj; |
| |
| auto calculateTestColor = [](size_t i) { |
| return GLColor(i % 256, (i + 1) % 256, (i + 2) % 256, 255); |
| }; |
| auto makeStep = [](Step base, size_t i) { |
| return static_cast<Step>(static_cast<size_t>(base) + i * 2); |
| }; |
| |
| // Render to the texture. |
| std::thread thread0 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0])); |
| EXPECT_EGL_SUCCESS(); |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| nullptr); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| glViewport(0, 0, kTexSize, kTexSize); |
| |
| ANGLE_GL_PROGRAM(colorProgram, essl1_shaders::vs::Simple(), |
| essl1_shaders::fs::UniformColor()); |
| GLint colorLocation = |
| glGetUniformLocation(colorProgram, angle::essl1_shaders::ColorUniform()); |
| ASSERT_NE(-1, colorLocation); |
| glUseProgram(colorProgram); |
| |
| // Notify second thread that initialization is finished. |
| threadSynchronization.nextStep(Step::Thread0Init); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Init)); |
| |
| for (size_t i = 0; i < iterationCount; ++i) |
| { |
| // Simulate heavy work... |
| glUniform4f(colorLocation, 0.0f, 0.0f, 0.0f, 0.0f); |
| for (size_t j = 0; j < heavyDrawCount; ++j) |
| { |
| drawQuad(colorProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| } |
| |
| // Draw with test color. |
| Vector4 color = calculateTestColor(i).toNormalizedVector(); |
| glUniform4f(colorLocation, color.x(), color.y(), color.z(), color.w()); |
| drawQuad(colorProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| |
| thread0DrawSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Notify second thread that draw is finished. |
| threadSynchronization.nextStep(makeStep(Step::Thread0Draw, i)); |
| ASSERT_TRUE(threadSynchronization.waitForStep( |
| (i == iterationCount - 1) ? Step::Finish : makeStep(Step::Thread1Draw, i))); |
| } |
| |
| EXPECT_GL_NO_ERROR(); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| }); |
| |
| // Sample texture |
| std::thread thread1 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[1], surface[1], ctx[1])); |
| EXPECT_EGL_SUCCESS(); |
| |
| glViewport(0, 0, kTexSize, kTexSize); |
| |
| ANGLE_GL_PROGRAM(textureProgram, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| glUseProgram(textureProgram); |
| |
| // Wait for first thread to finish initializing. |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Init)); |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| |
| // Wait for first thread to draw using the shared texture. |
| threadSynchronization.nextStep(Step::Thread1Init); |
| |
| for (size_t i = 0; i < iterationCount; ++i) |
| { |
| ASSERT_TRUE(threadSynchronization.waitForStep(makeStep(Step::Thread0Draw, i))); |
| |
| ASSERT_TRUE(thread0DrawSyncObj != nullptr); |
| glWaitSync(thread0DrawSyncObj, 0, GL_TIMEOUT_IGNORED); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Should draw test color. |
| drawQuad(textureProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| |
| // Check test color in four corners. |
| GLColor color = calculateTestColor(i); |
| EXPECT_PIXEL_EQ(0, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(0, kTexSize - 1, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, kTexSize - 1, color.R, color.G, color.B, color.A); |
| |
| glDeleteSync(thread0DrawSyncObj); |
| ASSERT_GL_NO_ERROR(); |
| thread0DrawSyncObj = nullptr; |
| |
| threadSynchronization.nextStep( |
| (i == iterationCount - 1) ? Step::Finish : makeStep(Step::Thread1Draw, i)); |
| } |
| |
| EXPECT_GL_NO_ERROR(); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| }); |
| |
| thread0.join(); |
| thread1.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 newly created Context with High Priority will correctly sample Texture already |
| // rendered by Share Context with Low Priority. |
| TEST_P(MultithreadingTestES3, RenderThenSampleInNewContextWithDifferentPriority) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_disjoint_timer_query")); |
| |
| const bool reduceLoad = isSwiftshader(); |
| const size_t heavyDrawCount = reduceLoad ? 75 : 1000; |
| |
| // Initialize contexts |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLConfig config = window->getConfig(); |
| |
| // Large enough texture to catch timing problems. |
| constexpr GLsizei kTexSize = 1024; |
| constexpr size_t kThreadCount = 2; |
| EGLSurface surface[kThreadCount] = {EGL_NO_SURFACE, EGL_NO_SURFACE}; |
| EGLContext ctx[kThreadCount] = {EGL_NO_CONTEXT, EGL_NO_CONTEXT}; |
| |
| EGLint priorities[kThreadCount] = {EGL_CONTEXT_PRIORITY_LOW_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG}; |
| |
| EGLint pbufferAttributes[kThreadCount][6] = { |
| {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE, EGL_NONE}, |
| {EGL_WIDTH, kTexSize, EGL_HEIGHT, kTexSize, EGL_NONE, EGL_NONE}}; |
| |
| EGLint attributes[] = {EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_NONE, |
| EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE, EGL_NONE, EGL_NONE}; |
| EGLint *extraAttributes = attributes; |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_ANGLE_context_virtualization")) |
| { |
| attributes[2] = EGL_NONE; |
| } |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_IMG_context_priority")) |
| { |
| // Run tests with single priority anyway. |
| extraAttributes += 2; |
| } |
| |
| for (size_t t = 0; t < kThreadCount; ++t) |
| { |
| surface[t] = eglCreatePbufferSurface(dpy, config, pbufferAttributes[t]); |
| EXPECT_EGL_SUCCESS(); |
| |
| attributes[1] = priorities[t]; |
| attributes[3] = mVirtualizationGroup++; |
| |
| // Second context will be created in a thread 1 |
| if (t == 0) |
| { |
| ctx[t] = window->createContext(t == 0 ? EGL_NO_CONTEXT : ctx[0], extraAttributes); |
| EXPECT_NE(EGL_NO_CONTEXT, ctx[t]); |
| } |
| } |
| |
| // Synchronization tools to ensure the two threads are interleaved as designed by this test. |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0Draw, |
| Thread1Draw, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| // Create shared resources before threads to minimize timing delays. |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0])); |
| EXPECT_EGL_SUCCESS(); |
| |
| 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()); |
| ANGLE_GL_PROGRAM(textureProgram, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| |
| GLTexture texture; |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| nullptr); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| glViewport(0, 0, kTexSize, kTexSize); |
| |
| EXPECT_GL_NO_ERROR(); |
| EXPECT_EGL_TRUE(window->makeCurrent()); |
| EXPECT_EGL_SUCCESS(); |
| |
| GLsync thread0DrawSyncObj; |
| |
| // Render to the texture. |
| std::thread thread0 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0])); |
| EXPECT_EGL_SUCCESS(); |
| |
| // Enable additive blend |
| glEnable(GL_BLEND); |
| glBlendFunc(GL_ONE, GL_ONE); |
| |
| GLuint query; |
| glGenQueries(1, &query); |
| glBeginQuery(GL_TIME_ELAPSED_EXT, query); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Simulate heavy work... |
| glUseProgram(redProgram); |
| for (size_t j = 0; j < heavyDrawCount; ++j) |
| { |
| // Draw with Red color. |
| drawQuad(redProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| } |
| |
| // Draw with Green color. |
| glUseProgram(greenProgram); |
| drawQuad(greenProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| |
| // This should force "flushToPrimary()" |
| glEndQuery(GL_TIME_ELAPSED_EXT); |
| glDeleteQueries(1, &query); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Continue draw with Blue color after flush... |
| glUseProgram(blueProgram); |
| drawQuad(blueProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| |
| thread0DrawSyncObj = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Notify second thread that draw is finished. |
| threadSynchronization.nextStep(Step::Thread0Draw); |
| 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(); |
| }); |
| |
| // Sample texture |
| std::thread thread1 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| // Wait for first thread to finish draw. |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Draw)); |
| |
| // Create High priority Context when Low priority Context already rendered to the texture. |
| ctx[1] = window->createContext(ctx[0], extraAttributes); |
| EXPECT_NE(EGL_NO_CONTEXT, ctx[1]); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[1], surface[1], ctx[1])); |
| EXPECT_EGL_SUCCESS(); |
| |
| glViewport(0, 0, kTexSize, kTexSize); |
| |
| ASSERT_TRUE(thread0DrawSyncObj != nullptr); |
| glWaitSync(thread0DrawSyncObj, 0, GL_TIMEOUT_IGNORED); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Should draw test color. |
| glUseProgram(textureProgram); |
| glBindTexture(GL_TEXTURE_2D, texture); |
| drawQuad(textureProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| |
| // Check test color in four corners. |
| GLColor color = GLColor::white; |
| EXPECT_PIXEL_EQ(0, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(0, kTexSize - 1, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, kTexSize - 1, color.R, color.G, color.B, color.A); |
| |
| glDeleteSync(thread0DrawSyncObj); |
| ASSERT_GL_NO_ERROR(); |
| thread0DrawSyncObj = nullptr; |
| |
| threadSynchronization.nextStep(Step::Finish); |
| |
| EXPECT_GL_NO_ERROR(); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| }); |
| |
| thread0.join(); |
| thread1.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 with High Priority will correctly sample EGLImage target Texture rendered by |
| // other Context with Low Priority into EGLImage source texture. |
| TEST_P(MultithreadingTestES3, RenderThenSampleDifferentContextPriorityUsingEGLImage) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!hasWaitSyncExtension() || !hasGLSyncExtension()); |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_EGL_image")); |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_disjoint_timer_query")); |
| |
| const bool reduceLoad = isSwiftshader(); |
| const size_t heavyDrawCount = reduceLoad ? 75 : 1000; |
| |
| // Initialize contexts |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLConfig config = window->getConfig(); |
| |
| ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_image_base")); |
| ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(dpy, "EGL_KHR_gl_texture_2D_image")); |
| |
| // Large enough texture to catch timing problems. |
| constexpr GLsizei kTexSize = 1024; |
| constexpr size_t kThreadCount = 2; |
| EGLSurface surface[kThreadCount] = {EGL_NO_SURFACE, EGL_NO_SURFACE}; |
| EGLContext ctx[kThreadCount] = {EGL_NO_CONTEXT, EGL_NO_CONTEXT}; |
| |
| EGLint priorities[kThreadCount] = {EGL_CONTEXT_PRIORITY_LOW_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG}; |
| |
| EGLint pbufferAttributes[kThreadCount][6] = { |
| {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE, EGL_NONE}, |
| {EGL_WIDTH, kTexSize, EGL_HEIGHT, kTexSize, EGL_NONE, EGL_NONE}}; |
| |
| EGLint attributes[] = {EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_NONE, |
| EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE, EGL_NONE, EGL_NONE}; |
| EGLint *extraAttributes = attributes; |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_ANGLE_context_virtualization")) |
| { |
| attributes[2] = EGL_NONE; |
| } |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_IMG_context_priority")) |
| { |
| // Run tests with single priority anyway. |
| extraAttributes += 2; |
| } |
| |
| for (size_t t = 0; t < kThreadCount; ++t) |
| { |
| surface[t] = eglCreatePbufferSurface(dpy, config, pbufferAttributes[t]); |
| EXPECT_EGL_SUCCESS(); |
| |
| attributes[1] = priorities[t]; |
| attributes[3] = mVirtualizationGroup++; |
| |
| // Contexts not shared |
| ctx[t] = window->createContext(EGL_NO_CONTEXT, extraAttributes); |
| EXPECT_NE(EGL_NO_CONTEXT, ctx[t]); |
| } |
| |
| // Synchronization tools to ensure the two threads are interleaved as designed by this test. |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread1Init, |
| Thread0Draw, |
| Thread1Draw, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| EGLImage image = EGL_NO_IMAGE_KHR; |
| EGLSyncKHR sync = EGL_NO_SYNC_KHR; |
| |
| // Render to the EGLImage source texture. |
| std::thread thread0 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0])); |
| EXPECT_EGL_SUCCESS(); |
| |
| // Create source texture. |
| GLTexture texture; |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| nullptr); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| glViewport(0, 0, kTexSize, kTexSize); |
| |
| 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()); |
| |
| // Wait for second thread to finish initializing. |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Init)); |
| |
| // Enable additive blend |
| glEnable(GL_BLEND); |
| glBlendFunc(GL_ONE, GL_ONE); |
| |
| GLuint query; |
| glGenQueries(1, &query); |
| glBeginQuery(GL_TIME_ELAPSED_EXT, query); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Simulate heavy work... |
| glUseProgram(redProgram); |
| for (size_t j = 0; j < heavyDrawCount; ++j) |
| { |
| // Draw with Red color. |
| drawQuad(redProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| } |
| |
| // Draw with Green color. |
| glUseProgram(greenProgram); |
| drawQuad(greenProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| |
| // This should force "flushToPrimary()" |
| glEndQuery(GL_TIME_ELAPSED_EXT); |
| glDeleteQueries(1, &query); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Continue draw with Blue color after flush... |
| glUseProgram(blueProgram); |
| drawQuad(blueProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| |
| sync = eglCreateSyncKHR(dpy, EGL_SYNC_FENCE_KHR, nullptr); |
| EXPECT_NE(sync, EGL_NO_SYNC_KHR); |
| |
| // Create EGLImage. |
| image = eglCreateImageKHR( |
| dpy, ctx[0], EGL_GL_TEXTURE_2D_KHR, |
| reinterpret_cast<EGLClientBuffer>(static_cast<uintptr_t>(texture)), nullptr); |
| ASSERT_EGL_SUCCESS(); |
| |
| // Notify second thread that draw is finished. |
| threadSynchronization.nextStep(Step::Thread0Draw); |
| 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(); |
| }); |
| |
| // Sample texture |
| std::thread thread1 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[1], surface[1], ctx[1])); |
| EXPECT_EGL_SUCCESS(); |
| |
| glViewport(0, 0, kTexSize, kTexSize); |
| |
| ANGLE_GL_PROGRAM(textureProgram, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| glUseProgram(textureProgram); |
| |
| // Create target texture. |
| GLTexture texture; |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| |
| // Wait for first thread to draw into the source texture. |
| threadSynchronization.nextStep(Step::Thread1Init); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Draw)); |
| |
| // Wait for draw to complete |
| ASSERT_TRUE(eglWaitSyncKHR(dpy, sync, 0)); |
| |
| // Specify target texture from EGLImage. |
| glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Should draw test color. |
| drawQuad(textureProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Check test color in four corners. |
| GLColor color = GLColor::white; |
| EXPECT_PIXEL_EQ(0, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(0, kTexSize - 1, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, kTexSize - 1, color.R, color.G, color.B, color.A); |
| |
| EXPECT_EGL_TRUE(eglDestroyImageKHR(dpy, image)); |
| image = EGL_NO_IMAGE_KHR; |
| |
| EXPECT_EGL_TRUE(eglDestroySyncKHR(dpy, sync)); |
| sync = EGL_NO_SYNC_KHR; |
| |
| threadSynchronization.nextStep(Step::Finish); |
| |
| EXPECT_GL_NO_ERROR(); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| }); |
| |
| thread0.join(); |
| thread1.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 mixing commands of Contexts with different Priorities in a single Command Buffers (Vulkan). |
| TEST_P(MultithreadingTestES3, ContextPriorityMixing) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_disjoint_timer_query")); |
| |
| constexpr size_t kIterationCountMax = 10; |
| |
| const bool reduceLoad = isSwiftshader(); |
| const size_t iterationCount = reduceLoad ? 3 : kIterationCountMax; |
| const size_t heavyDrawCount = reduceLoad ? 25 : 100; |
| |
| // Initialize contexts |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLConfig config = window->getConfig(); |
| |
| // Large enough texture to catch timing problems. |
| constexpr GLsizei kTexSize = 1024; |
| constexpr size_t kThreadCount = 2; |
| EGLSurface surface[kThreadCount] = {EGL_NO_SURFACE, EGL_NO_SURFACE}; |
| EGLContext ctx[kThreadCount] = {EGL_NO_CONTEXT, EGL_NO_CONTEXT}; |
| |
| EGLint priorities[kThreadCount] = {EGL_CONTEXT_PRIORITY_LOW_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG}; |
| |
| EGLint pbufferAttributes[kThreadCount][6] = { |
| {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE, EGL_NONE}, |
| {EGL_WIDTH, kTexSize, EGL_HEIGHT, kTexSize, EGL_NONE, EGL_NONE}}; |
| |
| EGLint attributes[] = {EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_NONE, |
| EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE, EGL_NONE, EGL_NONE}; |
| EGLint *extraAttributes = attributes; |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_ANGLE_context_virtualization")) |
| { |
| attributes[2] = EGL_NONE; |
| } |
| if (!IsEGLDisplayExtensionEnabled(dpy, "EGL_IMG_context_priority")) |
| { |
| // Run tests with single priority anyway. |
| extraAttributes += 2; |
| } |
| |
| for (size_t t = 0; t < kThreadCount; ++t) |
| { |
| surface[t] = eglCreatePbufferSurface(dpy, config, pbufferAttributes[t]); |
| EXPECT_EGL_SUCCESS(); |
| |
| attributes[1] = priorities[t]; |
| attributes[3] = mVirtualizationGroup++; |
| |
| // Contexts not shared |
| ctx[t] = window->createContext(EGL_NO_CONTEXT, extraAttributes); |
| EXPECT_NE(EGL_NO_CONTEXT, ctx[t]); |
| } |
| |
| // Synchronization tools to ensure the two threads are interleaved as designed by this test. |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread1DrawColor, |
| Thread0Iterate, |
| Finish = Thread1DrawColor + kIterationCountMax * 2, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto makeStep = [](Step base, size_t i) { |
| return static_cast<Step>(static_cast<size_t>(base) + i * 2); |
| }; |
| |
| // Triggers commands submission. |
| std::thread thread0 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0])); |
| EXPECT_EGL_SUCCESS(); |
| |
| ANGLE_GL_PROGRAM(redProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); |
| |
| for (size_t i = 0; i < iterationCount; ++i) |
| { |
| ASSERT_TRUE(threadSynchronization.waitForStep(makeStep(Step::Thread1DrawColor, i))); |
| |
| glUseProgram(redProgram); |
| drawQuad(redProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // This should perform commands submission. |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| |
| threadSynchronization.nextStep(makeStep(Step::Thread0Iterate, i)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[0], surface[0], ctx[0])); |
| EXPECT_EGL_SUCCESS(); |
| } |
| |
| 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(); |
| }); |
| |
| // Render and then sample texture. |
| std::thread thread1 = std::thread([&]() { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface[1], surface[1], ctx[1])); |
| EXPECT_EGL_SUCCESS(); |
| |
| glDisable(GL_DEPTH_TEST); |
| glViewport(0, 0, kTexSize, kTexSize); |
| |
| GLTexture texture; |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| nullptr); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| ANGLE_GL_PROGRAM(colorProgram, essl1_shaders::vs::Simple(), |
| essl1_shaders::fs::UniformColor()); |
| GLint colorLocation = |
| glGetUniformLocation(colorProgram, angle::essl1_shaders::ColorUniform()); |
| ASSERT_NE(-1, colorLocation); |
| |
| ANGLE_GL_PROGRAM(textureProgram, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| |
| for (size_t i = 0; i < iterationCount; ++i) |
| { |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glUseProgram(colorProgram); |
| |
| GLuint query; |
| glGenQueries(1, &query); |
| glBeginQuery(GL_TIME_ELAPSED_EXT, query); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Simulate heavy work... |
| glUniform4f(colorLocation, 0.0f, 0.0f, 0.0f, 0.0f); |
| for (size_t j = 0; j < heavyDrawCount; ++j) |
| { |
| drawQuad(colorProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| } |
| |
| // Draw with test color. |
| GLColor color(i % 256, (i + 1) % 256, (i + 2) % 256, 255); |
| Vector4 colorF = color.toNormalizedVector(); |
| glUniform4f(colorLocation, colorF.x(), colorF.y(), colorF.z(), colorF.w()); |
| drawQuad(colorProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // This should force "flushToPrimary()" |
| glEndQuery(GL_TIME_ELAPSED_EXT); |
| glDeleteQueries(1, &query); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(makeStep(Step::Thread1DrawColor, i)); |
| ASSERT_TRUE(threadSynchronization.waitForStep(makeStep(Step::Thread0Iterate, i))); |
| |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); |
| glUseProgram(textureProgram); |
| |
| // Should draw test color. |
| drawQuad(textureProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Check test color in four corners. |
| EXPECT_PIXEL_EQ(0, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(0, kTexSize - 1, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, 0, color.R, color.G, color.B, color.A); |
| EXPECT_PIXEL_EQ(kTexSize - 1, kTexSize - 1, color.R, color.G, color.B, color.A); |
| } |
| |
| threadSynchronization.nextStep(Step::Finish); |
| |
| EXPECT_GL_NO_ERROR(); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| }); |
| |
| thread0.join(); |
| thread1.join(); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| |
| // Clean up |
| for (size_t t = 0; t < kThreadCount; ++t) |
| { |
| eglDestroySurface(dpy, surface[t]); |
| eglDestroyContext(dpy, ctx[t]); |
| } |
| } |
| |
| // Test that it is possible to upload textures in one thread and use them in another with |
| // synchronization. |
| TEST_P(MultithreadingTestES3, MultithreadedTextureUploadAndDraw) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| constexpr size_t kTexSize = 4; |
| GLTexture texture1; |
| GLTexture texture2; |
| std::vector<GLColor> textureColors1(kTexSize * kTexSize, GLColor::red); |
| std::vector<GLColor> textureColors2(kTexSize * kTexSize, GLColor::green); |
| |
| // Sync primitives |
| GLsync sync = nullptr; |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0UploadFinish, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| // Threads to upload and draw with textures. |
| auto thread0Upload = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Start)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Two mipmap textures are defined here. They are used for drawing in the other thread. |
| glBindTexture(GL_TEXTURE_2D, texture1); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| textureColors1.data()); |
| glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kTexSize / 2, kTexSize / 2, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors1.data()); |
| glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, kTexSize / 4, kTexSize / 4, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors1.data()); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| ASSERT_GL_NO_ERROR(); |
| |
| glBindTexture(GL_TEXTURE_2D, texture2); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| textureColors2.data()); |
| glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kTexSize / 2, kTexSize / 2, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors2.data()); |
| glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, kTexSize / 4, kTexSize / 4, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors2.data()); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Create a sync object to be used for the draw thread. |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| glFlush(); |
| ASSERT_NE(sync, nullptr); |
| |
| threadSynchronization.nextStep(Step::Thread0UploadFinish); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| auto thread1Draw = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0UploadFinish)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for the sync object to be signaled. |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Draw using the textures from the texture upload thread. |
| ANGLE_GL_PROGRAM(textureProgram, essl1_shaders::vs::Texture2D(), |
| essl1_shaders::fs::Texture2D()); |
| glUseProgram(textureProgram); |
| |
| glBindTexture(GL_TEXTURE_2D, texture1); |
| drawQuad(textureProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| glFlush(); |
| ASSERT_GL_NO_ERROR(); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kTexSize, kTexSize, GLColor::red); |
| |
| glBindTexture(GL_TEXTURE_2D, texture2); |
| drawQuad(textureProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| glFlush(); |
| ASSERT_GL_NO_ERROR(); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kTexSize, kTexSize, GLColor::green); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0Upload), |
| std::move(thread1Draw), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that it is possible to create a new context after uploading mutable mipmap textures in the |
| // previous context, and use them in the new context. |
| TEST_P(MultithreadingTestES3, CreateNewContextAfterTextureUploadOnNewThread) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| constexpr size_t kTexSize = 4; |
| GLTexture texture1; |
| GLTexture texture2; |
| std::vector<GLColor> textureColors1(kTexSize * kTexSize, GLColor::red); |
| std::vector<GLColor> textureColors2(kTexSize * kTexSize, GLColor::green); |
| |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| |
| std::thread thread = std::thread([&]() { |
| // Create a context and upload the textures. |
| EGLContext ctx1 = createMultithreadedContext(window, EGL_NO_CONTEXT); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx1)); |
| |
| glBindTexture(GL_TEXTURE_2D, texture1); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| textureColors1.data()); |
| glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kTexSize / 2, kTexSize / 2, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors1.data()); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| ASSERT_GL_NO_ERROR(); |
| |
| glBindTexture(GL_TEXTURE_2D, texture2); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| textureColors2.data()); |
| glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kTexSize / 2, kTexSize / 2, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors2.data()); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Create a new context and use the uploaded textures. |
| EGLContext ctx2 = createMultithreadedContext(window, ctx1); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx2)); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture1, 0); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kTexSize, kTexSize, GLColor::red); |
| |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture2, 0); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kTexSize, kTexSize, GLColor::green); |
| |
| // Destroy the contexts. |
| EXPECT_EGL_TRUE(eglDestroyContext(dpy, ctx2)); |
| EXPECT_EGL_TRUE(eglDestroyContext(dpy, ctx1)); |
| }); |
| |
| thread.join(); |
| } |
| |
| // Test that it is possible to create a new context after uploading mutable mipmap textures in the |
| // main thread, and use them in the new context. |
| TEST_P(MultithreadingTestES3, CreateNewContextAfterTextureUploadOnMainThread) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| |
| // Upload the textures. |
| constexpr size_t kTexSize = 4; |
| GLTexture texture1; |
| GLTexture texture2; |
| std::vector<GLColor> textureColors1(kTexSize * kTexSize, GLColor::red); |
| std::vector<GLColor> textureColors2(kTexSize * kTexSize, GLColor::green); |
| |
| glBindTexture(GL_TEXTURE_2D, texture1); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| textureColors1.data()); |
| glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kTexSize / 2, kTexSize / 2, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors1.data()); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| ASSERT_GL_NO_ERROR(); |
| |
| glBindTexture(GL_TEXTURE_2D, texture2); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, |
| textureColors2.data()); |
| glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kTexSize / 2, kTexSize / 2, 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, textureColors2.data()); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| ASSERT_GL_NO_ERROR(); |
| |
| GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| |
| std::thread thread = std::thread([&]() { |
| // Create a context. |
| EGLContext ctx1 = createMultithreadedContext(window, window->getContext()); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx1)); |
| |
| // Wait for the sync object to be signaled. |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Use the uploaded textures in the main thread. |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture1, 0); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kTexSize, kTexSize, GLColor::red); |
| |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture2, 0); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kTexSize, kTexSize, GLColor::green); |
| |
| // Destroy the context. |
| EXPECT_EGL_TRUE(eglDestroyContext(dpy, ctx1)); |
| }); |
| |
| thread.join(); |
| } |
| |
| // Test when lots of upload happens on a different thread at the same time as the main thread doing |
| // draws. |
| TEST_P(MultithreadingTestES3, SimultaneousUploadAndDraw) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| // The following shader is used to create busy work while the worker thread is doing something. |
| // It intentionally spreads its uniforms and inputs so the main thread has to make many GL |
| // calls. |
| constexpr char kBusyDrawVS[] = R"(#version 300 es |
| uniform mediump float x0; |
| uniform mediump float y0; |
| uniform mediump float x1; |
| uniform mediump float y1; |
| |
| in mediump float r; |
| in mediump float g; |
| in mediump float b; |
| in mediump float a; |
| |
| out mediump vec4 color; |
| |
| void main() |
| { |
| // gl_VertexID x y |
| // 0 -1 -1 |
| // 1 1 -1 |
| // 2 -1 1 |
| // 3 1 1 |
| int bit0 = gl_VertexID & 1; |
| int bit1 = gl_VertexID >> 1; |
| gl_Position.x = bit0 == 0 ? x0 : x1; |
| gl_Position.y = bit1 == 0 ? y0 : y1; |
| gl_Position.z = 0.; |
| gl_Position.w = 1.; |
| |
| color = vec4(r, g, b, a); |
| })"; |
| constexpr char kBusyDrawFS[] = R"(#version 300 es |
| |
| in mediump vec4 color; |
| out mediump vec4 colorOut; |
| |
| void main() |
| { |
| colorOut = color; |
| })"; |
| |
| // The following shader is used to consume the results of texture uploads, ensuring appropriate |
| // synchronization. |
| constexpr char kTextureDrawVS[] = R"(#version 300 es |
| out mediump vec2 uv; |
| |
| void main() |
| { |
| // gl_VertexID x y |
| // 0 -1 -1 |
| // 1 1 -1 |
| // 2 -1 1 |
| // 3 1 1 |
| int bit0 = gl_VertexID & 1; |
| int bit1 = gl_VertexID >> 1; |
| gl_Position = vec4(bit0 * 2 - 1, bit1 * 2 - 1, 0, 1); |
| uv = vec2(bit0, bit1); |
| })"; |
| constexpr char kTextureDrawFS[] = R"(#version 300 es |
| |
| uniform mediump sampler2D s0; |
| uniform mediump sampler2D s1; |
| uniform mediump sampler2D s2; |
| uniform mediump sampler2D s3; |
| uniform mediump sampler2D s4; |
| uniform mediump sampler2D s5; |
| uniform mediump sampler2D s6; |
| uniform mediump sampler2D s7; |
| uniform mediump sampler2D s8; |
| uniform mediump sampler2D s9; |
| |
| in mediump vec2 uv; |
| out mediump vec4 colorOut; |
| |
| void main() |
| { |
| highp vec4 result = texture(s0, uv) + |
| texture(s1, uv) + |
| texture(s2, uv) + |
| texture(s3, uv) + |
| texture(s4, uv) + |
| texture(s5, uv) + |
| texture(s6, uv) + |
| texture(s7, uv) + |
| texture(s8, uv) + |
| texture(s9, uv); |
| result /= 10.; |
| |
| colorOut = result; |
| })"; |
| |
| constexpr uint32_t kTextureCount = 10; |
| GLuint textures[kTextureCount]; |
| |
| ASSERT(IsGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr") || |
| IsGLExtensionEnabled("GL_EXT_texture_compression_bptc")); |
| // Note ASTC may be emulated in ANGLE, so check for BPTC first |
| const bool hasBPTC = IsGLExtensionEnabled("GL_EXT_texture_compression_bptc"); |
| const GLenum compressedFormat = |
| hasBPTC ? GL_COMPRESSED_RGBA_BPTC_UNORM_EXT : GL_COMPRESSED_RGBA_ASTC_4x4_KHR; |
| |
| std::vector<uint8_t> textureData[kTextureCount]; |
| |
| constexpr int kSurfaceWidth = 256; |
| constexpr int kSurfaceHeight = 512; |
| constexpr int kTexSize = 1024; |
| |
| // Sync primitives |
| GLsync sync = nullptr; |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread1Ready, |
| Thread0UploadFinish, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| // Threads to upload and draw with textures. |
| auto thread0Upload = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for the other thread to set everything up |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Ready)); |
| |
| // Perform uploads while the other thread does draws |
| for (uint32_t i = 0; i < kTextureCount; ++i) |
| { |
| glBindTexture(GL_TEXTURE_2D, textures[i]); |
| glCompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTexSize, kTexSize, compressedFormat, |
| static_cast<GLsizei>(textureData[i].size()), |
| textureData[i].data()); |
| } |
| ASSERT_GL_NO_ERROR(); |
| |
| // Create a sync object to be used for the draw thread. |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| ASSERT_NE(sync, nullptr); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Thread0UploadFinish); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| auto thread1Draw = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| ANGLE_GL_PROGRAM(busyDrawProgram, kBusyDrawVS, kBusyDrawFS); |
| |
| // Set up the test. Don't let the other thread work yet. |
| glUseProgram(busyDrawProgram); |
| GLuint busyDrawX0Loc = glGetUniformLocation(busyDrawProgram, "x0"); |
| GLuint busyDrawY0Loc = glGetUniformLocation(busyDrawProgram, "y0"); |
| GLuint busyDrawX1Loc = glGetUniformLocation(busyDrawProgram, "x1"); |
| GLuint busyDrawY1Loc = glGetUniformLocation(busyDrawProgram, "y1"); |
| GLuint busyDrawRLoc = glGetAttribLocation(busyDrawProgram, "r"); |
| GLuint busyDrawGLoc = glGetAttribLocation(busyDrawProgram, "g"); |
| GLuint busyDrawBLoc = glGetAttribLocation(busyDrawProgram, "b"); |
| GLuint busyDrawALoc = glGetAttribLocation(busyDrawProgram, "a"); |
| |
| ANGLE_GL_PROGRAM(textureDrawProgram, kTextureDrawVS, kTextureDrawFS); |
| GLuint textureDrawSamplerLoc[kTextureCount] = {}; |
| |
| glUseProgram(textureDrawProgram); |
| glGenTextures(kTextureCount, textures); |
| for (uint32_t i = 0; i < kTextureCount; ++i) |
| { |
| std::ostringstream name; |
| name << "s" << i; |
| |
| textureDrawSamplerLoc[i] = glGetUniformLocation(textureDrawProgram, name.str().c_str()); |
| |
| glBindTexture(GL_TEXTURE_2D, textures[i]); |
| glTexStorage2D(GL_TEXTURE_2D, 1, compressedFormat, kTexSize, kTexSize); |
| |
| // Both ASTC 4x4 and BPTC have 1 byte per pixel. The textures' contents are arbitrary |
| // but distinct. |
| textureData[i].resize(kTexSize * kTexSize); |
| for (int y = 0; y < kTexSize; ++y) |
| { |
| for (int x = 0; x < kTexSize; ++x) |
| { |
| textureData[i][y * kTexSize + x] = (i * 50 + y + x) % 255; |
| } |
| } |
| } |
| ASSERT_GL_NO_ERROR(); |
| |
| // Now that everything is set up, let the upload thread work while this thread does draws. |
| threadSynchronization.nextStep(Step::Thread1Ready); |
| |
| int w = kSurfaceWidth; |
| int h = kSurfaceHeight; |
| |
| glClear(GL_COLOR_BUFFER_BIT); |
| glViewport(0, 0, w, h); |
| glUseProgram(busyDrawProgram); |
| for (uint32_t y = 0; y < 8; ++y) |
| { |
| for (uint32_t x = 0; x < 8; ++x) |
| { |
| float width = w / 4; |
| float height = h / 8; |
| |
| glUniform1f(busyDrawX0Loc, x * width / w - 1); |
| glUniform1f(busyDrawY0Loc, y * height / h); |
| glUniform1f(busyDrawX1Loc, (x + 1) * width / w - 1); |
| glUniform1f(busyDrawY1Loc, (y + 1) * height / h); |
| |
| glVertexAttrib1f(busyDrawRLoc, x / 8.0f); |
| glVertexAttrib1f(busyDrawGLoc, y / 8.0f); |
| glVertexAttrib1f(busyDrawBLoc, 0); |
| glVertexAttrib1f(busyDrawALoc, 1); |
| |
| glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); |
| } |
| } |
| ASSERT_GL_NO_ERROR(); |
| |
| // Wait for the other thread to finish with uploads. |
| threadSynchronization.waitForStep(Step::Thread0UploadFinish); |
| |
| // Wait for fence and use all textures in a draw. |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| |
| glUseProgram(textureDrawProgram); |
| for (uint32_t i = 0; i < kTextureCount; ++i) |
| { |
| glActiveTexture(GL_TEXTURE0 + i); |
| glBindTexture(GL_TEXTURE_2D, textures[i]); |
| glUniform1i(textureDrawSamplerLoc[i], i); |
| } |
| glViewport(0, 0, w, h / 2); |
| glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| |
| // Verify results |
| for (uint32_t y = 0; y < 8; ++y) |
| { |
| for (uint32_t x = 0; x < 8; ++x) |
| { |
| int width = w / 8; |
| int height = h / 16; |
| |
| EXPECT_PIXEL_COLOR_NEAR(x * width + width / 2, h - (y * height + height / 2), |
| GLColor(x * 255 / 8, (7 - y) * 255 / 8, 0, 255), 1); |
| } |
| } |
| ASSERT_GL_NO_ERROR(); |
| |
| for (uint32_t x = 0; x < 8; ++x) |
| { |
| // The compressed data is gibberish, just ensure it's not all black. |
| EXPECT_PIXEL_NE(x * w / 8, h / 4, 0, 0, 0, 0); |
| } |
| ASSERT_GL_NO_ERROR(); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0Upload), |
| std::move(thread1Draw), |
| }; |
| |
| RunLockStepThreadsWithSize(getEGLWindow(), kSurfaceWidth, kSurfaceHeight, threadFuncs.size(), |
| threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that calling glUniformBlockBinding on one context affects all contexts. |
| TEST_P(MultithreadingTestES3, UniformBlockBinding) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| constexpr char kVS[] = R"(#version 300 es |
| void main() |
| { |
| vec2 pos = vec2(0.0); |
| switch (gl_VertexID) { |
| case 0: pos = vec2(-1.0, -1.0); break; |
| case 1: pos = vec2(3.0, -1.0); break; |
| case 2: pos = vec2(-1.0, 3.0); break; |
| }; |
| gl_Position = vec4(pos, 0.0, 1.0); |
| })"; |
| constexpr char kFS[] = R"(#version 300 es |
| out mediump vec4 colorOut; |
| |
| layout(std140) uniform buffer { mediump vec4 color; }; |
| |
| void main() |
| { |
| colorOut = color; |
| })"; |
| |
| GLProgram program; |
| GLint uniformBufferIndex; |
| |
| // Sync primitives |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread1Ready, |
| Thread0BindingChanged, |
| Thread1FinishedDrawing, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| // Threads to create programs and draw with different uniform blocks. |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create buffers bound to bindings 1 and 2 |
| constexpr std::array<float, 4> kRed = {1, 0, 0, 1}; |
| constexpr std::array<float, 4> kTransparentGreen = {0, 1, 0, 0}; |
| GLBuffer red, transparentGreen; |
| glBindBuffer(GL_UNIFORM_BUFFER, red); |
| glBufferData(GL_UNIFORM_BUFFER, sizeof(kRed), kRed.data(), GL_STATIC_DRAW); |
| glBindBuffer(GL_UNIFORM_BUFFER, transparentGreen); |
| glBufferData(GL_UNIFORM_BUFFER, sizeof(kTransparentGreen), kTransparentGreen.data(), |
| GL_STATIC_DRAW); |
| |
| glBindBufferBase(GL_UNIFORM_BUFFER, 1, transparentGreen); |
| glBindBufferBase(GL_UNIFORM_BUFFER, 2, red); |
| |
| // Wait for the other thread to set everything up |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Ready)); |
| |
| // Issue a draw call. The buffer should be transparent green now |
| glClearColor(0, 0, 0, 0); |
| glClear(GL_COLOR_BUFFER_BIT); |
| glEnable(GL_BLEND); |
| glBlendFunc(GL_ONE, GL_ONE); |
| |
| glUseProgram(program); |
| glDrawArrays(GL_TRIANGLES, 0, 3); |
| |
| // Change the binding |
| glUniformBlockBinding(program, uniformBufferIndex, 1); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Let the other thread work before any deferred operations for the binding change above are |
| // processed in this context. |
| threadSynchronization.nextStep(Step::Thread0BindingChanged); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1FinishedDrawing)); |
| |
| // Draw again, it should accumulate blue and the buffer should become magenta. |
| glDrawArrays(GL_TRIANGLES, 0, 3); |
| |
| // Verify results |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::yellow); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create buffers bound to bindings 1 and 2 |
| constexpr std::array<float, 4> kBlue = {0, 0, 1, 1}; |
| constexpr std::array<float, 4> kTransparentRed = {1, 0, 0, 0}; |
| GLBuffer blue, transparentRed; |
| glBindBuffer(GL_UNIFORM_BUFFER, blue); |
| glBufferData(GL_UNIFORM_BUFFER, sizeof(kBlue), kBlue.data(), GL_STATIC_DRAW); |
| glBindBuffer(GL_UNIFORM_BUFFER, transparentRed); |
| glBufferData(GL_UNIFORM_BUFFER, sizeof(kTransparentRed), kTransparentRed.data(), |
| GL_STATIC_DRAW); |
| |
| glBindBufferBase(GL_UNIFORM_BUFFER, 1, blue); |
| glBindBufferBase(GL_UNIFORM_BUFFER, 2, transparentRed); |
| |
| // Create the program |
| program.makeRaster(kVS, kFS); |
| glUseProgram(program); |
| uniformBufferIndex = glGetUniformBlockIndex(program, "buffer"); |
| |
| // Configure the buffer binding to binding 2 |
| glUniformBlockBinding(program, uniformBufferIndex, 2); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Issue a draw call. The buffer should be transparent red now |
| glClearColor(0, 0, 0, 0); |
| glClear(GL_COLOR_BUFFER_BIT); |
| glEnable(GL_BLEND); |
| glBlendFunc(GL_ONE, GL_ONE); |
| |
| glDrawArrays(GL_TRIANGLES, 0, 3); |
| |
| // Now that everything is set up, let the other thread continue |
| threadSynchronization.nextStep(Step::Thread1Ready); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0BindingChanged)); |
| |
| // The other thread has changed the binding. Draw again, it should accumulate blue and the |
| // buffer should become magenta. |
| glDrawArrays(GL_TRIANGLES, 0, 3); |
| |
| // Verify results |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::magenta); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Tell the other thread to finish up. |
| threadSynchronization.nextStep(Step::Thread1FinishedDrawing); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that observers are notified of a change in foveation state of a texture |
| TEST_P(MultithreadingTestES3, SharedFoveatedTexture) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_QCOM_texture_foveated")); |
| |
| // Shared texture |
| GLTexture texture; |
| |
| // Sync primitives |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0Draw, |
| Thread1Draw, |
| Thread0ConfiguredTextureFoveation, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| // Thread to configure texture foveation. |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Create non-foveated framebuffer and attach shared texture as color attachment |
| GLFramebuffer framebuffer; |
| glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); |
| glActiveTexture(GL_TEXTURE0); |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, getWindowWidth(), getWindowHeight(), 0, GL_RGBA, |
| GL_UNSIGNED_BYTE, nullptr); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| EXPECT_GL_NO_ERROR(); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| ASSERT_GL_NO_ERROR(); |
| EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); |
| |
| // Render before configuring foveation on the texture |
| ANGLE_GL_PROGRAM(greenProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); |
| glUseProgram(greenProgram); |
| |
| // Draw |
| drawQuad(greenProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| EXPECT_GL_NO_ERROR(); |
| |
| // Verify results |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Thread0Draw); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Draw)); |
| |
| // Configure foveation for the texture |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_FOVEATED_FEATURE_BITS_QCOM, |
| GL_FOVEATION_ENABLE_BIT_QCOM); |
| EXPECT_GL_NO_ERROR(); |
| glTextureFoveationParametersQCOM(texture, 0, 0, 0.0f, 0.0f, 8.0f, 8.0f, 0.0f); |
| EXPECT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Thread0ConfiguredTextureFoveation); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Draw)); |
| |
| // Create non-foveated framebuffer and attach shared texture as color attachment |
| GLFramebuffer framebuffer; |
| glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); |
| glActiveTexture(GL_TEXTURE0); |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| ASSERT_GL_NO_ERROR(); |
| EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); |
| |
| // Render before configuring foveation on the texture |
| ANGLE_GL_PROGRAM(redProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); |
| glUseProgram(redProgram); |
| |
| // Draw |
| drawQuad(redProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| EXPECT_GL_NO_ERROR(); |
| |
| // Verify results |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Thread1Draw); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0ConfiguredTextureFoveation)); |
| |
| // Render after texture foveation was configured |
| ANGLE_GL_PROGRAM(blueProgram, essl1_shaders::vs::Simple(), essl1_shaders::fs::Blue()); |
| glUseProgram(blueProgram); |
| |
| // Draw |
| drawQuad(blueProgram, essl1_shaders::PositionAttrib(), 0.5f); |
| EXPECT_GL_NO_ERROR(); |
| |
| // Verify results |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test GL_EXT_sRGB_write_control works as expected when multiple contexts are used |
| TEST_P(MultithreadingTestES3, SharedSrgbTextureMultipleContexts) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_sRGB_write_control")); |
| |
| constexpr angle::GLColor encodedToSrgbColor(64, 127, 191, 255); |
| constexpr angle::GLColor inputColor(13, 54, 133, 255); |
| |
| EGLWindow *window = getEGLWindow(); |
| EGLDisplay dpy = window->getDisplay(); |
| EGLContext context1 = window->createContext(EGL_NO_CONTEXT, nullptr); |
| EGLContext context2 = window->createContext(context1, nullptr); |
| |
| // Shared texture |
| GLTexture texture; |
| |
| // Shared program |
| GLuint program; |
| |
| // Sync primitives |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0Draw1, |
| Thread1Draw1, |
| Thread0Draw2, |
| Thread1Draw2, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| // Thread0 rendering to shared texture. |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context1)); |
| EXPECT_EGL_SUCCESS(); |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA_EXT, 1, 1, 0, GL_SRGB_ALPHA_EXT, |
| GL_UNSIGNED_BYTE, nullptr); |
| |
| GLFramebuffer framebuffer; |
| glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| program = CompileProgram(essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); |
| ASSERT_NE(0u, program); |
| |
| GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); |
| ASSERT_NE(-1, colorLocation); |
| |
| glUseProgram(program); |
| glUniform4fv(colorLocation, 1, inputColor.toNormalizedVector().data()); |
| |
| glDisable(GL_FRAMEBUFFER_SRGB_EXT); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); |
| EXPECT_PIXEL_COLOR_NEAR(0, 0, inputColor, 1.0); |
| |
| threadSynchronization.nextStep(Step::Thread0Draw1); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Draw1)); |
| |
| glEnable(GL_FRAMEBUFFER_SRGB_EXT); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); |
| EXPECT_PIXEL_COLOR_NEAR(0, 0, encodedToSrgbColor, 1.0); |
| |
| threadSynchronization.nextStep(Step::Thread0Draw2); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Draw2)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| // Thread1 rendering to shared texture. |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context2)); |
| EXPECT_EGL_SUCCESS(); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Draw1)); |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| GLFramebuffer framebuffer; |
| glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); |
| |
| ASSERT_NE(0u, program); |
| GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform()); |
| ASSERT_NE(-1, colorLocation); |
| |
| glUseProgram(program); |
| glUniform4fv(colorLocation, 1, inputColor.toNormalizedVector().data()); |
| |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); |
| EXPECT_PIXEL_COLOR_NEAR(0, 0, encodedToSrgbColor, 1.0); |
| |
| threadSynchronization.nextStep(Step::Thread1Draw1); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Draw2)); |
| |
| glBindTexture(GL_TEXTURE_2D, texture); |
| glDisable(GL_FRAMEBUFFER_SRGB_EXT); |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); |
| EXPECT_PIXEL_COLOR_NEAR(0, 0, inputColor, 1.0); |
| |
| threadSynchronization.nextStep(Step::Thread1Draw2); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); |
| EXPECT_EGL_SUCCESS(); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| // Cleanup |
| EXPECT_EGL_TRUE(eglDestroyContext(dpy, context1)); |
| EXPECT_EGL_TRUE(eglDestroyContext(dpy, context2)); |
| EXPECT_EGL_SUCCESS(); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that a program linked in one context can be bound in another context while link may be |
| // happening in parallel. |
| TEST_P(MultithreadingTest, ProgramLinkAndBind) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| GLuint vs; |
| GLuint redfs; |
| GLuint greenfs; |
| |
| GLuint program; |
| |
| // Sync primitives |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread1Ready, |
| Thread0ProgramLinked, |
| Thread1FinishedDrawing, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| // Threads to create programs and draw. |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| // Wait for thread 1 to bind the program before linking it |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Ready)); |
| |
| glUseProgram(program); |
| |
| // Link a program, but don't resolve link. |
| glDetachShader(program, greenfs); |
| glAttachShader(program, redfs); |
| glLinkProgram(program); |
| |
| // Let the other thread bind and use the program. |
| threadSynchronization.nextStep(Step::Thread0ProgramLinked); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1FinishedDrawing)); |
| |
| // Draw in this context too |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0); |
| |
| // Verify results |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| vs = CompileShader(GL_VERTEX_SHADER, essl1_shaders::vs::Simple()); |
| redfs = CompileShader(GL_FRAGMENT_SHADER, essl1_shaders::fs::Red()); |
| greenfs = CompileShader(GL_FRAGMENT_SHADER, essl1_shaders::fs::Green()); |
| program = glCreateProgram(); |
| |
| glAttachShader(program, vs); |
| glAttachShader(program, greenfs); |
| glLinkProgram(program); |
| ASSERT_NE(CheckLinkStatusAndReturnProgram(program, true), 0u); |
| |
| // Bind the program before it's relinked. Otherwise the program is resolved before the |
| // binding happens. |
| glUseProgram(program); |
| |
| threadSynchronization.nextStep(Step::Thread1Ready); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0ProgramLinked)); |
| |
| // Unbind and rebind for extra testing |
| glUseProgram(0); |
| glUseProgram(program); |
| |
| // Issue a draw call |
| drawQuad(program, essl1_shaders::PositionAttrib(), 0); |
| |
| // Verify results |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); |
| ASSERT_GL_NO_ERROR(); |
| |
| // Tell the other thread to finish up. |
| threadSynchronization.nextStep(Step::Thread1FinishedDrawing); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that two contexts in share group can generate, delete and bind buffers for themselves in |
| // parallel. |
| TEST_P(MultithreadingTestES3, SimultaneousBufferBindAndGen) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| constexpr char kFS[] = R"(#version 300 es |
| precision mediump float; |
| |
| layout(std140) uniform Block |
| { |
| vec4 colorIn; |
| }; |
| |
| out vec4 color; |
| |
| void main() |
| { |
| color = colorIn; |
| })"; |
| |
| constexpr int kSurfaceWidth = 32; |
| constexpr int kSurfaceHeight = 128; |
| |
| // Sync primitives |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0Ready, |
| Thread1Ready, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto threadFunc = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context, uint32_t index) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS); |
| |
| // Make sure the two threads start work around the same time |
| if (index == 0) |
| { |
| threadSynchronization.nextStep(Step::Thread0Ready); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Ready)); |
| } |
| else |
| { |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Ready)); |
| threadSynchronization.nextStep(Step::Thread1Ready); |
| } |
| |
| std::vector<GLuint> buffers(kSurfaceWidth * kSurfaceHeight); |
| |
| glEnable(GL_SCISSOR_TEST); |
| for (int y = 0; y < kSurfaceHeight; ++y) |
| { |
| for (int x = 0; x < kSurfaceWidth; ++x) |
| { |
| GLuint &buffer = buffers[y * kSurfaceWidth + x]; |
| const float bufferData[4] = { |
| ((y * kSurfaceWidth + x + index * 100) % 255) / 255.0f, |
| ((y * kSurfaceWidth + x + index * 100 + 1) % 255) / 255.0f, |
| ((y * kSurfaceWidth + x + index * 100 + 2) % 255) / 255.0f, |
| ((y * kSurfaceWidth + x + index * 100 + 3) % 255) / 255.0f, |
| }; |
| |
| // Generate one buffer per pixel and shade the pixel with it. |
| glGenBuffers(1, &buffer); |
| glBindBuffer(GL_UNIFORM_BUFFER, buffers[y * kSurfaceWidth + x]); |
| glBufferData(GL_UNIFORM_BUFFER, sizeof(bufferData), bufferData, GL_STATIC_DRAW); |
| glBindBufferBase(GL_UNIFORM_BUFFER, 0, buffer); |
| |
| glScissor(x, y, 1, 1); |
| drawQuad(program, essl3_shaders::PositionAttrib(), 0); |
| |
| if ((x + y) % 2 == 0) |
| { |
| glDeleteBuffers(1, &buffer); |
| buffer = 0; |
| } |
| } |
| } |
| |
| // Verify the results |
| auto verify = [&](int x, int y) { |
| const GLColor expect((y * kSurfaceWidth + x + index * 100) % 255, |
| (y * kSurfaceWidth + x + index * 100 + 1) % 255, |
| (y * kSurfaceWidth + x + index * 100 + 2) % 255, |
| (y * kSurfaceWidth + x + index * 100 + 3) % 255); |
| EXPECT_PIXEL_COLOR_EQ(x, y, expect); |
| }; |
| |
| verify(0, 0); |
| verify(0, kSurfaceHeight - 1); |
| verify(kSurfaceWidth - 1, 0); |
| verify(kSurfaceWidth - 1, kSurfaceHeight - 1); |
| verify(kSurfaceWidth / 2, kSurfaceHeight / 2); |
| ASSERT_GL_NO_ERROR(); |
| |
| if (index == 0) |
| { |
| threadSynchronization.nextStep(Step::Finish); |
| } |
| else |
| { |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| } |
| }; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| threadFunc(dpy, surface, context, 0); |
| }; |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| threadFunc(dpy, surface, context, 1); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreadsWithSize(getEGLWindow(), kSurfaceWidth, kSurfaceHeight, threadFuncs.size(), |
| threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| |
| // Test that ref counting is thread-safe when the same buffer is used in multiple threads. |
| TEST_P(MultithreadingTestES3, SimultaneousBufferBind) |
| { |
| ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading()); |
| |
| constexpr char kFS[] = R"(#version 300 es |
| precision mediump float; |
| |
| layout(std140) uniform Block |
| { |
| vec4 colorIn; |
| }; |
| |
| out vec4 color; |
| |
| void main() |
| { |
| color = colorIn; |
| })"; |
| |
| constexpr int kSurfaceWidth = 32; |
| constexpr int kSurfaceHeight = 128; |
| |
| GLuint buffer; |
| GLsync sync = nullptr; |
| |
| // Sync primitives |
| std::mutex mutex; |
| std::condition_variable condVar; |
| |
| enum class Step |
| { |
| Start, |
| Thread0Ready, |
| Thread1Ready, |
| Finish, |
| Abort, |
| }; |
| Step currentStep = Step::Start; |
| |
| auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS); |
| |
| // Create the buffer in this context |
| glGenBuffers(1, &buffer); |
| |
| constexpr float kBufferData[4] = { |
| 10.0f / 255.0f, |
| 50.0f / 255.0f, |
| 130.0f / 255.0f, |
| 220.0f / 255.0f, |
| }; |
| glBindBuffer(GL_UNIFORM_BUFFER, buffer); |
| glBufferData(GL_UNIFORM_BUFFER, sizeof(kBufferData), kBufferData, GL_STATIC_DRAW); |
| |
| sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); |
| |
| // Make sure the two threads start work around the same time |
| threadSynchronization.nextStep(Step::Thread0Ready); |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1Ready)); |
| |
| // Bind and unbind the buffer many times. If ref counting is not thread safe, chances are |
| // the ref count would be incorrect in the end. This can result in the buffer prematurely |
| // getting deleted. |
| for (uint32_t i = 0; i < 8000; ++i) |
| { |
| glBindBuffer(GL_UNIFORM_BUFFER, i % 2 == 0 ? 0 : buffer); |
| } |
| ASSERT_GL_NO_ERROR(); |
| |
| threadSynchronization.nextStep(Step::Finish); |
| }; |
| auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { |
| ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); |
| EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); |
| |
| ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Ready)); |
| threadSynchronization.nextStep(Step::Thread1Ready); |
| |
| glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); |
| |
| // Bind and unbind the buffer many times. |
| for (uint32_t i = 0; i < 4000; ++i) |
| { |
| glBindBuffer(GL_UNIFORM_BUFFER, i % 2 == 0 ? buffer : 0); |
| } |
| |
| // Draw with it to make sure buffer is still valid and not accidentally deleted due to bad |
| // ref counting. |
| glBindBuffer(GL_UNIFORM_BUFFER, buffer); |
| glBindBufferBase(GL_UNIFORM_BUFFER, 0, buffer); |
| drawQuad(program, essl3_shaders::PositionAttrib(), 0); |
| |
| // Verify the results |
| const GLColor expect(10, 50, 130, 220); |
| EXPECT_PIXEL_RECT_EQ(0, 0, kSurfaceWidth, kSurfaceHeight, expect); |
| ASSERT_GL_NO_ERROR(); |
| |
| ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); |
| }; |
| |
| std::array<LockStepThreadFunc, 2> threadFuncs = { |
| std::move(thread0), |
| std::move(thread1), |
| }; |
| |
| RunLockStepThreadsWithSize(getEGLWindow(), kSurfaceWidth, kSurfaceHeight, threadFuncs.size(), |
| threadFuncs.data()); |
| |
| ASSERT_NE(currentStep, Step::Abort); |
| } |
| ANGLE_INSTANTIATE_TEST( |
| MultithreadingTest, |
| ES2_METAL(), |
| ES3_METAL(), |
| ES2_OPENGL(), |
| ES3_OPENGL(), |
| ES2_OPENGLES(), |
| ES3_OPENGLES(), |
| ES3_VULKAN(), |
| ES3_VULKAN_SWIFTSHADER().enable(Feature::PreferMonolithicPipelinesOverLibraries), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries) |
| .enable(Feature::SlowDownMonolithicPipelineCreationForTesting), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries) |
| .disable(Feature::MergeProgramPipelineCachesToGlobalCache), |
| ES3_VULKAN_SWIFTSHADER().enable(Feature::PermanentlySwitchToFramebufferFetchMode), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PermanentlySwitchToFramebufferFetchMode) |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PermanentlySwitchToFramebufferFetchMode) |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries) |
| .enable(Feature::SlowDownMonolithicPipelineCreationForTesting), |
| ES2_D3D11(), |
| ES3_D3D11()); |
| |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MultithreadingTestES3); |
| ANGLE_INSTANTIATE_TEST( |
| MultithreadingTestES3, |
| ES3_OPENGL(), |
| ES3_OPENGLES(), |
| ES3_VULKAN(), |
| ES3_VULKAN_SWIFTSHADER().enable(Feature::PreferMonolithicPipelinesOverLibraries), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries) |
| .enable(Feature::SlowDownMonolithicPipelineCreationForTesting), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries) |
| .disable(Feature::MergeProgramPipelineCachesToGlobalCache), |
| ES3_VULKAN_SWIFTSHADER().enable(Feature::PermanentlySwitchToFramebufferFetchMode), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PermanentlySwitchToFramebufferFetchMode) |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries), |
| ES3_VULKAN_SWIFTSHADER() |
| .enable(Feature::PermanentlySwitchToFramebufferFetchMode) |
| .enable(Feature::PreferMonolithicPipelinesOverLibraries) |
| .enable(Feature::SlowDownMonolithicPipelineCreationForTesting), |
| ES3_D3D11()); |
| |
| } // namespace angle |