From 90e63c9e2ad47343f1769637ce5252c78cfc0b7a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 3 Oct 2026 02:25:33 -0400 Subject: [PATCH] [Feature] (EGL+GL): advertise EGL_EXT_create_context_robustness and GL robustness, with per-context reset notification and a pid-targeted device-loss knob. --- .../MG_Impl/EGLImpl/EGLPlatformExtensions.h | 9 +- .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 53 +- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 112 ++++- .../MG_Impl/GLImpl/Program/GL_Program.cpp | 46 ++ MobileGL/MG_Impl/GLImpl/Program/GL_Program.h | 5 + MobileGL/MG_Impl/GetProcAddress.cpp | 22 + MobileGL/MG_Pipe/PipeApply.cpp | 42 ++ MobileGL/MG_Pipe/PipeSessionFail.h | 9 + MobileGL/MG_Remote/Client/ClientSession.cpp | 34 +- MobileGL/MG_Remote/Client/ClientSession.h | 4 + MobileGL/MG_Remote/Server/ServerLoop.cpp | 13 + MobileGL/MG_Remote/Server/ServerSession.cpp | 1 + MobileGL/MG_Remote/Server/ServerSession.h | 4 + MobileGL/MG_State/EGLState/Core.cpp | 63 ++- MobileGL/MG_State/EGLState/Core.h | 13 + MobileGL/MG_State/GLState/Core.h | 8 + MobileGL/MG_Test/EGLState/EGLStateTest.cpp | 89 ++++ MobileGL/MG_Test/Program/ProgramTest.cpp | 24 + MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp | 452 +++++++++++++++++- 19 files changed, 963 insertions(+), 40 deletions(-) diff --git a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h index 87a2501a..d083195a 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h @@ -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" diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 653cf4a2..a7bd8146 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -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); diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index a9081482..4807f77c 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -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& ClientSideExtensions() { + static const Vector withImages = {"GL_OES_EGL_image", "GL_KHR_robustness", + "GL_ARB_robustness", "GL_EXT_robustness"}; + static const Vector 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(rendererInfo.RendererGLInfo.Extensions.size()) + - (ClientSideExtension() != nullptr ? 1 : 0); + *params = static_cast(rendererInfo.RendererGLInfo.Extensions.size() + ClientSideExtensions().size()); break; case GL_POINT_SIZE_GRANULARITY: *params = static_cast(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 diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 3db44c8d..e772e094 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -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 + 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(type.matrixCols) * static_cast(type.matrixRows) + : type.isVector ? static_cast(type.vectorSize) + : 1; + } + if (bufSize < 0 || components * sizeof(T) > static_cast(bufSize)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("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); } diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h index ea8d66a0..83414f05 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h @@ -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); diff --git a/MobileGL/MG_Impl/GetProcAddress.cpp b/MobileGL/MG_Impl/GetProcAddress.cpp index 03f2deff..b0635067 100644 --- a/MobileGL/MG_Impl/GetProcAddress.cpp +++ b/MobileGL/MG_Impl/GetProcAddress.cpp @@ -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); diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index caa7cddc..d917d063 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -54,6 +54,7 @@ #include #include +#include #include #include #include @@ -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 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(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; diff --git a/MobileGL/MG_Pipe/PipeSessionFail.h b/MobileGL/MG_Pipe/PipeSessionFail.h index 3e8ca55b..25053232 100644 --- a/MobileGL/MG_Pipe/PipeSessionFail.h +++ b/MobileGL/MG_Pipe/PipeSessionFail.h @@ -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 diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 82b61ddc..0c7e1bdc 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -103,6 +103,27 @@ namespace MobileGL::MG_Remote::Client { thread_local Uint32 tl_wireMute = 0; thread_local Uint32 tl_currentSessionWork = 0; std::atomic 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 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 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() : "", 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) { diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 55bc9064..064b33d3 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -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, diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 8a76f348..1bceefbc 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -12,6 +12,7 @@ #include "ApplyThreadPolicy.h" #include "SessionRuntime.h" #include +#include #include #include @@ -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 diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index c2a4e00b..0c60be77 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -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; diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index d88de264..369f43af 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -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(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; diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index e4c500bc..f36c75af 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -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(m_nextContextHandle++); @@ -913,6 +946,26 @@ namespace MobileGL { return ctx ? ctx->OpenGLContextFlags : 0; } + EGLint EGLContext::GetCurrentContextResetNotificationStrategy() const { + const std::lock_guard 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 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, diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index a57a31ca..2c594ec6 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -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 { diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index aec10b8d..be2a1bb9 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -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 diff --git a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp index 9d236ac9..21183709 100644 --- a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp +++ b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp @@ -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)); +} diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index 649e8e4e..4f5e4449 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -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(3 * sizeof(float)), boundedRed); + EXPECT_EQ(GetError(), static_cast(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(2 * sizeof(float)), tooSmall); + EXPECT_EQ(GetError(), static_cast(GL_INVALID_OPERATION)); + EXPECT_EQ(tooSmall[0], -1.0f) << "a refused glGetnUniformfv wrote anyway"; + int boundedInt = 0; + GetnUniformiv(program, locInt, static_cast(sizeof(int)), &boundedInt); + EXPECT_EQ(GetError(), static_cast(GL_NO_ERROR)); + EXPECT_EQ(boundedInt, 114514); + GetnUniformiv(program, locInt, -1, &boundedInt); + EXPECT_EQ(GetError(), static_cast(GL_INVALID_OPERATION)); + // Every other error is the unbounded query's: a location the program does not have. + GetnUniformfv(program, 4096, static_cast(sizeof(boundedRed)), boundedRed); + EXPECT_EQ(GetError(), static_cast(GL_INVALID_OPERATION)); + auto programObj = MG_State::pGLContext->GetProgramObject(program); auto& shaderSpirvs = programObj->GetGeneratedSpirv(); for (int index = 0; index < shaderSpirvs.size(); ++index) { diff --git a/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp b/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp index 99f37eb3..51673d4e 100644 --- a/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp +++ b/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp @@ -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 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(glGetString(GL_EXTENSIONS)), "GL_KHR_robustness") ? 1 : 0; + GLint count = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &count); + r.glRobustnessIndexed = 0; + for (GLint i = 0; i < count; ++i) { + const auto* name = reinterpret_cast(glGetStringi(GL_EXTENSIONS, static_cast(i))); + if (name != nullptr && std::string(name) == "GL_KHR_robustness") r.glRobustnessIndexed = 1; + } + glGetIntegerv(GL_RESET_NOTIFICATION_STRATEGY, &r.strategyBefore); + GLint robust = -1; + glGetIntegerv(GL_CONTEXT_ROBUST_ACCESS, &robust); + r.robustAccess = robust; + } + // The client, in a fork: everything an application does around a loss, through the entry points. - 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(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 @@ -607,9 +770,163 @@ namespace { return report; } - RecoveryReport RunClient(const ServerProcess& server, Route route, const std::string& clientLogBase) { - return RunInClient(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 + struct HeldClient { + pid_t pid = -1; + int go = -1; + int report = -1; + + Bool Fork(const std::string& clientLogBase, Body body) { + int goFds[2] = {-1, -1}; + int reportFds[2] = {-1, -1}; + if (::pipe(goFds) != 0 || ::pipe(reportFds) != 0) return false; + std::fflush(nullptr); + pid = ::fork(); + if (pid == 0) { + ::close(goFds[1]); + ::close(reportFds[0]); + char endpoint[512] = {}; + const ssize_t got = ::read(goFds[0], endpoint, sizeof(endpoint) - 1); + if (got <= 0) ::_exit(0); + ::setenv("MOBILEGL_LOG_FILE_PATH", clientLogBase.c_str(), 1); + ::setenv("MOBILEGL_TRANSPORT", "spawn", 1); + ::setenv("MOBILEGL_IPC_CONTROL", endpoint, 1); + ::unsetenv("MOBILEGL_IPC_SURFACE"); + ::unsetenv("MOBILEGL_BACKEND_TYPE"); + ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT"); + ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID"); + Report r{}; + body(r); + const ssize_t wrote = ::write(reportFds[1], &r, sizeof(r)); + (void)wrote; + std::fflush(nullptr); + ::_exit(0); + } + ::close(goFds[0]); + ::close(reportFds[1]); + go = goFds[1]; + report = reportFds[0]; + return pid > 0; + } + + Report Release(const std::string& endpoint) { + Report r{}; + const ssize_t wrote = ::write(go, endpoint.c_str(), endpoint.size()); + (void)wrote; + ::close(go); + pollfd pfd{report, POLLIN, 0}; + if (::poll(&pfd, 1, 90000) > 0) { + if (::read(report, &r, sizeof(r)) != static_cast(sizeof(r))) { + r = Report{}; + Note(r, "the client died before reporting"); + } + } else { + Note(r, "the client timed out"); + ::kill(pid, SIGKILL); + } + ::close(report); + int status = 0; + ::waitpid(pid, &status, 0); + return r; + } + }; + + RecoveryReport RunClient(const ServerProcess& server, Route route, const std::string& clientLogBase, + EGLenum api = EGL_OPENGL_API, const EGLint* attribs = kRobustAttribs) { + return RunInClient(server, clientLogBase, [route, api, attribs](RecoveryReport& r) { + RunRecoveringClient(route, r, api, attribs); + }); } struct ClientLog { @@ -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(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(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(server, client.base, &RunUntoldClient); + const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); + ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; + ASSERT_EQ(r.firstPixel, kGreen) << std::hex << r.firstPixel << "\n" << diagnostics; + ASSERT_EQ(r.lossSeen, 1) << "the injected loss never latched\n" << diagnostics; + EXPECT_EQ(r.coreStrategy, GL_NO_RESET_NOTIFICATION); + EXPECT_EQ(r.coreStatus, static_cast(GL_NO_ERROR)) + << "a NO_RESET_NOTIFICATION core-profile context was told of the reset: 0x" << std::hex << r.coreStatus; + EXPECT_NE(r.coreError, static_cast(GL_CONTEXT_LOST)); + EXPECT_EQ(r.compatStatus, static_cast(GL_UNKNOWN_CONTEXT_RESET)) + << "the compatibility-profile context was not told: 0x" << std::hex << r.compatStatus; + EXPECT_EQ(r.compatError, static_cast(GL_CONTEXT_LOST)); + EXPECT_EQ(r.coreSwap, EGL_FALSE); + EXPECT_EQ(r.coreSwapError, EGL_CONTEXT_LOST) << std::hex << r.coreSwapError; + EXPECT_EQ(r.coreAgain, EGL_FALSE); + EXPECT_EQ(r.coreAgainError, EGL_CONTEXT_LOST) << std::hex << r.coreAgainError; + ASSERT_EQ(r.freshCurrent, 1) << r.note << "\n" << diagnostics; + EXPECT_EQ(r.freshStatus, static_cast(GL_NO_ERROR)); + EXPECT_EQ(r.freshPixel, kBlue) << std::hex << r.freshPixel << "\n" << diagnostics; ExpectOneLossAndACleanSecondSession(server); server.Stop(); } @@ -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 target; + ClientLog targetLog("pid-target"); + ASSERT_TRUE(target.Fork(targetLog.base, targetBody)); + + ServerProcess server; + ::setenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID", std::to_string(target.pid).c_str(), 1); + const bool launched = LaunchUnixInProcessServer(&server, "pid"); + ::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID"); + if (!launched) { + (void)target.Release(""); + FAIL() << server.Log(); + } + + ClientLog bystanderLog("pid-bystander"); + const BystanderReport bystander = + RunInClient(server, bystanderLog.base, &RunBystanderClient); + const TargetedReport r = target.Release(server.endpoint); + const std::string diagnostics = "server:\n" + server.Log() + "\ntarget:\n" + targetLog.Text() + + "\nbystander:\n" + bystanderLog.Text(); + + ASSERT_EQ(bystander.initialized, 1) << bystander.note << "\n" << diagnostics; + EXPECT_GT(bystander.frames, 5); + EXPECT_EQ(bystander.lastPixel, kRed) << std::hex << bystander.lastPixel; + EXPECT_EQ(bystander.resetStatus, static_cast(GL_NO_ERROR)) << "the bystander was lost"; + + ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; + ASSERT_EQ(r.lossSeen, 1) << "the targeted client was never lost\n" << diagnostics; + EXPECT_EQ(r.resetStatus, static_cast(GL_UNKNOWN_CONTEXT_RESET)) << std::hex << r.resetStatus; + ASSERT_EQ(r.freshCurrent, 1) << r.note << "\n" << diagnostics; + EXPECT_GT(r.freshFrames, 5); + EXPECT_EQ(r.freshPixel, kBlue) << std::hex << r.freshPixel << "\n" << diagnostics; + EXPECT_EQ(r.freshStatusEnd, static_cast(GL_NO_ERROR)) << "the recovered session was lost again"; + + // Three sessions: the bystander's and the recovered one end cleanly, the target's latched on + // the knob, naming the pid. + ASSERT_TRUE(WaitFor([&] { return Count(server.Log(), "reaped exit=") == 3; }, 10000)) << server.Log(); + const std::string log = server.Log(); + EXPECT_EQ(Count(log, "SessionLatch{"), 1u) << log; + EXPECT_NE(log.find("injected:pid=" + std::to_string(target.pid)), std::string::npos) << log; + EXPECT_EQ(Count(log, "reaped exit=75"), 1u) << log; + EXPECT_EQ(Count(log, "reaped exit=0"), 2u) << log; + server.Stop(); +}