// MobileGL - MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.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 "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <Config.h>
#include <algorithm>
#include <cmath>
#include <format>

namespace MobileGL::MG_Backend::DirectGLES {
    namespace {
        Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
            (void)dpy;
            return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
        }

        void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
            if (!gl.glGetError) return;
            while (gl.glGetError() != GL_NO_ERROR) {}
        }

        Bool CheckNoGLError(const MG_External::GLESFunctionsTable& gl) {
            return !gl.glGetError || gl.glGetError() == GL_NO_ERROR;
        }

        GLenum GetTextureBindingQuery(TextureTarget target) {
            switch (target) {
            case TextureTarget::Texture2D:
                return GL_TEXTURE_BINDING_2D;
            case TextureTarget::Texture3D:
                return GL_TEXTURE_BINDING_3D;
            case TextureTarget::TextureCubeMap:
                return GL_TEXTURE_BINDING_CUBE_MAP;
            case TextureTarget::Texture2DArray:
                return GL_TEXTURE_BINDING_2D_ARRAY;
            case TextureTarget::TextureCubeMapArray:
                return GL_TEXTURE_BINDING_CUBE_MAP_ARRAY;
            case TextureTarget::Texture2DMultisample:
                return GL_TEXTURE_BINDING_2D_MULTISAMPLE;
            case TextureTarget::Texture2DMultisampleArray:
                return GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY;
            default:
                return GL_UNKNOWN_MGL;
            }
        }

        Bool IsGLESProbeTextureTarget(TextureTarget target) {
            switch (target) {
            case TextureTarget::Texture2D:
            case TextureTarget::Texture3D:
            case TextureTarget::TextureCubeMap:
            case TextureTarget::Texture2DArray:
            case TextureTarget::TextureCubeMapArray:
            case TextureTarget::Texture2DMultisample:
            case TextureTarget::Texture2DMultisampleArray:
                return true;
            default:
                return false;
            }
        }

        Bool IsGLESProbeMultisampleTarget(TextureTarget target) {
            return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
        }

        GLenum GetFramebufferAttachment(TextureInternalFormat format) {
            const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(format);
            const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(format);
            if (isDepth && isStencil) return GL_DEPTH_STENCIL_ATTACHMENT;
            if (isDepth) return GL_DEPTH_ATTACHMENT;
            if (isStencil) return GL_STENCIL_ATTACHMENT;
            return GL_COLOR_ATTACHMENT0;
        }

        FormatCapabilityFlags GetAttachmentCaps(TextureInternalFormat format) {
            FormatCapabilityFlags caps = FormatCapability::FramebufferRenderable;
            const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(format);
            const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(format);
            if (!isDepth && !isStencil) {
                caps |= FormatCapability::ColorAttachment;
            }
            if (isDepth) {
                caps |= FormatCapability::DepthAttachment;
            }
            if (isStencil) {
                caps |= FormatCapability::StencilAttachment;
            }
            return caps;
        }

        Bool IsDepthOnlyFormat(TextureInternalFormat format) {
            return MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format);
        }

        Bool IsFilterableFormat(TextureInternalFormat format) {
            const GLenum glFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
            GLenum normalizedInternalFormat = glFormat;
            GLenum imageFormat = GL_RGBA;
            GLenum imageType = GL_UNSIGNED_BYTE;
            MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
                                                                  &normalizedInternalFormat, &imageFormat, &imageType);
            return imageFormat != GL_RED_INTEGER && imageFormat != GL_RG_INTEGER && imageFormat != GL_RGB_INTEGER &&
                   imageFormat != GL_RGBA_INTEGER && !MG_Util::IsDepthFormatInternalFormat(format) &&
                   !MG_Util::IsStencilFormatInternalFormat(format);
        }

        FormatCapabilityFlags GetTextureFeatureCaps(TextureInternalFormat format, TextureTarget target) {
            FormatCapabilityFlags caps = FormatCapability::Creatable | FormatCapability::Sampled;
            if (IsFilterableFormat(format)) {
                caps |= FormatCapability::LinearFilter;
            }
            if (!IsGLESProbeMultisampleTarget(target) && !MG_Util::IsStencilFormatInternalFormat(format)) {
                caps |= FormatCapability::GenerateMipmap;
            }
            if (IsDepthOnlyFormat(format)) {
                caps |= FormatCapability::TextureShadow;
            }
            if (IsGLESProbeMultisampleTarget(target)) {
                caps |= FormatCapability::MultisampleTexture;
            }
            return caps;
        }

        FormatCapabilityFlags GetRenderbufferFeatureCaps(TextureInternalFormat format) {
            return FormatCapabilityFlags(FormatCapability::Creatable) | GetAttachmentCaps(format) |
                   FormatCapability::MultisampleRenderbuffer;
        }

        struct GLESProbeFormatInfo {
            GLenum InternalFormat = GL_UNKNOWN_MGL;
            GLenum ImageFormat = GL_RGBA;
            GLenum ImageType = GL_UNSIGNED_BYTE;
            String Reason;
        };

        GLESProbeFormatInfo BuildNativeProbeFormatInfo(GLenum requestedInternalFormat) {
            GLESProbeFormatInfo info;
            info.InternalFormat = requestedInternalFormat;
            MG_Util::TextureFormatProcessor::NormalizePixelFormat(requestedInternalFormat,
                                                                  PixelFormatNormalizeOptionBit::None, nullptr,
                                                                  &info.ImageFormat, &info.ImageType);
            return info;
        }

        Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions(
            const MG_External::GLESCapabilities& capabilities) {
            Flags<PixelFormatNormalizeOptionBit> options;
            if (capabilities.IsAngleRenderer) {
                options |= PixelFormatNormalizeOptionBit::NoRgb16;
                options |= PixelFormatNormalizeOptionBit::NoSnorm16;
                options |= PixelFormatNormalizeOptionBit::NoSnorm8;
            }
            return options;
        }

        Flags<PixelFormatNormalizeOptionBit> GetDriverPixelFormatNormalizeOptions(
            const MG_External::GLESCapabilities& capabilities) {
            Flags<PixelFormatNormalizeOptionBit> options = PixelFormatNormalizeOptionBit::NoDepthComponent32;
            options |= PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
            options |= PixelFormatNormalizeOptionBit::NoRGB16Snorm;
            if (!capabilities.SupportsNorm16Texture) {
                options |= PixelFormatNormalizeOptionBit::NoNorm16;
            }
            return options;
        }

        String BuildPixelFormatFallbackReason(Flags<PixelFormatNormalizeOptionBit> options, Bool forced) {
            Vector<String> reasons;
            if (options & PixelFormatNormalizeOptionBit::NoNorm16) {
                reasons.push_back("EXT_texture_norm16 not supported");
            }
            if (options & PixelFormatNormalizeOptionBit::NoRgb16) {
                reasons.push_back(forced ? "RGB16 fallback forced by backend policy"
                                         : "RGB16 native path is not supported");
            }
            if (options & PixelFormatNormalizeOptionBit::NoSnorm16) {
                reasons.push_back(forced ? "SNORM16 fallback forced by backend policy"
                                         : "SNORM16 native path is not supported");
            }
            if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
                reasons.push_back(forced ? "SNORM8 fallback forced by backend policy"
                                         : "SNORM8 native path is not supported");
            }
            if (options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) {
                reasons.push_back(forced ? "RGBA8_SNORM fallback forced by backend policy"
                                         : "RGBA8_SNORM render target path is not supported");
            }
            if (options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) {
                reasons.push_back(forced ? "RGB16_SNORM fallback forced by backend policy"
                                         : "RGB16_SNORM render target path is not supported");
            }
            if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
                reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
            }
            if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
                reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
            }
            // A format is either 8- or 16-bit signed normalized, so at most one of the two ever
            // survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
            if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
                (options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
                reasons.push_back("EXT_render_snorm not supported");
            }

            String reason;
            for (SizeT i = 0; i < reasons.size(); ++i) {
                if (i != 0) reason += "; ";
                reason += reasons[i];
            }
            return reason.empty() ? "Native format probe failed" : reason;
        }

        String ConvertFallbackInternalFormatToString(GLenum internalFormat) {
            const TextureInternalFormat logicalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalFormat);
            if (logicalFormat != TextureInternalFormat::Unknown) {
                return MG_Util::ConvertTextureInternalFormatToString(logicalFormat);
            }
            return MG_Util::ConvertGLEnumToString(internalFormat);
        }

        void LogGLESFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
                                 const GLESProbeFormatInfo& fallbackInfo) {
            MGLOG_D("Caveat: %s %s not fully supported. Reason: %s. Fallback: %s",
                    GetFormatCapabilityTargetName(targetIndex).c_str(),
                    MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(), fallbackInfo.Reason.c_str(),
                    ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str());
        }

        Bool BuildFallbackProbeFormatInfo(GLenum requestedInternalFormat, Flags<PixelFormatNormalizeOptionBit> options,
                                          Bool forced, GLESProbeFormatInfo& outInfo) {
            const Flags<PixelFormatNormalizeOptionBit> applicableOptions =
                MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
                                                                                          options);
            if (!applicableOptions) {
                return false;
            }

            MG_Util::TextureFormatProcessor::NormalizePixelFormat(requestedInternalFormat, applicableOptions,
                                                                  &outInfo.InternalFormat, &outInfo.ImageFormat,
                                                                  &outInfo.ImageType);
            outInfo.Reason = BuildPixelFormatFallbackReason(applicableOptions, forced);
            return outInfo.InternalFormat != GL_UNKNOWN_MGL;
        }

        FormatCapabilityFlags BuildTextureCapsFromProbe(TextureInternalFormat logicalFormat, TextureTarget target,
                                                        Bool renderable) {
            FormatCapabilityFlags caps = GetTextureFeatureCaps(logicalFormat, target);
            if (renderable) {
                caps |= GetAttachmentCaps(logicalFormat);
                if (target == TextureTarget::Texture3D || target == TextureTarget::Texture2DArray ||
                    target == TextureTarget::TextureCubeMapArray ||
                    target == TextureTarget::Texture2DMultisampleArray) {
                    caps |= FormatCapability::FramebufferLayered;
                }
            }
            return caps;
        }

        void AddFullFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex,
                               FormatCapabilityFlags caps) {
            cache.FullCaps[targetIndex][formatIndex] |= caps;
        }

        Bool AddCaveatFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex,
                                 FormatCapabilityFlags caps) {
            Bool added = false;
            for (FormatCapability capability : kReportedFormatCapabilities) {
                if (HasFormatCapability(caps, capability) &&
                    !HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], capability)) {
                    cache.CaveatCaps[targetIndex][formatIndex] |= capability;
                    added = true;
                }
            }
            return added;
        }

        Int GetGLESFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
                                    TextureInternalFormat logicalFormat, GLenum imageFormat) {
            const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
            const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
            const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
                                   imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
            if (isDepth || isStencil) {
                return capabilities.MaxDepthTextureSamples;
            }
            if (isInteger) {
                return capabilities.MaxIntegerSamples;
            }
            return capabilities.MaxColorTextureSamples;
        }

        // The RENDERBUFFER twin, and it is a different set of pnames on purpose.
        // GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
        // bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
        // integer formats. Using the texture ceilings here - which is what the renderbuffer probe
        // did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
        // context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
        // in the GLES loader) and the walk below would never run past one sample, recording {1}
        // for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
        // reports 4. Once the frontend validates against this list, that would reject every
        // multisample renderbuffer on such a context.
        Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
                                                GLenum imageFormat) {
            const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
                                   imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
            return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
        }

        Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
                                                    GLuint texture, TextureInternalFormat format) {
            GLuint framebuffer = 0;
            GLint prevFramebuffer = 0;
            if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glCheckFramebufferStatus ||
                !gl.glDeleteFramebuffers) {
                return false;
            }

            gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
            gl.glGenFramebuffers(1, &framebuffer);
            gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

            const GLenum attachment = GetFramebufferAttachment(format);
            switch (target) {
            case TextureTarget::Texture2D:
                gl.glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, texture, 0);
                break;
            case TextureTarget::TextureCubeMap:
                gl.glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_CUBE_MAP_POSITIVE_X, texture, 0);
                break;
            case TextureTarget::Texture3D:
            case TextureTarget::Texture2DArray:
            case TextureTarget::TextureCubeMapArray:
            case TextureTarget::Texture2DMultisampleArray:
                if (!gl.glFramebufferTextureLayer) {
                    gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
                    gl.glDeleteFramebuffers(1, &framebuffer);
                    return false;
                }
                gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, attachment, texture, 0, 0);
                break;
            case TextureTarget::Texture2DMultisample:
                gl.glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D_MULTISAMPLE, texture, 0);
                break;
            default:
                gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
                gl.glDeleteFramebuffers(1, &framebuffer);
                return false;
            }

            const Bool complete = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
            gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
            gl.glDeleteFramebuffers(1, &framebuffer);
            return complete;
        }

        // Whether the driver renders to a framebuffer whose depth and stencil come from
        // two different renderbuffers. GL only requires support when both attachments are
        // the same image, and ES drivers commonly answer GL_FRAMEBUFFER_UNSUPPORTED here;
        // reporting COMPLETE from the frontend and then rendering into a framebuffer the
        // driver refuses leaves the results silently empty.
        Bool ProbeDistinctDepthStencilAttachments(const MG_External::GLESFunctionsTable& gl) {
            if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
                !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers ||
                !gl.glBindRenderbuffer || !gl.glRenderbufferStorage || !gl.glDeleteRenderbuffers) {
                return true;
            }

            GLint prevFramebuffer = 0, prevRenderbuffer = 0;
            gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
            gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);

            GLuint framebuffer = 0;
            GLuint renderbuffers[2] = {0, 0};
            gl.glGenFramebuffers(1, &framebuffer);
            gl.glGenRenderbuffers(2, renderbuffers);
            gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);
            gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4, 4);
            gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]);
            gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, 4, 4);
            gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
            gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[0]);
            gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[1]);
            const Bool supported = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;

            gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
            gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
            gl.glDeleteFramebuffers(1, &framebuffer);
            gl.glDeleteRenderbuffers(2, renderbuffers);
            return supported;
        }

        Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLuint renderbuffer,
                                                         TextureInternalFormat format) {
            GLuint framebuffer = 0;
            GLint prevFramebuffer = 0;
            if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
                !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers) {
                return false;
            }

            gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
            gl.glGenFramebuffers(1, &framebuffer);
            gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
            gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GetFramebufferAttachment(format), GL_RENDERBUFFER,
                                         renderbuffer);
            const Bool complete = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
            gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
            gl.glDeleteFramebuffers(1, &framebuffer);
            return complete;
        }

        // `samples` only reaches the multisample targets; every other target ignores it. The
        // descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than
        // repeating the gen/bind/completeness/delete dance.
        Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
                          GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
                          Bool* outRenderable, Int samples = 1) {
            if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
                return false;
            }

            const GLenum glTarget = MG_Util::ConvertTextureTargetToGLEnum(target);
            const GLenum bindingQuery = GetTextureBindingQuery(target);
            if (glTarget == GL_UNKNOWN_MGL || bindingQuery == GL_UNKNOWN_MGL) {
                return false;
            }

            GLint previousBinding = 0;
            gl.glGetIntegerv(bindingQuery, &previousBinding);
            GLuint texture = 0;
            gl.glGenTextures(1, &texture);
            gl.glBindTexture(glTarget, texture);
            ClearGLErrors(gl);

            const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
            if (isMultisample) {
                const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
                if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
                    gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
                } else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
                    gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
                } else {
                    gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
                    gl.glDeleteTextures(1, &texture);
                    return false;
                }
            } else {
                if (gl.glTexParameteri) {
                    gl.glTexParameteri(glTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
                    gl.glTexParameteri(glTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
                }
                switch (target) {
                case TextureTarget::Texture2D:
                    gl.glTexImage2D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 0, imageFormat, imageType,
                                    nullptr);
                    break;
                case TextureTarget::TextureCubeMap:
                    for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; ++face) {
                        gl.glTexImage2D(face, 0, static_cast<GLint>(internalFormat), 2, 2, 0, imageFormat, imageType,
                                        nullptr);
                    }
                    break;
                case TextureTarget::Texture3D:
                    gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 2, 0, imageFormat, imageType,
                                    nullptr);
                    break;
                case TextureTarget::Texture2DArray:
                    gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 1, 0, imageFormat, imageType,
                                    nullptr);
                    break;
                case TextureTarget::TextureCubeMapArray:
                    gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 6, 0, imageFormat, imageType,
                                    nullptr);
                    break;
                default:
                    break;
                }
            }

            const Bool created = CheckNoGLError(gl);
            Bool renderable = false;
            if (created) {
                renderable = ProbeFramebufferCompletenessForTexture(gl, target, texture, logicalFormat);
            }
            if (outRenderable) {
                *outRenderable = renderable;
            }
            gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
            gl.glDeleteTextures(1, &texture);
            ClearGLErrors(gl);
            return created;
        }

        Bool ProbeRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat,
                               TextureInternalFormat logicalFormat, Bool multisample, Int samples) {
            if (!gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glDeleteRenderbuffers) {
                return false;
            }

            GLint prevRenderbuffer = 0;
            gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
            GLuint renderbuffer = 0;
            gl.glGenRenderbuffers(1, &renderbuffer);
            gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
            ClearGLErrors(gl);
            if (multisample) {
                if (!gl.glRenderbufferStorageMultisample) {
                    gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
                    gl.glDeleteRenderbuffers(1, &renderbuffer);
                    return false;
                }
                gl.glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalFormat, 1, 1);
            } else {
                gl.glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, 1, 1);
            }
            const Bool created = CheckNoGLError(gl);
            const Bool complete =
                created && ProbeFramebufferCompletenessForRenderbuffer(gl, renderbuffer, logicalFormat);
            gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
            gl.glDeleteRenderbuffers(1, &renderbuffer);
            ClearGLErrors(gl);
            return complete;
        }

        Vector<Int> ProbeRenderbufferSampleCounts(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat,
                                                  TextureInternalFormat logicalFormat, Int maxSamples) {
            Vector<Int> sampleCounts;
            for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
                if (ProbeRenderbuffer(gl, internalFormat, logicalFormat, true, samples)) {
                    sampleCounts.push_back(samples);
                }
            }
            sampleCounts.push_back(1);
            return sampleCounts;
        }

        // The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a
        // hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum
        // for every format on the multisample targets even where glTexImage2DMultisample happily
        // accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the
        // two answers cannot both be right. Completeness is required at every count, exactly as
        // the renderbuffer walk requires it; the caller only reaches here once the one-sample
        // probe has already succeeded, so 1 terminates the list without being re-probed.
        Vector<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
                                             GLenum internalFormat, GLenum imageFormat, GLenum imageType,
                                             TextureInternalFormat logicalFormat, Int maxSamples) {
            Vector<Int> sampleCounts;
            for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
                Bool renderable = false;
                const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat,
                                                  &renderable, samples);
                if (created && renderable) {
                    sampleCounts.push_back(samples);
                }
            }
            sampleCounts.push_back(1);
            return sampleCounts;
        }

        void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
                                            const MG_External::GLESCapabilities& capabilities,
                                            FormatCapabilityCache& cache) {
            cache.Clear();
            const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
                GetForcedPixelFormatNormalizeOptions(capabilities);
            const Flags<PixelFormatNormalizeOptionBit> driverOptions =
                GetDriverPixelFormatNormalizeOptions(capabilities);

            for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
                const auto logicalFormat = static_cast<TextureInternalFormat>(formatIndex);
                GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(logicalFormat);
                if (requestedInternalFormat == GL_UNKNOWN_MGL) {
                    continue;
                }

                const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
                GLESProbeFormatInfo outerFallbackInfo;
                const Bool outerHasForcedFallback =
                    BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
                if (!outerHasForcedFallback) {
                    BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
                }

                for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
                    const auto target = static_cast<TextureTarget>(targetIndex);
                    // Colour-attachable targets need a colour-renderable fallback; the ordinary
                    // fallback for a three-channel format is another three-channel one, which ES
                    // accepts as a texture but never as an attachment. Recompute the fallback per
                    // target so those formats get widened where the target demands it.
                    const Flags<PixelFormatNormalizeOptionBit> renderTargetOptions =
                        TextureImpl::GetRenderTargetNormalizeOptions(capabilities, targetIndex);
                    // Multisample storage has no three-channel form on ES at all, so its widening
                    // is unconditional and skips the native probe (which cannot succeed). Every
                    // other target keeps the widening on the DRIVER branch, behind the native
                    // probe: `shouldProbeFallback = !nativeCreated || !nativeRenderable` below is
                    // what makes the substitution conditional on the driver actually refusing, so
                    // a driver that does render to a three-channel image keeps allocating it byte
                    // for byte. That is a per-format runtime answer, NOT a desktop-vs-device
                    // split: llvmpipe renders to GL_RGB16F but refuses GL_RGB8_SNORM, GL_SRGB8,
                    // GL_RGB32F and the RGB integer formats, so the CI driver widens those eight
                    // too. Re-run the retrace fixtures and the glcts suites on any change here.
                    const Bool widenUnconditionally = IsGLESProbeMultisampleTarget(target);
                    GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
                    Bool hasForcedFallback = outerHasForcedFallback;
                    if (renderTargetOptions) {
                        // Folded into the forced options only when a forced fallback already
                        // applies, so the render-target bits never *create* one: ANGLE's forced
                        // GL_RGB8_SNORM -> GL_RGB16F is still three-channel and still needs
                        // widening, but a non-ANGLE driver must not lose its native probe.
                        const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
                            (outerHasForcedFallback || widenUnconditionally) ? forcedOptions | renderTargetOptions
                                                                             : forcedOptions;
                        hasForcedFallback =
                            BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedProbeOptions, true,
                                                         fallbackInfo);
                        if (!hasForcedFallback) {
                            BuildFallbackProbeFormatInfo(requestedInternalFormat,
                                                         driverOptions | renderTargetOptions, false, fallbackInfo);
                        }
                        // HONEST STATUS OF THE FORCED PATH. A forced fallback is only ever built
                        // for ANGLE (GetForcedPixelFormatNormalizeOptions returns nothing for any
                        // other renderer), and it SKIPS the native probe entirely - the widened
                        // format is asserted rather than measured on this device. That assertion
                        // is validated on exactly one configuration, the android-angle retrace
                        // golden; it is NOT covered by the headless llvmpipe suites, which take
                        // the driver branch below and prove nothing about ANGLE's answers. So log
                        // the choice at INFO rather than the usual MGLOG_D caveat: on any other
                        // ANGLE device the device report is the only evidence there is of which
                        // storage format the image really got. Once per format on the ordinary 2D
                        // target - repeating it for all ten targets would bury the report.
                        if (hasForcedFallback && target == TextureTarget::Texture2D &&
                            (MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(
                                 requestedInternalFormat, renderTargetOptions) &
                             PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)) {
                            MGLOG_I("Three-channel widening (FORCED path, no native probe): %s stored as %s. "
                                    "Reason: %s. Device-validated on the android-angle golden only.",
                                    MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
                                    ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str(),
                                    fallbackInfo.Reason.c_str());
                        }
                    }

                    // 1D, 1D-array and rectangle textures live on an ES target (see
                    // TextureImpl::MapToBackendTextureTarget), so they have to be probed there too -
                    // probing the desktop-only target itself always failed, which left those slots
                    // of the cache empty and stopped any fallback format from being selected for
                    // them (a GL_DEPTH_COMPONENT32 1D texture then got no storage at all).
                    const TextureTarget probeTarget = TextureImpl::MapToBackendTextureTarget(target);

                    Bool shouldProbeFallback = hasForcedFallback;
                    if (!hasForcedFallback) {
                        Bool nativeRenderable = false;
                        const Bool nativeCreated =
                            ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
                                         nativeInfo.ImageType, logicalFormat, &nativeRenderable);
                        if (nativeCreated) {
                            AddFullFormatCaps(cache, targetIndex, formatIndex,
                                              BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
                            if (IsGLESProbeMultisampleTarget(target)) {
                                const Int maxSamples =
                                    GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
                                cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
                                    gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
                                    nativeInfo.ImageType, logicalFormat, maxSamples);
                            }
                        }
                        shouldProbeFallback = !nativeCreated || !nativeRenderable;
                    }

                    if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
                        Bool fallbackRenderable = false;
                        const Bool fallbackCreated =
                            ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
                                         fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
                        if (fallbackCreated) {
                            if (AddCaveatFormatCaps(
                                    cache, targetIndex, formatIndex,
                                    BuildTextureCapsFromProbe(logicalFormat, target, fallbackRenderable))) {
                                LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
                            }
                            if (IsGLESProbeMultisampleTarget(target)) {
                                const Int maxSamples =
                                    GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
                                cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
                                    gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
                                    fallbackInfo.ImageType, logicalFormat, maxSamples);
                            }
                        }
                    }
                }

                const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
                // A renderbuffer exists only to be attached, so it needs the same three-channel
                // widening the colour-attachable texture targets get - and on the same terms: the
                // native storage is probed first, so a driver that renders to it keeps it.
                const Flags<PixelFormatNormalizeOptionBit> renderbufferOptions =
                    TextureImpl::GetRenderTargetNormalizeOptions(capabilities, renderbufferTargetIndex);
                GLESProbeFormatInfo renderbufferFallbackInfo = outerFallbackInfo;
                Bool renderbufferHasForcedFallback = outerHasForcedFallback;
                if (renderbufferOptions) {
                    const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
                        outerHasForcedFallback ? forcedOptions | renderbufferOptions : forcedOptions;
                    renderbufferHasForcedFallback = BuildFallbackProbeFormatInfo(
                        requestedInternalFormat, forcedProbeOptions, true, renderbufferFallbackInfo);
                    if (!renderbufferHasForcedFallback) {
                        BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | renderbufferOptions,
                                                     false, renderbufferFallbackInfo);
                    }
                }

                Bool shouldProbeFallbackRenderbuffer = renderbufferHasForcedFallback;
                if (!renderbufferHasForcedFallback) {
                    const Bool nativeRenderbufferComplete =
                        ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
                    if (nativeRenderbufferComplete) {
                        AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
                                          GetRenderbufferFeatureCaps(logicalFormat));
                        const Int maxSamples =
                            GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
                        cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
                            ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
                    } else {
                        shouldProbeFallbackRenderbuffer = true;
                    }
                }
                if (shouldProbeFallbackRenderbuffer && renderbufferFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
                    ProbeRenderbuffer(gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
                    if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
                                            GetRenderbufferFeatureCaps(logicalFormat))) {
                        LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
                    }
                    const Int maxSamples =
                        GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
                    cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
                        gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
                }
            }
        }

        // The advertised renderer info must be mutable after its first use:
        // E_GL_ARB_timer_query can only be decided once the ES capabilities
        // are known, long after the list is first read (see
        // UpdateAdvertisedTimerQueryExtension below).
        RendererInfo& MutableRendererInfo() {
            static RendererInfo rendererInfo = {
                .RendererName = "Espryt",            // Renderer Name
                .BackendName = "Direct (OpenGL ES)", // Backend Name
                .ExtraVendor = Nullopt,              // Extra vendor
                .RendererGLInfo =
                    {
                        .TargetGLVersion = {4, 6, 0},   // GL target version
                        .TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
                        // Baseline advertisement (no runtime capabilities yet); reconciled once
                        // the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
                        .Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false),
                        .IsCompatibilityProfile = false // Is Compatibility Profile
                    },
                .StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
            };
            return rendererInfo;
        }

        // GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
        // string via glGetStringi plus non-null glQueryCounter and
        // glGetQueryObject(u)i64v entries). GetRendererInfo is first invoked
        // from LogBackendInfo() during MG_Backend::Init, BEFORE any ES
        // context or capabilities exist, so the advertisement cannot be baked
        // into the static initializer above; it is reconciled here at the end
        // of InitCapabilities instead (mirroring DirectVulkan's mutable
        // m_rendererInfo + UpdateAdvertisedExtensions). InitCapabilities
        // completes inside the first MakeEGLCurrent on a context, so an app
        // thread can only observe the extension string after the
        // advertisement for its context has settled; rebuilding the whole
        // list keeps the re-run after a context recreation idempotent.
        void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
            MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
                AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
                capabilities.SupportsDrawIndirect,
                capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
                capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
        }
    } // namespace

    void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl,
                                    const MG_External::GLESCapabilities& capabilities, FormatCapabilityCache& cache) {
        PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
    }

    Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
                                     Int samples) {
        if (samples <= 1) {
            return samples;
        }

        Int maxSamples = 0;
        const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
        if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
            formatIndex < kFormatCapabilityFormatCount) {
            // Descending, so the head is the largest count this device actually allocated.
            const Vector<Int>& probedCounts =
                pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
            if (!probedCounts.empty()) {
                maxSamples = probedCounts.front();
            }
        }
        if (maxSamples <= 0) {
            maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
        }
        return std::min(samples, std::max(maxSamples, 1));
    }

    BackendObject_DirectGLES::~BackendObject_DirectGLES() {
        DestroyEGLContext();
    }

    Bool BackendObject_DirectGLES::InitWindowSurface() {
        // Only use EGL for now
        auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
        if (!DirectGLES::InitWindowSurface(nativeWindow)) {
            MGLOG_E("Failed to initialize window surface for DirectGLES backend");
            return false;
        }
        return true;
    }

    void BackendObject_DirectGLES::Initialize() {
        MG_Util::BackendLoader::AcquireEGLFunctions(m_EGLFunctions);
        MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress);
        DirectGLES::SetEGLFuncsTable(m_EGLFunctions);
        DirectGLES::SetGLESFuncsTable(m_GLESFunctions);
        BufferImpl::RegisterBufferBackendOps();
        m_initialized = true;
    }

    Bool BackendObject_DirectGLES::InitCapabilities() {
        if (!m_initialized) {
            MGLOG_E("DirectGLES backend not initialized");
            return false;
        }

        if (!MG_Util::BackendLoader::FillInGLESCapabilities(m_GLESCapabilities, m_GLESFunctions)) {
            MGLOG_E("Failed to fill in GLES capabilities for DirectGLES backend");
            return false;
        }
        DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
        // Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
        // reconcile every runtime-gated advertisement (see the comment on
        // UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
        // built).
        UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
        UpdateDynamicBackendParameters();
        PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
        PrintFormatCapabilities(GetFormatCapabilities());
        return true;
    }

    Bool BackendObject_DirectGLES::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
        if (!m_initialized) {
            MGLOG_E("DirectGLES backend not initialized");
            return false;
        }
        return BackendObject::InitializeEGLDisplay(dpy, major, minor);
    }

    Bool BackendObject_DirectGLES::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
        const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
        if (!m_initialized) {
            MGLOG_E("DirectGLES backend not initialized");
            return false;
        }

        if ((handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
             handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32) ||
            !handle.Handle) {
            MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
            return false;
        }

        const Bool sameHandle = m_eglSurfaceInitialized && m_eglSurface == surface &&
                                m_eglSurfaceKind == SurfaceKind::Window && m_windowHandle.Backend == handle.Backend &&
                                m_windowHandle.Handle == handle.Handle;
        if (sameHandle) {
            return true;
        }

        if (m_eglSurfaceInitialized) {
            DestroyEGLContext();
            ResetEGLRuntimeState();
        }

        return BackendObject::CreateEGLWindowSurface(surface, handle);
    }

    Bool BackendObject_DirectGLES::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
        const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
        if (!m_initialized) {
            MGLOG_E("DirectGLES backend not initialized");
            return false;
        }

        if (m_eglSurfaceInitialized && m_eglSurface == surface && m_eglSurfaceKind == SurfaceKind::Pbuffer) {
            return true;
        }

        if (m_eglSurfaceInitialized) {
            DestroyEGLContext();
            ResetEGLRuntimeState();
        }

        return BackendObject::CreateEGLPbufferSurface(surface, width, height);
    }

    Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
        return DirectGLES::InitPbufferSurface(width, height);
    }

    Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
        const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
        if (IsReleaseCurrentRequest(dpy, draw, read, ctx)) {
            if (!DirectGLES::ReleaseCurrent()) {
                return false;
            }
            return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
        }

        if (!m_initialized) {
            MGLOG_E("DirectGLES backend not initialized");
            return false;
        }
        if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
            MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
            return false;
        }
        if (!m_eglSurfaceInitialized) {
            MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
            return false;
        }
        if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
            MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
            return false;
        }

        if (!DirectGLES::MakeCurrent()) {
            return false;
        }

        if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) {
            (void)DirectGLES::ReleaseCurrent();
            return false;
        }
        return true;
    }

    Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
        return BackendObject::SwapEGLBuffers(dpy, draw);
    }

    void BackendObject_DirectGLES::ReleaseEGLSurface(EGLSurface surface) {
        const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
        BackendObject::ReleaseEGLSurface(surface);
    }

    void BackendObject_DirectGLES::ReleaseEGLResources() {
        const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
        DestroyEGLContext();
        BackendObject::ReleaseEGLResources();
    }

    void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
        (void)surface;
        DestroyEGLContext();
    }

    const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
        return MutableRendererInfo();
    }

    String BackendObject_DirectGLES::GetBackendAPIVersionString() const {
        if (!m_initialized) {
            return "<uninitialized DirectGLES backend>";
        }
        return FormatBackendAPIVersionString(m_GLESCapabilities.GLESRendererString,
                                             m_GLESCapabilities.GLESVersion.Major,
                                             m_GLESCapabilities.GLESVersion.Minor);
    }

    const RendererInfo& GetRendererIdentity() {
        return MutableRendererInfo();
    }

    Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
                                                  Bool drawIndirectSupported,
                                                  Bool nonZeroIndirectBaseInstanceSupported,
                                                  Bool textureViewSupported, Bool cubeMapArraySupported) {
        Vector<GLExtension> extensions = {
            // The version tokens have to reach the version the backend actually claims:
            // TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
            // application feature-detecting off these tokens the opposite of what
            // GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
            V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
            V_OpenGL44, V_OpenGL45, V_OpenGL46,
            E_GL_ARB_draw_buffers_blend,
            E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
            E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
            E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
            E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
            E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
            E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
            E_GL_ARB_vertex_attrib_binding,
            // Both are core from GL 3.2/3.3 on and implemented here for
            // every advertised version, but an app targeting 3.0/3.1
            // only reaches them through the extension string - the CTS
            // picks a whole different shader for draw_buffers without
            // explicit_attrib_location. DirectVulkan advertises both.
            E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
            // Core since GL 3.1 and implemented for every version advertised here. The string
            // matters because applications gate the ENTRY POINTS on it rather than on the
            // version: a caller that finds the extension missing never resolves
            // glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
            // blocks anyway calls through a null pointer.
            E_GL_ARB_uniform_buffer_object,
            // Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
            // so on a 4.0 context the string is the only way to reach it. The host ES driver
            // has had the same texture parameter since ES 3.1, which every device MobileGL
            // runs on provides.
            E_GL_ARB_stencil_texturing,
            // Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
            // glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
            // glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
            // was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
            // NotSupported on a feature that works.
            E_GL_ARB_draw_elements_base_vertex,
            // The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
            // glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
            // backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v
            // answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
            // ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
            // list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
            // then calls through null if it uses fences anyway. Nothing in the CTS gates on this
            // string, so it is advertised on the strength of the implementation, not a test unlock.
            E_GL_ARB_sync,
            // Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
            // GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers
            // re-issues the counter buffer as an SSBO binding in the range reserved at the top of
            // the ES driver's shader-storage points, so a counter dispatch reads and writes the
            // buffer the application bound. DirectVulkan reaches the same place through its own
            // descriptor resolution, so the string is symmetric.
            E_GL_ARB_shader_atomic_counters,
            // glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
            // (Better Clouds' GLCompat among them) accept the extension string as an
            // ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
            // available, so withholding it makes MobileGL look less capable than it is.
            E_GL_ARB_instanced_arrays,
            // The whole of KHR_debug lives in GLImpl - the message log, the group stack and the
            // object-label table are MobileGL's own state, not the host driver's - so it is as
            // available here as it is on DirectVulkan, which has advertised it all along.
            E_GL_KHR_debug,
            // Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
            // existed, and that was simply never named. None of these unlocks a single CTS case -
            // the conformance suite reaches all of them through the version - so they are
            // advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
            // string set, and an application that gates its ENTRY POINTS on the string rather than
            // on the version never resolves them and then calls through null. Each is backed by
            // the entry points named beside it.
            //
            // glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
            E_GL_ARB_vertex_array_object,
            // The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
            // integer-valued Iiv/Iuiv forms.
            E_GL_ARB_sampler_objects,
            // glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
            // maps are already built on top of.
            E_GL_ARB_map_buffer_range,
            // glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
            E_GL_ARB_copy_buffer,
            // glCopyImageSubData, wired to a real backend hook on both backends.
            E_GL_ARB_copy_image,
            // GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES
            // driver's identical parameters.
            E_GL_ARB_texture_swizzle,
            // GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
            // the eight glVertexAttribP* entry points.
            E_GL_ARB_vertex_type_2_10_10_10_rev,
            // The R/RG internal formats. Named separately from the float ones because an
            // application may check either.
            E_GL_ARB_texture_rg,
            // GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
            E_GL_ARB_depth_buffer_float,
            // The floating-point colour formats. Unlike the rest of this block this string DOES
            // gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
            // core-version fallback, so eight cases per version list were NotSupported on formats
            // the backend has always had.
            E_GL_ARB_texture_float,
            // glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
            // glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
            // 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
            E_GL_ARB_viewport_array,
            // Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
            // extension explicitly permits. It is also the only thing that
            // exposes glProgramParameteri before GL 4.1.
            E_GL_ARB_get_program_binary};
        // Minecraft 26.3 checks this prerequisite before it even considers
        // GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
        // folds the version and pointer checks into SupportsDrawIndirect.
        if (drawIndirectSupported) {
            extensions.push_back(E_GL_ARB_draw_indirect);
        }
        // ARB_base_instance also defines the last word of an indirect command. Direct calls are
        // emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
        // draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
        // that incomplete case.
        if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
            extensions.push_back(E_GL_ARB_base_instance);
        }
        // GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
        // driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
        // are serviced entirely inside the frontend. Whether the device driver advertises
        // the string is irrelevant here (the POST reports it separately, for the day the
        // driver-side link is what gets parallelised).
        //
        // Gated on the async flag deliberately, and this is the whole reason the gate
        // exists. Advertising the string is the one part of asynchronous compilation that a
        // recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
        // the moment they see it - they enqueue whole pipeline batches and poll
        // GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
        // MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
        // change as well as the threading, or the kill switch would only be half a switch.
        if (MG_Util::Async::AsyncShaderCompileEnabled()) {
            extensions.push_back(E_GL_KHR_parallel_shader_compile);
        }
        // GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
        // shader compiles and runs already - it is narrowed to 32 bits before the module
        // reaches this backend - so an application that simply uses doubles needs nothing
        // advertised. What the extension additionally promises is 64-bit PRECISION, which no
        // mobile GPU has and the narrowing cannot fake, so advertising it by default would
        // make an application that checks the string take a path MobileGL cannot honour.
        if (MG_Config::Features.AdvertiseFp64) {
            extensions.push_back(E_GL_ARB_gpu_shader_fp64);
        }
        // Only advertised when the device driver actually has usable timer queries
        // (GL_EXT_disjoint_timer_query plus its entry points) and the
        // MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
        if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
            extensions.push_back(E_GL_ARB_timer_query);
        }
        // Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
        // EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
        // and a samplerCubeArray shader does not even compile, which is exactly what the POST
        // reports. So the string follows the host capability rather than the version.
        //
        // Named for the application's benefit rather than the suite's: measured on Adreno 830,
        // KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
        // this unlocks no conformance case. It is advertised because the feature is real and
        // because an application that feature-detects cube map arrays off the string (rather than
        // off the 4.0 version) would otherwise decline a path this backend serves.
        if (cubeMapArraySupported) {
            extensions.push_back(E_GL_ARB_texture_cube_map_array);
        }
        // Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
        // texture views at any version and no honest emulation exists: a view is a SECOND NAME
        // over the SAME storage, so that writes through either are visible through the other and
        // the two carry independent per-texture parameters at the same time - which is exactly
        // what applications use it for (Better Clouds samples one D24S8 through its own name with
        // DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
        // a single shading pass). A copy-based fallback satisfies neither half, and fails
        // silently; withholding the string and answering glTextureView with INVALID_OPERATION is
        // the only behaviour that cannot be mistaken for success.
        //
        // The host extension is necessary and NOT sufficient, which is why this second gate
        // exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of
        // KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and
        // view_sampling Fail and view_classes crashes, while only the two pure-API cases
        // (errors, gettexparameter - neither of which touches the host view) pass. The cause is
        // known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the
        // VIEW's ES internalformat independently of the storage it aliases, so whenever the two
        // land on different renderability carriers the host rejects the pair, the error is
        // swallowed, and the view is left as a storage-less name that samples as zeros.
        // DirectVulkan builds the view as a second VkImageView over one VkImage and has no such
        // seam - it passes 5 of the 7 cases on the same device - so the string stays there.
        //
        // Until that reconciliation exists, advertising here would be the same lie the comment
        // above refuses to tell, just with an extra prerequisite met. Set
        // MOBILEGL_ENABLE_GLES_TEXTURE_VIEW=1 to re-enable it for that work.
        if (textureViewSupported && MG_Config::Features.EnableGlesTextureView) {
            extensions.push_back(E_GL_ARB_texture_view);
        }
        // Only advertised when the host ES driver actually filters anisotropically: the sampler
        // state is accepted regardless, but forwarding it would be a no-op without the extension,
        // and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
        // get plain trilinear.
        if (anisotropicFilteringSupported) {
            extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
            extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
        }
        return extensions;
    }

    String FormatBackendAPIVersionString(const String& glesRendererString, Int glesMajor, Int glesMinor) {
        // Format:
        // <OpenGL ES Renderer>, OpenGL ES <OpenGL ES Version>
        return std::format("{}, OpenGL ES {}.{}", glesRendererString, glesMajor, glesMinor);
    }

    BackendType BackendObject_DirectGLES::GetBackendType() const {
        return BackendType::DirectGLES;
    }

    const GlobalBackendFunctionsTable& BackendObject_DirectGLES::GetBackendFunctions() const {
        static GlobalBackendFunctionsTable funcsTable;
        static Bool funcsTableInitialized = false;
        if (!funcsTableInitialized) {
            funcsTable.Present = DirectGLES::Present;
            funcsTable.SetSwapInterval = DirectGLES::SetSwapInterval;
            funcsTable.GL.DrawArrays = DrawArrays;
            funcsTable.GL.DrawElements = DrawElements;
            funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
            funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
            funcsTable.GL.MultiDrawElements = MultiDrawElements;
            funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
            funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
            funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
            funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
            funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
            funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
            funcsTable.GL.DrawRangeElements = DrawRangeElements;
            funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
            funcsTable.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex;
            funcsTable.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance;
            funcsTable.GL.DrawElementsInstanced = DrawElementsInstanced;
            funcsTable.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance;
            funcsTable.GL.DrawArraysInstanced = DrawArraysInstanced;
            funcsTable.GL.DrawElementsIndirect = DrawElementsIndirect;
            funcsTable.GL.DrawArraysIndirect = DrawArraysIndirect;
            funcsTable.GL.DispatchCompute = DispatchCompute;
            funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
            funcsTable.GL.MemoryBarrier = MemoryBarrier;
            funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
            funcsTable.GL.BindImageTexture = BindImageTexture;
            funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
            funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
            funcsTable.GL.GetProgramiv = GetProgramiv;
            funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
            funcsTable.GL.Clear = Clear;
            funcsTable.GL.ClearBufferfi = ClearBufferfi;
            funcsTable.GL.ClearBufferfv = ClearBufferfv;
            funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
            funcsTable.GL.ClearBufferiv = ClearBufferiv;
            funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
            funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
            funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
            funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
            funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
            funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
            funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
            funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
            funcsTable.GL.CopyImageSubData = CopyImageSubData;
            funcsTable.GL.GenerateMipmap = GenerateMipmap;
            funcsTable.GL.ReadPixels = ReadPixels;
            funcsTable.GL.GetTexImage = GetTexImage;
            funcsTable.GL.FenceSync = FenceSync;
            funcsTable.GL.ClientWaitSync = ClientWaitSync;
            funcsTable.GL.WaitSync = WaitSync;
            funcsTable.GL.DeleteSync = DeleteSync;
            funcsTable.GL.GetSyncStatus = GetSyncStatus;
            // Optional timer-query group: left null (the frontend then falls
            // back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
            // themselves additionally degrade to null handles / zero results
            // when GL_EXT_disjoint_timer_query or its entry points are
            // missing, or when the calling thread does not own the ES
            // context.
            if (!MG_Config::Features.DisableTimerQuery) {
                // AreTimerQueriesSupported is a pure capability read (no
                // current ES context required, false until the caps are
                // filled in), which is exactly the dynamic support check
                // the frontend wants from IsTimerQuerySupported.
                funcsTable.GL.IsTimerQuerySupported = AreTimerQueriesSupported;
                funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
                funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
                funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
                funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
            }
            // Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
            // and share the handle-based result/delete entries, which must exist even
            // when the timer-query group above is disabled.
            funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
            funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
            // Real driver primitive counters: the frontend's CPU accounting cannot see a
            // geometry shader's amplification.
            funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
            funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
            // ...but where it CAN see the whole capture - no geometry stage - the frontend's
            // own count is the desktop-exact one and the ES driver's is only as good as the
            // vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that
            // follows a large render pass). The query above stays installed: it is still what
            // answers an amplifying span, and PRIMITIVES_GENERATED always.
            funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
            funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
            funcsTable.GL.GetQueryResult64 = GetQueryResult64;
            funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
            // Transform feedback is captured by the real ES driver rather than
            // reconstructed from the draw recording, so the frontend has to hand the
            // span boundaries over.
            funcsTable.GL.PatchParameteri = DirectGLES::PatchParameteri;
            funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback;
            funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback;
            funcsTable.GL.PauseTransformFeedback = XfbImpl::PauseTransformFeedback;
            funcsTable.GL.ResumeTransformFeedback = XfbImpl::ResumeTransformFeedback;
            funcsTable.GL.BindTransformFeedback = XfbImpl::BindTransformFeedback;
            funcsTable.GL.DeleteTransformFeedback = XfbImpl::DeleteTransformFeedback;
            funcsTableInitialized = true;
        }
        return funcsTable;
    }

    const DynamicBackendParameters& BackendObject_DirectGLES::GetDynamicParameters() const {
        return m_dynamicParameters;
    }

    void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities) {
        m_GLESCapabilities = capabilities;
        UpdateDynamicBackendParameters();
    }

    void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
        m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
        m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
            m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
        m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
        m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
        m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
        m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin;
        m_dynamicParameters.SmoothLineWidthRangeMax = m_GLESCapabilities.SmoothLineWidthRangeMax;
        m_dynamicParameters.SmoothLineWidthGranularity = m_GLESCapabilities.SmoothLineWidthGranularity;
        m_dynamicParameters.PointSizeRangeMin = m_GLESCapabilities.PointSizeRangeMin;
        m_dynamicParameters.PointSizeRangeMax = m_GLESCapabilities.PointSizeRangeMax;
        m_dynamicParameters.PointSizeGranularity = m_GLESCapabilities.PointSizeGranularity;
        m_dynamicParameters.Max3DTextureSize = m_GLESCapabilities.Max3DTextureSize;
        m_dynamicParameters.MaxArrayTextureLayers = m_GLESCapabilities.MaxArrayTextureLayers;
        m_dynamicParameters.MaxCubeMapTextureSize = m_GLESCapabilities.MaxCubeMapTextureSize;
        m_dynamicParameters.MaxFramebufferWidth = m_GLESCapabilities.MaxFramebufferWidth;
        m_dynamicParameters.MaxFramebufferHeight = m_GLESCapabilities.MaxFramebufferHeight;
        m_dynamicParameters.MaxFramebufferLayers = m_GLESCapabilities.MaxFramebufferLayers;
        m_dynamicParameters.MaxRenderbufferSize = m_GLESCapabilities.MaxRenderbufferSize;
        m_dynamicParameters.MaxTextureSize = m_GLESCapabilities.MaxTextureSize;
        m_dynamicParameters.MaxColorTextureSamples = m_GLESCapabilities.MaxColorTextureSamples;
        m_dynamicParameters.MaxDepthTextureSamples = m_GLESCapabilities.MaxDepthTextureSamples;
        m_dynamicParameters.MaxFramebufferSamples = m_GLESCapabilities.MaxFramebufferSamples;
        m_dynamicParameters.MaxIntegerSamples = m_GLESCapabilities.MaxIntegerSamples;
        m_dynamicParameters.MaxSamples = m_GLESCapabilities.MaxSamples;
        m_dynamicParameters.MaxSampleMaskWords = m_GLESCapabilities.MaxSampleMaskWords;
        m_dynamicParameters.MaxPatchVertices = m_GLESCapabilities.MaxPatchVertices;
        m_dynamicParameters.MaxTessGenLevel = m_GLESCapabilities.MaxTessGenLevel;
        m_dynamicParameters.MinProgramTextureGatherOffset = m_GLESCapabilities.MinProgramTextureGatherOffset;
        m_dynamicParameters.MaxProgramTextureGatherOffset = m_GLESCapabilities.MaxProgramTextureGatherOffset;
        // Clamp the advertised sampler limits the same way the DirectVulkan backend does: per-stage
        // GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g.
        // Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined
        // limit must stay within our texture-unit state array capacity.
        m_dynamicParameters.MaxTextureImageUnits =
            std::min(m_GLESCapabilities.MaxTextureImageUnits,
                     static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS));
        m_dynamicParameters.MaxVertexTextureImageUnits =
            std::min(m_GLESCapabilities.MaxVertexTextureImageUnits,
                     static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS));
        m_dynamicParameters.MaxComputeTextureImageUnits =
            std::min(m_GLESCapabilities.MaxComputeTextureImageUnits,
                     static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS));
        m_dynamicParameters.MaxCombinedTextureImageUnits =
            std::min(m_GLESCapabilities.MaxCombinedTextureImageUnits,
                     static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
        // Never advertise more attributes than the state layer can store: the current-value array and
        // the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
        m_dynamicParameters.MaxVertexAttribs =
            std::min(m_GLESCapabilities.MaxVertexAttribs,
                     static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
        m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
        m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
        // Per-stage storage-block counts, forwarded from the host driver rather than invented.
        // A stage the driver cannot serve reports 0, which is a legal answer everywhere these
        // limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
        // graphics stage except fragment) and is the only answer that lets an application take
        // its own fallback instead of building a program the driver will refuse to link. The
        // stage limit cannot exceed the combined limit or the number of binding points there
        // are to bind buffers to, so clamp to both.
        const auto clampStageStorageBlocks = [this](Int stageLimit) {
            return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
                             std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
        };
        m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
        m_dynamicParameters.MaxVertexShaderStorageBlocks =
            clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
        m_dynamicParameters.MaxTessControlShaderStorageBlocks =
            clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
        m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
            clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
        m_dynamicParameters.MaxGeometryShaderStorageBlocks =
            clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
        m_dynamicParameters.MaxFragmentShaderStorageBlocks =
            clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
        m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
        m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
        // (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
        // This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
        // on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
        // than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
        // which). Reporting 0 instead was considered and rejected: MobileGL advertises an OpenGL
        // 4.x context, where buffer textures are core and the limit has a spec minimum of 65536,
        // so 0 is not a legal answer and applications are not written to survive it. GL offers no
        // way to say "this core feature is missing", so the honesty is carried outside the limit:
        // FillInGLESCapabilities logs the tier, glTexBuffer and the program build each name the
        // missing capability at MGLOG_I, and the driver POST carries a "Buffer textures" row that
        // FAILs on this tier.
        m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
        m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
        m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
        m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
        const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
        m_dynamicParameters.MaxImageUnits =
            std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
        m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0);
        const auto clampStageImageUniforms = [this](Int stageLimit) {
            return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
                             m_dynamicParameters.MaxCombinedImageUniforms});
        };
        m_dynamicParameters.MaxVertexImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
        m_dynamicParameters.MaxGeometryImageUniforms =
            clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
        m_dynamicParameters.MaxFragmentImageUniforms =
            clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
        m_dynamicParameters.MaxComputeImageUniforms =
            clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
        m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
            ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
        // SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
        // attachment's layer passed through, so this backend really does render to the layer it was
        // given - provided the driver resolved the entry point at all.
        // SyncAttachmentObject (Managers.cpp, the glFramebufferTextureLayer branch) routes exactly
        // five upload targets to glFramebufferTextureLayer with the attachment's layer passed
        // through, so this backend really does render to the layer it was given - provided the driver
        // resolved the entry point at all. The cube map array is the one target that also needs
        // ES-level support before it has any storage to attach.
        m_dynamicParameters.PerLayerFramebufferAttachmentTargets = 0;
        if (DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr) {
            using DynParams = MG_Backend::DynamicBackendParameters;
            m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
                DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D) |
                DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture1DArray) |
                DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
                DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
            if (m_GLESCapabilities.SupportsTextureCubeMapArray) {
                m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
                    DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
            }
        }
        // Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version
        // or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a
        // module that still declared Float64 would never reach the driver at all. The demotion is
        // mathematically mandatory here, on every device, forever - which is why this stays false
        // regardless of what the driver underneath happens to support.
        m_dynamicParameters.SupportsShaderFloat64 = false;
        // Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
        // fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
        m_dynamicParameters.SupportsFloat64VertexAttributes = false;
        m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
        m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
        m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
        // The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
        // is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
        // would reject.
        m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
        m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
        m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
        // Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
        // for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
        // devices. That is not a shortfall being hidden: without the extension only viewport 0 is
        // ever rasterized, so no vertex "selects" a viewport index and naming a convention would
        // describe behaviour this backend does not implement.
        m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
        m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
        m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
        m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
        m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
        m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
        m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
        m_dynamicParameters.MinFragmentInterpolationOffset =
            std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) &&
                    m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f
                ? m_GLESCapabilities.MinFragmentInterpolationOffset
                : -0.5f;
        m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
        m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
        if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 &&
            std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) {
            const Float requiredMaxOffset =
                0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
            if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
                m_dynamicParameters.MaxFragmentInterpolationOffset = m_GLESCapabilities.MaxFragmentInterpolationOffset;
                m_dynamicParameters.FragmentInterpolationOffsetBits =
                    m_GLESCapabilities.FragmentInterpolationOffsetBits;
            }
        }
        m_dynamicParameters.SupportsWideLines =
            m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;

        const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
            return std::any_of(needles.begin(), needles.end(),
                               [&](const char* needle) { return haystack.find(needle) != String::npos; });
        };
        const String vendorAndRenderer =
            m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
        if (containsAny(vendorAndRenderer, {"llvmpipe", "SwiftShader", "softpipe"})) {
            // Check software rasterizers first: ANGLE-on-llvmpipe reports both.
            m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
        } else if (containsAny(vendorAndRenderer, {"Qualcomm", "Adreno"})) {
            m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
        } else if (containsAny(vendorAndRenderer, {"Mali", "ARM"})) {
            m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
        } else if (containsAny(vendorAndRenderer, {"NVIDIA"})) {
            m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
        } else if (containsAny(vendorAndRenderer, {"AMD", "Radeon"})) {
            m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
        } else if (containsAny(vendorAndRenderer, {"Intel"})) {
            m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
        } else if (containsAny(vendorAndRenderer, {"Imagination", "PowerVR"})) {
            m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
        } else {
            m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
        }
    }

    const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const {
        return m_GLESFunctions;
    }

    const MG_External::EGLFunctionsTable& BackendObject_DirectGLES::GetEGLFunctions() const {
        return m_EGLFunctions;
    }
} // namespace MobileGL::MG_Backend::DirectGLES
