diff --git a/MobileGL/MG_Test/Wire/ConcurrentContextsTest.cpp b/MobileGL/MG_Test/Wire/ConcurrentContextsTest.cpp index 0e26a412..df94b162 100644 --- a/MobileGL/MG_Test/Wire/ConcurrentContextsTest.cpp +++ b/MobileGL/MG_Test/Wire/ConcurrentContextsTest.cpp @@ -13,10 +13,17 @@ // a context and a textured draw of its own colour, and reads it back over and over - through the // exported EGL and GL entry points, which is where the client serialises its threads. // -// DISABLED: KNOWN TO FAIL. With six threads, a thread's read-backs come back untouched once another -// thread has released and destroyed its context (both shapes, most runs), and a run sometimes hangs -// until the client is killed. On the phone an unshared thread can also read back nothing from its -// first frame. Run it with --gtest_also_run_disabled_tests while working on that. +// Every shape below has one rule in common: a thread's records apply under ITS context, whatever the +// other threads bind, release or destroy meanwhile. The shapes differ in what the other threads do: +// - each thread destroys its context when it is done (shared root, and one group per thread); +// - threads release and re-bind in the middle of their run and release without destroying at +// the end (the destroy comes after every thread has joined); +// - siblings of one share group with no root: the group's first context is destroyed while the +// others still draw with objects of the group; +// - a context destroyed by ANOTHER thread while it is current (EGL defers the destroy until it +// is released: the thread keeps drawing, and the context is gone once released); +// - one thread reading back without pause while others create, draw in, release and destroy +// contexts as fast as they can. #include "P12ServerRig.h" @@ -29,6 +36,7 @@ void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); void glClear(GLbitfield mask); void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels); void glGenTextures(GLsizei n, GLuint* textures); +void glDeleteTextures(GLsizei n, const GLuint* textures); void glBindTexture(GLenum target, GLuint texture); void glActiveTexture(GLenum texture); void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, @@ -59,6 +67,18 @@ namespace { constexpr int kThreads = 6; constexpr int kFrames = 40; + // The reader of the read-back race, and the frames it reads. + constexpr int kRaceChurners = 3; + constexpr int kRaceReaderFrames = 300; + + enum class Shape : Int32 { + SharedRoot, // every thread shares with one root context, destroys its own when done + Unshared, // one share group per thread, each destroys its own when done + ReleaseOnly, // one group per thread; release + re-bind mid-run, release (no destroy) at the end + Siblings, // one group, no root: the group's first context is destroyed while the rest draw + DestroyWhileCurrent, // another thread destroys a context while it is current (deferred by EGL) + ReadbackRace, // one reader without pause, the rest churn create/draw/release/destroy + }; struct ThreadReport { Int32 current = 0; @@ -66,29 +86,73 @@ namespace { Int32 good = 0; Int32 bad = 0; Uint32 firstBad = 0; + Uint32 firstExpected = 0; Int32 firstBadFrame = -1; Int32 vertexCompiled = -1; Int32 fragmentCompiled = -1; + Int32 rebinds = 0; // ReleaseOnly: mid-run release + make-current that succeeded + Int32 cycles = 0; // ReadbackRace churners: contexts created, used and destroyed + Int32 cycleFailures = 0; + // DestroyWhileCurrent: what eglDestroyContext answered on the OTHER thread, and what + // binding the destroyed context again after this thread released it answered. + Int32 destroyAnswer = -1; + Int32 destroyError = 0; + Int32 rebindAnswer = -1; + Int32 rebindError = 0; + Int32 destroyWaited = 0; // the destroy happened while this thread was current char linkLog[200] = {}; }; struct Report { Int32 initialized = 0; + Int32 finished = 0; ThreadReport threads[kThreads]; }; // A<<24|B<<16|G<<8|R of the texel each thread draws. - Uint32 ColourOf(int index) { return 0xFF000000u | (0x40u + 0x50u * static_cast(index)) << 8 | 0x20u; } + Uint32 ColourOf(int index) { return 0xFF000000u | (0x40u + 0x50u * static_cast(index % 3)) << 8 | + (0x20u + 0x30u * static_cast(index / 3)); } + // The clear colour of a churn cycle (ReadbackRace): a different one per thread and cycle. + Uint32 ChurnColourOf(int index, int cycle) { + return 0xFF000000u | (0x10u * static_cast(index)) << 16 | (static_cast(cycle * 7) & 0xFFu) << 8 | + 0x80u; + } - void RenderOnThread(EGLDisplay dpy, EGLConfig config, EGLContext share, int index, ThreadReport* out) { - const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE}; - const EGLSurface surface = eglCreatePbufferSurface(dpy, config, pbufferAttribs); - const EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; - const EGLContext context = eglCreateContext(dpy, config, share, attribs); - if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT || - eglMakeCurrent(dpy, surface, surface, context) != EGL_TRUE) - return; - out->current = 1; + struct Rig { + EGLDisplay dpy = EGL_NO_DISPLAY; + EGLConfig config = nullptr; + Shape shape = Shape::Unshared; + Report* report = nullptr; + EGLContext share = EGL_NO_CONTEXT; // SharedRoot's root + EGLContext given[kThreads] = {}; // contexts made on the main thread (Siblings) + std::atomic published[kThreads] = {}; // DestroyWhileCurrent: each thread's own + std::atomic halfway[kThreads] = {}; + std::atomic destroyed[kThreads] = {}; + std::atomic readerDone{0}; + }; + + const EGLint kContextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; + const EGLint kPbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE}; + + template + bool WaitUntil(Predicate predicate, int timeoutMs) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; + } + + struct Drawing { + GLuint program = 0; + GLuint vertexArray = 0; + GLuint texture = 0; + Uint32 colour = 0; + }; + + // The thread's program, vertex array and one-texel texture of its own colour. + Drawing BuildDrawing(int index, ThreadReport* out) { static const char* kVertex = "#version 330 core\n" "const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n" "void main() { gl_Position = vec4(corners[gl_VertexID], 0.0, 1.0); }\n"; @@ -96,60 +160,225 @@ namespace { "uniform sampler2D image;\n" "out vec4 color;\n" "void main() { color = texture(image, vec2(0.5)); }\n"; + Drawing drawing; const GLuint vertex = glCreateShader(GL_VERTEX_SHADER); const GLuint fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(vertex, 1, &kVertex, nullptr); glShaderSource(fragment, 1, &kFragment, nullptr); glCompileShader(vertex); glCompileShader(fragment); - const GLuint program = glCreateProgram(); - glAttachShader(program, vertex); - glAttachShader(program, fragment); - glLinkProgram(program); + drawing.program = glCreateProgram(); + glAttachShader(drawing.program, vertex); + glAttachShader(drawing.program, fragment); + glLinkProgram(drawing.program); GLint linked = 0; - glGetProgramiv(program, GL_LINK_STATUS, &linked); + glGetProgramiv(drawing.program, GL_LINK_STATUS, &linked); out->linked = linked; glGetShaderiv(vertex, GL_COMPILE_STATUS, &out->vertexCompiled); glGetShaderiv(fragment, GL_COMPILE_STATUS, &out->fragmentCompiled); - if (!linked) glGetProgramInfoLog(program, sizeof(out->linkLog) - 1, nullptr, out->linkLog); - GLuint vertexArray = 0; - glGenVertexArrays(1, &vertexArray); - GLuint texture = 0; - glGenTextures(1, &texture); - const Uint32 colour = ColourOf(index); + if (!linked) glGetProgramInfoLog(drawing.program, sizeof(out->linkLog) - 1, nullptr, out->linkLog); + glGenVertexArrays(1, &drawing.vertexArray); + glGenTextures(1, &drawing.texture); + drawing.colour = ColourOf(index); glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, texture); + glBindTexture(GL_TEXTURE_2D, drawing.texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &colour); - for (int frame = 0; frame < kFrames; ++frame) { - glViewport(0, 0, 8, 8); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - glUseProgram(program); - glUniform1i(glGetUniformLocation(program, "image"), 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, texture); - glBindVertexArray(vertexArray); - glDrawArrays(GL_TRIANGLES, 0, 3); - Uint32 pixel = 0x5A5A5A5Au; - glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel); - if (pixel == colour) { - ++out->good; - } else { - if (out->bad++ == 0) { - out->firstBad = pixel; - out->firstBadFrame = frame; - } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &drawing.colour); + return drawing; + } + + void Tally(ThreadReport* out, int frame, Uint32 pixel, Uint32 expected) { + if (pixel == expected) { + ++out->good; + return; + } + if (out->bad++ == 0) { + out->firstBad = pixel; + out->firstExpected = expected; + out->firstBadFrame = frame; + } + } + + // One frame: clear to transparent black, draw the thread's texel over the whole surface, read + // one pixel back. + void DrawFrame(const Drawing& drawing, int frame, ThreadReport* out) { + glViewport(0, 0, 8, 8); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + glUseProgram(drawing.program); + glUniform1i(glGetUniformLocation(drawing.program, "image"), 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, drawing.texture); + glBindVertexArray(drawing.vertexArray); + glDrawArrays(GL_TRIANGLES, 0, 3); + Uint32 pixel = 0x5A5A5A5Au; + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel); + Tally(out, frame, pixel, drawing.colour); + } + + // The frames a thread draws: staggered, so the threads finish (and tear down) one after another + // while the rest are still drawing. + int FramesOf(const Rig& rig, int index) { + if (rig.shape == Shape::Siblings) return index == 0 ? kFrames / 4 : kFrames + 4 * index; + return kFrames + 6 * index; + } + + void RenderOnThread(Rig* rig, int index) { + ThreadReport* out = &rig->report->threads[index]; + const EGLDisplay dpy = rig->dpy; + const EGLSurface surface = eglCreatePbufferSurface(dpy, rig->config, kPbufferAttribs); + EGLContext context = EGL_NO_CONTEXT; + switch (rig->shape) { + case Shape::SharedRoot: context = eglCreateContext(dpy, rig->config, rig->share, kContextAttribs); break; + case Shape::Siblings: context = rig->given[index]; break; + default: context = eglCreateContext(dpy, rig->config, EGL_NO_CONTEXT, kContextAttribs); break; + } + if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT || + eglMakeCurrent(dpy, surface, surface, context) != EGL_TRUE) { + if (rig->shape == Shape::DestroyWhileCurrent) rig->published[index].store(EGL_NO_CONTEXT); + rig->halfway[index].store(1); + return; + } + out->current = 1; + rig->published[index].store(context); + const Drawing drawing = BuildDrawing(index, out); + const int frames = FramesOf(*rig, index); + for (int frame = 0; frame < frames; ++frame) { + if (rig->shape == Shape::DestroyWhileCurrent && frame == frames / 2) { + // Halfway: let the main thread destroy this context while it is current here, and + // draw the second half in it all the same. + rig->halfway[index].store(1); + out->destroyWaited = WaitUntil([&] { return rig->destroyed[index].load() != 0; }, 20000) ? 1 : 0; } + if (rig->shape == Shape::ReleaseOnly && frame > 0 && frame % 8 == 0) { + // Release and bind again in the middle of the run: nothing is destroyed, and this + // thread's state must be just as it was. + if (eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) == EGL_TRUE && + eglMakeCurrent(dpy, surface, surface, context) == EGL_TRUE) + ++out->rebinds; + } + DrawFrame(drawing, frame, out); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } (void)eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - (void)eglDestroyContext(dpy, context); + switch (rig->shape) { + case Shape::ReleaseOnly: + break; // destroyed by the main thread once every thread has joined + case Shape::DestroyWhileCurrent: + // Released, so the destroy the main thread asked for has happened: the handle is gone. + out->rebindAnswer = eglMakeCurrent(dpy, surface, surface, context); + out->rebindError = eglGetError(); + if (out->rebindAnswer == EGL_TRUE) (void)eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + break; + default: + (void)eglDestroyContext(dpy, context); + break; + } (void)eglDestroySurface(dpy, surface); } - Report RunClient(const ServerProcess& server, const std::string& logBase, bool shareRoot) { + // ReadbackRace's reader: one context, no pause between frames, until it has read them all. + void ReadWithoutPause(Rig* rig, int index) { + ThreadReport* out = &rig->report->threads[index]; + const EGLSurface surface = eglCreatePbufferSurface(rig->dpy, rig->config, kPbufferAttribs); + const EGLContext context = eglCreateContext(rig->dpy, rig->config, EGL_NO_CONTEXT, kContextAttribs); + if (surface != EGL_NO_SURFACE && context != EGL_NO_CONTEXT && + eglMakeCurrent(rig->dpy, surface, surface, context) == EGL_TRUE) { + out->current = 1; + const Drawing drawing = BuildDrawing(index, out); + for (int frame = 0; frame < kRaceReaderFrames; ++frame) DrawFrame(drawing, frame, out); + (void)eglMakeCurrent(rig->dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + (void)eglDestroyContext(rig->dpy, context); + } + if (surface != EGL_NO_SURFACE) (void)eglDestroySurface(rig->dpy, surface); + rig->readerDone.store(1); + } + + // ReadbackRace's churners: create a context of its own, make it current, give it objects (a + // texture, deleted every other cycle and otherwise left to die with the context, and a vertex + // array), clear and read back, release and destroy it. Until the reader is done. + void ChurnContexts(Rig* rig, int index) { + ThreadReport* out = &rig->report->threads[index]; + const EGLSurface surface = eglCreatePbufferSurface(rig->dpy, rig->config, kPbufferAttribs); + if (surface == EGL_NO_SURFACE) return; + out->current = 1; + out->linked = 1; + for (int cycle = 0; rig->readerDone.load() == 0 && cycle < 400; ++cycle) { + const EGLContext context = eglCreateContext(rig->dpy, rig->config, EGL_NO_CONTEXT, kContextAttribs); + if (context == EGL_NO_CONTEXT || eglMakeCurrent(rig->dpy, surface, surface, context) != EGL_TRUE) { + ++out->cycleFailures; + if (context != EGL_NO_CONTEXT) (void)eglDestroyContext(rig->dpy, context); + continue; + } + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + const Uint32 texel = ChurnColourOf(index, cycle); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &texel); + GLuint vertexArray = 0; + glGenVertexArrays(1, &vertexArray); + glBindVertexArray(vertexArray); + const Uint32 colour = ChurnColourOf(index, cycle); + glViewport(0, 0, 8, 8); + glClearColor(static_cast(colour & 0xFFu) / 255.0f, static_cast((colour >> 8) & 0xFFu) / 255.0f, + static_cast((colour >> 16) & 0xFFu) / 255.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + Uint32 pixel = 0x5A5A5A5Au; + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel); + Tally(out, cycle, pixel, colour); + if (cycle % 2 == 0) glDeleteTextures(1, &texture); // the odd ones die with the context + (void)eglMakeCurrent(rig->dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + (void)eglDestroyContext(rig->dpy, context); + ++out->cycles; + } + (void)eglDestroySurface(rig->dpy, surface); + } + + void RunShape(Rig& rig) { + std::thread threads[kThreads]; + if (rig.shape == Shape::ReadbackRace) { + threads[0] = std::thread(ReadWithoutPause, &rig, 0); + for (int i = 1; i <= kRaceChurners; ++i) threads[i] = std::thread(ChurnContexts, &rig, i); + for (int i = 0; i <= kRaceChurners; ++i) threads[i].join(); + return; + } + if (rig.shape == Shape::SharedRoot) { + rig.share = eglCreateContext(rig.dpy, rig.config, EGL_NO_CONTEXT, kContextAttribs); + } + if (rig.shape == Shape::Siblings) { + // The group's first context, and the rest sharing with it; nothing else holds the group. + rig.given[0] = eglCreateContext(rig.dpy, rig.config, EGL_NO_CONTEXT, kContextAttribs); + for (int i = 1; i < kThreads; ++i) + rig.given[i] = eglCreateContext(rig.dpy, rig.config, rig.given[0], kContextAttribs); + } + for (int i = 0; i < kThreads; ++i) threads[i] = std::thread(RenderOnThread, &rig, i); + if (rig.shape == Shape::DestroyWhileCurrent) { + // Each context is destroyed from HERE while its own thread still has it current. EGL + // answers EGL_TRUE and destroys it once that thread releases it. + for (int i = 0; i < kThreads; ++i) { + ThreadReport& t = rig.report->threads[i]; + if (WaitUntil([&] { return rig.halfway[i].load() != 0; }, 30000)) { + const EGLContext context = rig.published[i].load(); + if (context != EGL_NO_CONTEXT) { + t.destroyAnswer = eglDestroyContext(rig.dpy, context); + t.destroyError = eglGetError(); + } + } + rig.destroyed[i].store(1); + } + } + for (auto& thread : threads) thread.join(); + if (rig.shape == Shape::ReleaseOnly) { + for (int i = 0; i < kThreads; ++i) { + const EGLContext context = rig.published[i].load(); + if (context != EGL_NO_CONTEXT) (void)eglDestroyContext(rig.dpy, context); + } + } + if (rig.share != EGL_NO_CONTEXT) (void)eglDestroyContext(rig.dpy, rig.share); + } + + Report RunClient(const ServerProcess& server, const std::string& logBase, Shape shape) { Report report{}; int fds[2] = {-1, -1}; if (::pipe(fds) != 0) return report; @@ -162,27 +391,23 @@ namespace { ::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1); ::unsetenv("MOBILEGL_IPC_SURFACE"); ::unsetenv("MOBILEGL_BACKEND_TYPE"); - Report r{}; - const EGLDisplay dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY); + static Report r{}; + static Rig rig; + rig.shape = shape; + rig.report = &r; + rig.dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY); EGLint major = 0; EGLint minor = 0; - if (dpy != EGL_NO_DISPLAY && eglInitialize(dpy, &major, &minor) == EGL_TRUE) { + if (rig.dpy != EGL_NO_DISPLAY && eglInitialize(rig.dpy, &major, &minor) == EGL_TRUE) { (void)eglBindAPI(EGL_OPENGL_API); const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_NONE}; - EGLConfig config = nullptr; EGLint count = 0; - if (eglChooseConfig(dpy, configAttribs, &config, 1, &count) == EGL_TRUE && count > 0) { + if (eglChooseConfig(rig.dpy, configAttribs, &rig.config, 1, &count) == EGL_TRUE && count > 0) { r.initialized = 1; - const EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; - const EGLContext share = - shareRoot ? eglCreateContext(dpy, config, EGL_NO_CONTEXT, attribs) : EGL_NO_CONTEXT; - std::thread threads[kThreads]; - for (int i = 0; i < kThreads; ++i) - threads[i] = std::thread(RenderOnThread, dpy, config, share, i, &r.threads[i]); - for (auto& thread : threads) thread.join(); - if (share != EGL_NO_CONTEXT) (void)eglDestroyContext(dpy, share); + RunShape(rig); + r.finished = 1; } - (void)eglTerminate(dpy); + (void)eglTerminate(rig.dpy); } const ssize_t wrote = ::write(fds[1], &r, sizeof(r)); (void)wrote; @@ -195,10 +420,10 @@ namespace { return report; } pollfd pfd{fds[0], POLLIN, 0}; - if (::poll(&pfd, 1, 90000) > 0) { + if (::poll(&pfd, 1, 60000) > 0) { if (::read(fds[0], &report, sizeof(report)) != static_cast(sizeof(report))) report = Report{}; } else { - ::kill(pid, SIGKILL); + ::kill(pid, SIGKILL); // a hang: report.initialized stays 0 and the case says so } ::close(fds[0]); int status = 0; @@ -206,28 +431,67 @@ namespace { return report; } - void Check(bool shareRoot) { + const char* NameOf(Shape shape) { + switch (shape) { + case Shape::SharedRoot: return "concurrent-shared"; + case Shape::Unshared: return "concurrent-unshared"; + case Shape::ReleaseOnly: return "concurrent-release"; + case Shape::Siblings: return "concurrent-siblings"; + case Shape::DestroyWhileCurrent: return "concurrent-destroy-current"; + case Shape::ReadbackRace: return "concurrent-race"; + } + return "concurrent"; + } + + void Check(Shape shape) { ::signal(SIGPIPE, SIG_IGN); ServerProcess server; - const std::string label = shareRoot ? "concurrent-shared" : "concurrent-unshared"; + const std::string label = NameOf(shape); ASSERT_TRUE(LaunchUnixInProcessServer(&server, label)) << server.Log(); const std::string logBase = "/tmp/mgl-" + label + "-client-" + std::to_string(::getpid()) + ".log"; Debug::TruncateRoleLogs(logBase.c_str()); - const Report r = RunClient(server, logBase, shareRoot); - ASSERT_EQ(r.initialized, 1) << server.Log(); - for (int i = 0; i < kThreads; ++i) { + const Report r = RunClient(server, logBase, shape); + ASSERT_EQ(r.initialized, 1) << "the client hung or did not come up; client log " << logBase << "\n" + << server.Log(); + ASSERT_EQ(r.finished, 1) << "the client did not finish"; + const int threads = shape == Shape::ReadbackRace ? 1 + kRaceChurners : kThreads; + for (int i = 0; i < threads; ++i) { const ThreadReport& t = r.threads[i]; if (t.current == 0) GTEST_SKIP() << "a context did not go current (no headless EGL here?)"; EXPECT_EQ(t.linked, 1) << "thread " << i << " (compiled " << t.vertexCompiled << "/" << t.fragmentCompiled << "): " << t.linkLog; EXPECT_EQ(t.bad, 0) << "thread " << i << " read back 0x" << std::hex << t.firstBad << " for 0x" - << ColourOf(i) << std::dec << " first at frame " << t.firstBadFrame << " (" + << t.firstExpected << std::dec << " first at frame " << t.firstBadFrame << " (" << t.good << " frames right)"; + EXPECT_GT(t.good, 0) << "thread " << i; + if (shape == Shape::ReleaseOnly) EXPECT_GT(t.rebinds, 0) << "thread " << i; + if (shape == Shape::DestroyWhileCurrent) { + EXPECT_EQ(t.destroyWaited, 1) << "thread " << i; + EXPECT_EQ(t.destroyAnswer, EGL_TRUE) + << "thread " << i << ": eglDestroyContext of a context current on another thread is deferred, " + << "not refused (error 0x" << std::hex << t.destroyError << ")"; + EXPECT_EQ(t.rebindAnswer, EGL_FALSE) << "thread " << i << ": the context outlived its release"; + EXPECT_EQ(t.rebindError, EGL_BAD_CONTEXT) << "thread " << i; + } + if (shape == Shape::ReadbackRace && i > 0) { + EXPECT_GT(t.cycles, 3) << "churner " << i; + EXPECT_EQ(t.cycleFailures, 0) << "churner " << i; + } } } } // namespace -TEST(ConcurrentContexts, DISABLED_ThreadsSharingOneRootContextEachDrawTheirOwnTexture) { Check(true); } +TEST(ConcurrentContexts, ThreadsSharingOneRootContextEachDrawTheirOwnTexture) { Check(Shape::SharedRoot); } -TEST(ConcurrentContexts, DISABLED_ThreadsInSeparateShareGroupsEachDrawTheirOwnTexture) { Check(false); } +TEST(ConcurrentContexts, ThreadsInSeparateShareGroupsEachDrawTheirOwnTexture) { Check(Shape::Unshared); } + +TEST(ConcurrentContexts, ReleasingWithoutDestroyingLeavesTheOtherThreadsDrawing) { Check(Shape::ReleaseOnly); } + +TEST(ConcurrentContexts, ShareGroupSiblingsOutliveTheGroupsFirstContext) { Check(Shape::Siblings); } + +TEST(ConcurrentContexts, DestroyingAContextCurrentOnAnotherThreadIsDeferredUntilItsRelease) { + Check(Shape::DestroyWhileCurrent); +} + +TEST(ConcurrentContexts, AReadBackRacingOtherThreadsContextTeardownsIsNotLost) { Check(Shape::ReadbackRace); }