[Feature] (EGL+GL): advertise EGL_EXT_create_context_robustness and GL robustness, with per-context reset notification and a pid-targeted device-loss knob.

This commit is contained in:
2026-10-03 04:37:33 -04:00
parent af2b9f4c1c
commit 90e63c9e2a
19 changed files with 963 additions and 40 deletions
@@ -18,9 +18,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
inline constexpr const char* kPlatformExtensionString =
"EGL_EXT_client_extensions EGL_EXT_platform_base EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
// EGL_EXT_create_context_robustness: a context may ask for EGL_LOSE_CONTEXT_ON_RESET, and a
// split client's contexts are lost exactly when their server session is (a device loss on the
// server ends that session). An application learns of it through glGetGraphicsResetStatus only
// if it could ask for it here: ANGLE's GL-on-EGL backend mirrors this extension onto its own
// display, and Chrome checks that display for it before it ever queries the reset status.
inline constexpr const char* kDisplayExtensionString =
"EGL_KHR_create_context EGL_KHR_surfaceless_context EGL_KHR_no_config_context EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_EXT_create_context_robustness EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
// The display list again, with the dma-buf import a backend with shared images has (a split
// client: the server allocates the images and names the descriptors that come back). A
@@ -28,7 +33,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
// same server answers the buffer age of its surfaces and takes swap damage to its presents.
inline constexpr const char* kDisplayExtensionStringWithSharedImages =
"EGL_KHR_create_context EGL_KHR_surfaceless_context EGL_KHR_no_config_context EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_EXT_create_context_robustness EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS
" EGL_KHR_image_base EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers"
" EGL_EXT_buffer_age EGL_KHR_partial_update EGL_KHR_swap_buffers_with_damage"
@@ -435,9 +435,9 @@ DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachm
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value)
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
@@ -1121,7 +1121,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetTextureSubImage, GLuint texture, GLint level,
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnCompressedTexImage, GLenum target, GLint lod, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnCompressedTexImage, target, lod, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImage, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImage, target, level, format, type, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformdv, GLuint program, GLint location, GLsizei bufSize, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformdv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_HEAD(void, GetnUniformdv, GLuint program, GLint location, GLsizei bufSize, GLdouble* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetnUniformdv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMapdv, GLenum target, GLenum query, GLsizei bufSize, GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMapdv, target, query, bufSize, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMapfv, GLenum target, GLenum query, GLsizei bufSize, GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMapfv, target, query, bufSize, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMapiv, GLenum target, GLenum query, GLsizei bufSize, GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMapiv, target, query, bufSize, v)
@@ -1292,7 +1292,6 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfARB, GLenum pname, GLfloat pa
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvARB, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvARB, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImageARB, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImageARB, target, level, format, type, bufSize, img)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnCompressedTexImageARB, GLenum target, GLint lod, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnCompressedTexImageARB, target, lod, bufSize, img)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformdvARB, GLuint program, GLint location, GLsizei bufSize, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformdvARB, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMapdvARB, GLenum target, GLenum query, GLsizei bufSize, GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMapdvARB, target, query, bufSize, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMapfvARB, GLenum target, GLenum query, GLsizei bufSize, GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMapfvARB, target, query, bufSize, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMapivARB, GLenum target, GLenum query, GLsizei bufSize, GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMapivARB, target, query, bufSize, v)
@@ -2993,6 +2992,50 @@ MOBILEGL_GL_API void glGetnUniformuivARB(GLuint program, GLint location, GLsizei
glGetnUniformuiv(program, location, bufSize, params);
}
MOBILEGL_GL_API void glGetnUniformdvARB(GLuint program, GLint location, GLsizei bufSize, GLdouble* params) {
glGetnUniformdv(program, location, bufSize, params);
}
// KHR_robustness and EXT_robustness name the same entry points with their own suffixes (an ES
// application, ANGLE's GL backend among them, looks them up by those names).
MOBILEGL_GL_API GLenum glGetGraphicsResetStatusKHR(void) {
return glGetGraphicsResetStatus();
}
MOBILEGL_GL_API GLenum glGetGraphicsResetStatusEXT(void) {
return glGetGraphicsResetStatus();
}
MOBILEGL_GL_API void glReadnPixelsKHR(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
GLsizei bufSize, void* data) {
glReadnPixels(x, y, width, height, format, type, bufSize, data);
}
MOBILEGL_GL_API void glReadnPixelsEXT(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
GLsizei bufSize, void* data) {
glReadnPixels(x, y, width, height, format, type, bufSize, data);
}
MOBILEGL_GL_API void glGetnUniformfvKHR(GLuint program, GLint location, GLsizei bufSize, GLfloat* params) {
glGetnUniformfv(program, location, bufSize, params);
}
MOBILEGL_GL_API void glGetnUniformfvEXT(GLuint program, GLint location, GLsizei bufSize, GLfloat* params) {
glGetnUniformfv(program, location, bufSize, params);
}
MOBILEGL_GL_API void glGetnUniformivKHR(GLuint program, GLint location, GLsizei bufSize, GLint* params) {
glGetnUniformiv(program, location, bufSize, params);
}
MOBILEGL_GL_API void glGetnUniformivEXT(GLuint program, GLint location, GLsizei bufSize, GLint* params) {
glGetnUniformiv(program, location, bufSize, params);
}
MOBILEGL_GL_API void glGetnUniformuivKHR(GLuint program, GLint location, GLsizei bufSize, GLuint* params) {
glGetnUniformuiv(program, location, bufSize, params);
}
MOBILEGL_GL_API void glGetObjectLabelEXT(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length,
GLchar* label) {
glGetObjectLabel(identifier, name, bufSize, length, label);
+98 -14
View File
@@ -592,12 +592,60 @@ namespace MobileGL::MG_Impl::GLImpl {
}
namespace {
// GL_OES_EGL_image binds the EGL side's dma-buf images, which only a backend with shared
// images has (EGLImpl::SharedImagesAvailable). The backend's own list - the server's,
// under split - cannot know that, so the extension is appended after it.
const char* ClientSideExtension() {
return MG_Impl::EGLImpl::SharedImagesAvailable() ? "GL_OES_EGL_image" : nullptr;
// Extensions this library implements itself, appended after the backend's own list (the
// server's, under split), which cannot know about them:
// * GL_OES_EGL_image binds the EGL side's dma-buf images, which only a backend with
// shared images has (EGLImpl::SharedImagesAvailable);
// * the robustness family - reset notification (glGetGraphicsResetStatus, the contexts'
// EGL reset strategy, GL_CONTEXT_LOST) and the bounded queries (glReadnPixels,
// glGetnUniform*) are this frontend's, whatever the backend. KHR for GL and ES, ARB for
// GL applications that ask for that name, EXT for ES applications that ask for that one.
// Robust buffer ACCESS is a separate promise (GL_CONTEXT_ROBUST_ACCESS) and is not
// implied by any of them.
// One list for glGetString and glGetStringi, so the two forms always agree.
const Vector<const char*>& ClientSideExtensions() {
static const Vector<const char*> withImages = {"GL_OES_EGL_image", "GL_KHR_robustness",
"GL_ARB_robustness", "GL_EXT_robustness"};
static const Vector<const char*> withoutImages = {"GL_KHR_robustness", "GL_ARB_robustness",
"GL_EXT_robustness"};
return MG_Impl::EGLImpl::SharedImagesAvailable() ? withImages : withoutImages;
}
// Robust buffer access as GL 4.5 / KHR_robust_buffer_access_behavior define it: the backend
// promises it by listing one of the behavior extensions. Neither backend does today, so a
// context that asked for robust access gets it reported as absent rather than claimed.
Bool BackendProvidesRobustBufferAccess() {
const auto& backend = MG_Backend::pActiveBackendObject;
if (!backend) return false;
const auto& extensions = backend->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), E_GL_KHR_robust_buffer_access_behavior) !=
extensions.end() ||
std::find(extensions.begin(), extensions.end(), E_GL_ARB_robust_buffer_access_behavior) !=
extensions.end();
}
Bool CurrentContextHasRobustAccess() {
return MG_State::pEGLContext && MG_State::pEGLContext->IsCurrentContextRobustAccessRequested() &&
BackendProvidesRobustBufferAccess();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// KHR_robustness 2.6.1: a context whose reset notification behavior is
// NO_RESET_NOTIFICATION is never told of a reset - glGetGraphicsResetStatus answers
// GL_NO_ERROR and no command raises GL_CONTEXT_LOST. That is what an explicit core-profile
// context gets. Everywhere else (MG_State::IsRelaxedSemanticsActive) the loss is reported
// to every context, as before the strategy existed: an application that queries the reset
// status without having asked for notification still learns that its context is gone,
// which on a split client it is - there is nothing to recover in place. A context created
// with EGL_LOSE_CONTEXT_ON_RESET is always told.
Bool ResetIsNotified() {
if (MG_State::pEGLContext &&
MG_State::pEGLContext->GetCurrentContextResetNotificationStrategy() == EGL_LOSE_CONTEXT_ON_RESET) {
return true;
}
return MG_State::IsRelaxedSemanticsActive();
}
#endif
} // namespace
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
@@ -664,7 +712,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
extensionsString += MG_Util::ConvertGLExtToString(ext);
}
if (const char* extra = ClientSideExtension()) {
for (const char* extra : ClientSideExtensions()) {
if (!extensionsString.empty()) extensionsString += " ";
extensionsString += extra;
}
@@ -688,8 +736,9 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
const auto& exts = rendererInfo.RendererGLInfo.Extensions;
if (index == exts.size()) {
if (const char* extra = ClientSideExtension()) return (const GLubyte*)extra;
if (index >= exts.size()) {
const auto& extras = ClientSideExtensions();
if (index - exts.size() < extras.size()) return (const GLubyte*)extras[index - exts.size()];
}
if (index >= exts.size()) {
return nullptr;
@@ -1675,8 +1724,20 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
case GL_CONTEXT_FLAGS: {
*params = MG_State::pEGLContext ? MG_State::pEGLContext->GetCurrentContextFlags() : 0;
if (CurrentContextHasRobustAccess()) *params |= GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT;
return;
}
case GL_CONTEXT_ROBUST_ACCESS:
// GL ES 3.2 / KHR_robustness: true only where it was asked for AND is provided.
*params = CurrentContextHasRobustAccess() ? GL_TRUE : GL_FALSE;
return;
case GL_RESET_NOTIFICATION_STRATEGY:
// The EGL reset notification behavior the context was created with.
*params = MG_State::pEGLContext && MG_State::pEGLContext->GetCurrentContextResetNotificationStrategy() ==
EGL_LOSE_CONTEXT_ON_RESET
? GL_LOSE_CONTEXT_ON_RESET
: GL_NO_RESET_NOTIFICATION;
return;
case GL_CULL_FACE:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace) ? GL_TRUE : GL_FALSE;
return;
@@ -2812,8 +2873,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
break;
case GL_NUM_EXTENSIONS:
*params = static_cast<Int>(rendererInfo.RendererGLInfo.Extensions.size()) +
(ClientSideExtension() != nullptr ? 1 : 0);
*params = static_cast<Int>(rendererInfo.RendererGLInfo.Extensions.size() + ClientSideExtensions().size());
break;
case GL_POINT_SIZE_GRANULARITY:
*params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
@@ -2883,7 +2943,9 @@ namespace MobileGL::MG_Impl::GLImpl {
// context and ahead of whatever else is queued, so an application that learns of the loss
// from glGetError (Chromium's decoder does) learns of it, and one that drains errors in a
// loop until GL_NO_ERROR still terminates.
if (MG_Remote::Client::ClientSession::DeviceLost() && MG_State::pGLContext &&
// Only where the reset is notified at all (ResetIsNotified: not on a NO_RESET_NOTIFICATION
// explicit core-profile context).
if (MG_Remote::Client::ClientSession::DeviceLost() && MG_State::pGLContext && ResetIsNotified() &&
MG_State::pGLContext->TakeContextLostReport()) {
return GL_CONTEXT_LOST;
}
@@ -2907,13 +2969,35 @@ namespace MobileGL::MG_Impl::GLImpl {
//
// GL_UNKNOWN_CONTEXT_RESET, not GUILTY or INNOCENT. Those two assign BLAME - guilty
// means this context's commands caused the reset, innocent means another context's did -
// and neither is knowable here: the server died for a reason that never crossed the
// wire, and it could as easily have been a driver fault as this client's draw. UNKNOWN is
// the spec's answer for exactly that, and it is the one that does not lie.
// and neither is knowable here:
// * a session ends for a fault the server names by FAMILY only (BackendDeviceLost, a
// protocol fault, ...) or by hanging up; nothing it sends attributes the fault to a
// context, and a hangup is a server death, not a GPU reset at all;
// * a GPU device loss is not attributable either: Vulkan's device loss carries no
// culprit, and the host GL driver's own GUILTY/INNOCENT (which Espryt reads when it
// latches) is about whichever backend context the server had current, while the
// session ends as a whole - every context of it is lost with that one;
// * INNOCENT would mean another context's fault reset this one, but a loss ends exactly
// the session that faulted (every other session keeps running), so the contexts that
// see it are all in the faulting session.
// UNKNOWN is the spec's answer for exactly that, and it is the one that does not lie. The
// cause - the latch's words, with the fault family where the server named one - is
// logged once per context, the first time its reset is reported.
//
// The status does not go back to GL_NO_ERROR: the context stays lost (KHR_robustness
// allows a reset status to be returned repeatedly while the reset "is in progress", and
// this one never completes - the context has to be recreated, which puts it on a fresh
// session).
//
// WHOLLY INSIDE THE GUARD, including the include above. In a pull build this function is
// byte-for-byte what it was, which is what G1 measures.
if (MG_Remote::Client::ClientSession::DeviceLost()) {
if (!ResetIsNotified()) return GL_NO_ERROR;
if (MG_State::pGLContext && MG_State::pGLContext->TakeResetStatusReport()) {
const String reason = MG_Remote::Client::ClientSession::LastLossReason();
MGLOG_W("glGetGraphicsResetStatus: GL_UNKNOWN_CONTEXT_RESET for a context of a lost session (%s)",
reason.empty() ? "the session was lost" : reason.c_str());
}
return GL_UNKNOWN_CONTEXT_RESET;
}
#endif
@@ -1380,6 +1380,36 @@ namespace MobileGL::MG_Impl::GLImpl {
GetUniformScalar_State(program, location, params);
}
// glGetnUniform* (GL 4.5 core, KHR_robustness): glGetUniform* bounded by bufSize. The query
// writes one value per component of the uniform at `location` (an opaque uniform's unit is
// one), so the bound is that count times the element size; a destination smaller than that -
// a negative bufSize included - is GL_INVALID_OPERATION and nothing is written. Every other
// error is the unbounded query's, raised by it.
template <typename T>
void GetnUniform_State(GLuint program, GLint location, GLsizei bufSize, T* params) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (programObject->GetLinkStatus() && programObject->IsValidUniformLocation(location)) {
SizeT components = 1;
if (!programObject->IsUniformOpaqueAtLocation(location)) {
const auto& type = programObject->GetUniformTypeFacts(location);
components = type.isMatrix ? static_cast<SizeT>(type.matrixCols) * static_cast<SizeT>(type.matrixRows)
: type.isVector ? static_cast<SizeT>(type.vectorSize)
: 1;
}
if (bufSize < 0 || components * sizeof(T) > static_cast<SizeT>(bufSize)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"the uniform's " + std::to_string(components) +
" component(s) do not fit in bufSize " + std::to_string(bufSize) +
"."));
return;
}
}
GetUniformScalar_State(program, location, params);
}
GLboolean IsProgram_State(GLuint program) {
// Deletion-flagged names stay valid while the object is still GL-visible (program in
// use, shader attached), so name validity is exactly the Is* answer.
@@ -2553,6 +2583,22 @@ namespace MobileGL::MG_Impl::GLImpl {
GetUniformuiv_State(program, location, params);
}
void GetnUniformfv(GLuint program, GLint location, GLsizei bufSize, GLfloat* params) {
GetnUniform_State(program, location, bufSize, params);
}
void GetnUniformiv(GLuint program, GLint location, GLsizei bufSize, GLint* params) {
GetnUniform_State(program, location, bufSize, params);
}
void GetnUniformuiv(GLuint program, GLint location, GLsizei bufSize, GLuint* params) {
GetnUniform_State(program, location, bufSize, params);
}
void GetnUniformdv(GLuint program, GLint location, GLsizei bufSize, GLdouble* params) {
GetnUniform_State(program, location, bufSize, params);
}
GLboolean IsProgram(GLuint program) {
return IsProgram_State(program);
}
@@ -46,6 +46,11 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetUniformfv(GLuint program, GLint location, GLfloat* params);
void GetUniformiv(GLuint program, GLint location, GLint* params);
void GetUniformuiv(GLuint program, GLint location, GLuint* params);
// GL 4.5 / KHR_robustness: the queries above, bounded by bufSize.
void GetnUniformfv(GLuint program, GLint location, GLsizei bufSize, GLfloat* params);
void GetnUniformiv(GLuint program, GLint location, GLsizei bufSize, GLint* params);
void GetnUniformuiv(GLuint program, GLint location, GLsizei bufSize, GLuint* params);
void GetnUniformdv(GLuint program, GLint location, GLsizei bufSize, GLdouble* params);
GLboolean IsProgram(GLuint program);
GLboolean IsShader(GLuint shader);
void LinkProgram(GLuint program);
+22
View File
@@ -29,6 +29,19 @@ extern "C" EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay dpy, EGLint format,
khronos_uint64_t* modifiers, EGLBoolean* external_only,
EGLint* num_modifiers);
extern "C" void glEGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image);
// KHR_robustness / EXT_robustness suffixed names (Definitions.cpp); the desktop headers declare
// only the unsuffixed and ARB ones.
extern "C" GLenum glGetGraphicsResetStatusKHR(void);
extern "C" GLenum glGetGraphicsResetStatusEXT(void);
extern "C" void glReadnPixelsKHR(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
GLsizei bufSize, void* data);
extern "C" void glReadnPixelsEXT(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
GLsizei bufSize, void* data);
extern "C" void glGetnUniformfvKHR(GLuint program, GLint location, GLsizei bufSize, GLfloat* params);
extern "C" void glGetnUniformfvEXT(GLuint program, GLint location, GLsizei bufSize, GLfloat* params);
extern "C" void glGetnUniformivKHR(GLuint program, GLint location, GLsizei bufSize, GLint* params);
extern "C" void glGetnUniformivEXT(GLuint program, GLint location, GLsizei bufSize, GLint* params);
extern "C" void glGetnUniformuivKHR(GLuint program, GLint location, GLsizei bufSize, GLuint* params);
#define GETPROC(name, var) \
if (strcmp(#name, var) == 0) { \
@@ -876,6 +889,15 @@ namespace MobileGL::MG_Impl {
GETPROC(glGetnUniformivARB, name);
GETPROC(glGetnUniformuivARB, name);
GETPROC(glGetnUniformdvARB, name);
GETPROC(glGetGraphicsResetStatusKHR, name);
GETPROC(glGetGraphicsResetStatusEXT, name);
GETPROC(glReadnPixelsKHR, name);
GETPROC(glReadnPixelsEXT, name);
GETPROC(glGetnUniformfvKHR, name);
GETPROC(glGetnUniformfvEXT, name);
GETPROC(glGetnUniformivKHR, name);
GETPROC(glGetnUniformivEXT, name);
GETPROC(glGetnUniformuivKHR, name);
GETPROC(glFramebufferSampleLocationsfvARB, name);
GETPROC(glNamedFramebufferSampleLocationsfvARB, name);
GETPROC(glEvaluateDepthValuesARB, name);
+42
View File
@@ -54,6 +54,7 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdarg>
#include <cstdio>
@@ -209,6 +210,47 @@ namespace MobileGL::MG_Pipe {
return true;
}
bool MGPipeDebugSessionLossDue(unsigned clientPid) {
if (clientPid == 0) return false;
// The first call on a thread only starts the clock, so a session is first asked 100 ms
// after it started rendering - never in the middle of its first make-current.
thread_local auto lastAsked = std::chrono::steady_clock::time_point{};
const auto now = std::chrono::steady_clock::now();
if (lastAsked == std::chrono::steady_clock::time_point{}) {
lastAsked = now;
return false;
}
if (now - lastAsked < std::chrono::milliseconds(100)) return false;
lastAsked = now;
long target = 0;
if (const char* value = std::getenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID")) {
target = std::strtol(value, nullptr, 10);
}
#if defined(__ANDROID__)
else {
char property[PROP_VALUE_MAX] = {};
if (__system_property_get("debug.mobilegl.inject_device_lost_pid", property) > 0) {
target = std::strtol(property, nullptr, 10);
}
}
#endif
// Once per value: the session it hit is over, and a later session of the same process (the
// recovery's) must not be lost again by the same setting. Another pid - or unset/0 and
// then the same one - re-arms it.
static std::atomic<long> firedFor{0};
if (target <= 0) {
if (firedFor.load(std::memory_order_relaxed) != 0) firedFor.store(0, std::memory_order_relaxed);
return false;
}
if (static_cast<unsigned long>(target) != clientPid) return false;
long previous = firedFor.load(std::memory_order_acquire);
if (previous == target || !firedFor.compare_exchange_strong(previous, target, std::memory_order_acq_rel)) {
return false;
}
MGLOG_E("MGPipe: debug knob inject_device_lost_pid=%ld - this session's device is reported LOST", target);
return true;
}
bool MGPipeSessionLatch(MGPipeFatalFamily family, const char* fmt, ...) {
char line[512];
va_list args;
+9
View File
@@ -124,6 +124,15 @@ namespace MobileGL::MG_Pipe {
// several sessions in turn. The backends ask at their frame boundaries and readbacks; unset
// (the default) it is one getenv.
bool MGPipeDebugDeviceLossDue();
// THE SAME KNOB, AIMED AT ONE CLIENT. MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID=P (Android: the
// property debug.mobilegl.inject_device_lost_pid) loses the session whose client named pid P in
// its Hello (its own pid, as it sees it - a container's own pid namespace included), once per
// value, whatever the backend and whether or not that client ever presents or reads back: the
// server's apply thread asks after every drain. It is how one application among several is
// made to lose its device - a browser's GPU process, which renders into dma-bufs and never
// presents, included. Asked only for a session with a context bound; asks the environment /
// property at most every 100 ms per thread.
bool MGPipeDebugSessionLossDue(unsigned clientPid);
#endif
} // namespace MobileGL::MG_Pipe
+32 -2
View File
@@ -103,6 +103,27 @@ namespace MobileGL::MG_Remote::Client {
thread_local Uint32 tl_wireMute = 0;
thread_local Uint32 tl_currentSessionWork = 0;
std::atomic<Uint64> g_remoteSessionsStarted{0};
// ClientSession::LastLossReason(). Leaked on purpose: read from GL entry points that may
// run during process exit.
std::mutex& LossReasonMutex() {
static auto* mutex = new std::mutex();
return *mutex;
}
String& LossReason() {
static auto* reason = new String();
return *reason;
}
// The latch's words for a session the server ended on a named fault: what happened, and
// the fault family the server named (BackendDeviceLost for a GPU device loss).
String FaultReason(const ::MobileGL::Wire::CtrlEnvelope* envelope, const char* what) {
String reason = what;
if (const auto* fatal = envelope->msg_as_Fatal(); fatal != nullptr && fatal->family() != nullptr) {
reason += " (Fatal family ";
reason += fatal->family()->c_str();
reason += ")";
}
return reason;
}
// The same bounded handshake deadline the server uses. Bounded, not kWaitForever: a
// bring-up that never answers has to be a red lane rather than a wedged CI job.
@@ -614,6 +635,11 @@ namespace MobileGL::MG_Remote::Client {
return context != nullptr && context->IsFromEndedWireSession();
}
String ClientSession::LastLossReason() {
const std::lock_guard<std::mutex> lock(LossReasonMutex());
return LossReason();
}
Bool ClientSession::SessionLatchedLost() {
const ClientSession* session = Active();
return session != nullptr && session->m_deviceLost.load(std::memory_order_acquire);
@@ -633,6 +659,10 @@ namespace MobileGL::MG_Remote::Client {
if (m_deviceLost.exchange(true, std::memory_order_acq_rel)) {
return;
}
{
const std::lock_guard<std::mutex> lock(LossReasonMutex());
LossReason() = why == nullptr ? "the peer hung up" : why;
}
MGLOG_E("MGPipe: DEVICE LOST - %s. This session's server side is gone; every verb from here "
"is DECLINED and glGetGraphicsResetStatus reports GL_UNKNOWN_CONTEXT_RESET. This is "
"latched from the peer's HANGUP (or its named SessionFault) and not from any "
@@ -1645,7 +1675,7 @@ namespace MobileGL::MG_Remote::Client {
MGLOG_E("MG_Remote client: the server published a SessionFault before dying - "
"Fatal family %s: %s", fam, msg);
}
LatchDeviceLost("the server published a SessionFault and then aborted");
LatchDeviceLost(FaultReason(envelope, "the server published a SessionFault and then aborted").c_str());
frame.ok = false;
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
@@ -2742,7 +2772,7 @@ namespace MobileGL::MG_Remote::Client {
fatal->family() ? fatal->family()->c_str() : "<none>",
fatal->message() ? fatal->message()->c_str() : "");
}
LatchDeviceLost("the server published a SessionFault for this session");
LatchDeviceLost(FaultReason(envelope, "the server published a SessionFault for this session").c_str());
continue;
}
if (envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::CapsSnapshot) {
@@ -100,6 +100,10 @@ namespace MobileGL::MG_Remote::Client {
// Arms the latch and says why, once. Safe to call repeatedly and from any thread; only
// the first call logs.
void LatchDeviceLost(const char* why);
// Why the most recent session of this process was lost, in the latch's words (empty
// before any loss). Kept past the recovery, because the contexts of the lost session
// outlive it: glGetGraphicsResetStatus names it when it first reports a context's reset.
static String LastLossReason();
// A reply-owning record came back DECLINED where only a latched server declines (a
// readback). The server publishes its SessionFault on the control plane BEFORE it declines,
+13
View File
@@ -12,6 +12,7 @@
#include "ApplyThreadPolicy.h"
#include "SessionRuntime.h"
#include <MG_Remote/FatalFunnel.h>
#include <MG_Pipe/PipeSessionFail.h>
#include <MG_Remote/Transport/LinkMetrics.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
@@ -633,6 +634,18 @@ namespace MobileGL::MG_Remote::Server {
if (stopOrForfeit()) break;
if (eventRingHasRoom()) DrainRing();
if (stopOrForfeit()) break;
// The session-targeted device-loss debug knob (PipeSessionFail.h): asked here, after a
// drain, so it reaches a client that never presents or reads back, once the session
// has a context bound (it renders); latched as the backends latch a real loss. Unset
// it is one clock read per iteration.
if (!SessionLatched() && session.CurrentContextToken() != 0 &&
MG_Pipe::MGPipeDebugSessionLossDue(session.ClientPid())) {
(void)MG_Pipe::MGPipeSessionLatch(MG_Pipe::MGPipeFatalFamily::DeviceLost,
"MGPipe: Fatal{BackendDeviceLost, \"injected:pid=%u\"} - the debug "
"knob lost this session's device; the session ends and its client "
"reads a lost context, every other session keeps running",
session.ClientPid());
}
if (eventRingHasRoom() && !SessionLatched()) verbs.ReportSignaledFences(/*flush=*/false);
// PH-1 (3): a latched session applies nothing more, so this thread leaves the loop
// (and does not spin on a ring whose head is still ahead of a tail DrainRing will no
@@ -687,6 +687,7 @@ namespace MobileGL::MG_Remote::Server {
m_peer = hello->dialMode() == ::MobileGL::Wire::DialMode::Connect ? ApplyPeer::Connected
: hello->dialMode() == ::MobileGL::Wire::DialMode::Fork ? ApplyPeer::Forked
: ApplyPeer::InProcess;
m_clientPid = hello->pid();
const MobileGLResult compatible = ValidatePeerHandshake(transport, hello->abiMajor(),
hello->abiMinor(), hello->wireFingerprint(), theirStamp, hello->dialMode());
if (compatible != MOBILEGL_OK) return compatible;
@@ -370,6 +370,9 @@ namespace MobileGL::MG_Remote::Server {
// 0 = no context is bound. Every record the session has applied so far belongs to this
// token; a session that never crossed a bind_context has never bound one.
Uint64 CurrentContextToken() const { return m_currentContextToken.load(std::memory_order_acquire); }
// The pid the client's Hello names (its own, in its own pid namespace; 0 before Accept).
// What the session-targeted device-loss debug knob matches.
Uint32 ClientPid() const { return m_clientPid; }
Bool HasContext(Uint64 token) const { return m_contexts.find(token) != m_contexts.end(); }
Uint64 ContextCount() const { return static_cast<Uint64>(m_contexts.size()); }
Uint64 ContextsCreated() const { return m_contextsCreated; }
@@ -418,6 +421,7 @@ namespace MobileGL::MG_Remote::Server {
// P11 B2 (T0). Set by Accept from the Hello, by SettleAdoptT0AtBind on the apply thread;
// the inbox and the counters are the apply thread's.
Bool m_adoptT0Asked = false;
Uint32 m_clientPid = 0;
Bool m_adoptT0Quiet = false;
Uint8 m_adoptAsk = 2;
Bool m_adoptT0Settled = false;
+58 -5
View File
@@ -679,6 +679,31 @@ namespace MobileGL {
return nullptr;
}
// RESET NOTIFICATION (EGL_EXT_create_context_robustness, EGL 1.5): validated before
// anything of the context is built, since building its GL state already makes
// objects. Both names of the attribute are taken - the extension's (what Chrome,
// KWin and Qt pass) and EGL 1.5's (what ANGLE's GL-on-EGL backend passes for its
// own native context) - with the two values they share. Contexts that share
// objects must agree on it (EGL_BAD_MATCH otherwise).
EGLint resetStrategy = EGL_NO_RESET_NOTIFICATION;
for (const EGLint name :
{EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT, EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY}) {
if (auto value = ParseAttribValue(attribList, name); value) {
if (*value != EGL_NO_RESET_NOTIFICATION && *value != EGL_LOSE_CONTEXT_ON_RESET) {
SetError(EGL_BAD_ATTRIBUTE);
return nullptr;
}
resetStrategy = *value;
}
}
if (shareCtx != nullptr) {
const auto* shared = TryGetContext(shareCtx);
if (shared != nullptr && shared->ResetNotificationStrategy != resetStrategy) {
SetError(EGL_BAD_MATCH);
return nullptr;
}
}
ContextObject contextObject = {
.Display = display,
.Config = config,
@@ -688,6 +713,7 @@ namespace MobileGL {
.MajorVersion = 1,
.MinorVersion = 0,
};
contextObject.ResetNotificationStrategy = resetStrategy;
// P14 S1: the wire identity, minted where the context is born. A context that
// shares with an existing one joins ITS share group; EGL_NO_CONTEXT starts a new
@@ -735,7 +761,7 @@ namespace MobileGL {
contextObject.OpenGLContextFlags |= GL_CONTEXT_FLAG_DEBUG_BIT;
}
if (*value & EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR) {
contextObject.OpenGLContextFlags |= GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT;
contextObject.RobustAccessRequested = true;
}
}
if (auto value = ParseAttribValue(attribList, EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE);
@@ -747,10 +773,17 @@ namespace MobileGL {
contextObject.EGLContextFlags |= EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR;
contextObject.OpenGLContextFlags |= GL_CONTEXT_FLAG_DEBUG_BIT;
}
if (auto value = ParseAttribValue(attribList, EGL_CONTEXT_OPENGL_ROBUST_ACCESS);
value && *value == EGL_TRUE) {
contextObject.EGLContextFlags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR;
contextObject.OpenGLContextFlags |= GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT;
// ROBUST BUFFER ACCESS is recorded as asked for and reported only where it is
// provided (GL_CONTEXT_ROBUST_ACCESS, GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT; the GL
// layer decides). The request is not refused where it is not: compositors and
// browsers ask for robust access and reset notification in the same attempt, and
// refusing the first would cost them the second, which is the one that matters to
// a split client - its contexts are lost exactly when the server session is.
for (const EGLint name : {EGL_CONTEXT_OPENGL_ROBUST_ACCESS, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT}) {
if (auto value = ParseAttribValue(attribList, name); value && *value == EGL_TRUE) {
contextObject.EGLContextFlags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR;
contextObject.RobustAccessRequested = true;
}
}
const auto context = EncodeHandle<EGLContextHandle>(m_nextContextHandle++);
@@ -913,6 +946,26 @@ namespace MobileGL {
return ctx ? ctx->OpenGLContextFlags : 0;
}
EGLint EGLContext::GetCurrentContextResetNotificationStrategy() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
if (currentIt == m_threadCurrents.end()) {
return EGL_NO_RESET_NOTIFICATION;
}
const auto* ctx = TryGetContext(currentIt->second.Context);
return ctx ? ctx->ResetNotificationStrategy : EGL_NO_RESET_NOTIFICATION;
}
Bool EGLContext::IsCurrentContextRobustAccessRequested() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
if (currentIt == m_threadCurrents.end()) {
return false;
}
const auto* ctx = TryGetContext(currentIt->second.Context);
return ctx && ctx->RobustAccessRequested;
}
EGLContext::EGLSurfaceHandle EGLContext::CreateWindowSurface(EGLDisplayHandle display,
EGLConfigHandle config,
NativeWindowType window,
+13
View File
@@ -69,6 +69,11 @@ namespace MobileGL {
Bool IsCurrentContextOpenGLCoreProfile() const;
Bool IsCurrentContextOpenGLCompatibilityProfile() const;
EGLint GetCurrentContextFlags() const;
// The calling thread's current context's reset notification behavior
// (EGL_NO_RESET_NOTIFICATION with no context current) and whether it asked for
// robust buffer access.
EGLint GetCurrentContextResetNotificationStrategy() const;
Bool IsCurrentContextRobustAccessRequested() const;
// P14 S1. The wire identity of a context, and of the context the CALLING THREAD
// has current (0 when it has none - the release edge's value, and the value a
@@ -265,7 +270,15 @@ namespace MobileGL {
EGLint MinorVersion = 0;
EGLint OpenGLProfileMask = 0;
EGLint EGLContextFlags = 0;
// GL_CONTEXT_FLAGS as requested, without GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT: that
// one is reported only where robust buffer access is actually provided, which
// the GL layer decides (RobustAccessRequested below is the request).
EGLint OpenGLContextFlags = 0;
// EGL_EXT_create_context_robustness / EGL 1.5. EGL_NO_RESET_NOTIFICATION or
// EGL_LOSE_CONTEXT_ON_RESET; contexts that share must agree on it.
EGLint ResetNotificationStrategy = EGL_NO_RESET_NOTIFICATION;
// EGL_CONTEXT_OPENGL_ROBUST_ACCESS(_EXT) or the KHR flag bit was given as true.
Bool RobustAccessRequested = false;
};
struct SurfaceObject {
+8
View File
@@ -115,6 +115,13 @@ namespace MobileGL {
m_contextLostReported = true;
return true;
}
// True the first time glGetGraphicsResetStatus reports this context's reset (the
// one time it names the cause in the log).
Bool TakeResetStatusReport() {
if (m_resetStatusReported) return false;
m_resetStatusReported = true;
return true;
}
#endif
// Error
@@ -743,6 +750,7 @@ namespace MobileGL {
// See WireEpoch(); taken when the context is constructed.
Uint64 m_wireEpoch = CurrentWireEpoch();
Bool m_contextLostReported = false;
Bool m_resetStatusReported = false;
#endif
};
} // namespace GLState
@@ -344,3 +344,92 @@ TEST(EGLStateDmaBufImport, FormatAndModifierQueries) {
EXPECT_FALSE(fixture->State.QueryDmaBufFormats(fixture->Display, true, 0, nullptr, &count));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_NOT_INITIALIZED);
}
// EGL_EXT_create_context_robustness / EGL 1.5: the reset notification behavior a context is
// created with, under both names of the attribute (the extension's, which Chrome, KWin and Qt
// pass, and EGL 1.5's, which ANGLE's GL-on-EGL backend passes for its own native context), the
// errors the extension defines, and robust buffer access recorded as a request only.
TEST(EGLStateRobustness, ResetNotificationStrategyIsRecordedValidatedAndMatchedAcrossShares) {
auto fixture = CreateFixture();
auto& state = fixture->State;
auto strategyOf = [&](StateContext::EGLContextHandle context) {
EXPECT_TRUE(state.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, context));
const EGLint strategy = state.GetCurrentContextResetNotificationStrategy();
EXPECT_TRUE(state.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
return strategy;
};
// The default, and no context current at all.
EXPECT_EQ(state.GetCurrentContextResetNotificationStrategy(), EGL_NO_RESET_NOTIFICATION);
EXPECT_EQ(strategyOf(fixture->Context), EGL_NO_RESET_NOTIFICATION);
const EGLint loseExt[] = {EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT, EGL_LOSE_CONTEXT_ON_RESET_EXT,
EGL_NONE};
const auto robustExt = state.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, loseExt);
ASSERT_NE(robustExt, EGL_NO_CONTEXT);
EXPECT_EQ(strategyOf(robustExt), EGL_LOSE_CONTEXT_ON_RESET);
// ANGLE's shape: an ES 3.2 request with the EGL 1.5 name.
const EGLint loseCore[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2,
EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY, EGL_LOSE_CONTEXT_ON_RESET, EGL_NONE};
const auto robustCore = state.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, loseCore);
ASSERT_NE(robustCore, EGL_NO_CONTEXT);
EXPECT_EQ(strategyOf(robustCore), EGL_LOSE_CONTEXT_ON_RESET);
const EGLint noReset[] = {EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT, EGL_NO_RESET_NOTIFICATION_EXT,
EGL_NONE};
const auto plain = state.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, noReset);
ASSERT_NE(plain, EGL_NO_CONTEXT);
EXPECT_EQ(strategyOf(plain), EGL_NO_RESET_NOTIFICATION);
// A value that is neither: EGL_BAD_ATTRIBUTE, for both names.
const EGLint badExt[] = {EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT, EGL_TRUE, EGL_NONE};
EXPECT_EQ(state.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, badExt), EGL_NO_CONTEXT);
EXPECT_EQ(state.ConsumeError(), EGL_BAD_ATTRIBUTE);
const EGLint badCore[] = {EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY, 0, EGL_NONE};
EXPECT_EQ(state.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, badCore), EGL_NO_CONTEXT);
EXPECT_EQ(state.ConsumeError(), EGL_BAD_ATTRIBUTE);
// Sharing: the two must agree (EGL_BAD_MATCH), in both directions; agreeing shares are made.
EXPECT_EQ(state.CreateContext(fixture->Display, fixture->Config, robustExt, nullptr), EGL_NO_CONTEXT);
EXPECT_EQ(state.ConsumeError(), EGL_BAD_MATCH);
EXPECT_EQ(state.CreateContext(fixture->Display, fixture->Config, fixture->Context, loseExt), EGL_NO_CONTEXT);
EXPECT_EQ(state.ConsumeError(), EGL_BAD_MATCH);
const auto sharedRobust = state.CreateContext(fixture->Display, fixture->Config, robustExt, loseCore);
ASSERT_NE(sharedRobust, EGL_NO_CONTEXT);
EXPECT_EQ(state.GetContextShareGroupToken(sharedRobust), state.GetContextShareGroupToken(robustExt));
EXPECT_EQ(strategyOf(sharedRobust), EGL_LOSE_CONTEXT_ON_RESET);
const auto sharedPlain = state.CreateContext(fixture->Display, fixture->Config, fixture->Context, noReset);
EXPECT_NE(sharedPlain, EGL_NO_CONTEXT);
}
TEST(EGLStateRobustness, RobustAccessIsARequestAndNeverAGLContextFlagByItself) {
auto fixture = CreateFixture();
auto& state = fixture->State;
auto requestOf = [&](const EGLint* attribs, EGLint* flags) {
const auto context = state.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, attribs);
EXPECT_NE(context, EGL_NO_CONTEXT);
EXPECT_TRUE(state.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, context));
const bool requested = state.IsCurrentContextRobustAccessRequested();
*flags = state.GetCurrentContextFlags();
EXPECT_TRUE(state.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
return requested;
};
EGLint flags = 0;
const EGLint none[] = {EGL_NONE};
EXPECT_FALSE(requestOf(none, &flags));
const EGLint ext[] = {EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT, EGL_TRUE, EGL_NONE};
EXPECT_TRUE(requestOf(ext, &flags));
EXPECT_EQ(flags & GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT, 0) << "GL_CONTEXT_FLAGS claims robust access";
const EGLint core[] = {EGL_CONTEXT_OPENGL_ROBUST_ACCESS, EGL_TRUE, EGL_NONE};
EXPECT_TRUE(requestOf(core, &flags));
EXPECT_EQ(flags & GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT, 0);
// KWin's desktop candidate: the KHR flag bit, next to the debug bit that IS a GL flag.
const EGLint khr[] = {EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR | EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR,
EGL_NONE};
EXPECT_TRUE(requestOf(khr, &flags));
EXPECT_EQ(flags & GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT, 0);
EXPECT_NE(flags & GL_CONTEXT_FLAG_DEBUG_BIT, 0);
const EGLint off[] = {EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT, EGL_FALSE, EGL_NONE};
EXPECT_FALSE(requestOf(off, &flags));
}
+24
View File
@@ -661,6 +661,30 @@ TEST_F(ProgramTest, CompileAndLink) {
GetUniformiv(program, locInt, &intVal);
EXPECT_EQ(intVal, 114514);
// glGetnUniform* (GL 4.5 / KHR_robustness): the same answers, bounded by bufSize. A
// destination too small for the uniform's components is GL_INVALID_OPERATION and untouched.
while (GetError() != GL_NO_ERROR) {
}
float boundedRed[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
GetnUniformfv(program, locRed, static_cast<GLsizei>(3 * sizeof(float)), boundedRed);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_EQ(boundedRed[0], 1.0f);
EXPECT_EQ(boundedRed[2], 5.0f);
EXPECT_EQ(boundedRed[3], -1.0f) << "glGetnUniformfv wrote past the uniform";
float tooSmall[3] = {-1.0f, -1.0f, -1.0f};
GetnUniformfv(program, locRed, static_cast<GLsizei>(2 * sizeof(float)), tooSmall);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
EXPECT_EQ(tooSmall[0], -1.0f) << "a refused glGetnUniformfv wrote anyway";
int boundedInt = 0;
GetnUniformiv(program, locInt, static_cast<GLsizei>(sizeof(int)), &boundedInt);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_EQ(boundedInt, 114514);
GetnUniformiv(program, locInt, -1, &boundedInt);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
// Every other error is the unbounded query's: a location the program does not have.
GetnUniformfv(program, 4096, static_cast<GLsizei>(sizeof(boundedRed)), boundedRed);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
auto programObj = MG_State::pGLContext->GetProgramObject(program);
auto& shaderSpirvs = programObj->GetGeneratedSpirv();
for (int index = 0; index < shaderSpirvs.size(); ++index) {
+440 -12
View File
@@ -14,8 +14,11 @@
//
// 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 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
@@ -41,6 +44,11 @@
#include <mutex>
extern "C" {
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);
@@ -130,9 +138,25 @@ namespace {
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);
}
@@ -233,10 +257,32 @@ namespace {
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);
@@ -244,7 +290,7 @@ namespace {
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);
(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)
@@ -255,21 +301,42 @@ namespace {
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);
}
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.
void RunRecoveringClient(Route route, RecoveryReport& r) {
// `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.
@@ -285,6 +352,10 @@ namespace {
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.
@@ -318,6 +389,8 @@ namespace {
(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);
@@ -331,6 +404,7 @@ namespace {
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
@@ -384,7 +458,7 @@ namespace {
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 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)
@@ -562,6 +636,95 @@ namespace {
(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>
@@ -607,9 +770,163 @@ namespace {
return report;
}
RecoveryReport RunClient(const ServerProcess& server, Route route, const std::string& clientLogBase) {
return RunInClient<RecoveryReport>(server, clientLogBase,
[route](RecoveryReport& r) { RunRecoveringClient(route, r); });
// 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;
}
};
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 {
@@ -646,6 +963,15 @@ namespace {
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;
@@ -653,6 +979,13 @@ namespace {
<< "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;
@@ -668,6 +1001,7 @@ namespace {
<< 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
@@ -724,6 +1058,51 @@ TEST_F(ClientRecovery, ANewContextAfterADeviceLossRendersOnAFreshSession) {
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();
}
@@ -791,3 +1170,52 @@ TEST_F(ClientRecovery, ContextsNamingALostShareContextShareOneNewGroup) {
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();
}