Files
MobileGL/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp
T

1361 lines
71 KiB
C++

// 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 <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Impl/Pipe/TextureEmit.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_State/EGLState/Core.h>
#include <condition_variable>
#include <mutex>
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<const char*>(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<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(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<ResetStatusFn>(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<Int64>(PublishedSeq()) - static_cast<Int64>(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<std::mutex> lock(m_mutex);
m_quit = true;
}
m_wake.notify_all();
m_thread.join();
}
void Run(const std::function<void()>& job) {
std::unique_lock<std::mutex> lock(m_mutex);
m_job = job;
m_wake.notify_all();
m_wake.wait(lock, [this] { return !m_job; });
}
private:
void Loop() {
std::unique_lock<std::mutex> lock(m_mutex);
for (;;) {
m_wake.wait(lock, [this] { return m_quit || static_cast<bool>(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<void()> 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<Int64>(PublishedSeq()) - static_cast<Int64>(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 <class Report, class Body>
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<ssize_t>(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 <class Report, class Body>
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<ssize_t>(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<const char*>(glGetString(GL_VERSION)));
std::snprintf(r.esGlsl, sizeof(r.esGlsl), "%s",
reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION)));
glGetIntegerv(GL_MAJOR_VERSION, &r.esMajor);
glGetIntegerv(GL_MINOR_VERSION, &r.esMinor);
r.bgraAdvertised = HasToken(reinterpret_cast<const char*>(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<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(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<const char*>(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<RecoveryReport>(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<Uint32>(GL_CONTEXT_LOST))
<< "glGetError after the loss: 0x" << std::hex << r.errorLost;
EXPECT_EQ(r.errorAfterLost, static_cast<Uint32>(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<Uint32>(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<Uint32>(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<Uint32>(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<UntoldReport>(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<Uint32>(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<Uint32>(GL_CONTEXT_LOST));
EXPECT_EQ(r.compatStatus, static_cast<Uint32>(GL_UNKNOWN_CONTEXT_RESET))
<< "the compatibility-profile context was not told: 0x" << std::hex << r.compatStatus;
EXPECT_EQ(r.compatError, static_cast<Uint32>(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<Uint32>(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<StaleThreadReport>(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<Uint32>(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<ShareReport>(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<TargetedReport, decltype(targetBody)> 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<BystanderReport>(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<Uint32>(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<Uint32>(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<Uint32>(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<EsIdentityReport>(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<Uint32>(GL_NO_ERROR)) << std::hex << r.bgraImageError;
EXPECT_EQ(r.bgraStorageError, static_cast<Uint32>(GL_NO_ERROR)) << std::hex << r.bgraStorageError;
EXPECT_EQ(r.bgraFboStatus, static_cast<Uint32>(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<RecoveryReport>(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();
}