// MobileGL - MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
//   https://www.gnu.org/licenses/gpl-3.0.txt
//   https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header

#include "Loader.h"
#include "MG_Util/SelfTest/DriverBugProbes.h"
#include "MG_Util/Types.h"
#include <Config.h>
#include <cmath>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
#endif
#include <windows.h>
#else
#include <dlfcn.h>
#endif

namespace MobileGL::MG_Util::BackendLoader {
    static Bool UseAngle() {
        return MG_Config::Features.UseAngle;
    }

#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
    static Bool IsTraceAngleLibrary(const String& name) {
        return name == "libGLESv2_angle.so" || name == "libEGL_angle.so";
    }

    static Bool IsAllowedTraceAngleVariant(const String& variant) {
        return variant == "ec889e6ea831" || variant == "90a62123d794";
    }

    static Bool ResolveTraceAngleLibraryPath(const String& name, String& path) {
        const String& variant = MG_Config::Features.TraceAngleVariant;
        if (!IsAllowedTraceAngleVariant(variant)) {
            MGLOG_F("Rejected trace ANGLE variant '%s'", variant.c_str());
            return false;
        }

        Dl_info mobileGlInfo{};
        if (dladdr(reinterpret_cast<const void*>(&ResolveTraceAngleLibraryPath), &mobileGlInfo) == 0 ||
            mobileGlInfo.dli_fname == nullptr) {
            MGLOG_F("Failed to resolve signed trace native library directory");
            return false;
        }

        String nativeLibraryPath = mobileGlInfo.dli_fname;
        const SizeT separator = nativeLibraryPath.find_last_of('/');
        if (separator == String::npos) {
            MGLOG_F("Invalid MobileGL library path: %s", nativeLibraryPath.c_str());
            return false;
        }

        const SizeT extension = name.rfind(".so");
        if (extension == String::npos) {
            MGLOG_F("Invalid trace ANGLE library name: %s", name.c_str());
            return false;
        }
        path = nativeLibraryPath.substr(0, separator + 1) + name.substr(0, extension) + "_" + variant + ".so";
        return true;
    }

#endif

    static void* OpenLib(const Vector<String>& names) {
#if defined(_WIN32)
        for (const auto& name : names) {
            if (HMODULE lib = LoadLibraryA(name.c_str())) {
                MGLOG_I("Loaded GL backend library: %s", name.c_str());
                return reinterpret_cast<void*>(lib);
            }
        }
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
        static const String LibPathPrefixes[] = {
#if defined(MOBILEGL_IOS)
            "@rpath/", "@executable_path/Frameworks/", "@loader_path/Frameworks/",
#else
            "/opt/vc/lib/", "/usr/local/lib/", "/usr/lib/", "/usr/lib/x86_64-linux-gnu/",
            "/usr/lib64/", "/lib64/",
#endif
            "" // Keep this last so the dynamic loader can use LD_LIBRARY_PATH.
        };

        void* lib = nullptr;

        Int flags = RTLD_LOCAL | RTLD_NOW;
        for (const auto& prefix : LibPathPrefixes) {
            for (const auto& name : names) {
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
                if (UseAngle() && IsTraceAngleLibrary(name)) {
                    String signedPath;
                    if (!ResolveTraceAngleLibraryPath(name, signedPath)) {
                        return nullptr;
                    }
                    lib = dlopen(signedPath.c_str(), flags);
                    if (lib == nullptr) {
                        MGLOG_F("Failed to open signed trace ANGLE library %s: %s",
                                signedPath.c_str(), dlerror());
                        return nullptr;
                    }
                    MGLOG_I("Loaded signed trace ANGLE library: %s", signedPath.c_str());
                    return lib;
                }
#endif
                String path_name = prefix + name;
                if ((lib = dlopen(path_name.c_str(), flags))) {
                    MGLOG_I("Loaded GL backend library: %s", path_name.c_str());
                    return lib;
                }
            }
        }
#endif
        return nullptr;
    }

    inline void* ProcAddress(void* lib, const char* name) {
#if defined(_WIN32)
        return reinterpret_cast<void*>(::GetProcAddress(reinterpret_cast<HMODULE>(lib), name));
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
        return dlsym(lib, name);
#else
        return nullptr;
#endif
    }

    void AcquireGLESFunctions(MG_External::GLESFunctionsTable& funcs,
                              MG_External::EGL::eglGetProcAddress_PTR procAddress) {
        if (!procAddress) {
            MGLOG_E("eglGetProcAddress is nullptr, cannot load GLES functions");
            return;
        }

#define INIT_GLES_FUNC(name)                                                                                           \
    do {                                                                                                               \
        funcs.name = (MG_External::GLES::name##_PTR)procAddress(#name);                                                \
        if (!funcs.name) {                                                                                             \
            MGLOG_E("Failed to load GLES function: %s", #name);                                                        \
        }                                                                                                              \
    } while (0);

// Optional (extension-provided) entry points: a null pointer is expected on drivers that lack the
// extension, so absence is not an error. The call site null-checks before use.
#define INIT_GLES_FUNC_OPTIONAL(name)                                                                                  \
    do {                                                                                                               \
        funcs.name = (MG_External::GLES::name##_PTR)procAddress(#name);                                                \
    } while (0);

        {
            INIT_GLES_FUNC(glActiveTexture)
            INIT_GLES_FUNC(glAttachShader)
            INIT_GLES_FUNC(glBindAttribLocation)
            INIT_GLES_FUNC(glBindBuffer)
            INIT_GLES_FUNC(glBindFramebuffer)
            INIT_GLES_FUNC(glBindRenderbuffer)
            INIT_GLES_FUNC(glBindTexture)
            INIT_GLES_FUNC(glBlendColor)
            INIT_GLES_FUNC(glBlendEquation)
            INIT_GLES_FUNC(glBlendEquationSeparate)
            INIT_GLES_FUNC(glBlendFunc)
            INIT_GLES_FUNC(glBlendFuncSeparate)
            INIT_GLES_FUNC(glBufferData)
            INIT_GLES_FUNC(glBufferSubData)
            INIT_GLES_FUNC(glCheckFramebufferStatus)
            INIT_GLES_FUNC(glClear)
            INIT_GLES_FUNC(glClearColor)
            INIT_GLES_FUNC(glClearDepthf)
            INIT_GLES_FUNC(glClearStencil)
            INIT_GLES_FUNC(glColorMask)
            INIT_GLES_FUNC(glCompileShader)
            INIT_GLES_FUNC(glCompressedTexImage2D)
            INIT_GLES_FUNC(glCompressedTexSubImage2D)
            //    INIT_GLES_FUNC(glCopyTexImage1D)
            INIT_GLES_FUNC(glCopyTexImage2D)
            INIT_GLES_FUNC(glCopyTexSubImage2D)
            INIT_GLES_FUNC(glCreateProgram)
            INIT_GLES_FUNC(glCreateShader)
            INIT_GLES_FUNC(glCullFace)
            INIT_GLES_FUNC(glDeleteBuffers)
            INIT_GLES_FUNC(glDeleteFramebuffers)
            INIT_GLES_FUNC(glDeleteProgram)
            INIT_GLES_FUNC(glDeleteRenderbuffers)
            INIT_GLES_FUNC(glDeleteShader)
            INIT_GLES_FUNC(glDeleteTextures)
            INIT_GLES_FUNC(glDepthFunc)
            INIT_GLES_FUNC(glDepthMask)
            INIT_GLES_FUNC(glDepthRangef)
            INIT_GLES_FUNC(glDetachShader)
            INIT_GLES_FUNC(glDisable)
            INIT_GLES_FUNC(glDisableVertexAttribArray)
            INIT_GLES_FUNC(glDrawArrays)
            INIT_GLES_FUNC(glDrawElements)
            INIT_GLES_FUNC(glEnable)
            INIT_GLES_FUNC(glEnableVertexAttribArray)
            INIT_GLES_FUNC(glFinish)
            INIT_GLES_FUNC(glFlush)
            INIT_GLES_FUNC(glFramebufferRenderbuffer)
            INIT_GLES_FUNC(glFramebufferTexture2D)
            INIT_GLES_FUNC(glFrontFace)
            INIT_GLES_FUNC(glGenBuffers)
            INIT_GLES_FUNC(glGenerateMipmap)
            INIT_GLES_FUNC(glGenFramebuffers)
            INIT_GLES_FUNC(glGenRenderbuffers)
            INIT_GLES_FUNC(glGenTextures)
            INIT_GLES_FUNC(glGetActiveAttrib)
            INIT_GLES_FUNC(glGetActiveUniform)
            INIT_GLES_FUNC(glGetAttachedShaders)
            INIT_GLES_FUNC(glGetAttribLocation)
            INIT_GLES_FUNC(glGetBooleanv)
            INIT_GLES_FUNC(glGetBufferParameteriv)
            INIT_GLES_FUNC(glGetError)
            INIT_GLES_FUNC(glGetString)
            INIT_GLES_FUNC(glGetStringi)
            INIT_GLES_FUNC(glGetFloatv)
            INIT_GLES_FUNC(glGetFramebufferAttachmentParameteriv)
            INIT_GLES_FUNC(glGetIntegerv)
            INIT_GLES_FUNC(glGetProgramiv)
            INIT_GLES_FUNC(glGetProgramInfoLog)
            INIT_GLES_FUNC(glGetRenderbufferParameteriv)
            INIT_GLES_FUNC(glGetShaderiv)
            INIT_GLES_FUNC(glGetShaderInfoLog)
            INIT_GLES_FUNC(glGetShaderPrecisionFormat)
            INIT_GLES_FUNC(glGetShaderSource)
            INIT_GLES_FUNC(glGetTexParameterfv)
            INIT_GLES_FUNC(glGetTexParameteriv)
            INIT_GLES_FUNC(glGetUniformfv)
            INIT_GLES_FUNC(glGetUniformiv)
            INIT_GLES_FUNC(glGetUniformLocation)
            INIT_GLES_FUNC(glGetVertexAttribfv)
            INIT_GLES_FUNC(glGetVertexAttribiv)
            INIT_GLES_FUNC(glGetVertexAttribPointerv)
            INIT_GLES_FUNC(glHint)
            INIT_GLES_FUNC(glIsBuffer)
            INIT_GLES_FUNC(glIsEnabled)
            INIT_GLES_FUNC(glIsFramebuffer)
            INIT_GLES_FUNC(glIsProgram)
            INIT_GLES_FUNC(glIsRenderbuffer)
            INIT_GLES_FUNC(glIsShader)
            INIT_GLES_FUNC(glIsTexture)
            INIT_GLES_FUNC(glLineWidth)
            INIT_GLES_FUNC(glLinkProgram)
            INIT_GLES_FUNC(glLogicOp)
            INIT_GLES_FUNC(glPointSize)
            INIT_GLES_FUNC(glPixelStorei)
            INIT_GLES_FUNC(glPolygonOffset)
            INIT_GLES_FUNC(glReadPixels)
            INIT_GLES_FUNC(glReleaseShaderCompiler)
            INIT_GLES_FUNC(glRenderbufferStorage)
            INIT_GLES_FUNC(glSampleCoverage)
            INIT_GLES_FUNC(glScissor)
            INIT_GLES_FUNC(glShaderBinary)
            INIT_GLES_FUNC(glShaderSource)
            INIT_GLES_FUNC(glStencilFunc)
            INIT_GLES_FUNC(glStencilFuncSeparate)
            INIT_GLES_FUNC(glStencilMask)
            INIT_GLES_FUNC(glStencilMaskSeparate)
            INIT_GLES_FUNC(glStencilOp)
            INIT_GLES_FUNC(glStencilOpSeparate)
            //    INIT_GLES_FUNC(glTexImage1D)
            INIT_GLES_FUNC(glTexImage2D)
            //    INIT_GLES_FUNC(glTexStorage1D)
            INIT_GLES_FUNC(glTexParameterf)
            INIT_GLES_FUNC(glTexParameterfv)
            INIT_GLES_FUNC(glTexParameteri)
            INIT_GLES_FUNC(glTexParameteriv)
            INIT_GLES_FUNC(glTexSubImage2D)
            INIT_GLES_FUNC(glUniform1f)
            INIT_GLES_FUNC(glUniform1fv)
            INIT_GLES_FUNC(glUniform1i)
            INIT_GLES_FUNC(glUniform1iv)
            INIT_GLES_FUNC(glUniform2f)
            INIT_GLES_FUNC(glUniform2fv)
            INIT_GLES_FUNC(glUniform2i)
            INIT_GLES_FUNC(glUniform2iv)
            INIT_GLES_FUNC(glUniform3f)
            INIT_GLES_FUNC(glUniform3fv)
            INIT_GLES_FUNC(glUniform3i)
            INIT_GLES_FUNC(glUniform3iv)
            INIT_GLES_FUNC(glUniform4f)
            INIT_GLES_FUNC(glUniform4fv)
            INIT_GLES_FUNC(glUniform4i)
            INIT_GLES_FUNC(glUniform4iv)
            INIT_GLES_FUNC(glUniformMatrix2fv)
            INIT_GLES_FUNC(glUniformMatrix3fv)
            INIT_GLES_FUNC(glUniformMatrix4fv)
            INIT_GLES_FUNC(glUseProgram)
            INIT_GLES_FUNC(glValidateProgram)
            INIT_GLES_FUNC(glVertexAttrib1f)
            INIT_GLES_FUNC(glVertexAttrib1fv)
            INIT_GLES_FUNC(glVertexAttrib2f)
            INIT_GLES_FUNC(glVertexAttrib2fv)
            INIT_GLES_FUNC(glVertexAttrib3f)
            INIT_GLES_FUNC(glVertexAttrib3fv)
            INIT_GLES_FUNC(glVertexAttrib4f)
            INIT_GLES_FUNC(glVertexAttrib4fv)
            INIT_GLES_FUNC(glVertexAttribPointer)
            INIT_GLES_FUNC(glViewport)
            INIT_GLES_FUNC(glReadBuffer)
            INIT_GLES_FUNC(glDrawRangeElements)
            INIT_GLES_FUNC(glTexImage3D)
            INIT_GLES_FUNC(glTexSubImage3D)
            INIT_GLES_FUNC(glCopyTexSubImage3D)
            INIT_GLES_FUNC(glCompressedTexImage3D)
            INIT_GLES_FUNC(glCompressedTexSubImage3D)
            INIT_GLES_FUNC(glGenQueries)
            INIT_GLES_FUNC(glDeleteQueries)
            INIT_GLES_FUNC(glIsQuery)
            INIT_GLES_FUNC(glBeginQuery)
            INIT_GLES_FUNC(glEndQuery)
            INIT_GLES_FUNC(glGetQueryiv)
            INIT_GLES_FUNC(glGetQueryObjectuiv)
            INIT_GLES_FUNC(glUnmapBuffer)
            INIT_GLES_FUNC(glGetBufferPointerv)
            INIT_GLES_FUNC(glDrawBuffers)
            INIT_GLES_FUNC(glUniformMatrix2x3fv)
            INIT_GLES_FUNC(glUniformMatrix3x2fv)
            INIT_GLES_FUNC(glUniformMatrix2x4fv)
            INIT_GLES_FUNC(glUniformMatrix4x2fv)
            INIT_GLES_FUNC(glUniformMatrix3x4fv)
            INIT_GLES_FUNC(glUniformMatrix4x3fv)
            INIT_GLES_FUNC(glBlitFramebuffer)
            INIT_GLES_FUNC(glRenderbufferStorageMultisample)
            INIT_GLES_FUNC(glFramebufferTextureLayer)
            INIT_GLES_FUNC(glFlushMappedBufferRange)
            INIT_GLES_FUNC(glBindVertexArray)
            INIT_GLES_FUNC(glDeleteVertexArrays)
            INIT_GLES_FUNC(glGenVertexArrays)
            INIT_GLES_FUNC(glIsVertexArray)
            INIT_GLES_FUNC(glGetIntegeri_v)
            INIT_GLES_FUNC(glBeginTransformFeedback)
            INIT_GLES_FUNC(glEndTransformFeedback)
            INIT_GLES_FUNC(glBindBufferRange)
            INIT_GLES_FUNC(glBindBufferBase)
            INIT_GLES_FUNC(glTransformFeedbackVaryings)
            INIT_GLES_FUNC(glGetTransformFeedbackVarying)
            INIT_GLES_FUNC(glVertexAttribIPointer)
            INIT_GLES_FUNC(glGetVertexAttribIiv)
            INIT_GLES_FUNC(glGetVertexAttribIuiv)
            INIT_GLES_FUNC(glVertexAttribI4i)
            INIT_GLES_FUNC(glVertexAttribI4ui)
            INIT_GLES_FUNC(glVertexAttribI4iv)
            INIT_GLES_FUNC(glVertexAttribI4uiv)
            INIT_GLES_FUNC(glGetUniformuiv)
            INIT_GLES_FUNC(glGetFragDataLocation)
            INIT_GLES_FUNC(glUniform1ui)
            INIT_GLES_FUNC(glUniform2ui)
            INIT_GLES_FUNC(glUniform3ui)
            INIT_GLES_FUNC(glUniform4ui)
            INIT_GLES_FUNC(glUniform1uiv)
            INIT_GLES_FUNC(glUniform2uiv)
            INIT_GLES_FUNC(glUniform3uiv)
            INIT_GLES_FUNC(glUniform4uiv)
            INIT_GLES_FUNC(glClearBufferiv)
            INIT_GLES_FUNC(glClearBufferuiv)
            INIT_GLES_FUNC(glClearBufferfv)
            INIT_GLES_FUNC(glClearBufferfi)
            INIT_GLES_FUNC(glCopyBufferSubData)
            INIT_GLES_FUNC(glGetUniformIndices)
            INIT_GLES_FUNC(glGetActiveUniformsiv)
            INIT_GLES_FUNC(glGetUniformBlockIndex)
            INIT_GLES_FUNC(glGetActiveUniformBlockiv)
            INIT_GLES_FUNC(glGetActiveUniformBlockName)
            INIT_GLES_FUNC(glUniformBlockBinding)
            INIT_GLES_FUNC(glDrawArraysInstanced)
            INIT_GLES_FUNC(glDrawElementsInstanced)
            INIT_GLES_FUNC(glFenceSync)
            INIT_GLES_FUNC(glIsSync)
            INIT_GLES_FUNC(glDeleteSync)
            INIT_GLES_FUNC(glClientWaitSync)
            INIT_GLES_FUNC(glWaitSync)
            INIT_GLES_FUNC(glGetInteger64v)
            INIT_GLES_FUNC(glGetSynciv)
            INIT_GLES_FUNC(glGetInteger64i_v)
            INIT_GLES_FUNC(glGetBufferParameteri64v)
            INIT_GLES_FUNC(glGenSamplers)
            INIT_GLES_FUNC(glDeleteSamplers)
            INIT_GLES_FUNC(glIsSampler)
            INIT_GLES_FUNC(glBindSampler)
            INIT_GLES_FUNC(glSamplerParameteri)
            INIT_GLES_FUNC(glSamplerParameteriv)
            INIT_GLES_FUNC(glSamplerParameterf)
            INIT_GLES_FUNC(glSamplerParameterfv)
            INIT_GLES_FUNC(glGetSamplerParameteriv)
            INIT_GLES_FUNC(glGetSamplerParameterfv)
            INIT_GLES_FUNC(glVertexAttribDivisor)
            INIT_GLES_FUNC(glBindTransformFeedback)
            INIT_GLES_FUNC(glDeleteTransformFeedbacks)
            INIT_GLES_FUNC(glGenTransformFeedbacks)
            INIT_GLES_FUNC(glIsTransformFeedback)
            INIT_GLES_FUNC(glPauseTransformFeedback)
            INIT_GLES_FUNC(glResumeTransformFeedback)
            INIT_GLES_FUNC(glGetProgramBinary)
            INIT_GLES_FUNC(glProgramBinary)
            INIT_GLES_FUNC(glProgramParameteri)
            INIT_GLES_FUNC(glInvalidateFramebuffer)
            INIT_GLES_FUNC(glInvalidateSubFramebuffer)
            INIT_GLES_FUNC(glTexStorage2D)
            INIT_GLES_FUNC(glTexStorage3D)
            INIT_GLES_FUNC(glGetInternalformativ)
            INIT_GLES_FUNC(glDispatchCompute)
            INIT_GLES_FUNC(glDispatchComputeIndirect)
            INIT_GLES_FUNC(glDrawArraysIndirect)
            INIT_GLES_FUNC(glDrawElementsIndirect)
            INIT_GLES_FUNC(glFramebufferParameteri)
            INIT_GLES_FUNC(glGetFramebufferParameteriv)
            INIT_GLES_FUNC(glGetProgramInterfaceiv)
            INIT_GLES_FUNC(glShaderStorageBlockBinding)
            INIT_GLES_FUNC(glGetProgramResourceIndex)
            INIT_GLES_FUNC(glGetProgramResourceName)
            INIT_GLES_FUNC(glGetProgramResourceiv)
            INIT_GLES_FUNC(glGetProgramResourceLocation)
            INIT_GLES_FUNC(glUseProgramStages)
            INIT_GLES_FUNC(glActiveShaderProgram)
            INIT_GLES_FUNC(glCreateShaderProgramv)
            INIT_GLES_FUNC(glBindProgramPipeline)
            INIT_GLES_FUNC(glDeleteProgramPipelines)
            INIT_GLES_FUNC(glGenProgramPipelines)
            INIT_GLES_FUNC(glIsProgramPipeline)
            INIT_GLES_FUNC(glGetProgramPipelineiv)
            INIT_GLES_FUNC(glProgramUniform1i)
            INIT_GLES_FUNC(glProgramUniform2i)
            INIT_GLES_FUNC(glProgramUniform3i)
            INIT_GLES_FUNC(glProgramUniform4i)
            INIT_GLES_FUNC(glProgramUniform1ui)
            INIT_GLES_FUNC(glProgramUniform2ui)
            INIT_GLES_FUNC(glProgramUniform3ui)
            INIT_GLES_FUNC(glProgramUniform4ui)
            INIT_GLES_FUNC(glProgramUniform1f)
            INIT_GLES_FUNC(glProgramUniform2f)
            INIT_GLES_FUNC(glProgramUniform3f)
            INIT_GLES_FUNC(glProgramUniform4f)
            INIT_GLES_FUNC(glProgramUniform1iv)
            INIT_GLES_FUNC(glProgramUniform2iv)
            INIT_GLES_FUNC(glProgramUniform3iv)
            INIT_GLES_FUNC(glProgramUniform4iv)
            INIT_GLES_FUNC(glProgramUniform1uiv)
            INIT_GLES_FUNC(glProgramUniform2uiv)
            INIT_GLES_FUNC(glProgramUniform3uiv)
            INIT_GLES_FUNC(glProgramUniform4uiv)
            INIT_GLES_FUNC(glProgramUniform1fv)
            INIT_GLES_FUNC(glProgramUniform2fv)
            INIT_GLES_FUNC(glProgramUniform3fv)
            INIT_GLES_FUNC(glProgramUniform4fv)
            INIT_GLES_FUNC(glProgramUniformMatrix2fv)
            INIT_GLES_FUNC(glProgramUniformMatrix3fv)
            INIT_GLES_FUNC(glProgramUniformMatrix4fv)
            INIT_GLES_FUNC(glProgramUniformMatrix2x3fv)
            INIT_GLES_FUNC(glProgramUniformMatrix3x2fv)
            INIT_GLES_FUNC(glProgramUniformMatrix2x4fv)
            INIT_GLES_FUNC(glProgramUniformMatrix4x2fv)
            INIT_GLES_FUNC(glProgramUniformMatrix3x4fv)
            INIT_GLES_FUNC(glProgramUniformMatrix4x3fv)
            INIT_GLES_FUNC(glValidateProgramPipeline)
            INIT_GLES_FUNC(glGetProgramPipelineInfoLog)
            INIT_GLES_FUNC(glBindImageTexture)
            INIT_GLES_FUNC(glGetBooleani_v)
            INIT_GLES_FUNC(glMemoryBarrier)
            INIT_GLES_FUNC(glMemoryBarrierByRegion)
            INIT_GLES_FUNC(glTexStorage2DMultisample)
            INIT_GLES_FUNC(glGetMultisamplefv)
            INIT_GLES_FUNC(glSampleMaski)
            INIT_GLES_FUNC(glGetTexLevelParameteriv)
            INIT_GLES_FUNC(glGetTexLevelParameterfv)
            INIT_GLES_FUNC(glBindVertexBuffer)
            INIT_GLES_FUNC(glVertexAttribFormat)
            INIT_GLES_FUNC(glVertexAttribIFormat)
            INIT_GLES_FUNC(glVertexAttribBinding)
            INIT_GLES_FUNC(glVertexBindingDivisor)
            INIT_GLES_FUNC(glBlendBarrier)
            INIT_GLES_FUNC(glCopyImageSubData)
            INIT_GLES_FUNC(glDebugMessageControl)
            INIT_GLES_FUNC(glDebugMessageInsert)
            INIT_GLES_FUNC(glDebugMessageCallback)
            INIT_GLES_FUNC(glGetDebugMessageLog)
            INIT_GLES_FUNC(glPushDebugGroup)
            INIT_GLES_FUNC(glPopDebugGroup)
            INIT_GLES_FUNC(glObjectLabel)
            INIT_GLES_FUNC(glGetObjectLabel)
            INIT_GLES_FUNC(glObjectPtrLabel)
            INIT_GLES_FUNC(glGetObjectPtrLabel)
            INIT_GLES_FUNC(glGetPointerv)
            INIT_GLES_FUNC(glEnablei)
            INIT_GLES_FUNC(glDisablei)
            INIT_GLES_FUNC(glBlendEquationi)
            INIT_GLES_FUNC(glBlendEquationSeparatei)
            INIT_GLES_FUNC(glBlendFunci)
            INIT_GLES_FUNC(glBlendFuncSeparatei)
            INIT_GLES_FUNC(glColorMaski)
            INIT_GLES_FUNC(glIsEnabledi)
            INIT_GLES_FUNC(glDrawElementsBaseVertex)
            INIT_GLES_FUNC(glDrawRangeElementsBaseVertex)
            INIT_GLES_FUNC(glDrawElementsInstancedBaseVertex)
            INIT_GLES_FUNC(glFramebufferTexture)
            INIT_GLES_FUNC(glPrimitiveBoundingBox)
            INIT_GLES_FUNC(glGetGraphicsResetStatus)
            INIT_GLES_FUNC(glReadnPixels)
            INIT_GLES_FUNC(glGetnUniformfv)
            INIT_GLES_FUNC(glGetnUniformiv)
            INIT_GLES_FUNC(glGetnUniformuiv)
            INIT_GLES_FUNC(glMinSampleShading)
            INIT_GLES_FUNC(glPatchParameteri)
            INIT_GLES_FUNC(glTexParameterIiv)
            INIT_GLES_FUNC(glTexParameterIuiv)
            INIT_GLES_FUNC(glGetTexParameterIiv)
            INIT_GLES_FUNC(glGetTexParameterIuiv)
            INIT_GLES_FUNC(glSamplerParameterIiv)
            INIT_GLES_FUNC(glSamplerParameterIuiv)
            INIT_GLES_FUNC(glGetSamplerParameterIiv)
            INIT_GLES_FUNC(glGetSamplerParameterIuiv)
            INIT_GLES_FUNC(glTexBuffer)
            INIT_GLES_FUNC(glTexBufferRange)
            // Optional: absent on an ES 3.2 core driver, and absent on ES 3.1 without the
            // matching extension. The tier resolution below picks whichever spelling the
            // driver's own support actually comes from.
            // Optional by nature: ES never made texture views core, so both spellings are
            // absent on plenty of drivers and neither absence is an error.
            INIT_GLES_FUNC_OPTIONAL(glTextureViewEXT)
            INIT_GLES_FUNC_OPTIONAL(glTextureViewOES)
            INIT_GLES_FUNC_OPTIONAL(glTexBufferEXT)
            INIT_GLES_FUNC_OPTIONAL(glTexBufferOES)
            INIT_GLES_FUNC_OPTIONAL(glTexBufferRangeEXT)
            INIT_GLES_FUNC_OPTIONAL(glTexBufferRangeOES)
            INIT_GLES_FUNC(glTexStorage3DMultisample)
            INIT_GLES_FUNC(glMapBufferRange)
            INIT_GLES_FUNC(glBufferStorageEXT)
            INIT_GLES_FUNC(glGetQueryObjectivEXT)
            INIT_GLES_FUNC(glGetQueryObjecti64vEXT)
            INIT_GLES_FUNC(glQueryCounterEXT)
            INIT_GLES_FUNC(glGetQueryObjectui64vEXT)
            INIT_GLES_FUNC(glBindFragDataLocationEXT)
            INIT_GLES_FUNC(glMapBufferOES)
            INIT_GLES_FUNC_OPTIONAL(glPolygonModeNV)
            INIT_GLES_FUNC_OPTIONAL(glPolygonModeANGLE)
            INIT_GLES_FUNC_OPTIONAL(glColorMaskiEXT)
            INIT_GLES_FUNC_OPTIONAL(glColorMaskiOES)
            // Extension-only multi-draw entry points. eglGetProcAddress may legally return a
            // non-NULL stub for these on drivers that do not implement them (NVIDIA's ES driver
            // returns one for glMultiDrawElementsBaseVertexEXT that silently drops every draw),
            // so pointer presence proves nothing: callers must gate on the extension-derived
            // SupportsMultiDrawIndirect / SupportsMultiDrawElementsBaseVertex capability flags,
            // never on these pointers alone.
            INIT_GLES_FUNC_OPTIONAL(glMultiDrawArraysIndirectEXT)
            INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsIndirectEXT)
            INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsBaseVertexEXT)

            INIT_GLES_FUNC_OPTIONAL(glDrawArraysInstancedBaseInstanceEXT)
            INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseInstanceEXT)
            INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseVertexBaseInstanceEXT)
        }
    }

    void AcquireEGLFunctions(MG_External::EGLFunctionsTable& funcs) {
        void* eglLib = nullptr;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
        void* angleGlesLib = nullptr;
#endif
#if defined(_WIN32)
        // ANGLE is the GLES provider on Windows regardless of UseAngle(). Preload
        // libGLESv2.dll so libEGL.dll resolves its dependency from the same directory.
        if (!OpenLib({"libGLESv2.dll"})) {
            MGLOG_E("Failed to open ANGLE libGLESv2.dll");
            return;
        }
        eglLib = OpenLib({"libEGL.dll"});
#else
        if (UseAngle()) {
            void* glesLib = OpenLib({"libGLESv2_angle.so"});
            if (!glesLib) {
                MGLOG_E("Failed to open ANGLE libGLESv2_angle.so");
                return;
            }
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
            angleGlesLib = glesLib;
#endif
            eglLib = OpenLib({"libEGL_angle.so"});
            if (!eglLib) {
                MGLOG_E("Failed to open ANGLE libEGL_angle.so");
                return;
            }
        } else {
#if defined(MOBILEGL_IOS)
            eglLib = OpenLib({"libtinygl4angle.dylib"});
#else
            // Versioned SONAME first. The unversioned "libEGL.so" is a development
            // symlink: it ships in libegl-dev/mesa-libEGL-devel, NOT in the runtime
            // package, so a machine that can run GL perfectly well may not have it -
            // every stock Ubuntu/Debian runtime image, the GitHub Actions runners
            // included. Asking only for the unversioned name there makes dlopen fail,
            // which used to leave the whole EGL function table null and take the next
            // call through a null pointer (SIGSEGV inside InitDisplayAndContext).
            // Developer machines have both names, which is exactly why this only ever
            // showed up in CI.
            eglLib = OpenLib({"libEGL.so.1", "libEGL.so"});
#endif
        }
#endif // !_WIN32

        if (!eglLib) {
            // MGLOG_F, not MGLOG_E: with no EGL there is no rendering at all, so this is a
            // bring-up abort rather than a recoverable error. It was forced to F while the
            // Log.h ordering compiled MGLOG_E out of every shipping and CI build; F is still
            // the right level on its own merits, so it stays.
            MGLOG_F("Failed to open EGL library: none of libEGL.so.1 / libEGL.so could be "
                    "dlopened; every EGL entry point will be null");
            return;
        }

        auto resolveEGLProc = [&](const char* name) -> void* {
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
            if (UseAngle()) {
                // Avoid the wrapper's canonical libGLESv2_angle.so lookup:
                // resolve its forwarding target from the verified variant.
                String target = "EGL_";
                target += name + 3;
                return ProcAddress(angleGlesLib, target.c_str());
            }
#endif
            return ProcAddress(eglLib, name);
        };

#define INIT_EGL_FUNC(name)                                                                                            \
    do {                                                                                                               \
        funcs.name = (MG_External::EGL::name##_PTR)resolveEGLProc(#name);                                              \
        if (!funcs.name) {                                                                                             \
            /* MGLOG_F for the same reason as the open failure above: a null entry     */                              \
            /* point is a crash waiting for its first caller, and MGLOG_E is compiled  */                              \
            /* out at the INFO level every shipping and CI build uses.                 */                              \
            MGLOG_F("Failed to load EGL function: %s", #name);                                                         \
        }                                                                                                              \
    } while (0);

        {
            INIT_EGL_FUNC(eglBindAPI)
            INIT_EGL_FUNC(eglBindTexImage)
            INIT_EGL_FUNC(eglChooseConfig)
            INIT_EGL_FUNC(eglCopyBuffers)
            INIT_EGL_FUNC(eglCreateContext)
            INIT_EGL_FUNC(eglCreatePbufferFromClientBuffer)
            INIT_EGL_FUNC(eglCreatePbufferSurface)
            INIT_EGL_FUNC(eglCreatePixmapSurface)
            INIT_EGL_FUNC(eglCreatePlatformPixmapSurface)
            INIT_EGL_FUNC(eglCreatePlatformWindowSurface)
            INIT_EGL_FUNC(eglCreateWindowSurface)
            INIT_EGL_FUNC(eglDestroyContext)
            INIT_EGL_FUNC(eglDestroySurface)
            INIT_EGL_FUNC(eglGetConfigAttrib)
            INIT_EGL_FUNC(eglGetConfigs)
            INIT_EGL_FUNC(eglGetCurrentContext)
            INIT_EGL_FUNC(eglGetCurrentDisplay)
            INIT_EGL_FUNC(eglGetCurrentSurface)
            INIT_EGL_FUNC(eglGetDisplay)
            INIT_EGL_FUNC(eglGetPlatformDisplay)
            INIT_EGL_FUNC(eglGetError)
            INIT_EGL_FUNC(eglGetProcAddress)
            INIT_EGL_FUNC(eglInitialize)
            INIT_EGL_FUNC(eglMakeCurrent)
            INIT_EGL_FUNC(eglQueryAPI)
            INIT_EGL_FUNC(eglQueryContext)
            INIT_EGL_FUNC(eglQueryString)
            INIT_EGL_FUNC(eglQuerySurface)
            INIT_EGL_FUNC(eglReleaseTexImage)
            INIT_EGL_FUNC(eglReleaseThread)
            INIT_EGL_FUNC(eglSurfaceAttrib)
            INIT_EGL_FUNC(eglSwapBuffers)
            INIT_EGL_FUNC(eglSwapBuffersWithDamageEXT)
            INIT_EGL_FUNC(eglSwapInterval)
            INIT_EGL_FUNC(eglTerminate)
            INIT_EGL_FUNC(eglUnlockSurfaceKHR)
            INIT_EGL_FUNC(eglWaitClient)
            INIT_EGL_FUNC(eglWaitGL)
            INIT_EGL_FUNC(eglWaitNative)
            INIT_EGL_FUNC(eglCreateSync)
            INIT_EGL_FUNC(eglDestroySync)
            INIT_EGL_FUNC(eglClientWaitSync)
            INIT_EGL_FUNC(eglGetSyncAttrib)
            INIT_EGL_FUNC(eglCreateImage)
            INIT_EGL_FUNC(eglDestroyImage)
            INIT_EGL_FUNC(eglGetPlatformDisplay)
            INIT_EGL_FUNC(eglWaitSync)
        }
    }

    // Detects whether indirect draws leak the command's baseInstance word ("reserved, must
    // be zero" in unextended ES) into gl_InstanceID. Conforming ES drivers keep
    // gl_InstanceID zero-based, but ANGLE's Vulkan backend forwards the command verbatim to
    // vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V InstanceIndex, which includes
    // firstInstance. The DirectGLES native indirect-draw path uses this answer to keep
    // gl_InstanceID zero-based in rewritten shaders (PromoteDrawParameterGlobalsToUniforms).
    Bool ProbeIndirectInstanceIdIncludesBaseInstance(const MG_External::GLESCapabilities& caps,
                                                     const MG_External::GLESFunctionsTable& f) {
        const Bool esVersionOk =
            caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
        if (!esVersionOk || !f.glDrawArraysIndirect || !f.glBindBufferBase || !f.glMapBufferRange ||
            !f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) {
            return false;
        }
        // Read from caps, not re-queried: the per-stage limits are resolved (and their query
        // errors drained) before this probe runs, so asking the driver again would be a second
        // round trip that can disagree with the number MobileGL actually advertises - and, on
        // the early return below, would leave its own GL_INVALID_ENUM in the queue for the
        // application's first glGetError to find.
        const GLint maxVertexSsboBlocks = caps.MaxVertexShaderStorageBlocks;
        if (maxVertexSsboBlocks < 1) {
            // The native indirect machinery cannot read the command buffer from the vertex
            // stage on this driver anyway; assume conforming zero-based gl_InstanceID.
            MGLOG_I("baseInstance probe skipped: GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = %d", maxVertexSsboBlocks);
            return false;
        }
        while (f.glGetError() != GL_NO_ERROR) {
        }

        const char* vsSource = "#version 310 es\n"
                               "layout(std430, binding = 0) buffer MgProbeResult { highp int mg_probeValue; };\n"
                               "void main() {\n"
                               "    mg_probeValue = gl_InstanceID;\n"
                               "    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n"
                               "    gl_PointSize = 1.0;\n"
                               "}\n";
        const char* fsSource = "#version 310 es\n"
                               "void main() {}\n";
        const auto compileShader = [&f](GLenum type, const char* src) -> GLuint {
            const GLuint shader = f.glCreateShader(type);
            if (shader == 0) {
                return 0;
            }
            f.glShaderSource(shader, 1, &src, nullptr);
            f.glCompileShader(shader);
            GLint status = GL_FALSE;
            f.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
            if (status != GL_TRUE) {
                f.glDeleteShader(shader);
                return 0;
            }
            return shader;
        };
        const GLuint vs = compileShader(GL_VERTEX_SHADER, vsSource);
        const GLuint fs = compileShader(GL_FRAGMENT_SHADER, fsSource);
        GLuint program = 0;
        if (vs != 0 && fs != 0) {
            program = f.glCreateProgram();
            if (program != 0) {
                f.glAttachShader(program, vs);
                f.glAttachShader(program, fs);
                f.glLinkProgram(program);
                GLint status = GL_FALSE;
                f.glGetProgramiv(program, GL_LINK_STATUS, &status);
                if (status != GL_TRUE) {
                    f.glDeleteProgram(program);
                    program = 0;
                }
            }
        }
        if (vs != 0) f.glDeleteShader(vs);
        if (fs != 0) f.glDeleteShader(fs);
        if (program == 0) {
            MGLOG_I("baseInstance probe skipped: probe program failed to build (vs=%u fs=%u)", vs, fs);
            while (f.glGetError() != GL_NO_ERROR) {
            }
            return false;
        }

        constexpr GLuint kProbeBaseInstance = 7;
        struct {
            GLuint count;
            GLuint instanceCount;
            GLuint first;
            GLuint baseInstance;
        } command = {1, 1, 0, kProbeBaseInstance};
        const GLint sentinel = -1;
        // ES makes indirect draws INVALID_OPERATION on the default vertex array object.
        GLuint vao = 0;
        f.glGenVertexArrays(1, &vao);
        f.glBindVertexArray(vao);
        GLuint buffers[2] = {0, 0}; // [0] = result SSBO, [1] = indirect command buffer
        f.glGenBuffers(2, buffers);
        f.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[0]);
        f.glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(sentinel), &sentinel, GL_STATIC_DRAW);
        f.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffers[0]);
        f.glBindBuffer(GL_DRAW_INDIRECT_BUFFER, buffers[1]);
        f.glBufferData(GL_DRAW_INDIRECT_BUFFER, sizeof(command), &command, GL_STATIC_DRAW);

        // The default framebuffer may be incomplete (e.g. surfaceless contexts) and draws
        // are validated against completeness even under GL_RASTERIZER_DISCARD, so give the
        // probe its own 1x1 target.
        GLuint framebuffer = 0;
        GLuint renderbuffer = 0;
        f.glGenFramebuffers(1, &framebuffer);
        f.glGenRenderbuffers(1, &renderbuffer);
        f.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
        f.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
        f.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        f.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);

        f.glUseProgram(program);
        f.glEnable(GL_RASTERIZER_DISCARD);
        f.glDrawArraysIndirect(GL_POINTS, nullptr);
        f.glDisable(GL_RASTERIZER_DISCARD);
        f.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);

        Bool includesBase = false;
        const GLenum drawError = f.glGetError();
        if (drawError == GL_NO_ERROR) {
            f.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[0]);
            const void* mapped = f.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLint), GL_MAP_READ_BIT);
            if (mapped != nullptr) {
                GLint written = -1;
                std::memcpy(&written, mapped, sizeof(written));
                f.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
                includesBase = written == static_cast<GLint>(kProbeBaseInstance);
                MGLOG_I("baseInstance probe: shader observed gl_InstanceID = %d (baseInstance was %u)", written,
                        kProbeBaseInstance);
            } else {
                MGLOG_I("baseInstance probe inconclusive: result map failed");
            }
        } else {
            MGLOG_I("baseInstance probe inconclusive: draw raised GL error 0x%x", drawError);
        }

        f.glUseProgram(0);
        f.glBindFramebuffer(GL_FRAMEBUFFER, 0);
        f.glBindRenderbuffer(GL_RENDERBUFFER, 0);
        f.glDeleteFramebuffers(1, &framebuffer);
        f.glDeleteRenderbuffers(1, &renderbuffer);
        f.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
        f.glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
        f.glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0);
        f.glBindVertexArray(0);
        f.glDeleteVertexArrays(1, &vao);
        f.glDeleteBuffers(2, buffers);
        f.glDeleteProgram(program);
        while (f.glGetError() != GL_NO_ERROR) {
        }
        return includesBase;
    }

    // Whether an inter-stage interface BLOCK that carries an explicit layout(location=)
    // actually transports its payload across a geometry (or tessellation) boundary.
    //
    // The Mali-G1-Ultra ES driver (r54p1) links such a program with an empty info log, runs
    // the draw, and delivers ZEROES to the consuming stage; the identical program with the
    // qualifier removed from the blocks carries the payload correctly. That is not a MobileGL
    // artefact - a bare EGL/GLES 3.2 program with no MobileGL in the process reproduces it -
    // and it is the whole of the KHR-GLxx.shading_language_420pack interface-block group's
    // failures on this device. A located block between a VERTEX and a FRAGMENT stage is fine
    // on the same driver, so the probe deliberately spans a geometry stage: that is the
    // shape that breaks, and answering the narrower question would report a healthy driver.
    //
    // Probed rather than matched on the renderer string, because "which drivers do this" is
    // not knowable and a quirk list is wrong the moment a driver is fixed. A driver that
    // cannot run the probe at all (pre-ES 3.2, no geometry stage, missing entry point) is
    // reported as HEALTHY: that leaves its ESSL exactly as it is today, which is the answer
    // with no behaviour change in it.
    Bool ProbeLocatedInterStageIoBlocksTransportPayload(const MG_External::GLESCapabilities& caps,
                                                        const MG_External::GLESFunctionsTable& f) {
        const Bool esVersionOk =
            caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
        if (!esVersionOk || !f.glCreateShader || !f.glCreateProgram || !f.glGenVertexArrays ||
            !f.glGenRenderbuffers || !f.glGenFramebuffers || !f.glReadPixels || !f.glDrawArrays) {
            MGLOG_I("located-IO-block probe skipped: needs an ES 3.2 context with a geometry stage");
            return true;
        }
        while (f.glGetError() != GL_NO_ERROR) {
        }

        // Position comes from gl_VertexID so the probe needs no vertex buffer, and the block
        // payload is two values that survive an 8-bit target exactly (0.25 -> 64, 0.5 -> 128).
        // TWO different block names, because the two boundaries this crosses (VS->GS and
        // GS->FS) are separate interfaces; a single name would also trip the in/out collision
        // UniquifyIoBlockNamesPass exists for and confuse one defect with the other.
        const char* vsSource = "#version 320 es\n"
                               "precision highp float;\n"
                               "layout(location = 0) out MgProbeBlock { vec2 mg_probeValue; } mg_probeOut;\n"
                               "void main() {\n"
                               "    vec2 p = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);\n"
                               "    gl_Position = vec4(p, 0.0, 1.0);\n"
                               "    mg_probeOut.mg_probeValue = vec2(0.25, 0.5);\n"
                               "}\n";
        const char* gsSource = "#version 320 es\n"
                               "precision highp float;\n"
                               "layout(triangles) in;\n"
                               "layout(triangle_strip, max_vertices = 3) out;\n"
                               "layout(location = 0) in MgProbeBlock { vec2 mg_probeValue; } mg_probeIn[];\n"
                               "layout(location = 0) out MgProbeBlock2 { vec2 mg_probeValue; } mg_probeOut;\n"
                               "void main() {\n"
                               "    for (int i = 0; i < 3; ++i) {\n"
                               "        gl_Position = gl_in[i].gl_Position;\n"
                               "        mg_probeOut.mg_probeValue = mg_probeIn[i].mg_probeValue;\n"
                               "        EmitVertex();\n"
                               "    }\n"
                               "}\n";
        const char* fsSource = "#version 320 es\n"
                               "precision highp float;\n"
                               "layout(location = 0) in MgProbeBlock2 { vec2 mg_probeValue; } mg_probeIn;\n"
                               "layout(location = 0) out vec4 mg_probeColor;\n"
                               "void main() { mg_probeColor = vec4(mg_probeIn.mg_probeValue, 0.0, 1.0); }\n";

        const auto compileShader = [&f](GLenum type, const char* src) -> GLuint {
            const GLuint shader = f.glCreateShader(type);
            if (shader == 0) return 0;
            f.glShaderSource(shader, 1, &src, nullptr);
            f.glCompileShader(shader);
            GLint status = GL_FALSE;
            f.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
            if (status != GL_TRUE) {
                f.glDeleteShader(shader);
                return 0;
            }
            return shader;
        };
        const GLuint vs = compileShader(GL_VERTEX_SHADER, vsSource);
        const GLuint gs = compileShader(GL_GEOMETRY_SHADER, gsSource);
        const GLuint fs = compileShader(GL_FRAGMENT_SHADER, fsSource);
        GLuint program = 0;
        if (vs != 0 && gs != 0 && fs != 0) {
            program = f.glCreateProgram();
            if (program != 0) {
                f.glAttachShader(program, vs);
                f.glAttachShader(program, gs);
                f.glAttachShader(program, fs);
                f.glLinkProgram(program);
                GLint status = GL_FALSE;
                f.glGetProgramiv(program, GL_LINK_STATUS, &status);
                if (status != GL_TRUE) {
                    f.glDeleteProgram(program);
                    program = 0;
                }
            }
        }
        if (vs != 0) f.glDeleteShader(vs);
        if (gs != 0) f.glDeleteShader(gs);
        if (fs != 0) f.glDeleteShader(fs);
        if (program == 0) {
            MGLOG_I("located-IO-block probe skipped: probe program failed to build (vs=%u gs=%u fs=%u)", vs,
                    gs, fs);
            while (f.glGetError() != GL_NO_ERROR) {
            }
            return true;
        }

        // Everything this probe changes is read back first and put back afterwards. The
        // capability run owns a context of its own, but a probe that leaves a 1x1 viewport or
        // a bound scratch framebuffer behind would be found by whatever draws next rather
        // than here, and that is not a bug anyone should have to chase twice.
        GLint savedProgram = 0, savedVao = 0, savedDrawFbo = 0, savedReadFbo = 0, savedRenderbuffer = 0;
        GLint savedViewport[4] = {0, 0, 0, 0};
        GLfloat savedClearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
        GLboolean savedColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
        f.glGetIntegerv(GL_CURRENT_PROGRAM, &savedProgram);
        f.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &savedVao);
        f.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFbo);
        f.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &savedReadFbo);
        f.glGetIntegerv(GL_RENDERBUFFER_BINDING, &savedRenderbuffer);
        f.glGetIntegerv(GL_VIEWPORT, savedViewport);
        f.glGetFloatv(GL_COLOR_CLEAR_VALUE, savedClearColor);
        f.glGetBooleanv(GL_COLOR_WRITEMASK, savedColorMask);
        // The five raster states that can void the draw and turn a healthy driver into a
        // "broken" verdict. Saved, forced off, and put back.
        constexpr GLenum kQuietedStates[] = {GL_SCISSOR_TEST, GL_RASTERIZER_DISCARD, GL_CULL_FACE,
                                             GL_DEPTH_TEST, GL_BLEND};
        GLboolean savedStates[5] = {GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE};
        for (SizeT i = 0; i < std::size(kQuietedStates); ++i) {
            savedStates[i] = f.glIsEnabled(kQuietedStates[i]);
            if (savedStates[i] == GL_TRUE) f.glDisable(kQuietedStates[i]);
        }
        f.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

        // ES makes a draw on the default vertex array object invalid in a core-profile sense,
        // and the default framebuffer may be incomplete (surfaceless contexts), so the probe
        // brings both of its own.
        GLuint vao = 0, framebuffer = 0, renderbuffer = 0;
        f.glGenVertexArrays(1, &vao);
        f.glBindVertexArray(vao);
        f.glGenRenderbuffers(1, &renderbuffer);
        f.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
        f.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
        f.glGenFramebuffers(1, &framebuffer);
        f.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        f.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
        f.glViewport(0, 0, 1, 1);
        f.glUseProgram(program);
        f.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        f.glClear(GL_COLOR_BUFFER_BIT);
        f.glDrawArrays(GL_TRIANGLES, 0, 3);

        Bool transports = true;
        const GLenum drawError = f.glGetError();
        if (drawError == GL_NO_ERROR) {
            GLubyte pixel[4] = {0, 0, 0, 0};
            f.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
            if (f.glGetError() == GL_NO_ERROR) {
                // Exactly the two values the vertex stage wrote, with one bit of slack for a
                // driver that rounds the 8-bit conversion the other way. A stage that received
                // nothing reads 0/0, which is nowhere near either.
                transports = pixel[0] >= 0x3f && pixel[0] <= 0x41 && pixel[1] >= 0x7f && pixel[1] <= 0x81;
                MGLOG_I("located-IO-block probe: fragment stage received (%u, %u), expected (64, 128)",
                        pixel[0], pixel[1]);
            } else {
                MGLOG_I("located-IO-block probe inconclusive: readback failed");
            }
        } else {
            MGLOG_I("located-IO-block probe inconclusive: draw raised GL error 0x%x", drawError);
        }

        f.glUseProgram(static_cast<GLuint>(savedProgram));
        f.glBindFramebuffer(GL_FRAMEBUFFER, 0);
        f.glDeleteFramebuffers(1, &framebuffer);
        f.glDeleteRenderbuffers(1, &renderbuffer);
        f.glBindVertexArray(0);
        f.glDeleteVertexArrays(1, &vao);
        f.glBindVertexArray(static_cast<GLuint>(savedVao));
        f.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(savedRenderbuffer));
        f.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(savedDrawFbo));
        f.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(savedReadFbo));
        f.glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]);
        f.glClearColor(savedClearColor[0], savedClearColor[1], savedClearColor[2], savedClearColor[3]);
        f.glColorMask(savedColorMask[0], savedColorMask[1], savedColorMask[2], savedColorMask[3]);
        for (SizeT i = 0; i < std::size(kQuietedStates); ++i) {
            if (savedStates[i] == GL_TRUE) f.glEnable(kQuietedStates[i]);
        }
        f.glDeleteProgram(program);
        while (f.glGetError() != GL_NO_ERROR) {
        }
        return transports;
    }

    // GL 4.6 table 23.65 admits exactly four answers for GL_LAYER_PROVOKING_VERTEX and
    // GL_VIEWPORT_INDEX_PROVOKING_VERTEX. Anything else means the driver wrote something MobileGL
    // cannot forward as a convention, and GL_UNDEFINED_VERTEX - a legal answer, not a placeholder
    // - is the accurate thing to say about it.
    static GLenum NormalizeProvokingVertexConvention(GLint driverValue) {
        switch (static_cast<GLenum>(driverValue)) {
        case GL_FIRST_VERTEX_CONVENTION:
        case GL_LAST_VERTEX_CONVENTION:
        case GL_PROVOKING_VERTEX:
        case GL_UNDEFINED_VERTEX:
            return static_cast<GLenum>(driverValue);
        default:
            return GL_UNDEFINED_VERTEX;
        }
    }

    static const char* ProvokingVertexConventionName(GLenum convention) {
        switch (convention) {
        case GL_FIRST_VERTEX_CONVENTION:
            return "GL_FIRST_VERTEX_CONVENTION";
        case GL_LAST_VERTEX_CONVENTION:
            return "GL_LAST_VERTEX_CONVENTION";
        case GL_PROVOKING_VERTEX:
            return "GL_PROVOKING_VERTEX";
        default:
            return "GL_UNDEFINED_VERTEX";
        }
    }

    Bool FillInGLESCapabilities(MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs) {
        if (!glesFuncs.glGetString || !glesFuncs.glGetIntegerv) {
            MGLOG_E("Required GLES functions are not loaded, cannot query capabilities");
            return false;
        }

        auto* vendorName = glesFuncs.glGetString(GL_VENDOR);
        MGLOG_I("GL_VENDOR: %s", vendorName);
        auto* gpuName = glesFuncs.glGetString(GL_RENDERER);
        MGLOG_I("GL_RENDERER: %s", gpuName);
        glesFuncs.glGetIntegerv(GL_MAJOR_VERSION, &caps.GLESVersion.Major);
        glesFuncs.glGetIntegerv(GL_MINOR_VERSION, &caps.GLESVersion.Minor);

        caps.GLESVersionString = String((char*)glesFuncs.glGetString(GL_VERSION));
        caps.GLESRendererString = String((char*)gpuName);
        caps.GLESVendorString = String((char*)vendorName);
        caps.GLESShadingLanguageVersionString = String((char*)glesFuncs.glGetString(GL_SHADING_LANGUAGE_VERSION));

        GLint extCount = 0;
        glesFuncs.glGetIntegerv(GL_NUM_EXTENSIONS, &extCount);
        MGLOG_I("Detected %d OpenGL ES extensions:", extCount);
        // Combined below: glMultiDrawElementsBaseVertexEXT exists only where EXT/OES
        // draw_elements_base_vertex interacts with GL_EXT_multi_draw_arrays.
        Bool hasMultiDrawIndirectExtension = false;
        Bool hasDrawElementsBaseVertexExtension = false;
        Bool hasMultiDrawArraysExtension = false;
        // Resolved into caps.TextureBufferSupport below, once the ES version is also known.
        Bool hasExtTextureBuffer = false;
        Bool hasOesTextureBuffer = false;
        // Resolved into caps.SupportsTextureView below. Two spellings of one extension; the
        // entry points differ only in suffix, so unlike the buffer-texture tier there is nothing
        // downstream that needs to know WHICH one answered.
        Bool hasExtTextureView = false;
        Bool hasOesTextureView = false;
        // Combined with the three entry points below; DirectGLES emulates baseInstance when this
        // comes out false, so a stub pointer counting as support would silently break the draws.
        Bool hasBaseInstanceExtension = false;
        for (GLint i = 0; i < extCount; ++i) {
            const char* extension = (const char*)glesFuncs.glGetStringi(GL_EXTENSIONS, i);
            if (extension) {
                MGLOG_I("    %s", extension);
                if (std::strcmp(extension, "GL_EXT_buffer_storage") == 0) {
                    caps.SupportsPersistentMapping = true;
                }
                if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
                    caps.SupportsNorm16Texture = true;
                }
                if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) {
                    caps.SupportsRenderSnorm = true;
                }
                if (std::strcmp(extension, "GL_EXT_color_buffer_float") == 0) {
                    caps.SupportsColorBufferFloat = true;
                }
                if (std::strcmp(extension, "GL_EXT_color_buffer_half_float") == 0) {
                    caps.SupportsColorBufferHalfFloat = true;
                }
                if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) {
                    caps.SupportsSrgbWriteControl = true;
                }
                if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) {
                    caps.SupportsTextureFilterAnisotropy = true;
                }
                if (std::strcmp(extension, "GL_EXT_texture_border_clamp") == 0 ||
                    std::strcmp(extension, "GL_OES_texture_border_clamp") == 0) {
                    caps.SupportsTextureBorderClamp = true;
                }
                if (std::strcmp(extension, "GL_EXT_texture_cube_map_array") == 0 ||
                    std::strcmp(extension, "GL_OES_texture_cube_map_array") == 0) {
                    caps.SupportsTextureCubeMapArray = true;
                }
                if (std::strcmp(extension, "GL_EXT_texture_view") == 0) {
                    hasExtTextureView = true;
                }
                if (std::strcmp(extension, "GL_OES_texture_view") == 0) {
                    hasOesTextureView = true;
                }
                if (std::strcmp(extension, "GL_EXT_texture_buffer") == 0) {
                    hasExtTextureBuffer = true;
                }
                if (std::strcmp(extension, "GL_OES_texture_buffer") == 0) {
                    hasOesTextureBuffer = true;
                }
                if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
                    hasBaseInstanceExtension = true;
                }
                if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) {
                    caps.SupportsDisjointTimerQuery = true;
                }
                if (std::strcmp(extension, "GL_KHR_parallel_shader_compile") == 0) {
                    caps.SupportsParallelShaderCompile = true;
                }
                if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
                    caps.SupportsDualSourceBlend = true;
                }
                if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
                    caps.SupportsNoperspectiveInterpolation = true;
                }
                if (std::strcmp(extension, "GL_NV_image_formats") == 0) {
                    caps.SupportsExtendedImageFormats = true;
                }
                if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
                    caps.SupportsShaderMultisampleInterpolation = true;
                }
                if (std::strcmp(extension, "GL_EXT_multi_draw_indirect") == 0) {
                    hasMultiDrawIndirectExtension = true;
                }
                if (std::strcmp(extension, "GL_EXT_draw_elements_base_vertex") == 0 ||
                    std::strcmp(extension, "GL_OES_draw_elements_base_vertex") == 0) {
                    hasDrawElementsBaseVertexExtension = true;
                }
                if (std::strcmp(extension, "GL_EXT_multi_draw_arrays") == 0) {
                    hasMultiDrawArraysExtension = true;
                }
                if (std::strcmp(extension, "GL_EXT_clip_cull_distance") == 0) {
                    caps.SupportsClipDistance = true;
                }
                if (std::strcmp(extension, "GL_OES_viewport_array") == 0) {
                    caps.SupportsViewportArray = true;
                }
                // EXT wins where both are advertised: it is the spelling the Android Extension
                // Pack mandates, so it is the one a driver is most likely to have tested.
                if (std::strcmp(extension, "GL_EXT_tessellation_point_size") == 0) {
                    caps.TessellationPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT;
                } else if (std::strcmp(extension, "GL_OES_tessellation_point_size") == 0 &&
                           caps.TessellationPointSizeSupport == MG_External::GLESCapabilities::PointSizeTier::None) {
                    caps.TessellationPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionOES;
                }
                if (std::strcmp(extension, "GL_EXT_geometry_point_size") == 0) {
                    caps.GeometryPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT;
                } else if (std::strcmp(extension, "GL_OES_geometry_point_size") == 0 &&
                           caps.GeometryPointSizeSupport == MG_External::GLESCapabilities::PointSizeTier::None) {
                    caps.GeometryPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionOES;
                }
            }
        }
        // The pointer check on top of the extension check makes each flag sufficient on its own
        // at a call site; the extension check on top of the pointer keeps a stub returned by
        // eglGetProcAddress (see AcquireGLESFunctions) from ever counting as support.
        caps.SupportsMultiDrawIndirect = hasMultiDrawIndirectExtension &&
                                         glesFuncs.glMultiDrawArraysIndirectEXT != nullptr &&
                                         glesFuncs.glMultiDrawElementsIndirectEXT != nullptr;
        caps.SupportsMultiDrawElementsBaseVertex = hasDrawElementsBaseVertexExtension &&
                                                   hasMultiDrawArraysExtension &&
                                                   glesFuncs.glMultiDrawElementsBaseVertexEXT != nullptr;
        // All three, not any: DirectGLES picks native-vs-emulated once per draw entry point off
        // this single flag, so a driver that resolved only some of them must count as absent.
        caps.SupportsBaseInstance = hasBaseInstanceExtension &&
                                    glesFuncs.glDrawArraysInstancedBaseInstanceEXT != nullptr &&
                                    glesFuncs.glDrawElementsInstancedBaseInstanceEXT != nullptr &&
                                    glesFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT != nullptr;
        // ES has no core texture views at any version, so this is extension-only by nature.
        // Each spelling must bring its OWN entry point: a driver that advertises the OES string
        // is not required to export glTextureViewEXT.
        caps.SupportsTextureView = (hasExtTextureView && glesFuncs.glTextureViewEXT != nullptr) ||
                                   (hasOesTextureView && glesFuncs.glTextureViewOES != nullptr);
        // Escape hatch for the integration suite: the no-extension path is the one MobileGL has
        // to refuse honestly rather than emulate, and on a driver that HAS the extension there
        // would otherwise be no way to exercise that refusal (see TextureViewScenario).
        if (std::getenv("MOBILEGL_DISABLE_TEXTURE_VIEW") != nullptr) {
            MGLOG_I("MOBILEGL_DISABLE_TEXTURE_VIEW is set; reporting no EXT/OES_texture_view support");
            caps.SupportsTextureView = false;
        }
        // Core from ES 3.2 on, so an extension string is not required there; below 3.2 the
        // extension is, and the pointer still has to have resolved either way.
        const Bool esAtLeast32 = caps.GLESVersion.Major > 3 ||
                                 (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
        const Bool esAtLeast31 = caps.GLESVersion.Major > 3 ||
                                 (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
        caps.SupportsDrawIndirect = esAtLeast31 && glesFuncs.glDrawArraysIndirect != nullptr &&
                                    glesFuncs.glDrawElementsIndirect != nullptr;
        caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) &&
                                              glesFuncs.glDrawElementsBaseVertex != nullptr;
        caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
                                     glesFuncs.glMemoryBarrier != nullptr &&
                                     glesFuncs.glCreateShader != nullptr &&
                                     glesFuncs.glCreateProgram != nullptr;
        caps.SupportsShaderMultisampleInterpolation =
            caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
            (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);

        // Detect optional raster/color-mask entry points by whether they loaded. glColorMaski is GLES
        // 3.2 core (no extension string), so pointer presence is the reliable signal for all of these.
        caps.SupportsPolygonMode =
            glesFuncs.glPolygonModeNV != nullptr || glesFuncs.glPolygonModeANGLE != nullptr;
        caps.SupportsIndexedColorMask = glesFuncs.glColorMaski != nullptr ||
                                        glesFuncs.glColorMaskiEXT != nullptr ||
                                        glesFuncs.glColorMaskiOES != nullptr;
        MGLOG_I("    glPolygonMode (NV/ANGLE): %s", caps.SupportsPolygonMode ? "yes" : "no");
        MGLOG_I("    indexed glColorMaski: %s", caps.SupportsIndexedColorMask ? "yes" : "no");
        MGLOG_I("    dual-source blend (EXT_blend_func_extended): %s",
                caps.SupportsDualSourceBlend ? "yes" : "no");
        MGLOG_I("    draw indirect (ES 3.1 core): %s", caps.SupportsDrawIndirect ? "yes" : "no");
        MGLOG_I("    multi-draw indirect (EXT_multi_draw_indirect): %s",
                caps.SupportsMultiDrawIndirect ? "yes" : "no");
        MGLOG_I("    multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
                caps.SupportsMultiDrawElementsBaseVertex ? "yes" : "no");
        MGLOG_I("    draw elements base vertex (ES 3.2 core or EXT/OES_draw_elements_base_vertex): %s",
                caps.SupportsDrawElementsBaseVertex ? "yes" : "no");
        MGLOG_I("    compute shaders (ES 3.1 core): %s", caps.SupportsComputeShader ? "yes" : "no");
        MGLOG_I("    base instance (EXT_base_instance; emulated by attribute offsets when absent): %s",
                caps.SupportsBaseInstance ? "yes" : "no");
        MGLOG_I("    clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
        MGLOG_I("    viewport array (OES_viewport_array; gl_ViewportIndex collapses to viewport 0 when absent): %s",
                caps.SupportsViewportArray ? "yes" : "no");
        {
            const auto pointSizeTierName = [](MG_External::GLESCapabilities::PointSizeTier tier) {
                switch (tier) {
                    case MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT: return "EXT";
                    case MG_External::GLESCapabilities::PointSizeTier::ExtensionOES: return "OES";
                    default: return "no";
                }
            };
            MGLOG_I("    tessellation gl_PointSize (EXT/OES_tessellation_point_size): %s",
                    pointSizeTierName(caps.TessellationPointSizeSupport));
            MGLOG_I("    geometry gl_PointSize (EXT/OES_geometry_point_size): %s",
                    pointSizeTierName(caps.GeometryPointSizeSupport));
        }

        // LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
        // is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
        // decide whether MobileGL got far enough to have a working context: if the probe ran,
        // a later surface loss is a real defect rather than an emulator fault worth retrying.
        // Demoting this line, renaming it, or moving it before the context is usable silently
        // inverts that retry logic. It is init-phase, so MGLOG_I is correct and it stays.
        MGLOG_I("OpenGL ES capabilities:");
        glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
        MGLOG_I("    GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: %d", caps.UniformBufferOffsetAlignment);
        // ES 3.1 core, so no extension gate - but a driver that somehow leaves it at zero would
        // make every storage-range offset legal, so an unusable answer keeps the 256 default.
        GLint shaderStorageOffsetAlignment = 0;
        glesFuncs.glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &shaderStorageOffsetAlignment);
        while (glesFuncs.glGetError() != GL_NO_ERROR) {
        }
        if (shaderStorageOffsetAlignment > 0) {
            caps.ShaderStorageBufferOffsetAlignment = shaderStorageOffsetAlignment;
        }
        MGLOG_I("    GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT: %d", caps.ShaderStorageBufferOffsetAlignment);
        GLfloat aliasedLineWidthRange[2] = {1.0f, 1.0f};
        GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
        GLfloat smoothLineWidthGranularity = 1.0f;
        GLfloat aliasedPointSizeRange[2] = {1.0f, 1.0f};
        // GL 4.6 core table 23.60 sets the MINIMUM VIEWPORT_BOUNDS_RANGE at [-32768, 32767], and
        // KHR-GL43.viewport_array.queries asserts exactly that floor. GLES has no such query, so
        // the glGetFloatv below raises GL_INVALID_ENUM and leaves this untouched - starting it at
        // {0, 0} advertised a range that admits no viewport origin at all.
        GLfloat viewportBoundsRange[2] = {-32768.0f, 32767.0f};
        GLint maxViewportDims[2] = {16384, 16384};
        GLint viewportSubpixelBits = 0;
        GLint max3DTextureSize = 16384;
        GLint maxArrayTextureLayers = 2048;
        GLint maxCubeMapTextureSize = 16384;
        GLint maxFramebufferWidth = 16384;
        GLint maxFramebufferHeight = 16384;
        GLint maxFramebufferLayers = 2048;
        GLint maxRenderbufferSize = 16384;
        GLint maxTextureSize = 16384;
        GLint maxColorTextureSamples = 1;
        GLint maxDepthTextureSamples = 1;
        GLint maxFramebufferSamples = 1;
        GLint maxIntegerSamples = 1;
        GLint maxSamples = 1;
        GLint maxSampleMaskWords = 1;
        GLint maxTextureImageUnits = 32;
        GLint maxVertexTextureImageUnits = 32;
        GLint maxComputeTextureImageUnits = 32;
        GLint maxCombinedTextureImageUnits = 192;
        GLint maxVertexAttribs = 16;
        GLint maxComputeShaderStorageBlocks = 8;
        GLint maxCombinedShaderStorageBlocks = 32;
        // ES 3.2 table 21.44 minimums. Zero for the four graphics stages below fragment is not a
        // placeholder - it is what the spec permits and what ARM's GLES driver actually reports,
        // so a probe that never runs (pre-ES 3.2, unsupported pname) leaves behind the truthful
        // answer rather than an optimistic one.
        GLint maxVertexShaderStorageBlocks = 0;
        GLint maxTessControlShaderStorageBlocks = 0;
        GLint maxTessEvaluationShaderStorageBlocks = 0;
        GLint maxGeometryShaderStorageBlocks = 0;
        GLint maxFragmentShaderStorageBlocks = 4;
        GLint maxComputeUniformBlocks = 12;
        GLint maxComputeWorkGroupInvocations = 128;
        GLint maxShaderStorageBufferBindings = 8;
        GLint maxTextureBufferSize = 65536;
        GLint maxUniformBufferBindings = 24;
        GLint maxUniformBlockSize = 16384;
        GLint maxImageUnits = 8;
        GLint maxCombinedImageUniforms = 8;
        GLint maxVertexImageUniforms = 0;
        GLint maxGeometryImageUniforms = 0;
        GLint maxFragmentImageUniforms = 8;
        GLint maxComputeImageUniforms = 8;
        GLint maxDrawBuffers = 8;
        GLint maxColorAttachments = 8;
        // Zero is a legal answer, not a placeholder. GL_MAX_CLIP_DISTANCES exists in ES only as
        // GL_MAX_CLIP_DISTANCES_EXT under GL_EXT_clip_cull_distance, so on a driver without that
        // extension there is nowhere to put a clip distance at all: SPIRV-Cross emits
        // gl_ClipDistance behind an `#extension ... : require` the ESSL compiler rejects, and
        // DirectGLES has no state to forward the per-distance enables into (see the gate in
        // DirectGLES::SyncRenderState). Starting at 8 meant a probe that could never run left an
        // optimistic 8 behind, so the frontend promised eight clip planes and every draw with a
        // clipping program silently rendered nothing. The guarded probe below only ever widens it.
        GLint maxClipDistances = 0;
        // The cull half of the same extension, and the same "zero is a legal answer" rule: a cull
        // distance discards the whole primitive, so promising eight on a driver that has none does
        // not fail loudly, it drops every draw of a culling program.
        GLint maxCullDistances = 0;
        GLint maxCombinedClipAndCullDistances = 0;
        GLint maxViewports = 16;
        // GL_UNDEFINED_VERTEX is what stands when the probes below cannot run, and it is a legal
        // answer rather than a placeholder: with neither geometry shaders nor a viewport array
        // there is no layered or multi-viewport draw for a convention to describe.
        GLenum layerProvokingVertex = GL_UNDEFINED_VERTEX;
        GLenum viewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
        GLfloat minFragmentInterpolationOffset = -0.5f;
        GLfloat maxFragmentInterpolationOffset = 0.4375f;
        GLint fragmentInterpolationOffsetBits = 4;
        // Core minimums of both APIs (GL 4.6 table 23.53, ES 3.1 table 20.40); the probe
        // below only ever widens them.
        GLint minProgramTextureGatherOffset = -8;
        GLint maxProgramTextureGatherOffset = 7;
        GLint maxPatchVertices = 32;
        GLint maxTessGenLevel = 64;
        // Function-scope, and used by every probe group below rather than redeclared inside each
        // one. Returns whether anything was drained, which is what lets a group tell "the driver
        // answered" from "the driver rejected the pname and left my local alone".
        const auto drainErrors = [&glesFuncs]() {
            Bool hadError = false;
            if (glesFuncs.glGetError) {
                while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
            }
            return hadError;
        };

        // THE GENERATOR OF THIS WHOLE BUG FAMILY, closed here. A bare glGetIntegerv/glGetFloatv
        // of a pname the driver does not have does two damaging things at once: it leaves the
        // local at whatever the declaration initialised it to - an optimistic number the frontend
        // then advertises as a capability - and it leaves a GL_INVALID_ENUM in the queue where
        // the next unrelated probe's caller, or the application's first glGetError, gets blamed
        // for it. The per-stage storage block, fragment interpolation and buffer texture probes
        // below already drain and fall back; this unconditional run did neither, which is how
        // GL_MAX_CLIP_DISTANCES came to be advertised as 8 on a driver with no clip distances at
        // all. Every pname here that is not ES core is now either gated on the capability that
        // makes it exist or floored at the value a rejected probe would have left, and the whole
        // run is bracketed by a drain.
        drainErrors();
        glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
        // GL_SMOOTH_LINE_WIDTH_RANGE / GL_SMOOTH_LINE_WIDTH_GRANULARITY (0x0B22 / 0x0B23) are
        // desktop-only - ES has never had an antialiased line width query - so on a real GLES
        // driver these two raise GL_INVALID_ENUM. Kept as probes rather than dropped because the
        // ANGLE and desktop-GL hosts MobileGL also runs on do answer them; the initialisers are
        // the GL 4.6 table 23.55 minimum of [1, 1], which is both the honest answer for a driver
        // that cannot say and what an untouched out-param already holds.
        glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
        glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
        glesFuncs.glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, aliasedPointSizeRange);
        glesFuncs.glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &max3DTextureSize);
        glesFuncs.glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &maxArrayTextureLayers);
        glesFuncs.glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &maxCubeMapTextureSize);
        glesFuncs.glGetIntegerv(GL_MAX_FRAMEBUFFER_WIDTH, &maxFramebufferWidth);
        glesFuncs.glGetIntegerv(GL_MAX_FRAMEBUFFER_HEIGHT, &maxFramebufferHeight);
        glesFuncs.glGetIntegerv(GL_MAX_FRAMEBUFFER_LAYERS, &maxFramebufferLayers);
        glesFuncs.glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
        glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
        glesFuncs.glGetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &maxColorTextureSamples);
        glesFuncs.glGetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &maxDepthTextureSamples);
        glesFuncs.glGetIntegerv(GL_MAX_FRAMEBUFFER_SAMPLES, &maxFramebufferSamples);
        glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
        glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
        glesFuncs.glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &maxSampleMaskWords);
        // MobileGL's sample-mask state only stores a single 32-bit word (see
        // RenderState::SampleMaskValue) and SampleMaski_State() rejects any
        // maskNumber other than 0. Advertising the real driver's value here (e.g.
        // NVIDIA's GLES driver reports 2) makes glSampleMaski(1, ...) - which
        // dEQP's per-case gluStateReset always issues up to GL_MAX_SAMPLE_MASK_WORDS -
        // raise GL_INVALID_VALUE and aborts the whole glcts process after every
        // single test case. 1 is a spec-legal value (the minimum required), so cap
        // to what is actually implemented instead of forwarding the raw driver limit.
        maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
        // The multisample ceilings above are ES 3.1 state apart from GL_MAX_SAMPLES, which is ES
        // 3.0, so a 3.0 context rejects five of the six and leaves whatever the out-param held.
        // One sample is what a rejected probe leaves behind and is also the smallest legal
        // answer, so clamp rather than trust: a zero reaching GL_Getter would have the frontend
        // reject the very sample count it just advertised (see GetAdvertisedMaxSamples).
        maxColorTextureSamples = std::max(maxColorTextureSamples, 1);
        maxDepthTextureSamples = std::max(maxDepthTextureSamples, 1);
        maxFramebufferSamples = std::max(maxFramebufferSamples, 1);
        maxIntegerSamples = std::max(maxIntegerSamples, 1);
        maxSamples = std::max(maxSamples, 1);
        maxSampleMaskWords = std::max(maxSampleMaskWords, 1);
        // ES 3.2 core, or EXT_tessellation_shader on 3.1. Probed rather than version-gated so a
        // 3.1 driver that HAS the extension still gets to answer; the clamp below is what makes a
        // rejected query safe, since GL 4.6 table 23.66 and ES 3.2 table 21.45 set the same
        // minimums the initialisers carry and neither API permits less.
        glesFuncs.glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
        glesFuncs.glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
        maxPatchVertices = std::max(maxPatchVertices, 32);
        maxTessGenLevel = std::max(maxTessGenLevel, 64);
        glesFuncs.glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET, &minProgramTextureGatherOffset);
        glesFuncs.glGetIntegerv(GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET, &maxProgramTextureGatherOffset);
        // A driver that leaves the probe untouched (pre-ES 3.1, or an ignored enum) must not
        // drag the advertised range below what GL 4.0 requires of us.
        minProgramTextureGatherOffset = std::min(minProgramTextureGatherOffset, -8);
        maxProgramTextureGatherOffset = std::max(maxProgramTextureGatherOffset, 7);
        glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
        glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
        glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
        glesFuncs.glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxCombinedTextureImageUnits);
        glesFuncs.glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribs);
        glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeShaderStorageBlocks);
        glesFuncs.glGetIntegerv(GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, &maxCombinedShaderStorageBlocks);
        glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &maxComputeUniformBlocks);
        glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &maxComputeWorkGroupInvocations);
        glesFuncs.glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxShaderStorageBufferBindings);
        // GL_MAX_TEXTURE_BUFFER_SIZE is deliberately NOT batched here: like
        // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT below, the pname only exists once buffer textures do,
        // so on a driver without them it raises GL_INVALID_ENUM, leaves the local at MobileGL's own
        // floor, and - because nothing drains the queue until the alignment probe far below - lets
        // that error be misattributed to any query in between. It is queried in the guarded block
        // that resolves caps.TextureBufferSupport instead.
        glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxUniformBufferBindings);
        glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize);
        glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
        glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms);
        glesFuncs.glGetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &maxVertexImageUniforms);
        glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
        glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
        // Geometry shaders and their image-uniform query are core only in ES 3.2. DirectGLES
        // emits ESSL 3.10 on an ES 3.1 context, so reporting zero there is both legal and an
        // accurate description of what the backend compiler can consume.
        if (caps.GLESVersion.Major > 3 ||
            (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
            glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
        }
        // Closes the bracket opened before the run: every local above now holds either the
        // driver's answer or a floor, and nothing this function asked for is left in the error
        // queue for a later probe - or the application - to be blamed for.
        if (drainErrors()) {
            MGLOG_W("One or more capability queries were rejected by this driver; the affected "
                    "limits keep MobileGL's spec-minimum floors");
        }
        // Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes
        // above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames
        // are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under
        // EXT_tessellation_shader / EXT_geometry_shader), so on an older context they raise
        // GL_INVALID_ENUM, leave the local untouched, and - with nothing draining the queue until
        // some later probe - let that error be misattributed to an unrelated query in between, or
        // leak into the application's first glGetError.
        //
        // A stage whose probe does not run keeps the spec minimum, which for all four graphics
        // stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context,
        // where those stages do not exist at all.
        {
            // Isolate from errors raised by the preceding probes so the drain below reports on
            // these queries only.
            drainErrors();
            glesFuncs.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexShaderStorageBlocks);
            glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, &maxFragmentShaderStorageBlocks);
            if (drainErrors()) {
                MGLOG_W("Per-stage shader storage block query failed for the vertex/fragment "
                        "stages; assuming the ES minimums (vertex 0, fragment 4)");
                maxVertexShaderStorageBlocks = 0;
                maxFragmentShaderStorageBlocks = 4;
            }
            if (esAtLeast32) {
                glesFuncs.glGetIntegerv(GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS,
                                        &maxTessControlShaderStorageBlocks);
                glesFuncs.glGetIntegerv(GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS,
                                        &maxTessEvaluationShaderStorageBlocks);
                glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS, &maxGeometryShaderStorageBlocks);
                if (drainErrors()) {
                    MGLOG_W("Per-stage shader storage block query failed for the tessellation/"
                            "geometry stages; assuming the ES minimum of 0");
                    maxTessControlShaderStorageBlocks = 0;
                    maxTessEvaluationShaderStorageBlocks = 0;
                    maxGeometryShaderStorageBlocks = 0;
                }
            }
            // A driver is free to report a negative or nonsensical count into an untouched
            // out-param; clamp before anything downstream treats it as a capacity.
            maxVertexShaderStorageBlocks = std::max(maxVertexShaderStorageBlocks, 0);
            maxTessControlShaderStorageBlocks = std::max(maxTessControlShaderStorageBlocks, 0);
            maxTessEvaluationShaderStorageBlocks = std::max(maxTessEvaluationShaderStorageBlocks, 0);
            maxGeometryShaderStorageBlocks = std::max(maxGeometryShaderStorageBlocks, 0);
            maxFragmentShaderStorageBlocks = std::max(maxFragmentShaderStorageBlocks, 0);
        }
        glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
        glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
        // GL_MAX_CLIP_DISTANCES is 0x0D32, which ES only ever spells GL_MAX_CLIP_DISTANCES_EXT and
        // only ever has under GL_EXT_clip_cull_distance. The extension was already resolved into
        // caps.SupportsClipDistance a few hundred lines above and is the same flag DirectGLES
        // gates the CLIP_DISTANCEi enable forwarding on, so ask the driver only where the pname
        // exists; everywhere else the honest 0 stands and no GL_INVALID_ENUM is left behind for an
        // unrelated query - or the application's first glGetError - to trip over.
        if (caps.SupportsClipDistance) {
            drainErrors();
            glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
            if (drainErrors()) {
                MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CLIP_DISTANCES was "
                        "rejected; reporting no clip distances");
                maxClipDistances = 0;
            }
            // GL_MAX_CULL_DISTANCES_EXT (0x82F9) and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT
            // (0x82FA) are the same tokens as their desktop spellings and arrive with the same
            // extension, so they are probed under the same guard and the same drain sandwich.
            drainErrors();
            glesFuncs.glGetIntegerv(GL_MAX_CULL_DISTANCES, &maxCullDistances);
            if (drainErrors()) {
                MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CULL_DISTANCES was "
                        "rejected; reporting no cull distances");
                maxCullDistances = 0;
            }
            drainErrors();
            glesFuncs.glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &maxCombinedClipAndCullDistances);
            if (drainErrors()) {
                MGLOG_W("GL_EXT_clip_cull_distance is advertised but "
                        "GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES was rejected; deriving it from the pair");
                maxCombinedClipAndCullDistances = 0;
            }
        }
        glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
        // GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is
        // what gl_Layer needs). Ask the driver where the pname exists rather than asserting a
        // convention: it is a statement about which vertex of a primitive supplies gl_Layer, and
        // MobileGL forwards the geometry stage to the driver rather than implementing the
        // selection itself, so the driver's answer IS MobileGL's answer. Below ES 3.2 there are
        // no layered draws to have a convention for and GL_UNDEFINED_VERTEX stands, which GL 4.6
        // table 23.65 explicitly permits.
        if (esAtLeast32) {
            GLint driverLayerConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
            drainErrors();
            glesFuncs.glGetIntegerv(GL_LAYER_PROVOKING_VERTEX, &driverLayerConvention);
            if (!drainErrors()) {
                layerProvokingVertex = NormalizeProvokingVertexConvention(driverLayerConvention);
            }
        }
        // GL_MAX_VIEWPORTS (0x825B), GL_VIEWPORT_SUBPIXEL_BITS (0x825C) and GL_VIEWPORT_BOUNDS_RANGE
        // (0x825D) all arrive with GL_OES_viewport_array and exist nowhere in ES core, so on the
        // drivers DirectGLES actually runs on all three raise GL_INVALID_ENUM. The values MobileGL
        // advertises do not change by asking: GL_Getter answers GL_MAX_VIEWPORTS from the frontend
        // state width (indexed viewport entry points validate against RenderStateParameters::
        // MAX_VIEWPORTS, so a device answer of 1 would reject indices the state can legitimately
        // hold), floors GL_SUBPIXEL_BITS at its own 4, and the bounds range is clamped to the core
        // minimum below. What changes is that the errors stop being manufactured.
        if (caps.SupportsViewportArray) {
            GLint driverViewportIndexConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
            drainErrors();
            glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
            glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
            glesFuncs.glGetIntegerv(GL_VIEWPORT_INDEX_PROVOKING_VERTEX, &driverViewportIndexConvention);
            if (glesFuncs.glGetFloatv) {
                glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange);
            }
            if (drainErrors()) {
                MGLOG_W("GL_OES_viewport_array is advertised but its viewport limit queries were "
                        "rejected; keeping the OpenGL core minimums");
                maxViewports = 16;
                viewportSubpixelBits = 0;
                viewportBoundsRange[0] = -32768.0f;
                viewportBoundsRange[1] = 32767.0f;
            } else {
                viewportIndexProvokingVertex =
                    NormalizeProvokingVertexConvention(driverViewportIndexConvention);
            }
        }
        if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
            // Isolate these optional queries from errors raised by preceding capability
            // probes, then consume any query error so initialization never leaks it into
            // the application's first glGetError call.
            drainErrors();
            glesFuncs.glGetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &minFragmentInterpolationOffset);
            glesFuncs.glGetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &maxFragmentInterpolationOffset);
            glesFuncs.glGetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &fragmentInterpolationOffsetBits);
            if (drainErrors()) {
                MGLOG_W("Fragment interpolation limit query failed; using OpenGL minimums");
                minFragmentInterpolationOffset = -0.5f;
                maxFragmentInterpolationOffset = 0.4375f;
                fragmentInterpolationOffsetBits = 4;
            }
        }
        // Only legal to query once the extension has been seen in the loop above, hence not batched
        // with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
        // Core from ES 3.2 on, whatever the extension string says.
        if (caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
            caps.SupportsTextureBorderClamp = true;
            caps.SupportsTextureCubeMapArray = true;
        }
        // Buffer-texture tier. Core from ES 3.2 on; below that the EXT spelling is preferred over
        // the OES one purely because SPIRV-Cross emits GL_EXT_texture_buffer natively, so a driver
        // with both needs no directive retargeting. The entry point has to have resolved either
        // way - the extension string alone is not support (see the multi-draw note above).
        {
            using Tier = MG_External::GLESCapabilities::TextureBufferTier;
            // Each tier needs the entry point that tier's support actually ships. Gating all
            // three on the unsuffixed name - the ES 3.2 CORE spelling - would make every
            // EXT/OES driver look unsupported on a strict loader, and would make MobileGL call
            // a core entry point the driver never exported on a permissive one. The suffixed
            // name is preferred where the support is an extension, with the core name accepted
            // as a fallback because drivers that expose both alias them.
            const Bool hasCoreEntryPoint = glesFuncs.glTexBuffer != nullptr;
            if (esAtLeast32 && hasCoreEntryPoint) {
                caps.TextureBufferSupport = Tier::CoreEs32;
            } else if (hasExtTextureBuffer && (glesFuncs.glTexBufferEXT != nullptr || hasCoreEntryPoint)) {
                caps.TextureBufferSupport = Tier::ExtensionEXT;
            } else if (hasOesTextureBuffer && (glesFuncs.glTexBufferOES != nullptr || hasCoreEntryPoint)) {
                caps.TextureBufferSupport = Tier::ExtensionOES;
            } else {
                caps.TextureBufferSupport = Tier::None;
            }

            // Assigned unconditionally, like every other capability in this function, so a
            // second fill on a reused struct cannot keep a stale true.
            caps.MaxTextureBufferSizeIsDriverReported = false;
            if (caps.TextureBufferSupport != Tier::None) {
                // Drain first: an error left by any earlier probe would otherwise read as this
                // query having failed, and the value would be discarded as a non-answer.
                if (glesFuncs.glGetError) {
                    while (glesFuncs.glGetError() != GL_NO_ERROR) {
                    }
                }
                glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize);
                if (glesFuncs.glGetError) {
                    Bool queryFailed = false;
                    while (glesFuncs.glGetError() != GL_NO_ERROR) {
                        queryFailed = true;
                    }
                    caps.MaxTextureBufferSizeIsDriverReported = !queryFailed;
                } else {
                    caps.MaxTextureBufferSizeIsDriverReported = true;
                }
            }
            // On the None tier the local keeps MobileGL's floor and
            // MaxTextureBufferSizeIsDriverReported stays false. The floor, not 0, is what the
            // frontend goes on advertising: MobileGL reports an OpenGL 4.x context, where buffer
            // textures are core and GL_MAX_TEXTURE_BUFFER_SIZE has a spec minimum of 65536, so 0
            // would be an illegal answer that no conformant app is prepared to read (several
            // divide by it or size an allocation with it). The dishonesty is contained by making
            // the missing capability loud instead - at capability init here, at glTexBuffer, at
            // program build, and as its own driver POST row - because GL offers no way to say
            // "buffer textures exist but cannot work".
            if (caps.TextureBufferSupport == Tier::None) {
                // Two ways to land here, and they are worth telling apart: the ordinary one (too
                // old, no extension) and the pathological one (the driver says it has them but
                // no entry point resolved), which is a driver or loader fault, not a missing
                // feature.
                const Bool claimsSupport = esAtLeast32 || hasExtTextureBuffer || hasOesTextureBuffer;
                MGLOG_I("    Buffer textures: UNSUPPORTED (%s). Any shader sampling a "
                        "samplerBuffer will fail to compile, and MobileGL keeps advertising "
                        "GL_MAX_TEXTURE_BUFFER_SIZE = %d because a GL 4.x context may not report 0.",
                        claimsSupport
                            ? "this driver advertises buffer textures but no glTexBuffer entry "
                              "point resolved, so none of them can be called"
                            : "ES core needs 3.2, and neither GL_EXT_texture_buffer nor "
                              "GL_OES_texture_buffer is present",
                        maxTextureBufferSize);
            }
        }
        if (caps.SupportsTextureFilterAnisotropy) {
            GLfloat maxTextureMaxAnisotropy = 1.0f;
            glesFuncs.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureMaxAnisotropy);
            caps.MaxTextureMaxAnisotropy = std::max(maxTextureMaxAnisotropy, 1.0f);
        }
        caps.AliasedLineWidthRangeMin = aliasedLineWidthRange[0];
        caps.AliasedLineWidthRangeMax = aliasedLineWidthRange[1];
        caps.SmoothLineWidthRangeMin = smoothLineWidthRange[0];
        caps.SmoothLineWidthRangeMax = smoothLineWidthRange[1];
        caps.SmoothLineWidthGranularity = smoothLineWidthGranularity;
        caps.PointSizeRangeMin = aliasedPointSizeRange[0];
        caps.PointSizeRangeMax = aliasedPointSizeRange[1];
        caps.PointSizeGranularity = 1.0f;
        caps.Max3DTextureSize = max3DTextureSize;
        caps.MaxArrayTextureLayers = maxArrayTextureLayers;
        caps.MaxCubeMapTextureSize = maxCubeMapTextureSize;
        caps.MaxFramebufferWidth = maxFramebufferWidth;
        caps.MaxFramebufferHeight = maxFramebufferHeight;
        caps.MaxFramebufferLayers = maxFramebufferLayers;
        caps.MaxRenderbufferSize = maxRenderbufferSize;
        caps.MaxTextureSize = maxTextureSize;
        caps.MaxColorTextureSamples = maxColorTextureSamples;
        caps.MaxDepthTextureSamples = maxDepthTextureSamples;
        caps.MaxFramebufferSamples = maxFramebufferSamples;
        caps.MaxIntegerSamples = maxIntegerSamples;
        caps.MaxSamples = maxSamples;
        caps.MaxSampleMaskWords = maxSampleMaskWords;
        caps.MaxPatchVertices = maxPatchVertices;
        caps.MaxTessGenLevel = maxTessGenLevel;
        caps.MinProgramTextureGatherOffset = minProgramTextureGatherOffset;
        caps.MaxProgramTextureGatherOffset = maxProgramTextureGatherOffset;
        caps.MaxTextureImageUnits = maxTextureImageUnits;
        caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits;
        caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits;
        caps.MaxCombinedTextureImageUnits = maxCombinedTextureImageUnits;
        // Not the driver's answer alone: MobileGL emits every vertex input as a
        // layout(location = N) qualifier, so an attribute the driver counts but its ESSL
        // compiler will not let anything DECLARE is not an attribute MobileGL can hand to an
        // application. The probe measures where the qualifier actually stops (see
        // SelfTest::ProbeExplicitVertexInputLocationCeiling - Adreno 830 advertises 32 and
        // refuses the qualifier from 16 up) and answers with the advertised count on every
        // driver that has no such gap and on any run that reaches no verdict, so this only ever
        // lowers the number, and only on evidence.
        const SelfTest::VertexInputLocationCeilingMeasurement& locationCeiling =
            SelfTest::ExplicitVertexInputLocationCeiling(glesFuncs);
        // Guarded on `detected` rather than on the number alone: a probe that reached no verdict
        // has measured nothing, and the clamp must be driven by evidence or not applied at all.
        caps.MaxVertexAttribs = locationCeiling.detected
                                    ? std::min(maxVertexAttribs, locationCeiling.usableLocations)
                                    : maxVertexAttribs;
        if (locationCeiling.detected) {
            MGLOG_I("    GL_MAX_VERTEX_ATTRIBS reduced from the driver's %d to %d: "
                    "layout(location = N) on a vertex input is refused from N = %d upward",
                    maxVertexAttribs, caps.MaxVertexAttribs, locationCeiling.usableLocations);
        }
        caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
        caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
        caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
        caps.MaxTessControlShaderStorageBlocks = maxTessControlShaderStorageBlocks;
        caps.MaxTessEvaluationShaderStorageBlocks = maxTessEvaluationShaderStorageBlocks;
        caps.MaxGeometryShaderStorageBlocks = maxGeometryShaderStorageBlocks;
        caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks;
        caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
        caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
        caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
        caps.MaxTextureBufferSize = maxTextureBufferSize;
        // Through glesFuncs, like every other capability query here: a bare glGetIntegerv resolves
        // to MobileGL's own exported entry point, which answers this pname from the very
        // capability table being filled in - so the driver's real alignment never arrived and the
        // backend reported an unconstrained offset it cannot honour.
        GLint textureBufferOffsetAlignment = 1;
        glesFuncs.glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &textureBufferOffsetAlignment);
        // Core in ES 3.2 and in EXT_texture_buffer; an older context rejects the pname and leaves
        // the default in place.
        if (glesFuncs.glGetError) {
            while (glesFuncs.glGetError() != GL_NO_ERROR) {}
        }
        caps.TextureBufferOffsetAlignment = std::max(1, textureBufferOffsetAlignment);
        caps.MaxUniformBufferBindings = maxUniformBufferBindings;
        caps.MaxUniformBlockSize = maxUniformBlockSize;
        caps.MaxImageUnits = maxImageUnits;
        caps.MaxCombinedImageUniforms = maxCombinedImageUniforms;
        caps.MaxVertexImageUniforms = maxVertexImageUniforms;
        caps.MaxGeometryImageUniforms = maxGeometryImageUniforms;
        caps.MaxFragmentImageUniforms = maxFragmentImageUniforms;
        caps.MaxComputeImageUniforms = maxComputeImageUniforms;
        caps.MaxDrawBuffers = maxDrawBuffers;
        caps.MaxColorAttachments = maxColorAttachments;
        // A driver is free to write nonsense into an out-param it then rejects, and without the
        // extension the probe above never ran at all - so the flag, not the local, decides.
        caps.MaxClipDistances = caps.SupportsClipDistance ? std::max(maxClipDistances, 0) : 0;
        caps.MaxCullDistances = caps.SupportsClipDistance ? std::max(maxCullDistances, 0) : 0;
        // The combined limit can never be smaller than either half (GL 4.6 core 11.1.3.10 / the
        // EXT spec say so), so a driver that rejected the combined query but answered the other
        // two still gets a usable - and never over-stated - number.
        caps.MaxCombinedClipAndCullDistances =
            caps.SupportsClipDistance
                ? std::max({maxCombinedClipAndCullDistances, caps.MaxClipDistances, caps.MaxCullDistances})
                : 0;
        caps.MaxViewports = maxViewports;
        caps.LayerProvokingVertex = layerProvokingVertex;
        caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex;
        caps.MaxViewportWidth = maxViewportDims[0];
        caps.MaxViewportHeight = maxViewportDims[1];
        // Only ever WIDER than the core minimum: a driver that answered the query is allowed to
        // exceed the floor but never to sit inside it, and a driver that rejected the query left
        // the floor in place. Written as a clamp rather than a plain assignment so a partial
        // write (one component answered, the other not) cannot narrow the range either.
        caps.ViewportBoundsRangeMin = std::min(viewportBoundsRange[0], -32768.0f);
        caps.ViewportBoundsRangeMax = std::max(viewportBoundsRange[1], 32767.0f);
        caps.ViewportSubpixelBits = viewportSubpixelBits;
        caps.MinFragmentInterpolationOffset =
            std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
                ? minFragmentInterpolationOffset
                : -0.5f;
        caps.MaxFragmentInterpolationOffset = 0.4375f;
        caps.FragmentInterpolationOffsetBits = 4;
        if (fragmentInterpolationOffsetBits >= 4 && std::isfinite(maxFragmentInterpolationOffset)) {
            const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -fragmentInterpolationOffsetBits);
            if (maxFragmentInterpolationOffset >= requiredMaxOffset) {
                caps.MaxFragmentInterpolationOffset = maxFragmentInterpolationOffset;
                caps.FragmentInterpolationOffsetBits = fragmentInterpolationOffsetBits;
            }
        }
        MGLOG_I("    GL_ALIASED_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.AliasedLineWidthRangeMin,
                caps.AliasedLineWidthRangeMax);
        MGLOG_I("    GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin,
                caps.SmoothLineWidthRangeMax);
        MGLOG_I("    GL_SMOOTH_LINE_WIDTH_GRANULARITY: %.3f", caps.SmoothLineWidthGranularity);
        MGLOG_I("    GL_ALIASED_POINT_SIZE_RANGE: [%.3f, %.3f]", caps.PointSizeRangeMin, caps.PointSizeRangeMax);
        MGLOG_I("    GL_MAX_3D_TEXTURE_SIZE: %d", caps.Max3DTextureSize);
        MGLOG_I("    GL_MAX_ARRAY_TEXTURE_LAYERS: %d", caps.MaxArrayTextureLayers);
        MGLOG_I("    GL_MAX_CUBE_MAP_TEXTURE_SIZE: %d", caps.MaxCubeMapTextureSize);
        MGLOG_I("    GL_MAX_FRAMEBUFFER_WIDTH: %d", caps.MaxFramebufferWidth);
        MGLOG_I("    GL_MAX_FRAMEBUFFER_HEIGHT: %d", caps.MaxFramebufferHeight);
        MGLOG_I("    GL_MAX_FRAMEBUFFER_LAYERS: %d", caps.MaxFramebufferLayers);
        MGLOG_I("    GL_MAX_RENDERBUFFER_SIZE: %d", caps.MaxRenderbufferSize);
        MGLOG_I("    GL_MAX_TEXTURE_SIZE: %d", caps.MaxTextureSize);
        MGLOG_I("    GL_MAX_COLOR_TEXTURE_SAMPLES: %d", caps.MaxColorTextureSamples);
        MGLOG_I("    GL_MAX_DEPTH_TEXTURE_SAMPLES: %d", caps.MaxDepthTextureSamples);
        MGLOG_I("    GL_MAX_FRAMEBUFFER_SAMPLES: %d", caps.MaxFramebufferSamples);
        MGLOG_I("    GL_MAX_INTEGER_SAMPLES: %d", caps.MaxIntegerSamples);
        MGLOG_I("    GL_MAX_SAMPLES: %d", caps.MaxSamples);
        MGLOG_I("    GL_MAX_SAMPLE_MASK_WORDS: %d", caps.MaxSampleMaskWords);
        MGLOG_I("    GL_MAX_TEXTURE_IMAGE_UNITS: %d", caps.MaxTextureImageUnits);
        MGLOG_I("    GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS: %d", caps.MaxVertexTextureImageUnits);
        MGLOG_I("    GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS: %d", caps.MaxComputeTextureImageUnits);
        MGLOG_I("    GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: %d", caps.MaxCombinedTextureImageUnits);
        MGLOG_I("    GL_MAX_VERTEX_ATTRIBS: %d", caps.MaxVertexAttribs);
        MGLOG_I("    GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks);
        MGLOG_I("    GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: %d", caps.MaxCombinedShaderStorageBlocks);
        // Worth a line each: a zero here is what stops an application's storage block from ever
        // working in that stage, and reading it back from an artifact is the difference between
        // "MobileGL dropped my draw" and "this driver has no SSBOs outside compute".
        MGLOG_I("    GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: %d", caps.MaxVertexShaderStorageBlocks);
        MGLOG_I("    GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessControlShaderStorageBlocks);
        MGLOG_I("    GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessEvaluationShaderStorageBlocks);
        MGLOG_I("    GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: %d", caps.MaxGeometryShaderStorageBlocks);
        MGLOG_I("    GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks);
        MGLOG_I("    GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
        MGLOG_I("    GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
        MGLOG_I("    GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
        // Three distinct states, and the suffix must not conflate them: a driver answer, a floor
        // kept because there are no buffer textures to ask about, and a floor kept because the
        // driver claimed buffer textures but then refused the query (which is a driver bug worth
        // seeing spelled out rather than hidden behind the same wording as the honest case).
        MGLOG_I("    GL_MAX_TEXTURE_BUFFER_SIZE: %d%s", caps.MaxTextureBufferSize,
                caps.MaxTextureBufferSizeIsDriverReported
                    ? ""
                    : (caps.TextureBufferSupport == MG_External::GLESCapabilities::TextureBufferTier::None
                           ? " (MobileGL floor - the driver has no buffer textures to ask)"
                           : " (MobileGL floor - the driver claims buffer textures but rejected the query)"));
        MGLOG_I("    GL_MAX_UNIFORM_BUFFER_BINDINGS: %d", caps.MaxUniformBufferBindings);
        MGLOG_I("    GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize);
        MGLOG_I("    GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits);
        MGLOG_I("    GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms);
        MGLOG_I("    GL_MAX_VERTEX_IMAGE_UNIFORMS: %d", caps.MaxVertexImageUniforms);
        MGLOG_I("    GL_MAX_GEOMETRY_IMAGE_UNIFORMS: %d", caps.MaxGeometryImageUniforms);
        MGLOG_I("    GL_MAX_FRAGMENT_IMAGE_UNIFORMS: %d", caps.MaxFragmentImageUniforms);
        MGLOG_I("    GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
        MGLOG_I("    GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
        MGLOG_I("    GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
        // Worth spelling the reason out for the same reason the per-stage storage block counts
        // are: a zero here is what stops an application's gl_ClipDistance from ever clipping, and
        // reading it back from an artifact is the difference between "MobileGL dropped my draw"
        // and "this driver has no clip distances".
        MGLOG_I("    GL_MAX_CLIP_DISTANCES: %d%s", caps.MaxClipDistances,
                caps.SupportsClipDistance ? "" : " (no GL_EXT_clip_cull_distance on this driver)");
        MGLOG_I("    GL_MAX_CULL_DISTANCES: %d", caps.MaxCullDistances);
        MGLOG_I("    GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES: %d", caps.MaxCombinedClipAndCullDistances);
        MGLOG_I("    GL_MAX_VIEWPORTS: %d", caps.MaxViewports);
        MGLOG_I("    GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight);
        MGLOG_I("    GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin,
                caps.ViewportBoundsRangeMax);
        MGLOG_I("    GL_VIEWPORT_SUBPIXEL_BITS: %d", caps.ViewportSubpixelBits);
        MGLOG_I("    GL_LAYER_PROVOKING_VERTEX: %s", ProvokingVertexConventionName(caps.LayerProvokingVertex));
        MGLOG_I("    GL_VIEWPORT_INDEX_PROVOKING_VERTEX: %s",
                ProvokingVertexConventionName(caps.ViewportIndexProvokingVertex));

        caps.IndirectDrawInstanceIdIncludesBaseInstance =
            ProbeIndirectInstanceIdIncludesBaseInstance(caps, glesFuncs);
        MGLOG_I("    Indirect draw gl_InstanceID includes baseInstance: %s",
                caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");

        caps.SupportsLocatedInterStageIoBlocks =
            ProbeLocatedInterStageIoBlocksTransportPayload(caps, glesFuncs);
        MGLOG_I("    Located inter-stage interface blocks transport their payload: %s",
                caps.SupportsLocatedInterStageIoBlocks
                    ? "true"
                    : "false (DirectGLES will emit tessellation/geometry programs' interface "
                      "blocks without a location qualifier)");

        caps.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos;
        caps.IsAngleLlvmpipeRenderer =
            caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
        caps.AvoidSamplerMipmapMinFilter =
            caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidSamplerMipmapMinFilter;
        caps.AvoidExplicitLodBias =
            caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidExplicitLodBias;
        MGLOG_I("    GL_EXT_disjoint_timer_query supported: %s",
                caps.SupportsDisjointTimerQuery ? "true" : "false");
        MGLOG_I("    GL_KHR_parallel_shader_compile supported: %s",
                caps.SupportsParallelShaderCompile ? "true" : "false");
        MGLOG_I("    ANGLE renderer: %s", caps.IsAngleRenderer ? "true" : "false");
        MGLOG_I("    ANGLE llvmpipe renderer: %s", caps.IsAngleLlvmpipeRenderer ? "true" : "false");
        MGLOG_I("    Avoid sampler mipmap min filter: %s",
                caps.AvoidSamplerMipmapMinFilter ? "true" : "false");
        MGLOG_I("    Avoid explicit LOD bias: %s", caps.AvoidExplicitLodBias ? "true" : "false");

        // Last line of defence. Capability init is the very first thing that touches the driver,
        // so anything it leaves in the error queue surfaces at the APPLICATION's first
        // glGetError and gets attributed to whatever call the app happened to make. Every group
        // above drains its own, but a probe added later must not be able to reintroduce the leak.
        if (drainErrors()) {
            MGLOG_W("Capability initialization left a GL error behind; it has been consumed so it "
                    "cannot surface at the application's first glGetError");
        }
        return true;
    }
} // namespace MobileGL::MG_Util::BackendLoader
