mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Test] (Wire): enable the concurrent-context repro and add release, sibling, deferred-destroy and read-back-race shapes.
This commit is contained in:
@@ -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<Uint32>(index)) << 8 | 0x20u; }
|
||||
Uint32 ColourOf(int index) { return 0xFF000000u | (0x40u + 0x50u * static_cast<Uint32>(index % 3)) << 8 |
|
||||
(0x20u + 0x30u * static_cast<Uint32>(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<Uint32>(index)) << 16 | (static_cast<Uint32>(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<EGLContext> published[kThreads] = {}; // DestroyWhileCurrent: each thread's own
|
||||
std::atomic<Int32> halfway[kThreads] = {};
|
||||
std::atomic<Int32> destroyed[kThreads] = {};
|
||||
std::atomic<Int32> 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 <typename Predicate>
|
||||
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<float>(colour & 0xFFu) / 255.0f, static_cast<float>((colour >> 8) & 0xFFu) / 255.0f,
|
||||
static_cast<float>((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<ssize_t>(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); }
|
||||
|
||||
Reference in New Issue
Block a user