// MobileGL - MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.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 "GL_Getter.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <Config.h>
#include <MGGitHash.h>
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/EGLState/Core.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Backend/BackendObjects.h>

namespace MobileGL::MG_Impl::GLImpl {
    // Declared rather than #included from GL_RenderState.h on purpose: that header also declares
    // a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum
    // this file's blend-state queries name unqualified.
    GLboolean IsEnabledi(GLenum target, GLuint index);

    namespace {
        enum class IndexedBufferQueryKind {
            Binding,
            Start,
            Size,
        };

        void CopyFloatsToInts(const GLfloat* src, SizeT count, GLint* dst) {
            for (SizeT i = 0; i < count; ++i) {
                dst[i] = static_cast<GLint>(src[i]);
            }
        }

        // Shared with the glslang resource table for the same reason as the atomic-counter
        // limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
        constexpr GLint kFrontendMaxComputeUniformComponents =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
        // Every atomic-counter limit is shared with the glslang resource table
        // (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
        // glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
        // used to be independent tables that disagreed on both the binding count and the buffer
        // size. Never move one of these without the other.
        constexpr GLint kFrontendMaxComputeAtomicCounters =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
        constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
        constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
        constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
        constexpr GLint kFrontendMaxCombinedAtomicCounters =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
        constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
        constexpr GLint kFrontendMaxFragmentAtomicCounters =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
        constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
        constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
        constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
        constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
        constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
        // Zero counters means zero buffers to hold them. These have to be ANSWERED rather than
        // left to the default INVALID_ENUM: a well-behaved application queries the limit exactly
        // to find out that the stage cannot do this, and an error instead both leaves its output
        // untouched (so it reads uninitialised memory and may conclude the opposite) and leaves a
        // GL error pending that surfaces at whatever unrelated call checks next.
        constexpr GLint kFrontendMaxGeometryAtomicCounterBuffers = 0;
        constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
        constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
        constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
        // GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
        // at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
        // the getter, the indexed queries and glBindBufferBase all share one ceiling.
        constexpr GLint kFrontendMaxAtomicCounterBufferSize =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
        // KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
        // limits they advertise still have to be legal.
        constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
        constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
        // The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second
        // literal: they used to be independent (4096 components against 128 vectors, 64 varying
        // components against 8 varying vectors) and could not both be describing the same
        // capacity. Both are shared with BuildTBuiltInResource through Types.h, because
        // gl_MaxVertexUniformVectors and gl_MaxVaryingVectors expand from the same numbers.
        constexpr GLint kFrontendMaxVertexUniformComponents =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS);
        constexpr GLint kFrontendMaxVertexUniformVectors =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS);
        constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
        constexpr GLint kFrontendMaxVertexOutputComponents = 64;
        constexpr GLint kFrontendMaxFragmentInputComponents = 128;
        constexpr GLint kFrontendMaxFragmentUniformComponents = 4096;
        constexpr GLint kFrontendMaxFragmentUniformVectors = 256;
        constexpr GLint kFrontendMaxFragmentUniformBlocks = 14;
        constexpr GLint kFrontendMaxGeometryInputComponents = 64;
        constexpr GLint kFrontendMaxGeometryOutputComponents = 128;
        constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16;
        constexpr GLint kFrontendMaxGeometryUniformComponents = 1024;
        constexpr GLint kFrontendMaxGeometryUniformBlocks = 14;
        // ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no
        // gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter
        // answer only; 32 is the GL 4.6 core minimum (table 23.57).
        constexpr GLint kFrontendMaxGeometryShaderInvocations = 32;
        constexpr GLint kFrontendMaxTessControlUniformBlocks = 14;
        constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14;
        // The compute stage's share of the combined sum below. Compute's own per-stage answer is
        // backend-derived (GL_MAX_COMPUTE_UNIFORM_BLOCKS reads dynamicParameters), so this is not
        // what that query returns - it is the GL 4.3 core minimum, present here only so the
        // combined total covers all SIX stages.
        constexpr GLint kFrontendMaxComputeUniformBlocksShare = 14;
        // GL 4.6 table 23.64 orders MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >=
        // every per-stage count, and the sum has to run over SIX stages, not three and not five.
        // Three (42) was the original bug. Five (70) replaced it and broke the middle term the
        // other way: compute's per-stage count is backend-derived and clamps at the binding count,
        // so a device reporting descriptor-indexing-scale uniform buffers (Adreno reports
        // maxPerStageDescriptorUniformBuffers = 16777216) advertised 84 compute blocks against a
        // combined 70. Six stages x 14 = 84, which is also exactly the binding-point count and the
        // arithmetic the GL 4.5 minimum of 84 bindings is built from, so the ordering is now tight
        // rather than accidental.
        constexpr GLint kFrontendMaxCombinedUniformBlocks =
            kFrontendMaxVertexUniformBlocks + kFrontendMaxTessControlUniformBlocks +
            kFrontendMaxTessEvaluationUniformBlocks + kFrontendMaxGeometryUniformBlocks +
            kFrontendMaxFragmentUniformBlocks + kFrontendMaxComputeUniformBlocksShare;
        constexpr GLint kFrontendMaxVaryingComponents =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_COMPONENTS);
        constexpr GLint kFrontendMaxVaryingVectors =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_VECTORS);
        constexpr GLint kFrontendMaxProgramTexelOffset = 7;
        constexpr GLint kFrontendMinProgramTexelOffset = -8;
        constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64;
        constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4;
        constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4;
        // ARB_transform_feedback3's vertex-stream count. One is what this implementation can
        // actually emit to; see the GL_MAX_VERTEX_STREAMS case for why it is not four.
        constexpr GLint kFrontendMaxVertexStreams = 1;
        constexpr GLint kFrontendMaxGeometryOutputVertices = 256;
        constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024;
        // GL 4.5 core table 23.64 requires 84 indexed uniform binding points, and that is exactly
        // how wide the state layer's array is (BufferState::BufferBindingPointCount) - see the
        // GL_MAX_UNIFORM_BUFFER_BINDINGS case for why the ES driver's own, smaller count is not
        // the ceiling here.
        constexpr GLint kFrontendMinUniformBufferBindings = 84;
        constexpr GLint kFrontendSubpixelBits = 4;
        constexpr GLint kFrontendMaxSamples =
            static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_ADVERTISED_MAX_SAMPLES);
        // ARB_shader_subroutine's two limits. NOTHING IMPLEMENTS SUBROUTINES: there is no
        // glGetSubroutineIndex / glUniformSubroutinesuiv, only the program-interface enum
        // plumbing. These are answered - with the GL 4.5 core minimums - because the conformance
        // suite queries them before it checks for the feature and an INVALID_ENUM both leaves the
        // caller reading its own uninitialised stack slot and strands an error for the next
        // unrelated call to trip over. The extension is deliberately NOT advertised, so the
        // numbers are a table entry, not a capability claim.
        constexpr GLint kFrontendMaxSubroutines = 256;
        constexpr GLint kFrontendMaxSubroutineUniformLocations = 1024;

        // The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
        // pipeline (CaptureCompileEnv floors the same driver answers at them, and
        // BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a
        // shader is allowed to compare the built-in constant against this query.
        constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
            return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0;
        }

        constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
            return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
        }

        // GL 4.6 core table 23.64: components + blocks * (blockSize / 4). The product has to be
        // formed in 64 bits and saturated on the way out - it overflowed a signed 32-bit int on
        // every Vulkan host that reports a large maxUniformBufferRange. A Mali driver answering
        // 0xFFFFFFFF saturates to INT32_MAX in the loader, and 14 * (2147483647 / 4) + 4096 wraps
        // to -1073737742, which the conformance suite read back as a limit "smaller than 58368".
        // Saturating instead of wrapping is also the only honest answer: an implementation that
        // can serve more components than a GLint holds still has to report a GLint.
        GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
                                              GLint maxUniformBlockSizeBytes) {
            const Int64 blocks = std::max<Int64>(static_cast<Int64>(maxUniformBlocks), 0);
            const Int64 componentsPerBlock = std::max<Int64>(static_cast<Int64>(maxUniformBlockSizeBytes), 0) / 4;
            const Int64 total = static_cast<Int64>(maxDefaultUniformComponents) + blocks * componentsPerBlock;
            return static_cast<GLint>(std::min<Int64>(total, std::numeric_limits<GLint>::max()));
        }

        bool TryDecodeIndexedBufferQuery(GLenum pname, BufferTarget& bufferTarget, IndexedBufferQueryKind& queryKind) {
            switch (pname) {
            case GL_UNIFORM_BUFFER_BINDING:
                bufferTarget = BufferTarget::Uniform;
                queryKind = IndexedBufferQueryKind::Binding;
                return true;
            case GL_UNIFORM_BUFFER_START:
                bufferTarget = BufferTarget::Uniform;
                queryKind = IndexedBufferQueryKind::Start;
                return true;
            case GL_UNIFORM_BUFFER_SIZE:
                bufferTarget = BufferTarget::Uniform;
                queryKind = IndexedBufferQueryKind::Size;
                return true;
            case GL_TRANSFORM_FEEDBACK_BUFFER_BINDING:
                bufferTarget = BufferTarget::TransformFeedback;
                queryKind = IndexedBufferQueryKind::Binding;
                return true;
            case GL_TRANSFORM_FEEDBACK_BUFFER_START:
                bufferTarget = BufferTarget::TransformFeedback;
                queryKind = IndexedBufferQueryKind::Start;
                return true;
            case GL_TRANSFORM_FEEDBACK_BUFFER_SIZE:
                bufferTarget = BufferTarget::TransformFeedback;
                queryKind = IndexedBufferQueryKind::Size;
                return true;
            case GL_ATOMIC_COUNTER_BUFFER_BINDING:
                bufferTarget = BufferTarget::AtomicCounter;
                queryKind = IndexedBufferQueryKind::Binding;
                return true;
            case GL_ATOMIC_COUNTER_BUFFER_START:
                bufferTarget = BufferTarget::AtomicCounter;
                queryKind = IndexedBufferQueryKind::Start;
                return true;
            case GL_ATOMIC_COUNTER_BUFFER_SIZE:
                bufferTarget = BufferTarget::AtomicCounter;
                queryKind = IndexedBufferQueryKind::Size;
                return true;
            case GL_SHADER_STORAGE_BUFFER_BINDING:
                bufferTarget = BufferTarget::ShaderStorage;
                queryKind = IndexedBufferQueryKind::Binding;
                return true;
            case GL_SHADER_STORAGE_BUFFER_START:
                bufferTarget = BufferTarget::ShaderStorage;
                queryKind = IndexedBufferQueryKind::Start;
                return true;
            case GL_SHADER_STORAGE_BUFFER_SIZE:
                bufferTarget = BufferTarget::ShaderStorage;
                queryKind = IndexedBufferQueryKind::Size;
                return true;
            default:
                return false;
            }
        }

        bool IsIndexedBufferBindingQueryKind(IndexedBufferQueryKind queryKind) {
            return queryKind == IndexedBufferQueryKind::Binding;
        }

        bool IsIndexedBufferRangeQueryKind(IndexedBufferQueryKind queryKind) {
            return queryKind == IndexedBufferQueryKind::Start || queryKind == IndexedBufferQueryKind::Size;
        }

        SizeT GetIndexedBufferQueryPointCount(BufferTarget bufferTarget) {
            const SizeT frontendCount = MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget);
            if (bufferTarget == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
                const Int backendCount =
                    MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
                return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
            }
            if (bufferTarget == BufferTarget::AtomicCounter) {
                // The counter family's binding count is NOT the state layer's array size: a
                // counter buffer only reaches a shader as a lowered storage block, so what an
                // implementation can serve is the reserved range, and that number is also what
                // glslang compiles a layout(binding = N) atomic_uint against. Clamped here so
                // GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, the indexed getters' index check and
                // glBindBufferBase's all report the same ceiling.
                return std::min(frontendCount,
                                static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
            }
            return frontendCount;
        }

        // A per-stage or combined BLOCK count is an amount of indexed binding points an
        // application will occupy, and GL 4.6 table 23.64 orders the two accordingly:
        // MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= every per-stage count,
        // and the same for the shader-storage family. The two families are answered from
        // unrelated places here - frontend constants, backend dynamic parameters, and a few
        // hard-coded TODOs - so nothing kept them ordered, and a backend that reports Vulkan
        // descriptor-indexing counts advertised 256 compute uniform blocks over 36 binding
        // points. KHR-GL44.multi_bind.dispatch_bind_buffers_base reads the block count and binds
        // that many buffers in ONE glBindBuffersBase, which is then INVALID_OPERATION before it
        // binds anything. Clamping is the only direction available: the binding count is the
        // capacity of the state layer's indexed-binding array, not a number we may inflate.
        GLint ClampBlockCountToBindingPoints(GLint blockCount, BufferTarget bufferTarget) {
            const GLint bindingPoints = static_cast<GLint>(GetIndexedBufferQueryPointCount(bufferTarget));
            return std::min(std::max(blockCount, 0), bindingPoints);
        }

        GLint ClampUniformBlockCount(GLint blockCount) {
            return ClampBlockCountToBindingPoints(blockCount, BufferTarget::Uniform);
        }

        GLint ClampStorageBlockCount(GLint blockCount) {
            return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
        }

        // The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS answers. Backend-derived, and NOT a
        // constant to be "restored" - these used to return a flat 16 for vertex, geometry and
        // both tessellation stages, which is wrong on any host that does not serve storage
        // blocks in those stages. Zero is a legal answer: GL 4.6 table 23.64 and ES 3.2 table
        // 21.44 both set the minimum at 0 for every graphics stage except fragment, which is
        // why the conformance suite gates each such test on the query instead of assuming it.
        // ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
        // bought nothing: the program still failed to link inside the backend, the frontend
        // still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
        GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
            static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
            const MG_Backend::DynamicBackendParameters& parameters =
                MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
                                                 : kBackendlessDefaults;
            return ClampStorageBlockCount(static_cast<GLint>(parameters.*stageLimit));
        }

        bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
            if (pname == GL_DRAW_BUFFER) {
                drawBufferIndex = 0;
                return true;
            }
            if (pname >= GL_DRAW_BUFFER0 && pname <= GL_DRAW_BUFFER15) {
                drawBufferIndex = static_cast<SizeT>(pname - GL_DRAW_BUFFER0);
                return true;
            }
            return false;
        }

        bool TryResolveImplementationColorReadParams(GLint& outFormat, GLint& outType) {
            const auto& readFbo =
                MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
            if (!readFbo) return false;

            const auto attachmentType = readFbo->GetReadBuffer();
            if (attachmentType == FramebufferAttachmentType::None) return false;

            const auto& attachment = readFbo->GetAttachment(attachmentType);
            TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
            if (attachment.IsTexture() && attachment.GetTexture()) {
                internalFormat = attachment.GetTexture()->GetFormat();
            } else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
                internalFormat = attachment.GetRenderbuffer()->GetInternalFormat();
            }

            if (internalFormat == TextureInternalFormat::Unknown) return false;

            const GLenum glInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
            GLenum normalizedInternalFormat = glInternalFormat;
            GLenum format = GL_RGBA;
            GLenum type = GL_UNSIGNED_BYTE;
            MG_Util::TextureFormatProcessor::NormalizePixelFormat(glInternalFormat, PixelFormatNormalizeOptionBit::None,
                                                                  &normalizedInternalFormat, &format, &type);
            outFormat = static_cast<GLint>(format);
            outType = static_cast<GLint>(type);
            return true;
        }

        void RecordIndexedOnlyGetterError(const char* functionName, GLenum pname) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", functionName,
                    "pname " + MG_Util::ConvertGLEnumToString(pname) +
                        " is only valid with indexed getter entrypoints."));
        }

        bool ValidateIndexedBufferQueryIndex(GLenum pname, GLuint index, const char* functionName,
                                             BufferTarget bufferTarget) {
            switch (bufferTarget) {
            case BufferTarget::Uniform:
            case BufferTarget::TransformFeedback:
            case BufferTarget::AtomicCounter:
            case BufferTarget::ShaderStorage:
                break;
            default:
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                                 "pname " + std::to_string(pname) +
                                                     " is not a supported indexed buffer query."));
                return false;
            }

            if (index >= GetIndexedBufferQueryPointCount(bufferTarget)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", functionName,
                        "index " + std::to_string(index) + " is out of range for indexed buffer query " +
                            std::to_string(pname) + "."));
                return false;
            }

            return true;
        }

        // GL_TEXTURE_BINDING_* is per-texture-unit state: glGetIntegerv answers for the
        // active unit, glGetIntegeri_v answers for unit `index`. Both need the same
        // pname -> target decode, so it lives here instead of being spelled out twice.
        bool TryDecodeTextureUnitBindingPname(GLenum pname, TextureTarget& outTarget) {
            switch (pname) {
            case GL_TEXTURE_BINDING_1D: outTarget = TextureTarget::Texture1D; return true;
            case GL_TEXTURE_BINDING_1D_ARRAY: outTarget = TextureTarget::Texture1DArray; return true;
            case GL_TEXTURE_BINDING_2D: outTarget = TextureTarget::Texture2D; return true;
            case GL_TEXTURE_BINDING_2D_ARRAY: outTarget = TextureTarget::Texture2DArray; return true;
            case GL_TEXTURE_BINDING_2D_MULTISAMPLE: outTarget = TextureTarget::Texture2DMultisample; return true;
            case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY:
                outTarget = TextureTarget::Texture2DMultisampleArray;
                return true;
            case GL_TEXTURE_BINDING_3D: outTarget = TextureTarget::Texture3D; return true;
            case GL_TEXTURE_BINDING_BUFFER: outTarget = TextureTarget::TextureBuffer; return true;
            case GL_TEXTURE_BINDING_CUBE_MAP: outTarget = TextureTarget::TextureCubeMap; return true;
            case GL_TEXTURE_BINDING_CUBE_MAP_ARRAY: outTarget = TextureTarget::TextureCubeMapArray; return true;
            case GL_TEXTURE_BINDING_RECTANGLE: outTarget = TextureTarget::TextureRectangle; return true;
            default: return false;
            }
        }

        GLint QueryTextureBindingOnUnit(Int unit, TextureTarget target) {
            auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
            const auto& obj = textureUnit.GetBindingSlot(target).GetBoundObject();
            return obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
        }

        GLint QuerySamplerBindingOnUnit(Int unit) {
            const auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
            const auto& sampler = textureUnit.GetSamplerObject();
            return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
        }

        // The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
        // frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
        // indexed getters must read the indexed storage - the generic path at the bottom of
        // GetIntegeri_v is a raw backend passthrough that has no case for them and returned
        // zeros, and routing them to the NON-indexed getter (what this used to do) answered every
        // index with viewport 0's value, which is what
        // KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
        Bool IsIndexedViewportQuery(GLenum target) {
            return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
        }

        // Component count of an indexed viewport-array query, so every width of getter writes the
        // caller's whole buffer instead of just element 0 (GL 4.6 core 22.1).
        GLsizei IndexedViewportQueryComponents(GLenum target) {
            return target == GL_DEPTH_RANGE ? 2 : 4;
        }

        // ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is
        // the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports -
        // taking it from the backend caps instead would let a device limit of 1 (a Vulkan device
        // without the multiViewport feature) make index 1 illegal even though the state exists.
        Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
            if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
            return false;
        }

        // The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
        // shape MobileGL stores (the viewport really is float state; the scissor box is integral
        // and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
        // getter width funnels through this so they can never disagree with each other.
        void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
            switch (target) {
            case GL_VIEWPORT: {
                const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
                out[0] = viewport.x();
                out[1] = viewport.y();
                out[2] = viewport.z();
                out[3] = viewport.w();
                return;
            }
            case GL_SCISSOR_BOX: {
                const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
                out[0] = static_cast<GLfloat>(box.x());
                out[1] = static_cast<GLfloat>(box.y());
                out[2] = static_cast<GLfloat>(box.z());
                out[3] = static_cast<GLfloat>(box.w());
                return;
            }
            case GL_DEPTH_RANGE: {
                const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
                out[0] = range.x();
                out[1] = range.y();
                return;
            }
            default:
                MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
                                static_cast<Uint32>(target));
                return;
            }
        }

        void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
            for (SizeT i = 0; i < count; ++i) {
                dst[i] = src[i] ? GL_TRUE : GL_FALSE;
            }
        }

        void CopyIntsToFloats(const GLint* src, SizeT count, GLfloat* dst) {
            for (SizeT i = 0; i < count; ++i) {
                dst[i] = static_cast<GLfloat>(src[i]);
            }
        }
    } // namespace

    // GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
    // before it is advertised. gl_MaxSamples expands from the same floored number
    // (BuildTBuiltInResource), which is also what sizes gl_SampleMask[].
    //
    // THE FLOOR STOPS HERE, and that is the point. It used to be applied to
    // GL_MAX_INTEGER_SAMPLES, GL_MAX_COLOR_TEXTURE_SAMPLES and GL_MAX_DEPTH_TEXTURE_SAMPLES too,
    // on the reasoning that an application reads GL_MAX_SAMPLES once and hands that count to
    // every glTexStorage*Multisample. Table 23.53 gives those three a minimum of ONE, and the
    // reasoning had it backwards: Adreno and Mali back an integer multisample texture with a
    // single sample, so flooring the query at 4 did not make four samples exist - it made the
    // backend silently under-allocate (ClampSamplesToBackendSupport) while the application wrote
    // per-sample data it could never read back. Reporting what was probed turns that into an
    // honest "unsupported" the application can branch on.
    GLint GetAdvertisedMaxSamples() {
        if (MG_Backend::pActiveBackendObject == nullptr) {
            return kFrontendMaxSamples;
        }
        return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
    }

    // GL 4.6 core table 23.53 minimum for the per-category multisample ceilings. One, not four:
    // see the note on GetAdvertisedMaxSamples. A zero would be a probe that never ran, so it is
    // floored rather than trusted.
    namespace {
        GLint AdvertisedCategoryMaxSamples(Int MG_Backend::DynamicBackendParameters::*categoryLimit) {
            if (MG_Backend::pActiveBackendObject == nullptr) {
                return 1;
            }
            return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().*categoryLimit, 1);
        }
    } // namespace

    GLint GetAdvertisedColorTextureMaxSamples() {
        return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxColorTextureSamples);
    }

    GLint GetAdvertisedDepthTextureMaxSamples() {
        return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxDepthTextureSamples);
    }

    GLint GetAdvertisedIntegerMaxSamples() {
        return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxIntegerSamples);
    }

    // Declared in GL_Getter.h, so that the draw path can feed the same number to the reserved
    // gl_NumSamples stand-in that glGetIntegerv(GL_SAMPLES) reports.
    GLint ResolveDrawFramebufferSampleCount() {
        const auto& drawFbo =
            MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
        if (!drawFbo) return 0;

        GLint maxSamples = 0;
        for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
            if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
                maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
            } else if (attachment.IsTexture() && attachment.GetTexture()) {
                // Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
                // report 1 for any multisampled draw framebuffer).
                maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
            }
        }
        return maxSamples;
    }

    /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
    const GLubyte* GetString(GLenum name) {
        static String vendorString;
        static String versionStr;
        static String rendererString;
        static String shadingLanguageVersion;
        static String extensionsString;
        const auto& activeBackendObject = MG_Backend::pActiveBackendObject;

        MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
        if (!activeBackendObject) {
            MGLOG_E_ONCE("activeBackendObject is not initialized!");
            return (GLubyte*)"Unknown";
        }

        const auto& rendererInfo = activeBackendObject->GetRendererInfo();

        switch (name) {
        case GL_VENDOR:
            if (rendererInfo.ExtraVendor.has_value()) {
                vendorString = std::format("{}{}", MG_Config::CoreVendor, rendererInfo.ExtraVendor.value());
            } else {
                vendorString = MG_Config::CoreVendor;
            }
            MGLOG_D("vendorString: %s", vendorString.c_str());
            return (const GLubyte*)vendorString.c_str();
        case GL_VERSION: {
            versionStr =
                std::format("{} {} {}, {} Backend, GIT@" GIT_COMMIT_HASH_SHORT,
                            rendererInfo.RendererGLInfo.TargetGLVersion.toString(), MG_Config::ProjectName,
                            MG_Config::CoreVersion.toFormattedString(MG_Config::DefaultVersionStringFormatAttrib),
                            rendererInfo.BackendName);
            MGLOG_D("versionStr: %s", versionStr.c_str());
            return (const GLubyte*)versionStr.c_str();
        }
        case GL_RENDERER: {
            String backendVersionStr = activeBackendObject->GetBackendAPIVersionString();
            rendererString =
                std::format("{} ({}) ({})", rendererInfo.RendererName, MG_Config::CoreName, backendVersionStr);
            MGLOG_D("rendererString: %s", rendererString.c_str());
            return (const GLubyte*)rendererString.c_str();
        }
        case GL_SHADING_LANGUAGE_VERSION:
            shadingLanguageVersion =
                std::format("{} {}", rendererInfo.RendererGLInfo.TargetGLSLVersion.toString({true, false}),
                            MG_Config::ProjectName);
            MGLOG_D("shadingLanguageVersion: %s", shadingLanguageVersion.c_str());
            return (const GLubyte*)shadingLanguageVersion.c_str();
        case GL_EXTENSIONS:
            extensionsString.clear();
            for (const auto& ext : rendererInfo.RendererGLInfo.Extensions) {
                if (!extensionsString.empty()) {
                    extensionsString += " ";
                }
                extensionsString += MG_Util::ConvertGLExtToString(ext);
            }
            return (const GLubyte*)extensionsString.c_str();
        default:
            return (const GLubyte*)"Unknown Enum";
        }
    }

    const GLubyte* GetStringi(GLenum name, GLuint index) {
        MGLOG_D("glGetStringi, name: %s, index: %u", MG_Util::ConvertGLEnumToString(name).c_str(), index);
        if (name != GL_EXTENSIONS) {
            return (const GLubyte*)"";
        }

        const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
        if (!activeBackendObject) {
            MGLOG_E_ONCE("activeBackendObject is not initialized!");
            return (GLubyte*)"Unknown";
        }
        const auto& rendererInfo = activeBackendObject->GetRendererInfo();

        const auto& exts = rendererInfo.RendererGLInfo.Extensions;
        if (index >= exts.size()) {
            return nullptr;
        }

        static Vector<String> extStrings;
        extStrings.clear();
        extStrings.reserve(exts.size());
        for (const auto& ext : exts) {
            extStrings.emplace_back(MG_Util::ConvertGLExtToString(ext));
        }

        return (const GLubyte*)extStrings[index].c_str();
    }

    void GetBooleanv(GLenum pname, GLboolean* params) {
        MGLOG_D("glGetBooleanv, pname: %s", MG_Util::ConvertGLEnumToString(pname).c_str());
        if (!params) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "params pointer cannot be null"));
            return;
        }

        switch (pname) {
        case GL_BLEND:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE;
            return;
        case GL_BLEND_COLOR: {
            const FloatVec4& blendColor = MG_State::pGLContext->GetBlendColor();
            params[0] = blendColor.x() != 0.0f ? GL_TRUE : GL_FALSE;
            params[1] = blendColor.y() != 0.0f ? GL_TRUE : GL_FALSE;
            params[2] = blendColor.z() != 0.0f ? GL_TRUE : GL_FALSE;
            params[3] = blendColor.w() != 0.0f ? GL_TRUE : GL_FALSE;
            return;
        }
        case GL_COLOR_CLEAR_VALUE: {
            const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
            params[0] = clearColor.x() != 0.0f ? GL_TRUE : GL_FALSE;
            params[1] = clearColor.y() != 0.0f ? GL_TRUE : GL_FALSE;
            params[2] = clearColor.z() != 0.0f ? GL_TRUE : GL_FALSE;
            params[3] = clearColor.w() != 0.0f ? GL_TRUE : GL_FALSE;
            return;
        }
        case GL_COLOR_WRITEMASK: {
            BoolVec4 mask = MG_State::pGLContext->GetColorMask();
            params[0] = mask.x() ? GL_TRUE : GL_FALSE;
            params[1] = mask.y() ? GL_TRUE : GL_FALSE;
            params[2] = mask.z() ? GL_TRUE : GL_FALSE;
            params[3] = mask.w() ? GL_TRUE : GL_FALSE;
            return;
        }
        case GL_DEPTH_CLEAR_VALUE:
            *params = MG_State::pGLContext->GetClearDepth() != 0.0f ? GL_TRUE : GL_FALSE;
            return;
        case GL_DEPTH_RANGE: {
            const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRange();
            params[0] = depthRange.x() != 0.0f ? GL_TRUE : GL_FALSE;
            params[1] = depthRange.y() != 0.0f ? GL_TRUE : GL_FALSE;
            return;
        }
        case GL_LINE_WIDTH:
            *params = MG_State::pGLContext->GetLineWidth() != 0.0f ? GL_TRUE : GL_FALSE;
            return;
        case GL_POINT_SIZE:
            *params = MG_State::pGLContext->GetPointSize() != 0.0f ? GL_TRUE : GL_FALSE;
            return;
        case GL_CULL_FACE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace) ? GL_TRUE : GL_FALSE;
            return;
        case GL_DEPTH_TEST:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ? GL_TRUE : GL_FALSE;
            return;
        case GL_DEPTH_WRITEMASK:
            *params = MG_State::pGLContext->GetDepthMask() ? GL_TRUE : GL_FALSE;
            return;
        case GL_DITHER:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Dither) ? GL_TRUE : GL_FALSE;
            return;
        case GL_MULTISAMPLE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
            return;
        case GL_POLYGON_OFFSET_FILL:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) ? GL_TRUE : GL_FALSE;
            return;
        case GL_PRIMITIVE_RESTART_FIXED_INDEX:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)
                          ? GL_TRUE
                          : GL_FALSE;
            return;
        case GL_RASTERIZER_DISCARD:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard) ? GL_TRUE : GL_FALSE;
            return;
        case GL_SAMPLE_ALPHA_TO_COVERAGE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleAlphaToCoverage)
                          ? GL_TRUE
                          : GL_FALSE;
            return;
        case GL_SAMPLE_ALPHA_TO_ONE:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleAlphaToOne) ? GL_TRUE : GL_FALSE;
            return;
        case GL_SAMPLE_COVERAGE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleCoverage)
                          ? GL_TRUE
                          : GL_FALSE;
            return;
        case GL_SAMPLE_MASK:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE;
            return;
        case GL_SCISSOR_TEST:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest) ? GL_TRUE : GL_FALSE;
            return;
        case GL_STENCIL_TEST:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
            return;
        case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
        case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
        case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
        // Same reason as the three above: the integer fallback would round the fraction to 0
        // or 1 first, so a 0.25 sample-shading rate would answer GL_FALSE.
        case GL_MIN_SAMPLE_SHADING_VALUE: {
            GLfloat value = 0.0f;
            GetFloatv(pname, &value);
            *params = value != 0.0f ? GL_TRUE : GL_FALSE;
            return;
        }
        // Float-native state, so GL 4.6 core 2.2.2's "zero becomes FALSE, every other value
        // becomes TRUE" has to be applied to the VALUE. Answering these through the integer getter
        // below instead - which rounds - reported GL_FALSE for a perfectly non-zero level of 0.25,
        // and every other float state in this function already reads through GetFloatv for exactly
        // that reason.
        case GL_PATCH_DEFAULT_OUTER_LEVEL:
        case GL_PATCH_DEFAULT_INNER_LEVEL: {
            const GLsizei componentCount = pname == GL_PATCH_DEFAULT_OUTER_LEVEL ? 4 : 2;
            GLfloat levels[4] = {};
            GetFloatv(pname, levels);
            for (GLsizei i = 0; i < componentCount; ++i) {
                params[i] = levels[i] != 0.0f ? GL_TRUE : GL_FALSE;
            }
            return;
        }
        default:
            break;
        }

        GLint ints[4] = {};
        GetIntegerv(pname, ints);
        switch (pname) {
        case GL_COLOR_WRITEMASK:
        case GL_SCISSOR_BOX:
            CopyIntsToBooleans(ints, 4, params);
            return;
        default:
            *params = ints[0] ? GL_TRUE : GL_FALSE;
            return;
        }
    }

    void GetFloatv(GLenum pname, GLfloat* params) {
        MGLOG_D("glGetFloatv, pname: %s", MG_Util::ConvertGLEnumToString(pname).c_str());
        if (!params) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "params pointer cannot be null"));
            return;
        }

        switch (pname) {
        case GL_BLEND_COLOR: {
            const FloatVec4& blendColor = MG_State::pGLContext->GetBlendColor();
            params[0] = blendColor.x();
            params[1] = blendColor.y();
            params[2] = blendColor.z();
            params[3] = blendColor.w();
            return;
        }
        case GL_COLOR_CLEAR_VALUE: {
            const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
            params[0] = clearColor.x();
            params[1] = clearColor.y();
            params[2] = clearColor.z();
            params[3] = clearColor.w();
            return;
        }
        case GL_DEPTH_RANGE: {
            const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRange();
            params[0] = depthRange.x();
            params[1] = depthRange.y();
            return;
        }
        // glPatchParameterfv's two states. Float-native, so they are answered here rather than
        // through the integer fallback below - which rounds, and would report 0 for a level of 0.5.
        case GL_PATCH_DEFAULT_OUTER_LEVEL: {
            const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
            params[0] = outer.x();
            params[1] = outer.y();
            params[2] = outer.z();
            params[3] = outer.w();
            return;
        }
        case GL_PATCH_DEFAULT_INNER_LEVEL: {
            const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
            params[0] = inner.x();
            params[1] = inner.y();
            return;
        }
        case GL_VIEWPORT_BOUNDS_RANGE: {
            const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
            params[0] = dynamicParameters.ViewportBoundsRangeMin;
            params[1] = dynamicParameters.ViewportBoundsRangeMax;
            return;
        }
        // Viewport 0's rectangle, verbatim. Falling through to the integer width below would
        // round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and
        // glGetFloatv(GL_VIEWPORT) is a lossless query of float state.
        case GL_VIEWPORT: {
            const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0);
            params[0] = viewport.x();
            params[1] = viewport.y();
            params[2] = viewport.z();
            params[3] = viewport.w();
            return;
        }
        case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
        case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
        case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
            const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
            if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) {
                params[0] = dynamicParameters.MinFragmentInterpolationOffset;
            } else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) {
                params[0] = dynamicParameters.MaxFragmentInterpolationOffset;
            } else {
                params[0] = static_cast<GLfloat>(dynamicParameters.FragmentInterpolationOffsetBits);
            }
            return;
        }
        case GL_DEPTH_CLEAR_VALUE:
            params[0] = MG_State::pGLContext->GetClearDepth();
            return;
        case GL_ALIASED_LINE_WIDTH_RANGE: {
            const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
            params[0] = dynamicParameters.AliasedLineWidthRangeMin;
            params[1] = dynamicParameters.AliasedLineWidthRangeMax;
            return;
        }
        case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: {
            // EXT_texture_filter_anisotropic queries this as a float; the integer path below widens
            // from here, so this case is the authoritative one.
            const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
            params[0] = dynamicParameters.MaxTextureMaxAnisotropy;
            return;
        }
        case GL_ALIASED_POINT_SIZE_RANGE:
        case GL_POINT_SIZE_RANGE: {
            const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
            params[0] = dynamicParameters.PointSizeRangeMin;
            params[1] = dynamicParameters.PointSizeRangeMax;
            return;
        }
        case GL_LINE_WIDTH:
            params[0] = MG_State::pGLContext->GetLineWidth();
            return;
        case GL_POINT_SIZE:
            params[0] = MG_State::pGLContext->GetPointSize();
            return;
        case GL_POLYGON_OFFSET_FACTOR:
            params[0] = MG_State::pGLContext->GetPolygonOffsetFactor();
            return;
        case GL_POLYGON_OFFSET_UNITS:
            params[0] = MG_State::pGLContext->GetPolygonOffsetUnits();
            return;
        case GL_POLYGON_OFFSET_CLAMP:
            // Float-native state, so it is answered here rather than through the integer
            // fallback: glPolygonOffsetClamp(1, 1, 0.5) must read back as 0.5, not as 0.
            params[0] = MG_State::pGLContext->GetPolygonOffsetClamp();
            return;
        case GL_SMOOTH_LINE_WIDTH_RANGE: {
            const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
            params[0] = dynamicParameters.SmoothLineWidthRangeMin;
            params[1] = dynamicParameters.SmoothLineWidthRangeMax;
            return;
        }
        case GL_SMOOTH_LINE_WIDTH_GRANULARITY:
            params[0] = MG_Backend::pActiveBackendObject->GetDynamicParameters().SmoothLineWidthGranularity;
            return;
        case GL_POINT_SIZE_GRANULARITY:
            params[0] = MG_Backend::pActiveBackendObject->GetDynamicParameters().PointSizeGranularity;
            return;
        case GL_SAMPLE_COVERAGE_VALUE:
            params[0] = MG_State::pGLContext->GetSampleCoverageValue();
            return;
        case GL_MIN_SAMPLE_SHADING_VALUE:
            // Float state, so it has to be answered here rather than through the integer
            // fallback: glMinSampleShading(0.5) must read back as 0.5 and not as 0.
            params[0] = MG_State::pGLContext->GetMinSampleShadingValue();
            return;
        case GL_POINT_FADE_THRESHOLD_SIZE:
            // Float state: read it directly so the fractional part is not lost to the integer path.
            params[0] = MG_State::pGLContext->GetPointFadeThresholdSize();
            return;
        default:
            break;
        }

        GLint ints[4] = {};
        GetIntegerv(pname, ints);
        switch (pname) {
        case GL_ALIASED_LINE_WIDTH_RANGE:
        case GL_ALIASED_POINT_SIZE_RANGE:
        case GL_SMOOTH_LINE_WIDTH_RANGE:
        case GL_POINT_SIZE_RANGE:
        case GL_MAX_VIEWPORT_DIMS:
            CopyIntsToFloats(ints, 2, params);
            return;
        case GL_SCISSOR_BOX:
        case GL_VIEWPORT:
            CopyIntsToFloats(ints, 4, params);
            return;
        default:
            params[0] = static_cast<GLfloat>(ints[0]);
            return;
        }
    }

    void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
        if (!data) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
            return;
        }

        BufferTarget bufferTarget = BufferTarget::Unknown;
        IndexedBufferQueryKind queryKind = IndexedBufferQueryKind::Binding;
        if (TryDecodeIndexedBufferQuery(target, bufferTarget, queryKind)) {
            if (!ValidateIndexedBufferQueryIndex(target, index, __func__, bufferTarget)) return;
            const auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
            const auto& bufferObject = bindingPoint.GetBoundObject();
            if (!bufferObject) {
                *data = 0;
                return;
            }

            switch (queryKind) {
            case IndexedBufferQueryKind::Binding:
                *data = static_cast<GLint>(bufferObject->GetExternalIndex());
                return;
            case IndexedBufferQueryKind::Start:
                if (!bindingPoint.HasExplicitRange()) {
                    *data = 0;
                    return;
                }
                *data = static_cast<GLint>(bindingPoint.GetRange().start);
                return;
            case IndexedBufferQueryKind::Size: {
                if (!bindingPoint.HasExplicitRange()) {
                    *data = 0;
                    return;
                }
                // GL 4.6 core table 23.4/23.5: *_BUFFER_SIZE reports the size glBindBufferRange
                // was ASKED for, verbatim. It is not clamped to the buffer's storage, and it does
                // not follow the buffer when a later glBufferData resizes it - a range may legally
                // name bytes the buffer does not have yet. Clamping it here answered 0 for the
                // common conformance shape of binding a range on a buffer that has no storage
                // yet (KHR-GL43.shader_storage_buffer_object.basic-binding).
                const Range1D range = bindingPoint.GetRange();
                *data = static_cast<GLint>(range.end - range.start);
                return;
            }
            default:
                break;
            }
        }

        // Per-texture-unit bindings: GL 4.6 core table 23.19 makes every GL_TEXTURE_BINDING_*
        // and GL_SAMPLER_BINDING indexed by texture unit. Without this they fell through to
        // the raw backend passthrough at the bottom, which knows nothing about the
        // frontend's binding state.
        if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
            TryDecodeTextureUnitBindingPname(target, textureBindingTarget) || target == GL_SAMPLER_BINDING) {
            GLint maxUnits = 0;
            GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits);
            maxUnits = std::min<GLint>(maxUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
            if (index >= static_cast<GLuint>(std::max(maxUnits, 0))) {
                *data = 0;
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture unit index is out of range."));
                return;
            }
            *data = target == GL_SAMPLER_BINDING
                ? QuerySamplerBindingOnUnit(static_cast<Int>(index))
                : QueryTextureBindingOnUnit(static_cast<Int>(index), textureBindingTarget);
            return;
        }

        // GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is
        // indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer
        // width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is
        // the sticky error KHR-GL43.viewport_array.queries trips over at its next error check.
        if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
            *data = IsEnabledi(target, index);
            return;
        }

        switch (target) {
        // ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
        // (gl4cMultiBindTests and the viewport_array group both do).
        case GL_VIEWPORT:
        case GL_SCISSOR_BOX:
        case GL_DEPTH_RANGE: {
            if (!ValidateViewportQueryIndex(index, __func__)) return;
            GLfloat values[4] = {};
            ReadIndexedViewportStateFloat(target, index, values);
            const GLsizei components = IndexedViewportQueryComponents(target);
            for (GLsizei i = 0; i < components; ++i) {
                // Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
                // (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
                data[i] = static_cast<GLint>(std::lround(values[i]));
            }
            return;
        }
        // The vertex buffer binding points of the vertex array object that is bound. Indexed by
        // binding point, not by attribute (GL 4.6 core 10.3.1).
        case GL_VERTEX_BINDING_BUFFER:
        case GL_VERTEX_BINDING_DIVISOR:
        case GL_VERTEX_BINDING_OFFSET:
        case GL_VERTEX_BINDING_STRIDE: {
            if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                                 "Vertex buffer binding index is out of range."));
                return;
            }
            const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
            if (!vao) {
                *data = 0;
                return;
            }
            const auto& binding = vao->GetBindingPoint(index);
            switch (target) {
            case GL_VERTEX_BINDING_BUFFER:
                *data = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
                return;
            case GL_VERTEX_BINDING_DIVISOR:
                *data = static_cast<GLint>(binding.Divisor);
                return;
            case GL_VERTEX_BINDING_OFFSET:
                *data = static_cast<GLint>(binding.Offset);
                return;
            default:
                *data = static_cast<GLint>(binding.Stride);
                return;
            }
        }
        case GL_IMAGE_BINDING_NAME:
        case GL_IMAGE_BINDING_LEVEL:
        case GL_IMAGE_BINDING_LAYERED:
        case GL_IMAGE_BINDING_LAYER:
        case GL_IMAGE_BINDING_ACCESS:
        case GL_IMAGE_BINDING_FORMAT: {
            const auto maxImageUnits = static_cast<GLuint>(std::min<GLint>(
                MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxImageUnits,
                MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
            if (index >= maxImageUnits) {
                *data = 0;
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Image unit index is out of range."));
                return;
            }

            const auto& binding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
            switch (target) {
            case GL_IMAGE_BINDING_NAME:
                *data = binding.Texture ? static_cast<GLint>(binding.Texture->GetExternalIndex()) : 0;
                return;
            case GL_IMAGE_BINDING_LEVEL:
                *data = binding.Level;
                return;
            case GL_IMAGE_BINDING_LAYERED:
                *data = binding.Layered;
                return;
            case GL_IMAGE_BINDING_LAYER:
                *data = binding.Layer;
                return;
            case GL_IMAGE_BINDING_ACCESS:
                *data = static_cast<GLint>(binding.Access);
                return;
            case GL_IMAGE_BINDING_FORMAT:
                *data = static_cast<GLint>(binding.Format);
                return;
            default:
                break;
            }
        }
        default:
            break;
        }

        auto getIntegeri = MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v;
        if (target == GL_MAX_COMPUTE_WORK_GROUP_COUNT || target == GL_MAX_COMPUTE_WORK_GROUP_SIZE) {
            if (index >= 3) {
                *data = 0;
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                                 "Compute work group index is out of range."));
                return;
            }

            const GLint minimum = target == GL_MAX_COMPUTE_WORK_GROUP_COUNT
                ? GetMinComputeWorkGroupCount(index)
                : GetMinComputeWorkGroupSize(index);
            GLint backendValue = 0;
            if (getIntegeri) {
                getIntegeri(target, index, &backendValue);
            }
            *data = std::max(backendValue, minimum);
            return;
        }

        if (!getIntegeri) {
            *data = 0;
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support indexed integer queries."));
            return;
        }
        getIntegeri(target, index, data);
    }

    // GL_ARB_viewport_array's typed indexed getters. They were no-op stubs, which left the
    // caller's output buffer holding whatever was on the stack. The multi-component indexed
    // rectangles are answered from the frontend's own viewport/scissor/depth-range state, via
    // the non-indexed getter of the matching type - GL_DEPTH_RANGE is float state, so putting
    // it through the integer query would round it to 0/1. Everything else MobileGL answers
    // indexed is scalar integer-domain state, where converting the integer query is exact.
    void GetFloati_v(GLenum target, GLuint index, GLfloat* data) {
        if (!data) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
            return;
        }
        if (IsIndexedViewportQuery(target)) {
            if (!ValidateViewportQueryIndex(index, __func__)) return;
            // Verbatim, NOT via the integer width: the viewport is float state and
            // KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
            // glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
            ReadIndexedViewportStateFloat(target, index, data);
            return;
        }
        GLint ints[4] = {};
        GetIntegeri_v(target, index, ints);
        data[0] = static_cast<GLfloat>(ints[0]);
    }

    void GetDoublei_v(GLenum target, GLuint index, GLdouble* data) {
        if (!data) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
            return;
        }
        if (IsIndexedViewportQuery(target)) {
            if (!ValidateViewportQueryIndex(index, __func__)) return;
            GLfloat values[4] = {};
            ReadIndexedViewportStateFloat(target, index, values);
            const GLsizei components = IndexedViewportQueryComponents(target);
            for (GLsizei i = 0; i < components; ++i) {
                data[i] = static_cast<GLdouble>(values[i]);
            }
            return;
        }
        GLint ints[4] = {};
        GetIntegeri_v(target, index, ints);
        data[0] = static_cast<GLdouble>(ints[0]);
    }

    void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
        if (!data) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
            return;
        }

        BufferTarget bufferTarget = BufferTarget::Unknown;
        IndexedBufferQueryKind queryKind = IndexedBufferQueryKind::Binding;
        if (TryDecodeIndexedBufferQuery(target, bufferTarget, queryKind)) {
            if (!ValidateIndexedBufferQueryIndex(target, index, __func__, bufferTarget)) return;
            const auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
            const auto& bufferObject = bindingPoint.GetBoundObject();
            if (!bufferObject) {
                *data = 0;
                return;
            }

            const Range1D range = bindingPoint.GetRange();
            switch (queryKind) {
            case IndexedBufferQueryKind::Binding:
                *data = static_cast<GLint64>(bufferObject->GetExternalIndex());
                return;
            case IndexedBufferQueryKind::Start:
                if (!bindingPoint.HasExplicitRange()) {
                    *data = 0;
                    return;
                }
                *data = static_cast<GLint64>(range.start);
                return;
            case IndexedBufferQueryKind::Size: {
                if (!bindingPoint.HasExplicitRange()) {
                    *data = 0;
                    return;
                }
                // Verbatim, unclamped - see the GetIntegeri_v arm.
                *data = static_cast<GLint64>(range.end - range.start);
                return;
            }
            default:
                break;
            }
        }

        // The one indexed pname whose value genuinely needs 64 bits: a vertex buffer binding
        // offset is an intptr, so taking the 32-bit route below would truncate it.
        if (target == GL_VERTEX_BINDING_OFFSET) {
            if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                                 "Vertex buffer binding index is out of range."));
                return;
            }
            const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
            *data = vao ? static_cast<GLint64>(vao->GetBindingPoint(index).Offset) : 0;
            return;
        }

        // Everything else is 32-bit indexed state that the glGetIntegeri_v pname table already
        // owns, and GL 4.6 core 22.1 says every indexed query answers every indexed pname.
        // Handing the leftovers straight to the backend instead made glGetInteger64i_v disagree
        // with glGetIntegeri_v on the very same pname - GL_MAX_COMPUTE_WORK_GROUP_COUNT read
        // back 0 while the 32-bit view said 65535 (KHR-GL43.compute_shader.max), because a
        // frontend-only value simply is not in the driver's table.
        GLint values[4] = {};
        GetIntegeri_v(target, index, values);
        // The viewport-array rectangles are the only multi-component indexed state here; every
        // other pname is scalar, so widening element 0 alone would silently truncate them.
        const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1;
        for (GLsizei i = 0; i < components; ++i) {
            data[i] = static_cast<GLint64>(values[i]);
        }
    }

    void GetInteger64v(GLenum pname, GLint64* params) {
        MGLOG_D("glGetInteger64v, pname: %s", MG_Util::ConvertGLEnumToString(pname).c_str());
        if (!params) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "params pointer cannot be null"));
            return;
        }

        switch (pname) {
        case GL_MAX_ELEMENT_INDEX:
            // The largest value a GL_UNSIGNED_INT index may take. It has to be answered HERE and
            // not left to the 32-bit fallback below: the conformance suite reads it with
            // glGetInteger64v, and widening the saturated GLint would report INT32_MAX where the
            // spec requires 2^32-1.
            params[0] = 0xFFFFFFFFLL;
            return;
        case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
            if (MG_Backend::pActiveBackendObject) {
                params[0] = static_cast<GLint64>(
                    MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBlockSize);
            } else {
                params[0] = static_cast<GLint64>(MG_Backend::DynamicBackendParameters{}.MaxShaderStorageBlockSize);
            }
            return;
        case GL_SUBGROUP_SIZE_KHR:
        case GL_SUBGROUP_SUPPORTED_STAGES_KHR:
        case GL_SUBGROUP_SUPPORTED_FEATURES_KHR:
        case GL_SUBGROUP_QUAD_ALL_STAGES_KHR: {
            GLint value = 0;
            GetIntegerv(pname, &value);
            params[0] = static_cast<GLint64>(value);
            return;
        }
        case GL_TIMESTAMP: {
            // Handled here (not via the 32-bit GetIntegerv fallback) so the
            // full 64-bit GPU timestamp survives; LWJGL reads it this way.
            Int64 timestamp = 0;
            if (!MG_Config::Features.DisableTimerQuery) {
                if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
                    timestamp = getGpuTimestampNs();
                }
            }
            params[0] = static_cast<GLint64>(timestamp);
            return;
        }
        default:
            break;
        }

        GLint ints[4] = {};
        GetIntegerv(pname, ints);

        // GL 4.6 core 22.1 gives glGetInteger64v the same accepted-pname set as glGetIntegerv, so
        // every pname the integer getter answers with several components owes them all here too.
        // A pname that reaches the `default:` arm writes params[0] and leaves the caller's other
        // components holding whatever they held, with no error to say so.
        switch (pname) {
        case GL_BLEND_COLOR:
        case GL_COLOR_CLEAR_VALUE:
        case GL_COLOR_WRITEMASK:
        case GL_SCISSOR_BOX:
        case GL_VIEWPORT:
        case GL_PATCH_DEFAULT_OUTER_LEVEL:
            for (int i = 0; i < 4; ++i) {
                params[i] = static_cast<GLint64>(ints[i]);
            }
            return;
        case GL_DEPTH_RANGE:
        case GL_ALIASED_POINT_SIZE_RANGE:
        case GL_MAX_VIEWPORT_DIMS:
        case GL_POINT_SIZE_RANGE:
        case GL_VIEWPORT_BOUNDS_RANGE:
        case GL_PATCH_DEFAULT_INNER_LEVEL:
            params[0] = static_cast<GLint64>(ints[0]);
            params[1] = static_cast<GLint64>(ints[1]);
            return;
        default:
            params[0] = static_cast<GLint64>(ints[0]);
            return;
        }
    }

    // glGetDoublev shares GetFloatv's accepted-pname set (and its INVALID_ENUM handling) and widens the
    // result. MobileGL stores no native-double state (depth range/clear are float), so widening from
    // float matches the resolution MobileGL actually holds. Only the pname's own component count is
    // written, never a fixed 4, so a 1-component query cannot overrun the caller's buffer.
    void GetDoublev(GLenum pname, GLdouble* params) {
        if (!params) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "params pointer cannot be null"));
            return;
        }
        GLfloat floats[4] = {};
        GetFloatv(pname, floats);
        GLsizei count = 1;
        switch (pname) {
        case GL_DEPTH_RANGE:
        case GL_VIEWPORT_BOUNDS_RANGE:
        case GL_ALIASED_LINE_WIDTH_RANGE:
        case GL_ALIASED_POINT_SIZE_RANGE:
        case GL_POINT_SIZE_RANGE:
        case GL_SMOOTH_LINE_WIDTH_RANGE:
        case GL_MAX_VIEWPORT_DIMS:
        case GL_PATCH_DEFAULT_INNER_LEVEL:
            count = 2;
            break;
        case GL_BLEND_COLOR:
        case GL_COLOR_CLEAR_VALUE:
        case GL_VIEWPORT:
        case GL_SCISSOR_BOX:
        case GL_COLOR_WRITEMASK:
        case GL_PATCH_DEFAULT_OUTER_LEVEL:
            count = 4;
            break;
        default:
            count = 1;
            break;
        }
        for (GLsizei i = 0; i < count; ++i) {
            params[i] = static_cast<GLdouble>(floats[i]);
        }
    }

    void GetIntegerv(GLenum pname, GLint* params) {
        MGLOG_D("glGetIntegerv, pname: %s", MG_Util::ConvertGLEnumToString(pname).c_str());
        if (!params) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv", "params pointer cannot be null"));
            return;
        }

        // Per-texture-unit bindings: the non-indexed query reports the active unit.
        if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
            TryDecodeTextureUnitBindingPname(pname, textureBindingTarget)) {
            *params = QueryTextureBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit(), textureBindingTarget);
            return;
        }

        switch (pname) {
        case GL_ACTIVE_TEXTURE:
            *params = MG_State::pGLContext->GetActiveTextureUnit() + GL_TEXTURE0;
            return;
        case GL_ARRAY_BUFFER_BINDING: {
            auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject();
            if (obj)
                *params = (GLint)obj->GetExternalIndex();
            else
                *params = 0;
            return;
        }
        // GL_TEXTURE_BUFFER_BINDING and GL_TEXTURE_BUFFER are the same token (0x8C2A): as a
        // glGetIntegerv pname it asks which BUFFER object is bound to the buffer-texture target,
        // not which texture is (that one is GL_TEXTURE_BINDING_BUFFER, handled by the texture-unit
        // decoder above).
        case GL_TEXTURE_BUFFER_BINDING: {
            auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Texture).GetBoundObject();
            *params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
            return;
        }
        case GL_BLEND:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE;
            return;
        case GL_BLEND_COLOR: {
            const FloatVec4& blendColor = MG_State::pGLContext->GetBlendColor();
            params[0] = static_cast<GLint>(blendColor.x());
            params[1] = static_cast<GLint>(blendColor.y());
            params[2] = static_cast<GLint>(blendColor.z());
            params[3] = static_cast<GLint>(blendColor.w());
            return;
        }
        case GL_BLEND_DST_ALPHA: {
            BlendFactor srcRGB, dstRGB, srcAlpha, dstAlpha;
            MG_State::pGLContext->GetBlendFunc(srcRGB, dstRGB, srcAlpha, dstAlpha);
            *params = static_cast<GLint>(MG_Util::ConvertBlendFactorToGLEnum(dstAlpha));
            return;
        }
        case GL_BLEND_DST_RGB: {
            BlendFactor srcRGB, dstRGB, srcAlpha, dstAlpha;
            MG_State::pGLContext->GetBlendFunc(srcRGB, dstRGB, srcAlpha, dstAlpha);
            *params = static_cast<GLint>(MG_Util::ConvertBlendFactorToGLEnum(dstRGB));
            return;
        }
        case GL_BLEND_EQUATION_RGB: {
            BlendEquation colorEquation = BlendEquation::Add;
            BlendEquation alphaEquation = BlendEquation::Add;
            MG_State::pGLContext->GetBlendEquation(colorEquation, alphaEquation);
            *params = static_cast<GLint>(MG_Util::ConvertBlendEquationToGLEnum(colorEquation));
            return;
        }
        case GL_BLEND_EQUATION_ALPHA: {
            BlendEquation colorEquation = BlendEquation::Add;
            BlendEquation alphaEquation = BlendEquation::Add;
            MG_State::pGLContext->GetBlendEquation(colorEquation, alphaEquation);
            *params = static_cast<GLint>(MG_Util::ConvertBlendEquationToGLEnum(alphaEquation));
            return;
        }
        case GL_BLEND_SRC_ALPHA: {
            BlendFactor srcRGB, dstRGB, srcAlpha, dstAlpha;
            MG_State::pGLContext->GetBlendFunc(srcRGB, dstRGB, srcAlpha, dstAlpha);
            *params = static_cast<GLint>(MG_Util::ConvertBlendFactorToGLEnum(srcAlpha));
            return;
        }
        case GL_BLEND_SRC_RGB: {
            BlendFactor srcRGB, dstRGB, srcAlpha, dstAlpha;
            MG_State::pGLContext->GetBlendFunc(srcRGB, dstRGB, srcAlpha, dstAlpha);
            *params = static_cast<GLint>(MG_Util::ConvertBlendFactorToGLEnum(srcRGB));
            return;
        }
        case GL_CLAMP_READ_COLOR:
            // Tri-state enum (GL_TRUE / GL_FALSE / GL_FIXED_ONLY). glGetIntegerv returns the raw
            // enum; GetFloatv/GetDoublev widen it and GetBooleanv converts nonzero to GL_TRUE, so
            // this single case serves every getter flavor.
            *params = static_cast<GLint>(MG_State::pGLContext->GetClampReadColor());
            return;
        // glClipControl's two state variables (GL 4.5 core table 23.7). They answer from the
        // state the entry point records, which is what the conformance suite's initial-value and
        // set-then-get cases read - the RASTERIZATION half of clip control is a separate,
        // backend-side question and does not gate the query.
        case GL_CLIP_ORIGIN:
            *params = static_cast<GLint>(MG_State::pGLContext->GetClipOrigin());
            return;
        case GL_CLIP_DEPTH_MODE:
            *params = static_cast<GLint>(MG_State::pGLContext->GetClipDepthMode());
            return;
        case GL_COLOR_CLEAR_VALUE: {
            const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
            params[0] = static_cast<GLint>(clearColor.x());
            params[1] = static_cast<GLint>(clearColor.y());
            params[2] = static_cast<GLint>(clearColor.z());
            params[3] = static_cast<GLint>(clearColor.w());
            return;
        }
        case GL_COLOR_LOGIC_OP:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) ? GL_TRUE : GL_FALSE;
            return;
        case GL_COLOR_WRITEMASK: {
            BoolVec4 mask = MG_State::pGLContext->GetColorMask();
            params[0] = mask.x() ? GL_TRUE : GL_FALSE;
            params[1] = mask.y() ? GL_TRUE : GL_FALSE;
            params[2] = mask.z() ? GL_TRUE : GL_FALSE;
            params[3] = mask.w() ? GL_TRUE : GL_FALSE;
            return;
        }
        case GL_COMPRESSED_TEXTURE_FORMATS:
            *params = 0; // compressed texture upload entrypoints are still unimplemented
            return;
        case GL_MAX_COMPUTE_UNIFORM_COMPONENTS:
            *params = kFrontendMaxComputeUniformComponents;
            return;
        case GL_MAX_COMPUTE_ATOMIC_COUNTERS:
            *params = kFrontendMaxComputeAtomicCounters;
            return;
        case GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS:
            *params = kFrontendMaxComputeAtomicCounterBuffers;
            return;
        case GL_MAX_COMPUTE_SHARED_MEMORY_SIZE:
            *params = kFrontendMaxComputeSharedMemorySize;
            return;
        case GL_DISPATCH_INDIRECT_BUFFER_BINDING: {
            auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
            *params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
            return;
        }
        case GL_DRAW_INDIRECT_BUFFER_BINDING: {
            auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
            *params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
            return;
        }
        case GL_MAX_SHADER_COMPILER_THREADS_KHR:
            // GL_KHR_parallel_shader_compile (GL_MAX_SHADER_COMPILER_THREADS_ARB is the same
            // 0x91B0). The number of threads MobileGL's compile pool would actually use, so
            // an application sizing its own submission batches gets a real answer.
            //
            // Zero when asynchronous compilation is off, which is the honest reply and the
            // one the extension defines for an implementation with no compiler threads: the
            // extension string is withdrawn in that configuration too, so a conforming
            // application never reaches this query, and one that asks anyway is told there
            // are none rather than being handed a thread count nothing will use.
            *params = MG_Util::Async::AsyncShaderCompileEnabled()
                          ? static_cast<GLint>(MG_Util::Async::ShaderCompilePool::Get().GetThreadCount())
                          : 0;
            return;
        case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
            // KHR_debug floors this at 64. It must agree with what GL_Debug.cpp actually enforces,
            // or an application that nests to the reported limit would take a STACK_OVERFLOW.
            *params = kFrontendMaxDebugGroupStackDepth;
            return;
        case GL_MAX_DEBUG_MESSAGE_LENGTH:
            *params = 1024; // agrees with GL_Debug.cpp's kMaxDebugMessageLength
            return;
        case GL_MAX_DEBUG_LOGGED_MESSAGES:
            // Size of the message log ring; KHR_debug requires at least 1.
            *params = kFrontendMaxDebugLoggedMessages;
            return;
        case GL_DEBUG_GROUP_STACK_DEPTH:
            // The live depth, which is never 0: GL 4.6 core 20.6 creates the context with one
            // group already on the stack, and that is the one glPopDebugGroup may not pop.
            *params = GetDebugGroupStackDepth();
            return;
        case GL_CONTEXT_FLAGS: {
            *params = MG_State::pEGLContext ? MG_State::pEGLContext->GetCurrentContextFlags() : 0;
            return;
        }
        case GL_CULL_FACE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace) ? GL_TRUE : GL_FALSE;
            return;
        case GL_CULL_FACE_MODE:
            *params = static_cast<GLint>(MG_Util::ConvertCullFaceModeToGLEnum(MG_State::pGLContext->GetCullFaceMode()));
            return;
        case GL_FRONT_FACE:
            *params = static_cast<GLint>(MG_Util::ConvertFrontFaceModeToGLEnum(MG_State::pGLContext->GetFrontFaceMode()));
            return;
        case GL_CURRENT_PROGRAM: {
            const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
            *params = currentProgram ? (GLint)currentProgram->GetExternalIndex() : 0;
            return;
        }
        case GL_DEPTH_CLEAR_VALUE:
            *params = (GLint)MG_State::pGLContext->GetClearDepth();
            return;
        case GL_DEPTH_FUNC:
            *params = (GLint)MG_Util::ConvertDepthTestFuncToGLEnum(MG_State::pGLContext->GetDepthFunc());
            return;
        case GL_DEPTH_RANGE: {
            const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRange();
            params[0] = static_cast<GLint>(depthRange.x());
            params[1] = static_cast<GLint>(depthRange.y());
            return;
        }
        case GL_DEPTH_TEST:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ? GL_TRUE : GL_FALSE;
            return;
        case GL_DEPTH_WRITEMASK:
            *params = MG_State::pGLContext->GetDepthMask() ? GL_TRUE : GL_FALSE;
            return;
        case GL_DEBUG_OUTPUT:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DebugOutput) ? GL_TRUE : GL_FALSE;
            return;
        case GL_DEBUG_OUTPUT_SYNCHRONOUS:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DebugOutputSynchronous)
                          ? GL_TRUE
                          : GL_FALSE;
            return;
        case GL_DITHER:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Dither) ? GL_TRUE : GL_FALSE;
            return;
        case GL_DOUBLEBUFFER: {
            if (!MG_State::pEGLContext) {
                *params = 0;
                return;
            }
            const auto currentDrawSurface = MG_State::pEGLContext->GetCurrentSurface(EGL_DRAW);
            *params = MG_State::pEGLContext->IsDoubleBufferedSurface(currentDrawSurface) ? GL_TRUE : GL_FALSE;
            return;
        }
        case GL_DRAW_BUFFER:
        case GL_DRAW_BUFFER0:
        case GL_DRAW_BUFFER1:
        case GL_DRAW_BUFFER2:
        case GL_DRAW_BUFFER3:
        case GL_DRAW_BUFFER4:
        case GL_DRAW_BUFFER5:
        case GL_DRAW_BUFFER6:
        case GL_DRAW_BUFFER7:
        case GL_DRAW_BUFFER8:
        case GL_DRAW_BUFFER9:
        case GL_DRAW_BUFFER10:
        case GL_DRAW_BUFFER11:
        case GL_DRAW_BUFFER12:
        case GL_DRAW_BUFFER13:
        case GL_DRAW_BUFFER14:
        case GL_DRAW_BUFFER15:
            if (const auto& fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw)
                                      .GetBoundObject()) {
                SizeT drawBufferIndex = 0;
                const bool decoded = TryDecodeDrawBufferQuery(pname, drawBufferIndex);
                MOBILEGL_ASSERT(decoded, "Draw buffer query enum should have been decoded already: 0x%X", pname);
                if (drawBufferIndex < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
                    *params = static_cast<GLint>(
                        MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(fbo->GetDrawBuffers()[drawBufferIndex]));
                } else {
                    *params = GL_NONE;
                }
            } else {
                *params = 0;
            }
            return;
        case GL_DRAW_FRAMEBUFFER_BINDING: {
            const auto& FBO = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
            *params = FBO ? (GLint)FBO->GetExternalIndex() : 0;
            return;
        }
        case GL_READ_FRAMEBUFFER_BINDING: {
            const auto& FBO = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
            *params = FBO ? (GLint)FBO->GetExternalIndex() : 0;
            return;
        }
        case GL_ELEMENT_ARRAY_BUFFER_BINDING: {
            if (!MG_State::pGLContext->GetBoundVertexArray()) {
                *params = 0;
                return;
            }
            const auto& bufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Index).GetBoundObject();
            *params = bufferObject ? (GLint)bufferObject->GetExternalIndex() : 0;
            return;
        }
        case GL_FRAGMENT_SHADER_DERIVATIVE_HINT:
            *params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
            return;
        case GL_IMPLEMENTATION_COLOR_READ_FORMAT: {
            GLint format = 0;
            GLint type = 0;
            *params = TryResolveImplementationColorReadParams(format, type) ? format : 0;
            return;
        }
        case GL_IMPLEMENTATION_COLOR_READ_TYPE: {
            GLint format = 0;
            GLint type = 0;
            *params = TryResolveImplementationColorReadParams(format, type) ? type : 0;
            return;
        }
        case GL_LINE_SMOOTH:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::LineSmooth) ? GL_TRUE : GL_FALSE;
            return;
        case GL_LINE_SMOOTH_HINT:
            *params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
            return;
        case GL_LINE_WIDTH:
            *params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
            return;
        case GL_LOGIC_OP_MODE:
            *params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
            return;
        case GL_MAX_COMBINED_ATOMIC_COUNTERS:
            *params = kFrontendMaxCombinedAtomicCounters;
            return;
        case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
            *params = kFrontendMaxCombinedAtomicCounterBuffers;
            return;
        case GL_MAX_COMBINED_UNIFORM_BLOCKS:
            *params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
            return;
        case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS:
            *params = 1; // TODO
            return;
        case GL_MAX_ELEMENTS_INDICES:
            *params = 1024 * 1024; // TODO
            return;
        case GL_MAX_ELEMENTS_VERTICES:
            *params = 1024 * 1024; // TODO
            return;
        case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
            *params = kFrontendMaxFragmentAtomicCounters;
            return;
        case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
            *params = kFrontendMaxFragmentAtomicCounterBuffers;
            return;
        case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
            *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
            return;
        case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
            *params = kFrontendMaxFragmentInputComponents;
            return;
        case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
            *params = MG_Backend::pActiveBackendObject
                          ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms
                          : MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms;
            return;
        case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
            *params = kFrontendMaxFragmentUniformComponents;
            return;
        case GL_MAX_FRAGMENT_UNIFORM_VECTORS:
            *params = kFrontendMaxFragmentUniformVectors;
            return;
        case GL_MAX_FRAGMENT_UNIFORM_BLOCKS:
            *params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks);
            return;
        case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
            *params = kFrontendMaxGeometryAtomicCounters;
            return;
        case GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS:
            *params = kFrontendMaxGeometryAtomicCounterBuffers;
            return;
        case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
            *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
            return;
        case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
            *params = kFrontendMaxGeometryInputComponents;
            return;
        case GL_MAX_GEOMETRY_OUTPUT_COMPONENTS:
            *params = kFrontendMaxGeometryOutputComponents;
            return;
        case GL_MAX_GEOMETRY_OUTPUT_VERTICES:
            *params = kFrontendMaxGeometryOutputVertices;
            return;
        case GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS:
            *params = kFrontendMaxGeometryTextureImageUnits;
            return;
        case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
            *params = MG_Backend::pActiveBackendObject
                          ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms
                          : MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms;
            return;
        case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
            *params = kFrontendMaxGeometryTotalOutputComponents;
            return;
        case GL_MAX_GEOMETRY_UNIFORM_BLOCKS:
            *params = ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks);
            return;
        case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
            *params = kFrontendMaxGeometryUniformComponents;
            return;
        case GL_MAX_GEOMETRY_SHADER_INVOCATIONS:
            *params = kFrontendMaxGeometryShaderInvocations;
            return;
        case GL_MAX_IMAGE_SAMPLES:
            *params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend
            return;
        case GL_MULTISAMPLE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
            return;
        case GL_MIN_MAP_BUFFER_ALIGNMENT:
            // The same constant the map paths align to (MG_State/GLState/BufferState/
            // PipeResource.h), never a literal: this number is a PROMISE about the pointers
            // glMapBuffer and glMapBufferRange return, and the two used to be unrelated - the
            // query said 64 while the pointers came out of a std::vector aligned to 16.
            *params = static_cast<GLint>(MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT);
            return;
        case GL_MAX_LABEL_LENGTH:
            *params = 256; // TODO
            return;
        case GL_MAX_PROGRAM_TEXEL_OFFSET:
            *params = kFrontendMaxProgramTexelOffset;
            return;
        case GL_MIN_PROGRAM_TEXEL_OFFSET:
            *params = kFrontendMinProgramTexelOffset;
            return;
        case GL_MAX_RECTANGLE_TEXTURE_SIZE:
            *params = 16 * 1024; // TODO
            return;
        case GL_MAX_SERVER_WAIT_TIMEOUT:
            *params = INT_MAX; // TODO
            return;
        case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
            *params = kFrontendMaxTessControlAtomicCounters;
            return;
        case GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS:
            *params = kFrontendMaxTessControlAtomicCounterBuffers;
            return;
        case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
            *params = kFrontendMaxTessEvaluationAtomicCounters;
            return;
        case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS:
            *params = kFrontendMaxTessEvaluationAtomicCounterBuffers;
            return;
        case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
            *params = 0;
            return;
        case GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS:
            *params = 0;
            return;
        case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
            *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
            return;
        case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
            *params =
                StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
            return;
        // The tessellation per-stage resource limits. Every one of these is ALSO a GLSL built-in
        // constant that BuildTBuiltInResource expands, and the two must report the same number
        // (KHR-GL45.limits.max_tess_* compares them directly) - which is why the values come from
        // the shared block in MG_Util/ShaderTranspiler/Types.h rather than from literals here.
        // They were the whole per-stage tess family: the table had been filled in only where the
        // honest answer was zero (the atomic counters, the image uniforms) or where a driver
        // query existed (GL_MAX_PATCH_VERTICES, GL_MAX_TESS_GEN_LEVEL), so every pname whose
        // answer is a real resource count fell through to GL_INVALID_ENUM.
        case GL_MAX_TESS_CONTROL_INPUT_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_INPUT_COMPONENTS);
            return;
        case GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_OUTPUT_COMPONENTS);
            return;
        case GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS);
            return;
        case GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS);
            return;
        case GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS);
            return;
        case GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_INPUT_COMPONENTS);
            return;
        case GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_OUTPUT_COMPONENTS);
            return;
        case GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS);
            return;
        case GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS);
            return;
        case GL_MAX_TESS_PATCH_COMPONENTS:
            *params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_PATCH_COMPONENTS);
            return;
        // Routed through the same clamp as every other per-stage block count so the
        // MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= per-stage ordering of
        // GL 4.6 table 23.64 cannot be broken by the two families moving independently.
        case GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS:
            *params = ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks);
            return;
        case GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS:
            *params = ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks);
            return;
        case GL_MAX_SUBROUTINES:
            *params = kFrontendMaxSubroutines;
            return;
        case GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS:
            *params = kFrontendMaxSubroutineUniformLocations;
            return;
        case GL_MAX_TEXTURE_LOD_BIAS:
            *params = 15; // TODO
            return;
        case GL_MAX_UNIFORM_LOCATIONS:
            // The same constant the link's location allocator enforces - see ProgramObject.
            *params = MG_State::GLState::ProgramObject::MAX_UNIFORM_LOCATIONS;
            return;
        case GL_MAX_VARYING_COMPONENTS:
            *params = kFrontendMaxVaryingComponents;
            return;
        case GL_MAX_VARYING_VECTORS:
            *params = kFrontendMaxVaryingVectors;
            return;
        case GL_MAX_VERTEX_ATOMIC_COUNTERS:
            *params = kFrontendMaxVertexAtomicCounters;
            return;
        case GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS:
            *params = kFrontendMaxVertexAtomicCounterBuffers;
            return;
        case GL_MAX_VERTEX_IMAGE_UNIFORMS:
            *params = MG_Backend::pActiveBackendObject
                          ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
                          : MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
            return;
        case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
            *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
            return;
        case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
            *params = kFrontendMaxVertexUniformComponents;
            return;
        case GL_MAX_VERTEX_UNIFORM_VECTORS:
            *params = kFrontendMaxVertexUniformVectors;
            return;
        case GL_MAX_VERTEX_OUTPUT_COMPONENTS:
            *params = kFrontendMaxVertexOutputComponents;
            return;
        case GL_MAX_VERTEX_UNIFORM_BLOCKS:
            *params = ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks);
            return;
        case GL_NUM_COMPRESSED_TEXTURE_FORMATS:
            *params = 0; // compressed texture upload entrypoints are still unimplemented
            return;
        case GL_NUM_PROGRAM_BINARY_FORMATS:
            *params = 0;
            return;
        // GL_ARB_spirv_extensions / GL 4.6 core 22.2. An implementation that advertises no
        // SPIR-V extension answers zero here, and glGetStringi(GL_SPIR_V_EXTENSIONS, i) is then
        // never legally called - MobileGL runs the module through its own translation pipeline
        // and relies on no SPIR-V extension to do it, so zero is the true answer rather than a
        // placeholder.
        case GL_NUM_SPIR_V_EXTENSIONS:
            *params = 0;
            return;
        // GL_ARB_gl_spirv, core since 4.6: exactly one shader binary format, and the pair has to
        // agree - an application sizes its GL_SHADER_BINARY_FORMATS array from the count.
        case GL_NUM_SHADER_BINARY_FORMATS:
            *params = 1;
            return;
        case GL_SHADER_BINARY_FORMATS:
            *params = static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V);
            return;
        case GL_PACK_ALIGNMENT:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment);
            return;
        case GL_PACK_IMAGE_HEIGHT:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackImageHeight);
            return;
        case GL_PACK_LSB_FIRST:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackLSBFirst);
            return;
        case GL_PACK_ROW_LENGTH:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackRowLength);
            return;
        case GL_PACK_SKIP_IMAGES:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackSkipImages);
            return;
        case GL_PACK_SKIP_PIXELS:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackSkipPixels);
            return;
        case GL_PACK_SKIP_ROWS:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackSkipRows);
            return;
        case GL_PACK_SWAP_BYTES:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackSwapBytes);
            return;
        case GL_PIXEL_PACK_BUFFER_BINDING:
            if (const auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject()) {
                *params = static_cast<GLint>(obj->GetExternalIndex());
            } else {
                *params = 0;
            }
            return;
        case GL_PIXEL_UNPACK_BUFFER_BINDING:
            if (const auto& obj =
                    MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject()) {
                *params = static_cast<GLint>(obj->GetExternalIndex());
            } else {
                *params = 0;
            }
            return;
        case GL_PARAMETER_BUFFER_BINDING_ARB: {
            auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
            *params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
            return;
        }
        case GL_POINT_FADE_THRESHOLD_SIZE:
            *params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetPointFadeThresholdSize()));
            return;
        case GL_POINT_SPRITE_COORD_ORIGIN:
            *params = static_cast<GLint>(MG_State::pGLContext->GetPointSpriteCoordOrigin());
            return;
        case GL_PRIMITIVE_RESTART:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ? GL_TRUE
                                                                                                    : GL_FALSE;
            return;
        case GL_PRIMITIVE_RESTART_INDEX:
            *params = static_cast<GLint>(MG_State::pGLContext->GetPrimitiveRestartIndex());
            return;
        case GL_POLYGON_OFFSET_CLAMP:
            // Float state (see GetFloatv); rounded to nearest for the integer query per GL 4.6
            // core 22.1's float-to-integer rule.
            *params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetPolygonOffsetClamp()));
            return;
        case GL_PROGRAM_BINARY_FORMATS:
            *params = 0; // program-binary entrypoints are stubbed
            return;
        case GL_PROGRAM_PIPELINE_BINDING:
            *params = static_cast<GLint>(MG_State::pGLContext->GetBoundProgramPipelineName());
            return;
        case GL_PROGRAM_POINT_SIZE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ProgramPointSize) ? GL_TRUE : GL_FALSE;
            return;
        case GL_PROVOKING_VERTEX:
            *params = static_cast<GLint>(
                MG_Util::ConvertProvokingVertexModeToGLEnum(MG_State::pGLContext->GetProvokingVertexMode()));
            return;
        case GL_POINT_SIZE:
            *params = static_cast<GLint>(MG_State::pGLContext->GetPointSize());
            return;
        case GL_POLYGON_MODE:
            params[0] = static_cast<GLint>(MG_State::pGLContext->GetPolygonModeFront());
            params[1] = static_cast<GLint>(MG_State::pGLContext->GetPolygonModeBack());
            return;
        case GL_POLYGON_OFFSET_FACTOR:
            *params = static_cast<GLint>(MG_State::pGLContext->GetPolygonOffsetFactor());
            return;
        case GL_POLYGON_OFFSET_UNITS:
            *params = static_cast<GLint>(MG_State::pGLContext->GetPolygonOffsetUnits());
            return;
        case GL_POLYGON_OFFSET_FILL:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) ? GL_TRUE : GL_FALSE;
            return;
        case GL_POLYGON_OFFSET_LINE:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetLine) ? GL_TRUE : GL_FALSE;
            return;
        case GL_POLYGON_OFFSET_POINT:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetPoint) ? GL_TRUE : GL_FALSE;
            return;
        case GL_POLYGON_SMOOTH:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonSmooth) ? GL_TRUE : GL_FALSE;
            return;
        case GL_POLYGON_SMOOTH_HINT:
            *params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
            return;
        case GL_READ_BUFFER:
            if (const auto& fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read)
                                      .GetBoundObject()) {
                *params =
                    static_cast<GLint>(MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(fbo->GetReadBuffer()));
            } else {
                *params = 0;
            }
            return;
        case GL_RENDERBUFFER_BINDING:
            if (const auto& obj =
                    MG_State::pGLContext->GetRenderbufferBindingSlot(RenderbufferTarget::Renderbuffer).GetBoundObject()) {
                *params = static_cast<GLint>(obj->GetExternalIndex());
            } else {
                *params = 0;
            }
            return;
        case GL_SAMPLE_BUFFERS:
            *params = ResolveDrawFramebufferSampleCount() > 0 ? 1 : 0;
            return;
        case GL_SAMPLE_COVERAGE_VALUE:
            *params = static_cast<GLint>(MG_State::pGLContext->GetSampleCoverageValue());
            return;
        case GL_SAMPLE_COVERAGE_INVERT:
            *params = MG_State::pGLContext->GetSampleCoverageInvert() ? GL_TRUE : GL_FALSE;
            return;
        case GL_SAMPLE_ALPHA_TO_COVERAGE:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleAlphaToCoverage) ? GL_TRUE : GL_FALSE;
            return;
        case GL_SAMPLE_ALPHA_TO_ONE:
            *params =
                MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleAlphaToOne) ? GL_TRUE : GL_FALSE;
            return;
        case GL_SAMPLE_COVERAGE:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleCoverage) ? GL_TRUE
                                                                                                  : GL_FALSE;
            return;
        case GL_SAMPLE_MASK:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE;
            return;
        case GL_SAMPLE_SHADING:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading) ? GL_TRUE : GL_FALSE;
            return;
        case GL_MIN_SAMPLE_SHADING_VALUE:
            // GL 4.6 core 22.2: a floating-point value queried as an integer rounds to nearest.
            *params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetMinSampleShadingValue()));
            return;
        case GL_SAMPLE_MASK_VALUE:
            *params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
            return;
        case GL_SAMPLER_BINDING:
            *params = QuerySamplerBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit());
            return;
        case GL_SAMPLES:
            *params = ResolveDrawFramebufferSampleCount();
            return;
        case GL_SCISSOR_BOX: {
            const IntVec4& scissorBox = MG_State::pGLContext->GetScissorBox();
            params[0] = scissorBox.x();
            params[1] = scissorBox.y();
            params[2] = scissorBox.z();
            params[3] = scissorBox.w();
            return;
        }
        case GL_SCISSOR_TEST:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest) ? GL_TRUE : GL_FALSE;
            return;
        case GL_SHADER_COMPILER:
            *params = GL_TRUE;
            return;
        case GL_SHADER_STORAGE_BUFFER_BINDING: {
            auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::ShaderStorage).GetBoundObject();
            *params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
            return;
        }
        case GL_SHADER_STORAGE_BUFFER_START:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_SHADER_STORAGE_BUFFER_SIZE:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_STENCIL_BACK_FAIL:
            *params = static_cast<GLint>(
                MG_Util::ConvertStencilOperationToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Back).FailOp));
            return;
        case GL_STENCIL_BACK_FUNC:
            *params = static_cast<GLint>(
                MG_Util::ConvertDepthTestFuncToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Back).Func));
            return;
        case GL_STENCIL_BACK_PASS_DEPTH_FAIL:
            *params = static_cast<GLint>(
                MG_Util::ConvertStencilOperationToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Back).PassDepthFailOp));
            return;
        case GL_STENCIL_BACK_PASS_DEPTH_PASS:
            *params = static_cast<GLint>(
                MG_Util::ConvertStencilOperationToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Back).PassDepthPassOp));
            return;
        case GL_STENCIL_BACK_REF:
            *params = MG_State::pGLContext->GetStencilState(StencilFace::Back).Ref;
            return;
        case GL_STENCIL_BACK_VALUE_MASK:
            *params = static_cast<GLint>(MG_State::pGLContext->GetStencilState(StencilFace::Back).ValueMask);
            return;
        case GL_STENCIL_BACK_WRITEMASK:
            *params = static_cast<GLint>(MG_State::pGLContext->GetStencilState(StencilFace::Back).WriteMask);
            return;
        case GL_STENCIL_CLEAR_VALUE:
            *params = static_cast<GLint>(MG_State::pGLContext->GetClearStencil());
            return;
        case GL_STENCIL_FAIL:
            *params = static_cast<GLint>(
                MG_Util::ConvertStencilOperationToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Front).FailOp));
            return;
        case GL_STENCIL_FUNC:
            *params = static_cast<GLint>(
                MG_Util::ConvertDepthTestFuncToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Front).Func));
            return;
        case GL_STENCIL_PASS_DEPTH_FAIL:
            *params = static_cast<GLint>(
                MG_Util::ConvertStencilOperationToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Front).PassDepthFailOp));
            return;
        case GL_STENCIL_PASS_DEPTH_PASS:
            *params = static_cast<GLint>(
                MG_Util::ConvertStencilOperationToGLEnum(
                    MG_State::pGLContext->GetStencilState(StencilFace::Front).PassDepthPassOp));
            return;
        case GL_STENCIL_REF:
            *params = MG_State::pGLContext->GetStencilState(StencilFace::Front).Ref;
            return;
        case GL_STENCIL_TEST:
            *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
            return;
        case GL_STENCIL_VALUE_MASK:
            *params = static_cast<GLint>(MG_State::pGLContext->GetStencilState(StencilFace::Front).ValueMask);
            return;
        case GL_STENCIL_WRITEMASK:
            *params = static_cast<GLint>(MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask);
            return;
        case GL_STEREO:
            *params = 0; // stereo surfaces are not exposed
            return;
        case GL_TEXTURE_COMPRESSION_HINT:
            *params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
            return;
        case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
            *params = MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment;
            return;
        case GL_TIMESTAMP: {
            Int64 timestamp = 0;
            if (!MG_Config::Features.DisableTimerQuery) {
                if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
                    timestamp = getGpuTimestampNs();
                }
            }
            // 32-bit query: clamp per the GL state-query conversion rules.
            *params = timestamp > static_cast<Int64>(INT_MAX) ? INT_MAX : static_cast<GLint>(timestamp);
            return;
        }
        case GL_TRANSFORM_FEEDBACK_BUFFER_BINDING:
            if (const auto& obj =
                    MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::TransformFeedback).GetBoundObject()) {
                *params = static_cast<GLint>(obj->GetExternalIndex());
            } else {
                *params = 0;
            }
            return;
        case GL_TRANSFORM_FEEDBACK_BUFFER_START:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_TRANSFORM_FEEDBACK_BUFFER_SIZE:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_UNIFORM_BUFFER_BINDING:
            if (const auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform).GetBoundObject()) {
                *params = static_cast<GLint>(obj->GetExternalIndex());
            } else {
                *params = 0;
            }
            return;
        case GL_UNIFORM_BUFFER_SIZE:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_UNIFORM_BUFFER_START:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        // glBindBufferBase/Range set the GENERIC binding point too (GL 4.6 core 6.1.1), and this
        // is the one indexed-buffer family whose non-indexed query was never answered - so it
        // fell through to INVALID_ENUM and left the caller's variable holding whatever was in its
        // stack slot. _START/_SIZE stay indexed-only, exactly like their uniform-buffer siblings.
        case GL_ATOMIC_COUNTER_BUFFER_BINDING:
            if (const auto& obj =
                    MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::AtomicCounter).GetBoundObject()) {
                *params = static_cast<GLint>(obj->GetExternalIndex());
            } else {
                *params = 0;
            }
            return;
        case GL_ATOMIC_COUNTER_BUFFER_START:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_ATOMIC_COUNTER_BUFFER_SIZE:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_UNPACK_ALIGNMENT:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
            return;
        case GL_UNPACK_IMAGE_HEIGHT:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackImageHeight);
            return;
        case GL_UNPACK_LSB_FIRST:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackLSBFirst);
            return;
        case GL_UNPACK_ROW_LENGTH:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackRowLength);
            return;
        case GL_UNPACK_SKIP_IMAGES:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackSkipImages);
            return;
        case GL_UNPACK_SKIP_PIXELS:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackSkipPixels);
            return;
        case GL_UNPACK_SKIP_ROWS:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackSkipRows);
            return;
        case GL_UNPACK_SWAP_BYTES:
            *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackSwapBytes);
            return;
        case GL_VERTEX_ARRAY_BINDING: {
            const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
            *params = vao ? static_cast<GLint>(vao->GetExternalIndex()) : 0;
            return;
        }
        // The vertex buffer binding points are per-binding-index state, so the non-indexed getter
        // has nothing to answer with (GL 4.6 core table 23.4).
        case GL_VERTEX_BINDING_BUFFER:
        case GL_VERTEX_BINDING_DIVISOR:
        case GL_VERTEX_BINDING_OFFSET:
        case GL_VERTEX_BINDING_STRIDE:
            RecordIndexedOnlyGetterError(__func__, pname);
            return;
        case GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET:
            *params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribRelativeOffset());
            return;
        case GL_MAX_VERTEX_ATTRIB_BINDINGS:
            *params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribBindings());
            return;
        case GL_MAX_VERTEX_ATTRIB_STRIDE:
            *params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribStride());
            return;
        case GL_VIEWPORT: {
            const auto& vp = MG_State::pGLContext->GetViewport();
            params[0] = vp.x();
            params[1] = vp.y();
            params[2] = vp.z();
            params[3] = vp.w();
            return;
        }
        case GL_MAX_ELEMENT_INDEX:
            // 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
            // state-query conversion rules - the same shape GL_MAX_SHADER_STORAGE_BLOCK_SIZE
            // uses. The real answer is 2^32-1 because both backends draw with GL_UNSIGNED_INT
            // indices and neither bounds an index value; the old `1024 * 1024` was a placeholder
            // that no draw path ever consulted.
            *params = INT32_MAX;
            return;
        case GL_CONTEXT_PROFILE_MASK:
            // Reports the requested context profile (EGL defaults 3.x contexts to core);
            // MOBILEGL_RELAXED_SEMANTICS loosens behavior without changing the identity.
            *params = MG_State::pEGLContext && MG_State::pEGLContext->IsCurrentContextOpenGLCompatibilityProfile()
                          ? GL_CONTEXT_COMPATIBILITY_PROFILE_BIT
                          : GL_CONTEXT_CORE_PROFILE_BIT;
            return;
        default:
            break;
        }

        const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
        if (!activeBackendObject) {
            MGLOG_E_ONCE("activeBackendObject is not initialized!");
            return;
        }
        const auto& rendererInfo = activeBackendObject->GetRendererInfo();
        const auto& dynamicParameters = activeBackendObject->GetDynamicParameters();

        switch (pname) {
        case GL_ALIASED_LINE_WIDTH_RANGE:
            params[0] = static_cast<GLint>(dynamicParameters.AliasedLineWidthRangeMin);
            params[1] = static_cast<GLint>(dynamicParameters.AliasedLineWidthRangeMax);
            break;
        case GL_ALIASED_POINT_SIZE_RANGE:
        case GL_POINT_SIZE_RANGE:
            params[0] = static_cast<GLint>(dynamicParameters.PointSizeRangeMin);
            params[1] = static_cast<GLint>(dynamicParameters.PointSizeRangeMax);
            break;
        case GL_SUBGROUP_SIZE_KHR:
            *params = static_cast<GLint>(dynamicParameters.SubgroupSize);
            break;
        case GL_SUBGROUP_SUPPORTED_STAGES_KHR:
            *params = static_cast<GLint>(dynamicParameters.SubgroupSupportedStages);
            break;
        case GL_SUBGROUP_SUPPORTED_FEATURES_KHR:
            *params = static_cast<GLint>(dynamicParameters.SubgroupSupportedFeatures);
            break;
        case GL_SUBGROUP_QUAD_ALL_STAGES_KHR:
            *params = dynamicParameters.SubgroupQuadOperationsInAllStages ? GL_TRUE : GL_FALSE;
            break;
        case GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS:
            *params = ClampStorageBlockCount(dynamicParameters.MaxComputeShaderStorageBlocks);
            break;
        case GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS:
            *params = ClampStorageBlockCount(dynamicParameters.MaxCombinedShaderStorageBlocks);
            break;
        case GL_MAX_COMPUTE_UNIFORM_BLOCKS:
            *params = ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks);
            break;
        case GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS:
            *params = dynamicParameters.MaxComputeTextureImageUnits;
            break;
        case GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS:
            // The CLAMPED block count, i.e. exactly what GL_MAX_COMPUTE_UNIFORM_BLOCKS answers.
            // GL 4.6 table 23.64 defines this as the components reachable through the blocks a
            // stage may declare, so deriving it from the raw backend number described 256 blocks
            // an application is only ever allowed 84 of.
            *params = GetMaxCombinedUniformComponents(kFrontendMaxComputeUniformComponents,
                                                      ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks),
                                                      dynamicParameters.MaxUniformBlockSize);
            break;
        case GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS:
            *params = std::max(dynamicParameters.MaxComputeWorkGroupInvocations,
                               kFrontendMaxComputeWorkGroupInvocations);
            break;
        case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
            GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &params[0]);
            GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1, &params[1]);
            GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2, &params[2]);
            break;
        case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
            GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 0, &params[0]);
            GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1, &params[1]);
            GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 2, &params[2]);
            break;
        case GL_MAJOR_VERSION:
            *params = rendererInfo.RendererGLInfo.TargetGLVersion.Major;
            break;
        case GL_MAX_3D_TEXTURE_SIZE:
            *params = dynamicParameters.Max3DTextureSize;
            break;
        case GL_MAX_ARRAY_TEXTURE_LAYERS:
            *params = dynamicParameters.MaxArrayTextureLayers;
            break;
        case GL_MAX_CLIP_DISTANCES:
            *params = dynamicParameters.MaxClipDistances;
            break;
        // Both were a hard-coded GL_LAST_VERTEX_CONVENTION, derived from nothing. GL 4.6 table
        // 23.65 permits GL_UNDEFINED_VERTEX for either, and that is what the backends report
        // wherever they do not actually pin a convention - claiming one is a statement about
        // which vertex of a primitive supplies gl_Layer / gl_ViewportIndex, and DirectGLES
        // rasterizes only viewport 0 on a driver without GL_OES_viewport_array while
        // DirectVulkan picks its provoking mode per pipeline. KHR-GLxx.viewport_array.query
        // accepts all four values, and .provoking_vertex - which failed on both devices, in
        // OPPOSITE directions - stops verifying as soon as either answer is undefined.
        case GL_LAYER_PROVOKING_VERTEX:
            *params = static_cast<GLint>(dynamicParameters.LayerProvokingVertex);
            break;
        case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
            *params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
            break;
        case GL_MAX_COLOR_TEXTURE_SAMPLES:
            *params = GetAdvertisedColorTextureMaxSamples();
            break;
        case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
            *params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
                                                      ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks),
                                                      dynamicParameters.MaxUniformBlockSize);
            break;
        case GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS:
            *params = GetMaxCombinedUniformComponents(kFrontendMaxGeometryUniformComponents,
                                                      ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks),
                                                      dynamicParameters.MaxUniformBlockSize);
            break;
        case GL_MAX_GEOMETRY_OUTPUT_VERTICES:
            *params = kFrontendMaxGeometryOutputVertices;
            break;
        case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
            *params = kFrontendMaxGeometryTotalOutputComponents;
            break;
        case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS:
            *params = dynamicParameters.MaxCombinedTextureImageUnits;
            break;
        case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:
            *params = GetMaxCombinedUniformComponents(kFrontendMaxVertexUniformComponents,
                                                      ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks),
                                                      dynamicParameters.MaxUniformBlockSize);
            break;
        case GL_MAX_CUBE_MAP_TEXTURE_SIZE:
            *params = dynamicParameters.MaxCubeMapTextureSize;
            break;
        case GL_MAX_DEPTH_TEXTURE_SAMPLES:
            *params = GetAdvertisedDepthTextureMaxSamples();
            break;
        case GL_MAX_FRAMEBUFFER_WIDTH:
            *params = dynamicParameters.MaxFramebufferWidth;
            break;
        case GL_MAX_FRAMEBUFFER_HEIGHT:
            *params = dynamicParameters.MaxFramebufferHeight;
            break;
        case GL_MAX_FRAMEBUFFER_LAYERS:
            *params = dynamicParameters.MaxFramebufferLayers;
            break;
        case GL_MAX_FRAMEBUFFER_SAMPLES:
            *params = dynamicParameters.MaxFramebufferSamples;
            break;
        case GL_MAX_IMAGE_UNITS:
            *params = dynamicParameters.MaxImageUnits;
            break;
        case GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS:
            *params = dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
            break;
        case GL_MAX_COMBINED_IMAGE_UNIFORMS:
            *params = dynamicParameters.MaxCombinedImageUniforms;
            break;
        case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
            *params = dynamicParameters.MaxComputeImageUniforms;
            break;
        case GL_MAX_INTEGER_SAMPLES:
            *params = GetAdvertisedIntegerMaxSamples();
            break;
        case GL_MAX_RENDERBUFFER_SIZE:
            *params = dynamicParameters.MaxRenderbufferSize;
            break;
        case GL_MAX_SAMPLE_MASK_WORDS:
            *params = dynamicParameters.MaxSampleMaskWords;
            break;
        case GL_PATCH_VERTICES:
            *params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
            break;
        // Float state, so glGetIntegerv rounds it (GL 4.6 core 2.2.2) - the exact values come back
        // through glGetFloatv. Answered here so glGetBooleanv, which delegates to this getter for
        // everything its own switch does not handle, does not report INVALID_ENUM for them.
        case GL_PATCH_DEFAULT_OUTER_LEVEL: {
            const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
            for (Uint i = 0; i < 4; ++i) params[i] = static_cast<GLint>(std::lround(outer[i]));
            break;
        }
        case GL_PATCH_DEFAULT_INNER_LEVEL: {
            const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
            for (Uint i = 0; i < 2; ++i) params[i] = static_cast<GLint>(std::lround(inner[i]));
            break;
        }
        // GL 4.6 core table 23.66: whether the primitive-restart index terminates a patch.
        // GL_FALSE is a legal answer and the true one - neither backend cuts a patch short, and
        // the DirectVulkan draw path relies on this staying false (it resolves primitive restart
        // to "never" for a PATCH_LIST topology on the strength of it).
        case GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED:
            *params = GL_FALSE;
            break;
        case GL_MAX_PATCH_VERTICES:
            *params = dynamicParameters.MaxPatchVertices;
            break;
        case GL_MAX_TESS_GEN_LEVEL:
            *params = dynamicParameters.MaxTessGenLevel;
            break;
        // Same helper, and so the same arithmetic, as every other GL_MAX_COMBINED_*_UNIFORM_
        // COMPONENTS: default-block components + blocks * (block size / 4). It reproduces the
        // conformance suite's own formula exactly, so the two cannot drift.
        case GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS:
            *params = GetMaxCombinedUniformComponents(
                static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS),
                ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks), dynamicParameters.MaxUniformBlockSize);
            break;
        case GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS:
            *params = GetMaxCombinedUniformComponents(
                static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS),
                ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks), dynamicParameters.MaxUniformBlockSize);
            break;
        // ARB_cull_distance. Backend-derived exactly like GL_MAX_CLIP_DISTANCES beside it, and
        // for a stronger reason: a cull distance discards the whole primitive, so advertising
        // eight the rasterizer cannot serve turns every culling draw into a silent no-op. Zero is
        // the honest answer on a host with no cull-distance route, and the conformance suite then
        // skips the functional cases instead of failing them deep inside a pixel comparison.
        case GL_MAX_CULL_DISTANCES:
            *params = dynamicParameters.MaxCullDistances;
            break;
        case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES:
            *params = dynamicParameters.MaxCombinedClipAndCullDistances;
            break;
        case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
            *params = dynamicParameters.MinProgramTextureGatherOffset;
            break;
        case GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET:
            *params = dynamicParameters.MaxProgramTextureGatherOffset;
            break;
        case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
            *params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
            break;
        case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
            // 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
            // state-query conversion rules.
            *params = static_cast<GLint>(std::min<Uint64>(dynamicParameters.MaxShaderStorageBlockSize,
                                                          static_cast<Uint64>(INT32_MAX)));
            break;
        case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
            // NOT the frontend's binding-point array size: GetIndexedBufferQueryPointCount
            // clamps this family to the range a lowered counter block can actually be served
            // from, which is the same number glslang compiles a layout(binding = N) atomic_uint
            // against and the same one glBindBufferBase validates an index against.
            *params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
            break;
        case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
            // The conformance suite splits this evenly across every advertised binding point and
            // binds all of them in one glBindBuffersRange
            // (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide -
            // a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The
            // shared constant is 16384 over 8 binding points, which divides.
            *params = kFrontendMaxAtomicCounterBufferSize;
            break;
        case GL_MAX_TEXTURE_BUFFER_SIZE:
            *params = dynamicParameters.MaxTextureBufferSize;
            break;
        case GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS:
            *params = kFrontendMaxTransformFeedbackInterleavedComponents;
            break;
        case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS:
            *params = kFrontendMaxTransformFeedbackSeparateAttribs;
            break;
        case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
            *params = kFrontendMaxTransformFeedbackSeparateComponents;
            break;
        // ARB_transform_feedback3 limits. The GL CTS queries these before checking
        // whether the extension is advertised and requires no GL error; desktop
        // drivers all accept them, so answer with the separate-attrib capacity and
        // the single vertex stream the backends provide.
        case GL_MAX_TRANSFORM_FEEDBACK_BUFFERS:
            *params = kFrontendMaxTransformFeedbackSeparateAttribs;
            break;
        case GL_MAX_VERTEX_STREAMS:
            // ONE, which is under the GL 4.5 core table 23.62 minimum of four and is a known,
            // deliberate non-conformance. It was briefly raised to 4 on the theory that streams
            // 1..3 could exist and be permanently empty; measuring that decision refuted it.
            // Raising the limit un-gates two CTS cases per package across KHR-GL40..GL46 -
            // transform_feedback.draw_xfb_stream_test (which stops being skipped) and
            // transform_feedback3.multiple_streams (which stops reporting NotSupported) - and
            // both then fail, because nothing in the shader pipeline supports layout(stream = N),
            // EmitStreamVertex or EndStreamPrimitive, and because the query state machine tracks
            // one active query per TARGET rather than per (target, stream). That is 14 new
            // failures against 2 gained limits passes, and a 4 nothing can back is the
            // advertised-caps lie with the sign flipped.
            //
            // The real fix is the feature, not the number: per-stream capture needs
            // layout(stream = N) through the transpiler plus per-(target, stream) query slots,
            // which DirectVulkan could back with VK_EXT_transform_feedback's geometryStreams and
            // DirectGLES cannot back at all (ES has no vertex streams). Until that lands, one is
            // the honest count and every stream-addressing entry point bounds itself by THIS
            // query, so raising it later moves them all together.
            *params = kFrontendMaxVertexStreams;
            break;
        case GL_TRANSFORM_FEEDBACK_ACTIVE:
            *params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
            break;
        case GL_TRANSFORM_FEEDBACK_PAUSED:
            *params = MG_State::pGLContext->IsTransformFeedbackPaused() ? 1 : 0;
            break;
        case GL_TRANSFORM_FEEDBACK_BINDING:
            *params = static_cast<GLint>(MG_State::pGLContext->GetBoundTransformFeedbackName());
            break;
        case GL_MAX_TEXTURE_IMAGE_UNITS:
            *params = dynamicParameters.MaxTextureImageUnits;
            break;
        case GL_MAX_TEXTURE_SIZE:
            *params = dynamicParameters.MaxTextureSize;
            break;
        case GL_MAX_UNIFORM_BUFFER_BINDINGS: {
            // Never advertise more bindings than the state layer's indexed-binding array can track
            // (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
            // capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
            // advertised index and expects no error. The floor is the GL 4.5 core minimum, and
            // the array was widened to exactly it, so the two coincide by construction.
            //
            // WHY THE BACKEND'S OWN COUNT IS NOT THE CEILING HERE, unlike the shader-storage
            // family. A GL uniform binding point is where an APPLICATION parks a buffer; it is
            // not a driver binding point. Neither backend forwards it as one on the draw path:
            // DirectGLES rebinds the blocks a program declares onto COMPACTED ES points
            // (BindCurrentProgramWithResources maps block i to ES point i+1) and DirectVulkan
            // resolves each block to a descriptor. So what the host driver's count bounds is how
            // many blocks ONE PROGRAM may use, not how many points an application may bind.
            //
            // That per-program number is NOT GL_MAX_COMBINED_UNIFORM_BLOCKS (84, the six-stage
            // sum): no single program can reach it. A graphics program is bounded by the five
            // graphics stages' per-stage counts, 14 each, so 70 blocks plus the global UBO at ES
            // point 0 = 71 - inside the ES 3.2 minimum of 72. A compute program is bounded by
            // GL_MAX_COMPUTE_UNIFORM_BLOCKS, which on DirectGLES is the ES driver's own count
            // (GL-scale, ~14) and on DirectVulkan is served from descriptors with no ES binding
            // points involved. Raising any per-stage graphics count past 14 is what would break
            // this, so that is the edit to check against the ES ceiling - not this one.
            static_assert(static_cast<GLint>(MG_State::GLState::BufferBindingPointCount) >=
                              kFrontendMinUniformBufferBindings,
                          "the indexed-binding array must be able to hold every advertised uniform binding point");
            *params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
                                 static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
            break;
        }
        case GL_MAX_UNIFORM_BLOCK_SIZE:
            *params = dynamicParameters.MaxUniformBlockSize;
            break;
        case GL_MAX_VERTEX_ATTRIBS:
            // Single source of truth with the validators: the value reported here is exactly the bound
            // glVertexAttrib*/glGetVertexAttrib*/glBindAttribLocation enforce, and it never exceeds the
            // state layer's current-value storage capacity.
            *params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribs());
            break;
        case GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS:
            *params = dynamicParameters.MaxVertexTextureImageUnits;
            break;
        case GL_MAX_VIEWPORT_DIMS:
            params[0] = dynamicParameters.MaxViewportWidth;
            params[1] = dynamicParameters.MaxViewportHeight;
            break;
        case GL_MAX_VIEWPORTS:
            // The frontend's own state width, not the backend's device limit. GL 4.3 core
            // requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates
            // against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would
            // either advertise viewports the state cannot hold or reject indices it can. A
            // Vulkan device without the multiViewport feature reports maxViewports == 1, which
            // limits what can be RASTERIZED to more than one rectangle (see the multiViewport
            // gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps
            // carrying that device number for exactly that decision.
            *params = static_cast<GLint>(RenderStateParameters::MAX_VIEWPORTS);
            break;
        case GL_MINOR_VERSION:
            *params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
            break;
        case GL_NUM_EXTENSIONS:
            *params = static_cast<Int>(rendererInfo.RendererGLInfo.Extensions.size());
            break;
        case GL_POINT_SIZE_GRANULARITY:
            *params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
            break;
        case GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT:
            // The STORAGE alignment, which is its own limit - this used to answer with the
            // uniform one. They differ on real hardware (Adreno 830: 32 uniform, 64 storage), and
            // under-reporting it is silent: ValidateBindBufferRange accepts the offset, the ES
            // driver accepts it too without raising an error, and the shader's writes then land
            // at an address the application never bound.
            *params = static_cast<GLint>(dynamicParameters.ShaderStorageBufferOffsetAlignment);
            break;
        case GL_SMOOTH_LINE_WIDTH_RANGE:
            params[0] = static_cast<GLint>(dynamicParameters.SmoothLineWidthRangeMin);
            params[1] = static_cast<GLint>(dynamicParameters.SmoothLineWidthRangeMax);
            break;
        case GL_SMOOTH_LINE_WIDTH_GRANULARITY:
            *params = static_cast<GLint>(dynamicParameters.SmoothLineWidthGranularity);
            break;
        case GL_SUBPIXEL_BITS:
            *params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
            break;
        case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
            *params = static_cast<GLint>(std::lround(dynamicParameters.MinFragmentInterpolationOffset));
            break;
        case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
            *params = static_cast<GLint>(std::lround(dynamicParameters.MaxFragmentInterpolationOffset));
            break;
        case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
            *params = dynamicParameters.FragmentInterpolationOffsetBits;
            break;
        case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
            *params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
            break;
        case GL_VIEWPORT_BOUNDS_RANGE:
            params[0] = static_cast<GLint>(dynamicParameters.ViewportBoundsRangeMin);
            params[1] = static_cast<GLint>(dynamicParameters.ViewportBoundsRangeMax);
            break;
        case GL_VIEWPORT_SUBPIXEL_BITS:
            *params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
            break;
        case GL_MAX_COLOR_ATTACHMENTS:
        case GL_MAX_DRAW_BUFFERS:
            *params = pname == GL_MAX_COLOR_ATTACHMENTS ? dynamicParameters.MaxColorAttachments
                                                        : dynamicParameters.MaxDrawBuffers;
            break;
        case GL_MAX_SAMPLES:
            *params = GetAdvertisedMaxSamples();
            break;
        case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
            // Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
            *params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
            break;
        default:
            MGLOG_D("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
            MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
                                                                           std::format("Invalid enum: 0x{:X}", pname)));

            break;
        }
    }

    GLenum GetError() {
        auto error = MG_State::pGLContext->PopGLError();
        if (!error || !error->get()) {
            return GL_NO_ERROR;
        }
        return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code);
    }

    GLenum GetGraphicsResetStatus() {
        // MobileGL does not implement robustness reset notification, so report GL_NO_ERROR
        // ("no reset detected"). Returning the generic stub's (GLenum)1 makes dEQP read a lost
        // device after every case (gl3cTestPackages.cpp:121) and, under the default
        // --deqp-terminate-on-device-lost=enable, tear the whole CTS run down.
        return GL_NO_ERROR;
    }
} // namespace MobileGL::MG_Impl::GLImpl
