// MobileGL - MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp // Copyright (c) 2025-2026 MobileGL-Dev // Licensed under the GNU Lesser General Public License v3.0: // https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header // A CLIENT OUTLIVES ITS LOST SESSION. A GPU device loss ends exactly one session on the server // (MultiSessionTest.ADeviceLossEndsOnlyItsOwnSession); this suite is the client's half. A real // MobileGL client - the EGL and GL entry points, MobileGL's own initialisation, a unix spawn // session - against the in-process display server, whose debug knob makes the second device check // in the server process (this client's second read-back) report the device lost. // // What an application that handles context loss (Chrome through ANGLE) needs, each case asserting // it from the application's side: // * the loss is REPORTED to a context that asked to be told (EGL_EXT_create_context_robustness, // EGL_LOSE_CONTEXT_ON_RESET - advertised, and what ANGLE asks for): glGetGraphicsResetStatus // answers a reset and keeps answering it, glGetError GL_CONTEXT_LOST (once), eglSwapBuffers // and eglMakeCurrent on the old context fail with EGL_CONTEXT_LOST; a context that did not ask // is told through GL only where semantics are relaxed, and through EGL everywhere; // * the old context is SAFE TO USE AND TO DESTROY: a draw and a texture upload on it after the // loss do nothing, and destroying it puts nothing on the new session's wire; // * a context created after the loss gets a FRESH session (the server starts session #2, nothing // latches again) and renders: a textured draw through a new program, a clear, and a texture // framebuffer, each read back; // * nothing of the lost session reaches the fresh one: no record names a handle it never created // (the server's per-session refusal line stays silent), and the old texture's name is gone; // * the surface the application kept works with the new context (EGL keeps surfaces across a // power-management loss; only their contents go); // * the other route back - eglTerminate, then eglInitialize - brings a fresh session up too; // * contexts recreated still naming a lost share context (Qt's global share context) share // with EACH OTHER in one new group, not each in a group of its own. #include "P12ServerRig.h" #include #include #include #include #include #include #include extern "C" { void glTexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); const GLubyte* glGetString(GLenum name); const GLubyte* glGetStringi(GLenum name, GLuint index); void glGetIntegerv(GLenum pname, GLint* data); void glGetnUniformfv(GLuint program, GLint location, GLsizei bufSize, GLfloat* params); void glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); 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); GLenum glGetError(void); GLenum glGetGraphicsResetStatus(void); void glGenTextures(GLsizei n, 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, GLenum format, GLenum type, const void* pixels); void glTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels); void glTexParameteri(GLenum target, GLenum pname, GLint param); GLboolean glIsTexture(GLuint texture); void glGenFramebuffers(GLsizei n, GLuint* framebuffers); void glBindFramebuffer(GLenum target, GLuint framebuffer); void glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); GLenum glCheckFramebufferStatus(GLenum target); GLuint glCreateShader(GLenum type); void glShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length); void glCompileShader(GLuint shader); void glGetShaderiv(GLuint shader, GLenum pname, GLint* params); GLuint glCreateProgram(void); void glAttachShader(GLuint program, GLuint shader); void glLinkProgram(GLuint program); void glGetProgramiv(GLuint program, GLenum pname, GLint* params); void glUseProgram(GLuint program); GLint glGetUniformLocation(GLuint program, const GLchar* name); void glUniform1i(GLint location, GLint v0); void glGenVertexArrays(GLsizei n, GLuint* arrays); void glBindVertexArray(GLuint array); void glViewport(GLint x, GLint y, GLsizei width, GLsizei height); void glDrawArrays(GLenum mode, GLint first, GLsizei count); } using namespace MobileGL; using namespace P12; namespace { namespace EGL = MobileGL::MG_Impl::EGLImpl; // A<<24|B<<16|G<<8|R, the little-endian bytes of a GL_RGBA/GL_UNSIGNED_BYTE texel. constexpr Uint32 kGreen = 0xFF00FF00u; constexpr Uint32 kBlue = 0xFFFF0000u; constexpr Uint32 kRed = 0xFF0000FFu; constexpr Uint32 kYellow = 0xFF00FFFFu; constexpr Uint32 kWhite = 0xFFFFFFFFu; constexpr Uint32 kUntouched = 0x5A5A5A5Au; enum class Route { NewContext, // destroy the lost context, create another, keep the surface TerminateInitialize, // destroy everything, eglTerminate, eglInitialize, start over }; struct RecoveryReport { Int32 initialized = 0; Uint32 firstPixel = 0; Uint32 lostPixel = 0; Uint32 resetStatusLost = 0; Uint32 errorLost = 0; Uint32 errorAfterLost = 0; Int32 swapLost = -1; Int32 swapLostError = 0; Int32 releaseOk = 0; Int32 oldCurrent = -1; Int32 oldCurrentError = 0; Int32 destroyOldOk = 0; Int64 recordsByOldDestroy = -1; Int32 newContextOk = 0; Int32 newContextError = 0; Int32 newCurrentOk = 0; Int32 newCurrentError = 0; Uint32 resetStatusNew = 0xFFFFFFFFu; Int32 oldNameIsTexture = -1; // What the client believes about the lost context's texture: whether the server has its // record (the publication latch) and whether an upload is still owed to it (the drain). Int32 oldTexturePublishedBefore = -1; Int32 oldTextureOwedBefore = -1; Int32 oldTexturePublishedAfter = -1; Int32 oldTextureOwedAfter = -1; Uint32 drawnPixel = 0; Uint32 secondPixel = 0; Uint32 fboStatus = 0; Uint32 fboPixel = 0; Int32 swapNew = -1; Uint32 resetStatusEnd = 0xFFFFFFFFu; Uint32 serverPidBefore = 0; Uint32 serverPidAfter = 0; // Robustness, as the application sees it: the extensions it checks before asking, the // strategy its contexts report, the status again (it stays) and through the KHR name. Int32 eglRobustnessAdvertised = -1; Int32 glRobustnessAdvertised = -1; Int32 glRobustnessIndexed = -1; Int32 strategyBefore = 0; Int32 robustAccess = -1; Uint32 resetStatusRepeat = 0; Uint32 resetStatusKhr = 0; Int32 strategyNew = 0; char note[256] = {}; }; Bool HasToken(const char* list, const char* token) { if (list == nullptr) return false; const std::string all = std::string(" ") + list + " "; return all.find(std::string(" ") + token + " ") != std::string::npos; } void Note(RecoveryReport& r, const char* text) { if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); } Uint32 ReadPixel() { Uint32 pixel = kUntouched; glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel); return pixel; } Uint32 ClearAndRead(GLfloat red, GLfloat green, GLfloat blue) { glClearColor(red, green, blue, 1.0f); glClear(GL_COLOR_BUFFER_BIT); return ReadPixel(); } // A full-surface triangle that samples one texel: a program, a vertex array, a texture with its // sampler parameters and a draw - the shader, sampler, texture-upload and render-state families. struct TexturedDraw { GLuint program = 0; GLuint vertexArray = 0; GLuint texture = 0; Bool Build(RecoveryReport& r, Uint32 rgba) { 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"; static const char* kFragment = "#version 330 core\n" "uniform sampler2D image;\n" "out vec4 color;\n" "void main() { color = texture(image, vec2(0.5)); }\n"; 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); GLint compiled = 0; glGetShaderiv(vertex, GL_COMPILE_STATUS, &compiled); if (!compiled) return Note(r, "the vertex shader did not compile"), false; glGetShaderiv(fragment, GL_COMPILE_STATUS, &compiled); if (!compiled) return Note(r, "the fragment shader did not compile"), false; program = glCreateProgram(); glAttachShader(program, vertex); glAttachShader(program, fragment); glLinkProgram(program); GLint linked = 0; glGetProgramiv(program, GL_LINK_STATUS, &linked); if (!linked) return Note(r, "the program did not link"), false; glGenVertexArrays(1, &vertexArray); glGenTextures(1, &texture); Fill(rgba); return true; } void Fill(Uint32 rgba) const { glActiveTexture(GL_TEXTURE0); 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); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &rgba); } void Draw() const { glViewport(0, 0, 8, 8); glUseProgram(program); glUniform1i(glGetUniformLocation(program, "image"), 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glBindVertexArray(vertexArray); glDrawArrays(GL_TRIANGLES, 0, 3); } }; Uint32 ServerPid() { auto* session = Remote::Client::ClientSession::Active(); return session != nullptr ? session->PeerServerPid() : 0; } // The wire handle of texture `name` of `context`, through the client's own books. MobileGL::MG_Pipe::MGPipeHandle TextureHandle(EGLContext context, GLuint name) { const auto glState = MobileGL::MG_State::pEGLContext->GetContextGLState(context); if (!glState) return MobileGL::MG_Pipe::kMGPipeNullHandle; const auto& texture = glState->GetTextureObject(name); if (!texture) return MobileGL::MG_Pipe::kMGPipeNullHandle; return MobileGL::MG_Pipe::MGPipeSlots().FindByLifetimeId(MobileGL::MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId()); } void NoteTextureBooks(MobileGL::MG_Pipe::MGPipeHandle handle, Int32* published, Int32* owed) { using namespace MobileGL::MG_Pipe; *published = MGPipeHandleIsPublished(MGPipeKind::Texture, handle) ? 1 : 0; *owed = MGPipeTextureEmitterInstance().OwesUpload(handle) ? 1 : 0; } Uint64 PublishedSeq() { auto* session = Remote::Client::ClientSession::Active(); return session != nullptr ? session->LastPublishedSeq() : 0; } // The contexts an application that handles loss creates: a 3.3 context that asks to be told of // a reset (EGL_EXT_create_context_robustness - what Chrome, KWin and Qt pass when the display // has the extension). Contexts that share must agree on it. const EGLint kRobustAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT, EGL_LOSE_CONTEXT_ON_RESET_EXT, EGL_NONE}; // ANGLE's GL-on-EGL backend's own native context: the ES API, the highest ES version it tries // first, and EGL 1.5's name of the attribute. const EGLint kAngleNativeAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2, EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY, EGL_LOSE_CONTEXT_ON_RESET, EGL_NONE}; struct Egl { EGLDisplay dpy = EGL_NO_DISPLAY; EGLConfig config = nullptr; EGLSurface surface = EGL_NO_SURFACE; EGLenum api = EGL_OPENGL_API; const EGLint* contextAttribs = kRobustAttribs; Bool Bring(RecoveryReport& r) { dpy = EGL::GetDisplay(EGL_DEFAULT_DISPLAY); EGLint major = 0; EGLint minor = 0; if (dpy == EGL_NO_DISPLAY || EGL::Initialize(dpy, &major, &minor) != EGL_TRUE) return Note(r, "eglInitialize failed (no spawn session?)"), false; (void)EGL::BindAPI(api); const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_NONE}; EGLint count = 0; if (EGL::ChooseConfig(dpy, configAttribs, &config, 1, &count) != EGL_TRUE || count < 1) return Note(r, "no config"), false; const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE}; surface = EGL::CreatePbufferSurface(dpy, config, pbufferAttribs); if (surface == EGL_NO_SURFACE) return Note(r, "no pbuffer"), false; return true; } EGLContext Context() const { return EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, contextAttribs); } }; // The robustness answers of the current context, before the loss. void NoteRobustness(Egl& egl, RecoveryReport& r) { r.eglRobustnessAdvertised = HasToken(EGL::QueryString(egl.dpy, EGL_EXTENSIONS), "EGL_EXT_create_context_robustness") ? 1 : 0; r.glRobustnessAdvertised = HasToken(reinterpret_cast(glGetString(GL_EXTENSIONS)), "GL_KHR_robustness") ? 1 : 0; GLint count = 0; glGetIntegerv(GL_NUM_EXTENSIONS, &count); r.glRobustnessIndexed = 0; for (GLint i = 0; i < count; ++i) { const auto* name = reinterpret_cast(glGetStringi(GL_EXTENSIONS, static_cast(i))); if (name != nullptr && std::string(name) == "GL_KHR_robustness") r.glRobustnessIndexed = 1; } glGetIntegerv(GL_RESET_NOTIFICATION_STRATEGY, &r.strategyBefore); GLint robust = -1; glGetIntegerv(GL_CONTEXT_ROBUST_ACCESS, &robust); r.robustAccess = robust; } // The client, in a fork: everything an application does around a loss, through the entry points. // `api` and `attribs` are the contexts' shape (the first and every replacement). void RunRecoveringClient(Route route, RecoveryReport& r, EGLenum api = EGL_OPENGL_API, const EGLint* attribs = kRobustAttribs) { Egl egl; egl.api = api; egl.contextAttribs = attribs; if (!egl.Bring(r)) return; const EGLContext lost = egl.Context(); if (lost == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, lost) != EGL_TRUE) return Note(r, "the first context did not go current"); r.initialized = 1; r.serverPidBefore = ServerPid(); NoteRobustness(egl, r); // The objects of the context that is about to be lost, and device check #1 in the server // process: a textured draw, read back. TexturedDraw old; if (!old.Build(r, kGreen)) return; old.Draw(); r.firstPixel = ReadPixel(); // Device check #2: the device is "lost" there. The session latches, the server ends it. r.lostPixel = ClearAndRead(0.0f, 0.0f, 1.0f); (void)WaitFor([] { return glGetGraphicsResetStatus() != GL_NO_ERROR; }, 5000); // THE LOSS, REPORTED. r.resetStatusLost = glGetGraphicsResetStatus(); r.errorLost = glGetError(); r.errorAfterLost = glGetError(); r.resetStatusRepeat = glGetGraphicsResetStatus(); using ResetStatusFn = GLenum (*)(void); if (auto khr = reinterpret_cast(EGL::GetProcAddress("glGetGraphicsResetStatusKHR"))) r.resetStatusKhr = khr(); // What an application does before it has looked: more work on the lost context - a draw, // and then an upload that no later verb of the lost context will ever carry (it is owed to // a server that is gone). None of it may surface on the fresh session. old.Draw(); const Uint32 white = kWhite; glBindTexture(GL_TEXTURE_2D, old.texture); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &white); const auto oldHandle = TextureHandle(lost, old.texture); NoteTextureBooks(oldHandle, &r.oldTexturePublishedBefore, &r.oldTextureOwedBefore); r.swapLost = EGL::SwapBuffers(egl.dpy, egl.surface); r.swapLostError = EGL::GetError(); r.releaseOk = EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) == EGL_TRUE; r.oldCurrent = EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, lost); r.oldCurrentError = EGL::GetError(); if (r.oldCurrent == EGL_TRUE) (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); EGLContext fresh = EGL_NO_CONTEXT; if (route == Route::NewContext) { // A NEW CONTEXT FIRST (Chrome's order: the replacement exists before the old one goes), // then the old one is destroyed - which must put nothing on the new session's wire. fresh = egl.Context(); r.newContextOk = fresh != EGL_NO_CONTEXT; r.newContextError = EGL::GetError(); // The fresh session is up: the lost texture is not on its server, and nothing is owed. NoteTextureBooks(oldHandle, &r.oldTexturePublishedAfter, &r.oldTextureOwedAfter); const Uint64 before = PublishedSeq(); r.destroyOldOk = EGL::DestroyContext(egl.dpy, lost) == EGL_TRUE; r.recordsByOldDestroy = static_cast(PublishedSeq()) - static_cast(before); } else { r.destroyOldOk = EGL::DestroyContext(egl.dpy, lost) == EGL_TRUE; (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); Egl again; again.api = egl.api; again.contextAttribs = egl.contextAttribs; if (!again.Bring(r)) return; egl = again; NoteTextureBooks(oldHandle, &r.oldTexturePublishedAfter, &r.oldTextureOwedAfter); fresh = egl.Context(); r.newContextOk = fresh != EGL_NO_CONTEXT; r.newContextError = EGL::GetError(); } if (fresh == EGL_NO_CONTEXT) return Note(r, "no context after the loss"); r.newCurrentOk = EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, fresh) == EGL_TRUE; r.newCurrentError = EGL::GetError(); if (!r.newCurrentOk) return Note(r, "the new context did not go current"); r.serverPidAfter = ServerPid(); r.resetStatusNew = glGetGraphicsResetStatus(); glGetIntegerv(GL_RESET_NOTIFICATION_STRATEGY, &r.strategyNew); r.oldNameIsTexture = glIsTexture(old.texture) == GL_TRUE ? 1 : 0; // THE NEW CONTEXT RENDERS: the same textured draw with a new program and texture, a clear of // the surface's own framebuffer, then a texture-backed framebuffer. TexturedDraw fresher; if (!fresher.Build(r, kRed)) return; fresher.Draw(); r.drawnPixel = ReadPixel(); r.secondPixel = ClearAndRead(0.0f, 0.0f, 1.0f); GLuint target = 0; GLuint framebuffer = 0; glGenTextures(1, &target); glBindTexture(GL_TEXTURE_2D, target); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glGenFramebuffers(1, &framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target, 0); r.fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); r.fboPixel = ClearAndRead(1.0f, 1.0f, 0.0f); glBindFramebuffer(GL_FRAMEBUFFER, 0); r.swapNew = EGL::SwapBuffers(egl.dpy, egl.surface); r.resetStatusEnd = glGetGraphicsResetStatus(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); (void)EGL::DestroyContext(egl.dpy, fresh); (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); } // QT'S ROUTE. A long-lived share context the application never recreates, and its working // contexts created sharing with it; after the loss the working contexts are recreated, still // naming the share context, which is lost. struct ShareReport { Int32 initialized = 0; Int32 firstOk = 0; Int32 secondOk = 0; Int32 firstError = 0; Int32 sameGroup = -1; Int32 lostGroupReused = -1; Int32 textureSeenAcross = -1; Uint32 drawnAcross = 0; char note[256] = {}; }; void Note(ShareReport& r, const char* text) { if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); } void RunShareReplacementClient(ShareReport& r) { Egl egl; RecoveryReport bring{}; if (!egl.Bring(bring)) return Note(r, bring.note); const EGLint* attribs = kRobustAttribs; const EGLContext share = egl.Context(); const EGLContext working = EGL::CreateContext(egl.dpy, egl.config, share, attribs); if (working == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, working) != EGL_TRUE) return Note(r, "the working context did not go current"); r.initialized = 1; TexturedDraw old; RecoveryReport built{}; if (!old.Build(built, kGreen)) return Note(r, built.note); old.Draw(); (void)ReadPixel(); // device check #1 (void)ClearAndRead(0.0f, 0.0f, 1.0f); // device check #2: lost (void)WaitFor([] { return glGetGraphicsResetStatus() != GL_NO_ERROR; }, 5000); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); (void)EGL::DestroyContext(egl.dpy, working); // Two windows' contexts, recreated naming the lost share context. const EGLContext first = EGL::CreateContext(egl.dpy, egl.config, share, attribs); r.firstError = EGL::GetError(); const EGLContext second = EGL::CreateContext(egl.dpy, egl.config, share, attribs); r.firstOk = first != EGL_NO_CONTEXT; r.secondOk = second != EGL_NO_CONTEXT; if (!r.firstOk || !r.secondOk) return Note(r, "a context naming the lost share context was refused"); const Uint64 lostGroup = MG_State::pEGLContext->GetContextShareGroupToken(share); const Uint64 firstGroup = MG_State::pEGLContext->GetContextShareGroupToken(first); r.sameGroup = firstGroup == MG_State::pEGLContext->GetContextShareGroupToken(second) ? 1 : 0; r.lostGroupReused = firstGroup == lostGroup ? 1 : 0; // An object of one is the other's: a texture filled on the first is drawn on the second. if (EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, first) != EGL_TRUE) return Note(r, "the first replacement did not go current"); TexturedDraw shared; if (!shared.Build(built, kRed)) return Note(r, built.note); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, second) != EGL_TRUE) return Note(r, "the second replacement did not go current"); r.textureSeenAcross = glIsTexture(shared.texture) == GL_TRUE ? 1 : 0; GLuint vertexArray = 0; // a vertex array is per context glGenVertexArrays(1, &vertexArray); shared.vertexArray = vertexArray; shared.Draw(); r.drawnAcross = ReadPixel(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); (void)EGL::DestroyContext(egl.dpy, second); (void)EGL::DestroyContext(egl.dpy, first); (void)EGL::DestroyContext(egl.dpy, share); (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); } // A thread of the client that runs what it is handed, one job at a time, and keeps whatever // EGL context it made current between jobs - an application's other GL thread. class Worker { public: Worker() : m_thread([this] { Loop(); }) {} ~Worker() { { const std::lock_guard lock(m_mutex); m_quit = true; } m_wake.notify_all(); m_thread.join(); } void Run(const std::function& job) { std::unique_lock lock(m_mutex); m_job = job; m_wake.notify_all(); m_wake.wait(lock, [this] { return !m_job; }); } private: void Loop() { std::unique_lock lock(m_mutex); for (;;) { m_wake.wait(lock, [this] { return m_quit || static_cast(m_job); }); if (m_quit) return; lock.unlock(); m_job(); lock.lock(); m_job = nullptr; m_wake.notify_all(); } } std::mutex m_mutex; std::condition_variable m_wake; std::function m_job; Bool m_quit = false; std::thread m_thread; }; // THE OTHER THREAD. The context that is lost stays current on a second thread of the // application while its main thread recovers and renders on a fresh session; then the second // thread - which has not looked yet - draws, uploads and reads on the lost context. All of it // must be refused as lost (reset status, nothing read back) and none of it may reach the fresh // session: no record published while it runs, no latch on the server (a binding that named the // lost context's token there would be Fatal{ProtocolCorruption}), and the main thread's // rendering is unaffected afterwards. struct StaleThreadReport { Int32 initialized = 0; Uint32 firstPixel = 0; Int32 newCurrentOk = 0; Uint32 drawnPixel = 0; Uint32 staleResetStatus = 0; Uint32 stalePixel = 0; Int32 staleSwap = -1; Int32 staleSwapError = 0; Int32 staleRelease = 0; Int64 recordsByStaleThread = -1; Uint32 pixelAfterStale = 0; Uint32 resetStatusAfterStale = 0xFFFFFFFFu; Int32 destroyOldOk = 0; char note[256] = {}; }; void Note(StaleThreadReport& r, const char* text) { if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); } void RunStaleThreadClient(StaleThreadReport& r) { Egl egl; RecoveryReport bring{}; if (!egl.Bring(bring)) return Note(r, bring.note); const EGLContext lost = egl.Context(); Worker second; TexturedDraw old; second.Run([&] { if (lost == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, lost) != EGL_TRUE) return Note(r, "the first context did not go current on the second thread"); RecoveryReport built{}; if (!old.Build(built, kGreen)) return Note(r, built.note); r.initialized = 1; old.Draw(); r.firstPixel = ReadPixel(); // device check #1 (void)ClearAndRead(0.0f, 0.0f, 1.0f); // device check #2: lost (void)WaitFor([] { return glGetGraphicsResetStatus() != GL_NO_ERROR; }, 5000); }); if (!r.initialized) return; // The main thread recovers: a surface and a context of its own, on a fresh session. const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE}; const EGLSurface mine = EGL::CreatePbufferSurface(egl.dpy, egl.config, pbufferAttribs); const EGLContext fresh = egl.Context(); r.newCurrentOk = fresh != EGL_NO_CONTEXT && mine != EGL_NO_SURFACE && EGL::MakeCurrent(egl.dpy, mine, mine, fresh) == EGL_TRUE; if (!r.newCurrentOk) return Note(r, "no fresh context went current on the main thread"); TexturedDraw fresher; RecoveryReport built{}; if (!fresher.Build(built, kRed)) return Note(r, built.note); fresher.Draw(); r.drawnPixel = ReadPixel(); // The second thread, still on the lost context, gets to it now. const Uint64 before = PublishedSeq(); second.Run([&] { r.staleResetStatus = glGetGraphicsResetStatus(); old.Draw(); old.Fill(kWhite); r.stalePixel = ClearAndRead(1.0f, 1.0f, 1.0f); r.staleSwap = EGL::SwapBuffers(egl.dpy, egl.surface); r.staleSwapError = EGL::GetError(); r.staleRelease = EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) == EGL_TRUE; }); r.recordsByStaleThread = static_cast(PublishedSeq()) - static_cast(before); r.destroyOldOk = EGL::DestroyContext(egl.dpy, lost) == EGL_TRUE; // The main thread renders on, unaffected. fresher.Draw(); r.pixelAfterStale = ReadPixel(); r.resetStatusAfterStale = glGetGraphicsResetStatus(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); (void)EGL::DestroyContext(egl.dpy, fresh); (void)EGL::DestroySurface(egl.dpy, mine); (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); } // NO RESET NOTIFICATION. Two contexts of one session that did not ask to be told of a reset, // each current on its own thread when the session is lost: an explicit core-profile one, which // gets KHR_robustness' NO_RESET_NOTIFICATION behavior exactly (no reset status, no // GL_CONTEXT_LOST), and a compatibility-profile one, which is told all the same (the relaxed // default). Both still learn it from EGL - a swap and a make-current fail with EGL_CONTEXT_LOST - // and a context created afterwards renders on a fresh session. const EGLint kCoreNoResetAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; const EGLint kCompatNoResetAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT, EGL_NONE}; struct UntoldReport { Int32 initialized = 0; Uint32 firstPixel = 0; Int32 lossSeen = 0; Int32 coreStrategy = 0; Uint32 coreStatus = 0xFFFFFFFFu; Uint32 coreError = 0xFFFFFFFFu; Int32 coreSwap = -1; Int32 coreSwapError = 0; Uint32 compatStatus = 0; Uint32 compatError = 0; Int32 coreAgain = -1; Int32 coreAgainError = 0; Int32 freshCurrent = 0; Uint32 freshStatus = 0xFFFFFFFFu; Uint32 freshPixel = 0; char note[256] = {}; }; void Note(UntoldReport& r, const char* text) { if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); } void RunUntoldClient(UntoldReport& r) { Egl egl; egl.contextAttribs = kCoreNoResetAttribs; RecoveryReport bring{}; if (!egl.Bring(bring)) return Note(r, bring.note); const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE}; const EGLSurface other = EGL::CreatePbufferSurface(egl.dpy, egl.config, pbufferAttribs); const EGLContext core = egl.Context(); const EGLContext compat = EGL::CreateContext(egl.dpy, egl.config, EGL_NO_CONTEXT, kCompatNoResetAttribs); if (core == EGL_NO_CONTEXT || compat == EGL_NO_CONTEXT || other == EGL_NO_SURFACE) return Note(r, "the contexts or the second surface were not created"); Worker second; Bool compatCurrent = false; second.Run([&] { compatCurrent = EGL::MakeCurrent(egl.dpy, other, other, compat) == EGL_TRUE; }); if (!compatCurrent || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, core) != EGL_TRUE) return Note(r, "the contexts did not go current"); r.initialized = 1; glGetIntegerv(GL_RESET_NOTIFICATION_STRATEGY, &r.coreStrategy); r.firstPixel = ClearAndRead(0.0f, 1.0f, 0.0f); // device check #1 (void)ClearAndRead(0.0f, 0.0f, 1.0f); // device check #2: lost r.lossSeen = WaitFor([] { return Remote::Client::ClientSession::SessionLatchedLost(); }, 5000) ? 1 : 0; r.coreStatus = glGetGraphicsResetStatus(); r.coreError = glGetError(); second.Run([&] { r.compatStatus = glGetGraphicsResetStatus(); r.compatError = glGetError(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); }); r.coreSwap = EGL::SwapBuffers(egl.dpy, egl.surface); r.coreSwapError = EGL::GetError(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); r.coreAgain = EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, core); r.coreAgainError = EGL::GetError(); if (r.coreAgain == EGL_TRUE) (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); const EGLContext fresh = egl.Context(); r.freshCurrent = fresh != EGL_NO_CONTEXT && EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, fresh) == EGL_TRUE; if (!r.freshCurrent) return Note(r, "no fresh context went current"); r.freshStatus = glGetGraphicsResetStatus(); r.freshPixel = ClearAndRead(0.0f, 0.0f, 1.0f); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); (void)EGL::DestroyContext(egl.dpy, fresh); (void)EGL::DestroyContext(egl.dpy, compat); (void)EGL::DestroyContext(egl.dpy, core); (void)EGL::DestroySurface(egl.dpy, other); (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); } // Runs `body` in a forked client (the environment a deployment sets, then the entry points) // and hands back the report it filled. template Report RunInClient(const ServerProcess& server, const std::string& clientLogBase, Body body) { Report report{}; int fds[2] = {-1, -1}; if (::pipe(fds) != 0) return report; std::fflush(nullptr); const pid_t pid = ::fork(); if (pid == 0) { ::close(fds[0]); ::setenv("MOBILEGL_LOG_FILE_PATH", clientLogBase.c_str(), 1); ::setenv("MOBILEGL_TRANSPORT", "spawn", 1); ::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1); ::unsetenv("MOBILEGL_IPC_SURFACE"); ::unsetenv("MOBILEGL_BACKEND_TYPE"); ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT"); Report r{}; body(r); const ssize_t wrote = ::write(fds[1], &r, sizeof(r)); (void)wrote; std::fflush(nullptr); ::_exit(0); } ::close(fds[1]); if (pid < 0) { ::close(fds[0]); return report; } pollfd pfd{fds[0], POLLIN, 0}; if (::poll(&pfd, 1, 90000) > 0) { if (::read(fds[0], &report, sizeof(report)) != static_cast(sizeof(report))) { report = Report{}; Note(report, "the client died before reporting"); } } else { Note(report, "the client timed out"); ::kill(pid, SIGKILL); } ::close(fds[0]); int status = 0; ::waitpid(pid, &status, 0); return report; } // THE SESSION-TARGETED KNOB (MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID): one client of several is // lost, whenever its session drains, whether or not it ever presents or reads back. Its pid // has to be known before the server starts (the knob is the server's environment), so it is // forked first and held until the server listens; the endpoint is handed to it then. struct TargetedReport { Int32 initialized = 0; Int32 lossSeen = 0; Uint32 resetStatus = 0; Int32 freshCurrent = 0; Uint32 freshPixel = 0; Int32 freshFrames = 0; Uint32 freshStatusEnd = 0xFFFFFFFFu; char note[256] = {}; }; void Note(TargetedReport& r, const char* text) { if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); } // Draws until its context reports the reset, recovers on a new context, then keeps drawing // for half a second - long enough for the knob to be asked again - and must not be lost again. void RunTargetedClient(TargetedReport& r) { Egl egl; RecoveryReport bring{}; if (!egl.Bring(bring)) return Note(r, bring.note); const EGLContext lost = egl.Context(); if (lost == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, lost) != EGL_TRUE) return Note(r, "the first context did not go current"); r.initialized = 1; r.lossSeen = WaitFor( [] { (void)ClearAndRead(0.0f, 1.0f, 0.0f); return glGetGraphicsResetStatus() != GL_NO_ERROR; }, 10000) ? 1 : 0; r.resetStatus = glGetGraphicsResetStatus(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); const EGLContext fresh = egl.Context(); (void)EGL::DestroyContext(egl.dpy, lost); r.freshCurrent = fresh != EGL_NO_CONTEXT && EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, fresh) == EGL_TRUE; if (!r.freshCurrent) return Note(r, "no fresh context went current"); const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); while (std::chrono::steady_clock::now() < until) { r.freshPixel = ClearAndRead(0.0f, 0.0f, 1.0f); ++r.freshFrames; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } r.freshStatusEnd = glGetGraphicsResetStatus(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); (void)EGL::DestroyContext(egl.dpy, fresh); (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); } // A client of the same server that is not the target: half a second of read-backs, untouched. struct BystanderReport { Int32 initialized = 0; Int32 frames = 0; Uint32 lastPixel = 0; Uint32 resetStatus = 0xFFFFFFFFu; char note[256] = {}; }; void Note(BystanderReport& r, const char* text) { if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); } void RunBystanderClient(BystanderReport& r) { Egl egl; RecoveryReport bring{}; if (!egl.Bring(bring)) return Note(r, bring.note); const EGLContext context = egl.Context(); if (context == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, context) != EGL_TRUE) return Note(r, "the context did not go current"); r.initialized = 1; const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); while (std::chrono::steady_clock::now() < until) { r.lastPixel = ClearAndRead(1.0f, 0.0f, 0.0f); ++r.frames; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } r.resetStatus = glGetGraphicsResetStatus(); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); (void)EGL::DestroyContext(egl.dpy, context); (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); } // A forked client that waits for the endpoint before it does anything. template struct HeldClient { pid_t pid = -1; int go = -1; int report = -1; Bool Fork(const std::string& clientLogBase, Body body) { int goFds[2] = {-1, -1}; int reportFds[2] = {-1, -1}; if (::pipe(goFds) != 0 || ::pipe(reportFds) != 0) return false; std::fflush(nullptr); pid = ::fork(); if (pid == 0) { ::close(goFds[1]); ::close(reportFds[0]); char endpoint[512] = {}; const ssize_t got = ::read(goFds[0], endpoint, sizeof(endpoint) - 1); if (got <= 0) ::_exit(0); ::setenv("MOBILEGL_LOG_FILE_PATH", clientLogBase.c_str(), 1); ::setenv("MOBILEGL_TRANSPORT", "spawn", 1); ::setenv("MOBILEGL_IPC_CONTROL", endpoint, 1); ::unsetenv("MOBILEGL_IPC_SURFACE"); ::unsetenv("MOBILEGL_BACKEND_TYPE"); ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT"); ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID"); Report r{}; body(r); const ssize_t wrote = ::write(reportFds[1], &r, sizeof(r)); (void)wrote; std::fflush(nullptr); ::_exit(0); } ::close(goFds[0]); ::close(reportFds[1]); go = goFds[1]; report = reportFds[0]; return pid > 0; } Report Release(const std::string& endpoint) { Report r{}; const ssize_t wrote = ::write(go, endpoint.c_str(), endpoint.size()); (void)wrote; ::close(go); pollfd pfd{report, POLLIN, 0}; if (::poll(&pfd, 1, 90000) > 0) { if (::read(report, &r, sizeof(r)) != static_cast(sizeof(r))) { r = Report{}; Note(r, "the client died before reporting"); } } else { Note(r, "the client timed out"); ::kill(pid, SIGKILL); } ::close(report); int status = 0; ::waitpid(pid, &status, 0); return r; } }; // THE ES IDENTITY (MOBILEGL_ES_CONTEXT_IDENTITY=1), what Chrome's GPU process runs with: ANGLE // offers GL_OES_EGL_image - the GPU process's dma-buf images - only over a native driver that // calls itself OpenGL ES, and then creates BGRA textures with the ES spelling. And the API a // dispatching libEGL reports wins over the one this library was told (glvnd never forwards a // bind of the API it already reports, EGL's default ES). struct EsIdentityReport { Int32 initialized = 0; Int32 dispatcherApiWins = -1; char esVersion[96] = {}; char esGlsl[64] = {}; Int32 esMajor = 0; Int32 esMinor = 0; Int32 bgraAdvertised = -1; Int32 bgraIndexed = -1; Uint32 bgraImageError = 0xFFFFFFFFu; Uint32 bgraStorageError = 0xFFFFFFFFu; Uint32 bgraFboStatus = 0; Uint32 bgraPixel = 0; char desktopVersion[96] = {}; char note[256] = {}; }; void Note(EsIdentityReport& r, const char* text) { if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); } EGLenum g_dispatcherApi = EGL_OPENGL_ES_API; void RunEsIdentityClient(EsIdentityReport& r) { ::setenv("MOBILEGL_ES_CONTEXT_IDENTITY", "1", 1); Egl egl; RecoveryReport bring{}; if (!egl.Bring(bring)) return Note(r, bring.note); // The dispatcher says ES although this library was told GL last (Bring binds the GL API). EGL::SetDispatcherCurrentApiHook([]() -> EGLenum { return g_dispatcherApi; }); const EGLContext es = EGL::CreateContext(egl.dpy, egl.config, EGL_NO_CONTEXT, kAngleNativeAttribs); g_dispatcherApi = EGL_OPENGL_API; const EGLContext desktop = EGL::CreateContext(egl.dpy, egl.config, EGL_NO_CONTEXT, kRobustAttribs); EGL::SetDispatcherCurrentApiHook(nullptr); if (es == EGL_NO_CONTEXT || desktop == EGL_NO_CONTEXT) return Note(r, "the contexts were not created"); EGLint type = 0; (void)EGL::QueryContext(egl.dpy, es, EGL_CONTEXT_CLIENT_TYPE, &type); r.dispatcherApiWins = type == EGL_OPENGL_ES_API ? 1 : 0; if (EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, es) != EGL_TRUE) return Note(r, "the ES context did not go current"); r.initialized = 1; std::snprintf(r.esVersion, sizeof(r.esVersion), "%s", reinterpret_cast(glGetString(GL_VERSION))); std::snprintf(r.esGlsl, sizeof(r.esGlsl), "%s", reinterpret_cast(glGetString(GL_SHADING_LANGUAGE_VERSION))); glGetIntegerv(GL_MAJOR_VERSION, &r.esMajor); glGetIntegerv(GL_MINOR_VERSION, &r.esMinor); r.bgraAdvertised = HasToken(reinterpret_cast(glGetString(GL_EXTENSIONS)), "GL_EXT_texture_format_BGRA8888") ? 1 : 0; GLint count = 0; glGetIntegerv(GL_NUM_EXTENSIONS, &count); r.bgraIndexed = 0; for (GLint i = 0; i < count; ++i) { const auto* name = reinterpret_cast(glGetStringi(GL_EXTENSIONS, static_cast(i))); if (name != nullptr && std::string(name) == "GL_EXT_texture_format_BGRA8888") r.bgraIndexed = 1; } while (glGetError() != GL_NO_ERROR) { } // BGRA as EXT_texture_format_BGRA8888 spells it: GL_BGRA_EXT as the internal format, and // GL_BGRA8_EXT through glTexStorage2D; a BGRA texel reads back in RGBA order. const Uint8 bgra[4] = {0x10, 0x20, 0x30, 0xFF}; // B, G, R, A GLuint textures[2] = {}; glGenTextures(2, textures); glBindTexture(GL_TEXTURE_2D, textures[0]); glTexImage2D(GL_TEXTURE_2D, 0, GL_BGRA, 1, 1, 0, GL_BGRA, GL_UNSIGNED_BYTE, bgra); r.bgraImageError = glGetError(); glBindTexture(GL_TEXTURE_2D, textures[1]); glTexStorage2D(GL_TEXTURE_2D, 1, 0x93A1 /* GL_BGRA8_EXT */, 1, 1); r.bgraStorageError = glGetError(); GLuint framebuffer = 0; glGenFramebuffers(1, &framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[0], 0); r.bgraFboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); r.bgraPixel = ReadPixel(); glBindFramebuffer(GL_FRAMEBUFFER, 0); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, desktop) == EGL_TRUE) { std::snprintf(r.desktopVersion, sizeof(r.desktopVersion), "%s", reinterpret_cast(glGetString(GL_VERSION))); (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } (void)EGL::DestroyContext(egl.dpy, es); (void)EGL::DestroyContext(egl.dpy, desktop); (void)EGL::DestroySurface(egl.dpy, egl.surface); (void)EGL::Terminate(egl.dpy); } RecoveryReport RunClient(const ServerProcess& server, Route route, const std::string& clientLogBase, EGLenum api = EGL_OPENGL_API, const EGLint* attribs = kRobustAttribs) { return RunInClient(server, clientLogBase, [route, api, attribs](RecoveryReport& r) { RunRecoveringClient(route, r, api, attribs); }); } struct ClientLog { std::string base; explicit ClientLog(const std::string& label) : base("/tmp/mgl-recover-client-" + label + "-" + std::to_string(::getpid()) + ".log") { Debug::TruncateRoleLogs(base.c_str()); } std::string Text() const { return ReadFile(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Client)); } ~ClientLog() { if (::testing::Test::HasFailure() || std::getenv("MOBILEGL_TEST_KEEP_LOGS") != nullptr) return; std::error_code ec; std::filesystem::remove(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Client), ec); std::filesystem::remove(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Server), ec); std::filesystem::remove(base, ec); } }; class ClientRecovery : public ::testing::Test { protected: void SetUp() override { ::signal(SIGPIPE, SIG_IGN); } // The server, with the loss armed for the second device check in its process. bool Launch(ServerProcess* server, const std::string& label) { ::setenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT", "2", 1); const bool launched = LaunchUnixInProcessServer(server, label); ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT"); return launched; } // What every route has to show from the application's side, before and after the loss. static void ExpectLossReportedAndRecovered(const RecoveryReport& r, const std::string& diagnostics) { ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; ASSERT_EQ(r.firstPixel, kGreen) << "the first draw (device check #1) did not read back: " << std::hex << r.firstPixel << "\n" << r.note << "\n" << diagnostics; EXPECT_NE(r.lostPixel, kBlue) << "a read-back on the lost device returned a frame"; // What the application checked before it asked to be told (ANGLE mirrors the EGL // extension onto its own display; Chrome checks that and GL_*_robustness), and what // its context then reports. Robust buffer ACCESS was not asked for and is not claimed. EXPECT_EQ(r.eglRobustnessAdvertised, 1) << "EGL_EXT_create_context_robustness is not advertised"; EXPECT_EQ(r.glRobustnessAdvertised, 1) << "GL_KHR_robustness is not in glGetString(GL_EXTENSIONS)"; EXPECT_EQ(r.glRobustnessIndexed, 1) << "GL_KHR_robustness is not in glGetStringi(GL_EXTENSIONS, i)"; EXPECT_EQ(r.strategyBefore, GL_LOSE_CONTEXT_ON_RESET) << "GL_RESET_NOTIFICATION_STRATEGY: 0x" << std::hex << r.strategyBefore; EXPECT_EQ(r.robustAccess, GL_FALSE); // The loss is reported. EXPECT_TRUE(r.resetStatusLost == GL_UNKNOWN_CONTEXT_RESET || r.resetStatusLost == GL_GUILTY_CONTEXT_RESET) << "glGetGraphicsResetStatus after the loss: 0x" << std::hex << r.resetStatusLost; EXPECT_EQ(r.errorLost, static_cast(GL_CONTEXT_LOST)) << "glGetError after the loss: 0x" << std::hex << r.errorLost; EXPECT_EQ(r.errorAfterLost, static_cast(GL_NO_ERROR)) << "GL_CONTEXT_LOST must be reported once, not forever (a drain loop must end)"; // LOSE_CONTEXT_ON_RESET: the context stays lost, so the status does not go back to // GL_NO_ERROR (that would read as a reset that completed in place), and the KHR name // an ES application looks up answers the same. EXPECT_EQ(r.resetStatusRepeat, r.resetStatusLost) << "the reset status of a lost context went back to 0x" << std::hex << r.resetStatusRepeat; EXPECT_EQ(r.resetStatusKhr, r.resetStatusLost) << "glGetGraphicsResetStatusKHR: 0x" << std::hex << r.resetStatusKhr; EXPECT_EQ(r.swapLost, EGL_FALSE) << "eglSwapBuffers on the lost context succeeded"; EXPECT_EQ(r.swapLostError, EGL_CONTEXT_LOST) << "eglSwapBuffers on the lost context: 0x" << std::hex << r.swapLostError; EXPECT_EQ(r.releaseOk, 1) << "releasing the lost context failed"; EXPECT_EQ(r.oldCurrent, EGL_FALSE) << "the lost context was made current again"; EXPECT_EQ(r.oldCurrentError, EGL_CONTEXT_LOST) << "eglMakeCurrent of the lost context: 0x" << std::hex << r.oldCurrentError; EXPECT_EQ(r.destroyOldOk, 1) << "the lost context could not be destroyed"; // A fresh session, and the new context renders. ASSERT_EQ(r.newContextOk, 1) << "no context could be created after the loss (EGL error 0x" << std::hex << r.newContextError << ")\n" << r.note << "\n" << diagnostics; ASSERT_EQ(r.newCurrentOk, 1) << "the new context did not go current (EGL error 0x" << std::hex << r.newCurrentError << ")\n" << r.note << "\n" << diagnostics; EXPECT_EQ(r.serverPidAfter, r.serverPidBefore) << "the new session is not on the same server process"; EXPECT_EQ(r.resetStatusNew, static_cast(GL_NO_ERROR)) << "the new context reports a reset"; EXPECT_EQ(r.strategyNew, GL_LOSE_CONTEXT_ON_RESET) << "the replacement is not told of resets"; EXPECT_EQ(r.oldNameIsTexture, 0) << "the old context's texture name resolves in the new context"; // The client's books on the lost texture: before the recovery the lost session had its // record (the arm proof that there was something to forget), and the upload made on the // lost context was queued for nobody; after it, neither - its death and its texels must // not reach the fresh server. EXPECT_EQ(r.oldTexturePublishedBefore, 1) << "the lost texture was never published (nothing to forget)"; EXPECT_EQ(r.oldTextureOwedBefore, 0) << "an upload on the lost context was queued; the next context's " "verb would carry it to whatever session is current"; EXPECT_EQ(r.oldTexturePublishedAfter, 0) << "the fresh session's client still believes it holds the " "lost texture's record"; EXPECT_EQ(r.oldTextureOwedAfter, 0) << "an upload owed to the lost session is queued for the fresh one"; EXPECT_EQ(r.drawnPixel, kRed) << "the new context's textured draw did not read back: " << std::hex << r.drawnPixel << "\n" << r.note << "\n" << diagnostics; EXPECT_EQ(r.secondPixel, kBlue) << "the new context's clear did not read back: " << std::hex << r.secondPixel; EXPECT_EQ(r.fboStatus, static_cast(GL_FRAMEBUFFER_COMPLETE)) << std::hex << r.fboStatus; EXPECT_EQ(r.fboPixel, kYellow) << "the new context's texture framebuffer did not read back: " << std::hex << r.fboPixel; EXPECT_EQ(r.swapNew, EGL_TRUE) << "eglSwapBuffers on the new context failed"; EXPECT_EQ(r.resetStatusEnd, static_cast(GL_NO_ERROR)) << "the new session was lost too"; } // The server's side: the first session latched on the injected loss and nothing else did; a // second session started and ended cleanly; nothing from the first reached it. static void ExpectOneLossAndACleanSecondSession(ServerProcess& server) { ASSERT_TRUE(WaitFor([&] { return Count(server.Log(), "in-process unix session #2 started") == 1; }, 5000)) << "the client never opened a second session:\n" << server.Log(); // Both sessions' ends: the lost one's latched exit and the fresh one's clean one. ASSERT_TRUE(WaitFor([&] { return Count(server.Log(), "reaped exit=") == 2; }, 10000)) << "the second session did not end:\n" << server.Log(); const std::string log = server.Log(); EXPECT_NE(log.find("SessionLatch{BackendDeviceLost}"), std::string::npos) << log; EXPECT_EQ(Count(log, "reaped exit=75"), 1u) << "the lost session did not end latched:\n" << log; EXPECT_EQ(Count(log, "reaped exit=0"), 1u) << "the fresh session did not end cleanly:\n" << log; // A fresh session latches the moment the client carries anything of the lost one it // checks: the applier-reset serial it counts from 0, a context binding naming a token it // never created, a create or bind it cannot parse - each Fatal{ProtocolCorruption}. EXPECT_EQ(Count(log, "SessionLatch{"), 1u) << "the recovered session latched too:\n" << log; EXPECT_EQ(log.find("Fatal{ProtocolCorruption"), std::string::npos) << "the recovered session was sent something it never created:\n" << log; EXPECT_TRUE(server.pid > 0 && ::kill(server.pid, 0) == 0) << "the server died"; } }; } // namespace // THE HEADLINE: Chrome's route. The lost context is reported lost, a new one is created on a fresh // session and renders into the surface the application kept, and destroying the old one is silent. TEST_F(ClientRecovery, ANewContextAfterADeviceLossRendersOnAFreshSession) { ServerProcess server; ASSERT_TRUE(Launch(&server, "newctx")) << server.Log(); ClientLog client("newctx"); const RecoveryReport r = RunClient(server, Route::NewContext, client.base); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); ExpectLossReportedAndRecovered(r, diagnostics); EXPECT_EQ(r.recordsByOldDestroy, 0) << "destroying the lost context published records on the new session"; // The status is UNKNOWN for a reason, and the client log names it once. EXPECT_EQ(Count(client.Text(), "GL_UNKNOWN_CONTEXT_RESET for a context of a lost session"), 1u) << client.Text(); ExpectOneLossAndACleanSecondSession(server); server.Stop(); } // ANGLE'S OWN NATIVE CONTEXT. ANGLE's GL-on-EGL backend (Chrome's --use-angle=gles) creates one ES // context per display with EGL 1.5's name of the reset attribute, and learns of a reset ONLY through // that context's reset status - which it then hands to Chrome. The same recovery, in its shape. TEST_F(ClientRecovery, AnglesNativeContextShapeIsToldOfTheLossAndRecovers) { ServerProcess server; ASSERT_TRUE(Launch(&server, "angle")) << server.Log(); ClientLog client("angle"); const RecoveryReport r = RunClient(server, Route::NewContext, client.base, EGL_OPENGL_ES_API, kAngleNativeAttribs); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); ExpectLossReportedAndRecovered(r, diagnostics); ExpectOneLossAndACleanSecondSession(server); server.Stop(); } // NO RESET NOTIFICATION (RunUntoldClient): the explicit core-profile context is not told through // GL, the compatibility-profile one is; both learn it from EGL, and the application recovers. TEST_F(ClientRecovery, ContextsThatDidNotAskAreToldOnlyWhereRelaxedAndStillRecoverThroughEgl) { ServerProcess server; ASSERT_TRUE(Launch(&server, "untold")) << server.Log(); ClientLog client("untold"); const UntoldReport r = RunInClient(server, client.base, &RunUntoldClient); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; ASSERT_EQ(r.firstPixel, kGreen) << std::hex << r.firstPixel << "\n" << diagnostics; ASSERT_EQ(r.lossSeen, 1) << "the injected loss never latched\n" << diagnostics; EXPECT_EQ(r.coreStrategy, GL_NO_RESET_NOTIFICATION); EXPECT_EQ(r.coreStatus, static_cast(GL_NO_ERROR)) << "a NO_RESET_NOTIFICATION core-profile context was told of the reset: 0x" << std::hex << r.coreStatus; EXPECT_NE(r.coreError, static_cast(GL_CONTEXT_LOST)); EXPECT_EQ(r.compatStatus, static_cast(GL_UNKNOWN_CONTEXT_RESET)) << "the compatibility-profile context was not told: 0x" << std::hex << r.compatStatus; EXPECT_EQ(r.compatError, static_cast(GL_CONTEXT_LOST)); EXPECT_EQ(r.coreSwap, EGL_FALSE); EXPECT_EQ(r.coreSwapError, EGL_CONTEXT_LOST) << std::hex << r.coreSwapError; EXPECT_EQ(r.coreAgain, EGL_FALSE); EXPECT_EQ(r.coreAgainError, EGL_CONTEXT_LOST) << std::hex << r.coreAgainError; ASSERT_EQ(r.freshCurrent, 1) << r.note << "\n" << diagnostics; EXPECT_EQ(r.freshStatus, static_cast(GL_NO_ERROR)); EXPECT_EQ(r.freshPixel, kBlue) << std::hex << r.freshPixel << "\n" << diagnostics; ExpectOneLossAndACleanSecondSession(server); server.Stop(); } // The other route back: everything destroyed, eglTerminate, eglInitialize, and a fresh start. TEST_F(ClientRecovery, TerminateAndInitializeAfterADeviceLossBringsUpAFreshSession) { ServerProcess server; ASSERT_TRUE(Launch(&server, "reinit")) << server.Log(); ClientLog client("reinit"); const RecoveryReport r = RunClient(server, Route::TerminateInitialize, client.base); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); ExpectLossReportedAndRecovered(r, diagnostics); ExpectOneLossAndACleanSecondSession(server); server.Stop(); } // THE OTHER THREAD: the lost context is still current on a second thread while the main thread // recovers. Everything that thread does afterwards is refused as lost and reaches nothing of the // fresh session - without the per-thread answer its first GL call would bind the lost context's // token on the fresh session, which latches it Fatal{ProtocolCorruption, "BindContext..."}. TEST_F(ClientRecovery, AThreadStillOnTheLostContextReachesNothingOfTheFreshSession) { ServerProcess server; ASSERT_TRUE(Launch(&server, "stale")) << server.Log(); ClientLog client("stale"); const StaleThreadReport r = RunInClient(server, client.base, &RunStaleThreadClient); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; ASSERT_EQ(r.firstPixel, kGreen) << std::hex << r.firstPixel << "\n" << diagnostics; ASSERT_EQ(r.newCurrentOk, 1) << r.note << "\n" << diagnostics; EXPECT_EQ(r.drawnPixel, kRed) << "the fresh context did not render: " << std::hex << r.drawnPixel; EXPECT_TRUE(r.staleResetStatus == GL_UNKNOWN_CONTEXT_RESET || r.staleResetStatus == GL_GUILTY_CONTEXT_RESET) << "the thread on the lost context sees no reset: 0x" << std::hex << r.staleResetStatus; EXPECT_NE(r.stalePixel, kWhite) << "a read-back on the lost context returned a frame"; EXPECT_EQ(r.staleSwap, EGL_FALSE); EXPECT_EQ(r.staleSwapError, EGL_CONTEXT_LOST) << std::hex << r.staleSwapError; EXPECT_EQ(r.staleRelease, 1) << "the second thread could not release the lost context"; EXPECT_EQ(r.recordsByStaleThread, 0) << "the thread on the lost context published records on the fresh session"; EXPECT_EQ(r.destroyOldOk, 1); EXPECT_EQ(r.pixelAfterStale, kRed) << "the fresh context stopped rendering: " << std::hex << r.pixelAfterStale << "\n" << diagnostics; EXPECT_EQ(r.resetStatusAfterStale, static_cast(GL_NO_ERROR)) << "the fresh session was lost too"; ExpectOneLossAndACleanSecondSession(server); server.Stop(); } // QT'S ROUTE: the windows' contexts are recreated still naming the long-lived share context, which // is lost. They are created, in ONE new share group (the lost group's objects are on no server): // a texture made on one is drawn on the other. Red with the share refused (EGL_BAD_CONTEXT): Qt // then falls back to an unshared context per window. TEST_F(ClientRecovery, ContextsNamingALostShareContextShareOneNewGroup) { ServerProcess server; ASSERT_TRUE(Launch(&server, "share")) << server.Log(); ClientLog client("share"); const ShareReport r = RunInClient(server, client.base, &RunShareReplacementClient); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; ASSERT_EQ(r.firstOk, 1) << "eglCreateContext naming the lost share context failed (0x" << std::hex << r.firstError << ")\n" << r.note; ASSERT_EQ(r.secondOk, 1) << r.note; EXPECT_EQ(r.sameGroup, 1) << "the replacements landed in separate share groups"; EXPECT_EQ(r.lostGroupReused, 0) << "a replacement joined the lost share group"; EXPECT_EQ(r.textureSeenAcross, 1) << "a texture of one replacement is not the other's"; EXPECT_EQ(r.drawnAcross, kRed) << "the shared texture did not draw on the other replacement: " << std::hex << r.drawnAcross << "\n" << r.note << "\n" << diagnostics; ExpectOneLossAndACleanSecondSession(server); server.Stop(); } // THE SESSION-TARGETED KNOB (what the device test sets on a running desktop): a bystander client // of the same server draws on untouched; the target is lost although it only clears and reads // back - no present - recovers on a fresh session, and is not lost again by the same setting. TEST_F(ClientRecovery, ThePidTargetedKnobLosesOnlyThatClientAndOnlyOnce) { auto targetBody = [](TargetedReport& r) { RunTargetedClient(r); }; HeldClient target; ClientLog targetLog("pid-target"); ASSERT_TRUE(target.Fork(targetLog.base, targetBody)); ServerProcess server; ::setenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID", std::to_string(target.pid).c_str(), 1); const bool launched = LaunchUnixInProcessServer(&server, "pid"); ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID"); if (!launched) { (void)target.Release(""); FAIL() << server.Log(); } ClientLog bystanderLog("pid-bystander"); const BystanderReport bystander = RunInClient(server, bystanderLog.base, &RunBystanderClient); const TargetedReport r = target.Release(server.endpoint); const std::string diagnostics = "server:\n" + server.Log() + "\ntarget:\n" + targetLog.Text() + "\nbystander:\n" + bystanderLog.Text(); ASSERT_EQ(bystander.initialized, 1) << bystander.note << "\n" << diagnostics; EXPECT_GT(bystander.frames, 5); EXPECT_EQ(bystander.lastPixel, kRed) << std::hex << bystander.lastPixel; EXPECT_EQ(bystander.resetStatus, static_cast(GL_NO_ERROR)) << "the bystander was lost"; ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; ASSERT_EQ(r.lossSeen, 1) << "the targeted client was never lost\n" << diagnostics; EXPECT_EQ(r.resetStatus, static_cast(GL_UNKNOWN_CONTEXT_RESET)) << std::hex << r.resetStatus; ASSERT_EQ(r.freshCurrent, 1) << r.note << "\n" << diagnostics; EXPECT_GT(r.freshFrames, 5); EXPECT_EQ(r.freshPixel, kBlue) << std::hex << r.freshPixel << "\n" << diagnostics; EXPECT_EQ(r.freshStatusEnd, static_cast(GL_NO_ERROR)) << "the recovered session was lost again"; // Three sessions: the bystander's and the recovered one end cleanly, the target's latched on // the knob, naming the pid. ASSERT_TRUE(WaitFor([&] { return Count(server.Log(), "reaped exit=") == 3; }, 10000)) << server.Log(); const std::string log = server.Log(); EXPECT_EQ(Count(log, "SessionLatch{"), 1u) << log; EXPECT_NE(log.find("injected:pid=" + std::to_string(target.pid)), std::string::npos) << log; EXPECT_EQ(Count(log, "reaped exit=75"), 1u) << log; EXPECT_EQ(Count(log, "reaped exit=0"), 2u) << log; server.Stop(); } // THE ES IDENTITY (RunEsIdentityClient): an ES context names itself OpenGL ES 3.2 and offers BGRA // the ES way, a desktop context of the same process does not change, and the dispatcher's API wins. TEST_F(ClientRecovery, AnEsContextUnderTheEsIdentityIsOpenGlEs32WithBgraTextures) { ServerProcess server; ASSERT_TRUE(LaunchUnixInProcessServer(&server, "esid")) << server.Log(); ClientLog client("esid"); const EsIdentityReport r = RunInClient(server, client.base, &RunEsIdentityClient); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); EXPECT_EQ(r.dispatcherApiWins, 1) << "the context was not created for the API the dispatcher reports"; ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; EXPECT_EQ(std::string(r.esVersion).rfind("OpenGL ES 3.2 ", 0), 0u) << r.esVersion; EXPECT_STREQ(r.esGlsl, "OpenGL ES GLSL ES 3.20"); EXPECT_EQ(r.esMajor, 3); EXPECT_EQ(r.esMinor, 2); EXPECT_EQ(r.bgraAdvertised, 1); EXPECT_EQ(r.bgraIndexed, 1); EXPECT_EQ(r.bgraImageError, static_cast(GL_NO_ERROR)) << std::hex << r.bgraImageError; EXPECT_EQ(r.bgraStorageError, static_cast(GL_NO_ERROR)) << std::hex << r.bgraStorageError; EXPECT_EQ(r.bgraFboStatus, static_cast(GL_FRAMEBUFFER_COMPLETE)) << std::hex << r.bgraFboStatus; EXPECT_EQ(r.bgraPixel, 0xFF102030u) << "a BGRA texel did not read back in RGBA order: " << std::hex << r.bgraPixel; EXPECT_EQ(std::string(r.desktopVersion).find("OpenGL ES"), std::string::npos) << "a desktop context took the ES identity: " << r.desktopVersion; EXPECT_NE(std::string(r.desktopVersion), "") << diagnostics; server.Stop(); } // Chrome's GPU process's own shape: ANGLE's native ES context, under the ES identity, told of the // loss and recovering like any other. TEST_F(ClientRecovery, AnglesNativeContextUnderTheEsIdentityRecovers) { ServerProcess server; ASSERT_TRUE(Launch(&server, "angle-es")) << server.Log(); ClientLog client("angle-es"); const RecoveryReport r = RunInClient(server, client.base, [](RecoveryReport& report) { ::setenv("MOBILEGL_ES_CONTEXT_IDENTITY", "1", 1); RunRecoveringClient(Route::NewContext, report, EGL_OPENGL_ES_API, kAngleNativeAttribs); }); const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); ExpectLossReportedAndRecovered(r, diagnostics); ExpectOneLossAndACleanSecondSession(server); server.Stop(); }