mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1361 lines
71 KiB
C++
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();
|
|
}
|