mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
794 lines
41 KiB
C++
794 lines
41 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: glGetGraphicsResetStatus answers a reset, glGetError GL_CONTEXT_LOST
|
|
// (once), eglSwapBuffers and eglMakeCurrent on the old context fail with EGL_CONTEXT_LOST;
|
|
// * 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 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;
|
|
char note[256] = {};
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
struct Egl {
|
|
EGLDisplay dpy = EGL_NO_DISPLAY;
|
|
EGLConfig config = nullptr;
|
|
EGLSurface surface = EGL_NO_SURFACE;
|
|
|
|
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(EGL_OPENGL_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 {
|
|
const EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE};
|
|
return EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, attribs);
|
|
}
|
|
};
|
|
|
|
// The client, in a fork: everything an application does around a loss, through the entry points.
|
|
void RunRecoveringClient(Route route, RecoveryReport& r) {
|
|
Egl egl;
|
|
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();
|
|
|
|
// 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();
|
|
// 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;
|
|
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();
|
|
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[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE};
|
|
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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
RecoveryReport RunClient(const ServerProcess& server, Route route, const std::string& clientLogBase) {
|
|
return RunInClient<RecoveryReport>(server, clientLogBase,
|
|
[route](RecoveryReport& r) { RunRecoveringClient(route, r); });
|
|
}
|
|
|
|
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";
|
|
// 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)";
|
|
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.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";
|
|
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();
|
|
}
|